Rails provides a common interface, ActiveSupport::ErrorReporter, for error reporting services. his allows external gems such as HoneyBadger, Sentry, Rollbar and more to standardize their monitoring of errors throughout the platform.
The ActiveSupport::ErrorReporter follows a pub-sub pattern, where subscribers can register to receive error reports. Every subscriber must be registered and respond to,
report(Exception, handled: Boolean, context: Hash)
The context hash is used to provide additional information about the error. For example, the context hash can include the current user, the current request, the current controller, and more.
Here’s an example,
class ErrorSubscriber
attr_reader :events
def initialize
@events = []
end
def report(error, handled:, context:)
@events << [error, { handled: handled, context: context }]
end
end
To capture an error ActiveSupport::ErrorReporter provides two methods, handle
and record
.
The basic difference is that handle
swallows the error while record
re-raises the error.
Both methods take an exception
and an optional context hash.
irb(main):001:1* Rails.error.handle do
irb(main):002:1* 1 + '1'
irb(main):003:0> end
=> nil
irb(main):004:1* Rails.error.record do
irb(main):005:1* 1 + '1'
irb(main):006:0> end
(irb):5:in `+': String can't be coerced into Integer (TypeError)
All captured errors are sent to all registered subscribers.
Before
In order to capture the source of the error, users previously could send the source as part of the context hash. This was not ideal because it was not standardized and hence was not possible to filter on the source.
Rails.error.record do
1 + '1'
end, context: { source: 'my_app' }
After
Thanks to this PR,
Rails now provides a standardized way to capture the source of the error.
The source can be set using the source
attribute of the ErrorReporter.
@reporter.record(source: "myapp") do
1 + '1'
end
This works in a similar fashion to the severity
attribute.
The source can be set to any string value.
The default value is application
.
Let’s now see an example. First let’s modify the Subscriber to capture the source
and severity
attribute.
We then register the subscriber
and then capture an error with a source.
irb(main):001:1* class ErrorSubscriber
irb(main):002:1* attr_reader :events
irb(main):003:1*
irb(main):004:2* def initialize
irb(main):005:2* @events = []
irb(main):006:1* end
irb(main):007:1*
irb(main):008:2* def report(error, handled:, severity:, source:, context:)
irb(main):009:2* @events << [error, { handled: handled, severity: severity, source: source, context: context }]
irb(main):010:1* end
irb(main):011:0> end
=> :report
irb(main):012:0> @reporter = ActiveSupport::ErrorReporter.new
=> #<ActiveSupport::ErrorReporter:0x0000000112c8d170 @logger=nil, @subscribers=[]>
irb(main):013:0> @subscriber = ErrorSubscriber.new
irb(main):014:0> @reporter.subscribe(@subscriber)
=> [#<ErrorSubscriber:0x00000001126a4d80 @events=[]>]
irb(main):015:1* @reporter.record(source: "myapp") do
irb(main):016:1* 0/0
irb(main):017:0> end
(irb):16:in `/': divided by 0 (ZeroDivisionError)
irb(main):018:0> @subscriber.events
=> [[#<ZeroDivisionError: divided by 0>, {:handled=>false, :severity=>:error, :source=>"myapp", :context=>{}}]]
Now, we can further modify the error subscriber to ignore all “application” source errors.
class ErrorSubscriber
attr_reader :events
def initialize
@events = []
end
def report(error, handled:, severity:, source:, context:)
return if source == "application"
@events << [error, { handled: handled, severity: severity, source: source, context: context }]
end
end
This makes it easier to filter out internal errors and focus on specific errors.