diff --git a/sentry_sdk/integrations/aiohttp.py b/sentry_sdk/integrations/aiohttp.py index d22f3a745b..f262f1330f 100644 --- a/sentry_sdk/integrations/aiohttp.py +++ b/sentry_sdk/integrations/aiohttp.py @@ -351,8 +351,12 @@ async def on_request_start( parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, ) - span: "Union[Span, StreamedSpan]" + span: "Union[Span, StreamedSpan, None]" if has_span_streaming_enabled(client.options): + if sentry_sdk.traces.get_current_span() is None: + trace_config_ctx.span = None + return + attributes: "Attributes" = { "sentry.op": OP.HTTP_CLIENT, "sentry.origin": AioHttpIntegration.origin, diff --git a/sentry_sdk/integrations/graphene.py b/sentry_sdk/integrations/graphene.py index 4938a5c0f3..ce2b545241 100644 --- a/sentry_sdk/integrations/graphene.py +++ b/sentry_sdk/integrations/graphene.py @@ -149,6 +149,10 @@ def graphql_span( ) if is_span_streaming_enabled: + if sentry_sdk.traces.get_current_span() is None: + yield + return + additional_attributes = {} if should_send_default_pii(): additional_attributes["graphql.document"] = source diff --git a/sentry_sdk/integrations/litestar.py b/sentry_sdk/integrations/litestar.py index f0c90a7921..cb7aa87ae9 100644 --- a/sentry_sdk/integrations/litestar.py +++ b/sentry_sdk/integrations/litestar.py @@ -164,6 +164,8 @@ async def _create_span_call( middleware_name = self.__class__.__name__ if has_span_streaming_enabled(client.options): + if sentry_sdk.traces.get_current_span() is None: + return await old_call(self, scope, receive, send) with sentry_sdk.traces.start_span( name=middleware_name, attributes={ @@ -179,6 +181,8 @@ async def _sentry_receive( ) -> "Union[HTTPReceiveMessage, WebSocketReceiveMessage]": if client.get_integration(LitestarIntegration) is None: return await receive(*args, **kwargs) + if sentry_sdk.traces.get_current_span() is None: + return await receive(*args, **kwargs) with sentry_sdk.traces.start_span( name=getattr(receive, "__qualname__", str(receive)), attributes={ @@ -197,6 +201,8 @@ async def _sentry_receive( async def _sentry_send(message: "Message") -> None: if client.get_integration(LitestarIntegration) is None: return await send(message) + if sentry_sdk.traces.get_current_span() is None: + return await send(message) with sentry_sdk.traces.start_span( name=getattr(send, "__qualname__", str(send)), attributes={ diff --git a/sentry_sdk/integrations/starlette.py b/sentry_sdk/integrations/starlette.py index 3f9fbf4d8e..74effb1516 100644 --- a/sentry_sdk/integrations/starlette.py +++ b/sentry_sdk/integrations/starlette.py @@ -35,6 +35,7 @@ capture_internal_exceptions, ensure_integration_enabled, event_from_exception, + nullcontext, parse_version, transaction_from_function, ) @@ -198,6 +199,8 @@ async def _create_span_call( def _start_middleware_span(op: str, name: str) -> "Any": if is_span_streaming_enabled: + if sentry_sdk.traces.get_current_span() is None: + return nullcontext() return sentry_sdk.traces.start_span( name=name, attributes={ diff --git a/sentry_sdk/integrations/starlite.py b/sentry_sdk/integrations/starlite.py index 1eebd37e84..3b759a62b7 100644 --- a/sentry_sdk/integrations/starlite.py +++ b/sentry_sdk/integrations/starlite.py @@ -10,6 +10,7 @@ from sentry_sdk.utils import ( ensure_integration_enabled, event_from_exception, + nullcontext, transaction_from_function, ) @@ -151,6 +152,8 @@ async def _create_span_call( def _start_middleware_span(op: str, name: str) -> "Any": if is_span_streaming_enabled: + if sentry_sdk.traces.get_current_span() is None: + return nullcontext() return sentry_sdk.traces.start_span( name=name, attributes={ diff --git a/sentry_sdk/integrations/strawberry.py b/sentry_sdk/integrations/strawberry.py index 5f00e8bf6d..4e41133f3a 100644 --- a/sentry_sdk/integrations/strawberry.py +++ b/sentry_sdk/integrations/strawberry.py @@ -188,6 +188,10 @@ def on_operation(self) -> "Generator[None, None, None]": client = sentry_sdk.get_client() is_span_streaming_enabled = has_span_streaming_enabled(client.options) if is_span_streaming_enabled: + if sentry_sdk.traces.get_current_span() is None: + yield + return + additional_attributes: "dict[str, Any]" = {} if should_send_default_pii(): @@ -244,6 +248,10 @@ def on_validate(self) -> "Generator[None, None, None]": is_span_streaming_enabled = has_span_streaming_enabled(client.options) if is_span_streaming_enabled: + if sentry_sdk.traces.get_current_span() is None: + yield + return + validation_span = sentry_sdk.traces.start_span( name="validation", attributes={ @@ -272,6 +280,10 @@ def on_parse(self) -> "Generator[None, None, None]": is_span_streaming_enabled = has_span_streaming_enabled(client.options) if is_span_streaming_enabled: + if sentry_sdk.traces.get_current_span() is None: + yield + return + parsing_span = sentry_sdk.traces.start_span( name="parsing", attributes={ @@ -333,6 +345,9 @@ async def resolve( client = sentry_sdk.get_client() is_span_streaming_enabled = has_span_streaming_enabled(client.options) if is_span_streaming_enabled: + if sentry_sdk.traces.get_current_span() is None: + return await self._resolve(_next, root, info, *args, **kwargs) + with sentry_sdk.traces.start_span( name=f"resolving {field_path}", attributes={ @@ -372,6 +387,9 @@ def resolve( client = sentry_sdk.get_client() is_span_streaming_enabled = has_span_streaming_enabled(client.options) if is_span_streaming_enabled: + if sentry_sdk.traces.get_current_span() is None: + return _next(root, info, *args, **kwargs) + with sentry_sdk.traces.start_span( name=f"resolving {field_path}", attributes={ diff --git a/tests/integrations/aiohttp/test_aiohttp.py b/tests/integrations/aiohttp/test_aiohttp.py index 583e7a50b6..4a7c663d0e 100644 --- a/tests/integrations/aiohttp/test_aiohttp.py +++ b/tests/integrations/aiohttp/test_aiohttp.py @@ -1162,16 +1162,12 @@ async def hello(request): sentry_sdk.flush() - # The aiohttp_client fixture is itself sentry-instrumented and emits the - # first http.client segment; the server-side http.server span is the other - # segment. Asserting the exact length confirms no other spans leak in. - assert len(items) == 2 - - server_span, client_span = [item.payload for item in items] + # The server-side http.server span is the only segment. The aiohttp_client + # fixture's outgoing http.client span is suppressed because there is no + # active span when the test client makes the request. + assert len(items) == 1 - assert client_span["is_segment"] is True - assert client_span["attributes"]["sentry.op"] == "http.client" - assert client_span["name"].startswith("GET http://127.0.0.1:") + (server_span,) = [item.payload for item in items] assert server_span["is_segment"] is True assert ( @@ -1242,7 +1238,7 @@ async def hello(request): sentry_sdk.flush() - server_span, _client_segment = [item.payload for item in items] + (server_span,) = [item.payload for item in items] # send_default_pii defaults to False, so _filter_headers substitutes # sensitive headers with SENSITIVE_DATA_SUBSTITUTE ("[Filtered]"). The @@ -1282,7 +1278,7 @@ async def hello(request): sentry_sdk.flush() - server_span, _client_segment = [item.payload for item in items] + (server_span,) = [item.payload for item in items] # With send_default_pii=True, _filter_headers is a no-op and the original # value reaches the span attribute. @@ -1321,8 +1317,8 @@ async def hello(request): sentry_sdk.flush() - assert len(items) == 2 - server_segment, client_segment = [item.payload for item in items] + assert len(items) == 1 + (server_segment,) = [item.payload for item in items] if send_pii: assert server_segment["attributes"]["url.query"] == "foo=bar&baz=qux" @@ -1378,8 +1374,8 @@ async def hello(request): sentry_sdk.flush() - assert len(items) == 2 - server_segment, client_segment = [item.payload for item in items] + assert len(items) == 1 + (server_segment,) = [item.payload for item in items] assert server_segment["name"] == expected_name assert server_segment["is_segment"] @@ -1408,21 +1404,14 @@ async def hello(request): sentry_sdk.flush() - # 1 error event + 2 spans (server http.server, test client http.client segment) - assert len(items) == 3 + # 1 error event + 1 span (server http.server) + assert len(items) == 2 error_event = items[0] assert error_event.type == "event" assert error_event.payload["exception"]["values"][0]["type"] == "ZeroDivisionError" - server_span, segment = [item.payload for item in items[1:]] - - assert segment["is_segment"] is True - assert segment["attributes"]["sentry.op"] == "http.client" - # The test client receives the 500 response that aiohttp's outer error - # handler synthesizes from the unhandled exception. - assert segment["attributes"]["http.response.status_code"] == 500 - assert segment["status"] == "error" + server_span = items[1].payload # The integration's generic Exception path reraises without recording # http.response.status_code on the server span. StreamedSpan.__exit__ @@ -1456,13 +1445,8 @@ async def hello(request): sentry_sdk.flush() - assert len(items) == 2 - server_span, segment = [item.payload for item in items] - - assert segment["is_segment"] is True - assert segment["attributes"]["sentry.op"] == "http.client" - assert segment["attributes"]["http.response.status_code"] == 403 - assert segment["status"] == "error" + assert len(items) == 1 + (server_span,) = [item.payload for item in items] assert server_span["attributes"]["sentry.op"] == "http.server" assert server_span["attributes"]["http.response.status_code"] == 403 @@ -1493,13 +1477,8 @@ async def hello(request): sentry_sdk.flush() - assert len(items) == 2 - server_span, segment = [item.payload for item in items] - - assert segment["is_segment"] is True - assert segment["attributes"]["sentry.op"] == "http.client" - assert segment["attributes"]["http.response.status_code"] == 302 - assert segment["status"] == "ok" + assert len(items) == 1 + (server_span,) = [item.payload for item in items] assert server_span["attributes"]["sentry.op"] == "http.server" assert server_span["attributes"]["http.response.status_code"] == 302 @@ -1538,18 +1517,13 @@ async def hello(request): sentry_sdk.flush() - # 3 spans, finished inner-first: + # 2 spans, finished inner-first: # #0 inner http.client (server -> raw_server) # #1 server http.server - # #2 outer http.client segment (test client -> server) - assert len(items) == 3 + assert len(items) == 2 inner_client_span = items[0].payload server_span = items[1].payload - segment = items[2].payload - - assert segment["is_segment"] is True - assert segment["attributes"]["sentry.op"] == "http.client" assert server_span["attributes"]["sentry.op"] == "http.server" @@ -1592,25 +1566,13 @@ async def handler(request): items = capture_items("span") client = await aiohttp_client(raw_server) - resp = await client.get("/") + await client.get("/") sentry_sdk.flush() - # raw_server bypasses Application._handle, so only the test client's - # outgoing http.client segment is emitted. - assert len(items) == 1 - client_span = items[0].payload - - assert client_span["is_segment"] is True - assert client_span["attributes"]["sentry.op"] == "http.client" - assert client_span["name"].startswith("GET http://127.0.0.1:") - assert resp.request_info.headers[ - "sentry-trace" - ] == "{trace_id}-{span_id}-{sampled}".format( - trace_id=client_span["trace_id"], - span_id=client_span["span_id"], - sampled=1, - ) + # The outgoing http.client span is suppressed because there is no active + # span when the test client makes the request. + assert len(items) == 0 @pytest.mark.asyncio @@ -1640,7 +1602,7 @@ async def hello(request): sentry_sdk.flush() - child_span, server_span, client_span = [item.payload for item in items] + child_span, server_span = [item.payload for item in items] if send_default_pii: assert server_span["attributes"]["user.ip_address"] == "127.0.0.1"