Skip to content

Commit 75b2cf0

Browse files
committed
Normalize Oban exception reasons for better reports
1 parent 5f6a0c9 commit 75b2cf0

File tree

2 files changed

+79
-22
lines changed

2 files changed

+79
-22
lines changed

lib/sentry/integrations/oban/error_reporter.ex

+24-13
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,20 @@ defmodule Sentry.Integrations.Oban.ErrorReporter do
2323
%{required(:job) => struct(), optional(term()) => term()},
2424
:no_config
2525
) :: :ok
26-
def handle_event([:oban, :job, :exception], _measurements, %{job: job} = _metadata, :no_config) do
27-
%{reason: reason, stacktrace: stacktrace} = job.unsaved_error
28-
26+
def handle_event(
27+
[:oban, :job, :exception],
28+
_measurements,
29+
%{job: job, kind: kind, reason: reason, stacktrace: stacktrace} = _metadata,
30+
:no_config
31+
) do
2932
if report?(reason) do
30-
report(job, reason, stacktrace)
33+
report(job, kind, reason, stacktrace)
3134
else
3235
:ok
3336
end
3437
end
3538

36-
defp report(job, reason, stacktrace) do
39+
defp report(job, kind, reason, stacktrace) do
3740
stacktrace =
3841
case {apply(Oban.Worker, :from_string, [job.worker]), stacktrace} do
3942
{{:ok, atom_worker}, []} -> [{atom_worker, :process, 1, []}]
@@ -51,15 +54,19 @@ defmodule Sentry.Integrations.Oban.ErrorReporter do
5154
]
5255

5356
_ =
54-
case maybe_unwrap_exception(reason) do
57+
case maybe_unwrap_exception(kind, reason, stacktrace) do
5558
exception when is_exception(exception) ->
5659
Sentry.capture_exception(exception, opts)
5760

5861
_other ->
59-
Sentry.capture_message(
60-
"Oban job #{job.worker} errored out: %s",
61-
opts ++ [interpolation_parameters: [inspect(reason)]]
62-
)
62+
message =
63+
case kind do
64+
:exit -> "Oban job #{job.worker} exited: %s"
65+
:throw -> "Oban job #{job.worker} exited with an uncaught throw: %s"
66+
_other -> "Oban job #{job.worker} errored out: %s"
67+
end
68+
69+
Sentry.capture_message(message, opts ++ [interpolation_parameters: [inspect(reason)]])
6370
end
6471

6572
:ok
@@ -75,12 +82,16 @@ defmodule Sentry.Integrations.Oban.ErrorReporter do
7582
true
7683
end
7784

78-
defp maybe_unwrap_exception(%{reason: {:error, error}} = perform_error)
85+
defp maybe_unwrap_exception(
86+
:error = _kind,
87+
%{reason: {:error, error}} = perform_error,
88+
_stacktrace
89+
)
7990
when is_exception(perform_error, Oban.PerformError) and is_exception(error) do
8091
error
8192
end
8293

83-
defp maybe_unwrap_exception(reason) do
84-
reason
94+
defp maybe_unwrap_exception(kind, reason, stacktrace) do
95+
Exception.normalize(kind, reason, stacktrace)
8596
end
8697
end

test/sentry/integrations/oban/error_reporter_test.exs

+55-9
Original file line numberDiff line numberDiff line change
@@ -69,21 +69,21 @@ defmodule Sentry.Integrations.Oban.ErrorReporterTest do
6969
assert event.fingerprint == [@worker_as_string, "{{ default }}"]
7070
end
7171

72-
test "reports non-exception errors to Sentry" do
72+
test "reports normalized non-exception errors to Sentry" do
7373
Sentry.Test.start_collecting()
7474

7575
emit_telemetry_for_failed_job(:error, :undef, [])
7676

7777
assert [event] = Sentry.Test.pop_sentry_reports()
7878
assert %{job: %Oban.Job{}} = event.integration_meta.oban
7979

