Skip to content

Commit 94a6c2a

Browse files
fix(tracing): Only propagate headers from spans within transactions
This change ensures that we only propagate trace headers from spans that are within a transaction. This fixes a bug where any child transactions of a span created outside a transaction are missing a dynamic sampling context and are part of a trace missing a root transaction (because the root is the span). Also, remove/modify tests that were asserting the old behavior. Fixes #3068
1 parent a02eb9c commit 94a6c2a

File tree

5 files changed

+78
-70
lines changed

5 files changed

+78
-70
lines changed

sentry_sdk/tracing.py

+10-4
Original file line numberDiff line numberDiff line change
@@ -426,12 +426,18 @@ def iter_headers(self):
426426
If the span's containing transaction doesn't yet have a ``baggage`` value,
427427
this will cause one to be generated and stored.
428428
"""
429+
if not self.containing_transaction:
430+
# Do not propagate headers if there is no containing transaction. Otherwise, this
431+
# span ends up being the root span of a new trace, and since it does not get sent
432+
# to Sentry, the trace will be missing a root transaction. The dynamic sampling
433+
# context will also be missing, breaking dynamic sampling & traces.
434+
return
435+
429436
yield SENTRY_TRACE_HEADER_NAME, self.to_traceparent()
430437

431-
if self.containing_transaction:
432-
baggage = self.containing_transaction.get_baggage().serialize()
433-
if baggage:
434-
yield BAGGAGE_HEADER_NAME, baggage
438+
baggage = self.containing_transaction.get_baggage().serialize()
439+
if baggage:
440+
yield BAGGAGE_HEADER_NAME, baggage
435441

436442
@classmethod
437443
def from_traceparent(

tests/integrations/aiohttp/test_aiohttp.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -404,13 +404,17 @@ async def hello(request):
404404
# The aiohttp_client is instrumented so will generate the sentry-trace header and add request.
405405
# Get the sentry-trace header from the request so we can later compare with transaction events.
406406
client = await aiohttp_client(app)
407-
resp = await client.get("/")
407+
with start_transaction():
408+
# Headers are only added to the span if there is an active transaction
409+
resp = await client.get("/")
410+
408411
sentry_trace_header = resp.request_info.headers.get("sentry-trace")
409412
trace_id = sentry_trace_header.split("-")[0]
410413

411414
assert resp.status == 500
412415

413-
msg_event, error_event, transaction_event = events
416+
# Last item is the custom transaction event wrapping `client.get("/")`
417+
msg_event, error_event, transaction_event, _ = events
414418

415419
assert msg_event["contexts"]["trace"]
416420
assert "trace_id" in msg_event["contexts"]["trace"]

tests/integrations/celery/test_update_celery_task_headers.py

-60
Original file line numberDiff line numberDiff line change
@@ -77,33 +77,6 @@ def test_span_with_transaction(sentry_init):
7777
)
7878

7979

80-
def test_span_with_no_transaction(sentry_init):
81-
sentry_init(enable_tracing=True)
82-
headers = {}
83-
84-
with sentry_sdk.start_span(op="test_span") as span:
85-
updated_headers = _update_celery_task_headers(headers, span, False)
86-
87-
assert updated_headers["sentry-trace"] == span.to_traceparent()
88-
assert updated_headers["headers"]["sentry-trace"] == span.to_traceparent()
89-
assert "baggage" not in updated_headers.keys()
90-
assert "baggage" not in updated_headers["headers"].keys()
91-
92-
93-
def test_custom_span(sentry_init):
94-
sentry_init(enable_tracing=True)
95-
span = sentry_sdk.tracing.Span()
96-
headers = {}
97-
98-
with sentry_sdk.start_transaction(name="test_transaction"):
99-
updated_headers = _update_celery_task_headers(headers, span, False)
100-
101-
assert updated_headers["sentry-trace"] == span.to_traceparent()
102-
assert updated_headers["headers"]["sentry-trace"] == span.to_traceparent()
103-
assert "baggage" not in updated_headers.keys()
104-
assert "baggage" not in updated_headers["headers"].keys()
105-
106-
10780
def test_span_with_transaction_custom_headers(sentry_init):
10881
sentry_init(enable_tracing=True)
10982
headers = {
@@ -137,36 +110,3 @@ def test_span_with_transaction_custom_headers(sentry_init):
137110
assert updated_headers["headers"]["baggage"] == combined_baggage.serialize(
138111
include_third_party=True
139112
)
140-
141-
142-
def test_span_with_no_transaction_custom_headers(sentry_init):
143-
sentry_init(enable_tracing=True)
144-
headers = {
145-
"baggage": BAGGAGE_VALUE,
146-
"sentry-trace": SENTRY_TRACE_VALUE,
147-
}
148-
149-
with sentry_sdk.start_span(op="test_span") as span:
150-
updated_headers = _update_celery_task_headers(headers, span, False)
151-
152-
assert updated_headers["sentry-trace"] == span.to_traceparent()
153-
assert updated_headers["headers"]["sentry-trace"] == span.to_traceparent()
154-
assert updated_headers["baggage"] == headers["baggage"]
155-
assert updated_headers["headers"]["baggage"] == headers["baggage"]
156-
157-
158-
def test_custom_span_custom_headers(sentry_init):
159-
sentry_init(enable_tracing=True)
160-
span = sentry_sdk.tracing.Span()
161-
headers = {
162-
"baggage": BAGGAGE_VALUE,
163-
"sentry-trace": SENTRY_TRACE_VALUE,
164-
}
165-
166-
with sentry_sdk.start_transaction(name="test_transaction"):
167-
updated_headers = _update_celery_task_headers(headers, span, False)
168-
169-
assert updated_headers["sentry-trace"] == span.to_traceparent()
170-
assert updated_headers["headers"]["sentry-trace"] == span.to_traceparent()
171-
assert updated_headers["baggage"] == headers["baggage"]
172-
assert updated_headers["headers"]["baggage"] == headers["baggage"]

tests/integrations/httpx/test_httpx.py

+22-4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import pytest
66
import responses
77

8+
import sentry_sdk
89
from sentry_sdk import capture_message, start_transaction
910
from sentry_sdk.consts import MATCH_ALL, SPANDATA
1011
from sentry_sdk.integrations.httpx import HttpxIntegration
@@ -258,10 +259,11 @@ def test_option_trace_propagation_targets(
258259
integrations=[HttpxIntegration()],
259260
)
260261

261-
if asyncio.iscoroutinefunction(httpx_client.get):
262-
asyncio.get_event_loop().run_until_complete(httpx_client.get(url))
263-
else:
264-
httpx_client.get(url)
262+
with sentry_sdk.start_transaction(): # Must be in a transaction to propagate headers
263+
if asyncio.iscoroutinefunction(httpx_client.get):
264+
asyncio.get_event_loop().run_until_complete(httpx_client.get(url))
265+
else:
266+
httpx_client.get(url)
265267

266268
request_headers = httpx_mock.get_request().headers
267269

@@ -271,6 +273,22 @@ def test_option_trace_propagation_targets(
271273
assert "sentry-trace" not in request_headers
272274

273275

276+
def test_do_not_propagate_outside_transaction(sentry_init, httpx_mock):
277+
httpx_mock.add_response()
278+
279+
sentry_init(
280+
traces_sample_rate=1.0,
281+
trace_propagation_targets=[MATCH_ALL],
282+
integrations=[HttpxIntegration()],
283+
)
284+
285+
httpx_client = httpx.Client()
286+
httpx_client.get("http://example.com/")
287+
288+
request_headers = httpx_mock.get_request().headers
289+
assert "sentry-trace" not in request_headers
290+
291+
274292
@pytest.mark.tests_internal_exceptions
275293
def test_omit_url_data_if_parsing_fails(sentry_init, capture_events):
276294
sentry_init(integrations=[HttpxIntegration()])

tests/tracing/test_propagation.py

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import sentry_sdk
2+
import pytest
3+
4+
5+
def test_standalone_span_iter_headers(sentry_init):
6+
sentry_init(enable_tracing=True)
7+
8+
with sentry_sdk.start_span(op="test") as span:
9+
with pytest.raises(StopIteration):
10+
# We should not have any propagation headers
11+
next(span.iter_headers())
12+
13+
14+
def test_span_in_span_iter_headers(sentry_init):
15+
sentry_init(enable_tracing=True)
16+
17+
with sentry_sdk.start_span(op="test"):
18+
with sentry_sdk.start_span(op="test2") as span_inner:
19+
with pytest.raises(StopIteration):
20+
# We should not have any propagation headers
21+
next(span_inner.iter_headers())
22+
23+
24+
def test_span_in_transaction(sentry_init):
25+
sentry_init(enable_tracing=True)
26+
27+
with sentry_sdk.start_transaction(op="test"):
28+
with sentry_sdk.start_span(op="test2") as span:
29+
# Ensure the headers are there
30+
next(span.iter_headers())
31+
32+
33+
def test_span_in_span_in_transaction(sentry_init):
34+
sentry_init(enable_tracing=True)
35+
36+
with sentry_sdk.start_transaction(op="test"):
37+
with sentry_sdk.start_span(op="test2"):
38+
with sentry_sdk.start_span(op="test3") as span_inner:
39+
# Ensure the headers are there
40+
next(span_inner.iter_headers())

0 commit comments

Comments
 (0)