diff --git a/CHANGELOG.md b/CHANGELOG.md index 4207eec59..0ad988113 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,10 @@ - Pick up config.cron.default_timezone from Rails config ([#2213](https://github.com/getsentry/sentry-ruby/pull/2213)) +### Bug Fixes + +- Sentry will not record traces of HTTP OPTIONS and HEAD requests in Rack and Rails apps [#2181](https://github.com/getsentry/sentry-ruby/pull/2181) + ## 5.15.2 ### Bug Fixes diff --git a/sentry-rails/lib/sentry/rails/action_cable.rb b/sentry-rails/lib/sentry/rails/action_cable.rb index 155377466..b8dd51a1b 100644 --- a/sentry-rails/lib/sentry/rails/action_cable.rb +++ b/sentry-rails/lib/sentry/rails/action_cable.rb @@ -3,6 +3,7 @@ module Rails module ActionCableExtensions class ErrorHandler OP_NAME = "websocket.server".freeze + IGNORED_HTTP_METHODS = ["HEAD", "OPTIONS"].freeze class << self def capture(connection, transaction_name:, extra_context: nil, &block) @@ -33,6 +34,8 @@ def capture(connection, transaction_name:, extra_context: nil, &block) end def start_transaction(env, scope) + return nil if IGNORED_HTTP_METHODS.include?(env["REQUEST_METHOD"]) + options = { name: scope.transaction_name, source: scope.transaction_source, op: OP_NAME } transaction = Sentry.continue_trace(env, **options) Sentry.start_transaction(transaction: transaction, **options) diff --git a/sentry-rails/lib/sentry/rails/capture_exceptions.rb b/sentry-rails/lib/sentry/rails/capture_exceptions.rb index 8d93784ed..5b7c28abe 100644 --- a/sentry-rails/lib/sentry/rails/capture_exceptions.rb +++ b/sentry-rails/lib/sentry/rails/capture_exceptions.rb @@ -2,6 +2,7 @@ module Sentry module Rails class CaptureExceptions < Sentry::Rack::CaptureExceptions RAILS_7_1 = Gem::Version.new(::Rails.version) >= Gem::Version.new("7.1.0.alpha") + IGNORED_HTTP_METHODS = ["HEAD", "OPTIONS"].freeze def initialize(_) super @@ -32,6 +33,8 @@ def capture_exception(exception, env) end def start_transaction(env, scope) + return nil if IGNORED_HTTP_METHODS.include?(env["REQUEST_METHOD"]) + options = { name: scope.transaction_name, source: scope.transaction_source, op: transaction_op } if @assets_regexp && scope.transaction_name.match?(@assets_regexp) diff --git a/sentry-ruby/lib/sentry/rack/capture_exceptions.rb b/sentry-ruby/lib/sentry/rack/capture_exceptions.rb index 2248799a3..f5c7862b4 100644 --- a/sentry-ruby/lib/sentry/rack/capture_exceptions.rb +++ b/sentry-ruby/lib/sentry/rack/capture_exceptions.rb @@ -4,6 +4,7 @@ module Sentry module Rack class CaptureExceptions ERROR_EVENT_ID_KEY = "sentry.error_event_id" + IGNORED_HTTP_METHODS = ["HEAD", "OPTIONS"].freeze def initialize(app) @app = app @@ -62,7 +63,18 @@ def capture_exception(exception, env) end def start_transaction(env, scope) - options = { name: scope.transaction_name, source: scope.transaction_source, op: transaction_op } + # Tell Sentry to not sample this transaction if this is an HTTP OPTIONS or HEAD request. + # Return early to avoid extra work that's not useful anyway, because this + # transaction won't be sampled. + # If we return nil here, it'll be passed to `finish_transaction` later, which is safe. + return nil if IGNORED_HTTP_METHODS.include?(env["REQUEST_METHOD"]) + + options = { + name: scope.transaction_name, + source: scope.transaction_source, + op: transaction_op + } + transaction = Sentry.continue_trace(env, **options) Sentry.start_transaction(transaction: transaction, custom_sampling_context: { env: env }, **options) end diff --git a/sentry-ruby/spec/sentry/rack/capture_exceptions_spec.rb b/sentry-ruby/spec/sentry/rack/capture_exceptions_spec.rb index 24a90c87b..f8cbae0e5 100644 --- a/sentry-ruby/spec/sentry/rack/capture_exceptions_spec.rb +++ b/sentry-ruby/spec/sentry/rack/capture_exceptions_spec.rb @@ -178,6 +178,7 @@ def inspect expect(event.breadcrumbs.count).to eq(1) expect(event.breadcrumbs.peek.message).to eq("request breadcrumb") end + it "doesn't pollute the top-level scope" do request_1 = lambda do |e| Sentry.configure_scope { |s| s.set_tags({tag_1: "foo"}) } @@ -192,6 +193,7 @@ def inspect expect(event.tags).to eq(tag_1: "foo") expect(Sentry.get_current_scope.tags).to eq(tag_1: "don't change me") end + it "doesn't pollute other request's scope" do request_1 = lambda do |e| Sentry.configure_scope { |s| s.set_tags({tag_1: "foo"}) } @@ -295,6 +297,20 @@ def will_be_sampled_by_sdk verify_transaction_attributes(transaction) verify_transaction_inherits_external_transaction(transaction, external_transaction) end + + context "performing an HTTP OPTIONS request" do + let(:additional_headers) do + { method: "OPTIONS" } + end + + it "doesn't sample transaction" do + wont_be_sampled_by_sdk + + stack.call(env) + + expect(transaction).to be_nil + end + end end context "with unsampled trace" do