Skip to content

ref: Don't auto-promote child spans to segments in integrations in span streaming#6807

Closed
sentrivana wants to merge 5 commits into
masterfrom
ivana/keep-child-span-child-spans
Closed

ref: Don't auto-promote child spans to segments in integrations in span streaming#6807
sentrivana wants to merge 5 commits into
masterfrom
ivana/keep-child-span-child-spans

Conversation

@sentrivana

Copy link
Copy Markdown
Contributor

Description

Issues

Reminders

sentrivana and others added 5 commits July 13, 2026 14:47
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>
@sentrivana

Copy link
Copy Markdown
Contributor Author

Split up into multiple smaller PRs

@sentrivana sentrivana closed this Jul 13, 2026
Comment on lines +21 to 28
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

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.

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 = None and only assigns a real span when get_current_span() is not None, then return span.
  • runner.py:45 uses with agent_workflow_span(agent) as workflow_span:; entering a with None invokes None.__enter__() raising AttributeError.
  • runner.py:156-157 does workflow_span = agent_workflow_span(agent) then workflow_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-294
  • sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py:72
  • sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py:54-62
  • sentry_sdk/integrations/openai_agents/spans/invoke_agent.py:104
  • sentry_sdk/integrations/openai_agents/spans/ai_client.py:58

Identified by Warden code-review · BB3-UMW

Comment on lines +176 to +178
if sentry_sdk.traces.get_current_span() is None:
return await real_send(self, request, **kwargs)

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 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 the with 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 else branch still injects propagation headers unconditionally, so behavior now diverges when no parent span is active.

Identified by Warden code-review · RW3-RUU

Comment on lines 35 to 58
@@ -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

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.

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 = None and only assigns a real span when sentry_sdk.traces.get_current_span() is not None, so ai_client_span now returns None in the streaming-no-parent case.
  • patches/models.py:115 (with ai_client_span(agent, kwargs) as span:) and patches/models.py:154 (with ai_client_span(agent, span_kwargs) as span:) consume the return value directly as a context manager, with no None guard.
  • with None as span: invokes None.__enter__, raising AttributeError, so the wrapped get_response/stream_response call is aborted before the original model call runs.
  • The subsequent span.set_attribute/span.set_data in the streaming wrapper (models.py:158-166) would also fail on None, but the with statement fails first.
Also found at 4 additional locations
  • sentry_sdk/integrations/pydantic_ai/spans/ai_client.py:286-319
  • sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py:54-54
  • sentry_sdk/integrations/openai_agents/spans/agent_workflow.py:21-28
  • sentry_sdk/integrations/openai_agents/spans/execute_tool.py:22-35

Identified by Warden find-bugs · 786-YEA

Comment on lines +117 to +118
if sentry_sdk.traces.get_current_span() is None:
return real_putrequest(self, method, url, *args, **kwargs)

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.

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 calls iter_trace_propagation_headers and self.putheader(key, value) when should_propagate_trace(client, real_url) is true.
  • scope.iter_trace_propagation_headers (scope.py:680) falls back to get_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 None case differently: it still calls _update_client_call_details_metadata_from_scope to 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-178
  • sentry_sdk/integrations/httpx.py:176-178

Identified by Warden find-bugs · KKL-B67

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant