Skip to content

Commit cff159f

Browse files
authored
Merge pull request rails#54512 from Shopify/error-reporter-metadata-provider
Introduce ActiveSupport::ErrorReport#metadata_provider
2 parents a72205e + 0b8e083 commit cff159f

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)