Skip to content

Commit 0b8e083

Browse files
andrewn617dersam
authored andcommitted
Introduce ErrorReporter context middleware
Co-authored-by: Sam Schmidt <[email protected]> When reporting an error, the error context middleware will be called with the reported error and base execution context. The stack may mutate the context hash. The mutated context will then be passed to error subscribers.
1 parent a72205e commit 0b8e083

File tree

3 files changed

+86
-1
lines changed

3 files changed

+86
-1
lines changed

activesupport/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
* Introduce ActiveSupport::ErrorReporter#add_middleware
2+
3+
When reporting an error, the error context middleware will be called with the reported error
4+
and base execution context. The stack may mutate the context hash. The mutated context will
5+
then be passed to error subscribers.
6+
7+
*Andrew Novoselac*, *Sam Schmidt*
8+
19
* Change execution wrapping to report all exceptions, including `Exception`.
210

311
If a more serious error like `SystemStackError` or `NoMemoryError` happens,

activesupport/lib/active_support/error_reporter.rb

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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
276317
end

activesupport/test/error_reporter_test.rb

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,4 +362,40 @@ def report(_error, handled:, severity:, context:, source:)
362362
expected = "Error subscriber raised an error: Big Oopsie (ErrorReporterTest::FailingErrorSubscriber::Error)"
363363
assert_equal expected, log.string.lines.first.chomp
364364
end
365+
366+
test "error context middleware can mutate context hash" do
367+
middleware = -> (_, context) { context.merge({ foo: :bar }) }
368+
369+
error = ArgumentError.new("Oops")
370+
371+
@reporter.add_middleware(middleware)
372+
@reporter.report(error)
373+
374+
assert_equal [[error, true, :warning, "application", { foo: :bar }]], @subscriber.events
375+
end
376+
377+
class MyErrorContextMiddleware
378+
def call(_, context)
379+
context.merge({ bar: :baz })
380+
end
381+
end
382+
383+
test "can have multiple error context middlewares" do
384+
@reporter.add_middleware(-> (_, context) { context.merge({ foo: :bar }) })
385+
@reporter.add_middleware(MyErrorContextMiddleware.new)
386+
387+
error = ArgumentError.new("Oops")
388+
@reporter.report(error)
389+
390+
assert_equal [[error, true, :warning, "application", { foo: :bar, bar: :baz }]], @subscriber.events
391+
end
392+
393+
test "last error context middleware to update a key wins" do
394+
@reporter.add_middleware(-> (_, context) { context.merge({ foo: :bar }) })
395+
@reporter.add_middleware(-> (_, context) { context.merge({ foo: :baz }) })
396+
error = ArgumentError.new("Oops")
397+
@reporter.report(error)
398+
399+
assert_equal [[error, true, :warning, "application", { foo: :baz }]], @subscriber.events
400+
end
365401
end

0 commit comments

Comments
 (0)