Skip to content

Commit c82b312

Browse files
authored
Dashboard (#13)
Add a live dashboard that allows users to view registered errors and mark them as resolved or unresolved.
1 parent 4cf8290 commit c82b312

22 files changed

Lines changed: 715 additions & 30 deletions

.credo.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,6 @@
121121
#
122122
{Credo.Check.Refactor.Apply, []},
123123
{Credo.Check.Refactor.CondStatements, []},
124-
{Credo.Check.Refactor.CyclomaticComplexity, []},
125124
{Credo.Check.Refactor.FilterCount, []},
126125
{Credo.Check.Refactor.FilterFilter, []},
127126
{Credo.Check.Refactor.FunctionArity, []},
@@ -189,6 +188,7 @@
189188
{Credo.Check.Readability.WithCustomTaggedTuple, []},
190189
{Credo.Check.Refactor.ABCSize, []},
191190
{Credo.Check.Refactor.AppendSingleItem, []},
191+
{Credo.Check.Refactor.CyclomaticComplexity, []},
192192
{Credo.Check.Refactor.DoubleBooleanNegation, []},
193193
{Credo.Check.Refactor.FilterReject, []},
194194
{Credo.Check.Refactor.IoPuts, []},

lib/error_tracker.ex

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,30 +8,36 @@ defmodule ErrorTracker do
88
"""
99
@type context :: %{String.t() => any()}
1010

11+
alias ErrorTracker.Error
12+
alias ErrorTracker.Repo
13+
1114
def report(exception, stacktrace, given_context \\ %{}) do
1215
{:ok, stacktrace} = ErrorTracker.Stacktrace.new(stacktrace)
13-
{:ok, error} = ErrorTracker.Error.new(exception, stacktrace)
16+
{:ok, error} = Error.new(exception, stacktrace)
1417

1518
context = Map.merge(get_context(), given_context)
1619

1720
error =
18-
repo().insert!(error,
19-
on_conflict: [set: [status: :unresolved]],
20-
conflict_target: :fingerprint,
21-
prefix: prefix()
21+
Repo.insert!(error,
22+
on_conflict: [set: [status: :unresolved, last_occurrence_at: DateTime.utc_now()]],
23+
conflict_target: :fingerprint
2224
)
2325

2426
error
2527
|> Ecto.build_assoc(:occurrences, stacktrace: stacktrace, context: context)
26-
|> repo().insert!(prefix: prefix())
28+
|> Repo.insert!()
2729
end
2830

29-
def repo do
30-
Application.fetch_env!(:error_tracker, :repo)
31+
def resolve(error = %Error{status: :unresolved}) do
32+
changeset = Ecto.Changeset.change(error, status: :resolved)
33+
34+
Repo.update(changeset)
3135
end
3236

33-
def prefix do
34-
Application.get_env(:error_tracker, :prefix, "public")
37+
def unresolve(error = %Error{status: :resolved}) do
38+
changeset = Ecto.Changeset.change(error, status: :unresolved)
39+
40+
Repo.update(changeset)
3541
end
3642

3743
@spec set_context(context()) :: context()

lib/error_tracker/migrations.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ defmodule ErrorTracker.Migration do
116116
end
117117

118118
defp migrator do
119-
case ErrorTracker.repo().__adapter__() do
119+
case ErrorTracker.Repo.__adapter__() do
120120
Ecto.Adapters.Postgres -> ErrorTracker.Migrations.Postgres
121121
adapter -> raise "ErrorTracker does not support #{adapter}"
122122
end

lib/error_tracker/migrations/postgres/v01.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ defmodule ErrorTracker.Migrations.Postgres.V01 do
1313
add :source_function, :text, null: false
1414
add :status, :string, null: false
1515
add :fingerprint, :string, null: false
16+
add :last_occurrence_at, :utc_datetime_usec, null: false
1617

1718
timestamps(type: :utc_datetime_usec)
1819
end

lib/error_tracker/repo.ex

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
defmodule ErrorTracker.Repo do
2+
@moduledoc """
3+
Wraps Ecto.Repo calls and applies default options.
4+
"""
5+
def insert!(struct_or_changeset, opts \\ []) do
6+
dispatch(:insert!, [struct_or_changeset], opts)
7+
end
8+
9+
def update(changeset, opts \\ []) do
10+
dispatch(:update, [changeset], opts)
11+
end
12+
13+
def get(queryable, id, opts \\ []) do
14+
dispatch(:get, [queryable, id], opts)
15+
end
16+
17+
def get!(queryable, id, opts \\ []) do
18+
dispatch(:get!, [queryable, id], opts)
19+
end
20+
21+
def all(queryable, opts \\ []) do
22+
dispatch(:all, [queryable], opts)
23+
end
24+
25+
def aggregate(queryable, aggregate, opts \\ []) do
26+
dispatch(:aggregate, [queryable, aggregate], opts)
27+
end
28+
29+
def __adapter__, do: repo().__adapter__()
30+
31+
defp dispatch(action, args, opts) do
32+
defaults = [prefix: Application.get_env(:error_tracker, :prefix, "public")]
33+
opts_w_defaults = Keyword.merge(defaults, opts)
34+
35+
apply(repo(), action, args ++ [opts_w_defaults])
36+
end
37+
38+
defp repo do
39+
Application.fetch_env!(:error_tracker, :repo)
40+
end
41+
end

lib/error_tracker/schemas/error.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ defmodule ErrorTracker.Error do
1515
field :source_function, :string
1616
field :status, Ecto.Enum, values: [:resolved, :unresolved], default: :unresolved
1717
field :fingerprint, :binary
18+
field :last_occurrence_at, :utc_datetime_usec
1819

1920
has_many :occurrences, ErrorTracker.Occurrence
2021

@@ -48,6 +49,7 @@ defmodule ErrorTracker.Error do
4849
%__MODULE__{}
4950
|> Ecto.Changeset.change(params)
5051
|> Ecto.Changeset.put_change(:fingerprint, Base.encode16(fingerprint))
52+
|> Ecto.Changeset.put_change(:last_occurrence_at, DateTime.utc_now())
5153
|> Ecto.Changeset.apply_action(:new)
5254
end
5355
end

lib/error_tracker/web.ex

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ defmodule ErrorTracker.Web do
7373
import Phoenix.HTML
7474
import Phoenix.LiveView.Helpers
7575

76+
import ErrorTracker.Web.CoreComponents
77+
import ErrorTracker.Web.Helpers
78+
import ErrorTracker.Web.Router.Routes
79+
7680
alias Phoenix.LiveView.JS
7781
end
7882
end
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
defmodule ErrorTracker.Web.CoreComponents do
2+
@moduledoc false
3+
use Phoenix.Component
4+
5+
@doc """
6+
Renders a button.
7+
8+
## Examples
9+
10+
<.button>Send!</.button>
11+
<.button phx-click="go" class="ml-2">Send!</.button>
12+
"""
13+
attr :type, :string, default: nil
14+
attr :class, :string, default: nil
15+
attr :rest, :global, include: ~w(disabled form name value href patch navigate)
16+
17+
slot :inner_block, required: true
18+
19+
def button(assigns = %{type: "link"}) do
20+
~H"""
21+
<.link
22+
class={[
23+
"phx-submit-loading:opacity-75 rounded-lg bg-zinc-600 hover:bg-zinc-400 py-[11.5px] px-3",
24+
"text-sm font-semibold leading-6 text-white active:text-white/80",
25+
@class
26+
]}
27+
{@rest}
28+
>
29+
<%= render_slot(@inner_block) %>
30+
</.link>
31+
"""
32+
end
33+
34+
def button(assigns) do
35+
~H"""
36+
<button
37+
type={@type}
38+
class={[
39+
"phx-submit-loading:opacity-75 rounded-lg bg-zinc-600 hover:bg-zinc-400 py-2 px-3",
40+
"text-sm font-semibold leading-6 text-white active:text-white/80",
41+
@class
42+
]}
43+
{@rest}
44+
>
45+
<%= render_slot(@inner_block) %>
46+
</button>
47+
"""
48+
end
49+
50+
@doc """
51+
Renders a badge.
52+
53+
## Examples
54+
55+
<.badge>Info</.badge>
56+
<.badge color={:red}>Error</.badge>
57+
"""
58+
attr :color, :atom, default: :blue
59+
attr :rest, :global
60+
61+
slot :inner_block, required: true
62+
63+
def badge(assigns) do
64+
color_class =
65+
case assigns.color do
66+
:blue -> "bg-blue-900 text-blue-300"
67+
:gray -> "bg-gray-700 text-gray-300"
68+
:red -> "bg-red-900 text-red-300"
69+
:green -> "bg-green-900 text-green-300"
70+
:yellow -> "bg-yellow-900 text-yellow-300"
71+
:indigo -> "bg-indigo-900 text-indigo-300"
72+
:purple -> "bg-purple-900 text-purple-300"
73+
:pink -> "bg-pink-900 text-pink-300"
74+
end
75+
76+
assigns = Map.put(assigns, :color_class, color_class)
77+
78+
~H"""
79+
<span class={["text-sm font-medium me-2 px-2.5 py-1.5 rounded", @color_class]} {@rest}>
80+
<%= render_slot(@inner_block) %>
81+
</span>
82+
"""
83+
end
84+
85+
attr :page, :integer, required: true
86+
attr :total_pages, :integer, required: true
87+
attr :event_previous, :string, default: "prev-page"
88+
attr :event_next, :string, default: "next-page"
89+
90+
def pagination(assigns) do
91+
~H"""
92+
<div class="mt-10 w-full flex">
93+
<button
94+
:if={@page > 1}
95+
class="flex items-center justify-center px-4 h-10 text-base font-medium text-gray-400 bg-gray-800 border border-gray-700 rounded-lg hover:bg-gray-700 hover:text-white"
96+
phx-click={@event_previous}
97+
>
98+
Previous page
99+
</button>
100+
<button
101+
:if={@page < @total_pages}
102+
class="flex items-center justify-center px-4 h-10 text-base font-medium text-gray-400 bg-gray-800 border border-gray-700 rounded-lg hover:bg-gray-700 hover:text-white"
103+
phx-click={@event_next}
104+
>
105+
Next page
106+
</button>
107+
</div>
108+
"""
109+
end
110+
111+
attr :title, :string
112+
attr :title_class, :string, default: nil
113+
attr :rest, :global
114+
115+
slot :inner_block, required: true
116+
117+
def section(assigns) do
118+
~H"""
119+
<div>
120+
<h2 :if={assigns[:title]} class={["text-sm font-bold mb-2 uppercase", @title_class]}>
121+
<%= @title %>
122+
</h2>
123+
<%= render_slot(@inner_block) %>
124+
</div>
125+
"""
126+
end
127+
end

lib/error_tracker/web/components/layouts.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
defmodule ErrorTracker.Web.Layouts do
22
use ErrorTracker.Web, :html
33

4+
alias ErrorTracker.Web.Layouts.Navbar
5+
46
@css_path :code.priv_dir(:error_tracker) |> Path.join("static/app.css")
57
@js_path :code.priv_dir(:error_tracker) |> Path.join("static/app.js")
68

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1-
<main>
1+
<.live_component module={Navbar} id="navbar" {assigns} />
2+
<main class="container px-4 mx-auto mt-4 mb-4">
23
<%= @inner_content %>
34
</main>

0 commit comments

Comments
 (0)