Skip to content

Commit cc6aacb

Browse files
odarribacrbelaus
andauthored
Integrate breadcrumbs (#107)
This change adds support for breadcrumbs as a first-class field on our occurrences. Breadcrumbs are now a way to store a list of strings indicating in which order they were added, so they can be used to track which code was executed and on which order. They are managed per-process (which in Phoenix means per-request in general) * A new section in occurrence detail LiveView was added * Some helper functions to add and list breadcrumbs were added * The integration with `Ash` and `Splode` was updated to match the new system * Some tests were added * A new migration was added to create the new field --------- Co-authored-by: crbelaus <cristian@crbelaus.com>
1 parent 428f4c7 commit cc6aacb

File tree

13 files changed

+208
-22
lines changed

13 files changed

+208
-22
lines changed

dev.exs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,14 +71,20 @@ defmodule ErrorTrackerDevWeb.PageController do
7171
end
7272

7373
def call(conn, :noroute) do
74+
ErrorTracker.add_breadcrumb("ErrorTrackerDevWeb.PageController.no_route")
7475
raise Phoenix.Router.NoRouteError, conn: conn, router: ErrorTrackerDevWeb.Router
7576
end
7677

7778
def call(_conn, :exception) do
78-
raise "This is a controller exception"
79+
ErrorTracker.add_breadcrumb("ErrorTrackerDevWeb.PageController.exception")
80+
81+
raise CustomException,
82+
message: "This is a controller exception",
83+
bread_crumbs: ["First", "Second"]
7984
end
8085

8186
def call(_conn, :exit) do
87+
ErrorTracker.add_breadcrumb("ErrorTrackerDevWeb.PageController.exit")
8288
exit(:timeout)
8389
end
8490

@@ -89,6 +95,10 @@ defmodule ErrorTrackerDevWeb.PageController do
8995
end
9096
end
9197

98+
defmodule CustomException do
99+
defexception [:message, :bread_crumbs]
100+
end
101+
92102
defmodule ErrorTrackerDevWeb.ErrorView do
93103
def render("404.html", _assigns) do
94104
"This is a 404"
@@ -142,10 +152,16 @@ defmodule ErrorTrackerDevWeb.Endpoint do
142152

143153
plug Plug.RequestId
144154
plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
155+
plug :add_breadcrumb
145156
plug :maybe_exception
146157
plug :set_csp
147158
plug ErrorTrackerDevWeb.Router
148159

160+
def add_breadcrumb(conn, _) do
161+
ErrorTracker.add_breadcrumb("ErrorTrackerDevWeb.Endpoint.add_breadcrumb")
162+
conn
163+
end
164+
149165
def maybe_exception(%Plug.Conn{path_info: ["plug-exception"]}, _), do: raise("Plug exception")
150166
def maybe_exception(conn, _), do: conn
151167

guides/Getting Started.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ Open the generated migration and call the `up` and `down` functions on `ErrorTra
5656
defmodule MyApp.Repo.Migrations.AddErrorTracker do
5757
use Ecto.Migration
5858

59-
def up, do: ErrorTracker.Migration.up(version: 3)
59+
def up, do: ErrorTracker.Migration.up(version: 4)
6060

6161
# We specify `version: 1` in `down`, to ensure we remove all migrations.
6262
def down, do: ErrorTracker.Migration.down(version: 1)

lib/error_tracker.ex

Lines changed: 63 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,24 @@ defmodule ErrorTracker do
6060
As we had seen before, you can use `ErrorTracker.report/3` to manually report an
6161
error. The third parameter of this function is optional and allows you to include
6262
extra context that will be tracked along with the error.
63+
64+
## Breadcrumbs
65+
66+
Aside from contextual information, it is sometimes useful to know in which points
67+
of your code the code was executed in a given request / process.
68+
69+
Using breadcrumbs allows you to add that information to any error generated and
70+
stored on a given process / request. And if you are using `Ash` or `Splode`their
71+
exceptions' breadcrumbs will be automatically populated.
72+
73+
If you want to add a breadcrumb you can do so:
74+
75+
```elixir
76+
ErrorTracker.add_breadcrumb("Executed my super secret code")
77+
```
78+
79+
Breadcrumbs can be viewed in the dashboard while viewing the details of an
80+
occurrence.
6381
"""
6482

6583
@typedoc """
@@ -119,15 +137,14 @@ defmodule ErrorTracker do
119137
{:ok, stacktrace} = ErrorTracker.Stacktrace.new(stacktrace)
120138
{:ok, error} = Error.new(kind, reason, stacktrace)
121139
context = Map.merge(get_context(), given_context)
122-
123-
context =
124-
if bread_crumbs = bread_crumbs(exception),
125-
do: Map.put(context, "bread_crumbs", bread_crumbs),
126-
else: context
140+
breadcrumbs = get_breadcrumbs() ++ exception_breadcrumbs(exception)
127141

128142
if enabled?() && !ignored?(error, context) do
129143
sanitized_context = sanitize_context(context)
130-
{_error, occurrence} = upsert_error!(error, stacktrace, sanitized_context, reason)
144+
145+
{_error, occurrence} =
146+
upsert_error!(error, stacktrace, sanitized_context, breadcrumbs, reason)
147+
131148
occurrence
132149
else
133150
:noop
@@ -205,6 +222,40 @@ defmodule ErrorTracker do
205222
Process.get(:error_tracker_context, %{})
206223
end
207224

225+
@doc """
226+
Adds a breadcrumb to the current process.
227+
228+
The new breadcrumb will be added as the most recent entry of the breadcrumbs
229+
list.
230+
231+
## Breadcrumbs limit
232+
233+
Breadcrumbs are a powerful tool that allows to add an infinite number of
234+
entries. However, it is not recommended to store errors with an excessive
235+
amount of breadcrumbs.
236+
237+
As they are stored as an array of strings under the hood, storing many
238+
entries per error can lead to some delays and using extra disk space on the
239+
database.
240+
"""
241+
@spec add_breadcrumb(String.t()) :: list(String.t())
242+
def add_breadcrumb(breadcrumb) when is_binary(breadcrumb) do
243+
current_breadcrumbs = Process.get(:error_tracker_breadcrumbs, [])
244+
new_breadcrumbs = current_breadcrumbs ++ [breadcrumb]
245+
246+
Process.put(:error_tracker_breadcrumbs, new_breadcrumbs)
247+
248+
new_breadcrumbs
249+
end
250+
251+
@doc """
252+
Obtain the breadcrumbs of the current process.
253+
"""
254+
@spec get_breadcrumbs() :: list(String.t())
255+
def get_breadcrumbs do
256+
Process.get(:error_tracker_breadcrumbs, [])
257+
end
258+
208259
defp enabled? do
209260
!!Application.get_env(:error_tracker, :enabled, true)
210261
end
@@ -237,15 +288,15 @@ defmodule ErrorTracker do
237288
end
238289
end
239290

240-
defp bread_crumbs(exception) do
291+
defp exception_breadcrumbs(exception) do
241292
case exception do
242-
{_kind, exception} -> bread_crumbs(exception)
243-
%{bread_crumbs: bread_crumbs} -> bread_crumbs
244-
_other -> nil
293+
{_kind, exception} -> exception_breadcrumbs(exception)
294+
%{bread_crumbs: breadcrumbs} -> breadcrumbs
295+
_other -> []
245296
end
246297
end
247298

248-
defp upsert_error!(error, stacktrace, context, reason) do
299+
defp upsert_error!(error, stacktrace, context, breadcrumbs, reason) do
249300
existing_status =
250301
Repo.one(from e in Error, where: [fingerprint: ^error.fingerprint], select: e.status)
251302

@@ -271,6 +322,7 @@ defmodule ErrorTracker do
271322
|> Occurrence.changeset(%{
272323
stacktrace: stacktrace,
273324
context: context,
325+
breadcrumbs: breadcrumbs,
274326
reason: reason
275327
})
276328
|> Repo.insert!()

lib/error_tracker/migration/mysql.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ defmodule ErrorTracker.Migration.MySQL do
77
alias ErrorTracker.Migration.SQLMigrator
88

99
@initial_version 3
10-
@current_version 3
10+
@current_version 4
1111

1212
@impl ErrorTracker.Migration
1313
def up(opts) do
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
defmodule ErrorTracker.Migration.MySQL.V04 do
2+
@moduledoc false
3+
4+
use Ecto.Migration
5+
6+
def up(_opts) do
7+
alter table(:error_tracker_occurrences) do
8+
add :breadcrumbs, :json, null: true
9+
end
10+
end
11+
12+
def down(_opts) do
13+
alter table(:error_tracker_occurrences) do
14+
remove :breadcrumbs
15+
end
16+
end
17+
end

lib/error_tracker/migration/postgres.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ defmodule ErrorTracker.Migration.Postgres do
77
alias ErrorTracker.Migration.SQLMigrator
88

99
@initial_version 1
10-
@current_version 3
10+
@current_version 4
1111
@default_prefix "public"
1212

1313
@impl ErrorTracker.Migration
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
defmodule ErrorTracker.Migration.Postgres.V04 do
2+
@moduledoc false
3+
4+
use Ecto.Migration
5+
6+
def up(_opts) do
7+
alter table(:error_tracker_occurrences) do
8+
add :breadcrumbs, {:array, :string}, default: [], null: false
9+
end
10+
end
11+
12+
def down(_opts) do
13+
alter table(:error_tracker_occurrences) do
14+
remove :breadcrumbs
15+
end
16+
end
17+
end

lib/error_tracker/migration/sqlite.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ defmodule ErrorTracker.Migration.SQLite do
77
alias ErrorTracker.Migration.SQLMigrator
88

99
@initial_version 2
10-
@current_version 3
10+
@current_version 4
1111

1212
@impl ErrorTracker.Migration
1313
def up(opts) do
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
defmodule ErrorTracker.Migration.SQLite.V04 do
2+
@moduledoc false
3+
4+
use Ecto.Migration
5+
6+
def up(_opts) do
7+
alter table(:error_tracker_occurrences) do
8+
add :breadcrumbs, {:array, :string}, default: [], null: false
9+
end
10+
end
11+
12+
def down(_opts) do
13+
alter table(:error_tracker_occurrences) do
14+
remove :breadcrumbs
15+
end
16+
end
17+
end

lib/error_tracker/schemas/occurrence.ex

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ defmodule ErrorTracker.Occurrence do
1515
@type t :: %__MODULE__{}
1616

1717
schema "error_tracker_occurrences" do
18-
field :context, :map
1918
field :reason, :string
2019

20+
field :context, :map
21+
field :breadcrumbs, {:array, :string}
22+
2123
embeds_one :stacktrace, ErrorTracker.Stacktrace
2224
belongs_to :error, ErrorTracker.Error
2325

@@ -27,7 +29,7 @@ defmodule ErrorTracker.Occurrence do
2729
@doc false
2830
def changeset(occurrence, attrs) do
2931
occurrence
30-
|> cast(attrs, [:context, :reason])
32+
|> cast(attrs, [:context, :reason, :breadcrumbs])
3133
|> maybe_put_stacktrace()
3234
|> validate_required([:reason, :stacktrace])
3335
|> validate_context()

lib/error_tracker/web/live/show.html.heex

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,25 @@
2525
<%= @error.source_line %></pre>
2626
</.section>
2727

28+
<.section :if={@occurrence.breadcrumbs != []} title="Bread crumbs">
29+
<div class="relative overflow-x-auto shadow-md sm:rounded-lg ring-1 ring-gray-900">
30+
<table class="w-full text-sm text-gray-400 table-fixed">
31+
<tr
32+
:for={
33+
{breadcrumb, index} <-
34+
@occurrence.breadcrumbs |> Enum.reverse() |> Enum.with_index()
35+
}
36+
class="border-b bg-gray-400/10 border-gray-900 last:border-b-0"
37+
>
38+
<td class="w-11 pl-2 py-4 font-medium text-white relative text-right">
39+
<%= length(@occurrence.breadcrumbs) - index %>.
40+
</td>
41+
<td class="px-2 py-4 font-medium text-white relative"><%= breadcrumb %></td>
42+
</tr>
43+
</table>
44+
</div>
45+
</.section>
46+
2847
<.section :if={@occurrence.stacktrace.lines != []} title="Stacktrace">
2948
<div class="p-4 bg-gray-300/10 border border-gray-900 rounded-lg">
3049
<div class="w-full mb-4">

priv/static/app.css

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -885,6 +885,10 @@ select {
885885
width: 2.5rem;
886886
}
887887

888+
.w-11 {
889+
width: 2.75rem;
890+
}
891+
888892
.w-28 {
889893
width: 7rem;
890894
}
@@ -1129,6 +1133,10 @@ select {
11291133
padding-bottom: 11.5px;
11301134
}
11311135

1136+
.pl-2 {
1137+
padding-left: 0.5rem;
1138+
}
1139+
11321140
.pr-2 {
11331141
padding-right: 0.5rem;
11341142
}
@@ -1145,6 +1153,10 @@ select {
11451153
text-align: center;
11461154
}
11471155

1156+
.text-right {
1157+
text-align: right;
1158+
}
1159+
11481160
.align-top {
11491161
vertical-align: top;
11501162
}
@@ -1318,6 +1330,10 @@ select {
13181330
border-radius: 4px;
13191331
}
13201332

1333+
.last\:border-b-0:last-child {
1334+
border-bottom-width: 0px;
1335+
}
1336+
13211337
.last-of-type\:border-b-0:last-of-type {
13221338
border-bottom-width: 0px;
13231339
}

0 commit comments

Comments
 (0)