Skip to content

Commit

Permalink
Register Sentry's ErrorSubscriber for Rails 7.0+ apps (#1705)
Browse files Browse the repository at this point in the history
* Register Sentry's ErrorSubscriber for Rails 7.0+ apps

* Mark captured exceptions to avoid duplicated reporting

* Prevent ActiveSupport::ExecutionContext pollute reported events in tests

* Update changelog
  • Loading branch information
st0012 authored Feb 1, 2022
1 parent 4217838 commit 9889bbe
Show file tree
Hide file tree
Showing 6 changed files with 66 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
### Features

- Support serializing ActiveRecord job arguments in global id form [#1688](https://github.com/getsentry/sentry-ruby/pull/1688)
- Register Sentry's ErrorSubscriber for Rails 7.0+ apps [#1705](https://github.com/getsentry/sentry-ruby/pull/1705)

## 5.0.2

Expand Down
20 changes: 20 additions & 0 deletions sentry-rails/lib/sentry/rails/error_subscriber.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module Sentry
module Rails
# This is not a user-facing class. You should use it with Rails 7.0's error reporter feature and its interfaces.
# See https://github.com/rails/rails/blob/main/activesupport/lib/active_support/error_reporter.rb for more information.
class ErrorSubscriber
def report(error, handled:, severity:, context:)
# a component may already have an integration to capture exceptions while its operation is also wrapped inside an `app.executor.wrap` (e.g. ActionCable)
# in such condition, the exception would be captured repeatedly. it usually happens in this order:
#
# 1. exception captured and reported by the component integration and re-raised
# 2. exception captured by the executor, which then reports it with executor.error_reporter
#
# and because there's no direct communication between the 2 callbacks, we need a way to identify if an exception has been captured before
# using a Sentry-specific intance variable should be the last impactful way
return if error.instance_variable_get(:@__sentry_captured)
Sentry::Rails.capture_exception(error, level: severity, contexts: { "rails.error" => context }, tags: { handled: handled })
end
end
end
end
7 changes: 7 additions & 0 deletions sentry-rails/lib/sentry/rails/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ class Railtie < ::Rails::Railtie
setup_backtrace_cleanup_callback
inject_breadcrumbs_logger
activate_tracing

register_error_subscriber(app) if ::Rails.version.to_f >= 7.0
end

runner do
Expand Down Expand Up @@ -115,5 +117,10 @@ def activate_tracing
Sentry::Rails::Tracing.patch_active_support_notifications
end
end

def register_error_subscriber(app)
require "sentry/rails/error_subscriber"
app.executor.error_reporter.subscribe(Sentry::Rails::ErrorSubscriber.new)
end
end
end
29 changes: 29 additions & 0 deletions sentry-rails/spec/sentry/rails_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -207,4 +207,33 @@
expect(Sentry.configuration.trusted_proxies).to eq(["5.5.5.5"])
end
end

describe "error reporter integration", skip: Rails.version.to_f < 7.0 do
before do
make_basic_app
end

it "registers Sentry::Rails::ErrorSubscriber to Rails" do
Rails.error.report(Exception.new, handled: false)

expect(transport.events.count).to eq(1)

ActiveSupport.error_reporter.report(Exception.new, handled: false)

expect(transport.events.count).to eq(2)
end

it "sets correct contextual data to the reported event" do
Rails.error.handle(severity: :info, context: { foo: "bar" }) do
1/0
end

expect(transport.events.count).to eq(1)

event = transport.events.first
expect(event.tags).to eq({ handled: true })
expect(event.level).to eq(:info)
expect(event.contexts).to include({ "rails.error" => { foo: "bar" }})
end
end
end
5 changes: 5 additions & 0 deletions sentry-rails/spec/support/test_rails_app/app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,11 @@ def make_basic_app
# the callbacks duplicate after each time we initialize the application and cause issues when they're executed
ActiveSupport::Executor.reset_callbacks(:run)
ActiveSupport::Executor.reset_callbacks(:complete)

# Rails uses this module to set a global context for its ErrorReporter feature.
# this needs to be cleared so previously set context won't pollute later reportings (see ErrorSubscriber).
ActiveSupport::ExecutionContext.clear if defined?(ActiveSupport::ExecutionContext)

if defined?(ActionCable)
ActionCable::Channel::Base.reset_callbacks(:subscribe)
ActionCable::Channel::Base.reset_callbacks(:unsubscribe)
Expand Down
5 changes: 4 additions & 1 deletion sentry-ruby/lib/sentry/hub.rb
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,10 @@ def capture_exception(exception, **options, &block)

return unless event

capture_event(event, **options, &block)
capture_event(event, **options, &block).tap do
# mark the exception as captured so we can use this information to avoid duplicated capturing
exception.instance_variable_set(:@__sentry_captured, true)
end
end

def capture_message(message, **options, &block)
Expand Down

0 comments on commit 9889bbe

Please sign in to comment.