Skip to content

Commit dbf5d03

Browse files
authored
fix(tracing): Skip child span creation in streaming path when no current span (web frameworks) (#6810)
1 parent 5c14034 commit dbf5d03

7 files changed

Lines changed: 84 additions & 78 deletions

File tree

sentry_sdk/integrations/aiohttp.py

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -351,23 +351,28 @@ async def on_request_start(
351351
parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE,
352352
)
353353

354-
span: "Union[Span, StreamedSpan]"
354+
span: "Union[Span, StreamedSpan, None]"
355355
if has_span_streaming_enabled(client.options):
356-
attributes: "Attributes" = {
357-
"sentry.op": OP.HTTP_CLIENT,
358-
"sentry.origin": AioHttpIntegration.origin,
359-
"http.request.method": method,
360-
}
361-
if parsed_url is not None and should_send_default_pii():
362-
attributes["url.full"] = parsed_url.url
363-
attributes["url.path"] = params.url.path
364-
365-
if parsed_url.query:
366-
attributes["url.query"] = parsed_url.query
367-
if parsed_url.fragment:
368-
attributes["url.fragment"] = parsed_url.fragment
369-
370-
span = sentry_sdk.traces.start_span(name=span_name, attributes=attributes)
356+
if sentry_sdk.traces.get_current_span() is None:
357+
span = None
358+
else:
359+
attributes: "Attributes" = {
360+
"sentry.op": OP.HTTP_CLIENT,
361+
"sentry.origin": AioHttpIntegration.origin,
362+
"http.request.method": method,
363+
}
364+
if parsed_url is not None and should_send_default_pii():
365+
attributes["url.full"] = parsed_url.url
366+
attributes["url.path"] = params.url.path
367+
368+
if parsed_url.query:
369+
attributes["url.query"] = parsed_url.query
370+
if parsed_url.fragment:
371+
attributes["url.fragment"] = parsed_url.fragment
372+
373+
span = sentry_sdk.traces.start_span(
374+
name=span_name, attributes=attributes
375+
)
371376
else:
372377
legacy_span = sentry_sdk.start_span(
373378
op=OP.HTTP_CLIENT,

sentry_sdk/integrations/graphene.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,10 @@ def graphql_span(
149149
)
150150

151151
if is_span_streaming_enabled:
152+
if sentry_sdk.traces.get_current_span() is None:
153+
yield
154+
return
155+
152156
additional_attributes = {}
153157
if should_send_default_pii():
154158
additional_attributes["graphql.document"] = source

sentry_sdk/integrations/litestar.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,8 @@ async def _create_span_call(
164164

165165
middleware_name = self.__class__.__name__
166166
if has_span_streaming_enabled(client.options):
167+
if sentry_sdk.traces.get_current_span() is None:
168+
return await old_call(self, scope, receive, send)
167169
with sentry_sdk.traces.start_span(
168170
name=middleware_name,
169171
attributes={
@@ -179,6 +181,8 @@ async def _sentry_receive(
179181
) -> "Union[HTTPReceiveMessage, WebSocketReceiveMessage]":
180182
if client.get_integration(LitestarIntegration) is None:
181183
return await receive(*args, **kwargs)
184+
if sentry_sdk.traces.get_current_span() is None:
185+
return await receive(*args, **kwargs)
182186
with sentry_sdk.traces.start_span(
183187
name=getattr(receive, "__qualname__", str(receive)),
184188
attributes={
@@ -197,6 +201,8 @@ async def _sentry_receive(
197201
async def _sentry_send(message: "Message") -> None:
198202
if client.get_integration(LitestarIntegration) is None:
199203
return await send(message)
204+
if sentry_sdk.traces.get_current_span() is None:
205+
return await send(message)
200206
with sentry_sdk.traces.start_span(
201207
name=getattr(send, "__qualname__", str(send)),
202208
attributes={

sentry_sdk/integrations/starlette.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
capture_internal_exceptions,
3636
ensure_integration_enabled,
3737
event_from_exception,
38+
nullcontext,
3839
parse_version,
3940
transaction_from_function,
4041
)
@@ -198,6 +199,8 @@ async def _create_span_call(
198199

199200
def _start_middleware_span(op: str, name: str) -> "Any":
200201
if is_span_streaming_enabled:
202+
if sentry_sdk.traces.get_current_span() is None:
203+
return nullcontext()
201204
return sentry_sdk.traces.start_span(
202205
name=name,
203206
attributes={

sentry_sdk/integrations/starlite.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from sentry_sdk.utils import (
1111
ensure_integration_enabled,
1212
event_from_exception,
13+
nullcontext,
1314
transaction_from_function,
1415
)
1516

@@ -151,6 +152,8 @@ async def _create_span_call(
151152

152153
def _start_middleware_span(op: str, name: str) -> "Any":
153154
if is_span_streaming_enabled:
155+
if sentry_sdk.traces.get_current_span() is None:
156+
return nullcontext()
154157
return sentry_sdk.traces.start_span(
155158
name=name,
156159
attributes={

sentry_sdk/integrations/strawberry.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,10 @@ def on_operation(self) -> "Generator[None, None, None]":
188188
client = sentry_sdk.get_client()
189189
is_span_streaming_enabled = has_span_streaming_enabled(client.options)
190190
if is_span_streaming_enabled:
191+
if sentry_sdk.traces.get_current_span() is None:
192+
yield
193+
return
194+
191195
additional_attributes: "dict[str, Any]" = {}
192196

193197
if should_send_default_pii():
@@ -244,6 +248,10 @@ def on_validate(self) -> "Generator[None, None, None]":
244248
is_span_streaming_enabled = has_span_streaming_enabled(client.options)
245249

246250
if is_span_streaming_enabled:
251+
if sentry_sdk.traces.get_current_span() is None:
252+
yield
253+
return
254+
247255
validation_span = sentry_sdk.traces.start_span(
248256
name="validation",
249257
attributes={
@@ -272,6 +280,10 @@ def on_parse(self) -> "Generator[None, None, None]":
272280
is_span_streaming_enabled = has_span_streaming_enabled(client.options)
273281

274282
if is_span_streaming_enabled:
283+
if sentry_sdk.traces.get_current_span() is None:
284+
yield
285+
return
286+
275287
parsing_span = sentry_sdk.traces.start_span(
276288
name="parsing",
277289
attributes={
@@ -333,6 +345,9 @@ async def resolve(
333345
client = sentry_sdk.get_client()
334346
is_span_streaming_enabled = has_span_streaming_enabled(client.options)
335347
if is_span_streaming_enabled:
348+
if sentry_sdk.traces.get_current_span() is None:
349+
return await self._resolve(_next, root, info, *args, **kwargs)
350+
336351
with sentry_sdk.traces.start_span(
337352
name=f"resolving {field_path}",
338353
attributes={
@@ -372,6 +387,9 @@ def resolve(
372387
client = sentry_sdk.get_client()
373388
is_span_streaming_enabled = has_span_streaming_enabled(client.options)
374389
if is_span_streaming_enabled:
390+
if sentry_sdk.traces.get_current_span() is None:
391+
return _next(root, info, *args, **kwargs)
392+
375393
with sentry_sdk.traces.start_span(
376394
name=f"resolving {field_path}",
377395
attributes={

tests/integrations/aiohttp/test_aiohttp.py

Lines changed: 29 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1162,16 +1162,12 @@ async def hello(request):
11621162

11631163
sentry_sdk.flush()
11641164

1165-
# The aiohttp_client fixture is itself sentry-instrumented and emits the
1166-
# first http.client segment; the server-side http.server span is the other
1167-
# segment. Asserting the exact length confirms no other spans leak in.
1168-
assert len(items) == 2
1169-
1170-
server_span, client_span = [item.payload for item in items]
1165+
# The server-side http.server span is the only segment. The aiohttp_client
1166+
# fixture's outgoing http.client span is suppressed because there is no
1167+
# active span when the test client makes the request.
1168+
assert len(items) == 1
11711169

1172-
assert client_span["is_segment"] is True
1173-
assert client_span["attributes"]["sentry.op"] == "http.client"
1174-
assert client_span["name"].startswith("GET http://127.0.0.1:")
1170+
(server_span,) = [item.payload for item in items]
11751171

11761172
assert server_span["is_segment"] is True
11771173
assert (
@@ -1242,7 +1238,7 @@ async def hello(request):
12421238

12431239
sentry_sdk.flush()
12441240

1245-
server_span, _client_segment = [item.payload for item in items]
1241+
(server_span,) = [item.payload for item in items]
12461242

12471243
# send_default_pii defaults to False, so _filter_headers substitutes
12481244
# sensitive headers with SENSITIVE_DATA_SUBSTITUTE ("[Filtered]"). The
@@ -1282,7 +1278,7 @@ async def hello(request):
12821278

12831279
sentry_sdk.flush()
12841280

1285-
server_span, _client_segment = [item.payload for item in items]
1281+
(server_span,) = [item.payload for item in items]
12861282

12871283
# With send_default_pii=True, _filter_headers is a no-op and the original
12881284
# value reaches the span attribute.
@@ -1321,8 +1317,8 @@ async def hello(request):
13211317

13221318
sentry_sdk.flush()
13231319

1324-
assert len(items) == 2
1325-
server_segment, client_segment = [item.payload for item in items]
1320+
assert len(items) == 1
1321+
(server_segment,) = [item.payload for item in items]
13261322

13271323
if send_pii:
13281324
assert server_segment["attributes"]["url.query"] == "foo=bar&baz=qux"
@@ -1378,8 +1374,8 @@ async def hello(request):
13781374

13791375
sentry_sdk.flush()
13801376

1381-
assert len(items) == 2
1382-
server_segment, client_segment = [item.payload for item in items]
1377+
assert len(items) == 1
1378+
(server_segment,) = [item.payload for item in items]
13831379

13841380
assert server_segment["name"] == expected_name
13851381
assert server_segment["is_segment"]
@@ -1408,21 +1404,14 @@ async def hello(request):
14081404

14091405
sentry_sdk.flush()
14101406

1411-
# 1 error event + 2 spans (server http.server, test client http.client segment)
1412-
assert len(items) == 3
1407+
# 1 error event + 1 span (server http.server)
1408+
assert len(items) == 2
14131409

14141410
error_event = items[0]
14151411
assert error_event.type == "event"
14161412
assert error_event.payload["exception"]["values"][0]["type"] == "ZeroDivisionError"
14171413

1418-
server_span, segment = [item.payload for item in items[1:]]
1419-
1420-
assert segment["is_segment"] is True
1421-
assert segment["attributes"]["sentry.op"] == "http.client"
1422-
# The test client receives the 500 response that aiohttp's outer error
1423-
# handler synthesizes from the unhandled exception.
1424-
assert segment["attributes"]["http.response.status_code"] == 500
1425-
assert segment["status"] == "error"
1414+
server_span = items[1].payload
14261415

14271416
# The integration's generic Exception path reraises without recording
14281417
# http.response.status_code on the server span. StreamedSpan.__exit__
@@ -1456,13 +1445,8 @@ async def hello(request):
14561445

14571446
sentry_sdk.flush()
14581447

1459-
assert len(items) == 2
1460-
server_span, segment = [item.payload for item in items]
1461-
1462-
assert segment["is_segment"] is True
1463-
assert segment["attributes"]["sentry.op"] == "http.client"
1464-
assert segment["attributes"]["http.response.status_code"] == 403
1465-
assert segment["status"] == "error"
1448+
assert len(items) == 1
1449+
(server_span,) = [item.payload for item in items]
14661450

14671451
assert server_span["attributes"]["sentry.op"] == "http.server"
14681452
assert server_span["attributes"]["http.response.status_code"] == 403
@@ -1493,13 +1477,8 @@ async def hello(request):
14931477

14941478
sentry_sdk.flush()
14951479

1496-
assert len(items) == 2
1497-
server_span, segment = [item.payload for item in items]
1498-
1499-
assert segment["is_segment"] is True
1500-
assert segment["attributes"]["sentry.op"] == "http.client"
1501-
assert segment["attributes"]["http.response.status_code"] == 302
1502-
assert segment["status"] == "ok"
1480+
assert len(items) == 1
1481+
(server_span,) = [item.payload for item in items]
15031482

15041483
assert server_span["attributes"]["sentry.op"] == "http.server"
15051484
assert server_span["attributes"]["http.response.status_code"] == 302
@@ -1538,18 +1517,13 @@ async def hello(request):
15381517

15391518
sentry_sdk.flush()
15401519

1541-
# 3 spans, finished inner-first:
1520+
# 2 spans, finished inner-first:
15421521
# #0 inner http.client (server -> raw_server)
15431522
# #1 server http.server
1544-
# #2 outer http.client segment (test client -> server)
1545-
assert len(items) == 3
1523+
assert len(items) == 2
15461524

15471525
inner_client_span = items[0].payload
15481526
server_span = items[1].payload
1549-
segment = items[2].payload
1550-
1551-
assert segment["is_segment"] is True
1552-
assert segment["attributes"]["sentry.op"] == "http.client"
15531527

15541528
assert server_span["attributes"]["sentry.op"] == "http.server"
15551529

@@ -1596,21 +1570,14 @@ async def handler(request):
15961570

15971571
sentry_sdk.flush()
15981572

1599-
# raw_server bypasses Application._handle, so only the test client's
1600-
# outgoing http.client segment is emitted.
1601-
assert len(items) == 1
1602-
client_span = items[0].payload
1603-
1604-
assert client_span["is_segment"] is True
1605-
assert client_span["attributes"]["sentry.op"] == "http.client"
1606-
assert client_span["name"].startswith("GET http://127.0.0.1:")
1607-
assert resp.request_info.headers[
1608-
"sentry-trace"
1609-
] == "{trace_id}-{span_id}-{sampled}".format(
1610-
trace_id=client_span["trace_id"],
1611-
span_id=client_span["span_id"],
1612-
sampled=1,
1613-
)
1573+
# The outgoing http.client span is suppressed because there is no active
1574+
# span when the test client makes the request.
1575+
assert len(items) == 0
1576+
1577+
# Even though no span is created, the trace propagation headers must still
1578+
# be added to the outgoing request so the trace is not broken.
1579+
assert "sentry-trace" in resp.request_info.headers
1580+
assert "baggage" in resp.request_info.headers
16141581

16151582

16161583
@pytest.mark.asyncio
@@ -1640,7 +1607,7 @@ async def hello(request):
16401607

16411608
sentry_sdk.flush()
16421609

1643-
child_span, server_span, client_span = [item.payload for item in items]
1610+
child_span, server_span = [item.payload for item in items]
16441611

16451612
if send_default_pii:
16461613
assert server_span["attributes"]["user.ip_address"] == "127.0.0.1"

0 commit comments

Comments
 (0)