Skip to content

Commit 5f5d814

Browse files
authored
Mute error occurrences (#130)
Errors can now be muted from the UI. This information is included in the new occurrence telemetry events, so integrations can choose to ignore muted errors and avoid sending notifications about them.
1 parent 0849bd3 commit 5f5d814

File tree

19 files changed

+290
-55
lines changed

19 files changed

+290
-55
lines changed

dev.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
#
55
Mix.install([
66
{:ecto_sqlite3, ">= 0.0.0"},
7-
{:error_tracker, path: "."},
7+
{:error_tracker, path: ".", force: true},
88
{:phoenix_playground, "~> 0.1.7"}
99
])
1010

guides/Getting Started.md

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ Open the generated migration and call the `up` and `down` functions on `ErrorTra
5656
defmodule MyApp.Repo.Migrations.AddErrorTracker do
5757
use Ecto.Migration
5858

59-
def up, do: ErrorTracker.Migration.up(version: 4)
59+
def up, do: ErrorTracker.Migration.up(version: 5)
6060

6161
# We specify `version: 1` in `down`, to ensure we remove all migrations.
6262
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.
152152
The `ErrorTracker.Plugins.Pruner` module provides automatic pruning functionality with a configurable
153153
interval and error age.
154154

155-
## Ignoring errors
155+
## Ignoring and Muting Errors
156+
157+
ErrorTracker provides two different ways to silence errors:
158+
159+
### Ignoring Errors
156160

157161
ErrorTracker tracks every error by default. In certain cases some errors may be expected or just not interesting to track.
158-
ErrorTracker provides functionality that allows you to ignore errors based on their attributes and context.
162+
The `ErrorTracker.Ignorer` behaviour allows you to ignore errors based on their attributes and context.
163+
164+
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.
165+
166+
### Muting Errors
167+
168+
Sometimes you may want to keep tracking error occurrences but avoid receiving notifications about them. For these cases,
169+
ErrorTracker allows you to mute specific errors.
170+
171+
When an error is muted:
172+
- New occurrences are still tracked and stored in the database
173+
- You can still see the error and its occurrences in the web UI
174+
- [Telemetry events](ErrorTracker.Telemetry.html) for new occurrences include the `muted: true` flag so you can ignore them as needed.
175+
176+
This is particularly useful for noisy errors that you want to keep tracking but don't want to receive notifications about.
159177

160-
Take a look at the `ErrorTracker.Ignorer` behaviour for more information about how to implement your own ignorer.
178+
You can mute and unmute errors manually through the web UI or programmatically using the `ErrorTracker.mute/1` and `ErrorTracker.unmute/1` functions.

lib/error_tracker.ex

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -141,10 +141,7 @@ defmodule ErrorTracker do
141141
if enabled?() && !ignored?(error, context) do
142142
sanitized_context = sanitize_context(context)
143143

144-
{_error, occurrence} =
145-
upsert_error!(error, stacktrace, sanitized_context, breadcrumbs, reason)
146-
147-
occurrence
144+
upsert_error!(error, stacktrace, sanitized_context, breadcrumbs, reason)
148145
else
149146
:noop
150147
end
@@ -179,6 +176,37 @@ defmodule ErrorTracker do
179176
end
180177
end
181178

179+
@doc """
180+
Mutes the error so new occurrences won't send telemetry events.
181+
182+
When an error is muted:
183+
- New occurrences are still tracked and stored in the database
184+
- No telemetry events are emitted for new occurrences
185+
- You can still see the error and its occurrences in the web UI
186+
187+
This is useful for noisy errors that you want to keep tracking but don't want to
188+
receive notifications about.
189+
"""
190+
@spec mute(Error.t()) :: {:ok, Error.t()} | {:error, Ecto.Changeset.t()}
191+
def mute(error = %Error{}) do
192+
changeset = Ecto.Changeset.change(error, muted: true)
193+
194+
Repo.update(changeset)
195+
end
196+
197+
@doc """
198+
Unmutes the error so new occurrences will send telemetry events again.
199+
200+
This reverses the effect of `mute/1`, allowing telemetry events to be emitted
201+
for new occurrences of this error again.
202+
"""
203+
@spec unmute(Error.t()) :: {:ok, Error.t()} | {:error, Ecto.Changeset.t()}
204+
def unmute(error = %Error{}) do
205+
changeset = Ecto.Changeset.change(error, muted: false)
206+
207+
Repo.update(changeset)
208+
end
209+
182210
@doc """
183211
Sets the current process context.
184212
@@ -300,8 +328,16 @@ defmodule ErrorTracker do
300328
end
301329

302330
defp upsert_error!(error, stacktrace, context, breadcrumbs, reason) do
303-
existing_status =
304-
Repo.one(from e in Error, where: [fingerprint: ^error.fingerprint], select: e.status)
331+
status_and_muted_query =
332+
from e in Error,
333+
where: [fingerprint: ^error.fingerprint],
334+
select: {e.status, e.muted}
335+
336+
{existing_status, muted} =
337+
case Repo.one(status_and_muted_query) do
338+
{existing_status, muted} -> {existing_status, muted}
339+
nil -> {nil, false}
340+
end
305341

306342
{:ok, {error, occurrence}} =
307343
Repo.transaction(fn ->
@@ -333,6 +369,8 @@ defmodule ErrorTracker do
333369
{error, occurrence}
334370
end)
335371

372+
occurrence = %Occurrence{occurrence | error: error}
373+
336374
# If the error existed and was marked as resolved before this exception,
337375
# sent a Telemetry event
338376
# If it is a new error, sent a Telemetry event
@@ -342,9 +380,7 @@ defmodule ErrorTracker do
342380
nil -> Telemetry.new_error(error)
343381
end
344382

345-
# Always send a new occurrence Telemetry event
346-
Telemetry.new_occurrence(occurrence)
347-
348-
{error, occurrence}
383+
Telemetry.new_occurrence(occurrence, muted)
384+
occurrence
349385
end
350386
end

lib/error_tracker/ignorer.ex

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@ defmodule ErrorTracker.Ignorer do
22
@moduledoc """
33
Behaviour for ignoring errors.
44
5+
> #### Ignoring vs muting errors {: .info}
6+
>
7+
> Ignoring an error keeps it from being tracked by the ErrorTracker. While this may be useful in
8+
> certain cases, in other cases you may prefer to track the error but don't send telemetry events.
9+
> Take a look at the `ErrorTracker.mute/1` function to see how to mute errors.
10+
511
The ErrorTracker tracks every error that happens in your application. In certain cases you may
612
want to ignore some errors and don't track them. To do so you can implement this behaviour.
713

lib/error_tracker/migration/mysql.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ defmodule ErrorTracker.Migration.MySQL do
77
alias ErrorTracker.Migration.SQLMigrator
88

99
@initial_version 3
10-
@current_version 4
10+
@current_version 5
1111

1212
@impl ErrorTracker.Migration
1313
def up(opts) do
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
defmodule ErrorTracker.Migration.MySQL.V05 do
2+
@moduledoc false
3+
4+
use Ecto.Migration
5+
6+
def up(_opts) do
7+
alter table(:error_tracker_errors) do
8+
add :muted, :boolean, default: false, null: false
9+
end
10+
end
11+
12+
def down(_opts) do
13+
alter table(:error_tracker_errors) do
14+
remove :muted
15+
end
16+
end
17+
end

lib/error_tracker/migration/postgres.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ defmodule ErrorTracker.Migration.Postgres do
77
alias ErrorTracker.Migration.SQLMigrator
88

99
@initial_version 1
10-
@current_version 4
10+
@current_version 5
1111
@default_prefix "public"
1212

1313
@impl ErrorTracker.Migration
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
defmodule ErrorTracker.Migration.Postgres.V05 do
2+
@moduledoc false
3+
4+
use Ecto.Migration
5+
6+
def up(%{prefix: prefix}) do
7+
alter table(:error_tracker_errors, prefix: prefix) do
8+
add :muted, :boolean, default: false, null: false
9+
end
10+
end
11+
12+
def down(%{prefix: prefix}) do
13+
alter table(:error_tracker_errors, prefix: prefix) do
14+
remove :muted
15+
end
16+
end
17+
end

lib/error_tracker/migration/sqlite.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ defmodule ErrorTracker.Migration.SQLite do
77
alias ErrorTracker.Migration.SQLMigrator
88

99
@initial_version 2
10-
@current_version 4
10+
@current_version 5
1111

1212
@impl ErrorTracker.Migration
1313
def up(opts) do
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
defmodule ErrorTracker.Migration.SQLite.V05 do
2+
@moduledoc false
3+
4+
use Ecto.Migration
5+
6+
def up(_opts) do
7+
alter table(:error_tracker_errors) do
8+
add :muted, :boolean, default: false, null: false
9+
end
10+
end
11+
12+
def down(_opts) do
13+
alter table(:error_tracker_errors) do
14+
remove :muted
15+
end
16+
end
17+
end

lib/error_tracker/schemas/error.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ defmodule ErrorTracker.Error do
2222
field :status, Ecto.Enum, values: [:resolved, :unresolved], default: :unresolved
2323
field :fingerprint, :binary
2424
field :last_occurrence_at, :utc_datetime_usec
25+
field :muted, :boolean
2526

2627
has_many :occurrences, ErrorTracker.Occurrence
2728

lib/error_tracker/telemetry.ex

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,39 +31,45 @@ defmodule ErrorTracker.Telemetry do
3131
Each event is emitted with some measures and metadata, which can be used to
3232
receive information without having to query the database again:
3333
34-
| event | measures | metadata |
35-
| --------------------------------------- | -------------- | ------------- |
36-
| `[:error_tracker, :error, :new]` | `:system_time` | `:error` |
37-
| `[:error_tracker, :error, :unresolved]` | `:system_time` | `:error` |
38-
| `[:error_tracker, :error, :resolved]` | `:system_time` | `:error` |
39-
| `[:error_tracker, :occurrence, :new]` | `:system_time` | `:occurrence` |
34+
| event | measures | metadata |
35+
| --------------------------------------- | -------------- | ----------------------------------|
36+
| `[:error_tracker, :error, :new]` | `:system_time` | `:error` |
37+
| `[:error_tracker, :error, :unresolved]` | `:system_time` | `:error` |
38+
| `[:error_tracker, :error, :resolved]` | `:system_time` | `:error` |
39+
| `[:error_tracker, :occurrence, :new]` | `:system_time` | `:occurrence`, `:error`, `:muted` |
40+
41+
The metadata keys contain the following data:
42+
43+
* `:error` - An `%ErrorTracker.Error{}` struct representing the error.
44+
* `:occurrence` - An `%ErrorTracker.Occurrence{}` struct representing the occurrence.
45+
* `:muted` - A boolean indicating whether the error is muted or not.
4046
"""
4147

4248
@doc false
43-
def new_error(error) do
49+
def new_error(error = %ErrorTracker.Error{}) do
4450
measurements = %{system_time: System.system_time()}
4551
metadata = %{error: error}
4652
:telemetry.execute([:error_tracker, :error, :new], measurements, metadata)
4753
end
4854

4955
@doc false
50-
def unresolved_error(error) do
56+
def unresolved_error(error = %ErrorTracker.Error{}) do
5157
measurements = %{system_time: System.system_time()}
5258
metadata = %{error: error}
5359
:telemetry.execute([:error_tracker, :error, :unresolved], measurements, metadata)
5460
end
5561

5662
@doc false
57-
def resolved_error(error) do
63+
def resolved_error(error = %ErrorTracker.Error{}) do
5864
measurements = %{system_time: System.system_time()}
5965
metadata = %{error: error}
6066
:telemetry.execute([:error_tracker, :error, :resolved], measurements, metadata)
6167
end
6268

6369
@doc false
64-
def new_occurrence(occurrence) do
70+
def new_occurrence(occurrence = %ErrorTracker.Occurrence{}, muted) when is_boolean(muted) do
6571
measurements = %{system_time: System.system_time()}
66-
metadata = %{occurrence: occurrence}
72+
metadata = %{error: occurrence.error, occurrence: occurrence, muted: muted}
6773
:telemetry.execute([:error_tracker, :occurrence, :new], measurements, metadata)
6874
end
6975
end

lib/error_tracker/web/components/core_components.ex

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,4 +130,41 @@ defmodule ErrorTracker.Web.CoreComponents do
130130
</div>
131131
"""
132132
end
133+
134+
attr :name, :string, values: ~w[bell bell-slash]
135+
136+
def icon(assigns = %{name: "bell"}) do
137+
~H"""
138+
<svg
139+
xmlns="http://www.w3.org/2000/svg"
140+
viewBox="0 0 16 16"
141+
fill="currentColor"
142+
class="!h-4 !w-4 inline-block"
143+
>
144+
<path
145+
fill-rule="evenodd"
146+
d="M12 5a4 4 0 0 0-8 0v2.379a1.5 1.5 0 0 1-.44 1.06L2.294 9.707a1 1 0 0 0-.293.707V11a1 1 0 0 0 1 1h2a3 3 0 1 0 6 0h2a1 1 0 0 0 1-1v-.586a1 1 0 0 0-.293-.707L12.44 8.44A1.5 1.5 0 0 1 12 7.38V5Zm-5.5 7a1.5 1.5 0 0 0 3 0h-3Z"
147+
clip-rule="evenodd"
148+
/>
149+
</svg>
150+
"""
151+
end
152+
153+
def icon(assigns = %{name: "bell-slash"}) do
154+
~H"""
155+
<svg
156+
xmlns="http://www.w3.org/2000/svg"
157+
viewBox="0 0 16 16"
158+
fill="currentColor"
159+
class="!h-4 !w-4 inline-block"
160+
>
161+
<path
162+
fill-rule="evenodd"
163+
d="M4 7.379v-.904l6.743 6.742A3 3 0 0 1 5 12H3a1 1 0 0 1-1-1v-.586a1 1 0 0 1 .293-.707L3.56 8.44A1.5 1.5 0 0 0 4 7.38ZM6.5 12a1.5 1.5 0 0 0 3 0h-3Z"
164+
clip-rule="evenodd"
165+
/>
166+
<path d="M14 11a.997.997 0 0 1-.096.429L4.92 2.446A4 4 0 0 1 12 5v2.379c0 .398.158.779.44 1.06l1.267 1.268a1 1 0 0 1 .293.707V11ZM2.22 2.22a.75.75 0 0 1 1.06 0l10.5 10.5a.75.75 0 1 1-1.06 1.06L2.22 3.28a.75.75 0 0 1 0-1.06Z" />
167+
</svg>
168+
"""
169+
end
133170
end

lib/error_tracker/web/live/dashboard.ex

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,22 @@ defmodule ErrorTracker.Web.Live.Dashboard do
5757
{:noreply, paginate_errors(socket)}
5858
end
5959

60+
@impl Phoenix.LiveView
61+
def handle_event("mute", %{"error_id" => id}, socket) do
62+
error = Repo.get(Error, id)
63+
{:ok, _muted} = ErrorTracker.mute(error)
64+
65+
{:noreply, paginate_errors(socket)}
66+
end
67+
68+
@impl Phoenix.LiveView
69+
def handle_event("unmute", %{"error_id" => id}, socket) do
70+
error = Repo.get(Error, id)
71+
{:ok, _unmuted} = ErrorTracker.unmute(error)
72+
73+
{:noreply, paginate_errors(socket)}
74+
end
75+
6076
defp paginate_errors(socket) do
6177
%{page: page, search: search} = socket.assigns
6278
offset = (page - 1) * @per_page

0 commit comments

Comments
 (0)