Skip to content

Commit 67ff769

Browse files
fix(openai-agents): Stop setting transaction status when child span fails
1 parent 3d9c131 commit 67ff769

5 files changed

Lines changed: 155 additions & 80 deletions

File tree

sentry_sdk/integrations/openai_agents/patches/agent_run.py

Lines changed: 73 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33

44
from sentry_sdk.consts import SPANDATA
55
from sentry_sdk.integrations import DidNotEnable
6-
from sentry_sdk.tracing_utils import set_span_errored
76
from sentry_sdk.utils import capture_internal_exceptions, reraise
87

98
from ..spans import (
10-
end_invoke_agent_span,
9+
_AgentInvocationSpanContext,
1110
handoff_span,
1211
invoke_agent_span,
12+
update_invoke_agent_span,
1313
)
1414

1515
if TYPE_CHECKING:
@@ -65,7 +65,9 @@ def _maybe_start_agent_span(
6565
if _has_active_agent_span(context_wrapper):
6666
current_agent = _get_current_agent(context_wrapper)
6767
if current_agent and current_agent != agent:
68-
end_invoke_agent_span(context_wrapper, current_agent)
68+
_AgentInvocationSpanContext(
69+
context_wrapper=context_wrapper, agent=current_agent
70+
).__exit__(None, None, None)
6971

7072
# Store the agent on the context wrapper so we can access it later
7173
context_wrapper._sentry_current_agent = agent
@@ -103,15 +105,23 @@ async def _run_single_turn(
103105
context_wrapper, agent, should_run_agent_start_hooks, kwargs
104106
)
105107

108+
if span is None or span.timestamp is not None:
109+
return await original_run_single_turn(*args, **kwargs)
110+
106111
try:
107-
result = await original_run_single_turn(*args, **kwargs)
112+
return await original_run_single_turn(*args, **kwargs)
108113
except Exception:
109-
if span is not None and span.timestamp is None:
110-
set_span_errored(span)
111-
end_invoke_agent_span(context_wrapper, agent)
112-
reraise(*sys.exc_info())
114+
exc_info = sys.exc_info()
115+
with capture_internal_exceptions():
116+
span = getattr(context_wrapper, "_sentry_agent_span", None)
117+
if not span:
118+
return
113119

114-
return result
120+
update_invoke_agent_span(span=span, context=context_wrapper, agent=agent)
121+
122+
span.__exit__(*exc_info)
123+
delattr(context_wrapper, "_sentry_agent_span")
124+
reraise(*exc_info)
115125

116126

117127
async def _run_single_turn_streamed(
@@ -174,18 +184,20 @@ async def _run_single_turn_streamed(
174184
is_streaming=True,
175185
)
176186

177-
try:
178-
result = await original_run_single_turn_streamed(*args, **kwargs)
179-
except Exception:
180-
exc_info = sys.exc_info()
181-
with capture_internal_exceptions():
182-
if span is not None and span.timestamp is None:
183-
set_span_errored(span)
184-
end_invoke_agent_span(context_wrapper, agent)
185-
_close_streaming_workflow_span(agent)
186-
reraise(*exc_info)
187+
if span is None or span.timestamp is not None:
188+
return await original_run_single_turn_streamed(*args, **kwargs)
187189

188-
return result
190+
with _AgentInvocationSpanContext(
191+
context_wrapper=context_wrapper,
192+
agent=agent,
193+
):
194+
try:
195+
return await original_run_single_turn_streamed(*args, **kwargs)
196+
except Exception:
197+
exc_info = sys.exc_info()
198+
with capture_internal_exceptions():
199+
_close_streaming_workflow_span(agent)
200+
reraise(*exc_info)
189201

190202

191203
async def _execute_handoffs(
@@ -211,18 +223,30 @@ async def _execute_handoffs(
211223
handoff_agent_name = first_handoff.handoff.agent_name
212224
handoff_span(context_wrapper, agent, handoff_agent_name)
213225

214-
# Call original method with all parameters
215-
try:
216-
result = await original_execute_handoffs(*args, **kwargs)
217-
except Exception:
218-
exc_info = sys.exc_info()
219-
with capture_internal_exceptions():
220-
_close_streaming_workflow_span(agent)
221-
reraise(*exc_info)
222-
finally:
223-
# End span for current agent after handoff processing is complete
224-
if agent and context_wrapper and _has_active_agent_span(context_wrapper):
225-
end_invoke_agent_span(context_wrapper, agent)
226+
if not agent or not context_wrapper or not _has_active_agent_span(context_wrapper):
227+
# Call original method with all parameters
228+
try:
229+
result = await original_execute_handoffs(*args, **kwargs)
230+
except Exception:
231+
exc_info = sys.exc_info()
232+
with capture_internal_exceptions():
233+
_close_streaming_workflow_span(agent)
234+
reraise(*exc_info)
235+
236+
return result
237+
238+
with _AgentInvocationSpanContext(
239+
context_wrapper=context_wrapper,
240+
agent=agent,
241+
):
242+
# Call original method with all parameters
243+
try:
244+
result = await original_execute_handoffs(*args, **kwargs)
245+
except Exception:
246+
exc_info = sys.exc_info()
247+
with capture_internal_exceptions():
248+
_close_streaming_workflow_span(agent)
249+
reraise(*exc_info)
226250

227251
return result
228252

@@ -243,13 +267,22 @@ async def _execute_final_output(
243267
context_wrapper = kwargs.get("context_wrapper")
244268
final_output = kwargs.get("final_output")
245269

246-
try:
247-
result = await original_execute_final_output(*args, **kwargs)
248-
finally:
249-
with capture_internal_exceptions():
250-
if agent and context_wrapper and _has_active_agent_span(context_wrapper):
251-
end_invoke_agent_span(context_wrapper, agent, final_output)
252-
# For streaming, close the workflow span (non-streaming uses context manager in _create_run_wrapper)
253-
_close_streaming_workflow_span(agent)
270+
if not agent or not context_wrapper or not _has_active_agent_span(context_wrapper):
271+
try:
272+
result = await original_execute_final_output(*args, **kwargs)
273+
finally:
274+
with capture_internal_exceptions():
275+
# For streaming, close the workflow span (non-streaming uses context manager in _create_run_wrapper)
276+
_close_streaming_workflow_span(agent)
277+
278+
with _AgentInvocationSpanContext(
279+
context_wrapper=context_wrapper, agent=agent, output=final_output
280+
):
281+
try:
282+
result = await original_execute_final_output(*args, **kwargs)
283+
finally:
284+
with capture_internal_exceptions():
285+
# For streaming, close the workflow span (non-streaming uses context manager in _create_run_wrapper)
286+
_close_streaming_workflow_span(agent)
254287

255288
return result

sentry_sdk/integrations/openai_agents/patches/runner.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@
44
import sentry_sdk
55
from sentry_sdk.consts import SPANDATA
66
from sentry_sdk.integrations import DidNotEnable
7-
from sentry_sdk.tracing_utils import set_span_errored
87
from sentry_sdk.utils import capture_internal_exceptions, reraise
98

10-
from ..spans import agent_workflow_span, end_invoke_agent_span
9+
from ..spans import agent_workflow_span, update_invoke_agent_span
1110
from ..utils import _capture_exception
1211

1312
try:
@@ -66,8 +65,14 @@ async def wrapper(*args: "Any", **kwargs: "Any") -> "Any":
6665
invoke_agent_span is not None
6766
and invoke_agent_span.timestamp is None
6867
):
69-
set_span_errored(invoke_agent_span)
70-
end_invoke_agent_span(context_wrapper, agent)
68+
update_invoke_agent_span(
69+
span=invoke_agent_span,
70+
context=context_wrapper,
71+
agent=agent,
72+
)
73+
74+
invoke_agent_span.__exit__(*exc_info)
75+
delattr(context_wrapper, "_sentry_agent_span")
7176
reraise(*exc_info)
7277
except Exception as exc:
7378
exc_info = sys.exc_info()
@@ -78,7 +83,20 @@ async def wrapper(*args: "Any", **kwargs: "Any") -> "Any":
7883
_capture_exception(exc)
7984
reraise(*exc_info)
8085

81-
end_invoke_agent_span(run_result.context_wrapper, agent)
86+
invoke_agent_span = getattr(
87+
run_result.context_wrapper, "_sentry_agent_span", None
88+
)
89+
if not invoke_agent_span:
90+
return run_result
91+
92+
update_invoke_agent_span(
93+
span=invoke_agent_span,
94+
context=run_result.context_wrapper,
95+
agent=agent,
96+
)
97+
98+
invoke_agent_span.__exit__(None, None, None)
99+
delattr(run_result.context_wrapper, "_sentry_agent_span")
82100
return run_result
83101

84102
return wrapper

sentry_sdk/integrations/openai_agents/spans/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from .execute_tool import execute_tool_span, update_execute_tool_span # noqa: F401
44
from .handoff import handoff_span # noqa: F401
55
from .invoke_agent import (
6-
end_invoke_agent_span, # noqa: F401
6+
_AgentInvocationSpanContext, # noqa: F401
77
invoke_agent_span, # noqa: F401
88
update_invoke_agent_span, # noqa: F401
99
)

sentry_sdk/integrations/openai_agents/spans/invoke_agent.py

Lines changed: 58 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
)
1010
from sentry_sdk.consts import OP, SPANDATA
1111
from sentry_sdk.scope import should_send_default_pii
12-
from sentry_sdk.utils import safe_serialize
12+
from sentry_sdk.utils import capture_internal_exceptions, safe_serialize
1313

1414
from ..consts import SPAN_ORIGIN
1515
from ..utils import _set_agent_data, _set_usage_data
@@ -85,37 +85,64 @@ def invoke_agent_span(
8585

8686

8787
def update_invoke_agent_span(
88-
context: "agents.RunContextWrapper", agent: "agents.Agent", output: "Any"
88+
span: "sentry_sdk.tracing.Span",
89+
context: "agents.RunContextWrapper",
90+
agent: "agents.Agent",
91+
output: "Any" = None,
8992
) -> None:
90-
span = getattr(context, "_sentry_agent_span", None)
91-
92-
if span:
93-
# Add aggregated usage data from context_wrapper
94-
if hasattr(context, "usage"):
95-
_set_usage_data(span, context.usage)
93+
# Add aggregated usage data from context_wrapper
94+
if hasattr(context, "usage"):
95+
_set_usage_data(span, context.usage)
9696

97-
if should_send_default_pii():
98-
set_data_normalized(
99-
span, SPANDATA.GEN_AI_RESPONSE_TEXT, output, unpack=False
97+
if should_send_default_pii():
98+
set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output, unpack=False)
99+
100+
# Add conversation ID from agent
101+
conv_id = getattr(agent, "_sentry_conversation_id", None)
102+
if conv_id:
103+
span.set_data(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id)
104+
105+
106+
class _AgentInvocationSpanContext:
107+
"""
108+
Sets accumulated data on the Invoke Agent span and finishes the span on exit.
109+
Is a no-op if the context wrapper has no span set, i.e., when the span has already been finished.
110+
"""
111+
112+
def __init__(
113+
self,
114+
context_wrapper: "agents.RunContextWrapper",
115+
agent: "agents.Agent",
116+
output: "Optional[Any]" = None,
117+
) -> None:
118+
self._context_wrapper = context_wrapper
119+
self._agent = agent
120+
self._output = output
121+
122+
def __enter__(self) -> "_AgentInvocationSpanContext":
123+
return self
124+
125+
def __exit__(
126+
self,
127+
exc_type: "Optional[type[BaseException]]",
128+
exc_val: "Optional[BaseException]",
129+
exc_tb: "Optional[Any]",
130+
) -> None:
131+
with capture_internal_exceptions():
132+
# Clear the stored agent
133+
if hasattr(self._context_wrapper, "_sentry_current_agent"):
134+
delattr(self._context_wrapper, "_sentry_current_agent")
135+
136+
span = getattr(self._context_wrapper, "_sentry_agent_span", None)
137+
if not span:
138+
return
139+
140+
update_invoke_agent_span(
141+
span=span,
142+
context=self._context_wrapper,
143+
agent=self._agent,
144+
output=self._output,
100145
)
101146

102-
# Add conversation ID from agent
103-
conv_id = getattr(agent, "_sentry_conversation_id", None)
104-
if conv_id:
105-
span.set_data(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id)
106-
107-
span.__exit__(None, None, None)
108-
delattr(context, "_sentry_agent_span")
109-
110-
111-
def end_invoke_agent_span(
112-
context_wrapper: "agents.RunContextWrapper",
113-
agent: "agents.Agent",
114-
output: "Optional[Any]" = None,
115-
) -> None:
116-
"""End the agent invocation span"""
117-
# Clear the stored agent
118-
if hasattr(context_wrapper, "_sentry_current_agent"):
119-
delattr(context_wrapper, "_sentry_current_agent")
120-
121-
update_invoke_agent_span(context_wrapper, agent, output)
147+
span.__exit__(exc_type, exc_val, exc_tb)
148+
delattr(self._context_wrapper, "_sentry_agent_span")

sentry_sdk/integrations/openai_agents/utils.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS
1818
from sentry_sdk.integrations import DidNotEnable
1919
from sentry_sdk.scope import should_send_default_pii
20-
from sentry_sdk.tracing_utils import set_span_errored
2120
from sentry_sdk.utils import event_from_exception, safe_serialize
2221

2322
if TYPE_CHECKING:
@@ -35,8 +34,6 @@
3534

3635

3736
def _capture_exception(exc: "Any") -> None:
38-
set_span_errored()
39-
4037
event, hint = event_from_exception(
4138
exc,
4239
client_options=sentry_sdk.get_client().options,

0 commit comments

Comments
 (0)