Skip to content

Integrate breadcrumbs #107

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Nov 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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