From ab08deaa98ce3e4344d40bb3cfeeb2c5466e2137 Mon Sep 17 00:00:00 2001 From: crbelaus Date: Thu, 13 Feb 2025 18:12:16 +0100 Subject: [PATCH 01/14] Add silenced column --- lib/error_tracker/migration/mysql.ex | 2 +- lib/error_tracker/migration/mysql/v05.ex | 17 +++++++++++++++++ lib/error_tracker/migration/postgres.ex | 2 +- lib/error_tracker/migration/postgres/v05.ex | 17 +++++++++++++++++ lib/error_tracker/migration/sqlite.ex | 2 +- lib/error_tracker/migration/sqlite/v05.ex | 17 +++++++++++++++++ 6 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 lib/error_tracker/migration/mysql/v05.ex create mode 100644 lib/error_tracker/migration/postgres/v05.ex create mode 100644 lib/error_tracker/migration/sqlite/v05.ex diff --git a/lib/error_tracker/migration/mysql.ex b/lib/error_tracker/migration/mysql.ex index 0c28986..ff9efd9 100644 --- a/lib/error_tracker/migration/mysql.ex +++ b/lib/error_tracker/migration/mysql.ex @@ -7,7 +7,7 @@ defmodule ErrorTracker.Migration.MySQL do alias ErrorTracker.Migration.SQLMigrator @initial_version 3 - @current_version 4 + @current_version 5 @impl ErrorTracker.Migration def up(opts) do diff --git a/lib/error_tracker/migration/mysql/v05.ex b/lib/error_tracker/migration/mysql/v05.ex new file mode 100644 index 0000000..db54d13 --- /dev/null +++ b/lib/error_tracker/migration/mysql/v05.ex @@ -0,0 +1,17 @@ +defmodule ErrorTracker.Migration.MySQL.V05 do + @moduledoc false + + use Ecto.Migration + + def up(_opts) do + alter table(:error_tracker_errors) do + add :muted, :boolean, default: false, null: false + end + end + + def down(_opts) do + alter table(:error_tracker_errors) do + remove :muted + end + end +end diff --git a/lib/error_tracker/migration/postgres.ex b/lib/error_tracker/migration/postgres.ex index ccb7014..f0a6a38 100644 --- a/lib/error_tracker/migration/postgres.ex +++ b/lib/error_tracker/migration/postgres.ex @@ -7,7 +7,7 @@ defmodule ErrorTracker.Migration.Postgres do alias ErrorTracker.Migration.SQLMigrator @initial_version 1 - @current_version 4 + @current_version 5 @default_prefix "public" @impl ErrorTracker.Migration diff --git a/lib/error_tracker/migration/postgres/v05.ex b/lib/error_tracker/migration/postgres/v05.ex new file mode 100644 index 0000000..0b85543 --- /dev/null +++ b/lib/error_tracker/migration/postgres/v05.ex @@ -0,0 +1,17 @@ +defmodule ErrorTracker.Migration.Postgres.V05 do + @moduledoc false + + use Ecto.Migration + + def up(%{prefix: prefix}) do + alter table(:error_tracker_errors, prefix: prefix) do + add :muted, :boolean, default: false, null: false + end + end + + def down(%{prefix: prefix}) do + alter table(:error_tracker_errors, prefix: prefix) do + remove :muted + end + end +end diff --git a/lib/error_tracker/migration/sqlite.ex b/lib/error_tracker/migration/sqlite.ex index 383446a..7d51024 100644 --- a/lib/error_tracker/migration/sqlite.ex +++ b/lib/error_tracker/migration/sqlite.ex @@ -7,7 +7,7 @@ defmodule ErrorTracker.Migration.SQLite do alias ErrorTracker.Migration.SQLMigrator @initial_version 2 - @current_version 4 + @current_version 5 @impl ErrorTracker.Migration def up(opts) do diff --git a/lib/error_tracker/migration/sqlite/v05.ex b/lib/error_tracker/migration/sqlite/v05.ex new file mode 100644 index 0000000..c740ca0 --- /dev/null +++ b/lib/error_tracker/migration/sqlite/v05.ex @@ -0,0 +1,17 @@ +defmodule ErrorTracker.Migration.SQLite.V05 do + @moduledoc false + + use Ecto.Migration + + def up(_opts) do + alter table(:error_tracker_errors) do + add :muted, :boolean, default: false, null: false + end + end + + def down(_opts) do + alter table(:error_tracker_errors) do + remove :muted + end + end +end From 5115547b59a69a95a303d7608cbc6c50e8754d6f Mon Sep 17 00:00:00 2001 From: crbelaus Date: Thu, 13 Feb 2025 18:30:06 +0100 Subject: [PATCH 02/14] Add muted field to errors --- lib/error_tracker/schemas/error.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/error_tracker/schemas/error.ex b/lib/error_tracker/schemas/error.ex index de7efd9..d2d8d4c 100644 --- a/lib/error_tracker/schemas/error.ex +++ b/lib/error_tracker/schemas/error.ex @@ -22,6 +22,7 @@ defmodule ErrorTracker.Error do field :status, Ecto.Enum, values: [:resolved, :unresolved], default: :unresolved field :fingerprint, :binary field :last_occurrence_at, :utc_datetime_usec + field :muted, :boolean has_many :occurrences, ErrorTracker.Occurrence From de5a90ce42dc09429265d7e45e463553d666633f Mon Sep 17 00:00:00 2001 From: crbelaus Date: Thu, 13 Feb 2025 18:30:06 +0100 Subject: [PATCH 03/14] Mute and unmute errors --- lib/error_tracker.ex | 20 +++++++++++++++++++ lib/error_tracker/web/live/dashboard.ex | 16 +++++++++++++++ .../web/live/dashboard.html.heex | 8 ++++++++ lib/error_tracker/web/live/show.ex | 14 +++++++++++++ lib/error_tracker/web/live/show.html.heex | 8 ++++++++ 5 files changed, 66 insertions(+) diff --git a/lib/error_tracker.ex b/lib/error_tracker.ex index 43a1fce..153963d 100644 --- a/lib/error_tracker.ex +++ b/lib/error_tracker.ex @@ -179,6 +179,26 @@ defmodule ErrorTracker do end end + @doc """ + Mutes the error so next ocurrences won't send telemetry events. + """ + @spec mute(Error.t()) :: {:ok, Error.t()} | {:error, Ecto.Changeset.t()} + def mute(error = %Error{muted: false}) do + changeset = Ecto.Changeset.change(error, muted: true) + + Repo.update(changeset) + end + + @doc """ + Unmutes the error so next ocurrences will send telemetry events. + """ + @spec unmute(Error.t()) :: {:ok, Error.t()} | {:error, Ecto.Changeset.t()} + def unmute(error = %Error{muted: true}) do + changeset = Ecto.Changeset.change(error, muted: false) + + Repo.update(changeset) + end + @doc """ Sets the current process context. diff --git a/lib/error_tracker/web/live/dashboard.ex b/lib/error_tracker/web/live/dashboard.ex index de46f28..da66a31 100644 --- a/lib/error_tracker/web/live/dashboard.ex +++ b/lib/error_tracker/web/live/dashboard.ex @@ -57,6 +57,22 @@ defmodule ErrorTracker.Web.Live.Dashboard do {:noreply, paginate_errors(socket)} end + @impl Phoenix.LiveView + def handle_event("mute", %{"error_id" => id}, socket) do + error = Repo.get(Error, id) + {:ok, _muted} = ErrorTracker.mute(error) + + {:noreply, paginate_errors(socket)} + end + + @impl Phoenix.LiveView + def handle_event("unmute", %{"error_id" => id}, socket) do + error = Repo.get(Error, id) + {:ok, _unmuted} = ErrorTracker.unmute(error) + + {:noreply, paginate_errors(socket)} + end + defp paginate_errors(socket) do %{page: page, search: search} = socket.assigns offset = (page - 1) * @per_page diff --git a/lib/error_tracker/web/live/dashboard.html.heex b/lib/error_tracker/web/live/dashboard.html.heex index 4c81d2f..71875f9 100644 --- a/lib/error_tracker/web/live/dashboard.html.heex +++ b/lib/error_tracker/web/live/dashboard.html.heex @@ -102,6 +102,14 @@ > Unresolve + + <.button :if={!error.muted} phx-click="mute" type="link" phx-value-error_id={error.id}> + Mute + + + <.button :if={error.muted} phx-click="unmute" type="link" phx-value-error_id={error.id}> + Unmute + diff --git a/lib/error_tracker/web/live/show.ex b/lib/error_tracker/web/live/show.ex index 0982219..c39d347 100644 --- a/lib/error_tracker/web/live/show.ex +++ b/lib/error_tracker/web/live/show.ex @@ -69,6 +69,20 @@ defmodule ErrorTracker.Web.Live.Show do {:noreply, assign(socket, :error, updated_error)} end + @impl Phoenix.LiveView + def handle_event("mute", _params, socket) do + {:ok, updated_error} = ErrorTracker.mute(socket.assigns.error) + + {:noreply, assign(socket, :error, updated_error)} + end + + @impl Phoenix.LiveView + def handle_event("unmute", _params, socket) do + {:ok, updated_error} = ErrorTracker.unmute(socket.assigns.error) + + {:noreply, assign(socket, :error, updated_error)} + end + defp load_related_occurrences(socket) do current_occurrence = socket.assigns.occurrence base_query = Ecto.assoc(socket.assigns.error, :occurrences) diff --git a/lib/error_tracker/web/live/show.html.heex b/lib/error_tracker/web/live/show.html.heex index 765c8b4..5b9ac95 100644 --- a/lib/error_tracker/web/live/show.html.heex +++ b/lib/error_tracker/web/live/show.html.heex @@ -124,6 +124,14 @@ <.button :if={@error.status == :resolved} phx-click="unresolve"> Mark as unresolved + + <.button :if={!@error.muted} phx-click="mute" type="link"> + Mute + + + <.button :if={@error.muted} phx-click="unmute" type="link"> + Unmute + From 4cd56fcfdc6ed42b98637fc8843c07dac7f2b106 Mon Sep 17 00:00:00 2001 From: crbelaus Date: Thu, 13 Feb 2025 18:54:08 +0100 Subject: [PATCH 04/14] Ensure fresh ErrorTracker compilation --- dev.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev.exs b/dev.exs index a09f496..af25241 100644 --- a/dev.exs +++ b/dev.exs @@ -4,7 +4,7 @@ # Mix.install([ {:ecto_sqlite3, ">= 0.0.0"}, - {:error_tracker, path: "."}, + {:error_tracker, path: ".", force: true}, {:phoenix_playground, "~> 0.1.7"} ]) From ab24eecc21bb97f646073d22119c7a0701214b30 Mon Sep 17 00:00:00 2001 From: crbelaus Date: Thu, 13 Feb 2025 18:54:08 +0100 Subject: [PATCH 05/14] Icon component --- .../web/components/core_components.ex | 37 +++++++++++++++++++ priv/static/app.css | 12 ++++++ 2 files changed, 49 insertions(+) diff --git a/lib/error_tracker/web/components/core_components.ex b/lib/error_tracker/web/components/core_components.ex index 99af7a6..4943c16 100644 --- a/lib/error_tracker/web/components/core_components.ex +++ b/lib/error_tracker/web/components/core_components.ex @@ -130,4 +130,41 @@ defmodule ErrorTracker.Web.CoreComponents do """ end + + attr :name, :string, values: ~w[bell bell-slash] + + def icon(assigns = %{name: "bell"}) do + ~H""" + + + + """ + end + + def icon(assigns = %{name: "bell-slash"}) do + ~H""" + + + + + """ + end end diff --git a/priv/static/app.css b/priv/static/app.css index 2574a39..e9294f1 100644 --- a/priv/static/app.css +++ b/priv/static/app.css @@ -873,6 +873,10 @@ select { display: none; } +.\!h-4 { + height: 1rem !important; +} + .h-10 { height: 2.5rem; } @@ -881,6 +885,10 @@ select { height: 1.25rem; } +.\!w-4 { + width: 1rem !important; +} + .w-10 { width: 2.5rem; } @@ -945,6 +953,10 @@ select { gap: 0.5rem; } +.gap-y-4 { + row-gap: 1rem; +} + .space-y-8 > :not([hidden]) ~ :not([hidden]) { --tw-space-y-reverse: 0; margin-top: calc(2rem * calc(1 - var(--tw-space-y-reverse))); From daf557a9381bb278b1388b7d8340344d06fddaf7 Mon Sep 17 00:00:00 2001 From: crbelaus Date: Thu, 13 Feb 2025 18:54:08 +0100 Subject: [PATCH 06/14] Mute and unmnute styles --- .../web/live/dashboard.html.heex | 47 +++++++++++-------- lib/error_tracker/web/live/show.html.heex | 32 +++++++------ 2 files changed, 44 insertions(+), 35 deletions(-) diff --git a/lib/error_tracker/web/live/dashboard.html.heex b/lib/error_tracker/web/live/dashboard.html.heex index 71875f9..77474cc 100644 --- a/lib/error_tracker/web/live/dashboard.html.heex +++ b/lib/error_tracker/web/live/dashboard.html.heex @@ -87,29 +87,36 @@ <.badge :if={error.status == :unresolved} color={:red}>Unresolved - <.button - :if={error.status == :unresolved} - phx-click="resolve" - phx-value-error_id={error.id} - > - Resolve - +
+ <.button + :if={error.status == :unresolved} + phx-click="resolve" + phx-value-error_id={error.id} + > + Resolve + - <.button - :if={error.status == :resolved} - phx-click="unresolve" - phx-value-error_id={error.id} - > - Unresolve - + <.button + :if={error.status == :resolved} + phx-click="unresolve" + phx-value-error_id={error.id} + > + Unresolve + - <.button :if={!error.muted} phx-click="mute" type="link" phx-value-error_id={error.id}> - Mute - + <.button :if={!error.muted} phx-click="mute" type="link" phx-value-error_id={error.id}> + <.icon name="bell-slash" /> Mute + - <.button :if={error.muted} phx-click="unmute" type="link" phx-value-error_id={error.id}> - Unmute - + <.button + :if={error.muted} + phx-click="unmute" + type="link" + phx-value-error_id={error.id} + > + <.icon name="bell" /> Unmute + +
diff --git a/lib/error_tracker/web/live/show.html.heex b/lib/error_tracker/web/live/show.html.heex index 5b9ac95..c72ddb4 100644 --- a/lib/error_tracker/web/live/show.html.heex +++ b/lib/error_tracker/web/live/show.html.heex @@ -117,21 +117,23 @@ <.section> - <.button :if={@error.status == :unresolved} phx-click="resolve"> - Mark as resolved - - - <.button :if={@error.status == :resolved} phx-click="unresolve"> - Mark as unresolved - - - <.button :if={!@error.muted} phx-click="mute" type="link"> - Mute - - - <.button :if={@error.muted} phx-click="unmute" type="link"> - Unmute - +
+ <.button :if={@error.status == :unresolved} phx-click="resolve"> + Mark as resolved + + + <.button :if={@error.status == :resolved} phx-click="unresolve"> + Mark as unresolved + + + <.button :if={!@error.muted} phx-click="mute" type="link"> + <.icon name="bell-slash" /> Mute + + + <.button :if={@error.muted} phx-click="unmute" type="link"> + <.icon name="bell" /> Unmute + +
From b04d48af921c46070ee70f8425a440e45358ab66 Mon Sep 17 00:00:00 2001 From: crbelaus Date: Thu, 13 Feb 2025 19:07:11 +0100 Subject: [PATCH 07/14] Don't send telemetry events for occurrences if muted --- lib/error_tracker.ex | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/error_tracker.ex b/lib/error_tracker.ex index 153963d..cdcbe72 100644 --- a/lib/error_tracker.ex +++ b/lib/error_tracker.ex @@ -320,8 +320,12 @@ defmodule ErrorTracker do end defp upsert_error!(error, stacktrace, context, breadcrumbs, reason) do - existing_status = - Repo.one(from e in Error, where: [fingerprint: ^error.fingerprint], select: e.status) + {existing_status, muted} = + Repo.one( + from e in Error, + where: [fingerprint: ^error.fingerprint], + select: {e.status, e.muted} + ) {:ok, {error, occurrence}} = Repo.transaction(fn -> @@ -362,8 +366,8 @@ defmodule ErrorTracker do nil -> Telemetry.new_error(error) end - # Always send a new occurrence Telemetry event - Telemetry.new_occurrence(occurrence) + # Send telemetry for new occurrences if not muted + if !muted, do: Telemetry.new_occurrence(occurrence) {error, occurrence} end From 633a7f6e2c4fc0c6390aa829bc030dfcabc6dcaa Mon Sep 17 00:00:00 2001 From: crbelaus Date: Fri, 14 Feb 2025 18:51:24 +0100 Subject: [PATCH 08/14] Load the error into the returned occurrence --- lib/error_tracker.ex | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/lib/error_tracker.ex b/lib/error_tracker.ex index cdcbe72..4d9feda 100644 --- a/lib/error_tracker.ex +++ b/lib/error_tracker.ex @@ -141,10 +141,7 @@ defmodule ErrorTracker do if enabled?() && !ignored?(error, context) do sanitized_context = sanitize_context(context) - {_error, occurrence} = - upsert_error!(error, stacktrace, sanitized_context, breadcrumbs, reason) - - occurrence + upsert_error!(error, stacktrace, sanitized_context, breadcrumbs, reason) else :noop end @@ -183,7 +180,7 @@ defmodule ErrorTracker do Mutes the error so next ocurrences won't send telemetry events. """ @spec mute(Error.t()) :: {:ok, Error.t()} | {:error, Ecto.Changeset.t()} - def mute(error = %Error{muted: false}) do + def mute(error = %Error{}) do changeset = Ecto.Changeset.change(error, muted: true) Repo.update(changeset) @@ -193,7 +190,7 @@ defmodule ErrorTracker do Unmutes the error so next ocurrences will send telemetry events. """ @spec unmute(Error.t()) :: {:ok, Error.t()} | {:error, Ecto.Changeset.t()} - def unmute(error = %Error{muted: true}) do + def unmute(error = %Error{}) do changeset = Ecto.Changeset.change(error, muted: false) Repo.update(changeset) @@ -320,12 +317,16 @@ defmodule ErrorTracker do end defp upsert_error!(error, stacktrace, context, breadcrumbs, reason) do + status_and_muted_query = + from e in Error, + where: [fingerprint: ^error.fingerprint], + select: {e.status, e.muted} + {existing_status, muted} = - Repo.one( - from e in Error, - where: [fingerprint: ^error.fingerprint], - select: {e.status, e.muted} - ) + case Repo.one(status_and_muted_query) do + {existing_status, muted} -> {existing_status, muted} + nil -> {nil, nil} + end {:ok, {error, occurrence}} = Repo.transaction(fn -> @@ -369,6 +370,6 @@ defmodule ErrorTracker do # Send telemetry for new occurrences if not muted if !muted, do: Telemetry.new_occurrence(occurrence) - {error, occurrence} + %Occurrence{occurrence | error: error} end end From 248f64d58df238d8007acdc99a052f794717ee1f Mon Sep 17 00:00:00 2001 From: crbelaus Date: Fri, 14 Feb 2025 18:51:24 +0100 Subject: [PATCH 09/14] Test telemetry for muted errors --- test/error_tracker/telemetry_test.exs | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/test/error_tracker/telemetry_test.exs b/test/error_tracker/telemetry_test.exs index ce1cd45..11a6ac7 100644 --- a/test/error_tracker/telemetry_test.exs +++ b/test/error_tracker/telemetry_test.exs @@ -11,22 +11,36 @@ defmodule ErrorTracker.TelemetryTest do end test "events are emitted for new errors" do + {exception, stacktrace} = + try do + raise "This is a test" + rescue + e -> {e, __STACKTRACE__} + end + # Since the error is new, both the new error and new occurrence events will be emitted - report_error(fn -> raise "This is a test" end) + %Occurrence{error: error = %Error{}} = ErrorTracker.report(exception, stacktrace) assert_receive {:telemetry_event, [:error_tracker, :error, :new], _, %{error: %Error{}}} assert_receive {:telemetry_event, [:error_tracker, :occurrence, :new], _, %{occurrence: %Occurrence{}}} # The error is already known so the new error event won't be emitted - report_error(fn -> raise "This is a test" end) + ErrorTracker.report(exception, stacktrace) - refute_receive {:telemetry_event, [:error_tracker, :error, :new], _, - %{occurrence: %Occurrence{}}}, + refute_receive {:telemetry_event, [:error_tracker, :error, :new], _, %{error: %Error{}}}, 150 assert_receive {:telemetry_event, [:error_tracker, :occurrence, :new], _, %{occurrence: %Occurrence{}}} + + # The error is muted so the new occurrence event won't be emitted + ErrorTracker.mute(error) + ErrorTracker.report(exception, stacktrace) + + refute_receive {:telemetry_event, [:error_tracker, :occurrence, :new], _, + %{occurrence: %Occurrence{}}}, + 150 end test "events are emitted for resolved and unresolved errors" do From b9c7e66a5f5a4ad2cea2598ea7941c6a1458a718 Mon Sep 17 00:00:00 2001 From: crbelaus Date: Sat, 15 Feb 2025 07:51:32 +0100 Subject: [PATCH 10/14] Muted is false by default --- lib/error_tracker/schemas/error.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/error_tracker/schemas/error.ex b/lib/error_tracker/schemas/error.ex index d2d8d4c..2a7e3f1 100644 --- a/lib/error_tracker/schemas/error.ex +++ b/lib/error_tracker/schemas/error.ex @@ -22,7 +22,7 @@ defmodule ErrorTracker.Error do field :status, Ecto.Enum, values: [:resolved, :unresolved], default: :unresolved field :fingerprint, :binary field :last_occurrence_at, :utc_datetime_usec - field :muted, :boolean + field :muted, :boolean, default: false has_many :occurrences, ErrorTracker.Occurrence From 4e210cdfa8e476ffeb7ea23dc71ec71e6960c537 Mon Sep 17 00:00:00 2001 From: crbelaus Date: Sat, 15 Feb 2025 07:51:32 +0100 Subject: [PATCH 11/14] Update docs --- guides/Getting Started.md | 26 ++++++++++++++++++++++---- lib/error_tracker.ex | 15 +++++++++++++-- lib/error_tracker/ignorer.ex | 6 ++++++ 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/guides/Getting Started.md b/guides/Getting Started.md index 053b71b..58b9688 100644 --- a/guides/Getting Started.md +++ b/guides/Getting Started.md @@ -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: 4) + def up, do: ErrorTracker.Migration.up(version: 5) # We specify `version: 1` in `down`, to ensure we remove all migrations. def down, do: ErrorTracker.Migration.down(version: 1) @@ -152,9 +152,27 @@ environments where you may want to prune old errors that have been resolved. The `ErrorTracker.Plugins.Pruner` module provides automatic pruning functionality with a configurable interval and error age. -## Ignoring errors +## Ignoring and Muting Errors + +ErrorTracker provides two different ways to silence errors: + +### Ignoring Errors ErrorTracker tracks every error by default. In certain cases some errors may be expected or just not interesting to track. -ErrorTracker provides functionality that allows you to ignore errors based on their attributes and context. +The `ErrorTracker.Ignorer` behaviour allows you to ignore errors based on their attributes and context. + +When an error is ignored, its occurrences are not tracked at all. This is useful for expected errors that you don't want to store in your database. + +### Muting Errors + +Sometimes you may want to keep tracking error occurrences but avoid receiving notifications about them. For these cases, +ErrorTracker allows you to mute specific errors. + +When an error is muted: +- New occurrences are still tracked and stored in the database +- No telemetry events are emitted for new occurrences +- You can still see the error and its occurrences in the web UI + +This is particularly useful for noisy errors that you want to keep tracking but don't want to receive notifications about. -Take a look at the `ErrorTracker.Ignorer` behaviour for more information about how to implement your own ignorer. +You can mute and unmute errors manually through the web UI or programmatically using the `ErrorTracker.mute/1` and `ErrorTracker.unmute/1` functions. diff --git a/lib/error_tracker.ex b/lib/error_tracker.ex index 4d9feda..6a13018 100644 --- a/lib/error_tracker.ex +++ b/lib/error_tracker.ex @@ -177,7 +177,15 @@ defmodule ErrorTracker do end @doc """ - Mutes the error so next ocurrences won't send telemetry events. + Mutes the error so new occurrences won't send telemetry events. + + When an error is muted: + - New occurrences are still tracked and stored in the database + - No telemetry events are emitted for new occurrences + - You can still see the error and its occurrences in the web UI + + This is useful for noisy errors that you want to keep tracking but don't want to + receive notifications about. """ @spec mute(Error.t()) :: {:ok, Error.t()} | {:error, Ecto.Changeset.t()} def mute(error = %Error{}) do @@ -187,7 +195,10 @@ defmodule ErrorTracker do end @doc """ - Unmutes the error so next ocurrences will send telemetry events. + Unmutes the error so new occurrences will send telemetry events again. + + This reverses the effect of `mute/1`, allowing telemetry events to be emitted + for new occurrences of this error again. """ @spec unmute(Error.t()) :: {:ok, Error.t()} | {:error, Ecto.Changeset.t()} def unmute(error = %Error{}) do diff --git a/lib/error_tracker/ignorer.ex b/lib/error_tracker/ignorer.ex index 28e19b9..d0b74fb 100644 --- a/lib/error_tracker/ignorer.ex +++ b/lib/error_tracker/ignorer.ex @@ -2,6 +2,12 @@ defmodule ErrorTracker.Ignorer do @moduledoc """ Behaviour for ignoring errors. + > #### Ignoring vs muting errors {: .info} + > + > Ignoring an error keeps it from being tracked by the ErrorTracker. While this may be useful in + > certain cases, in other cases you may prefer to track the error but don't send telemetry events. + > Take a look at the `ErrorTracker.mute/1` function to see how to mute errors. + The ErrorTracker tracks every error that happens in your application. In certain cases you may want to ignore some errors and don't track them. To do so you can implement this behaviour. From 1fb42cf1f6fe0eec255e7e96a993d75484f1cb25 Mon Sep 17 00:00:00 2001 From: crbelaus Date: Sun, 2 Mar 2025 10:26:47 +0100 Subject: [PATCH 12/14] Include `muted` and `error` metadata for new occurrence telemetry Aside from the occurrence itself, the new occurrence telemetry event now also includes the error and the muted flag so users can choose wether the occurrence should be reported or not. --- lib/error_tracker.ex | 10 +++++----- lib/error_tracker/telemetry.ex | 28 ++++++++++++++++----------- test/error_tracker/telemetry_test.exs | 11 +++++------ 3 files changed, 27 insertions(+), 22 deletions(-) diff --git a/lib/error_tracker.ex b/lib/error_tracker.ex index 6a13018..fa0184a 100644 --- a/lib/error_tracker.ex +++ b/lib/error_tracker.ex @@ -336,7 +336,7 @@ defmodule ErrorTracker do {existing_status, muted} = case Repo.one(status_and_muted_query) do {existing_status, muted} -> {existing_status, muted} - nil -> {nil, nil} + nil -> {nil, false} end {:ok, {error, occurrence}} = @@ -369,6 +369,8 @@ defmodule ErrorTracker do {error, occurrence} end) + occurrence = %Occurrence{occurrence | error: error} + # 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 @@ -378,9 +380,7 @@ defmodule ErrorTracker do nil -> Telemetry.new_error(error) end - # Send telemetry for new occurrences if not muted - if !muted, do: Telemetry.new_occurrence(occurrence) - - %Occurrence{occurrence | error: error} + Telemetry.new_occurrence(occurrence, muted) + occurrence end end diff --git a/lib/error_tracker/telemetry.ex b/lib/error_tracker/telemetry.ex index 94f31e7..075b3f9 100644 --- a/lib/error_tracker/telemetry.ex +++ b/lib/error_tracker/telemetry.ex @@ -31,39 +31,45 @@ 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` | + | 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` | + + The metadata keys contain the following data: + + * `:error` - An `%ErrorTracker.Error{}` struct representing the error. + * `:occurrence` - An `%ErrorTracker.Occurrence{}` struct representing the occurrence. + * `:muted` - A boolean indicating whether the error is muted or not. """ @doc false - def new_error(error) do + def new_error(error = %ErrorTracker.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 + def unresolved_error(error = %ErrorTracker.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 + def resolved_error(error = %ErrorTracker.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 + def new_occurrence(occurrence = %ErrorTracker.Occurrence{}, muted) when is_boolean(muted) do measurements = %{system_time: System.system_time()} - metadata = %{occurrence: occurrence} + metadata = %{error: occurrence.error, occurrence: occurrence, muted: muted} :telemetry.execute([:error_tracker, :occurrence, :new], measurements, metadata) end end diff --git a/test/error_tracker/telemetry_test.exs b/test/error_tracker/telemetry_test.exs index 11a6ac7..6a760b3 100644 --- a/test/error_tracker/telemetry_test.exs +++ b/test/error_tracker/telemetry_test.exs @@ -23,7 +23,7 @@ defmodule ErrorTracker.TelemetryTest do assert_receive {:telemetry_event, [:error_tracker, :error, :new], _, %{error: %Error{}}} assert_receive {:telemetry_event, [:error_tracker, :occurrence, :new], _, - %{occurrence: %Occurrence{}}} + %{occurrence: %Occurrence{}, muted: false}} # The error is already known so the new error event won't be emitted ErrorTracker.report(exception, stacktrace) @@ -32,15 +32,14 @@ defmodule ErrorTracker.TelemetryTest do 150 assert_receive {:telemetry_event, [:error_tracker, :occurrence, :new], _, - %{occurrence: %Occurrence{}}} + %{occurrence: %Occurrence{}, muted: false}} - # The error is muted so the new occurrence event won't be emitted + # The error is muted so the new occurrence event will include the muted=true metadata ErrorTracker.mute(error) ErrorTracker.report(exception, stacktrace) - refute_receive {:telemetry_event, [:error_tracker, :occurrence, :new], _, - %{occurrence: %Occurrence{}}}, - 150 + assert_receive {:telemetry_event, [:error_tracker, :occurrence, :new], _, + %{occurrence: %Occurrence{}, muted: true}} end test "events are emitted for resolved and unresolved errors" do From b0a02abf928b6d7d73920bf3e90fa8f949c523ab Mon Sep 17 00:00:00 2001 From: crbelaus Date: Sun, 2 Mar 2025 10:30:19 +0100 Subject: [PATCH 13/14] Don't set the default muted value in Elixir --- lib/error_tracker/schemas/error.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/error_tracker/schemas/error.ex b/lib/error_tracker/schemas/error.ex index 2a7e3f1..d2d8d4c 100644 --- a/lib/error_tracker/schemas/error.ex +++ b/lib/error_tracker/schemas/error.ex @@ -22,7 +22,7 @@ defmodule ErrorTracker.Error do field :status, Ecto.Enum, values: [:resolved, :unresolved], default: :unresolved field :fingerprint, :binary field :last_occurrence_at, :utc_datetime_usec - field :muted, :boolean, default: false + field :muted, :boolean has_many :occurrences, ErrorTracker.Occurrence From a3945c1c48af8d0aa547066c5c7eb56b20388854 Mon Sep 17 00:00:00 2001 From: crbelaus Date: Sun, 2 Mar 2025 10:43:13 +0100 Subject: [PATCH 14/14] Update docs --- guides/Getting Started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/Getting Started.md b/guides/Getting Started.md index 58b9688..516cf5c 100644 --- a/guides/Getting Started.md +++ b/guides/Getting Started.md @@ -170,8 +170,8 @@ ErrorTracker allows you to mute specific errors. When an error is muted: - New occurrences are still tracked and stored in the database -- No telemetry events are emitted for new occurrences - You can still see the error and its occurrences in the web UI +- [Telemetry events](ErrorTracker.Telemetry.html) for new occurrences include the `muted: true` flag so you can ignore them as needed. This is particularly useful for noisy errors that you want to keep tracking but don't want to receive notifications about.