Skip to content

Commit aeb7bd8

Browse files
ericapisanisl0thentr0py
authored andcommitted
fix(tracing): Propagate trace headers in streaming path when no current span (HTTP clients)
Previously, when span streaming was enabled and there was no active span, httpx/httpx2/httplib skipped trace header propagation entirely by returning early. This diverged from the legacy behavior and dropped distributed tracing continuity for outgoing requests made outside of a span. Extract the shared header-propagation logic into `propagate_trace_headers()` in tracing_utils.py and call it from the no-current-span early-return branches so headers are still attached from the scope's propagation context.
1 parent 1e9a5d7 commit aeb7bd8

6 files changed

Lines changed: 211 additions & 145 deletions

File tree

sentry_sdk/integrations/httpx.py

Lines changed: 5 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,15 @@
44
from sentry_sdk.consts import OP, SPANDATA
55
from sentry_sdk.integrations import DidNotEnable, Integration
66
from sentry_sdk.scope import should_send_default_pii
7-
from sentry_sdk.tracing import BAGGAGE_HEADER_NAME
87
from sentry_sdk.tracing_utils import (
98
add_http_request_source,
10-
add_sentry_baggage_to_headers,
119
has_span_streaming_enabled,
12-
should_propagate_trace,
10+
propagate_trace_headers,
1311
)
1412
from sentry_sdk.utils import (
1513
SENSITIVE_DATA_SUBSTITUTE,
1614
capture_internal_exceptions,
1715
ensure_integration_enabled,
18-
logger,
1916
parse_url,
2017
)
2118

@@ -84,21 +81,7 @@ def send(self: "Client", request: "Request", **kwargs: "Any") -> "Response":
8481
if parsed_url.fragment:
8582
attributes["url.fragment"] = parsed_url.fragment
8683

87-
if should_propagate_trace(client, str(request.url)):
88-
for (
89-
key,
90-
value,
91-
) in (
92-
sentry_sdk.get_current_scope().iter_trace_propagation_headers()
93-
):
94-
logger.debug(
95-
f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}."
96-
)
97-
98-
if key == BAGGAGE_HEADER_NAME:
99-
add_sentry_baggage_to_headers(request.headers, value)
100-
else:
101-
request.headers[key] = value
84+
propagate_trace_headers(client, request)
10285

10386
try:
10487
rv = real_send(self, request, **kwargs)
@@ -128,21 +111,7 @@ def send(self: "Client", request: "Request", **kwargs: "Any") -> "Response":
128111
span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query)
129112
span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment)
130113

131-
if should_propagate_trace(client, str(request.url)):
132-
for (
133-
key,
134-
value,
135-
) in (
136-
sentry_sdk.get_current_scope().iter_trace_propagation_headers()
137-
):
138-
logger.debug(
139-
f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}."
140-
)
141-
142-
if key == BAGGAGE_HEADER_NAME:
143-
add_sentry_baggage_to_headers(request.headers, value)
144-
else:
145-
request.headers[key] = value
114+
propagate_trace_headers(client, request)
146115

147116
rv = real_send(self, request, **kwargs)
148117

@@ -197,21 +166,7 @@ async def send(
197166
if parsed_url.fragment:
198167
attributes["url.fragment"] = parsed_url.fragment
199168

200-
if should_propagate_trace(client, str(request.url)):
201-
for (
202-
key,
203-
value,
204-
) in (
205-
sentry_sdk.get_current_scope().iter_trace_propagation_headers()
206-
):
207-
logger.debug(
208-
f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}."
209-
)
210-
211-
if key == BAGGAGE_HEADER_NAME:
212-
add_sentry_baggage_to_headers(request.headers, value)
213-
else:
214-
request.headers[key] = value
169+
propagate_trace_headers(client, request)
215170

216171
try:
217172
rv = await real_send(self, request, **kwargs)
@@ -241,20 +196,7 @@ async def send(
241196
span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query)
242197
span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment)
243198

244-
if should_propagate_trace(client, str(request.url)):
245-
for (
246-
key,
247-
value,
248-
) in (
249-
sentry_sdk.get_current_scope().iter_trace_propagation_headers()
250-
):
251-
logger.debug(
252-
f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}."
253-
)
254-
if key == BAGGAGE_HEADER_NAME:
255-
add_sentry_baggage_to_headers(request.headers, value)
256-
else:
257-
request.headers[key] = value
199+
propagate_trace_headers(client, request)
258200

