Skip to content

Commit 36c0489

Browse files
committed
tests(oban): add integration tests for :skip_error_report_callback config
1 parent 25deb1f commit 36c0489

2 files changed

Lines changed: 251 additions & 2 deletions

File tree

lib/sentry/integrations/oban/error_reporter.ex

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,15 @@ defmodule Sentry.Integrations.Oban.ErrorReporter do
5151
defp call_skip_error_report_callback(callback, job) do
5252
worker =
5353
case apply(Oban.Worker, :from_string, [job.worker]) do
54-
{:ok, mod} -> mod
55-
:error -> nil
54+
{:ok, mod} ->
55+
mod
56+
57+
{:error, _} ->
58+
Logger.warning(
59+
"Could not resolve Oban worker module from string: #{inspect(job.worker)}"
60+
)
61+
62+
nil
5663
end
5764

5865
try do

test_integrations/phoenix_app/test/phoenix_app/oban_test.exs

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ defmodule Sentry.Integrations.Phoenix.ObanTest do
22
use PhoenixAppWeb.ConnCase, async: false
33
use Oban.Testing, repo: PhoenixApp.Repo
44

5+
import ExUnit.CaptureLog
56
import Sentry.TestHelpers
67

8+
alias Sentry.Integrations.Oban.ErrorReporter
9+
710
setup do
811
put_test_config(dsn: "http://public:secret@localhost:8080/1", traces_sample_rate: 1.0)
912

@@ -21,6 +24,17 @@ defmodule Sentry.Integrations.Phoenix.ObanTest do
2124
end
2225
end
2326

27+
defmodule FailingWorker do
28+
use Oban.Worker, max_attempts: 3
29+
30+
@impl Oban.Worker
31+
def perform(%Oban.Job{args: %{"should_fail" => true}}) do
32+
raise "intentional failure for testing"
33+
end
34+
35+
def perform(_job), do: :ok
36+
end
37+
2438
test "captures Oban worker execution as transaction" do
2539
:ok = perform_job(TestWorker, %{test: "args"})
2640

@@ -42,4 +56,232 @@ defmodule Sentry.Integrations.Phoenix.ObanTest do
4256

