Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion sentry_sdk/integrations/aiohttp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
sentry-warden[bot] marked this conversation as resolved.

attributes: "Attributes" = {
"sentry.op": OP.HTTP_CLIENT,
"sentry.origin": AioHttpIntegration.origin,
Expand Down
4 changes: 4 additions & 0 deletions sentry_sdk/integrations/graphene.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions sentry_sdk/integrations/litestar.py
Original file line number Diff line number Diff line change
Expand Up @@ -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={
Expand All @@ -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={
Expand All @@ -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={
Expand Down
3 changes: 3 additions & 0 deletions sentry_sdk/integrations/starlette.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
capture_internal_exceptions,
ensure_integration_enabled,
event_from_exception,
nullcontext,
parse_version,
transaction_from_function,
)
Expand Down Expand Up @@ -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={
Expand Down
3 changes: 3 additions & 0 deletions sentry_sdk/integrations/starlite.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from sentry_sdk.utils import (
ensure_integration_enabled,
event_from_exception,
nullcontext,
transaction_from_function,
)

Expand Down Expand Up @@ -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={
Expand Down
18 changes: 18 additions & 0 deletions sentry_sdk/integrations/strawberry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down Expand Up @@ -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={
Expand Down Expand Up @@ -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={
Expand Down Expand Up @@ -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={
Expand Down Expand Up @@ -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={
Expand Down
88 changes: 25 additions & 63 deletions tests/integrations/aiohttp/test_aiohttp.py
Original file line number Diff line number Diff line change
Expand Up @@ -1162,16 +1162,12 @@

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 (
Expand Down Expand Up @@ -1242,7 +1238,7 @@

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
Expand Down Expand Up @@ -1282,7 +1278,7 @@

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.
Expand Down Expand Up @@ -1321,8 +1317,8 @@

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"
Expand Down Expand Up @@ -1378,8 +1374,8 @@

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"]
Expand Down Expand Up @@ -1408,21 +1404,14 @@

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__
Expand Down Expand Up @@ -1456,13 +1445,8 @@

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
Expand Down Expand Up @@ -1493,13 +1477,8 @@

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
Expand Down Expand Up @@ -1538,18 +1517,13 @@

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"

Expand Down Expand Up @@ -1592,28 +1566,16 @@
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

Check warning on line 1578 in tests/integrations/aiohttp/test_aiohttp.py

View check run for this annotation

@sentry/warden / warden: code-review

Outgoing trace header propagation dropped when no active span in streaming mode

This test previously verified that the `sentry-trace` header was propagated on outgoing requests, but now asserts zero spans and no header assertions; the underlying early-return in `aiohttp.py` (lines 356-358) skips adding trace headers entirely when there is no current span, which can break distributed trace continuation for outgoing HTTP calls.
Comment on lines +1573 to 1578

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Outgoing trace header propagation dropped when no active span in streaming mode

This test previously verified that the sentry-trace header was propagated on outgoing requests, but now asserts zero spans and no header assertions; the underlying early-return in aiohttp.py (lines 356-358) skips adding trace headers entirely when there is no current span, which can break distributed trace continuation for outgoing HTTP calls.

Evidence
  • aiohttp.py:356-358 returns early (trace_config_ctx.span = None; return) before the block that attaches sentry-trace/baggage headers when get_current_span() is None.
  • The revised test drops the prior assertions on resp.request_info.headers['sentry-trace'] and now only checks len(items) == 0, removing coverage for outgoing trace-header propagation.
  • Distributed tracing normally propagates trace context from the propagation context even absent an active span, so suppressing the header can sever the trace chain.

Identified by Warden code-review · DYK-FKT

@pytest.mark.parametrize("send_default_pii", [True, False])
async def test_user_ip_address_on_all_spans(
sentry_init, aiohttp_client, capture_items, send_default_pii
Expand All @@ -1640,7 +1602,7 @@

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"
Expand Down
Loading