From 05e15864b5cac6c88bc56439ba28b5588700b12d Mon Sep 17 00:00:00 2001 From: Jon Lauridsen Date: Fri, 10 Apr 2026 15:25:34 +0200 Subject: [PATCH 1/3] Include occurrence in new error telemetry event The :new error event now carries the occurrence in its metadata so consumers can inspect context (e.g. to distinguish genuinely new errors from noise) without a separate database query. --- lib/error_tracker.ex | 2 +- lib/error_tracker/telemetry.ex | 4 ++-- test/error_tracker/telemetry_test.exs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/error_tracker.ex b/lib/error_tracker.ex index d88ced0..e465ad4 100644 --- a/lib/error_tracker.ex +++ b/lib/error_tracker.ex @@ -377,7 +377,7 @@ defmodule ErrorTracker do case existing_status do :resolved -> Telemetry.unresolved_error(error) :unresolved -> :noop - nil -> Telemetry.new_error(error) + nil -> Telemetry.new_error(error, occurrence) end Telemetry.new_occurrence(occurrence, muted) diff --git a/lib/error_tracker/telemetry.ex b/lib/error_tracker/telemetry.ex index 6246068..0bfeaaa 100644 --- a/lib/error_tracker/telemetry.ex +++ b/lib/error_tracker/telemetry.ex @@ -46,9 +46,9 @@ defmodule ErrorTracker.Telemetry do """ @doc false - def new_error(%ErrorTracker.Error{} = error) do + def new_error(%ErrorTracker.Error{} = error, %ErrorTracker.Occurrence{} = occurrence) do measurements = %{system_time: System.system_time()} - metadata = %{error: error} + metadata = %{error: error, occurrence: occurrence} :telemetry.execute([:error_tracker, :error, :new], measurements, metadata) end diff --git a/test/error_tracker/telemetry_test.exs b/test/error_tracker/telemetry_test.exs index 07c0405..d4cb719 100644 --- a/test/error_tracker/telemetry_test.exs +++ b/test/error_tracker/telemetry_test.exs @@ -19,8 +19,8 @@ defmodule ErrorTracker.TelemetryTest do end # Since the error is new, both the new error and new occurrence events will be emitted - %Occurrence{error: error = %Error{}} = ErrorTracker.report(exception, stacktrace) - assert_receive {:telemetry_event, [:error_tracker, :error, :new], _, %{error: %Error{}}} + occurrence = %Occurrence{error: error = %Error{}} = ErrorTracker.report(exception, stacktrace) + assert_receive {:telemetry_event, [:error_tracker, :error, :new], _, %{error: %Error{}, occurrence: ^occurrence}} assert_receive {:telemetry_event, [:error_tracker, :occurrence, :new], _, %{occurrence: %Occurrence{}, muted: false}} From 1e6b8d19039cb3e386d0e1fc216ec3c1af18ef5d Mon Sep 17 00:00:00 2001 From: Jon Lauridsen Date: Fri, 10 Apr 2026 15:26:14 +0200 Subject: [PATCH 2/3] Include occurrence in unresolved error telemetry events When a previously-resolved error re-occurs, the :unresolved event now carries the triggering occurrence so consumers can inspect its context (e.g. to decide whether to re-alert). Manual unresolves (via the UI) emit occurrence: nil since there is no new occurrence involved. --- lib/error_tracker.ex | 2 +- lib/error_tracker/telemetry.ex | 9 ++++++++- test/error_tracker/telemetry_test.exs | 23 ++++++++++++++++++++++- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/lib/error_tracker.ex b/lib/error_tracker.ex index e465ad4..ba43ec0 100644 --- a/lib/error_tracker.ex +++ b/lib/error_tracker.ex @@ -375,7 +375,7 @@ defmodule ErrorTracker do # sent a Telemetry event # If it is a new error, sent a Telemetry event case existing_status do - :resolved -> Telemetry.unresolved_error(error) + :resolved -> Telemetry.previously_resolved_error(error, occurrence) :unresolved -> :noop nil -> Telemetry.new_error(error, occurrence) end diff --git a/lib/error_tracker/telemetry.ex b/lib/error_tracker/telemetry.ex index 0bfeaaa..c5d14ea 100644 --- a/lib/error_tracker/telemetry.ex +++ b/lib/error_tracker/telemetry.ex @@ -55,7 +55,14 @@ defmodule ErrorTracker.Telemetry do @doc false def unresolved_error(%ErrorTracker.Error{} = error) do measurements = %{system_time: System.system_time()} - metadata = %{error: error} + metadata = %{error: error, occurrence: nil} + :telemetry.execute([:error_tracker, :error, :unresolved], measurements, metadata) + end + + @doc false + def previously_resolved_error(%ErrorTracker.Error{} = error, %ErrorTracker.Occurrence{} = occurrence) do + measurements = %{system_time: System.system_time()} + metadata = %{error: error, occurrence: occurrence} :telemetry.execute([:error_tracker, :error, :unresolved], measurements, metadata) end diff --git a/test/error_tracker/telemetry_test.exs b/test/error_tracker/telemetry_test.exs index d4cb719..86796bb 100644 --- a/test/error_tracker/telemetry_test.exs +++ b/test/error_tracker/telemetry_test.exs @@ -49,6 +49,27 @@ defmodule ErrorTracker.TelemetryTest do # The unresolved event will be emitted {:ok, _unresolved} = ErrorTracker.unresolve(resolved) - assert_receive {:telemetry_event, [:error_tracker, :error, :unresolved], _, %{error: %Error{}}} + assert_receive {:telemetry_event, [:error_tracker, :error, :unresolved], _, %{error: %Error{}, occurrence: nil}} + end + + test "events are emitted for previously resolved errors" do + {exception, stacktrace} = + try do + raise "This error was resolved but came back" + rescue + e -> {e, __STACKTRACE__} + end + + %Occurrence{error: error = %Error{}} = ErrorTracker.report(exception, stacktrace) + + ErrorTracker.resolve(error) + + occurrence = ErrorTracker.report(exception, stacktrace) + + assert_receive {:telemetry_event, [:error_tracker, :error, :unresolved], _, + %{ + error: %Error{reason: "This error was resolved but came back"}, + occurrence: ^occurrence + }} end end From 1b70e4c3eb2a2e9ff9e749170ef1c3a6530e7a91 Mon Sep 17 00:00:00 2001 From: Jon Lauridsen Date: Fri, 10 Apr 2026 15:26:40 +0200 Subject: [PATCH 3/3] Update telemetry documentation for occurrence metadata Reflect that :new and :unresolved error events now include the occurrence in their metadata, and document the nullable semantics for manual unresolves. --- lib/error_tracker/telemetry.ex | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/error_tracker/telemetry.ex b/lib/error_tracker/telemetry.ex index c5d14ea..94d0e60 100644 --- a/lib/error_tracker/telemetry.ex +++ b/lib/error_tracker/telemetry.ex @@ -31,17 +31,20 @@ defmodule ErrorTracker.Telemetry do 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`, `:error`, `:muted` | + | event | measures | metadata | + | --------------------------------------- | -------------- | ----------------------------------------------- | + | `[:error_tracker, :error, :new]` | `:system_time` | `:error`, `:occurrence` | + | `[:error_tracker, :error, :unresolved]` | `:system_time` | `:error`, `:occurrence` (nullable) | + | `[:error_tracker, :error, :resolved]` | `:system_time` | `:error` | + | `[:error_tracker, :occurrence, :new]` | `:system_time` | `:occurrence`, `:error`, `:muted` | The metadata keys contain the following data: * `:error` - An `%ErrorTracker.Error{}` struct representing the error. * `:occurrence` - An `%ErrorTracker.Occurrence{}` struct representing the occurrence. + For `:new` error events this is the first occurrence. For `:unresolved` events this + is the occurrence that triggered the state change, or `nil` when the error was manually + unresolved from the UI. * `:muted` - A boolean indicating whether the error is muted or not. """