259201
rv = await real_send(self, request, **kwargs)
260202

sentry_sdk/integrations/httpx2.py

Lines changed: 9 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,15 @@
44
from sentry_sdk.consts import OP, SPANDATA
55
from sentry_sdk.integrations import DidNotEnable, Integration
66
from sentry_sdk.scope import should_send_default_pii
7-
from sentry_sdk.tracing import BAGGAGE_HEADER_NAME
87
from sentry_sdk.tracing_utils import (
98
add_http_request_source,
10-
add_sentry_baggage_to_headers,
119
has_span_streaming_enabled,
12-
should_propagate_trace,
10+
propagate_trace_headers,
1311
)
1412
from sentry_sdk.utils import (
1513
SENSITIVE_DATA_SUBSTITUTE,
1614
capture_internal_exceptions,
1715
ensure_integration_enabled,
18-
logger,
1916
parse_url,
2017
)
2118

@@ -61,6 +58,8 @@ def send(self: "Client", request: "Request", **kwargs: "Any") -> "Response":
6158

6259
if is_span_streaming_enabled:
6360
if sentry_sdk.traces.get_current_span() is None:
61+
propagate_trace_headers(client, request)
62+
6463
return real_send(self, request, **kwargs)
6564

6665
with sentry_sdk.traces.start_span(
@@ -84,21 +83,7 @@ def send(self: "Client", request: "Request", **kwargs: "Any") -> "Response":
8483
if parsed_url.fragment:
8584
attributes["url.fragment"] = parsed_url.fragment
8685

87-
if should_propagate_trace(client, str(request.url)):
88-
for (
89-
key,
90-
value,
91-
) in (
92-
sentry_sdk.get_current_scope().iter_trace_propagation_headers()
93-
):
94-
logger.debug(
95-
f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}."
96-
)
97-
98-
if key == BAGGAGE_HEADER_NAME:
99-
add_sentry_baggage_to_headers(request.headers, value)
100-
else:
101-
request.headers[key] = value
86+
propagate_trace_headers(client, request)
10287

10388
try:
10489
rv = real_send(self, request, **kwargs)
@@ -128,21 +113,7 @@ def send(self: "Client", request: "Request", **kwargs: "Any") -> "Response":
128113
span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query)
129114
span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment)
130115

131-
if should_propagate_trace(client, str(request.url)):
132-
for (
133-
key,
134-
value,
135-
) in (
136-
sentry_sdk.get_current_scope().iter_trace_propagation_headers()
137-
):
138-
logger.debug(
139-
f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}."
140-
)
141-
142-
if key == BAGGAGE_HEADER_NAME:
143-
add_sentry_baggage_to_headers(request.headers, value)
144-
else:
145-
request.headers[key] = value
116+
propagate_trace_headers(client, request)
146117

147118
rv = real_send(self, request, **kwargs)
148119

@@ -174,6 +145,8 @@ async def send(
174145

175146
if is_span_streaming_enabled:
176147
if sentry_sdk.traces.get_current_span() is None:
148+
propagate_trace_headers(client, request)
149+
177150
return await real_send(self, request, **kwargs)
178151

179152
with sentry_sdk.traces.start_span(
@@ -197,21 +170,7 @@ async def send(
197170
if parsed_url.fragment:
198171
attributes["url.fragment"] = parsed_url.fragment
199172

200-
if should_propagate_trace(client, str(request.url)):
201-
for (
202-
key,
203-
value,
204-
) in (
205-
sentry_sdk.get_current_scope().iter_trace_propagation_headers()
206-
):
207-
logger.debug(
208-
f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}."
209-
)
210-
211-
if key == BAGGAGE_HEADER_NAME:
212-
add_sentry_baggage_to_headers(request.headers, value)
213-
else:
214-
request.headers[key] = value
173+
propagate_trace_headers(client, request)
215174

216175
try:
217176
rv = await real_send(self, request, **kwargs)
@@ -241,20 +200,7 @@ async def send(
241200
span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query)
242201
span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment)
243202

244-
if should_propagate_trace(client, str(request.url)):
245-
for (
246-
key,
247-
value,
248-
) in (
249-
sentry_sdk.get_current_scope().iter_trace_propagation_headers()
250-
):
251-
logger.debug(
252-
f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}."
253-
)
254-
if key == BAGGAGE_HEADER_NAME:
255-
add_sentry_baggage_to_headers(request.headers, value)
256-
else:
257-
request.headers[key] = value
203+
propagate_trace_headers(client, request)
258204

