Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@
# Create a transaction or a span if an transaction is already active
span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options)
if span_streaming:
span = sentry_sdk.traces.start_span(
name=f"{agent.name} workflow", attributes={"sentry.origin": SPAN_ORIGIN}
)
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

Check failure on line 28 in sentry_sdk/integrations/openai_agents/spans/agent_workflow.py

View check run for this annotation

@sentry/warden / warden: code-review

[P4J-HWE] ai_client_span can return None, breaking `with ... as span` in streaming path (additional location)

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`.

Check failure on line 28 in sentry_sdk/integrations/openai_agents/spans/agent_workflow.py

View check run for this annotation

@sentry/warden / warden: find-bugs

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.
Comment on lines +21 to 28

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 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 = None and only assigns a real span when get_current_span() is not None, so return span can return None.
  • runner.py:45 uses with agent_workflow_span(agent) as workflow_span:; entering a with None block raises AttributeError: __enter__.
  • runner.py:156-157 does workflow_span = agent_workflow_span(agent) then workflow_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:58
  • sentry_sdk/integrations/openai_agents/spans/invoke_agent.py:33-41
  • sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py:72
  • sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py:53-63
  • sentry_sdk/integrations/pydantic_ai/spans/ai_client.py:319
  • sentry_sdk/integrations/openai_agents/spans/execute_tool.py:22-22

Identified by Warden find-bugs · WP8-UZK


span = get_start_span_function()(
name=f"{agent.name} workflow",
Expand Down
23 changes: 13 additions & 10 deletions sentry_sdk/integrations/openai_agents/spans/ai_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,17 @@
model_name = agent._sentry_request_model

span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options)
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",
},
)

Check failure on line 44 in sentry_sdk/integrations/openai_agents/spans/ai_client.py

View check run for this annotation

@sentry/warden / warden: code-review

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`.
Comment on lines 34 to +44

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 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_span sets span = None and only assigns a span when get_current_span() is not None, so it returns None in the guarded streaming case.
  • patches/models.py:115 (with ai_client_span(agent, kwargs) as span:) and models.py:154 (with ai_client_span(agent, span_kwargs) as span:) use the return value as a context manager.
  • None has no __enter__/__exit__, so entering the with block raises AttributeError before any span logic runs.
  • Downstream update_ai_client_span(span, ...) and span.set_attribute in the streaming wrapper also dereference span without a None guard.
Also found at 3 additional locations
  • sentry_sdk/integrations/openai_agents/spans/execute_tool.py:23-35
  • sentry_sdk/integrations/openai_agents/spans/invoke_agent.py:32-41
  • sentry_sdk/integrations/openai_agents/spans/agent_workflow.py:21-28

Identified by Warden code-review · P4J-HWE

else:
span = sentry_sdk.start_span(
op=OP.GEN_AI_CHAT,
Expand All @@ -49,10 +51,11 @@
# 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

Check failure on line 58 in sentry_sdk/integrations/openai_agents/spans/ai_client.py

View check run for this annotation

@sentry/warden / warden: find-bugs

[WP8-UZK] agent_workflow_span can return None, crashing both callers (additional location)

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.


def update_ai_client_span(
Expand Down
28 changes: 15 additions & 13 deletions sentry_sdk/integrations/openai_agents/spans/execute_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,21 @@
tool: "agents.Tool", *args: "Any", **kwargs: "Any"
) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]":
span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options)
if span_streaming:

Check failure on line 22 in sentry_sdk/integrations/openai_agents/spans/execute_tool.py

View check run for this annotation

@sentry/warden / warden: find-bugs

[WP8-UZK] agent_workflow_span can return None, crashing both callers (additional location)

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.
span = sentry_sdk.traces.start_span(
name=f"execute_tool {tool.name}",
attributes={
"sentry.op": OP.GEN_AI_EXECUTE_TOOL,
"sentry.origin": SPAN_ORIGIN,
SPANDATA.GEN_AI_OPERATION_NAME: "execute_tool",
SPANDATA.GEN_AI_TOOL_NAME: tool.name,
SPANDATA.GEN_AI_TOOL_DESCRIPTION: tool.description,
},
)

set_on_span = span.set_attribute
span = None
if sentry_sdk.traces.get_current_span() is not None:
span = sentry_sdk.traces.start_span(
name=f"execute_tool {tool.name}",
attributes={
"sentry.op": OP.GEN_AI_EXECUTE_TOOL,
"sentry.origin": SPAN_ORIGIN,
SPANDATA.GEN_AI_OPERATION_NAME: "execute_tool",
SPANDATA.GEN_AI_TOOL_NAME: tool.name,
SPANDATA.GEN_AI_TOOL_DESCRIPTION: tool.description,
},
)

