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.ex b/lib/error_tracker.ex
index 7f45232..5997214 100644
--- a/lib/error_tracker.ex
+++ b/lib/error_tracker.ex
@@ -8,30 +8,36 @@ 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)
- {:ok, error} = ErrorTracker.Error.new(exception, stacktrace)
+ {:ok, error} = Error.new(exception, stacktrace)
context = Map.merge(get_context(), given_context)
error =
- repo().insert!(error,
- on_conflict: [set: [status: :unresolved]],
- conflict_target: :fingerprint,
- prefix: prefix()
+ Repo.insert!(error,
+ on_conflict: [set: [status: :unresolved, last_occurrence_at: DateTime.utc_now()]],
+ conflict_target: :fingerprint
)
error
|> Ecto.build_assoc(:occurrences, stacktrace: stacktrace, context: context)
- |> repo().insert!(prefix: prefix())
+ |> Repo.insert!()
end
- def repo do
- Application.fetch_env!(:error_tracker, :repo)
+ def resolve(error = %Error{status: :unresolved}) do
+ changeset = Ecto.Changeset.change(error, status: :resolved)
+
+ Repo.update(changeset)
end
- def prefix do
- Application.get_env(:error_tracker, :prefix, "public")
+ def unresolve(error = %Error{status: :resolved}) do
+ changeset = Ecto.Changeset.change(error, status: :unresolved)
+
+ 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/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/repo.ex b/lib/error_tracker/repo.ex
new file mode 100644
index 0000000..f736052
--- /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/schemas/error.ex b/lib/error_tracker/schemas/error.ex
index 828ee53..443352a 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
@@ -48,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
diff --git a/lib/error_tracker/web.ex b/lib/error_tracker/web.ex
index f866fb5..eb291c6 100644
--- a/lib/error_tracker/web.ex
+++ b/lib/error_tracker/web.ex
@@ -73,6 +73,10 @@ defmodule ErrorTracker.Web do
import Phoenix.HTML
import Phoenix.LiveView.Helpers
+ import ErrorTracker.Web.CoreComponents
+ import ErrorTracker.Web.Helpers
+ import ErrorTracker.Web.Router.Routes
+
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..4f7e004
--- /dev/null
+++ b/lib/error_tracker/web/components/core_components.ex
@@ -0,0 +1,127 @@
+defmodule ErrorTracker.Web.CoreComponents do
+ @moduledoc false
+ 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 href patch navigate)
+
+ slot :inner_block, required: true
+
+ def button(assigns = %{type: "link"}) do
+ ~H"""
+ <.link
+ class={[
+ "phx-submit-loading:opacity-75 rounded-lg bg-zinc-600 hover:bg-zinc-400 py-[11.5px] px-3",
+ "text-sm font-semibold leading-6 text-white active:text-white/80",
+ @class
+ ]}
+ {@rest}
+ >
+ <%= render_slot(@inner_block) %>
+
+ """
+ end
+
+ 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"
+ end
+
+ assigns = Map.put(assigns, :color_class, color_class)
+
+ ~H"""
+
+ <%= render_slot(@inner_block) %>
+
+ """
+ end
+
+ attr :page, :integer, required: true
+ attr :total_pages, :integer, required: true
+ attr :event_previous, :string, default: "prev-page"
+ attr :event_next, :string, default: "next-page"
+
+ def pagination(assigns) do
+ ~H"""
+
+
+
+
+ """
+ end
+
+ attr :title, :string
+ attr :title_class, :string, default: nil
+ attr :rest, :global
+
+ slot :inner_block, required: true
+
+ def section(assigns) do
+ ~H"""
+
+
+ <%= @title %>
+
+ <%= render_slot(@inner_block) %>
+
+ """
+ end
+end
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..864819d 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" {assigns} />
+
<%= @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..d4478d5
--- /dev/null
+++ b/lib/error_tracker/web/components/layouts/navbar.ex
@@ -0,0 +1,67 @@
+defmodule ErrorTracker.Web.Layouts.Navbar do
+ @moduledoc false
+ 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 %>
diff --git a/lib/error_tracker/web/helpers.ex b/lib/error_tracker/web/helpers.ex
new file mode 100644
index 0000000..2309a7e
--- /dev/null
+++ b/lib/error_tracker/web/helpers.ex
@@ -0,0 +1,10 @@
+defmodule ErrorTracker.Web.Helpers do
+ @moduledoc false
+
+ @doc false
+ def sanitize_module(<<"Elixir.", str::binary>>), do: str
+ def sanitize_module(str), do: str
+
+ @doc false
+ def format_datetime(dt = %DateTime{}), do: Calendar.strftime(dt, "%c %Z")
+end
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/live/dashboard.ex b/lib/error_tracker/web/live/dashboard.ex
index caf12e6..d0b06be 100644
--- a/lib/error_tracker/web/live/dashboard.ex
+++ b/lib/error_tracker/web/live/dashboard.ex
@@ -3,18 +3,97 @@ defmodule ErrorTracker.Web.Live.Dashboard do
use ErrorTracker.Web, :live_view
+ import Ecto.Query
+
+ alias ErrorTracker.Error
+ alias ErrorTracker.Repo
+
+ @per_page 10
+
@impl Phoenix.LiveView
- def mount(_params, _session, socket) do
- {:ok, assign(socket, :counter, 0)}
+ 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_params(_params, _uri, socket) do
- {:noreply, socket}
+ 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("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
+ {:noreply, socket |> assign(page: socket.assigns.page - 1) |> paginate_errors()}
+ end
+
+ @impl Phoenix.LiveView
+ def handle_event("resolve", %{"error_id" => id}, socket) do
+ error = Repo.get(Error, id)
+ {:ok, _resolved} = ErrorTracker.resolve(error)
+
+ {:noreply, paginate_errors(socket)}
end
@impl Phoenix.LiveView
- def handle_event("increment", _params, socket) do
- {:noreply, assign(socket, :counter, socket.assigns.counter + 1)}
+ def handle_event("unresolve", %{"error_id" => id}, socket) do
+ error = Repo.get(Error, id)
+ {:ok, _unresolved} = ErrorTracker.unresolve(error)
+
+ {:noreply, paginate_errors(socket)}
+ end
+
+ defp paginate_errors(socket) do
+ %{page: page, search: search} = socket.assigns
+
+ query = filter(Error, search)
+
+ total_errors = Repo.aggregate(query, :count)
+
+ errors_query =
+ query
+ |> order_by(desc: :last_occurrence_at)
+ |> offset((^page - 1) * @per_page)
+ |> limit(@per_page)
+
+ assign(socket,
+ errors: Repo.all(errors_query),
+ 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
+
+ defp do_filter({:status, status}, query) do
+ where(query, [error], error.status == ^status)
+ 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 07f5708..35d24f6 100644
--- a/lib/error_tracker/web/live/dashboard.html.heex
+++ b/lib/error_tracker/web/live/dashboard.html.heex
@@ -1,8 +1,93 @@
-Hello world!
-Number of presses: <%= @counter %>
-
+
+
+
+
+
+
+
+
+
+
+ | Error |
+ Last occurrence |
+ Status |
+ |
+
+
+
+
+ |
+ <.link navigate={error_path(assigns, error)}>
+ (<%= sanitize_module(error.kind) %>) <%= error.reason %>
+
+
+ <%= sanitize_module(error.source_function) %>
+
+ <%= error.source_line %>
+
+ |
+
+ <%= format_datetime(error.last_occurrence_at) %>
+ |
+
+ <.badge :if={error.status == :resolved} color={:green}>Resolved
+ <.badge :if={error.status == :unresolved} color={:red}>Unresolved
+ |
+
+ <.button
+ :if={error.status == :unresolved}
+ phx-click="resolve"
+ phx-value-error_id={error.id}
+ >
+ Resolve
+
+
+ <.button
+ :if={error.status == :resolved}
+ phx-click="unresolve"
+ phx-value-error_id={error.id}
+ >
+ Unresolve
+
+ |
+
+
+
+
+
+<.pagination page={@page} total_pages={@total_pages} />
diff --git a/lib/error_tracker/web/live/show.ex b/lib/error_tracker/web/live/show.ex
new file mode 100644
index 0000000..564b6a1
--- /dev/null
+++ b/lib/error_tracker/web/live/show.ex
@@ -0,0 +1,117 @@
+defmodule ErrorTracker.Web.Live.Show do
+ @moduledoc false
+ use ErrorTracker.Web, :live_view
+
+ import Ecto.Query
+
+ alias ErrorTracker.Error
+ alias ErrorTracker.Occurrence
+ alias ErrorTracker.Repo
+
+ @occurrences_to_navigate 50
+
+ @impl Phoenix.LiveView
+ def mount(%{"id" => id}, _session, socket) do
+ error = Repo.get!(Error, id)
+ {:ok, assign(socket, error: error, app: Application.fetch_env!(:error_tracker, :application))}
+ end
+
+ @impl Phoenix.LiveView
+ def handle_params(%{"occurrence_id" => occurrence_id}, _uri, socket) do
+ occurrence =
+ socket.assigns.error
+ |> Ecto.assoc(:occurrences)
+ |> Repo.get!(occurrence_id)
+
+ socket =
+ socket
+ |> assign(:occurrence, occurrence)
+ |> load_related_occurrences()
+
+ {:noreply, socket}
+ end
+
+ def handle_params(_, _uri, socket) do
+ [occurrence] =
+ socket.assigns.error
+ |> Ecto.assoc(:occurrences)
+ |> order_by([o], desc: o.id)
+ |> limit(1)
+ |> Repo.all()
+
+ socket =
+ socket
+ |> assign(:occurrence, occurrence)
+ |> load_related_occurrences()
+
+ {:noreply, socket}
+ end
+
+ @impl Phoenix.LiveView
+ def handle_event("occurrence_navigation", %{"occurrence_id" => id}, socket) do
+ {:noreply,
+ push_patch(socket,
+ to: occurrence_path(socket.assigns, %Occurrence{error_id: socket.assigns.error.id, id: id})
+ )}
+ end
+
+ @impl Phoenix.LiveView
+ def handle_event("resolve", _params, socket) do
+ {:ok, updated_error} = ErrorTracker.resolve(socket.assigns.error)
+
+ {:noreply, assign(socket, :error, updated_error)}
+ end
+
+ @impl Phoenix.LiveView
+ def handle_event("unresolve", _params, socket) do
+ {:ok, updated_error} = ErrorTracker.unresolve(socket.assigns.error)
+
+ {:noreply, assign(socket, :error, updated_error)}
+ end
+
+ 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])
+ |> limit(^num_results)
+ |> Repo.all()
+ 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..c01f7c4
--- /dev/null
+++ b/lib/error_tracker/web/live/show.html.heex
@@ -0,0 +1,101 @@
+
+ <.button type="link" href={dashboard_path(assigns)}>Back to the dashboard
+
+
+
+
+
+
+ <.section title="Full message">
+
<%= @error.reason %>
+
+
+ <.section title="Source">
+
+ <%= sanitize_module(@error.source_function) %>
+ <%= @error.source_line %>
+
+
+ <.section title="Stacktrace">
+
+
+
+
+
+
+
+
+ (<%= line.application || @app %>) |
+
+ <%= "#{sanitize_module(line.module)}.#{line.function}/#{line.arity}" %>
+ <%= "#{line.file}.#{line.line}" %>
+ |
+
+
+
+
+
+
+ <.section title="Context">
+
<%= Jason.encode!(@occurrence.context, pretty: true) %>
+
+
+
+
+ <.section title="Occurrence">
+
+
+
+ <.section title="Error kind">
+
<%= sanitize_module(@error.kind) %>
+
+
+ <.section title="Last seen">
+
<%= format_datetime(@error.last_occurrence_at) %>
+
+
+ <.section title="Status" title_class="mb-3">
+ <.badge :if={@error.status == :resolved} color={:green}>Resolved
+ <.badge :if={@error.status == :unresolved} color={:red}>Unresolved
+
+
+ <.section>
+ <.button :if={@error.status == :unresolved} phx-click="resolve">
+ Mark as resolved
+
+
+ <.button :if={@error.status == :resolved} phx-click="unresolve">
+ Mark as unresolved
+
+
+
+
diff --git a/lib/error_tracker/web/router.ex b/lib/error_tracker/web/router.ex
index 52136df..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
@@ -15,14 +15,20 @@ 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 = [
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
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"},