@@ -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
45287end
0 commit comments