80-
assert event.message == %Sentry.Interfaces.Message{
81-
formatted: "Oban job #{@worker_as_string} errored out: :undef",
82-
message: "Oban job #{@worker_as_string} errored out: %s",
83-
params: [":undef"]
84-
}
80+
assert event.message == nil
8581

86-
assert [%Sentry.Interfaces.Thread{stacktrace: %{frames: [stacktrace]}}] = event.threads
82+
assert [%{stacktrace: %{frames: [stacktrace]}} = exception] = event.exception
83+
84+
assert exception.type == "UndefinedFunctionError"
85+
assert exception.value == "function #{@worker_as_string}.process/1 is undefined or private"
86+
assert exception.mechanism.handled == true
8787
assert stacktrace.module == MyWorker
8888
assert stacktrace.function == "#{@worker_as_string}.process/1"
8989

@@ -94,6 +94,53 @@ defmodule Sentry.Integrations.Oban.ErrorReporterTest do
9494
assert event.fingerprint == [@worker_as_string, "{{ default }}"]
9595
end
9696

97+
test "reports exits to Sentry" do
98+
Sentry.Test.start_collecting()
99+
100+
emit_telemetry_for_failed_job(:exit, :oops, [])
101+
102+
assert [event] = Sentry.Test.pop_sentry_reports()
103+
assert %{job: %Oban.Job{}} = event.integration_meta.oban
104+
105+
assert event.message == %Sentry.Interfaces.Message{
106+
message: "Oban job #{@worker_as_string} exited: %s",
107+
params: [":oops"],
108+
formatted: "Oban job #{@worker_as_string} exited: :oops"
109+
}
110+
111+
assert event.exception == []
112+
113+
assert event.tags.oban_queue == "default"
114+
assert event.tags.oban_state == "available"
115+
assert event.tags.oban_worker == @worker_as_string
116+
117+
assert event.fingerprint == [@worker_as_string, "{{ default }}"]
118+
end
119+
120+
test "reports throws to Sentry" do
121+
Sentry.Test.start_collecting()
122+
123+
emit_telemetry_for_failed_job(:throw, :this_was_not_caught, [])
124+
125+
assert [event] = Sentry.Test.pop_sentry_reports()
126+
assert %{job: %Oban.Job{}} = event.integration_meta.oban
127+
128+
assert event.message == %Sentry.Interfaces.Message{
129+
message: "Oban job #{@worker_as_string} exited with an uncaught throw: %s",
130+
params: [":this_was_not_caught"],
131+
formatted:
132+
"Oban job #{@worker_as_string} exited with an uncaught throw: :this_was_not_caught"
133+
}
134+
135+
assert event.exception == []
136+
137+
assert event.tags.oban_queue == "default"
138+
assert event.tags.oban_state == "available"
139+
assert event.tags.oban_worker == @worker_as_string
140+
141+
assert event.fingerprint == [@worker_as_string, "{{ default }}"]
142+
end
143+
97144
for reason <- [:cancel, :discard] do
98145
test "doesn't report Oban.PerformError with reason #{inspect(reason)}" do
99146
Sentry.Test.start_collecting()
@@ -116,13 +163,12 @@ defmodule Sentry.Integrations.Oban.ErrorReporterTest do
116163
%{"id" => "123", "entity" => "user", "type" => "delete"}
117164
|> MyWorker.new()
118165
|> Ecto.Changeset.apply_action!(:validate)
119-
|> Map.replace!(:unsaved_error, %{kind: kind, reason: reason, stacktrace: stacktrace})
120166

121167
assert :ok =
122168
ErrorReporter.handle_event(
123169
[:oban, :job, :exception],
124170
%{},
125-
%{job: job},
171+
%{job: job, kind: kind, reason: reason, stacktrace: stacktrace},
126172
:no_config
127173
)
128174

0 commit comments

Comments
 (0)