Skip to content

Commit

Permalink
Integrate breadcrumbs (#107)
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
odarriba and crbelaus authored Nov 23, 2024
1 parent 428f4c7 commit cc6aacb
Show file tree
Hide file tree
Showing 13 changed files with 208 additions and 22 deletions.
18 changes: 17 additions & 1 deletion dev.exs
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,20 @@ defmodule ErrorTrackerDevWeb.PageController do
end

def call(conn, :noroute) do
ErrorTracker.add_breadcrumb("ErrorTrackerDevWeb.PageController.no_route")
raise Phoenix.Router.NoRouteError, conn: conn, router: ErrorTrackerDevWeb.Router
end

def call(_conn, :exception) do
raise "This is a controller exception"
ErrorTracker.add_breadcrumb("ErrorTrackerDevWeb.PageController.exception")

raise CustomException,
message: "This is a controller exception",
bread_crumbs: ["First", "Second"]
end

def call(_conn, :exit) do
ErrorTracker.add_breadcrumb("ErrorTrackerDevWeb.PageController.exit")
exit(:timeout)
end

Expand All @@ -89,6 +95,10 @@ defmodule ErrorTrackerDevWeb.PageController do
end
end

defmodule CustomException do
defexception [:message, :bread_crumbs]
end

defmodule ErrorTrackerDevWeb.ErrorView do
def render("404.html", _assigns) do
"This is a 404"
Expand Down Expand Up @@ -142,10 +152,16 @@ defmodule ErrorTrackerDevWeb.Endpoint do

plug Plug.RequestId
plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
plug :add_breadcrumb
plug :maybe_exception
plug :set_csp
plug ErrorTrackerDevWeb.Router

def add_breadcrumb(conn, _) do
ErrorTracker.add_breadcrumb("ErrorTrackerDevWeb.Endpoint.add_breadcrumb")
conn
end

def maybe_exception(%Plug.Conn{path_info: ["plug-exception"]}, _), do: raise("Plug exception")
def maybe_exception(conn, _), do: conn

Expand Down
2 changes: 1 addition & 1 deletion guides/Getting Started.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Open the generated migration and call the `up` and `down` functions on `ErrorTra
defmodule MyApp.Repo.Migrations.AddErrorTracker do
use Ecto.Migration

def up, do: ErrorTracker.Migration.up(version: 3)
def up, do: ErrorTracker.Migration.up(version: 4)

# We specify `version: 1` in `down`, to ensure we remove all migrations.
def down, do: ErrorTracker.Migration.down(version: 1)
Expand Down
74 changes: 63 additions & 11 deletions lib/error_tracker.ex
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,24 @@ defmodule ErrorTracker do
As we had seen before, you can use `ErrorTracker.report/3` to manually report an
error. The third parameter of this function is optional and allows you to include
extra context that will be tracked along with the error.
## Breadcrumbs
Aside from contextual information, it is sometimes useful to know in which points
of your code the code was executed in a given request / process.
Using breadcrumbs allows you to add that information to any error generated and
stored on a given process / request. And if you are using `Ash` or `Splode`their
exceptions' breadcrumbs will be automatically populated.
If you want to add a breadcrumb you can do so:
```elixir
ErrorTracker.add_breadcrumb("Executed my super secret code")
```
Breadcrumbs can be viewed in the dashboard while viewing the details of an
occurrence.
"""

@typedoc """
Expand Down Expand Up @@ -119,15 +137,14 @@ defmodule ErrorTracker do
{:ok, stacktrace} = ErrorTracker.Stacktrace.new(stacktrace)
{:ok, error} = Error.new(kind, reason, stacktrace)
context = Map.merge(get_context(), given_context)

context =
if bread_crumbs = bread_crumbs(exception),
do: Map.put(context, "bread_crumbs", bread_crumbs),
else: context
breadcrumbs = get_breadcrumbs() ++ exception_breadcrumbs(exception)

if enabled?() && !ignored?(error, context) do
sanitized_context = sanitize_context(context)
{_error, occurrence} = upsert_error!(error, stacktrace, sanitized_context, reason)

{_error, occurrence} =
upsert_error!(error, stacktrace, sanitized_context, breadcrumbs, reason)

occurrence
else
:noop
Expand Down Expand Up @@ -205,6 +222,40 @@ defmodule ErrorTracker do
Process.get(:error_tracker_context, %{})
end

@doc """
Adds a breadcrumb to the current process.
The new breadcrumb will be added as the most recent entry of the breadcrumbs
list.
## Breadcrumbs limit
Breadcrumbs are a powerful tool that allows to add an infinite number of
entries. However, it is not recommended to store errors with an excessive
amount of breadcrumbs.
As they are stored as an array of strings under the hood, storing many
entries per error can lead to some delays and using extra disk space on the
database.
"""
@spec add_breadcrumb(String.t()) :: list(String.t())
def add_breadcrumb(breadcrumb) when is_binary(breadcrumb) do
current_breadcrumbs = Process.get(:error_tracker_breadcrumbs, [])
new_breadcrumbs = current_breadcrumbs ++ [breadcrumb]

Process.put(:error_tracker_breadcrumbs, new_breadcrumbs)

new_breadcrumbs
end

@doc """
Obtain the breadcrumbs of the current process.
"""
@spec get_breadcrumbs() :: list(String.t())
def get_breadcrumbs do
Process.get(:error_tracker_breadcrumbs, [])
end

defp enabled? do
!!Application.get_env(:error_tracker, :enabled, true)
end
Expand Down Expand Up @@ -237,15 +288,15 @@ defmodule ErrorTracker do
end
end

defp bread_crumbs(exception) do
defp exception_breadcrumbs(exception) do
case exception do
{_kind, exception} -> bread_crumbs(exception)
%{bread_crumbs: bread_crumbs} -> bread_crumbs
_other -> nil
{_kind, exception} -> exception_breadcrumbs(exception)
%{bread_crumbs: breadcrumbs} -> breadcrumbs
_other -> []
end
end

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

Expand All @@ -271,6 +322,7 @@ defmodule ErrorTracker do
|> Occurrence.changeset(%{
stacktrace: stacktrace,
context: context,
breadcrumbs: breadcrumbs,
reason: reason
})
|> Repo.insert!()
Expand Down
2 changes: 1 addition & 1 deletion lib/error_tracker/migration/mysql.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ defmodule ErrorTracker.Migration.MySQL do
alias ErrorTracker.Migration.SQLMigrator

@initial_version 3
@current_version 3
@current_version 4

@impl ErrorTracker.Migration
def up(opts) do
Expand Down
17 changes: 17 additions & 0 deletions lib/error_tracker/migration/mysql/v04.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
defmodule ErrorTracker.Migration.MySQL.V04 do
@moduledoc false

use Ecto.Migration

def up(_opts) do
alter table(:error_tracker_occurrences) do
add :breadcrumbs, :json, null: true
end
end

def down(_opts) do
alter table(:error_tracker_occurrences) do
remove :breadcrumbs
end
end
end
2 changes: 1 addition & 1 deletion lib/error_tracker/migration/postgres.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ defmodule ErrorTracker.Migration.Postgres do
alias ErrorTracker.Migration.SQLMigrator

@initial_version 1
@current_version 3
@current_version 4
@default_prefix "public"

@impl ErrorTracker.Migration
Expand Down
17 changes: 17 additions & 0 deletions lib/error_tracker/migration/postgres/v04.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
defmodule ErrorTracker.Migration.Postgres.V04 do
@moduledoc false

use Ecto.Migration

def up(_opts) do
alter table(:error_tracker_occurrences) do
add :breadcrumbs, {:array, :string}, default: [], null: false
end
end

def down(_opts) do
alter table(:error_tracker_occurrences) do
remove :breadcrumbs
end
end
end
2 changes: 1 addition & 1 deletion lib/error_tracker/migration/sqlite.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ defmodule ErrorTracker.Migration.SQLite do
alias ErrorTracker.Migration.SQLMigrator

@initial_version 2
@current_version 3
@current_version 4

@impl ErrorTracker.Migration
def up(opts) do
Expand Down
17 changes: 17 additions & 0 deletions lib/error_tracker/migration/sqlite/v04.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
defmodule ErrorTracker.Migration.SQLite.V04 do
@moduledoc false

use Ecto.Migration

def up(_opts) do
alter table(:error_tracker_occurrences) do
add :breadcrumbs, {:array, :string}, default: [], null: false
end
end

def down(_opts) do
alter table(:error_tracker_occurrences) do
remove :breadcrumbs
end
end
end
6 changes: 4 additions & 2 deletions lib/error_tracker/schemas/occurrence.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ defmodule ErrorTracker.Occurrence do
@type t :: %__MODULE__{}

schema "error_tracker_occurrences" do
field :context, :map
field :reason, :string

field :context, :map
field :breadcrumbs, {:array, :string}

embeds_one :stacktrace, ErrorTracker.Stacktrace
belongs_to :error, ErrorTracker.Error

Expand All @@ -27,7 +29,7 @@ defmodule ErrorTracker.Occurrence do
@doc false
def changeset(occurrence, attrs) do
occurrence
|> cast(attrs, [:context, :reason])
|> cast(attrs, [:context, :reason, :breadcrumbs])
|> maybe_put_stacktrace()
|> validate_required([:reason, :stacktrace])
|> validate_context()
Expand Down
19 changes: 19 additions & 0 deletions lib/error_tracker/web/live/show.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,25 @@
<%= @error.source_line %></pre>
</.section>

<.section :if={@occurrence.breadcrumbs != []} title="Bread crumbs">
<div class="relative overflow-x-auto shadow-md sm:rounded-lg ring-1 ring-gray-900">
<table class="w-full text-sm text-gray-400 table-fixed">
<tr
:for={
{breadcrumb, index} <-
@occurrence.breadcrumbs |> Enum.reverse() |> Enum.with_index()
}
class="border-b bg-gray-400/10 border-gray-900 last:border-b-0"
>
<td class="w-11 pl-2 py-4 font-medium text-white relative text-right">
<%= length(@occurrence.breadcrumbs) - index %>.
</td>
<td class="px-2 py-4 font-medium text-white relative"><%= breadcrumb %></td>
</tr>
</table>
</div>
</.section>

<.section :if={@occurrence.stacktrace.lines != []} title="Stacktrace">
<div class="p-4 bg-gray-300/10 border border-gray-900 rounded-lg">
<div class="w-full mb-4">
Expand Down
16 changes: 16 additions & 0 deletions priv/static/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -885,6 +885,10 @@ select {
width: 2.5rem;
}

.w-11 {
width: 2.75rem;
}

.w-28 {
width: 7rem;
}
Expand Down Expand Up @@ -1129,6 +1133,10 @@ select {
padding-bottom: 11.5px;
}

.pl-2 {
padding-left: 0.5rem;
}

.pr-2 {
padding-right: 0.5rem;
}
Expand All @@ -1145,6 +1153,10 @@ select {
text-align: center;
}

.text-right {
text-align: right;
}

.align-top {
vertical-align: top;
}
Expand Down Expand Up @@ -1318,6 +1330,10 @@ select {
border-radius: 4px;
}

.last\:border-b-0:last-child {
border-bottom-width: 0px;
}

.last-of-type\:border-b-0:last-of-type {
border-bottom-width: 0px;
}
Expand Down
Loading

0 comments on commit cc6aacb

Please sign in to comment.