diff --git a/CHANGELOG.md b/CHANGELOG.md index 62448291a..60284abc7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## Unreleased + +### Internal + +- Add external_propagation_context support ([#2841](https://github.com/getsentry/sentry-ruby/pull/2841)) + ## 6.3.0 ### Features diff --git a/sentry-ruby/lib/sentry-ruby.rb b/sentry-ruby/lib/sentry-ruby.rb index fbdbc6b50..5a88ae59a 100644 --- a/sentry-ruby/lib/sentry-ruby.rb +++ b/sentry-ruby/lib/sentry-ruby.rb @@ -666,6 +666,38 @@ def sdk_meta META end + # Registers a callback function that retrieves the current external propagation context. + # This is used by OpenTelemetry integration to provide trace_id and span_id from OTel context. + # + # @param callback [Proc, nil] A callable that returns [trace_id, span_id] or nil + # @return [void] + # + # @example + # Sentry.register_external_propagation_context do + # span_context = OpenTelemetry::Trace.current_span.context + # return nil unless span_context.valid? + # [span_context.hex_trace_id, span_context.hex_span_id] + # end + def register_external_propagation_context(&callback) + @external_propagation_context_callback = callback + end + + # Returns the external propagation context (trace_id, span_id) if a callback is registered. + # + # @return [Array, nil] A tuple of [trace_id, span_id] or nil if no context is available + def get_external_propagation_context + return nil unless @external_propagation_context_callback + + @external_propagation_context_callback.call + rescue => e + sdk_logger&.debug(LOGGER_PROGNAME) { "Error getting external propagation context: #{e.message}" } if initialized? + nil + end + + def clear_external_propagation_context + @external_propagation_context_callback = nil + end + # @!visibility private def utc_now Time.now.utc diff --git a/sentry-ruby/lib/sentry/scope.rb b/sentry-ruby/lib/sentry/scope.rb index 6d0ec2d36..2d77b289e 100644 --- a/sentry-ruby/lib/sentry/scope.rb +++ b/sentry-ruby/lib/sentry/scope.rb @@ -60,19 +60,10 @@ def apply_to_event(event, hint = nil) event.attachments = attachments end - if span - event.contexts[:trace] ||= span.get_trace_context - - if event.respond_to?(:dynamic_sampling_context) - event.dynamic_sampling_context ||= span.get_dynamic_sampling_context - end - else - event.contexts[:trace] ||= propagation_context.get_trace_context - - if event.respond_to?(:dynamic_sampling_context) - event.dynamic_sampling_context ||= propagation_context.get_dynamic_sampling_context - end - end + trace_context = get_trace_context + dynamic_sampling_context = trace_context.delete(:dynamic_sampling_context) + event.contexts[:trace] ||= trace_context + event.dynamic_sampling_context ||= dynamic_sampling_context all_event_processors = self.class.global_event_processors + @event_processors @@ -94,7 +85,7 @@ def apply_to_event(event, hint = nil) # @return [MetricEvent, LogEvent] the telemetry event with scope context applied def apply_to_telemetry(telemetry) # TODO-neel when new scope set_attribute api is added: add them here - trace_context = span ? span.get_trace_context : propagation_context.get_trace_context + trace_context = get_trace_context telemetry.trace_id = trace_context[:trace_id] telemetry.span_id = trace_context[:span_id] @@ -305,6 +296,20 @@ def get_span span end + # Returns the trace context for this scope. + # Prioritizes external propagation context (from OTel) over local propagation context. + # @return [Hash] + def get_trace_context + if span + span.get_trace_context.merge(dynamic_sampling_context: span.get_dynamic_sampling_context) + elsif (external_context = Sentry.get_external_propagation_context) + trace_id, span_id = external_context + { trace_id: trace_id, span_id: span_id } + else + propagation_context.get_trace_context.merge(dynamic_sampling_context: propagation_context.get_dynamic_sampling_context) + end + end + # Sets the scope's fingerprint attribute. # @param fingerprint [Array] # @return [Array] diff --git a/sentry-ruby/spec/sentry/scope_spec.rb b/sentry-ruby/spec/sentry/scope_spec.rb index 0f4cbfae7..a31dda16f 100644 --- a/sentry-ruby/spec/sentry/scope_spec.rb +++ b/sentry-ruby/spec/sentry/scope_spec.rb @@ -187,6 +187,78 @@ end end + describe "#get_trace_context" do + before { perform_basic_setup } + + context "with span" do + let(:transaction) { Sentry::Transaction.new(op: "test") } + + before do + subject.set_span(transaction) + end + + it "returns the span's trace context with dynamic_sampling_context" do + trace_context = subject.get_trace_context + expect(trace_context[:trace_id]).to eq(transaction.trace_id) + expect(trace_context[:span_id]).to eq(transaction.span_id) + expect(trace_context[:op]).to eq("test") + expect(trace_context[:dynamic_sampling_context]).to eq(transaction.get_dynamic_sampling_context) + end + + it "prioritizes span over external propagation context" do + Sentry.register_external_propagation_context do + ["abc123def456789012345678901234ab", "1234567890abcdef"] + end + + trace_context = subject.get_trace_context + expect(trace_context[:trace_id]).to eq(transaction.trace_id) + expect(trace_context[:dynamic_sampling_context]).to eq(transaction.get_dynamic_sampling_context) + + Sentry.clear_external_propagation_context + end + end + + context "with external propagation context" do + let(:external_trace_id) { "abc123def456789012345678901234ab" } + let(:external_span_id) { "1234567890abcdef" } + + before do + Sentry.register_external_propagation_context do + [external_trace_id, external_span_id] + end + end + + after do + Sentry.clear_external_propagation_context + end + + it "returns the external propagation context's trace context" do + trace_context = subject.get_trace_context + expect(trace_context[:trace_id]).to eq(external_trace_id) + expect(trace_context[:span_id]).to eq(external_span_id) + end + end + + context "when external propagation context callback returns nil" do + before do + Sentry.register_external_propagation_context do + nil + end + end + + after do + Sentry.clear_external_propagation_context + end + + it "falls back to local propagation context with dynamic_sampling_context" do + trace_context = subject.get_trace_context + expect(trace_context[:trace_id]).to eq(subject.propagation_context.trace_id) + expect(trace_context[:span_id]).to eq(subject.propagation_context.span_id) + expect(trace_context[:dynamic_sampling_context]).to eq(subject.propagation_context.get_dynamic_sampling_context) + end + end + end + describe "#apply_to_event" do before { perform_basic_setup } @@ -300,6 +372,7 @@ subject.apply_to_event(event) expect(event.contexts[:trace]).to eq(transaction.get_trace_context) + expect(event.contexts[:trace]).not_to have_key(:dynamic_sampling_context) expect(event.contexts.dig(:trace, :op)).to eq("foo") expect(event.dynamic_sampling_context).to eq(transaction.get_dynamic_sampling_context) end @@ -307,6 +380,7 @@ it "sets trace context and dynamic_sampling_context from propagation context if there's no span" do subject.apply_to_event(event) expect(event.contexts[:trace]).to eq(subject.propagation_context.get_trace_context) + expect(event.contexts[:trace]).not_to have_key(:dynamic_sampling_context) expect(event.dynamic_sampling_context).to eq(subject.propagation_context.get_dynamic_sampling_context) end diff --git a/sentry-ruby/spec/sentry_spec.rb b/sentry-ruby/spec/sentry_spec.rb index 38ba360bf..22c7a5859 100644 --- a/sentry-ruby/spec/sentry_spec.rb +++ b/sentry-ruby/spec/sentry_spec.rb @@ -938,6 +938,55 @@ end end + describe ".register_external_propagation_context" do + after do + described_class.clear_external_propagation_context + end + + it "registers a callback function" do + described_class.register_external_propagation_context do + ["trace123", "span456"] + end + + expect(described_class.get_external_propagation_context).to eq(["trace123", "span456"]) + end + end + + describe ".get_external_propagation_context" do + after do + described_class.clear_external_propagation_context + end + + it "returns nil when no callback is registered" do + expect(described_class.get_external_propagation_context).to be_nil + end + + it "returns nil when callback returns nil" do + described_class.register_external_propagation_context do + nil + end + + expect(described_class.get_external_propagation_context).to be_nil + end + + it "returns the result from the callback" do + described_class.register_external_propagation_context do + ["abc123def456789012345678901234", "1234567890abcdef"] + end + + result = described_class.get_external_propagation_context + expect(result).to eq(["abc123def456789012345678901234", "1234567890abcdef"]) + end + + it "catches errors from the callback and returns nil" do + described_class.register_external_propagation_context do + raise "Something went wrong" + end + + expect(described_class.get_external_propagation_context).to be_nil + end + end + describe ".continue_trace" do context "without incoming sentry trace" do let(:env) { { "HTTP_FOO" => "bar" } }