Skip to content

Commit dc9b392

Browse files
committed
feat(tracing): add W3C traceparent header support and corresponding tests
1 parent fbde4a7 commit dc9b392

File tree

3 files changed

+124
-1
lines changed

3 files changed

+124
-1
lines changed

sentry_sdk/tracing.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -531,7 +531,8 @@ def continue_from_headers(
531531
def iter_headers(self):
532532
# type: () -> Iterator[Tuple[str, str]]
533533
"""
534-
Creates a generator which returns the span's ``sentry-trace`` and ``baggage`` headers.
534+
Creates a generator which returns the span's ``sentry-trace`` and ``baggage`` headers,
535+
as well as a ``traceparent`` header for W3C compatibility.
535536
If the span's containing transaction doesn't yet have a ``baggage`` value,
536537
this will cause one to be generated and stored.
537538
"""
@@ -543,6 +544,7 @@ def iter_headers(self):
543544
return
544545

545546
yield SENTRY_TRACE_HEADER_NAME, self.to_traceparent()
547+
yield W3C_TRACE_HEADER_NAME, self.to_w3c_traceparent()
546548

547549
baggage = self.containing_transaction.get_baggage().serialize()
548550
if baggage:
@@ -588,6 +590,16 @@ def to_traceparent(self):
588590

589591
return traceparent
590592

593+
def to_w3c_traceparent(self):
594+
# type: () -> str
595+
if self.sampled is True:
596+
trace_flags = "01"
597+
else:
598+
trace_flags = "00"
599+
600+
traceparent = "00-%s-%s-%s" % (self.trace_id, self.span_id, trace_flags)
601+
return traceparent
602+
591603
def to_baggage(self):
592604
# type: () -> Optional[Baggage]
593605
"""Returns the :py:class:`~sentry_sdk.tracing_utils.Baggage`
@@ -1233,6 +1245,10 @@ def to_traceparent(self):
12331245
# type: () -> str
12341246
return ""
12351247

1248+
def to_w3c_traceparent(self):
1249+
# type: () -> str
1250+
return ""
1251+
12361252
def to_baggage(self):
12371253
# type: () -> Optional[Baggage]
12381254
return None

tests/tracing/test_http_headers.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,14 @@ def test_iter_headers(monkeypatch):
6262
mock.Mock(return_value="12312012123120121231201212312012-0415201309082013-0"),
6363
)
6464

65+
monkeypatch.setattr(
66+
Transaction,
67+
"to_w3c_traceparent",
68+
mock.Mock(
69+
return_value="00-12312012123120121231201212312012-0415201309082013-00"
70+
),
71+
)
72+
6573
transaction = Transaction(
6674
name="/interactions/other-dogs/new-dog",
6775
op="greeting.sniff",
@@ -71,3 +79,7 @@ def test_iter_headers(monkeypatch):
7179
assert (
7280
headers["sentry-trace"] == "12312012123120121231201212312012-0415201309082013-0"
7381
)
82+
assert (
83+
headers["traceparent"]
84+
== "00-12312012123120121231201212312012-0415201309082013-00"
85+
)

tests/tracing/test_integration_tests.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,101 @@ def test_continue_from_headers(
147147
assert message_payload["message"] == "hello"
148148

149149

150+
@pytest.mark.parametrize("parent_sampled", [True, False, None])
151+
@pytest.mark.parametrize("sample_rate", [0.0, 1.0])
152+
def test_continue_from_w3c_headers(
153+
sentry_init, capture_envelopes, parent_sampled, sample_rate
154+
):
155+
"""
156+
Ensure data is actually passed along via headers, and that they are read
157+
correctly. This test is similar to the one above, but uses W3C headers
158+
instead of Sentry headers.
159+
"""
160+
sentry_init(traces_sample_rate=sample_rate)
161+
envelopes = capture_envelopes()
162+
163+
# make a parent transaction (normally this would be in a different service)
164+
with start_transaction(name="hi", sampled=True if sample_rate == 0 else None):
165+
with start_span() as old_span:
166+
old_span.sampled = parent_sampled
167+
headers = dict(
168+
sentry_sdk.get_current_scope().iter_trace_propagation_headers(old_span)
169+
)
170+
headers["baggage"] = (
171+
"other-vendor-value-1=foo;bar;baz, "
172+
"sentry-trace_id=d055bff6ed16698222b464d97f980489, "
173+
"sentry-public_key=49d0f7386ad645858ae85020e393bef3, "
174+
"sentry-sample_rate=0.01337, sentry-user_id=Amelie, "
175+
"other-vendor-value-2=foo;bar;"
176+
)
177+
178+
# child transaction, to prove that we can read 'sentry-trace' header data correctly
179+
child_transaction = Transaction.continue_from_headers(headers, name="WRONG")
180+
assert child_transaction is not None
181+
assert child_transaction.parent_sampled == parent_sampled
182+
assert child_transaction.trace_id == old_span.trace_id
183+
assert child_transaction.same_process_as_parent is False
184+
assert child_transaction.parent_span_id == old_span.span_id
185+
assert child_transaction.span_id != old_span.span_id
186+
187+
baggage = child_transaction._baggage
188+
assert baggage
189+
assert not baggage.mutable
190+
assert baggage.sentry_items == {
191+
"public_key": "49d0f7386ad645858ae85020e393bef3",
192+
"trace_id": "d055bff6ed16698222b464d97f980489",
193+
"user_id": "Amelie",
194+
"sample_rate": "0.01337",
195+
}
196+
197+
# add child transaction to the scope, to show that the captured message will
198+
# be tagged with the trace id (since it happens while the transaction is
199+
# open)
200+
with start_transaction(child_transaction):
201+
# change the transaction name from "WRONG" to make sure the change
202+
# is reflected in the final data
203+
sentry_sdk.get_current_scope().transaction = "ho"
204+
capture_message("hello")
205+
206+
if parent_sampled is False or (sample_rate == 0 and parent_sampled is None):
207+
# in this case the child transaction won't be captured
208+
trace1, message = envelopes
209+
message_payload = message.get_event()
210+
trace1_payload = trace1.get_transaction_event()
211+
212+
assert trace1_payload["transaction"] == "hi"
213+
else:
214+
trace1, message, trace2 = envelopes
215+
trace1_payload = trace1.get_transaction_event()
216+
message_payload = message.get_event()
217+
trace2_payload = trace2.get_transaction_event()
218+
219+
assert trace1_payload["transaction"] == "hi"
220+
assert trace2_payload["transaction"] == "ho"
221+
222+
assert (
223+
trace1_payload["contexts"]["trace"]["trace_id"]
224+
== trace2_payload["contexts"]["trace"]["trace_id"]
225+
== child_transaction.trace_id
226+
== message_payload["contexts"]["trace"]["trace_id"]
227+
)
228+
229+
if parent_sampled is not None:
230+
expected_sample_rate = str(float(parent_sampled))
231+
else:
232+
expected_sample_rate = str(sample_rate)
233+
234+
assert trace2.headers["trace"] == baggage.dynamic_sampling_context()
235+
assert trace2.headers["trace"] == {
236+
"public_key": "49d0f7386ad645858ae85020e393bef3",
237+
"trace_id": "d055bff6ed16698222b464d97f980489",
238+
"user_id": "Amelie",
239+
"sample_rate": expected_sample_rate,
240+
}
241+
242+
assert message_payload["message"] == "hello"
243+
244+
150245
@pytest.mark.parametrize("sample_rate", [0.0, 1.0])
151246
def test_propagate_traces_deprecation_warning(sentry_init, sample_rate):
152247
sentry_init(traces_sample_rate=sample_rate, propagate_traces=False)

0 commit comments

Comments
 (0)