4357
assert [] = transaction.spans
4458
end
59+
60+
describe "skip_error_report_callback config" do
61+
setup do
62+
:telemetry.detach(ErrorReporter)
63+
64+
on_exit(fn ->
65+
_ = :telemetry.detach(ErrorReporter)
66+
ErrorReporter.attach([])
67+
end)
68+
69+
:ok
70+
end
71+
72+
test "skips error reporting when callback returns true" do
73+
test_pid = self()
74+
75+
ErrorReporter.attach(
76+
skip_error_report_callback: fn worker, job ->
77+
send(test_pid, {:callback_invoked, worker, job})
78+
true
79+
end
80+
)
81+
82+
{:ok, _job} =
83+
%{"should_fail" => true}
84+
|> FailingWorker.new()
85+
|> Oban.insert()
86+
87+
assert %{failure: 1} = Oban.drain_queue(queue: :default)
88+
89+
assert_receive {:callback_invoked, worker, received_job}
90+
assert worker == FailingWorker
91+
assert %Oban.Job{} = received_job
92+
assert received_job.args == %{"should_fail" => true}
93+
94+
assert [] = Sentry.Test.pop_sentry_reports()
95+
end
96+
97+
test "reports error when callback returns false" do
98+
test_pid = self()
99+
100+
ErrorReporter.attach(
101+
skip_error_report_callback: fn worker, job ->
102+
send(test_pid, {:callback_invoked, worker, job})
103+
false
104+
end
105+
)
106+
107+
{:ok, _job} =
108+
%{"should_fail" => true}
109+
|> FailingWorker.new()
110+
|> Oban.insert()
111+
112+
assert %{failure: 1} = Oban.drain_queue(queue: :default)
113+
114+
assert_receive {:callback_invoked, _worker, _job}
115+
116+
assert [event] = Sentry.Test.pop_sentry_reports()
117+
assert event.original_exception == %RuntimeError{message: "intentional failure for testing"}
118+
119+
assert event.tags.oban_worker ==
120+
"Sentry.Integrations.Phoenix.ObanTest.FailingWorker"
121+
end
122+
123+
test "callback receives worker module and full job struct" do
124+
test_pid = self()
125+
126+
ErrorReporter.attach(
127+
skip_error_report_callback: fn worker, job ->
128+
send(test_pid, {:callback_args, worker, job})
129+
false
130+
end
131+
)
132+
133+
{:ok, _job} =
134+
%{"should_fail" => true, "user_id" => 123}
135+
|> FailingWorker.new()
136+
|> Oban.insert()
137+
138+
Oban.drain_queue(queue: :default)
139+
140+
assert_receive {:callback_args, worker, job}
141+
142+
assert worker == FailingWorker
143+
assert is_atom(worker)
144+
145+
assert %Oban.Job{} = job
146+
assert job.args == %{"should_fail" => true, "user_id" => 123}
147+
assert job.worker == "Sentry.Integrations.Phoenix.ObanTest.FailingWorker"
148+
assert job.queue == "default"
149+
assert job.max_attempts == 3
150+
assert is_integer(job.attempt)
151+
assert is_integer(job.id)
152+
end
153+
154+
test "callback can make decisions based on attempt number" do
155+
test_pid = self()
156+
157+
ErrorReporter.attach(
158+
skip_error_report_callback: fn _worker, job ->
159+
should_skip = job.attempt < job.max_attempts
160+
send(test_pid, {:skip_decision, job.attempt, job.max_attempts, should_skip})
161+
should_skip
162+
end
163+
)
164+
165+
{:ok, _job} =
166+
%{"should_fail" => true}
167+
|> FailingWorker.new()
168+
|> Oban.insert()
169+
170+
Oban.drain_queue(queue: :default)
171+
172+
assert_receive {:skip_decision, attempt, max_attempts, should_skip}
173+
assert attempt == 1
174+
assert max_attempts == 3
175+
assert should_skip == true
176+
177+
assert [] = Sentry.Test.pop_sentry_reports()
178+
end
179+
180+
test "handles callback errors gracefully and defaults to reporting" do
181+
log =
182+
capture_log(fn ->
183+
ErrorReporter.attach(
184+
skip_error_report_callback: fn _worker, _job ->
185+
raise "callback crashed!"
186+
end
187+
)
188+
189+
{:ok, _job} =
190+
%{"should_fail" => true}
191+
|> FailingWorker.new()
192+
|> Oban.insert()
193+
194+
Oban.drain_queue(queue: :default)
195+
end)
196+
197+
assert log =~ "skip_error_report_callback failed"
198+
assert log =~ "FailingWorker"
199+
assert log =~ "callback crashed!"
200+
201+
assert [event] = Sentry.Test.pop_sentry_reports()
202+
assert event.original_exception == %RuntimeError{message: "intentional failure for testing"}
203+
end
204+
205+
test "reports error when no callback is configured" do
206+
ErrorReporter.attach([])
207+
208+
{:ok, _job} =
209+
%{"should_fail" => true}
210+
|> FailingWorker.new()
211+
|> Oban.insert()
212+
213+
Oban.drain_queue(queue: :default)
214+
215+
assert [event] = Sentry.Test.pop_sentry_reports()
216+
assert event.original_exception == %RuntimeError{message: "intentional failure for testing"}
217+
end
218+
219+
test "callback can filter based on worker type" do
220+
test_pid = self()
221+
222+
ErrorReporter.attach(
223+
skip_error_report_callback: fn worker, _job ->
224+
should_skip = worker == FailingWorker
225+
send(test_pid, {:worker_check, worker, should_skip})
226+
should_skip
227+
end
228+
)
229+
230+
{:ok, _job} =
231+
%{"should_fail" => true}
232+
|> FailingWorker.new()
233+
|> Oban.insert()
234+
235+
Oban.drain_queue(queue: :default)
236+
237+
assert_receive {:worker_check, FailingWorker, true}
238+
239+
assert [] = Sentry.Test.pop_sentry_reports()
240+
end
241+
242+
test "callback receives nil and logs warning for non-existent worker module" do
243+
test_pid = self()
244+
245+
log =
246+
capture_log(fn ->
247+
ErrorReporter.attach(
248+
skip_error_report_callback: fn worker, job ->
249+
send(test_pid, {:callback_with_unknown_worker, worker, job})
250+
false
251+
end
252+
)
253+
254+
job = %Oban.Job{
255+
id: 999,
256+
args: %{"test" => true},
257+
worker: "NonExistent.Worker.Module",
258+
queue: "default",
259+
state: "executing",
260+
attempt: 1,
261+
max_attempts: 3
262+
}
263+
264+
:telemetry.execute(
265+
[:oban, :job, :exception],
266+
%{duration: 1000},
267+
%{
268+
job: job,
269+
kind: :error,
270+
reason: %RuntimeError{message: "worker failed"},
271+
stacktrace: []
272+
}
273+
)
274+
end)
275+
276+
assert log =~ "Could not resolve Oban worker module from string"
277+
assert log =~ "NonExistent.Worker.Module"
278+
279+
assert_receive {:callback_with_unknown_worker, worker, received_job}
280+
assert worker == nil
281+
assert received_job.worker == "NonExistent.Worker.Module"
282+
283+
assert [event] = Sentry.Test.pop_sentry_reports()
284+
assert event.tags.oban_worker == "NonExistent.Worker.Module"
285+
end
286+
end
45287
end

0 commit comments

Comments
 (0)