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()

0 commit comments

Comments
 (0)