From 19819e92cab893487f9c31d0b4beb631300a3a01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Daxb=C3=B6ck?= Date: Wed, 4 Feb 2026 20:48:47 +0100 Subject: [PATCH 1/6] fix(rails): Use ActionDispatch::ExceptionWrapper for correct HTTP status codes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes an issue where 404 errors (and other non-500 exceptions) were incorrectly reported with HTTP status code 500 in Sentry transaction traces when Rails public error pages were served. Root cause: - In sentry-ruby/lib/sentry/rack/capture_exceptions.rb, when an exception was caught and re-raised, the transaction status was hardcoded to 500 - This caused all exceptions to be reported as 500, even if Rails would render them with different status codes (e.g., 404 for RoutingError) Solution: - Added status_code_for_exception(exception) method to Rack::CaptureExceptions that returns 500 by default (preserving existing behavior) - Override status_code_for_exception in Rails::CaptureExceptions to use ActionDispatch::ExceptionWrapper.status_code_for_exception which knows the correct HTTP status code mapping for Rails exceptions - This ensures RoutingError → 404, RecordNotFound → 404, etc. Changes: - sentry-ruby/lib/sentry/rack/capture_exceptions.rb: Extract status code determination into overridable method - sentry-rails/lib/sentry/rails/capture_exceptions.rb: Override to use ActionDispatch::ExceptionWrapper - sentry-rails/spec/sentry/rails/tracing_spec.rb: Add test for 404 status code with public error pages enabled --- .../lib/sentry/rails/capture_exceptions.rb | 4 ++++ .../spec/sentry/rails/tracing_spec.rb | 23 +++++++++++++++++++ .../lib/sentry/rack/capture_exceptions.rb | 6 ++++- 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/sentry-rails/lib/sentry/rails/capture_exceptions.rb b/sentry-rails/lib/sentry/rails/capture_exceptions.rb index 5e581c00a..4f11a5eec 100644 --- a/sentry-rails/lib/sentry/rails/capture_exceptions.rb +++ b/sentry-rails/lib/sentry/rails/capture_exceptions.rb @@ -60,6 +60,10 @@ def show_exceptions?(exception, env) request.show_exceptions? end end + + def status_code_for_exception(exception) + ActionDispatch::ExceptionWrapper.status_code_for_exception(exception.class.name) + end end end end diff --git a/sentry-rails/spec/sentry/rails/tracing_spec.rb b/sentry-rails/spec/sentry/rails/tracing_spec.rb index aee2c38f9..debd1264a 100644 --- a/sentry-rails/spec/sentry/rails/tracing_spec.rb +++ b/sentry-rails/spec/sentry/rails/tracing_spec.rb @@ -112,6 +112,29 @@ end end + context "with report_rescued_exceptions and public error pages" do + before do + make_basic_app do |config, app| + config.traces_sample_rate = 1.0 + config.rails.report_rescued_exceptions = true + # Enable capturing RoutingError which is excluded by default + config.excluded_exceptions -= ['ActionController::RoutingError'] + app.config.consider_all_requests_local = false + end + end + + it "records correct status code for 404 errors served by public error pages" do + get "/nonexistent_route" + + expect(response).to have_http_status(:not_found) + expect(transport.events.count).to be >= 1 + + # Verify the captured event has correct 404 status in trace data, not 500 + event = transport.events.first.to_h + expect(event.dig(:contexts, :trace, :data, "http.response.status_code")).to eq(404) + end + end + describe "filtering pii data" do context "with send_default_pii = false" do before do diff --git a/sentry-ruby/lib/sentry/rack/capture_exceptions.rb b/sentry-ruby/lib/sentry/rack/capture_exceptions.rb index 40cdfb4f8..a475fa79a 100644 --- a/sentry-ruby/lib/sentry/rack/capture_exceptions.rb +++ b/sentry-ruby/lib/sentry/rack/capture_exceptions.rb @@ -33,7 +33,7 @@ def call(env) raise # Don't capture Sentry errors rescue Exception => e capture_exception(e, env) - finish_transaction(transaction, 500) + finish_transaction(transaction, status_code_for_exception(e)) raise end @@ -86,6 +86,10 @@ def finish_transaction(transaction, status_code) def mechanism Sentry::Mechanism.new(type: MECHANISM_TYPE, handled: false) end + + def status_code_for_exception(exception) + 500 + end end end end From b7b27b2d4215acd5d037861a14b3407132e34b0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Daxb=C3=B6ck?= Date: Wed, 4 Feb 2026 21:38:37 +0100 Subject: [PATCH 2/6] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5a6cf977..8bfab338e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### Bug Fixes +- Use `ActionDispatch::ExceptionWrapper` for correct HTTP status code ([#2850](https://github.com/getsentry/sentry-ruby/pull/2850)) - Add explicit dependency on logger gem to fix Ruby 4.0 warning ([#2837](https://github.com/getsentry/sentry-ruby/pull/2837)) ### Internal From 643144855ab48f4287ae43f4c1f1f4e8a16d1367 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Daxb=C3=B6ck?= Date: Wed, 4 Feb 2026 23:07:04 +0100 Subject: [PATCH 3/6] Fix test transaction status code is not 500 anymore, but 404, so filtered out --- sentry-rails/spec/sentry/rails/tracing_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry-rails/spec/sentry/rails/tracing_spec.rb b/sentry-rails/spec/sentry/rails/tracing_spec.rb index debd1264a..651d57a16 100644 --- a/sentry-rails/spec/sentry/rails/tracing_spec.rb +++ b/sentry-rails/spec/sentry/rails/tracing_spec.rb @@ -246,7 +246,7 @@ get "/assets/application-ad022df6f1289ec07a560bb6c9a227ecf7bdd5a5cace5e9a8cdbd50b454931fb.css" expect(response).to have_http_status(:not_found) - expect(transport.events.count).to eq(1) + expect(transport.events.count).to eq(0) end end end From b4b4f171092ecf88af936893a9eb188b56ef1789 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Daxb=C3=B6ck?= Date: Thu, 5 Feb 2026 13:34:55 +0100 Subject: [PATCH 4/6] Update tracing tests add test from Peter for and update existing asset 404 test to accept 404 to not have to change assertion and actually send transaction --- .../spec/sentry/rails/tracing_spec.rb | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/sentry-rails/spec/sentry/rails/tracing_spec.rb b/sentry-rails/spec/sentry/rails/tracing_spec.rb index 651d57a16..56248f6fe 100644 --- a/sentry-rails/spec/sentry/rails/tracing_spec.rb +++ b/sentry-rails/spec/sentry/rails/tracing_spec.rb @@ -135,6 +135,39 @@ end end + context "with public error pages for controller exceptions" do + before do + make_basic_app do |config, app| + config.traces_sample_rate = 1.0 + # Don't enable report_rescued_exceptions since RecordNotFound is excluded by default anyway + app.config.consider_all_requests_local = false + # Allow 404 status codes to be traced (default ignores 401-404) + config.trace_ignore_status_codes = [(301..303), (305..399)] + end + end + + it "records correct status when Rails rescues exception to non-500 status" do + get "/posts/999999" + + expect(response).to have_http_status(:not_found) + expect(transport.events.count).to eq(1) + + transaction = transport.events.last.to_h + + expect(transaction[:type]).to eq("transaction") + # Transaction should have correct 404 status (this is what the PR fixes) + expect(transaction.dig(:contexts, :trace, :status)).to eq("not_found") + expect(transaction.dig(:contexts, :trace, :data, "http.response.status_code")).to eq(404) + + # Controller action span will still show internal_error because exception was raised + first_span = transaction[:spans][0] + expect(first_span[:op]).to eq("view.process_action.action_controller") + expect(first_span[:description]).to eq("PostsController#show") + expect(first_span[:status]).to eq("internal_error") + expect(first_span[:data]["http.response.status_code"]).to eq(500) + end + end + describe "filtering pii data" do context "with send_default_pii = false" do before do @@ -233,6 +266,8 @@ config.traces_sample_rate = 1.0 config.sdk_logger = logger config.rails.assets_regexp = %r{/foo/} + # Allow 404 status codes to be traced (default ignores 401-404) + config.trace_ignore_status_codes = [(301..303), (305..399)] end end @@ -246,7 +281,7 @@ get "/assets/application-ad022df6f1289ec07a560bb6c9a227ecf7bdd5a5cace5e9a8cdbd50b454931fb.css" expect(response).to have_http_status(:not_found) - expect(transport.events.count).to eq(0) + expect(transport.events.count).to eq(1) end end end From e6d46b6cdfd96f26027ee3ae1915cafdc50f13a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Daxb=C3=B6ck?= Date: Thu, 5 Feb 2026 14:10:33 +0100 Subject: [PATCH 5/6] =?UTF-8?q?Fix=20spec=20=F0=9F=A4=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sentry-rails/spec/sentry/rails/tracing_spec.rb | 6 ------ 1 file changed, 6 deletions(-) diff --git a/sentry-rails/spec/sentry/rails/tracing_spec.rb b/sentry-rails/spec/sentry/rails/tracing_spec.rb index 56248f6fe..750551222 100644 --- a/sentry-rails/spec/sentry/rails/tracing_spec.rb +++ b/sentry-rails/spec/sentry/rails/tracing_spec.rb @@ -117,7 +117,6 @@ make_basic_app do |config, app| config.traces_sample_rate = 1.0 config.rails.report_rescued_exceptions = true - # Enable capturing RoutingError which is excluded by default config.excluded_exceptions -= ['ActionController::RoutingError'] app.config.consider_all_requests_local = false end @@ -129,7 +128,6 @@ expect(response).to have_http_status(:not_found) expect(transport.events.count).to be >= 1 - # Verify the captured event has correct 404 status in trace data, not 500 event = transport.events.first.to_h expect(event.dig(:contexts, :trace, :data, "http.response.status_code")).to eq(404) end @@ -139,9 +137,7 @@ before do make_basic_app do |config, app| config.traces_sample_rate = 1.0 - # Don't enable report_rescued_exceptions since RecordNotFound is excluded by default anyway app.config.consider_all_requests_local = false - # Allow 404 status codes to be traced (default ignores 401-404) config.trace_ignore_status_codes = [(301..303), (305..399)] end end @@ -155,11 +151,9 @@ transaction = transport.events.last.to_h expect(transaction[:type]).to eq("transaction") - # Transaction should have correct 404 status (this is what the PR fixes) expect(transaction.dig(:contexts, :trace, :status)).to eq("not_found") expect(transaction.dig(:contexts, :trace, :data, "http.response.status_code")).to eq(404) - # Controller action span will still show internal_error because exception was raised first_span = transaction[:spans][0] expect(first_span[:op]).to eq("view.process_action.action_controller") expect(first_span[:description]).to eq("PostsController#show") From f5b9888678ed8cace60c24c88a7974216ca7aa4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Daxb=C3=B6ck?= Date: Thu, 5 Feb 2026 17:57:08 +0100 Subject: [PATCH 6/6] Add fix for Rails < 6 --- sentry-rails/spec/sentry/rails/tracing_spec.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/sentry-rails/spec/sentry/rails/tracing_spec.rb b/sentry-rails/spec/sentry/rails/tracing_spec.rb index 750551222..6364c19f7 100644 --- a/sentry-rails/spec/sentry/rails/tracing_spec.rb +++ b/sentry-rails/spec/sentry/rails/tracing_spec.rb @@ -139,6 +139,13 @@ config.traces_sample_rate = 1.0 app.config.consider_all_requests_local = false config.trace_ignore_status_codes = [(301..303), (305..399)] + + # In Rails < 6.0, ActiveRecord::RecordNotFound is not automatically mapped to :not_found + # https://github.com/rails/rails/blob/main/guides/source/configuring.md?configaction_dispatchrescue_responses + # We need to add it to the rescue_responses hash + if Rails.gem_version < Gem::Version.new("6.0.0") + ActionDispatch::ExceptionWrapper.rescue_responses['ActiveRecord::RecordNotFound'] = :not_found + end end end