Skip to content

Commit 9a0dba3

Browse files
authored
Add external_propagation_context support (#2841)
1 parent c31a9f7 commit 9a0dba3

File tree

5 files changed

+180
-14
lines changed

5 files changed

+180
-14
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## Unreleased
2+
3+
### Internal
4+
5+
- Add external_propagation_context support ([#2841](https://github.com/getsentry/sentry-ruby/pull/2841))
6+
17
## 6.3.0
28

39
### Features

sentry-ruby/lib/sentry-ruby.rb

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,38 @@ def sdk_meta
666666
META
667667
end
668668

669+
# Registers a callback function that retrieves the current external propagation context.
670+
# This is used by OpenTelemetry integration to provide trace_id and span_id from OTel context.
671+
#
672+
# @param callback [Proc, nil] A callable that returns [trace_id, span_id] or nil
673+
# @return [void]
674+
#
675+
# @example
676+
# Sentry.register_external_propagation_context do
677+
# span_context = OpenTelemetry::Trace.current_span.context
678+
# return nil unless span_context.valid?
679+
# [span_context.hex_trace_id, span_context.hex_span_id]
680+
# end
681+
def register_external_propagation_context(&callback)
682+
@external_propagation_context_callback = callback
683+
end
684+
685+
# Returns the external propagation context (trace_id, span_id) if a callback is registered.
686+
#
687+
# @return [Array<String>, nil] A tuple of [trace_id, span_id] or nil if no context is available
688+
def get_external_propagation_context
689+
return nil unless @external_propagation_context_callback
690+
691+
@external_propagation_context_callback.call
692+
rescue => e
693+
sdk_logger&.debug(LOGGER_PROGNAME) { "Error getting external propagation context: #{e.message}" } if initialized?
694+
nil
695+
end
696+
697+
def clear_external_propagation_context
698+
@external_propagation_context_callback = nil
699+
end
700+
669701
# @!visibility private
670702
def utc_now
671703
Time.now.utc

sentry-ruby/lib/sentry/scope.rb

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -60,19 +60,10 @@ def apply_to_event(event, hint = nil)
6060
event.attachments = attachments
6161
end
6262

63-
if span
64-
event.contexts[:trace] ||= span.get_trace_context
65-
66-
if event.respond_to?(:dynamic_sampling_context)
67-
event.dynamic_sampling_context ||= span.get_dynamic_sampling_context
68-
end
69-
else
70-
event.contexts[:trace] ||= propagation_context.get_trace_context
71-
72-
if event.respond_to?(:dynamic_sampling_context)
73-
event.dynamic_sampling_context ||= propagation_context.get_dynamic_sampling_context
74-
end
75-
end
63+
trace_context = get_trace_context
64+
dynamic_sampling_context = trace_context.delete(:dynamic_sampling_context)
65+
event.contexts[:trace] ||= trace_context
66+
event.dynamic_sampling_context ||= dynamic_sampling_context
7667

7768
all_event_processors = self.class.global_event_processors + @event_processors
7869

@@ -94,7 +85,7 @@ def apply_to_event(event, hint = nil)
9485
# @return [MetricEvent, LogEvent] the telemetry event with scope context applied
9586
def apply_to_telemetry(telemetry)
9687
# TODO-neel when new scope set_attribute api is added: add them here
97-
trace_context = span ? span.get_trace_context : propagation_context.get_trace_context
88+
trace_context = get_trace_context
9889
telemetry.trace_id = trace_context[:trace_id]
9990
telemetry.span_id = trace_context[:span_id]
10091

@@ -305,6 +296,20 @@ def get_span
305296
span
306297
end
307298

299+
# Returns the trace context for this scope.
300+
# Prioritizes external propagation context (from OTel) over local propagation context.
301+
# @return [Hash]
302+
def get_trace_context
303+
if span
304+
span.get_trace_context.merge(dynamic_sampling_context: span.get_dynamic_sampling_context)
305+
elsif (external_context = Sentry.get_external_propagation_context)
306+
trace_id, span_id = external_context
307+
{ trace_id: trace_id, span_id: span_id }
308+
else
309+
propagation_context.get_trace_context.merge(dynamic_sampling_context: propagation_context.get_dynamic_sampling_context)
310+
end
311+
end
312+
308313
# Sets the scope's fingerprint attribute.
309314
# @param fingerprint [Array]
310315
# @return [Array]

sentry-ruby/spec/sentry/scope_spec.rb

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,78 @@
187187
end
188188
end
189189

190+
describe "#get_trace_context" do
191+
before { perform_basic_setup }
192+
193+
context "with span" do
194+
let(:transaction) { Sentry::Transaction.new(op: "test") }
195+
196+
before do
197+
subject.set_span(transaction)
198+
end
199+
200+
it "returns the span's trace context with dynamic_sampling_context" do
201+
trace_context = subject.get_trace_context
202+
expect(trace_context[:trace_id]).to eq(transaction.trace_id)
203+
expect(trace_context[:span_id]).to eq(transaction.span_id)
204+
expect(trace_context[:op]).to eq("test")
205+
expect(trace_context[:dynamic_sampling_context]).to eq(transaction.get_dynamic_sampling_context)
206+
end
207+
208+
it "prioritizes span over external propagation context" do
209+
Sentry.register_external_propagation_context do
210+
["abc123def456789012345678901234ab", "1234567890abcdef"]
211+
end
212+
213+
trace_context = subject.get_trace_context
214+
expect(trace_context[:trace_id]).to eq(transaction.trace_id)
215+
expect(trace_context[:dynamic_sampling_context]).to eq(transaction.get_dynamic_sampling_context)
216+
217+
Sentry.clear_external_propagation_context
218+
end
219+
end
220+
221+
context "with external propagation context" do
222+
let(:external_trace_id) { "abc123def456789012345678901234ab" }
223+
let(:external_span_id) { "1234567890abcdef" }
224+
225+
before do
226+
Sentry.register_external_propagation_context do
227+
[external_trace_id, external_span_id]
228+
end
229+
end
230+
231+
after do
232+
Sentry.clear_external_propagation_context
233+
end
234+
235+
it "returns the external propagation context's trace context" do
236+
trace_context = subject.get_trace_context
237+
expect(trace_context[:trace_id]).to eq(external_trace_id)
238+
expect(trace_context[:span_id]).to eq(external_span_id)
239+
end
240+
end
241+
242+
context "when external propagation context callback returns nil" do
243+
before do
244+
Sentry.register_external_propagation_context do
245+
nil
246+
end
247+
end
248+
249+
after do
250+
Sentry.clear_external_propagation_context
251+
end
252+
253+
it "falls back to local propagation context with dynamic_sampling_context" do
254+
trace_context = subject.get_trace_context
255+
expect(trace_context[:trace_id]).to eq(subject.propagation_context.trace_id)
256+
expect(trace_context[:span_id]).to eq(subject.propagation_context.span_id)
257+
expect(trace_context[:dynamic_sampling_context]).to eq(subject.propagation_context.get_dynamic_sampling_context)
258+
end
259+
end
260+
end
261+
190262
describe "#apply_to_event" do
191263
before { perform_basic_setup }
192264

@@ -300,13 +372,15 @@
300372
subject.apply_to_event(event)
301373

302374
expect(event.contexts[:trace]).to eq(transaction.get_trace_context)
375+
expect(event.contexts[:trace]).not_to have_key(:dynamic_sampling_context)
303376
expect(event.contexts.dig(:trace, :op)).to eq("foo")
304377
expect(event.dynamic_sampling_context).to eq(transaction.get_dynamic_sampling_context)
305378
end
306379

307380
it "sets trace context and dynamic_sampling_context from propagation context if there's no span" do
308381
subject.apply_to_event(event)
309382
expect(event.contexts[:trace]).to eq(subject.propagation_context.get_trace_context)
383+
expect(event.contexts[:trace]).not_to have_key(:dynamic_sampling_context)
310384
expect(event.dynamic_sampling_context).to eq(subject.propagation_context.get_dynamic_sampling_context)
311385
end
312386

sentry-ruby/spec/sentry_spec.rb

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -938,6 +938,55 @@
938938
end
939939
end
940940

941+
describe ".register_external_propagation_context" do
942+
after do
943+
described_class.clear_external_propagation_context
944+
end
945+
946+
it "registers a callback function" do
947+
described_class.register_external_propagation_context do
948+
["trace123", "span456"]
949+
end
950+
951+
expect(described_class.get_external_propagation_context).to eq(["trace123", "span456"])
952+
end
953+
end
954+
955+
describe ".get_external_propagation_context" do
956+
after do
957+
described_class.clear_external_propagation_context
958+
end
959+
960+
it "returns nil when no callback is registered" do
961+
expect(described_class.get_external_propagation_context).to be_nil
962+
end
963+
964+
it "returns nil when callback returns nil" do
965+
described_class.register_external_propagation_context do
966+
nil
967+
end
968+
969+
expect(described_class.get_external_propagation_context).to be_nil
970+
end
971+
972+
it "returns the result from the callback" do
973+
described_class.register_external_propagation_context do
974+
["abc123def456789012345678901234", "1234567890abcdef"]
975+
end
976+
977+
result = described_class.get_external_propagation_context
978+
expect(result).to eq(["abc123def456789012345678901234", "1234567890abcdef"])
979+
end
980+
981+
it "catches errors from the callback and returns nil" do
982+
described_class.register_external_propagation_context do
983+
raise "Something went wrong"
984+
end
985+
986+
expect(described_class.get_external_propagation_context).to be_nil
987+
end
988+
end
989+
941990
describe ".continue_trace" do
942991
context "without incoming sentry trace" do
943992
let(:env) { { "HTTP_FOO" => "bar" } }

0 commit comments

Comments
 (0)