@@ -36,6 +36,7 @@ def initialize(*subscribers, logger: nil)
3636 @subscribers = subscribers . flatten
3737 @logger = logger
3838 @debug_mode = false
39+ @context_middlewares = ErrorContextMiddlewareStack . new
3940 end
4041
4142 # Evaluates the given block, reporting and swallowing any unhandled error.
@@ -202,6 +203,22 @@ def set_context(...)
202203 ActiveSupport ::ExecutionContext . set ( ...)
203204 end
204205
206+ # Add a middleware to modify the error context before it is sent to subscribers.
207+ #
208+ # Middleware is added to a stack of callables run on an error's execution context
209+ # before passing to subscribers. Allows creation of entries in error context that
210+ # are shared by all subscribers.
211+ #
212+ # A context middleware receives the error and current state of the context hash.
213+ # It must return a hash - the middleware stack returns the hash after it has
214+ # run through all middlewares. A middleware can mutate or replace the hash.
215+ #
216+ # Rails.error.add_middleware(-> (error, context) { context.merge({ foo: :bar }) })
217+ #
218+ def add_middleware ( middleware )
219+ @context_middlewares . use ( middleware )
220+ end
221+
205222 # Report an error directly to subscribers. You can use this method when the
206223 # block-based #handle and #record methods are not suitable.
207224 #
@@ -223,7 +240,11 @@ def report(error, handled: true, severity: handled ? :warning : :error, context:
223240 raise ArgumentError , "severity must be one of #{ SEVERITIES . map ( &:inspect ) . join ( ", " ) } , got: #{ severity . inspect } "
224241 end
225242
226- full_context = ActiveSupport ::ExecutionContext . to_h . merge ( context || { } )
243+ full_context = @context_middlewares . execute (
244+ error ,
245+ ActiveSupport ::ExecutionContext . to_h . merge ( context || { } )
246+ )
247+
227248 disabled_subscribers = ActiveSupport ::IsolatedExecutionState [ self ]
228249 @subscribers . each do |subscriber |
229250 unless disabled_subscribers &.any? { |s | s === subscriber }
@@ -272,5 +293,25 @@ def ensure_backtrace(error)
272293
273294 error . backtrace . shift ( count )
274295 end
296+
297+ class ErrorContextMiddlewareStack # :nodoc:
298+ def initialize
299+ @stack = [ ]
300+ end
301+
302+ # Add a middleware to the error context stack.
303+ def use ( middleware )
304+ unless middleware . respond_to? ( :call )
305+ raise ArgumentError , "Error context middleware must respond to #call"
306+ end
307+
308+ @stack << middleware
309+ end
310+
311+ # Run all middlewares in the stack on an error and context.
312+ def execute ( error , context )
313+ @stack . inject ( context ) { |c , middleware | middleware . call ( error , c ) }
314+ end
315+ end
275316 end
276317end
0 commit comments