ref: Don't auto-promote child spans to segments in integrations in span streaming#6807
ref: Don't auto-promote child spans to segments in integrations in span streaming#6807sentrivana wants to merge 5 commits into
Conversation
Move nullcontext from _wsgi_common to utils so it can be imported without pulling in WSGI-specific code. Update all consumers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ent span In span streaming mode, sentry_sdk.traces.start_span() without parent_span=None creates a new root segment when there's no current span. For integrations that create child spans (not root/transaction spans), this is wrong — they should do nothing instead. Add get_current_span() checks to all streaming-path child span creation across 52 integration files. Root span creators (those with parent_span=None in the non-streaming path) are left untouched. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace nullcontext + if-span-is-not-None patterns with early returns when the with block ends with a return. This removes unnecessary nesting and nullcontext imports from 23 files. The only remaining nullcontext usage is in pyreqwest.py where the function is a generator context manager that must yield. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Split up into multiple smaller PRs |
| span = None | ||
| if sentry_sdk.traces.get_current_span() is not None: | ||
| span = sentry_sdk.traces.start_span( | ||
| name=f"{agent.name} workflow", | ||
| attributes={"sentry.origin": SPAN_ORIGIN}, | ||
| ) | ||
|
|
||
| return span |
There was a problem hiding this comment.
agent_workflow_span can now return None, crashing both callers
When span streaming is enabled and there is no current span, this returns None, but callers do with agent_workflow_span(...) and workflow_span.__enter__() which raise AttributeError on None.
Evidence
- New code sets
span = Noneand only assigns a real span whenget_current_span() is not None, thenreturn span. runner.py:45useswith agent_workflow_span(agent) as workflow_span:; entering awith NoneinvokesNone.__enter__()raisingAttributeError.runner.py:156-157doesworkflow_span = agent_workflow_span(agent)thenworkflow_span.__enter__(), dereferencing None.- The return type annotation is
Union[Span, StreamedSpan]and does not include None, so callers do not guard for it.
Also found at 5 additional locations
sentry_sdk/integrations/pydantic_ai/spans/ai_client.py:284-294sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py:72sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py:54-62sentry_sdk/integrations/openai_agents/spans/invoke_agent.py:104sentry_sdk/integrations/openai_agents/spans/ai_client.py:58
Identified by Warden code-review · BB3-UMW
| if sentry_sdk.traces.get_current_span() is None: | ||
| return await real_send(self, request, **kwargs) | ||
|
|
There was a problem hiding this comment.
Outgoing requests skip trace propagation when no current span exists
When span streaming is enabled and there is no current span, the early return await real_send(...) bypasses should_propagate_trace/header injection, so outgoing requests won't carry trace propagation headers — was this intended?
Evidence
- The new guard returns
real_send(self, request, **kwargs)before entering thewith sentry_sdk.traces.start_span(...)block. - Trace propagation (
should_propagate_trace,iter_trace_propagation_headers, baggage/header injection) only runs inside that block (lines ~200+). - The non-streaming
elsebranch still injects propagation headers unconditionally, so behavior now diverges when no parent span is active.
Identified by Warden code-review · RW3-RUU
| @@ -49,8 +51,9 @@ def ai_client_span( | |||
| # TODO-anton: remove hardcoded stuff and replace something that also works for embedding and so on | |||
| span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat") | |||
|
|
|||
| _set_agent_data(span, agent) | |||
| _set_input_data(span, get_response_kwargs) | |||
| if span is not None: | |||
| _set_agent_data(span, agent) | |||
| _set_input_data(span, get_response_kwargs) | |||
|
|
|||
| return span | |||
There was a problem hiding this comment.
ai_client_span can return None, breaking the with statement in callers
When span streaming is enabled but there is no current span, ai_client_span returns None, which is then used as with ai_client_span(...) as span: in patches/models.py and raises AttributeError: __enter__.
Evidence
- The new branch sets
span = Noneand only assigns a real span whensentry_sdk.traces.get_current_span() is not None, soai_client_spannow returnsNonein the streaming-no-parent case. patches/models.py:115(with ai_client_span(agent, kwargs) as span:) andpatches/models.py:154(with ai_client_span(agent, span_kwargs) as span:) consume the return value directly as a context manager, with noNoneguard.with None as span:invokesNone.__enter__, raisingAttributeError, so the wrappedget_response/stream_responsecall is aborted before the original model call runs.- The subsequent
span.set_attribute/span.set_datain the streaming wrapper (models.py:158-166) would also fail onNone, but thewithstatement fails first.
Also found at 4 additional locations
sentry_sdk/integrations/pydantic_ai/spans/ai_client.py:286-319sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py:54-54sentry_sdk/integrations/openai_agents/spans/agent_workflow.py:21-28sentry_sdk/integrations/openai_agents/spans/execute_tool.py:22-35
Identified by Warden find-bugs · 786-YEA
| if sentry_sdk.traces.get_current_span() is None: | ||
| return real_putrequest(self, method, url, *args, **kwargs) |
There was a problem hiding this comment.
Early return skips outgoing trace propagation headers when no current streamed span
In span-streaming mode, when there is no current span the patched putrequest returns before reaching the should_propagate_trace block, so outgoing HTTP requests no longer get sentry-trace/baggage headers, breaking distributed trace propagation for requests made outside an active streamed span.
Evidence
- The new
if sentry_sdk.traces.get_current_span() is None: return real_putrequest(...)at stdlib.py:117-118 returns before the trace-propagation block that callsiter_trace_propagation_headersandself.putheader(key, value)whenshould_propagate_trace(client, real_url)is true. scope.iter_trace_propagation_headers(scope.py:680) falls back toget_active_propagation_context().iter_headers()when no span is passed, so headers would still be produced from the scope's propagation context — the early return discards them.- The grpc client integration (grpc/client.py:36-40) handles the same
get_current_span() is Nonecase differently: it still calls_update_client_call_details_metadata_from_scopeto inject propagation metadata before returning, showing propagation is intended even without a streamed span. - The non-streaming branch always creates a span and always reaches the propagation block, so distributed tracing continues there — the streaming path is the one that loses propagation.
Also found at 2 additional locations
sentry_sdk/integrations/httpx2.py:176-178sentry_sdk/integrations/httpx.py:176-178
Identified by Warden find-bugs · KKL-B67
Description
Issues
Reminders
uv run ruff.feat:,fix:,ref:,meta:)