Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## Unreleased

### Internal

- Add external_propagation_context support ([#2841](https://github.com/getsentry/sentry-ruby/pull/2841))

## 6.3.0

### Features
Expand Down
32 changes: 32 additions & 0 deletions sentry-ruby/lib/sentry-ruby.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>, 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
Expand Down
33 changes: 19 additions & 14 deletions sentry-ruby/lib/sentry/scope.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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]

Expand Down Expand Up @@ -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]
Expand Down
74 changes: 74 additions & 0 deletions sentry-ruby/spec/sentry/scope_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Expand Down Expand Up @@ -300,13 +372,15 @@
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

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

Expand Down
49 changes: 49 additions & 0 deletions sentry-ruby/spec/sentry_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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" } }
Expand Down
Loading