Skip to content
Draft
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
9 changes: 6 additions & 3 deletions sentry_sdk/integrations/openai_agents/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ async def new_wrapped_run_single_turn(
*args: "Any", **kwargs: "Any"
) -> "SingleStepResult":
return await _run_single_turn(
run_loop.run_single_turn, *args, **kwargs
run_loop.run_single_turn, use_run_hooks, *args, **kwargs
)

agents.run.run_single_turn = new_wrapped_run_single_turn
Expand All @@ -125,6 +125,7 @@ async def new_wrapped_run_single_turn_streamed(
) -> "SingleStepResult":
return await _run_single_turn_streamed(
original_run_single_turn_streamed,
use_run_hooks,
*args,
**kwargs,
)
Expand Down Expand Up @@ -207,7 +208,9 @@ def old_wrapped_get_model(
async def old_wrapped_run_single_turn(
cls: "agents.Runner", *args: "Any", **kwargs: "Any"
) -> "SingleStepResult":
return await _run_single_turn(original_run_single_turn, *args, **kwargs)
return await _run_single_turn(
original_run_single_turn, use_run_hooks, *args, **kwargs
)

agents.run.AgentRunner._run_single_turn = classmethod(
old_wrapped_run_single_turn
Expand All @@ -220,7 +223,7 @@ async def old_wrapped_run_single_turn_streamed(
cls: "agents.Runner", *args: "Any", **kwargs: "Any"
) -> "SingleStepResult":
return await _run_single_turn_streamed(
original_run_single_turn_streamed, *args, **kwargs
original_run_single_turn_streamed, use_run_hooks, *args, **kwargs
)

agents.run.AgentRunner._run_single_turn_streamed = classmethod(
Expand Down
141 changes: 90 additions & 51 deletions sentry_sdk/integrations/openai_agents/patches/agent_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
if TYPE_CHECKING:
from typing import Any, Awaitable, Callable, Optional, Union

from agents import TResponseInputItem
from agents.run_internal.run_steps import SingleStepResult

from sentry_sdk.tracing import Span
Expand Down Expand Up @@ -49,7 +50,7 @@
context_wrapper: "agents.RunContextWrapper",
agent: "agents.Agent",
should_run_agent_start_hooks: bool,
span_kwargs: "dict[str, Any]",
turn_input: "Optional[list[TResponseInputItem]]",
is_streaming: bool = False,
) -> "Optional[Union[Span, StreamedSpan]]":
"""
Expand All @@ -68,14 +69,14 @@
span = getattr(context_wrapper, "_sentry_agent_span", None)
if span:
update_invoke_agent_span(
span=span, context=context_wrapper, agent=agent
span=span, usage=context_wrapper.usage, agent=agent
)
span.__exit__(None, None, None)
delattr(context_wrapper, "_sentry_agent_span")

# Store the agent on the context wrapper so we can access it later
context_wrapper._sentry_current_agent = agent
span = invoke_agent_span(context_wrapper, agent, span_kwargs)
span = invoke_agent_span(agent, turn_input)
context_wrapper._sentry_agent_span = span
agent._sentry_agent_span = span

Expand All @@ -92,6 +93,7 @@

async def _run_single_turn(
original_run_single_turn: "Callable[..., Awaitable[SingleStepResult]]",
use_run_hooks: "bool",
*args: "Any",
**kwargs: "Any",
) -> "SingleStepResult":
Expand All @@ -107,39 +109,56 @@
if bindings is not None
else kwargs.get("agent")
)
context_wrapper = kwargs.get("context_wrapper")
should_run_agent_start_hooks = kwargs.get("should_run_agent_start_hooks", False)

span = _maybe_start_agent_span(
context_wrapper, agent, should_run_agent_start_hooks, kwargs
)
context_wrapper: "agents.RunContextWrapper[Any]" = kwargs.get("context_wrapper")
if not use_run_hooks:
should_run_agent_start_hooks = kwargs.get("should_run_agent_start_hooks", False)

if (
span is None
or (isinstance(span, StreamedSpan) and span.end_timestamp is not None)
or (not isinstance(span, StreamedSpan) and span.timestamp is not None)
):
return await original_run_single_turn(*args, **kwargs)
span = _maybe_start_agent_span(
context_wrapper,
agent,
should_run_agent_start_hooks,
kwargs.get("input"),
)

if (
span is None
or (isinstance(span, StreamedSpan) and span.end_timestamp is not None)
or (not isinstance(span, StreamedSpan) and span.timestamp is not None)
):
return await original_run_single_turn(*args, **kwargs)

try:
result = await original_run_single_turn(*args, **kwargs)
except Exception:
exc_info = sys.exc_info()
with capture_internal_exceptions():
span = getattr(context_wrapper, "_sentry_agent_span", None)
if span:
update_invoke_agent_span(
span=span, context=context_wrapper, agent=agent
)
span.__exit__(*exc_info)
delattr(context_wrapper, "_sentry_agent_span")
if use_run_hooks:
run_hooks = kwargs.get("hooks")
if run_hooks is not None:
span = getattr(run_hooks, "_sentry_invoke_agent_span", None)

Check failure on line 139 in sentry_sdk/integrations/openai_agents/patches/agent_run.py

View check run for this annotation

@sentry/warden / warden: find-bugs

[XD7-X2Q] Invoke agent span leaks on exception when user-provided hooks are used (additional location)

The exception handler retrieves `_sentry_invoke_agent_span` from `run_hooks`, but when the user provides a `RunHooks` instance `_patch_run_hooks` stores the span on a separate internal `_SentryRunHooks` object, so the span is never finished and leaks on every exception.
if span is not None:
update_invoke_agent_span(
span=span, usage=context_wrapper.usage, agent=agent
Comment thread
alexander-alderman-webb marked this conversation as resolved.
)
del run_hooks._sentry_invoke_agent_span
span.__exit__(*exc_info)
else:
span = getattr(context_wrapper, "_sentry_agent_span", None)
if span:
update_invoke_agent_span(
span=span, usage=context_wrapper.usage, agent=agent
)
span.__exit__(*exc_info)
delattr(context_wrapper, "_sentry_agent_span")
reraise(*exc_info)

return result


async def _run_single_turn_streamed(
original_run_single_turn_streamed: "Callable[..., Awaitable[SingleStepResult]]",
use_run_hooks: "bool",
*args: "Any",
**kwargs: "Any",
) -> "SingleStepResult":
Expand Down Expand Up @@ -181,42 +200,59 @@
args[1] if len(args) > 1 else kwargs.get("bindings", kwargs.get("agent"))
)
agent = getattr(agent_or_bindings, "public_agent", agent_or_bindings)
context_wrapper = args[3] if len(args) > 3 else kwargs.get("context_wrapper")
should_run_agent_start_hooks = bool(
args[5] if len(args) > 5 else kwargs.get("should_run_agent_start_hooks", False)

context_wrapper: "agents.RunContextWrapper[Any]" = (
args[3] if len(args) > 3 else kwargs.get("context_wrapper")
)
if not use_run_hooks:
should_run_agent_start_hooks = bool(
args[5]
if len(args) > 5
else kwargs.get("should_run_agent_start_hooks", False)
)

span_kwargs: "dict[str, Any]" = {}
if streamed_result and hasattr(streamed_result, "input"):
span_kwargs["original_input"] = streamed_result.input
span_kwargs: "dict[str, Any]" = {}
if streamed_result and hasattr(streamed_result, "input"):
span_kwargs["original_input"] = streamed_result.input

span = _maybe_start_agent_span(
context_wrapper,
agent,
should_run_agent_start_hooks,
span_kwargs,
is_streaming=True,
)
span = _maybe_start_agent_span(
context_wrapper,
agent,
should_run_agent_start_hooks,
getattr(streamed_result, "input", None),
is_streaming=True,
)

if (
span is None
or (isinstance(span, StreamedSpan) and span.end_timestamp is not None)
or (not isinstance(span, StreamedSpan) and span.timestamp is not None)
):
return await original_run_single_turn_streamed(*args, **kwargs)
if (
span is None
or (isinstance(span, StreamedSpan) and span.end_timestamp is not None)
or (not isinstance(span, StreamedSpan) and span.timestamp is not None)
):
return await original_run_single_turn_streamed(*args, **kwargs)

try:
result = await original_run_single_turn_streamed(*args, **kwargs)
except Exception:
exc_info = sys.exc_info()
with capture_internal_exceptions():
span = getattr(context_wrapper, "_sentry_agent_span", None)
if span:
update_invoke_agent_span(
span=span, context=context_wrapper, agent=agent
)
span.__exit__(*exc_info)
delattr(context_wrapper, "_sentry_agent_span")
if use_run_hooks:
run_hooks = args[2] if len(args) > 2 else kwargs.get("hooks")
if run_hooks is not None:
span = getattr(run_hooks, "_sentry_invoke_agent_span", None)
if span is not None:
update_invoke_agent_span(
span=span, usage=context_wrapper.usage, agent=agent
)
del run_hooks._sentry_invoke_agent_span
span.__exit__(*exc_info)
else:

Check failure on line 248 in sentry_sdk/integrations/openai_agents/patches/agent_run.py

View check run for this annotation

@sentry/warden / warden: code-review

[ZDE-9YD] Invoke agent span leaks when user-provided RunHooks are patched (additional location)

When user-provided hooks are patched, the invoke agent span is stored on an internal `_SentryRunHooks` instance, but exception handlers look for it on the user's hooks object, leaving the span unfished on error.
span = getattr(context_wrapper, "_sentry_agent_span", None)
if span:

Check failure on line 250 in sentry_sdk/integrations/openai_agents/patches/agent_run.py

View check run for this annotation

@sentry/warden / warden: find-bugs

Invoke agent span leaks on exception when user-provided hooks are used

The exception handler retrieves `_sentry_invoke_agent_span` from `run_hooks`, but when the user provides a `RunHooks` instance `_patch_run_hooks` stores the span on a separate internal `_SentryRunHooks` object, so the span is never finished and leaks on every exception.
Comment on lines +242 to +250

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.

Invoke agent span leaks on exception when user-provided hooks are used

The exception handler retrieves _sentry_invoke_agent_span from run_hooks, but when the user provides a RunHooks instance _patch_run_hooks stores the span on a separate internal _SentryRunHooks object, so the span is never finished and leaks on every exception.

Evidence
  • run_hooks is retrieved from args[2] / kwargs.get("hooks"), which is the user-provided RunHooks instance.
  • _patch_run_hooks in runner.py:129 creates a separate _SentryRunHooks() instance (sentry_hooks) whose on_agent_start stores _sentry_invoke_agent_span on that private object.
  • The patched on_agent_start delegates to sentry_hooks.on_agent_start, but the user's hooks object never receives the _sentry_invoke_agent_span attribute.
  • getattr(run_hooks, "_sentry_invoke_agent_span", None) therefore always resolves to None on patched user hooks.
  • Because the span is never updated or exited, it leaks on every exception path when custom hooks are present.
Also found at 2 additional locations
  • sentry_sdk/integrations/openai_agents/patches/agent_run.py:139
  • sentry_sdk/integrations/openai_agents/patches/runner.py:131-131

Identified by Warden · find-bugs · XD7-X2Q

update_invoke_agent_span(
span=span, usage=context_wrapper.usage, agent=agent
)
span.__exit__(*exc_info)
delattr(context_wrapper, "_sentry_agent_span")
_close_streaming_workflow_span(agent)
reraise(*exc_info)

Expand Down Expand Up @@ -266,15 +302,15 @@
span = getattr(context_wrapper, "_sentry_agent_span", None)
if span:
update_invoke_agent_span(
span=span, context=context_wrapper, agent=agent
span=span, usage=context_wrapper.usage, agent=agent
)
span.__exit__(*exc_info)
delattr(context_wrapper, "_sentry_agent_span")
reraise(*exc_info)

span = getattr(context_wrapper, "_sentry_agent_span", None)
if span:
update_invoke_agent_span(span=span, context=context_wrapper, agent=agent)
update_invoke_agent_span(span=span, usage=context_wrapper.usage, agent=agent)
span.__exit__(None, None, None)
delattr(context_wrapper, "_sentry_agent_span")

Expand Down Expand Up @@ -315,7 +351,10 @@
span = getattr(context_wrapper, "_sentry_agent_span", None)
if span:
update_invoke_agent_span(
span=span, context=context_wrapper, agent=agent, output=final_output
span=span,
usage=context_wrapper.usage,
agent=agent,
output=final_output,
)
span.__exit__(*exc_info)
delattr(context_wrapper, "_sentry_agent_span")
Expand All @@ -324,7 +363,7 @@
span = getattr(context_wrapper, "_sentry_agent_span", None)
if span:
update_invoke_agent_span(
span=span, context=context_wrapper, agent=agent, output=final_output
span=span, usage=context_wrapper.usage, agent=agent, output=final_output
)
span.__exit__(None, None, None)
delattr(context_wrapper, "_sentry_agent_span")
Expand Down
Loading
Loading