fix(tracing): Skip child span creation in streaming path when no current span (AI agents)#6813
fix(tracing): Skip child span creation in streaming path when no current span (AI agents)#6813sentrivana wants to merge 1 commit into
Conversation
…ent span (AI agents) When span streaming is enabled and there is no current span, AI agent integrations should not create new root segments for child-span operations. This adds a guard check using `sentry_sdk.traces.get_current_span()` before creating spans in the streaming path. Affected integrations: openai_agents, pydantic_ai. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| if span_streaming: | ||
| span = sentry_sdk.traces.start_span( | ||
| name=f"chat {model_name}", | ||
| attributes={ | ||
| "sentry.op": OP.GEN_AI_CHAT, | ||
| "sentry.origin": SPAN_ORIGIN, | ||
| SPANDATA.GEN_AI_OPERATION_NAME: "chat", | ||
| }, | ||
| ) | ||
| span = None | ||
| if sentry_sdk.traces.get_current_span() is not None: | ||
| span = sentry_sdk.traces.start_span( | ||
| name=f"chat {model_name}", | ||
| attributes={ | ||
| "sentry.op": OP.GEN_AI_CHAT, | ||
| "sentry.origin": SPAN_ORIGIN, | ||
| SPANDATA.GEN_AI_OPERATION_NAME: "chat", | ||
| }, | ||
| ) |
There was a problem hiding this comment.
ai_client_span can return None, breaking with ... as span in streaming path
In streaming mode with no current span, ai_client_span now returns None, but callers use it via with ai_client_span(...) as span, which raises AttributeError: __enter__ on None.
Evidence
ai_client_spansetsspan = Noneand only assigns a span whenget_current_span() is not None, so it returnsNonein the guarded streaming case.patches/models.py:115(with ai_client_span(agent, kwargs) as span:) andmodels.py:154(with ai_client_span(agent, span_kwargs) as span:) use the return value as a context manager.Nonehas no__enter__/__exit__, so entering thewithblock raisesAttributeErrorbefore any span logic runs.- Downstream
update_ai_client_span(span, ...)andspan.set_attributein the streaming wrapper also dereferencespanwithout aNoneguard.
Also found at 3 additional locations
sentry_sdk/integrations/openai_agents/spans/execute_tool.py:23-35sentry_sdk/integrations/openai_agents/spans/invoke_agent.py:32-41sentry_sdk/integrations/openai_agents/spans/agent_workflow.py:21-28
Identified by Warden code-review · P4J-HWE
| @@ -302,16 +304,17 @@ def ai_client_span( | |||
| # Set streaming flag from contextvar | |||
| span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, get_is_streaming()) | |||
|
|
|||
| _set_agent_data(span, agent) | |||
| _set_model_data(span, model, model_settings) | |||
| if span is not None: | |||
| _set_agent_data(span, agent) | |||
| _set_model_data(span, model, model_settings) | |||
|
|
|||
| # Add available tools if agent is available | |||
| agent_obj = agent or get_current_agent() | |||
| _set_available_tools(span, agent_obj) | |||
| # Add available tools if agent is available | |||
| agent_obj = agent or get_current_agent() | |||
| _set_available_tools(span, agent_obj) | |||
|
|
|||
| # Set input messages (full conversation history) | |||
| if messages: | |||
| _set_input_messages(span, messages) | |||
| # Set input messages (full conversation history) | |||
| if messages: | |||
| _set_input_messages(span, messages) | |||
There was a problem hiding this comment.
ai_client_span can return None but callers use it as a context manager
When streaming is enabled and there is no current span, ai_client_span now returns None, but callers do with ai_client_span(...) as span: (graph_nodes.py:62, 89) and span.__enter__() (init.py), which will raise a TypeError/AttributeError on None.
Evidence
- New
span = Nonebranch is returned unchanged whenget_current_span() is None. graph_nodes.pywraps the result inwith ai_client_span(...) as span:(lines 62 and 89);with NoneraisesTypeError: 'NoneType' object does not support the context manager protocol.pydantic_ai/__init__.pycallsspan.__enter__()directly after assigning the return value, which raisesAttributeErroron None.- The declared return type
Union[Span, StreamedSpan]does not includeNone, so callers are not prompted to guard against it.
Also found at 2 additional locations
sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py:71sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py:53-54
Identified by Warden code-review · MGK-CD6
| 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 return None, crashing both callers
In streaming mode with no current span this returns None, but callers do with agent_workflow_span(agent) as workflow_span: and workflow_span.__enter__(), both of which raise AttributeError on None.
Evidence
- The new branch sets
span = Noneand only assigns a real span whenget_current_span() is not None, soreturn spancan return None. runner.py:45useswith agent_workflow_span(agent) as workflow_span:; entering awith Noneblock raisesAttributeError: __enter__.runner.py:156-157doesworkflow_span = agent_workflow_span(agent)thenworkflow_span.__enter__(), dereferencing None with no guard.- Neither caller checks the return value for None before using it.
Also found at 6 additional locations
sentry_sdk/integrations/openai_agents/spans/ai_client.py:58sentry_sdk/integrations/openai_agents/spans/invoke_agent.py:33-41sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py:72sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py:53-63sentry_sdk/integrations/pydantic_ai/spans/ai_client.py:319sentry_sdk/integrations/openai_agents/spans/execute_tool.py:22-22
Identified by Warden find-bugs · WP8-UZK
Summary
sentry_sdk.traces.get_current_span() is Noneguard before creating spans in the streaming pathTest plan
🤖 Generated with Claude Code