Skip to content

With register_error_subscriber, view errors are reported twice: the wrapper by the middleware, and its cause by the error subscriber #3166

Open

Description

@ClayShentrup

Issue Description

With config.rails.register_error_subscriber = true, every exception raised in a view becomes two Sentry issues.

  • Sentry::Rails::CaptureExceptions captures the ActionView::Template::Error and marks that object with @__sentry_captured.
  • ActionDispatch::Executor reports ExceptionWrapper#unwrapped_exception to Rails.error. For a template error, that's the wrapper's cause.
  • Sentry::Rails::ErrorSubscriber forwards that report. The cause isn't the marked object, so Hub#capture_exception doesn't skip it.

The two events have different types and stacks, so they group separately. When reloading is off, as in production, the executor reports outside the middleware's scope, so the second event also has no request URL or transaction. In development, ActionDispatch::Reloader reports the cause first, from inside the scope.

Related, from the other direction: #2931

Reproduction Steps

Below is a single-file app. enable_reloading = false matches production, and setting it to true shows the development order.

require "bundler/inline"
gemfile { source "https://rubygems.org"; gem "rails", "8.1.4"; gem "sentry-rails", "7.1.0" }
require "action_controller/railtie"

EVENTS = []

class ReproApp < Rails::Application
  config.root = __dir__
  config.eager_load = false
  config.enable_reloading = false
  config.consider_all_requests_local = false
  config.hosts.clear
  config.secret_key_base = "x" * 64
  config.logger = Logger.new(nil)
  routes.append { get "/" => "pages#show" }
end

Sentry.init do |c|
  c.dsn = "https://key@o0.ingest.sentry.io/0"
  c.transport.transport_class = Sentry::DummyTransport
  c.background_worker_threads = 0
  c.rails.register_error_subscriber = true
  c.before_send = ->(event, hint) { EVENTS << "#{hint[:exception].class} url=#{event.request&.url.inspect}"; nil }
end

class PagesController < ActionController::Base
  def show = render(inline: "<%= raise 'boom' %>")
end

ReproApp.initialize!
ReproApp.call(Rack::MockRequest.env_for("/"))
p EVENTS

Output:

["ActionView::Template::Error url=\"http://example.org/\"", "RuntimeError url=nil"]

Expected Behavior

One event per view error, with request context.

Suggested fix

We tested this in the same app: it produced one event with request context, with reloading on and off. In Hub#capture_exception:

  • skip capturing when any exception in the cause chain is already marked as captured;
  • mark the whole chain after capturing.

The event already includes the chain, via Sentry::Utils::ExceptionCauseChain, so a later capture of any part of it duplicates information Sentry already has.

Versions

Ruby 4.0.9, Rails 8.1.4, sentry-ruby / sentry-rails 7.1.0.

Workaround

Leave register_error_subscriber off and subscribe a Rails.error subscriber that forwards only handled: true reports.

Activity

  1. linear-code commented on Oct 30, 2026

    @linear-code
  2. added a commit that references this issue on Nov 3, 2026
    a240ac1
  3. self-assigned this
    on Nov 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions