diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ec6fd7db..7cb52ec0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,13 @@ ### Features - Make `:value` in `SingleExceptionInterface` writable, so that it can be modified in `before_send` under `event.exception.values[n].value` [#2072](https://github.com/getsentry/sentry-ruby/pull/2072) +- Add new `config.trace_propagation_targets` option to set targets for which headers are propagated in outgoing HTTP requests [#2079](https://github.com/getsentry/sentry-ruby/pull/2079) + + ```rb + # takes an array of strings or regexps + config.trace_propagation_targets = [/.*/] # default is to all targets + config.trace_propagation_targets = [/example.com/, 'foobar.org/api/v2'] + ``` ## 5.10.0 diff --git a/sentry-ruby/lib/sentry/configuration.rb b/sentry-ruby/lib/sentry/configuration.rb index fb619adcb..11c2ce3bd 100644 --- a/sentry-ruby/lib/sentry/configuration.rb +++ b/sentry-ruby/lib/sentry/configuration.rb @@ -243,6 +243,11 @@ def capture_exception_frame_locals=(value) # @return [Boolean] attr_accessor :auto_session_tracking + # Allowlist of outgoing request targets to which sentry-trace and baggage headers are attached. + # Default is all (/.*/) + # @return [Array] + attr_accessor :trace_propagation_targets + # The instrumenter to use, :sentry or :otel # @return [Symbol] attr_reader :instrumenter @@ -290,6 +295,8 @@ def capture_exception_frame_locals=(value) INSTRUMENTERS = [:sentry, :otel] + PROPAGATION_TARGETS_MATCH_ALL = /.*/.freeze + class << self # Post initialization callbacks are called at the end of initialization process # allowing extending the configuration of sentry-ruby by multiple extensions @@ -332,6 +339,7 @@ def initialize self.dsn = ENV['SENTRY_DSN'] self.server_name = server_name_from_env self.instrumenter = :sentry + self.trace_propagation_targets = [PROPAGATION_TARGETS_MATCH_ALL] self.before_send = nil self.before_send_transaction = nil diff --git a/sentry-ruby/lib/sentry/net/http.rb b/sentry-ruby/lib/sentry/net/http.rb index 470d1a796..989d8f393 100644 --- a/sentry-ruby/lib/sentry/net/http.rb +++ b/sentry-ruby/lib/sentry/net/http.rb @@ -30,13 +30,13 @@ def request(req, body = nil, &block) return super if from_sentry_sdk? Sentry.with_child_span(op: OP_NAME, start_timestamp: Sentry.utc_now.to_f) do |sentry_span| - set_sentry_trace_header(req, sentry_span) + request_info = extract_request_info(req) + set_sentry_trace_header(req, sentry_span, request_info) super.tap do |res| - record_sentry_breadcrumb(req, res) + record_sentry_breadcrumb(request_info, res) if sentry_span - request_info = extract_request_info(req) sentry_span.set_description("#{request_info[:method]} #{request_info[:url]}") sentry_span.set_data('url', request_info[:url]) sentry_span.set_data('http.method', request_info[:method]) @@ -49,10 +49,11 @@ def request(req, body = nil, &block) private - def set_sentry_trace_header(req, sentry_span) + def set_sentry_trace_header(req, sentry_span, request_info) return unless sentry_span client = Sentry.get_current_client + return unless propagate_trace?(request_info[:url], client.configuration.trace_propagation_targets) trace = client.generate_sentry_trace(sentry_span) req[SENTRY_TRACE_HEADER_NAME] = trace if trace @@ -61,11 +62,9 @@ def set_sentry_trace_header(req, sentry_span) req[BAGGAGE_HEADER_NAME] = baggage if baggage && !baggage.empty? end - def record_sentry_breadcrumb(req, res) + def record_sentry_breadcrumb(request_info, res) return unless Sentry.initialized? && Sentry.configuration.breadcrumbs_logger.include?(:http_logger) - request_info = extract_request_info(req) - crumb = Sentry::Breadcrumb.new( level: :info, category: BREADCRUMB_CATEGORY, @@ -96,6 +95,10 @@ def extract_request_info(req) result end + + def propagate_trace?(url, trace_propagation_targets) + url && trace_propagation_targets.any? { |target| url.match?(target) } + end end end end diff --git a/sentry-ruby/spec/sentry/configuration_spec.rb b/sentry-ruby/spec/sentry/configuration_spec.rb index ac49db207..6cd571600 100644 --- a/sentry-ruby/spec/sentry/configuration_spec.rb +++ b/sentry-ruby/spec/sentry/configuration_spec.rb @@ -511,6 +511,17 @@ class SentryConfigurationSample < Sentry::Configuration end end + describe "#trace_propagation_targets" do + it "returns match all by default" do + expect(subject.trace_propagation_targets).to eq([/.*/]) + end + + it "accepts array of strings or regexps" do + subject.trace_propagation_targets = ["example.com", /foobar.org\/api\/v2/] + expect(subject.trace_propagation_targets).to eq(["example.com", /foobar.org\/api\/v2/]) + end + end + describe "#instrumenter" do it "returns :sentry by default" do expect(subject.instrumenter).to eq(:sentry) diff --git a/sentry-ruby/spec/sentry/net/http_spec.rb b/sentry-ruby/spec/sentry/net/http_spec.rb index 35279ad81..dc4605ffc 100644 --- a/sentry-ruby/spec/sentry/net/http_spec.rb +++ b/sentry-ruby/spec/sentry/net/http_spec.rb @@ -216,6 +216,76 @@ end end + context "with custom trace_propagation_targets" do + before do + Sentry.configuration.trace_propagation_targets = ["example.com", /foobar.org\/api\/v2/] + end + + it "doesn't add sentry headers to outgoing requests to different target" do + stub_normal_response + + uri = URI("http://google.com/path") + http = Net::HTTP.new(uri.host, uri.port) + request = Net::HTTP::Get.new(uri.request_uri) + + transaction = Sentry.start_transaction + Sentry.get_current_scope.set_span(transaction) + + http.request(request) + + expect(request.key?("sentry-trace")).to eq(false) + expect(request.key?("baggage")).to eq(false) + end + + it "doesn't add sentry headers to outgoing requests to different target path" do + stub_normal_response + + uri = URI("http://foobar.org/api/v1/path") + http = Net::HTTP.new(uri.host, uri.port) + request = Net::HTTP::Get.new(uri.request_uri) + + transaction = Sentry.start_transaction + Sentry.get_current_scope.set_span(transaction) + + http.request(request) + + expect(request.key?("sentry-trace")).to eq(false) + expect(request.key?("baggage")).to eq(false) + end + + it "adds sentry headers to outgoing requests matching string" do + stub_normal_response + + uri = URI("http://example.com/path") + http = Net::HTTP.new(uri.host, uri.port) + request = Net::HTTP::Get.new(uri.request_uri) + + transaction = Sentry.start_transaction + Sentry.get_current_scope.set_span(transaction) + + http.request(request) + + expect(request.key?("sentry-trace")).to eq(true) + expect(request.key?("baggage")).to eq(true) + end + + it "adds sentry headers to outgoing requests matching regexp" do + stub_normal_response + + uri = URI("http://foobar.org/api/v2/path") + http = Net::HTTP.new(uri.host, uri.port) + request = Net::HTTP::Get.new(uri.request_uri) + + transaction = Sentry.start_transaction + Sentry.get_current_scope.set_span(transaction) + + http.request(request) + + expect(request.key?("sentry-trace")).to eq(true) + expect(request.key?("baggage")).to eq(true) + end + end + it "doesn't record span for the SDK's request" do stub_sentry_response