Check failure on line 35 in sentry_sdk/integrations/openai_agents/spans/execute_tool.py

View check run for this annotation

@sentry/warden / warden: code-review

[P4J-HWE] ai_client_span can return None, breaking `with ... as span` in streaming path (additional location)

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`.
set_on_span = span.set_attribute if span is not None else None
else:
span = sentry_sdk.start_span(
op=OP.GEN_AI_EXECUTE_TOOL,
Expand All @@ -46,7 +48,7 @@

set_on_span = span.set_data

if should_send_default_pii():
if span is not None and should_send_default_pii():
input = args[1]
set_on_span(SPANDATA.GEN_AI_TOOL_INPUT, input)

Expand Down
3 changes: 3 additions & 0 deletions sentry_sdk/integrations/openai_agents/spans/handoff.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ def handoff_span(
) -> None:
span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options)
if span_streaming:
if sentry_sdk.traces.get_current_span() is None:
return

with sentry_sdk.traces.start_span(
name=f"handoff from {from_agent.name} to {to_agent_name}",
attributes={
Expand Down
107 changes: 55 additions & 52 deletions sentry_sdk/integrations/openai_agents/spans/invoke_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,17 @@
context: "agents.RunContextWrapper", agent: "agents.Agent", kwargs: "dict[str, Any]"
) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]":
span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options)
if span_streaming:
span = sentry_sdk.traces.start_span(
name=f"invoke_agent {agent.name}",
attributes={
"sentry.op": OP.GEN_AI_INVOKE_AGENT,
"sentry.origin": SPAN_ORIGIN,
SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent",
},
)
span = None
if sentry_sdk.traces.get_current_span() is not None:
span = sentry_sdk.traces.start_span(
name=f"invoke_agent {agent.name}",
attributes={
"sentry.op": OP.GEN_AI_INVOKE_AGENT,
"sentry.origin": SPAN_ORIGIN,
SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent",
},

Check failure on line 41 in sentry_sdk/integrations/openai_agents/spans/invoke_agent.py

View check run for this annotation

@sentry/warden / warden: code-review

[P4J-HWE] ai_client_span can return None, breaking `with ... as span` in streaming path (additional location)

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`.

Check failure on line 41 in sentry_sdk/integrations/openai_agents/spans/invoke_agent.py

View check run for this annotation

@sentry/warden / warden: find-bugs

[WP8-UZK] agent_workflow_span can return None, crashing both callers (additional location)

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.
)
else:
start_span_function = get_start_span_function()
span = start_span_function(
Expand All @@ -49,53 +51,54 @@

span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent")

if should_send_default_pii():
messages = []
if agent.instructions:
message = (
agent.instructions
if isinstance(agent.instructions, str)
else safe_serialize(agent.instructions)
)
messages.append(
{
"content": [{"text": message, "type": "text"}],
"role": "system",
}
)

original_input = kwargs.get("original_input")
if original_input is not None:
message = (
original_input
if isinstance(original_input, str)
else safe_serialize(original_input)
)
messages.append(
{
"content": [{"text": message, "type": "text"}],
"role": "user",
}
)
if span is not None:
if should_send_default_pii():
messages = []
if agent.instructions:
message = (
agent.instructions
if isinstance(agent.instructions, str)
else safe_serialize(agent.instructions)
)
messages.append(
{
"content": [{"text": message, "type": "text"}],
"role": "system",
}
)

if len(messages) > 0:
normalized_messages = normalize_message_roles(messages)
client = sentry_sdk.get_client()
scope = sentry_sdk.get_current_scope()
messages_data = (
truncate_and_annotate_messages(normalized_messages, span, scope)
if should_truncate_gen_ai_input(client.options)
else normalized_messages
)
if messages_data is not None:
set_data_normalized(
span,
SPANDATA.GEN_AI_REQUEST_MESSAGES,
messages_data,
unpack=False,
original_input = kwargs.get("original_input")
if original_input is not None:
message = (
original_input
if isinstance(original_input, str)
else safe_serialize(original_input)
)
messages.append(
{
"content": [{"text": message, "type": "text"}],
"role": "user",
}
)

_set_agent_data(span, agent)
if len(messages) > 0:
normalized_messages = normalize_message_roles(messages)
client = sentry_sdk.get_client()
scope = sentry_sdk.get_current_scope()
messages_data = (
truncate_and_annotate_messages(normalized_messages, span, scope)
if should_truncate_gen_ai_input(client.options)
else normalized_messages
)
if messages_data is not None:
set_data_normalized(
span,
SPANDATA.GEN_AI_REQUEST_MESSAGES,
messages_data,
unpack=False,
)

_set_agent_data(span, agent)

return span

Expand Down
37 changes: 20 additions & 17 deletions sentry_sdk/integrations/pydantic_ai/spans/ai_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,39 +281,42 @@
model_name = _get_model_name(model_obj) or "unknown"

span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options)
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",
SPANDATA.GEN_AI_RESPONSE_STREAMING: get_is_streaming(),
},
)
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",
SPANDATA.GEN_AI_RESPONSE_STREAMING: get_is_streaming(),
},
)
else:
span = sentry_sdk.start_span(
op=OP.GEN_AI_CHAT,
name=f"chat {model_name}",
origin=SPAN_ORIGIN,
)

