Rails adds source attribute to its ErrorReporting API


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.

Join Our Newsletter