259205
rv = await real_send(self, request, **kwargs)
260206

sentry_sdk/integrations/stdlib.py

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -112,28 +112,30 @@ def putrequest(
112112
parsed_url = parse_url(real_url, sanitize=False)
113113

114114
span_streaming = has_span_streaming_enabled(client.options)
115-
span: "Union[Span, StreamedSpan]"
115+
span: "Union[Span, StreamedSpan, None]"
116116
if span_streaming:
117117
if sentry_sdk.traces.get_current_span() is None:
118-
return real_putrequest(self, method, url, *args, **kwargs)
119-
120-
span = sentry_sdk.traces.start_span(
121-
name="%s %s"
122-
% (method, parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE),
123-
attributes={
124-
"sentry.origin": "auto.http.stdlib.httplib",
125-
"sentry.op": OP.HTTP_CLIENT,
126-
SPANDATA.HTTP_REQUEST_METHOD: method,
127-
},
128-
)
129-
130-
if parsed_url is not None and should_send_default_pii():
131-
span.set_attribute(SPANDATA.URL_FRAGMENT, parsed_url.fragment)
132-
span.set_attribute(SPANDATA.URL_FULL, parsed_url.url)
133-
span.set_attribute(SPANDATA.URL_QUERY, parsed_url.query)
118+
span = None
119+
else:
120+
span = sentry_sdk.traces.start_span(
121+
name="%s %s"
122+
% (
123+
method,
124+
parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE,
125+
),
126+
attributes={
127+
"sentry.origin": "auto.http.stdlib.httplib",
128+
"sentry.op": OP.HTTP_CLIENT,
129+
SPANDATA.HTTP_REQUEST_METHOD: method,
130+
},
131+
)
134132

135-
set_on_span = span.set_attribute
133+
if parsed_url is not None and should_send_default_pii():
134+
span.set_attribute(SPANDATA.URL_FRAGMENT, parsed_url.fragment)
135+
span.set_attribute(SPANDATA.URL_FULL, parsed_url.url)
136+
span.set_attribute(SPANDATA.URL_QUERY, parsed_url.query)
136137

138+
set_on_span = span.set_attribute
137139
else:
138140
span = sentry_sdk.start_span(
139141
op=OP.HTTP_CLIENT,
@@ -151,7 +153,7 @@ def putrequest(
151153
set_on_span = span.set_data
152154

153155
# for proxies, these point to the proxy host/port
154-
if tunnel_host:
156+
if span and tunnel_host:
155157
set_on_span(SPANDATA.NETWORK_PEER_ADDRESS, self.host)
156158
set_on_span(SPANDATA.NETWORK_PEER_PORT, self.port)
157159

sentry_sdk/tracing_utils.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -973,6 +973,31 @@ def should_propagate_trace(client: "sentry_sdk.client.BaseClient", url: str) ->
973973
return match_regex_list(url, trace_propagation_targets, substring_matching=True)
974974

975975

976+
def propagate_trace_headers(
977+
client: "sentry_sdk.client.BaseClient", request: "Any"
978+
) -> None:
979+
"""
980+
Attach Sentry trace propagation headers (``sentry-trace``/``baggage``) from the
981+
current scope's propagation context to an outgoing request, if the request's
982+
URL matches the configured ``trace_propagation_targets``.
983+
984+
``request`` is expected to expose ``url`` and a mutable ``headers`` mapping
985+
(e.g. an ``httpx``/``httpx2`` ``Request``).
986+
"""
987+
if not should_propagate_trace(client, str(request.url)):
988+
return
989+
990+
for key, value in sentry_sdk.get_current_scope().iter_trace_propagation_headers():
991+
logger.debug(
992+
f"[Tracing] Adding `{key}` header {value} to outgoing request to {request.url}."
993+
)
994+
995+
if key == BAGGAGE_HEADER_NAME:
996+
add_sentry_baggage_to_headers(request.headers, value)
997+
else:
998+
request.headers[key] = value
999+
1000+
9761001
def normalize_incoming_data(incoming_data: "Dict[str, Any]") -> "Dict[str, Any]":
9771002
"""
9781003
Normalizes incoming data so the keys are all lowercase with dashes instead of underscores and stripped from known prefixes.

0 commit comments

Comments
 (0)