33require "set"
44require "sentry/rails/serializer"
55require "sentry/rails/error_reporter_context"
6+ require "sentry/rails/active_job/job_extension"
7+ require "sentry/rails/active_job/producer"
68
79module Sentry
810 module Rails
911 module ActiveJobExtensions
1012 SENTRY_PAYLOAD_KEY = "_sentry"
1113
12- USER_FIELDS_ALLOWLIST = %w[ id email username ] . freeze
13-
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+ payload : _sentry_job_data ,
32+ transaction : transaction ,
33+ scope : scope
2734 ) { super }
2835 end
2936 end
3037
3138 def serialize
3239 payload = super
3340 return payload if !Sentry . initialized? || already_supported_by_sentry_integration?
34-
35- begin
36- sentry_data = { }
37- if Sentry . configuration . rails . active_job_propagate_traces
38- headers = Sentry . get_trace_propagation_headers
39- sentry_data [ "trace_propagation_headers" ] = headers if headers && !headers . empty?
40- end
41-
42- if Sentry . configuration . data_collection . user_info
43- user = Sentry . get_current_scope . user
44- allowed = user . transform_keys ( &:to_s ) . slice ( *USER_FIELDS_ALLOWLIST )
45- sentry_data [ "user" ] = allowed unless allowed . empty?
46- end
47-
48- payload [ SENTRY_PAYLOAD_KEY ] = sentry_data unless sentry_data . empty?
49- rescue StandardError => e
50- Sentry . sdk_logger &.error ( "sentry-rails: failed to inject _sentry payload: #{ e . class } : #{ e . message } \n #{ Array ( e . backtrace ) . first ( 5 ) . join ( "\n " ) } " )
51- end
52-
53- payload
41+ SentryReporter . producer ( self ) . prepare_payload ( self , payload )
5442 end
5543
5644 def deserialize ( job_data )
5745 super
5846 return if !Sentry . initialized? || already_supported_by_sentry_integration?
5947
6048 begin
49+ @_sentry_job_data = job_data
6150 self . _sentry = job_data [ SENTRY_PAYLOAD_KEY ]
6251 rescue StandardError => e
6352 Sentry . sdk_logger &.error ( "sentry-rails: failed to extract _sentry payload: #{ e . class } : #{ e . message } \n #{ Array ( e . backtrace ) . first ( 5 ) . join ( "\n " ) } " )
6453 end
6554 end
6655
6756 def already_supported_by_sentry_integration?
68- Sentry . configuration . rails . skippable_job_adapters . include ?( self . class . queue_adapter . class . to_s )
57+ ! Sentry :: Rails . active_job_owns_adapter ?( self . class . queue_adapter . class )
6958 end
7059
7160 class SentryReporter
72- OP_NAME = "queue.process"
73- SPAN_ORIGIN = "auto.queue.active_job"
74-
75- # Emitted as messaging.system when the configured queue adapter's
76- # identity cannot be resolved from the job. ActiveJob is adapter-agnostic
77- # and supports arbitrary third-party backends, so the concrete value is
78- # normally derived from the adapter itself; this is only the last resort.
79- MESSAGING_SYSTEM_FALLBACK = "activejob"
80-
8161 EVENT_HANDLERS = {
8262 "enqueue_retry.active_job" => :retry_handler
8363 }
8464
8565 class << self
86- include ErrorReporterContext
66+ def default_extension
67+ @default_extension ||= JobExtension . new
68+ end
69+
70+ def consumer
71+ @consumer ||= Sentry ::JobExtensions ::Consumer . new ( integration : Sentry ::Rails , origin : JobExtension ::SPAN_ORIGIN , registry : job_extensions )
72+ end
73+
74+ def producer ( job )
75+ job_extensions . call ( job_extensions . for ( job ) , :producer , job : job )
76+ end
77+
78+ def job_extensions
79+ default_extension . registry
80+ end
8781
8882 def producer_callback_registered?
8983 @producer_callback_registered ||= false
@@ -95,144 +89,51 @@ def producer_callback_registered!
9589
9690 def record_producer_span ( job , &enqueue )
9791 return yield if !Sentry . initialized? || job . already_supported_by_sentry_integration?
92+ producer ( job ) . record_span ( job , &enqueue )
93+ end
9894
99- enqueued = false
95+ def record ( job , trace_headers : nil , user : nil , payload : nil , transaction : nil , scope : nil , &block )
96+ options = { trace_headers : trace_headers , user : user , payload : payload , transaction : transaction }
97+ return record_in_scope ( scope , job , **options , backend_scope : true , &block ) if scope
10098
101- run_enqueue = lambda do
102- enqueued = true
103- enqueue . call
99+ with_isolated_scope do |isolated_scope |
100+ record_in_scope ( isolated_scope , job , **options , &block )
104101 end
102+ end
105103
104+ def record_in_scope ( scope , job , trace_headers : nil , user : nil , payload : nil , transaction : nil , backend_scope : false )
106105 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
106+ scope . set_user ( user . transform_keys ( &:to_sym ) ) if user && !user . empty?
123107
124- log_producer_span_error ( e )
108+ consumer . apply_scope_data ( scope , job )
125109
126- run_enqueue . call
127- end
128- end
110+ scope . set_transaction_name ( consumer . transaction_name ( job ) , source : :task )
129111
130- def log_producer_span_error ( e )
131- Sentry . sdk_logger &.error ( "sentry-rails: failed to record producer span: #{ e . class } : #{ e . message } \n #{ Array ( e . backtrace ) . first ( 5 ) . join ( "\n " ) } " )
132- end
133-
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
112+ transaction = consumer . start_transaction (
113+ scope , job ,
114+ transaction : transaction . is_a? ( Sentry ::Transaction ) ? transaction : nil ,
115+ trace_headers : trace_headers ,
116+ payload : payload
117+ )
137118
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 )
119+ yield . tap do
120+ consumer . finish_transaction ( transaction , 200 )
121+ end
122+ rescue Exception => e # rubocop:disable Lint/RescueException
123+ consumer . finish_transaction ( transaction , 500 )
176124
125+ if backend_scope
126+ scope . update_from_options ( **default_extension . error_data ( job : job , exception : e ) )
127+ elsif consumer . report_exception? ( job , e )
177128 capture_exception ( job , e )
178-
179- raise
180129 end
181- end
182- ensure
183- Sentry . set_current_hub_internal ( original_hub )
184- end
185-
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
193130
194- if ( latency = compute_latency ( job ) )
195- transaction . set_data ( Sentry ::Span ::DataConventions ::MESSAGING_MESSAGE_RECEIVE_LATENCY , latency )
131+ raise
196132 end
197133 end
198134
199- def messaging_system ( job )
200- name = job . class . queue_adapter_name if job . class . respond_to? ( :queue_adapter_name )
201- name = name . to_s
202- name . empty? ? MESSAGING_SYSTEM_FALLBACK : name
203- end
204-
205- # Number of retries the job has already gone through, as observed at
206- # the moment the span is opened. On the consumer the span opens before
207- # ActiveJob increments +executions+, so a job's first attempt reads 0,
208- # its first retry reads 1, and so on. On the producer the retry enqueue
209- # runs after the failed attempt bumped +executions+, so it observes the
210- # same progression.
211- def retry_count ( job )
212- job . executions . to_i if job . respond_to? ( :executions )
213- end
214-
215- def compute_latency ( job )
216- return unless job . respond_to? ( :enqueued_at ) && job . enqueued_at
217-
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
220- end
221-
222135 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
234-
235- Sentry ::Rails . capture_exception ( e , **options )
136+ consumer . capture_exception ( job , e , **default_extension . error_data ( job : job , exception : e ) )
236137 end
237138
238139 def register_event_handlers
@@ -255,7 +156,7 @@ def retry_handler(*args)
255156 handle_error_event ( *args ) do |job , error |
256157 return if error . nil?
257158 return if !Sentry . initialized? || job . already_supported_by_sentry_integration?
258- return unless Sentry . configuration . rails . active_job_report_on_retry_error
159+ return unless consumer . report_retried_exception? ( job , error , default : nil )
259160
260161 capture_exception ( job , error )
261162 end
@@ -266,35 +167,18 @@ def handle_error_event(*args)
266167 yield ( event . payload [ :job ] , event . payload [ :error ] )
267168 end
268169
269- def finish_sentry_transaction ( transaction , status )
270- return unless transaction
271-
272- transaction . set_http_status ( status )
273- transaction . finish
274- end
170+ private :record_in_scope
275171
276- def sentry_context ( job )
277- context = {
278- active_job : job . class . name ,
279- scheduled_at : job . scheduled_at ,
280- job_id : job . job_id ,
281- provider_job_id : job . provider_job_id ,
282- locale : job . locale
283- }
284-
285- if Sentry . configuration . data_collection . queues
286- context [ :arguments ] = sentry_serialize_arguments ( job . arguments )
287- end
288-
289- context
290- end
172+ private
291173
292- def sentry_serialize_arguments ( argument )
293- Sentry ::Rails ::Serializer . serialize ( argument )
174+ def with_isolated_scope ( &block )
175+ original_hub = Sentry . get_current_hub_internal
176+ Sentry . clone_hub_to_current_thread
177+ Sentry . with_scope ( &block )
178+ ensure
179+ Sentry . set_current_hub_internal ( original_hub )
294180 end
295181
296- private
297-
298182 def subscribers
299183 @__subscribers__ ||= Set . new
300184 end
0 commit comments