span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat")
# 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)

Check failure on line 317 in sentry_sdk/integrations/pydantic_ai/spans/ai_client.py

View check run for this annotation

@sentry/warden / warden: code-review

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.
Comment on lines 284 to +317

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 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 = None branch is returned unchanged when get_current_span() is None.
  • graph_nodes.py wraps the result in with ai_client_span(...) as span: (lines 62 and 89); with None raises TypeError: 'NoneType' object does not support the context manager protocol.
  • pydantic_ai/__init__.py calls span.__enter__() directly after assigning the return value, which raises AttributeError on None.
  • The declared return type Union[Span, StreamedSpan] does not include None, so callers are not prompted to guard against it.
Also found at 2 additional locations
  • sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py:71
  • sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py:53-54

Identified by Warden code-review · MGK-CD6


return span

Check failure on line 319 in sentry_sdk/integrations/pydantic_ai/spans/ai_client.py

View check run for this annotation

@sentry/warden / warden: find-bugs

[WP8-UZK] agent_workflow_span can return None, crashing both callers (additional location)

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.


def update_ai_client_span(
Expand Down
41 changes: 22 additions & 19 deletions sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,19 @@
"""
span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options)
if span_streaming:
span = sentry_sdk.traces.start_span(
name=f"execute_tool {tool_name}",
attributes={
"sentry.op": OP.GEN_AI_EXECUTE_TOOL,
"sentry.origin": SPAN_ORIGIN,
SPANDATA.GEN_AI_OPERATION_NAME: "execute_tool",
SPANDATA.GEN_AI_TOOL_NAME: tool_name,
},
)

set_on_span = span.set_attribute
span = None
if sentry_sdk.traces.get_current_span() is not None:
span = sentry_sdk.traces.start_span(
name=f"execute_tool {tool_name}",
attributes={
"sentry.op": OP.GEN_AI_EXECUTE_TOOL,
"sentry.origin": SPAN_ORIGIN,
SPANDATA.GEN_AI_OPERATION_NAME: "execute_tool",
SPANDATA.GEN_AI_TOOL_NAME: tool_name,
},
)

set_on_span = span.set_attribute if span is not None else None
else:
span = sentry_sdk.start_span(
op=OP.GEN_AI_EXECUTE_TOOL,
Expand All @@ -54,19 +56,20 @@

set_on_span = span.set_data

if tool_definition is not None and hasattr(tool_definition, "description"):
set_on_span(
SPANDATA.GEN_AI_TOOL_DESCRIPTION,
tool_definition.description,
)
if span is not None:
if tool_definition is not None and hasattr(tool_definition, "description"):
set_on_span(
SPANDATA.GEN_AI_TOOL_DESCRIPTION,
tool_definition.description,
)

_set_agent_data(span, agent)
_set_agent_data(span, agent)

if _should_send_prompts() and tool_args is not None:
set_on_span(SPANDATA.GEN_AI_TOOL_INPUT, safe_serialize(tool_args))
if _should_send_prompts() and tool_args is not None:
set_on_span(SPANDATA.GEN_AI_TOOL_INPUT, safe_serialize(tool_args))

return span

Check failure on line 71 in sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py

View check run for this annotation

@sentry/warden / warden: code-review

[MGK-CD6] ai_client_span can return None but callers use it as a context manager (additional location)

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.

Check failure on line 72 in sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py

View check run for this annotation

@sentry/warden / warden: find-bugs

[WP8-UZK] agent_workflow_span can return None, crashing both callers (additional location)

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.

def update_execute_tool_span(
span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", result: "Any"
Expand Down
Loading
Loading