Skip to content
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
26 changes: 26 additions & 0 deletions dev.exs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,30 @@ defmodule ErrorTrackerDevWeb.Endpoint do
def maybe_exception(conn, _), do: conn
end

defmodule ErrorTrackerDev.Telemetry do
require Logger

def start do
:telemetry.attach_many(
"error-tracker-events",
[
[:error_tracker, :error, :new],
[:error_tracker, :error, :resolved],
[:error_tracker, :error, :unresolved],
[:error_tracker, :occurrence, :new]
],
&__MODULE__.handle_event/4,
[]
)

Logger.info("Telemtry attached")
end

def handle_event(event, measure, metadata, _opts) do
dbg([event, measure, metadata])
end
end

defmodule Migration0 do
use Ecto.Migration

Expand All @@ -165,6 +189,8 @@ Task.async(fn ->
ErrorTrackerDevWeb.Endpoint
]

ErrorTrackerDev.Telemetry.start()

{:ok, _} = Supervisor.start_link(children, strategy: :one_for_one)

# Automatically run the migrations on boot
Expand Down
8 changes: 8 additions & 0 deletions guides/Getting Started.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,11 @@ You can also use `ErrorTracker.report/3` and set some custom context that will b
ErrorTracker also provides a dashboard built with Phoenix LiveView that can be used to see and manage the recorded errors.

This is completely optional, and you can find more information about it in the `ErrorTracker.Web` module documentation.

## Notifications

We currently do not support notifications out of the box.

However, we provideo some detailed Telemetry events that you may use to implement your own notifications following your custom rules and notification channels.

If you want to take a look at the events you can attach to, take a look at `ErrorTracker.Telemetry` module documentation.
53 changes: 43 additions & 10 deletions lib/error_tracker.ex
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,11 @@ defmodule ErrorTracker do
"""
@type context :: %{String.t() => any()}

import Ecto.Query

alias ErrorTracker.Error
alias ErrorTracker.Repo
alias ErrorTracker.Telemetry

@doc """
Report an exception to be stored.
Expand Down Expand Up @@ -104,15 +107,9 @@ defmodule ErrorTracker do

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

error =
Repo.insert!(error,
on_conflict: [set: [status: :unresolved, last_occurrence_at: DateTime.utc_now()]],
conflict_target: :fingerprint
)
{_error, occurrence} = upsert_error!(error, stacktrace, context, reason)

error
|> Ecto.build_assoc(:occurrences, stacktrace: stacktrace, context: context, reason: reason)
|> Repo.insert!()
occurrence
end

@doc """
Expand All @@ -124,7 +121,10 @@ defmodule ErrorTracker do
def resolve(error = %Error{status: :unresolved}) do
changeset = Ecto.Changeset.change(error, status: :resolved)

Repo.update(changeset)
with {:ok, updated_error} <- Repo.update(changeset) do
Telemetry.resolved_error(updated_error)
{:ok, updated_error}
end
end

@doc """
Expand All @@ -133,7 +133,10 @@ defmodule ErrorTracker do
def unresolve(error = %Error{status: :resolved}) do
changeset = Ecto.Changeset.change(error, status: :unresolved)

Repo.update(changeset)
with {:ok, updated_error} <- Repo.update(changeset) do
Telemetry.unresolved_error(updated_error)
{:ok, updated_error}
end
end

@doc """
Expand Down Expand Up @@ -180,4 +183,34 @@ defmodule ErrorTracker do
{to_string(kind), to_string(other)}
end
end

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

error =
Repo.insert!(error,
on_conflict: [set: [status: :unresolved, last_occurrence_at: DateTime.utc_now()]],
conflict_target: :fingerprint
)

occurrence =
error
|> Ecto.build_assoc(:occurrences, stacktrace: stacktrace, context: context, reason: reason)
|> Repo.insert!()

# If the error existed and was marked as resolved before this exception,
# sent a Telemetry event
# If it is a new error, sent a Telemetry event
case existing_status do
:resolved -> Telemetry.unresolved_error(error)
:unresolved -> :noop
nil -> Telemetry.new_error(error)
end

# Always send a new occurrence Telemetry event
Telemetry.new_occurrence(occurrence)

{error, occurrence}
end
end
4 changes: 4 additions & 0 deletions lib/error_tracker/repo.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ defmodule ErrorTracker.Repo do
dispatch(:get!, [queryable, id], opts)
end

def one(queryable, opts \\ []) do
dispatch(:one, [queryable], opts)
end

def all(queryable, opts \\ []) do
dispatch(:all, [queryable], opts)
end
Expand Down
69 changes: 69 additions & 0 deletions lib/error_tracker/telemetry.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
defmodule ErrorTracker.Telemetry do
@moduledoc """
Telemetry events of ErrorTracker.

ErrorTracker emits some events to allow third parties to receive information
of errors and occurrences stored.

### Error events

Those occur during the life cycle of an error:

* `[:error_tracker, :error, :new]`: is emitted when a new error is stored and
no previous occurrences were known.

* `[:error_tracker, :error, :resolved]`: is emitted when a new error is marked
as resolved on the UI.

* `[:error_tracker, :error, :unresolved]`: is emitted when a new error is
marked as unresolved on the UI or a new occurrence is registered, moving the
error to the unresolved state.

### Occurrence events

There is only one event emitted for occurrences:

* `[:error_tracker, :occurrence, :new]`: is emitted when a new occurrence is
stored.

### Measures and metadata

Each event is emitted with some measures and metadata, which can be used to
receive information without having to query the database again:

| event | measures | metadata |
| --------------------------------------- | -------------- | ------------- |
| `[:error_tracker, :error, :new]` | `:system_time` | `:error` |
| `[:error_tracker, :error, :unresolved]` | `:system_time` | `:error` |
| `[:error_tracker, :error, :resolved]` | `:system_time` | `:error` |
| `[:error_tracker, :occurrence, :new]` | `:system_time` | `:occurrence` |
"""

@doc false
def new_error(error) do
measurements = %{system_time: System.system_time()}
metadata = %{error: error}
:telemetry.execute([:error_tracker, :error, :new], measurements, metadata)
end

@doc false
def unresolved_error(error) do
measurements = %{system_time: System.system_time()}
metadata = %{error: error}
:telemetry.execute([:error_tracker, :error, :unresolved], measurements, metadata)
end

@doc false
def resolved_error(error) do
measurements = %{system_time: System.system_time()}
metadata = %{error: error}
:telemetry.execute([:error_tracker, :error, :resolved], measurements, metadata)
end

@doc false
def new_occurrence(occurrence) do
measurements = %{system_time: System.system_time()}
metadata = %{occurrence: occurrence}
:telemetry.execute([:error_tracker, :occurrence, :new], measurements, metadata)
end
end