From 56016ba377efcc75c6db8dab6a073bab108848de Mon Sep 17 00:00:00 2001 From: crbelaus Date: Mon, 15 Jul 2024 18:58:45 +0200 Subject: [PATCH 01/34] Track errors last occurrence --- lib/error_tracker.ex | 2 +- lib/error_tracker/migrations/postgres/v01.ex | 1 + lib/error_tracker/schemas/error.ex | 4 +++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/error_tracker.ex b/lib/error_tracker.ex index 7f45232..c70121b 100644 --- a/lib/error_tracker.ex +++ b/lib/error_tracker.ex @@ -16,7 +16,7 @@ defmodule ErrorTracker do error = repo().insert!(error, - on_conflict: [set: [status: :unresolved]], + on_conflict: [set: [status: :unresolved, last_occurrence_at: DateTime.utc_now()]], conflict_target: :fingerprint, prefix: prefix() ) diff --git a/lib/error_tracker/migrations/postgres/v01.ex b/lib/error_tracker/migrations/postgres/v01.ex index 46854d1..056bf1f 100644 --- a/lib/error_tracker/migrations/postgres/v01.ex +++ b/lib/error_tracker/migrations/postgres/v01.ex @@ -13,6 +13,7 @@ defmodule ErrorTracker.Migrations.Postgres.V01 do add :source_function, :text, null: false add :status, :string, null: false add :fingerprint, :string, null: false + add :last_occurrence_at, :utc_datetime_usec, null: false timestamps(type: :utc_datetime_usec) end diff --git a/lib/error_tracker/schemas/error.ex b/lib/error_tracker/schemas/error.ex index 828ee53..226a672 100644 --- a/lib/error_tracker/schemas/error.ex +++ b/lib/error_tracker/schemas/error.ex @@ -15,6 +15,7 @@ defmodule ErrorTracker.Error do field :source_function, :string field :status, Ecto.Enum, values: [:resolved, :unresolved], default: :unresolved field :fingerprint, :binary + field :last_occurrence_at, :utc_datetime_usec has_many :occurrences, ErrorTracker.Occurrence @@ -40,7 +41,8 @@ defmodule ErrorTracker.Error do kind: to_string(kind), reason: reason, source_line: "#{source.file}:#{source.line}", - source_function: "#{source.module}.#{source.function}/#{source.arity}" + source_function: "#{source.module}.#{source.function}/#{source.arity}", + last_occurrence_at: DateTime.utc_now() ] fingerprint = :crypto.hash(:sha256, params |> Keyword.values() |> Enum.join()) From 8eb7ee26122e3fee6f8f3e1daf755bd12bc14df3 Mon Sep 17 00:00:00 2001 From: crbelaus Date: Mon, 15 Jul 2024 18:49:15 +0200 Subject: [PATCH 02/34] WIP: error dashboard index --- lib/error_tracker.ex | 16 +++- lib/error_tracker/web/live/dashboard.ex | 74 ++++++++++++++++++- .../web/live/dashboard.html.heex | 58 +++++++++++++-- 3 files changed, 136 insertions(+), 12 deletions(-) diff --git a/lib/error_tracker.ex b/lib/error_tracker.ex index c70121b..ad20062 100644 --- a/lib/error_tracker.ex +++ b/lib/error_tracker.ex @@ -8,9 +8,11 @@ defmodule ErrorTracker do """ @type context :: %{String.t() => any()} + alias ErrorTracker.Error + def report(exception, stacktrace, given_context \\ %{}) do {:ok, stacktrace} = ErrorTracker.Stacktrace.new(stacktrace) - {:ok, error} = ErrorTracker.Error.new(exception, stacktrace) + {:ok, error} = Error.new(exception, stacktrace) context = Map.merge(get_context(), given_context) @@ -26,6 +28,18 @@ defmodule ErrorTracker do |> repo().insert!(prefix: prefix()) end + def resolve(error = %Error{status: :unresolved}) do + changeset = Ecto.Changeset.change(error, status: :resolved) + + repo().update(changeset, prefix: prefix()) + end + + def unresolve(error = %Error{status: :resolved}) do + changeset = Ecto.Changeset.change(error, status: :unresolved) + + repo().update(changeset, prefix: prefix()) + end + def repo do Application.fetch_env!(:error_tracker, :repo) end diff --git a/lib/error_tracker/web/live/dashboard.ex b/lib/error_tracker/web/live/dashboard.ex index caf12e6..6818f87 100644 --- a/lib/error_tracker/web/live/dashboard.ex +++ b/lib/error_tracker/web/live/dashboard.ex @@ -3,9 +3,16 @@ defmodule ErrorTracker.Web.Live.Dashboard do use ErrorTracker.Web, :live_view + import Ecto.Query + + alias ErrorTracker.Error + @impl Phoenix.LiveView def mount(_params, _session, socket) do - {:ok, assign(socket, :counter, 0)} + {:ok, + socket + |> assign(page: 1, per_page: 5) + |> paginate_errors(1)} end @impl Phoenix.LiveView @@ -14,7 +21,68 @@ defmodule ErrorTracker.Web.Live.Dashboard do end @impl Phoenix.LiveView - def handle_event("increment", _params, socket) do - {:noreply, assign(socket, :counter, socket.assigns.counter + 1)} + def handle_event("next-page", _params, socket) do + {:noreply, paginate_errors(socket, socket.assigns.page + 1)} + end + + @impl Phoenix.LiveView + def handle_event("prev-page", %{"_overran" => true}, socket) do + {:noreply, paginate_errors(socket, 1)} + end + + @impl Phoenix.LiveView + def handle_event("prev-page", _params, socket) do + if socket.assigns.page > 1 do + {:noreply, paginate_errors(socket, socket.assigns.page - 1)} + else + {:noreply, socket} + end + end + + @impl Phoenix.LiveView + def handle_event("resolve", %{"error_id" => id}, socket) do + error = ErrorTracker.repo().get(Error, id, prefix: ErrorTracker.prefix()) + {:ok, resolved} = ErrorTracker.resolve(error) + + {:noreply, stream_insert(socket, :errors, resolved)} + end + + @impl Phoenix.LiveView + def handle_event("unresolve", %{"error_id" => id}, socket) do + error = ErrorTracker.repo().get(Error, id, prefix: ErrorTracker.prefix()) + {:ok, unresolved} = ErrorTracker.unresolve(error) + + {:noreply, stream_insert(socket, :errors, unresolved)} + end + + defp paginate_errors(socket, new_page) when new_page >= 1 do + %{per_page: per_page, page: cur_page} = socket.assigns + + errors = + ErrorTracker.repo().all( + from(Error, + order_by: [desc: :last_occurrence_at], + offset: (^new_page - 1) * ^per_page, + limit: ^per_page + ), + prefix: ErrorTracker.prefix() + ) + + {errors, at, limit} = + if new_page >= cur_page do + {errors, -1, per_page * 3 * -1} + else + {Enum.reverse(errors), 0, per_page * 3} + end + + case errors do + [] -> + assign(socket, end_of_errors?: at == -1) + + [_ | _] -> + socket + |> assign(end_of_errors?: false, page: new_page) + |> stream(:errors, errors, at: at, limit: limit) + end end end diff --git a/lib/error_tracker/web/live/dashboard.html.heex b/lib/error_tracker/web/live/dashboard.html.heex index 07f5708..eabc7f3 100644 --- a/lib/error_tracker/web/live/dashboard.html.heex +++ b/lib/error_tracker/web/live/dashboard.html.heex @@ -1,8 +1,50 @@ -

Hello world!

-

Number of presses: <%= @counter %>

- + + + + + + + + + + + 1, do: "prev-page"} + phx-viewport-bottom={if !@end_of_errors?, do: "next-page"} + phx-page-loading + > + + + + + + + + +
ErrorSourceLastStatus
<%= String.slice(error.reason, 0..100) %> + <%= error.source_line %> +
+ <%= error.source_function %> +
+ <%= error.last_occurrence_at %> + + <%= error.status %> + + + + +
From ae89054d1e76bab26c46684e3644140f3a2f89db Mon Sep 17 00:00:00 2001 From: crbelaus Date: Wed, 17 Jul 2024 19:24:50 +0200 Subject: [PATCH 03/34] Filters and pagination --- lib/error_tracker/web/live/dashboard.ex | 117 ++++++++++-------- .../web/live/dashboard.html.heex | 51 ++++++-- mix.exs | 1 + mix.lock | 1 + 4 files changed, 113 insertions(+), 57 deletions(-) diff --git a/lib/error_tracker/web/live/dashboard.ex b/lib/error_tracker/web/live/dashboard.ex index 6818f87..86239d3 100644 --- a/lib/error_tracker/web/live/dashboard.ex +++ b/lib/error_tracker/web/live/dashboard.ex @@ -7,82 +7,101 @@ defmodule ErrorTracker.Web.Live.Dashboard do alias ErrorTracker.Error + @per_page 10 + @impl Phoenix.LiveView - def mount(_params, _session, socket) do - {:ok, - socket - |> assign(page: 1, per_page: 5) - |> paginate_errors(1)} + def mount(params, _session, socket) do + {_search, search_form} = search_terms(params) + + {:ok, assign(socket, page: 1, total_pages: 1, search_form: search_form, errors: [])} end @impl Phoenix.LiveView - def handle_params(_params, _uri, socket) do - {:noreply, socket} + def handle_params(params, uri, socket) do + {search, search_form} = search_terms(params) + + path = struct(URI, uri |> URI.parse() |> Map.take([:path, :query])) + + {:noreply, + socket + |> assign(path: path, search: search, page: 1, search_form: search_form) + |> paginate_errors()} end @impl Phoenix.LiveView - def handle_event("next-page", _params, socket) do - {:noreply, paginate_errors(socket, socket.assigns.page + 1)} + def handle_event("search", params, socket) do + {search, _search_form} = search_terms(params["search"] || %{}) + + path_w_filters = %URI{socket.assigns.path | query: URI.encode_query(search)} + + {:noreply, push_patch(socket, to: URI.to_string(path_w_filters))} end @impl Phoenix.LiveView - def handle_event("prev-page", %{"_overran" => true}, socket) do - {:noreply, paginate_errors(socket, 1)} + def handle_event("next-page", _params, socket) do + {:noreply, socket |> assign(page: socket.assigns.page + 1) |> paginate_errors()} end @impl Phoenix.LiveView def handle_event("prev-page", _params, socket) do - if socket.assigns.page > 1 do - {:noreply, paginate_errors(socket, socket.assigns.page - 1)} - else - {:noreply, socket} - end + {:noreply, socket |> assign(page: socket.assigns.page - 1) |> paginate_errors()} end @impl Phoenix.LiveView def handle_event("resolve", %{"error_id" => id}, socket) do error = ErrorTracker.repo().get(Error, id, prefix: ErrorTracker.prefix()) - {:ok, resolved} = ErrorTracker.resolve(error) + {:ok, _resolved} = ErrorTracker.resolve(error) - {:noreply, stream_insert(socket, :errors, resolved)} + {:noreply, paginate_errors(socket)} end @impl Phoenix.LiveView def handle_event("unresolve", %{"error_id" => id}, socket) do error = ErrorTracker.repo().get(Error, id, prefix: ErrorTracker.prefix()) - {:ok, unresolved} = ErrorTracker.unresolve(error) + {:ok, _unresolved} = ErrorTracker.unresolve(error) + + {:noreply, paginate_errors(socket)} + end + + defp paginate_errors(socket) do + %{page: page, search: search} = socket.assigns + repo = ErrorTracker.repo() + prefix = ErrorTracker.prefix() + + query = filter(Error, search) + + total_errors = repo.aggregate(query, :count, prefix: prefix) + + errors_query = + query + |> order_by(desc: :last_occurrence_at) + |> offset((^page - 1) * @per_page) + |> limit(@per_page) + + assign(socket, + errors: repo.all(errors_query, prefix: prefix), + total_pages: (total_errors / @per_page) |> Float.ceil() |> trunc + ) + end + + defp search_terms(params) do + data = %{} + types = %{reason: :string, source_line: :string, source_function: :string, status: :string} + + changeset = Ecto.Changeset.cast({data, types}, params, Map.keys(types)) + + {Ecto.Changeset.apply_changes(changeset), to_form(changeset, as: :search)} + end + + defp filter(query, search) do + Enum.reduce(search, query, &do_filter/2) + end - {:noreply, stream_insert(socket, :errors, unresolved)} + defp do_filter({:status, status}, query) do + where(query, [error], error.status == ^status) end - defp paginate_errors(socket, new_page) when new_page >= 1 do - %{per_page: per_page, page: cur_page} = socket.assigns - - errors = - ErrorTracker.repo().all( - from(Error, - order_by: [desc: :last_occurrence_at], - offset: (^new_page - 1) * ^per_page, - limit: ^per_page - ), - prefix: ErrorTracker.prefix() - ) - - {errors, at, limit} = - if new_page >= cur_page do - {errors, -1, per_page * 3 * -1} - else - {Enum.reverse(errors), 0, per_page * 3} - end - - case errors do - [] -> - assign(socket, end_of_errors?: at == -1) - - [_ | _] -> - socket - |> assign(end_of_errors?: false, page: new_page) - |> stream(:errors, errors, at: at, limit: limit) - end + defp do_filter({field, value}, query) do + where(query, [error], ilike(field(error, ^field), ^"%#{value}%")) end end diff --git a/lib/error_tracker/web/live/dashboard.html.heex b/lib/error_tracker/web/live/dashboard.html.heex index eabc7f3..41dfb69 100644 --- a/lib/error_tracker/web/live/dashboard.html.heex +++ b/lib/error_tracker/web/live/dashboard.html.heex @@ -1,3 +1,36 @@ +<.form for={@search_form} id="search" phx-change="search"> + + + + + + @@ -8,14 +41,8 @@ - 1, do: "prev-page"} - phx-viewport-bottom={if !@end_of_errors?, do: "next-page"} - phx-page-loading - > - + +
<%= String.slice(error.reason, 0..100) %> <%= error.source_line %> @@ -48,3 +75,11 @@
+ + + + diff --git a/mix.exs b/mix.exs index 38a3fe7..f40688d 100644 --- a/mix.exs +++ b/mix.exs @@ -47,6 +47,7 @@ defmodule ErrorTracker.MixProject do {:ecto, "~> 3.11"}, {:jason, "~> 1.1"}, {:phoenix_live_view, "~> 0.19 or ~> 1.0"}, + {:phoenix_ecto, "~> 4.6"}, {:plug, "~> 1.10"}, {:postgrex, ">= 0.0.0"}, # Dev dependencies diff --git a/mix.lock b/mix.lock index 0f48b67..61237f0 100644 --- a/mix.lock +++ b/mix.lock @@ -20,6 +20,7 @@ "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, "phoenix": {:hex, :phoenix, "1.7.12", "1cc589e0eab99f593a8aa38ec45f15d25297dd6187ee801c8de8947090b5a9d3", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "d646192fbade9f485b01bc9920c139bfdd19d0f8df3d73fd8eaf2dfbe0d2837c"}, + "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.2", "3b83b24ab5a2eb071a20372f740d7118767c272db386831b2e77638c4dcc606d", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "3f94d025f59de86be00f5f8c5dd7b5965a3298458d21ab1c328488be3b5fcd59"}, "phoenix_html": {:hex, :phoenix_html, "4.1.1", "4c064fd3873d12ebb1388425a8f2a19348cef56e7289e1998e2d2fa758aa982e", [:mix], [], "hexpm", "f2f2df5a72bc9a2f510b21497fd7d2b86d932ec0598f0210fed4114adc546c6f"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.5.3", "f2161c207fda0e4fb55165f650f7f8db23f02b29e3bff00ff7ef161d6ac1f09d", [:mix], [{:file_system, "~> 0.3 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b4ec9cd73cb01ff1bd1cac92e045d13e7030330b74164297d1aee3907b54803c"}, "phoenix_live_view": {:hex, :phoenix_live_view, "0.20.14", "70fa101aa0539e81bed4238777498f6215e9dda3461bdaa067cad6908110c364", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "82f6d006c5264f979ed5eb75593d808bbe39020f20df2e78426f4f2d570e2402"}, From b01a326ae9c0a32f4ff61377f8c8d8dab982bbc8 Mon Sep 17 00:00:00 2001 From: crbelaus Date: Thu, 18 Jul 2024 17:10:32 +0200 Subject: [PATCH 04/34] Remove unused callback --- lib/error_tracker/web/live/dashboard.ex | 7 ------- 1 file changed, 7 deletions(-) diff --git a/lib/error_tracker/web/live/dashboard.ex b/lib/error_tracker/web/live/dashboard.ex index 86239d3..8cfc4b0 100644 --- a/lib/error_tracker/web/live/dashboard.ex +++ b/lib/error_tracker/web/live/dashboard.ex @@ -9,13 +9,6 @@ defmodule ErrorTracker.Web.Live.Dashboard do @per_page 10 - @impl Phoenix.LiveView - def mount(params, _session, socket) do - {_search, search_form} = search_terms(params) - - {:ok, assign(socket, page: 1, total_pages: 1, search_form: search_form, errors: [])} - end - @impl Phoenix.LiveView def handle_params(params, uri, socket) do {search, search_form} = search_terms(params) From adaecd299e7454472af6a9ba817cd3baa5e04ede Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93scar=20de=20Arriba?= Date: Thu, 18 Jul 2024 17:27:32 +0200 Subject: [PATCH 05/34] Layout changes --- lib/error_tracker/web/components/layouts.ex | 2 + .../web/components/layouts/live.html.heex | 3 +- .../web/components/layouts/navbar.ex | 63 +++++++++++++++++++ .../web/components/layouts/root.html.heex | 2 +- 4 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 lib/error_tracker/web/components/layouts/navbar.ex diff --git a/lib/error_tracker/web/components/layouts.ex b/lib/error_tracker/web/components/layouts.ex index 33ecade..292429f 100644 --- a/lib/error_tracker/web/components/layouts.ex +++ b/lib/error_tracker/web/components/layouts.ex @@ -1,6 +1,8 @@ defmodule ErrorTracker.Web.Layouts do use ErrorTracker.Web, :html + alias ErrorTracker.Web.Layouts.Navbar + @css_path :code.priv_dir(:error_tracker) |> Path.join("static/app.css") @js_path :code.priv_dir(:error_tracker) |> Path.join("static/app.js") diff --git a/lib/error_tracker/web/components/layouts/live.html.heex b/lib/error_tracker/web/components/layouts/live.html.heex index c753bc6..959d8b7 100644 --- a/lib/error_tracker/web/components/layouts/live.html.heex +++ b/lib/error_tracker/web/components/layouts/live.html.heex @@ -1,3 +1,4 @@ -
+<.live_component module={Navbar} id="navbar" /> +
<%= @inner_content %>
diff --git a/lib/error_tracker/web/components/layouts/navbar.ex b/lib/error_tracker/web/components/layouts/navbar.ex new file mode 100644 index 0000000..64abaa9 --- /dev/null +++ b/lib/error_tracker/web/components/layouts/navbar.ex @@ -0,0 +1,63 @@ +defmodule ErrorTracker.Web.Layouts.Navbar do + use ErrorTracker.Web, :live_component + + def render(assigns) do + ~H""" + + """ + end + + attr :to, :string, required: true + attr :rest, :global + + slot :inner_block, required: true + + def navbar_item(assigns) do + ~H""" +
  • + + <%= render_slot(@inner_block) %> + +
  • + """ + end +end diff --git a/lib/error_tracker/web/components/layouts/root.html.heex b/lib/error_tracker/web/components/layouts/root.html.heex index a802f2e..439fc12 100644 --- a/lib/error_tracker/web/components/layouts/root.html.heex +++ b/lib/error_tracker/web/components/layouts/root.html.heex @@ -18,7 +18,7 @@ - + <%= @inner_content %> From 76cd0a0041a76a8533cde988a148c9553c355e0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93scar=20de=20Arriba?= Date: Thu, 18 Jul 2024 18:11:27 +0200 Subject: [PATCH 06/34] WIP dashboard and components --- lib/error_tracker/web.ex | 2 + .../web/components/core_components.ex | 70 +++++++++++++ .../web/live/dashboard.html.heex | 97 ++++++++++--------- 3 files changed, 124 insertions(+), 45 deletions(-) create mode 100644 lib/error_tracker/web/components/core_components.ex diff --git a/lib/error_tracker/web.ex b/lib/error_tracker/web.ex index f866fb5..46725a3 100644 --- a/lib/error_tracker/web.ex +++ b/lib/error_tracker/web.ex @@ -73,6 +73,8 @@ defmodule ErrorTracker.Web do import Phoenix.HTML import Phoenix.LiveView.Helpers + import ErrorTracker.Web.CoreComponents + alias Phoenix.LiveView.JS end end diff --git a/lib/error_tracker/web/components/core_components.ex b/lib/error_tracker/web/components/core_components.ex new file mode 100644 index 0000000..413b9d6 --- /dev/null +++ b/lib/error_tracker/web/components/core_components.ex @@ -0,0 +1,70 @@ +defmodule ErrorTracker.Web.CoreComponents do + use Phoenix.Component + + @doc """ + Renders a button. + + ## Examples + + <.button>Send! + <.button phx-click="go" class="ml-2">Send! + """ + attr :type, :string, default: nil + attr :class, :string, default: nil + attr :rest, :global, include: ~w(disabled form name value) + + slot :inner_block, required: true + + def button(assigns) do + ~H""" + + """ + end + + @doc """ + Renders a badge. + + ## Examples + + <.badge>Info + <.badge color={:red}>Error + """ + attr :color, :atom, default: :blue + attr :rest, :global + + slot :inner_block, required: true + + def badge(assigns) do + color_class = + case assigns.color do + :blue -> "bg-blue-900 text-blue-300" + :gray -> "bg-gray-700 text-gray-300" + :red -> "bg-red-900 text-red-300" + :green -> "bg-green-900 text-green-300" + :yellow -> "bg-yellow-900 text-yellow-300" + :indigo -> "bg-indigo-900 text-indigo-300" + :purple -> "bg-purple-900 text-purple-300" + :pink -> "bg-pink-900 text-pink-300" + :gray -> "bg-gray-700 text-gray-300" + :gray -> "bg-gray-700 text-gray-300" + end + + assigns = Map.put(assigns, :color_class, color_class) + + ~H""" + + <%= render_slot(@inner_block) %> + + """ + end +end diff --git a/lib/error_tracker/web/live/dashboard.html.heex b/lib/error_tracker/web/live/dashboard.html.heex index 41dfb69..46bd8b3 100644 --- a/lib/error_tracker/web/live/dashboard.html.heex +++ b/lib/error_tracker/web/live/dashboard.html.heex @@ -1,4 +1,4 @@ -<.form for={@search_form} id="search" phx-change="search"> +<.form for={@search_form} id="search" class="mb-4 text-black" phx-change="search"> - + @@ -31,50 +31,57 @@ - - - - - - - - - - - - - - - - - + + +
    ErrorSourceLastStatus
    <%= String.slice(error.reason, 0..100) %> - <%= error.source_line %> -
    - <%= error.source_function %> -
    - <%= error.last_occurrence_at %> - - <%= error.status %> - -
    + + + + """ + end end diff --git a/lib/error_tracker/web/live/dashboard.html.heex b/lib/error_tracker/web/live/dashboard.html.heex index 46bd8b3..37585be 100644 --- a/lib/error_tracker/web/live/dashboard.html.heex +++ b/lib/error_tracker/web/live/dashboard.html.heex @@ -83,10 +83,4 @@ - - - +<.pagination page={@page} total_pages={@total_pages} /> From af418f955833196e30712a2b53bf78c19b7ce2fc Mon Sep 17 00:00:00 2001 From: crbelaus Date: Thu, 18 Jul 2024 18:38:43 +0200 Subject: [PATCH 08/34] Navigate to error detail --- .../web/live/dashboard.html.heex | 4 +++- lib/error_tracker/web/live/show.ex | 16 +++++++++++++++ lib/error_tracker/web/live/show.html.heex | 20 +++++++++++++++++++ lib/error_tracker/web/router.ex | 1 + 4 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 lib/error_tracker/web/live/show.ex create mode 100644 lib/error_tracker/web/live/show.html.heex diff --git a/lib/error_tracker/web/live/dashboard.html.heex b/lib/error_tracker/web/live/dashboard.html.heex index 37585be..df714fc 100644 --- a/lib/error_tracker/web/live/dashboard.html.heex +++ b/lib/error_tracker/web/live/dashboard.html.heex @@ -47,7 +47,9 @@ scope="row" class="px-4 py-4 font-medium text-white whitespace-nowrap text-ellipsis overflow-hidden" > - (<%= error.kind %>) <%= error.reason %> + <.link navigate={"/errors/#{error.id}"}> + (<%= error.kind %>) <%= error.reason %> +

    <%= error.source_function %>
    diff --git a/lib/error_tracker/web/live/show.ex b/lib/error_tracker/web/live/show.ex new file mode 100644 index 0000000..f01f5c4 --- /dev/null +++ b/lib/error_tracker/web/live/show.ex @@ -0,0 +1,16 @@ +defmodule ErrorTracker.Web.Live.Show do + @moduledoc false + + use ErrorTracker.Web, :live_view + + alias ErrorTracker.Error + + def mount(params, _session, socket) do + error = ErrorTracker.repo().get!(Error, params["id"], prefix: ErrorTracker.prefix()) + error = ErrorTracker.repo().preload(error, [:occurrences], prefix: ErrorTracker.prefix()) + + dbg(error) + + {:ok, assign(socket, error: error)} + end +end diff --git a/lib/error_tracker/web/live/show.html.heex b/lib/error_tracker/web/live/show.html.heex new file mode 100644 index 0000000..389cf27 --- /dev/null +++ b/lib/error_tracker/web/live/show.html.heex @@ -0,0 +1,20 @@ +<.link navigate="/errors">Back to the dashboard + +

    <%= @error.reason |> String.split("\n", trim: true) |> List.first() %>

    + +

    Full message

    +
    +  <%= @error.reason %>
    +
    + +

    Source

    + +
    +  <%= @error.source_line %>
    +
    +
    +  <%= @error.source_function %>
    +
    + +

    Last occurrence

    +<%= @error.last_occurrence_at %> diff --git a/lib/error_tracker/web/router.ex b/lib/error_tracker/web/router.ex index 52136df..31952d0 100644 --- a/lib/error_tracker/web/router.ex +++ b/lib/error_tracker/web/router.ex @@ -15,6 +15,7 @@ defmodule ErrorTracker.Web.Router do live_session unquote(session_name), unquote(session_opts) do live "/", ErrorTracker.Web.Live.Dashboard, :index, as: unquote(session_name) + live "/:id", ErrorTracker.Web.Live.Show, :show, as: unquote(session_name) end end end From 8c9a63833b09781da61e2ace9a413de064a2f294 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93scar=20de=20Arriba?= Date: Sat, 20 Jul 2024 11:59:24 +0200 Subject: [PATCH 09/34] Load occurrences list --- lib/error_tracker/web/live/show.ex | 62 ++++++++++++++++++++--- lib/error_tracker/web/live/show.html.heex | 6 +++ 2 files changed, 62 insertions(+), 6 deletions(-) diff --git a/lib/error_tracker/web/live/show.ex b/lib/error_tracker/web/live/show.ex index f01f5c4..27456db 100644 --- a/lib/error_tracker/web/live/show.ex +++ b/lib/error_tracker/web/live/show.ex @@ -1,16 +1,66 @@ defmodule ErrorTracker.Web.Live.Show do @moduledoc false - use ErrorTracker.Web, :live_view - alias ErrorTracker.Error + import Ecto.Query - def mount(params, _session, socket) do - error = ErrorTracker.repo().get!(Error, params["id"], prefix: ErrorTracker.prefix()) - error = ErrorTracker.repo().preload(error, [:occurrences], prefix: ErrorTracker.prefix()) + alias ErrorTracker.Error + alias ErrorTracker.Occurrence - dbg(error) + @occurreneces_to_navigate 50 + def mount(%{"id" => id}, _session, socket) do + error = ErrorTracker.repo().get!(Error, id, prefix: ErrorTracker.prefix()) {:ok, assign(socket, error: error)} end + + def handle_params(%{"occurence_id" => occurrence_id}, _uri, socket) do + base_query = Ecto.assoc(socket.assigns.error, :occurrences) + + occurrence = + ErrorTracker.repo().get!(base_query, occurrence_id, prefix: ErrorTracker.prefix()) + + previous_occurrences = + base_query + |> where([o], o.id < ^occurrence.id) + |> related_occurrences(@occurreneces_to_navigate / 2) + + limit_next_occurrences = @occurreneces_to_navigate - length(previous_occurrences) - 1 + + next_occurrences = + base_query + |> where([o], o.id > ^occurrence.id) + |> related_occurrences(limit_next_occurrences) + + socket = + socket + |> assign(:occurrences, previous_occurrences ++ occurrence ++ next_occurrences) + |> assign(:occurrence, occurrence) + + {:noreply, socket} + end + + def handle_params(_, _uri, socket) do + base_query = Ecto.assoc(socket.assigns.error, :occurrences) + + occurrences = related_occurrences(base_query) + + occurrence = + ErrorTracker.repo().get!(base_query, hd(occurrences).id, prefix: ErrorTracker.prefix()) + + socket = + socket + |> assign(:occurrences, occurrences) + |> assign(:occurrence, occurrence) + + {:noreply, socket} + end + + defp related_occurrences(query, num_results \\ @occurreneces_to_navigate) do + query + |> order_by([o], desc: o.id) + |> select([:id, :inserted_at]) + |> limit(^num_results) + |> ErrorTracker.repo().all(prefix: ErrorTracker.prefix()) + end end diff --git a/lib/error_tracker/web/live/show.html.heex b/lib/error_tracker/web/live/show.html.heex index 389cf27..f1f0f45 100644 --- a/lib/error_tracker/web/live/show.html.heex +++ b/lib/error_tracker/web/live/show.html.heex @@ -18,3 +18,9 @@

    Last occurrence

    <%= @error.last_occurrence_at %> + +

    Details of occurrence

    +<%= inspect(@occurrence) %> + +

    List of occurrences

    +<%= inspect(@occurrences) %> From f0f8282f9d24d20f11cc07c4445aa73ddb5b04e4 Mon Sep 17 00:00:00 2001 From: crbelaus Date: Sat, 20 Jul 2024 12:07:19 +0200 Subject: [PATCH 10/34] Don't hash last_occurrence_at --- lib/error_tracker/schemas/error.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/error_tracker/schemas/error.ex b/lib/error_tracker/schemas/error.ex index 226a672..443352a 100644 --- a/lib/error_tracker/schemas/error.ex +++ b/lib/error_tracker/schemas/error.ex @@ -41,8 +41,7 @@ defmodule ErrorTracker.Error do kind: to_string(kind), reason: reason, source_line: "#{source.file}:#{source.line}", - source_function: "#{source.module}.#{source.function}/#{source.arity}", - last_occurrence_at: DateTime.utc_now() + source_function: "#{source.module}.#{source.function}/#{source.arity}" ] fingerprint = :crypto.hash(:sha256, params |> Keyword.values() |> Enum.join()) @@ -50,6 +49,7 @@ defmodule ErrorTracker.Error do %__MODULE__{} |> Ecto.Changeset.change(params) |> Ecto.Changeset.put_change(:fingerprint, Base.encode16(fingerprint)) + |> Ecto.Changeset.put_change(:last_occurrence_at, DateTime.utc_now()) |> Ecto.Changeset.apply_action(:new) end end From 6113ddbf2ad2736a467ca1cdf26b1f2b488e2d53 Mon Sep 17 00:00:00 2001 From: crbelaus Date: Sat, 20 Jul 2024 12:25:46 +0200 Subject: [PATCH 11/34] Fix warning --- lib/error_tracker/web/components/core_components.ex | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/error_tracker/web/components/core_components.ex b/lib/error_tracker/web/components/core_components.ex index 385e7b7..c19227b 100644 --- a/lib/error_tracker/web/components/core_components.ex +++ b/lib/error_tracker/web/components/core_components.ex @@ -56,7 +56,6 @@ defmodule ErrorTracker.Web.CoreComponents do :purple -> "bg-purple-900 text-purple-300" :pink -> "bg-pink-900 text-pink-300" :gray -> "bg-gray-700 text-gray-300" - :gray -> "bg-gray-700 text-gray-300" end assigns = Map.put(assigns, :color_class, color_class) From efca1aa9c65387f766fe0c1f2b9cf8f0a3caf39d Mon Sep 17 00:00:00 2001 From: crbelaus Date: Sat, 20 Jul 2024 12:53:21 +0200 Subject: [PATCH 12/34] Wrap Ecto.Repo Closes #14 --- lib/error_tracker.ex | 20 ++++-------- lib/error_tracker/migrations.ex | 2 +- lib/error_tracker/repo.ex | 41 +++++++++++++++++++++++++ lib/error_tracker/web/live/dashboard.ex | 11 +++---- lib/error_tracker/web/live/show.ex | 14 +++------ 5 files changed, 58 insertions(+), 30 deletions(-) create mode 100644 lib/error_tracker/repo.ex diff --git a/lib/error_tracker.ex b/lib/error_tracker.ex index ad20062..5997214 100644 --- a/lib/error_tracker.ex +++ b/lib/error_tracker.ex @@ -9,6 +9,7 @@ defmodule ErrorTracker do @type context :: %{String.t() => any()} alias ErrorTracker.Error + alias ErrorTracker.Repo def report(exception, stacktrace, given_context \\ %{}) do {:ok, stacktrace} = ErrorTracker.Stacktrace.new(stacktrace) @@ -17,35 +18,26 @@ defmodule ErrorTracker do context = Map.merge(get_context(), given_context) error = - repo().insert!(error, + Repo.insert!(error, on_conflict: [set: [status: :unresolved, last_occurrence_at: DateTime.utc_now()]], - conflict_target: :fingerprint, - prefix: prefix() + conflict_target: :fingerprint ) error |> Ecto.build_assoc(:occurrences, stacktrace: stacktrace, context: context) - |> repo().insert!(prefix: prefix()) + |> Repo.insert!() end def resolve(error = %Error{status: :unresolved}) do changeset = Ecto.Changeset.change(error, status: :resolved) - repo().update(changeset, prefix: prefix()) + Repo.update(changeset) end def unresolve(error = %Error{status: :resolved}) do changeset = Ecto.Changeset.change(error, status: :unresolved) - repo().update(changeset, prefix: prefix()) - end - - def repo do - Application.fetch_env!(:error_tracker, :repo) - end - - def prefix do - Application.get_env(:error_tracker, :prefix, "public") + Repo.update(changeset) end @spec set_context(context()) :: context() diff --git a/lib/error_tracker/migrations.ex b/lib/error_tracker/migrations.ex index b61fa7d..94a4c32 100644 --- a/lib/error_tracker/migrations.ex +++ b/lib/error_tracker/migrations.ex @@ -116,7 +116,7 @@ defmodule ErrorTracker.Migration do end defp migrator do - case ErrorTracker.repo().__adapter__() do + case ErrorTracker.Repo.__adapter__() do Ecto.Adapters.Postgres -> ErrorTracker.Migrations.Postgres adapter -> raise "ErrorTracker does not support #{adapter}" end diff --git a/lib/error_tracker/repo.ex b/lib/error_tracker/repo.ex new file mode 100644 index 0000000..e8cce11 --- /dev/null +++ b/lib/error_tracker/repo.ex @@ -0,0 +1,41 @@ +defmodule ErrorTracker.Repo do + @moduledoc """ + Wraps Ecto.Repo calls and applies default options. + """ + def insert!(struct_or_changeset, opts \\ []) do + dispatch(:insert, [struct_or_changeset], opts) + end + + def update(changeset, opts \\ []) do + dispatch(:update, [changeset], opts) + end + + def get(queryable, id, opts \\ []) do + dispatch(:get, [queryable, id], opts) + end + + def get!(queryable, id, opts \\ []) do + dispatch(:get!, [queryable, id], opts) + end + + def all(queryable, opts \\ []) do + dispatch(:all, [queryable], opts) + end + + def aggregate(queryable, aggregate, opts \\ []) do + dispatch(:aggregate, [queryable, aggregate], opts) + end + + def __adapter__, do: repo().__adapter__() + + defp dispatch(action, args, opts) do + defaults = [prefix: Application.get_env(:error_tracker, :prefix, "public")] + opts_w_defaults = Keyword.merge(defaults, opts) + + apply(repo(), action, args ++ [opts_w_defaults]) + end + + defp repo do + Application.fetch_env!(:error_tracker, :repo) + end +end diff --git a/lib/error_tracker/web/live/dashboard.ex b/lib/error_tracker/web/live/dashboard.ex index 8cfc4b0..d0b06be 100644 --- a/lib/error_tracker/web/live/dashboard.ex +++ b/lib/error_tracker/web/live/dashboard.ex @@ -6,6 +6,7 @@ defmodule ErrorTracker.Web.Live.Dashboard do import Ecto.Query alias ErrorTracker.Error + alias ErrorTracker.Repo @per_page 10 @@ -42,7 +43,7 @@ defmodule ErrorTracker.Web.Live.Dashboard do @impl Phoenix.LiveView def handle_event("resolve", %{"error_id" => id}, socket) do - error = ErrorTracker.repo().get(Error, id, prefix: ErrorTracker.prefix()) + error = Repo.get(Error, id) {:ok, _resolved} = ErrorTracker.resolve(error) {:noreply, paginate_errors(socket)} @@ -50,7 +51,7 @@ defmodule ErrorTracker.Web.Live.Dashboard do @impl Phoenix.LiveView def handle_event("unresolve", %{"error_id" => id}, socket) do - error = ErrorTracker.repo().get(Error, id, prefix: ErrorTracker.prefix()) + error = Repo.get(Error, id) {:ok, _unresolved} = ErrorTracker.unresolve(error) {:noreply, paginate_errors(socket)} @@ -58,12 +59,10 @@ defmodule ErrorTracker.Web.Live.Dashboard do defp paginate_errors(socket) do %{page: page, search: search} = socket.assigns - repo = ErrorTracker.repo() - prefix = ErrorTracker.prefix() query = filter(Error, search) - total_errors = repo.aggregate(query, :count, prefix: prefix) + total_errors = Repo.aggregate(query, :count) errors_query = query @@ -72,7 +71,7 @@ defmodule ErrorTracker.Web.Live.Dashboard do |> limit(@per_page) assign(socket, - errors: repo.all(errors_query, prefix: prefix), + errors: Repo.all(errors_query), total_pages: (total_errors / @per_page) |> Float.ceil() |> trunc ) end diff --git a/lib/error_tracker/web/live/show.ex b/lib/error_tracker/web/live/show.ex index 27456db..2774b41 100644 --- a/lib/error_tracker/web/live/show.ex +++ b/lib/error_tracker/web/live/show.ex @@ -5,20 +5,18 @@ defmodule ErrorTracker.Web.Live.Show do import Ecto.Query alias ErrorTracker.Error - alias ErrorTracker.Occurrence + alias ErrorTracker.Repo @occurreneces_to_navigate 50 def mount(%{"id" => id}, _session, socket) do - error = ErrorTracker.repo().get!(Error, id, prefix: ErrorTracker.prefix()) + error = Repo.get!(Error, id) {:ok, assign(socket, error: error)} end def handle_params(%{"occurence_id" => occurrence_id}, _uri, socket) do base_query = Ecto.assoc(socket.assigns.error, :occurrences) - - occurrence = - ErrorTracker.repo().get!(base_query, occurrence_id, prefix: ErrorTracker.prefix()) + occurrence = Repo.get!(base_query, occurrence_id) previous_occurrences = base_query @@ -44,9 +42,7 @@ defmodule ErrorTracker.Web.Live.Show do base_query = Ecto.assoc(socket.assigns.error, :occurrences) occurrences = related_occurrences(base_query) - - occurrence = - ErrorTracker.repo().get!(base_query, hd(occurrences).id, prefix: ErrorTracker.prefix()) + occurrence = Repo.get!(base_query, hd(occurrences).id) socket = socket @@ -61,6 +57,6 @@ defmodule ErrorTracker.Web.Live.Show do |> order_by([o], desc: o.id) |> select([:id, :inserted_at]) |> limit(^num_results) - |> ErrorTracker.repo().all(prefix: ErrorTracker.prefix()) + |> Repo.all() end end From 1af59291d07d0c235dcf84ca7c4fd95290d1e0db Mon Sep 17 00:00:00 2001 From: crbelaus Date: Sat, 20 Jul 2024 12:57:00 +0200 Subject: [PATCH 13/34] Fix Credo --- .credo.exs | 2 +- lib/error_tracker/web/components/core_components.ex | 1 + lib/error_tracker/web/components/layouts/navbar.ex | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.credo.exs b/.credo.exs index 871a021..e130a68 100644 --- a/.credo.exs +++ b/.credo.exs @@ -121,7 +121,6 @@ # {Credo.Check.Refactor.Apply, []}, {Credo.Check.Refactor.CondStatements, []}, - {Credo.Check.Refactor.CyclomaticComplexity, []}, {Credo.Check.Refactor.FilterCount, []}, {Credo.Check.Refactor.FilterFilter, []}, {Credo.Check.Refactor.FunctionArity, []}, @@ -189,6 +188,7 @@ {Credo.Check.Readability.WithCustomTaggedTuple, []}, {Credo.Check.Refactor.ABCSize, []}, {Credo.Check.Refactor.AppendSingleItem, []}, + {Credo.Check.Refactor.CyclomaticComplexity, []}, {Credo.Check.Refactor.DoubleBooleanNegation, []}, {Credo.Check.Refactor.FilterReject, []}, {Credo.Check.Refactor.IoPuts, []}, diff --git a/lib/error_tracker/web/components/core_components.ex b/lib/error_tracker/web/components/core_components.ex index c19227b..06e5784 100644 --- a/lib/error_tracker/web/components/core_components.ex +++ b/lib/error_tracker/web/components/core_components.ex @@ -1,4 +1,5 @@ defmodule ErrorTracker.Web.CoreComponents do + @moduledoc false use Phoenix.Component @doc """ diff --git a/lib/error_tracker/web/components/layouts/navbar.ex b/lib/error_tracker/web/components/layouts/navbar.ex index 64abaa9..7063e1d 100644 --- a/lib/error_tracker/web/components/layouts/navbar.ex +++ b/lib/error_tracker/web/components/layouts/navbar.ex @@ -1,4 +1,5 @@ defmodule ErrorTracker.Web.Layouts.Navbar do + @moduledoc false use ErrorTracker.Web, :live_component def render(assigns) do From 851f78b8ec1024c6dc7cd057b6d97b4fef13470d Mon Sep 17 00:00:00 2001 From: crbelaus Date: Sat, 20 Jul 2024 12:58:44 +0200 Subject: [PATCH 14/34] Fix warnings --- lib/error_tracker/web/components/core_components.ex | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/error_tracker/web/components/core_components.ex b/lib/error_tracker/web/components/core_components.ex index 06e5784..084d72a 100644 --- a/lib/error_tracker/web/components/core_components.ex +++ b/lib/error_tracker/web/components/core_components.ex @@ -56,7 +56,6 @@ defmodule ErrorTracker.Web.CoreComponents do :indigo -> "bg-indigo-900 text-indigo-300" :purple -> "bg-purple-900 text-purple-300" :pink -> "bg-pink-900 text-pink-300" - :gray -> "bg-gray-700 text-gray-300" end assigns = Map.put(assigns, :color_class, color_class) From 47390b051bd821a3d112741f144c8de5decad376 Mon Sep 17 00:00:00 2001 From: crbelaus Date: Sat, 20 Jul 2024 13:03:05 +0200 Subject: [PATCH 15/34] Full width filters --- lib/error_tracker/web/live/dashboard.html.heex | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/error_tracker/web/live/dashboard.html.heex b/lib/error_tracker/web/live/dashboard.html.heex index df714fc..7cfe65f 100644 --- a/lib/error_tracker/web/live/dashboard.html.heex +++ b/lib/error_tracker/web/live/dashboard.html.heex @@ -1,4 +1,9 @@ -<.form for={@search_form} id="search" class="mb-4 text-black" phx-change="search"> +<.form + for={@search_form} + id="search" + class="mb-4 text-black grid grid-cols-4 gap-2" + phx-change="search" +> Date: Sat, 20 Jul 2024 13:30:52 +0200 Subject: [PATCH 16/34] fixup! Wrap Ecto.Repo --- lib/error_tracker/repo.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/error_tracker/repo.ex b/lib/error_tracker/repo.ex index e8cce11..f736052 100644 --- a/lib/error_tracker/repo.ex +++ b/lib/error_tracker/repo.ex @@ -3,7 +3,7 @@ defmodule ErrorTracker.Repo do Wraps Ecto.Repo calls and applies default options. """ def insert!(struct_or_changeset, opts \\ []) do - dispatch(:insert, [struct_or_changeset], opts) + dispatch(:insert!, [struct_or_changeset], opts) end def update(changeset, opts \\ []) do From 9e93a23ab88bd3c4e46eb922b095ffd177e96b6c Mon Sep 17 00:00:00 2001 From: crbelaus Date: Sat, 20 Jul 2024 13:31:05 +0200 Subject: [PATCH 17/34] WIP: styles --- .../web/live/dashboard.html.heex | 2 +- lib/error_tracker/web/live/show.ex | 8 +-- lib/error_tracker/web/live/show.html.heex | 57 ++++++++++++------- 3 files changed, 43 insertions(+), 24 deletions(-) diff --git a/lib/error_tracker/web/live/dashboard.html.heex b/lib/error_tracker/web/live/dashboard.html.heex index 7cfe65f..cf1fe59 100644 --- a/lib/error_tracker/web/live/dashboard.html.heex +++ b/lib/error_tracker/web/live/dashboard.html.heex @@ -62,7 +62,7 @@

    - <%= error.last_occurrence_at %> + <%= Calendar.strftime(error.last_occurrence_at, "%c") %> <.badge :if={error.status == :resolved} color={:green}>Resolved diff --git a/lib/error_tracker/web/live/show.ex b/lib/error_tracker/web/live/show.ex index 2774b41..7b78e0e 100644 --- a/lib/error_tracker/web/live/show.ex +++ b/lib/error_tracker/web/live/show.ex @@ -14,16 +14,16 @@ defmodule ErrorTracker.Web.Live.Show do {:ok, assign(socket, error: error)} end - def handle_params(%{"occurence_id" => occurrence_id}, _uri, socket) do + def handle_params(%{"occurrence_id" => occurrence_id}, _uri, socket) do base_query = Ecto.assoc(socket.assigns.error, :occurrences) occurrence = Repo.get!(base_query, occurrence_id) previous_occurrences = base_query |> where([o], o.id < ^occurrence.id) - |> related_occurrences(@occurreneces_to_navigate / 2) + |> related_occurrences(round(@occurreneces_to_navigate / 2)) - limit_next_occurrences = @occurreneces_to_navigate - length(previous_occurrences) - 1 + limit_next_occurrences = dbg(@occurreneces_to_navigate - length(previous_occurrences) - 1) next_occurrences = base_query @@ -32,7 +32,7 @@ defmodule ErrorTracker.Web.Live.Show do socket = socket - |> assign(:occurrences, previous_occurrences ++ occurrence ++ next_occurrences) + |> assign(:occurrences, previous_occurrences ++ [occurrence] ++ next_occurrences) |> assign(:occurrence, occurrence) {:noreply, socket} diff --git a/lib/error_tracker/web/live/show.html.heex b/lib/error_tracker/web/live/show.html.heex index f1f0f45..f08fe1b 100644 --- a/lib/error_tracker/web/live/show.html.heex +++ b/lib/error_tracker/web/live/show.html.heex @@ -1,26 +1,45 @@ -<.link navigate="/errors">Back to the dashboard +
    + <.link navigate="/errors">Back to the dashboard +
    -

    <%= @error.reason |> String.split("\n", trim: true) |> List.first() %>

    +
    +
    +

    <%= @error.reason |> String.split("\n", trim: true) |> List.first() %>

    -

    Full message

    -
    -  <%= @error.reason %>
    -
    +
    +

    Full message

    +
    +      <%= @error.reason %>
    +    
    +
    -

    Source

    +
    +

    Source

    -
    -  <%= @error.source_line %>
    -
    -
    -  <%= @error.source_function %>
    -
    +
    +      <%= @error.source_line %>
    +      <%= @error.source_function %>
    +    
    +
    -

    Last occurrence

    -<%= @error.last_occurrence_at %> +

    Last occurrence

    + <%= Calendar.strftime(@error.last_occurrence_at, "%c") %> -

    Details of occurrence

    -<%= inspect(@occurrence) %> +

    Details of occurrence

    + <%= inspect(@occurrence) %> +
    -

    List of occurrences

    -<%= inspect(@occurrences) %> +
    +

    List of occurrences

    + +
    + <.link + :for={occurrence <- @occurrences} + class={if occurrence.id == @occurrence.id, do: "font-bold"} + patch={"/errors/#{@error.id}?occurrence_id=#{occurrence.id}"} + > + <%= Calendar.strftime(occurrence.inserted_at, "%c") %> + +
    +
    +
    From 3afac95d4f145b6c9c817c0368889fae1cc677b4 Mon Sep 17 00:00:00 2001 From: crbelaus Date: Sun, 21 Jul 2024 11:41:19 +0200 Subject: [PATCH 18/34] Show occurrence stacktrace and context --- lib/error_tracker/web/live/show.html.heex | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/error_tracker/web/live/show.html.heex b/lib/error_tracker/web/live/show.html.heex index f08fe1b..c72332e 100644 --- a/lib/error_tracker/web/live/show.html.heex +++ b/lib/error_tracker/web/live/show.html.heex @@ -25,8 +25,17 @@

    Last occurrence

    <%= Calendar.strftime(@error.last_occurrence_at, "%c") %> -

    Details of occurrence

    - <%= inspect(@occurrence) %> +

    Stacktrace

    + +
    +      <%= to_string(@occurrence.stacktrace) %>
    +    
    + +

    Context

    + +
    +      <%= Jason.encode!(@occurrence.context, pretty: true) %>
    +    
    From 552c65529bf75948c347e05b84a8b31874795381 Mon Sep 17 00:00:00 2001 From: crbelaus Date: Sun, 21 Jul 2024 11:56:48 +0200 Subject: [PATCH 19/34] Show only app frames in stacktrace --- lib/error_tracker/web/live/show.ex | 2 +- lib/error_tracker/web/live/show.html.heex | 26 ++++++++++++++++++++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/lib/error_tracker/web/live/show.ex b/lib/error_tracker/web/live/show.ex index 7b78e0e..5c246cf 100644 --- a/lib/error_tracker/web/live/show.ex +++ b/lib/error_tracker/web/live/show.ex @@ -11,7 +11,7 @@ defmodule ErrorTracker.Web.Live.Show do def mount(%{"id" => id}, _session, socket) do error = Repo.get!(Error, id) - {:ok, assign(socket, error: error)} + {:ok, assign(socket, error: error, app: Application.fetch_env!(:error_tracker, :application))} end def handle_params(%{"occurrence_id" => occurrence_id}, _uri, socket) do diff --git a/lib/error_tracker/web/live/show.html.heex b/lib/error_tracker/web/live/show.html.heex index c72332e..be612df 100644 --- a/lib/error_tracker/web/live/show.html.heex +++ b/lib/error_tracker/web/live/show.html.heex @@ -27,9 +27,29 @@

    Stacktrace

    -
    -      <%= to_string(@occurrence.stacktrace) %>
    -    
    + + + + + + + + + + + + + + + + + + +
    AppFunctionSource
    <%= line.application || @app %><%= "#{line.module}.#{line.function}/#{line.arity}" %><%= "#{line.file}.#{line.line}" %>

    Context

    From f9425dc0368481caf383ce20503203c056407703 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93scar=20de=20Arriba?= Date: Sun, 21 Jul 2024 12:08:01 +0200 Subject: [PATCH 20/34] Add button as links --- .../web/components/core_components.ex | 17 ++++++++++++++++- lib/error_tracker/web/live/show.html.heex | 2 +- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/error_tracker/web/components/core_components.ex b/lib/error_tracker/web/components/core_components.ex index 084d72a..7ca85a1 100644 --- a/lib/error_tracker/web/components/core_components.ex +++ b/lib/error_tracker/web/components/core_components.ex @@ -12,10 +12,25 @@ defmodule ErrorTracker.Web.CoreComponents do """ attr :type, :string, default: nil attr :class, :string, default: nil - attr :rest, :global, include: ~w(disabled form name value) + attr :rest, :global, include: ~w(disabled form name value navigate) slot :inner_block, required: true + def button(%{type: "link"} = assigns) do + ~H""" + + <%= render_slot(@inner_block) %> + + """ + end + def button(assigns) do ~H"""
    From 645faf665aef73c5a1534daa49d891278dc05b35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93scar=20de=20Arriba?= Date: Sun, 21 Jul 2024 12:35:41 +0200 Subject: [PATCH 21/34] Set configured dashboard path on the assigns --- lib/error_tracker/web/hooks/set_assigns.ex | 10 ++++++++++ lib/error_tracker/web/router.ex | 11 ++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 lib/error_tracker/web/hooks/set_assigns.ex diff --git a/lib/error_tracker/web/hooks/set_assigns.ex b/lib/error_tracker/web/hooks/set_assigns.ex new file mode 100644 index 0000000..653aab0 --- /dev/null +++ b/lib/error_tracker/web/hooks/set_assigns.ex @@ -0,0 +1,10 @@ +defmodule ErrorTracker.Web.Hooks.SetAssigns do + @moduledoc """ + Mounting hooks to set environment configuration on the socket. + """ + import Phoenix.Component + + def on_mount({:set_dashboard_path, path}, _params, _session, socket) do + {:cont, assign(socket, :dashboard_path, path)} + end +end diff --git a/lib/error_tracker/web/router.ex b/lib/error_tracker/web/router.ex index 31952d0..8254b34 100644 --- a/lib/error_tracker/web/router.ex +++ b/lib/error_tracker/web/router.ex @@ -7,7 +7,7 @@ defmodule ErrorTracker.Web.Router do It requires a path in which you are going to serve the web interface. """ defmacro error_tracker_dashboard(path, opts \\ []) do - {session_name, session_opts} = parse_options(opts) + {session_name, session_opts} = parse_options(opts, path) quote do scope unquote(path), alias: false, as: false do @@ -16,14 +16,19 @@ defmodule ErrorTracker.Web.Router do live_session unquote(session_name), unquote(session_opts) do live "/", ErrorTracker.Web.Live.Dashboard, :index, as: unquote(session_name) live "/:id", ErrorTracker.Web.Live.Show, :show, as: unquote(session_name) + live "/:id/:occurrence_id", ErrorTracker.Web.Live.Show, :show, as: unquote(session_name) end end end end @doc false - def parse_options(opts) do - on_mount = Keyword.get(opts, :on_mount, []) + def parse_options(opts, path) do + custom_on_mount = Keyword.get(opts, :on_mount, []) + + on_mount = + [{ErrorTracker.Web.Hooks.SetAssigns, {:set_dashboard_path, path}}] ++ custom_on_mount + session_name = Keyword.get(opts, :as, :error_tracker_dashboard) session_opts = [ From 14cbbde2668390c52e131539b2e399924c2b3911 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93scar=20de=20Arriba?= Date: Sun, 21 Jul 2024 12:35:51 +0200 Subject: [PATCH 22/34] Fix button as link --- lib/error_tracker/web/components/core_components.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/error_tracker/web/components/core_components.ex b/lib/error_tracker/web/components/core_components.ex index 7ca85a1..7aba7a4 100644 --- a/lib/error_tracker/web/components/core_components.ex +++ b/lib/error_tracker/web/components/core_components.ex @@ -12,13 +12,13 @@ defmodule ErrorTracker.Web.CoreComponents do """ attr :type, :string, default: nil attr :class, :string, default: nil - attr :rest, :global, include: ~w(disabled form name value navigate) + attr :rest, :global, include: ~w(disabled form name value href patch navigate) slot :inner_block, required: true def button(%{type: "link"} = assigns) do ~H""" - <%= render_slot(@inner_block) %> - + """ end From 2c027edb4a2c83f4f0e0af1abf94b5f53eb49e8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93scar=20de=20Arriba?= Date: Sun, 21 Jul 2024 12:36:09 +0200 Subject: [PATCH 23/34] Implement routes generator helper --- lib/error_tracker/web.ex | 1 + .../web/live/dashboard.html.heex | 2 +- lib/error_tracker/web/live/show.ex | 2 +- lib/error_tracker/web/live/show.html.heex | 4 ++-- lib/error_tracker/web/router/routes.ex | 24 +++++++++++++++++++ 5 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 lib/error_tracker/web/router/routes.ex diff --git a/lib/error_tracker/web.ex b/lib/error_tracker/web.ex index 46725a3..f7640bf 100644 --- a/lib/error_tracker/web.ex +++ b/lib/error_tracker/web.ex @@ -74,6 +74,7 @@ defmodule ErrorTracker.Web do import Phoenix.LiveView.Helpers import ErrorTracker.Web.CoreComponents + import ErrorTracker.Web.Router.Routes alias Phoenix.LiveView.JS end diff --git a/lib/error_tracker/web/live/dashboard.html.heex b/lib/error_tracker/web/live/dashboard.html.heex index cf1fe59..948c5b6 100644 --- a/lib/error_tracker/web/live/dashboard.html.heex +++ b/lib/error_tracker/web/live/dashboard.html.heex @@ -52,7 +52,7 @@ scope="row" class="px-4 py-4 font-medium text-white whitespace-nowrap text-ellipsis overflow-hidden" > - <.link navigate={"/errors/#{error.id}"}> + <.link navigate={error_path(assigns, error)}> (<%= error.kind %>) <%= error.reason %>

    diff --git a/lib/error_tracker/web/live/show.ex b/lib/error_tracker/web/live/show.ex index 5c246cf..172426b 100644 --- a/lib/error_tracker/web/live/show.ex +++ b/lib/error_tracker/web/live/show.ex @@ -55,7 +55,7 @@ defmodule ErrorTracker.Web.Live.Show do defp related_occurrences(query, num_results \\ @occurreneces_to_navigate) do query |> order_by([o], desc: o.id) - |> select([:id, :inserted_at]) + |> select([:id, :error_id, :inserted_at]) |> limit(^num_results) |> Repo.all() end diff --git a/lib/error_tracker/web/live/show.html.heex b/lib/error_tracker/web/live/show.html.heex index a29cec8..977edd7 100644 --- a/lib/error_tracker/web/live/show.html.heex +++ b/lib/error_tracker/web/live/show.html.heex @@ -1,5 +1,5 @@

    - <.button type="link" href="/errors">Back to the dashboard + <.button type="link" href={dashboard_path(assigns)}>Back to the dashboard
    @@ -65,7 +65,7 @@ <.link :for={occurrence <- @occurrences} class={if occurrence.id == @occurrence.id, do: "font-bold"} - patch={"/errors/#{@error.id}?occurrence_id=#{occurrence.id}"} + patch={occurrence_path(assigns, occurrence)} > <%= Calendar.strftime(occurrence.inserted_at, "%c") %> diff --git a/lib/error_tracker/web/router/routes.ex b/lib/error_tracker/web/router/routes.ex new file mode 100644 index 0000000..5945b42 --- /dev/null +++ b/lib/error_tracker/web/router/routes.ex @@ -0,0 +1,24 @@ +defmodule ErrorTracker.Web.Router.Routes do + @moduledoc """ + Module used to generate dashboard routes. + """ + + alias ErrorTracker.Error + alias ErrorTracker.Occurrence + + @doc """ + Returns the dashboard path + """ + def dashboard_path(%{dashboard_path: dashboard_path}), do: dashboard_path + + @doc """ + Returns the path to see the details of an error + """ + def error_path(assigns, %Error{id: id}), do: dashboard_path(assigns) <> "/#{id}" + + @doc """ + Returns the path to see the details of an occurrence + """ + def occurrence_path(assigns, %Occurrence{id: id, error_id: error_id}), + do: dashboard_path(assigns) <> "/#{error_id}/#{id}" +end From e9c42023592083e330f346301c84d07d07dc9f3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93scar=20de=20Arriba?= Date: Sun, 21 Jul 2024 12:38:46 +0200 Subject: [PATCH 24/34] Happy credo --- lib/error_tracker/web/components/core_components.ex | 2 +- lib/error_tracker/web/live/show.ex | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/error_tracker/web/components/core_components.ex b/lib/error_tracker/web/components/core_components.ex index 7aba7a4..4c50fbc 100644 --- a/lib/error_tracker/web/components/core_components.ex +++ b/lib/error_tracker/web/components/core_components.ex @@ -16,7 +16,7 @@ defmodule ErrorTracker.Web.CoreComponents do slot :inner_block, required: true - def button(%{type: "link"} = assigns) do + def button(assigns = %{type: "link"}) do ~H""" <.link class={[ diff --git a/lib/error_tracker/web/live/show.ex b/lib/error_tracker/web/live/show.ex index 172426b..bb13ace 100644 --- a/lib/error_tracker/web/live/show.ex +++ b/lib/error_tracker/web/live/show.ex @@ -23,7 +23,7 @@ defmodule ErrorTracker.Web.Live.Show do |> where([o], o.id < ^occurrence.id) |> related_occurrences(round(@occurreneces_to_navigate / 2)) - limit_next_occurrences = dbg(@occurreneces_to_navigate - length(previous_occurrences) - 1) + limit_next_occurrences = @occurreneces_to_navigate - length(previous_occurrences) - 1 next_occurrences = base_query From 1bab26bf2875ad64fd515b65dc52ae6b87b2459f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93scar=20de=20Arriba?= Date: Sun, 21 Jul 2024 12:59:04 +0200 Subject: [PATCH 25/34] Refactor how related occurrences are loaded --- lib/error_tracker/web/live/show.ex | 74 +++++++++++++++++++++--------- 1 file changed, 52 insertions(+), 22 deletions(-) diff --git a/lib/error_tracker/web/live/show.ex b/lib/error_tracker/web/live/show.ex index bb13ace..53ebaae 100644 --- a/lib/error_tracker/web/live/show.ex +++ b/lib/error_tracker/web/live/show.ex @@ -7,7 +7,7 @@ defmodule ErrorTracker.Web.Live.Show do alias ErrorTracker.Error alias ErrorTracker.Repo - @occurreneces_to_navigate 50 + @occurrences_to_navigate 50 def mount(%{"id" => id}, _session, socket) do error = Repo.get!(Error, id) @@ -15,44 +15,74 @@ defmodule ErrorTracker.Web.Live.Show do end def handle_params(%{"occurrence_id" => occurrence_id}, _uri, socket) do - base_query = Ecto.assoc(socket.assigns.error, :occurrences) - occurrence = Repo.get!(base_query, occurrence_id) - - previous_occurrences = - base_query - |> where([o], o.id < ^occurrence.id) - |> related_occurrences(round(@occurreneces_to_navigate / 2)) - - limit_next_occurrences = @occurreneces_to_navigate - length(previous_occurrences) - 1 - - next_occurrences = - base_query - |> where([o], o.id > ^occurrence.id) - |> related_occurrences(limit_next_occurrences) + occurrence = + socket.assigns.error + |> Ecto.assoc(:occurrences) + |> Repo.get!(occurrence_id) socket = socket - |> assign(:occurrences, previous_occurrences ++ [occurrence] ++ next_occurrences) |> assign(:occurrence, occurrence) + |> load_related_occurrences() {:noreply, socket} end def handle_params(_, _uri, socket) do - base_query = Ecto.assoc(socket.assigns.error, :occurrences) - - occurrences = related_occurrences(base_query) - occurrence = Repo.get!(base_query, hd(occurrences).id) + [occurrence] = + socket.assigns.error + |> Ecto.assoc(:occurrences) + |> order_by([o], desc: o.id) + |> limit(1) + |> Repo.all() socket = socket - |> assign(:occurrences, occurrences) |> assign(:occurrence, occurrence) + |> load_related_occurrences() {:noreply, socket} end - defp related_occurrences(query, num_results \\ @occurreneces_to_navigate) do + defp load_related_occurrences(socket) do + current_occurrence = socket.assigns.occurrence + base_query = Ecto.assoc(socket.assigns.error, :occurrences) + + half_limit = floor(@occurrences_to_navigate / 2) + + previous_occurrences_query = where(base_query, [o], o.id < ^current_occurrence.id) + next_occurrences_query = where(base_query, [o], o.id > ^current_occurrence.id) + previous_count = Repo.aggregate(previous_occurrences_query, :count) + next_count = Repo.aggregate(next_occurrences_query, :count) + + {previous_limit, next_limit} = + cond do + previous_count < half_limit and next_count < half_limit -> + {previous_count, next_count} + + previous_count < half_limit -> + {previous_count, @occurrences_to_navigate - previous_count - 1} + + next_count < half_limit -> + {@occurrences_to_navigate - next_count - 1, next_count} + + true -> + {half_limit, half_limit} + end + + occurrences = + [ + related_occurrences(next_occurrences_query, next_limit), + current_occurrence, + related_occurrences(previous_occurrences_query, previous_limit) + ] + |> List.flatten() + |> Enum.reverse() + + assign(socket, :occurrences, occurrences) + end + + defp related_occurrences(query, num_results) do query |> order_by([o], desc: o.id) |> select([:id, :error_id, :inserted_at]) From d9aa8186bd3a5e38583bf1a6d7f5b825e50c0edc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93scar=20de=20Arriba?= Date: Sun, 21 Jul 2024 13:56:41 +0200 Subject: [PATCH 26/34] WIP UI --- lib/error_tracker/web/live/dashboard.html.heex | 2 +- lib/error_tracker/web/live/show.html.heex | 17 ++++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/lib/error_tracker/web/live/dashboard.html.heex b/lib/error_tracker/web/live/dashboard.html.heex index 948c5b6..570987c 100644 --- a/lib/error_tracker/web/live/dashboard.html.heex +++ b/lib/error_tracker/web/live/dashboard.html.heex @@ -50,7 +50,7 @@ <.link navigate={error_path(assigns, error)}> (<%= error.kind %>) <%= error.reason %> diff --git a/lib/error_tracker/web/live/show.html.heex b/lib/error_tracker/web/live/show.html.heex index 977edd7..b206181 100644 --- a/lib/error_tracker/web/live/show.html.heex +++ b/lib/error_tracker/web/live/show.html.heex @@ -1,11 +1,18 @@ -
    +
    <.button type="link" href={dashboard_path(assigns)}>Back to the dashboard
    -
    -
    -

    <%= @error.reason |> String.split("\n", trim: true) |> List.first() %>

    + +
    +

    Full message

    @@ -58,7 +65,7 @@
         
    -
    +

    List of occurrences

    From e06e6888ec242436dd57582798cc0b919b43f401 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93scar=20de=20Arriba?= Date: Sun, 21 Jul 2024 13:59:04 +0200 Subject: [PATCH 27/34] Update navbar and layout --- lib/error_tracker/web/components/layouts/live.html.heex | 4 ++-- lib/error_tracker/web/components/layouts/navbar.ex | 9 ++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/error_tracker/web/components/layouts/live.html.heex b/lib/error_tracker/web/components/layouts/live.html.heex index 959d8b7..864819d 100644 --- a/lib/error_tracker/web/components/layouts/live.html.heex +++ b/lib/error_tracker/web/components/layouts/live.html.heex @@ -1,4 +1,4 @@ -<.live_component module={Navbar} id="navbar" /> -
    +<.live_component module={Navbar} id="navbar" {assigns} /> +
    <%= @inner_content %>
    diff --git a/lib/error_tracker/web/components/layouts/navbar.ex b/lib/error_tracker/web/components/layouts/navbar.ex index 7063e1d..d4478d5 100644 --- a/lib/error_tracker/web/components/layouts/navbar.ex +++ b/lib/error_tracker/web/components/layouts/navbar.ex @@ -5,10 +5,13 @@ defmodule ErrorTracker.Web.Layouts.Navbar do def render(assigns) do ~H"""