@@ -13,30 +13,42 @@ module ActiveJobExtensions
1313
1414 def self . prepended ( base )
1515 base . attr_accessor :_sentry
16+ base . attr_reader :_sentry_job_data
1617 end
1718
1819 def perform_now
1920 if !Sentry . initialized? || already_supported_by_sentry_integration?
2021 super
2122 else
2223 data = _sentry || { }
24+ current_scope = Sentry . get_current_scope
25+ scope = current_scope if data [ "execution_scope_id" ] == current_scope . object_id
26+ transaction = scope &.get_span
2327 SentryReporter . record (
2428 self ,
2529 trace_headers : data [ "trace_propagation_headers" ] ,
26- user : data [ "user" ]
30+ user : data [ "user" ] ,
31+ delivery_queue : data [ "delivery_queue" ] ,
32+ payload : _sentry_job_data ,
33+ transaction : transaction ,
34+ scope : scope
2735 ) { super }
2836 end
2937 end
3038
3139 def serialize
3240 payload = super
3341 return payload if !Sentry . initialized? || already_supported_by_sentry_integration?
42+ return payload if SentryReporter . producer . instrumented_by_extension? ( self )
3443
3544 begin
3645 sentry_data = { }
3746 if Sentry . configuration . rails . active_job_propagate_traces
3847 headers = Sentry . get_trace_propagation_headers
39- sentry_data [ "trace_propagation_headers" ] = headers if headers && !headers . empty?
48+ if headers && !headers . empty?
49+ sentry_data [ "trace_propagation_headers" ] = headers
50+ payload = SentryReporter . producer . inject_trace_headers ( self , payload , headers )
51+ end
4052 end
4153
4254 if Sentry . configuration . data_collection . user_info
@@ -58,18 +70,18 @@ def deserialize(job_data)
5870 return if !Sentry . initialized? || already_supported_by_sentry_integration?
5971
6072 begin
73+ @_sentry_job_data = job_data
6174 self . _sentry = job_data [ SENTRY_PAYLOAD_KEY ]
6275 rescue StandardError => e
6376 Sentry . sdk_logger &.error ( "sentry-rails: failed to extract _sentry payload: #{ e . class } : #{ e . message } \n #{ Array ( e . backtrace ) . first ( 5 ) . join ( "\n " ) } " )
6477 end
6578 end
6679
6780 def already_supported_by_sentry_integration?
68- Sentry . configuration . rails . skippable_job_adapters . include ?( self . class . queue_adapter . class . to_s )
81+ ! Sentry :: Rails . active_job_owns_adapter ?( self . class . queue_adapter . class )
6982 end
7083
7184 class SentryReporter
72- OP_NAME = "queue.process"
7385 SPAN_ORIGIN = "auto.queue.active_job"
7486
7587 # Emitted as messaging.system when the configured queue adapter's
@@ -85,6 +97,22 @@ class SentryReporter
8597 class << self
8698 include ErrorReporterContext
8799
100+ def consumer
101+ @consumer ||= Sentry ::JobExtensions ::Consumer . new ( integration : Sentry ::Rails , origin : SPAN_ORIGIN )
102+ end
103+
104+ def producer
105+ @producer ||= Sentry ::JobExtensions ::Producer . new ( origin : SPAN_ORIGIN )
106+ end
107+
108+ def job_extensions
109+ @job_extensions ||= Sentry . job_extensions
110+ end
111+
112+ def call_hook ( job , hook , **arguments )
113+ job_extensions . call ( job_extensions . for ( job ) , hook , job : job , **arguments )
114+ end
115+
88116 def producer_callback_registered?
89117 @producer_callback_registered ||= false
90118 end
@@ -95,104 +123,83 @@ def producer_callback_registered!
95123
96124 def record_producer_span ( job , &enqueue )
97125 return yield if !Sentry . initialized? || job . already_supported_by_sentry_integration?
98-
99- enqueued = false
100-
101- run_enqueue = lambda do
102- enqueued = true
103- enqueue . call
104- end
105-
106- begin
107- Sentry . with_child_span ( op : "queue.publish" , description : job . class . name ) do |span |
108- if span
109- span . set_origin ( SPAN_ORIGIN )
110- span . set_data ( Sentry ::Span ::DataConventions ::MESSAGING_SYSTEM , messaging_system ( job ) )
111- span . set_data ( Sentry ::Span ::DataConventions ::MESSAGING_MESSAGE_ID , job . job_id )
112- span . set_data ( Sentry ::Span ::DataConventions ::MESSAGING_DESTINATION_NAME , job . queue_name )
113-
114- if ( count = retry_count ( job ) )
115- span . set_data ( Sentry ::Span ::DataConventions ::MESSAGING_MESSAGE_RETRY_COUNT , count )
116- end
117- end
118-
119- run_enqueue . call
120- end
121- rescue StandardError => e
122- raise if enqueued
123-
124- log_producer_span_error ( e )
125-
126- run_enqueue . call
127- end
126+ return yield if producer . instrumented_by_extension? ( job )
127+
128+ producer . record_span (
129+ job ,
130+ description : job . class . name ,
131+ messaging_data : -> {
132+ {
133+ system : messaging_system ( job ) ,
134+ message_id : job . job_id ,
135+ destination : job . queue_name
136+ } . compact
137+ } ,
138+ default_retry_count : -> { executions ( job ) } ,
139+ on_instrumentation_error : method ( :log_producer_span_error ) ,
140+ &enqueue
141+ )
128142 end
129143
130144 def log_producer_span_error ( e )
131145 Sentry . sdk_logger &.error ( "sentry-rails: failed to record producer span: #{ e . class } : #{ e . message } \n #{ Array ( e . backtrace ) . first ( 5 ) . join ( "\n " ) } " )
132146 end
133147
134- def record ( job , trace_headers : nil , user : nil , &block )
135- original_hub = Sentry . get_current_hub_internal
136- Sentry . clone_hub_to_current_thread
148+ def record ( job , trace_headers : nil , user : nil , delivery_queue : nil , payload : nil , transaction : nil , scope : nil , &block )
149+ options = { trace_headers : trace_headers , user : user , delivery_queue : delivery_queue , payload : payload , transaction : transaction }
150+ return record_in_scope ( scope , job , ** options , backend_scope : true , & block ) if scope
137151
138- Sentry . with_scope do |scope |
139- begin
140- scope . set_user ( user . transform_keys ( &:to_sym ) ) if user && !user . empty?
141-
142- scope . set_transaction_name ( job . class . name , source : :task )
143- scope . set_tags ( queue : job . queue_name )
144-
145- scope . set_contexts ( active_job : {
146- job_class : job . class . name ,
147- job_id : job . job_id ,
148- queue : job . queue_name ,
149- provider_job_id : job . provider_job_id
150- } )
151-
152- transaction_options = {
153- name : scope . transaction_name ,
154- source : scope . transaction_source ,
155- op : OP_NAME ,
156- origin : SPAN_ORIGIN
157- }
158-
159- transaction = if trace_headers && !trace_headers . empty?
160- continued = Sentry . continue_trace ( trace_headers , **transaction_options )
161- Sentry . start_transaction ( transaction : continued , **transaction_options )
162- else
163- Sentry . start_transaction ( **transaction_options )
164- end
165-
166- if transaction
167- set_messaging_data ( transaction , job )
168- scope . set_span ( transaction )
169- end
170-
171- yield . tap do
172- finish_sentry_transaction ( transaction , 200 )
173- end
174- rescue Exception => e # rubocop:disable Lint/RescueException
175- finish_sentry_transaction ( transaction , 500 )
152+ with_isolated_scope do |isolated_scope |
153+ record_in_scope ( isolated_scope , job , **options , &block )
154+ end
155+ end
176156
177- capture_exception ( job , e )
157+ def record_in_scope ( scope , job , trace_headers : nil , user : nil , delivery_queue : nil , payload : nil , transaction : nil , backend_scope : false )
158+ begin
159+ scope . set_user ( user . transform_keys ( &:to_sym ) ) if user && !user . empty?
160+
161+ consumer . apply_scope_data ( scope , job )
162+
163+ scope . set_transaction_name ( consumer . transaction_name ( job ) || job . class . name , source : :task )
164+ scope . set_tags ( queue : delivery_queue || job . queue_name )
178165
179- raise
166+ scope . set_contexts ( active_job : {
167+ job_class : job . class . name ,
168+ job_id : job . job_id ,
169+ queue : job . queue_name ,
170+ provider_job_id : job . provider_job_id
171+ } )
172+
173+ transaction = consumer . start_transaction (
174+ scope , job ,
175+ transaction : transaction . is_a? ( Sentry ::Transaction ) ? transaction : nil ,
176+ trace_headers : trace_headers ,
177+ payload : payload ,
178+ messaging_data : -> {
179+ {
180+ system : messaging_system ( job ) ,
181+ message_id : job . job_id ,
182+ destination : job . queue_name ,
183+ receive_latency : receive_latency ( job ) ,
184+ body_size : payload && body_size { payload }
185+ }
186+ } ,
187+ default_retry_count : executions ( job )
188+ )
189+
190+ yield . tap do
191+ consumer . finish_transaction ( transaction , 200 )
180192 end
181- end
182- ensure
183- Sentry . set_current_hub_internal ( original_hub )
184- end
193+ rescue Exception => e # rubocop:disable Lint/RescueException
194+ consumer . finish_transaction ( transaction , 500 )
185195
186- def set_messaging_data ( transaction , job )
187- transaction . set_data ( Sentry ::Span ::DataConventions ::MESSAGING_SYSTEM , messaging_system ( job ) )
188- transaction . set_data ( Sentry ::Span ::DataConventions ::MESSAGING_MESSAGE_ID , job . job_id )
189- transaction . set_data ( Sentry ::Span ::DataConventions ::MESSAGING_DESTINATION_NAME , job . queue_name )
190- if ( count = retry_count ( job ) )
191- transaction . set_data ( Sentry ::Span ::DataConventions ::MESSAGING_MESSAGE_RETRY_COUNT , count )
192- end
196+ if backend_scope
197+ scope . update_from_options ( **exception_data ( job ) )
198+ elsif consumer . report_exception? ( job , e )
199+ capture_exception ( job , e )
200+ end
193201
194- if ( latency = compute_latency ( job ) )
195- transaction . set_data ( Sentry ::Span ::DataConventions ::MESSAGING_MESSAGE_RECEIVE_LATENCY , latency )
202+ raise
196203 end
197204 end
198205
@@ -208,31 +215,30 @@ def messaging_system(job)
208215 # its first retry reads 1, and so on. On the producer the retry enqueue
209216 # runs after the failed attempt bumped +executions+, so it observes the
210217 # same progression.
211- def retry_count ( job )
218+ def executions ( job )
212219 job . executions . to_i if job . respond_to? ( :executions )
213220 end
214221
215- def compute_latency ( job )
216- return unless job . respond_to? ( :enqueued_at ) && job . enqueued_at
222+ def receive_latency ( job )
223+ Sentry ::Utils ::Latency . milliseconds_since ( job . enqueued_at ) if job . respond_to? ( :enqueued_at )
224+ end
217225
218- enqueued_time = job . enqueued_at . is_a? ( String ) ? Time . parse ( job . enqueued_at ) : job . enqueued_at
219- ( Time . now . to_f - enqueued_time . to_f ) * 1000.0
226+ def body_size
227+ ::JSON . generate ( yield ) . bytesize
228+ rescue StandardError
229+ nil
220230 end
221231
222232 def capture_exception ( job , e )
223- options = {
224- extra : sentry_context ( job ) ,
225- tags : {
226- job_id : job . job_id ,
227- provider_job_id : job . provider_job_id
228- } ,
229- # Send synchronously: a worker process may exit before the async
230- # background worker flushes its queue, which would drop the event.
231- hint : { background : false }
232- }
233- options [ :contexts ] = execution_context if Sentry . configuration . data_collection . queues
233+ consumer . capture_exception ( job , e , **exception_data ( job ) )
234+ end
234235
235- Sentry ::Rails . capture_exception ( e , **options )
236+ def exception_data ( job )
237+ {
238+ contexts : Sentry . configuration . data_collection . queues ? execution_context : { } ,
239+ tags : { job_id : job . job_id , provider_job_id : job . provider_job_id } ,
240+ extra : sentry_context ( job )
241+ }
236242 end
237243
238244 def register_event_handlers
@@ -255,7 +261,7 @@ def retry_handler(*args)
255261 handle_error_event ( *args ) do |job , error |
256262 return if error . nil?
257263 return if !Sentry . initialized? || job . already_supported_by_sentry_integration?
258- return unless Sentry . configuration . rails . active_job_report_on_retry_error
264+ return unless consumer . report_retried_exception? ( job , error , default : Sentry . configuration . rails . active_job_report_on_retry_error )
259265
260266 capture_exception ( job , error )
261267 end
@@ -266,13 +272,6 @@ def handle_error_event(*args)
266272 yield ( event . payload [ :job ] , event . payload [ :error ] )
267273 end
268274
269- def finish_sentry_transaction ( transaction , status )
270- return unless transaction
271-
272- transaction . set_http_status ( status )
273- transaction . finish
274- end
275-
276275 def sentry_context ( job )
277276 context = {
278277 active_job : job . class . name ,
@@ -293,8 +292,18 @@ def sentry_serialize_arguments(argument)
293292 Sentry ::Rails ::Serializer . serialize ( argument )
294293 end
295294
295+ private :record_in_scope , :exception_data
296+
296297 private
297298
299+ def with_isolated_scope ( &block )
300+ original_hub = Sentry . get_current_hub_internal
301+ Sentry . clone_hub_to_current_thread
302+ Sentry . with_scope ( &block )
303+ ensure
304+ Sentry . set_current_hub_internal ( original_hub )
305+ end
306+
298307 def subscribers
299308 @__subscribers__ ||= Set . new
300309 end
0 commit comments