Skip to content

Commit 55d9a21

Browse files
sentrivanaclaude
andcommitted
fix(tracing): Skip child span creation in streaming path when no current 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>
1 parent 2b9c102 commit 55d9a21

8 files changed

Lines changed: 215 additions & 192 deletions

File tree

sentry_sdk/integrations/openai_agents/spans/agent_workflow.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,12 @@ def agent_workflow_span(
1818
# Create a transaction or a span if an transaction is already active
1919
span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options)
2020
if span_streaming:
21-
span = sentry_sdk.traces.start_span(
22-
name=f"{agent.name} workflow", attributes={"sentry.origin": SPAN_ORIGIN}
23-
)
21+
span = None
22+
if sentry_sdk.traces.get_current_span() is not None:
23+
span = sentry_sdk.traces.start_span(
24+
name=f"{agent.name} workflow",
25+
attributes={"sentry.origin": SPAN_ORIGIN},
26+
)
2427

2528
return span
2629

sentry_sdk/integrations/openai_agents/spans/ai_client.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,16 @@ def ai_client_span(
3232

3333
span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options)
3434
if span_streaming:
35-
span = sentry_sdk.traces.start_span(
36-
name=f"chat {model_name}",
37-
attributes={
38-
"sentry.op": OP.GEN_AI_CHAT,
39-
"sentry.origin": SPAN_ORIGIN,
40-
SPANDATA.GEN_AI_OPERATION_NAME: "chat",
41-
},
42-
)
35+
span = None
36+
if sentry_sdk.traces.get_current_span() is not None:
37+
span = sentry_sdk.traces.start_span(
38+
name=f"chat {model_name}",
39+
attributes={
40+
"sentry.op": OP.GEN_AI_CHAT,
41+
"sentry.origin": SPAN_ORIGIN,
42+
SPANDATA.GEN_AI_OPERATION_NAME: "chat",
43+
},
44+
)
4345
else:
4446
span = sentry_sdk.start_span(
4547
op=OP.GEN_AI_CHAT,
@@ -49,8 +51,9 @@ def ai_client_span(
4951
# TODO-anton: remove hardcoded stuff and replace something that also works for embedding and so on
5052
span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat")
5153

52-
_set_agent_data(span, agent)
53-
_set_input_data(span, get_response_kwargs)
54+
if span is not None:
55+
_set_agent_data(span, agent)
56+
_set_input_data(span, get_response_kwargs)
5457

5558
return span
5659

sentry_sdk/integrations/openai_agents/spans/execute_tool.py

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,20 @@ def execute_tool_span(
2020
) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]":
2121
span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options)
2222
if span_streaming:
23-
span = sentry_sdk.traces.start_span(
24-
name=f"execute_tool {tool.name}",
25-
attributes={
26-
"sentry.op": OP.GEN_AI_EXECUTE_TOOL,
27-
"sentry.origin": SPAN_ORIGIN,
28-
SPANDATA.GEN_AI_OPERATION_NAME: "execute_tool",
29-
SPANDATA.GEN_AI_TOOL_NAME: tool.name,
30-
SPANDATA.GEN_AI_TOOL_DESCRIPTION: tool.description,
31-
},
32-
)
33-
34-
set_on_span = span.set_attribute
23+
span = None
24+
if sentry_sdk.traces.get_current_span() is not None:
25+
span = sentry_sdk.traces.start_span(
26+
name=f"execute_tool {tool.name}",
27+
attributes={
28+
"sentry.op": OP.GEN_AI_EXECUTE_TOOL,
29+
"sentry.origin": SPAN_ORIGIN,
30+
SPANDATA.GEN_AI_OPERATION_NAME: "execute_tool",
31+
SPANDATA.GEN_AI_TOOL_NAME: tool.name,
32+
SPANDATA.GEN_AI_TOOL_DESCRIPTION: tool.description,
33+
},
34+
)
35+
36+
set_on_span = span.set_attribute if span is not None else None
3537
else:
3638
span = sentry_sdk.start_span(
3739
op=OP.GEN_AI_EXECUTE_TOOL,
@@ -46,7 +48,7 @@ def execute_tool_span(
4648

4749
set_on_span = span.set_data
4850

49-
if should_send_default_pii():
51+
if span is not None and should_send_default_pii():
5052
input = args[1]
5153
set_on_span(SPANDATA.GEN_AI_TOOL_INPUT, input)
5254

sentry_sdk/integrations/openai_agents/spans/handoff.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ def handoff_span(
1515
) -> None:
1616
span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options)
1717
if span_streaming:
18+
if sentry_sdk.traces.get_current_span() is None:
19+
return
20+
1821
with sentry_sdk.traces.start_span(
1922
name=f"handoff from {from_agent.name} to {to_agent_name}",
2023
attributes={

sentry_sdk/integrations/openai_agents/spans/invoke_agent.py

Lines changed: 55 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,16 @@ def invoke_agent_span(
3030
) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]":
3131
span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options)
3232
if span_streaming:
33-
span = sentry_sdk.traces.start_span(
34-
name=f"invoke_agent {agent.name}",
35-
attributes={
36-
"sentry.op": OP.GEN_AI_INVOKE_AGENT,
37-
"sentry.origin": SPAN_ORIGIN,
38-
SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent",
39-
},
40-
)
33+
span = None
34+
if sentry_sdk.traces.get_current_span() is not None:
35+
span = sentry_sdk.traces.start_span(
36+
name=f"invoke_agent {agent.name}",
37+
attributes={
38+
"sentry.op": OP.GEN_AI_INVOKE_AGENT,
39+
"sentry.origin": SPAN_ORIGIN,
40+
SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent",
41+
},
42+
)
4143
else:
4244
start_span_function = get_start_span_function()
4345
span = start_span_function(
@@ -49,53 +51,54 @@ def invoke_agent_span(
4951

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

52-
if should_send_default_pii():
53-
messages = []
54-
if agent.instructions:
55-
message = (
56-
agent.instructions
57-
if isinstance(agent.instructions, str)
58-
else safe_serialize(agent.instructions)
59-
)
60-
messages.append(
61-
{
62-
"content": [{"text": message, "type": "text"}],
63-
"role": "system",
64-
}
65-
)
66-
67-
original_input = kwargs.get("original_input")
68-
if original_input is not None:
69-
message = (
70-
original_input
71-
if isinstance(original_input, str)
72-
else safe_serialize(original_input)
73-
)
74-
messages.append(
75-
{
76-
"content": [{"text": message, "type": "text"}],
77-
"role": "user",
78-
}
79-
)
54+
if span is not None:
55+
if should_send_default_pii():
56+
messages = []
57+
if agent.instructions:
58+
message = (
59+
agent.instructions
60+
if isinstance(agent.instructions, str)
61+
else safe_serialize(agent.instructions)
62+
)
63+
messages.append(
64+
{
65+
"content": [{"text": message, "type": "text"}],
66+
"role": "system",
67+
}
68+
)
8069

81-
if len(messages) > 0:
82-
normalized_messages = normalize_message_roles(messages)
83-
client = sentry_sdk.get_client()
84-
scope = sentry_sdk.get_current_scope()
85-
messages_data = (
86-
truncate_and_annotate_messages(normalized_messages, span, scope)
87-
if should_truncate_gen_ai_input(client.options)
88-
else normalized_messages
89-
)
90-
if messages_data is not None:
91-
set_data_normalized(
92-
span,
93-
SPANDATA.GEN_AI_REQUEST_MESSAGES,
94-
messages_data,
95-
unpack=False,
70+
original_input = kwargs.get("original_input")
71+
if original_input is not None:
72+
message = (
73+
original_input
74+
if isinstance(original_input, str)
75+
else safe_serialize(original_input)
76+
)
77+
messages.append(
78+
{
79+
"content": [{"text": message, "type": "text"}],
80+
"role": "user",
81+
}
9682
)
9783

98-
_set_agent_data(span, agent)
84+
if len(messages) > 0:
85+
normalized_messages = normalize_message_roles(messages)
86+
client = sentry_sdk.get_client()
87+
scope = sentry_sdk.get_current_scope()
88+
messages_data = (
89+
truncate_and_annotate_messages(normalized_messages, span, scope)
90+
if should_truncate_gen_ai_input(client.options)
91+
else normalized_messages
92+
)
93+
if messages_data is not None:
94+
set_data_normalized(
95+
span,
96+
SPANDATA.GEN_AI_REQUEST_MESSAGES,
97+
messages_data,
98+
unpack=False,
99+
)
100+
101+
_set_agent_data(span, agent)
99102

100103
return span
101104

sentry_sdk/integrations/pydantic_ai/spans/ai_client.py

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -282,15 +282,17 @@ def ai_client_span(
282282

283283
span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options)
284284
if span_streaming:
285-
span = sentry_sdk.traces.start_span(
286-
name=f"chat {model_name}",
287-
attributes={
288-
"sentry.op": OP.GEN_AI_CHAT,
289-
"sentry.origin": SPAN_ORIGIN,
290-
SPANDATA.GEN_AI_OPERATION_NAME: "chat",
291-
SPANDATA.GEN_AI_RESPONSE_STREAMING: get_is_streaming(),
292-
},
293-
)
285+
span = None
286+
if sentry_sdk.traces.get_current_span() is not None:
287+
span = sentry_sdk.traces.start_span(
288+
name=f"chat {model_name}",
289+
attributes={
290+
"sentry.op": OP.GEN_AI_CHAT,
291+
"sentry.origin": SPAN_ORIGIN,
292+
SPANDATA.GEN_AI_OPERATION_NAME: "chat",
293+
SPANDATA.GEN_AI_RESPONSE_STREAMING: get_is_streaming(),
294+
},
295+
)
294296
else:
295297
span = sentry_sdk.start_span(
296298
op=OP.GEN_AI_CHAT,
@@ -302,16 +304,17 @@ def ai_client_span(
302304
# Set streaming flag from contextvar
303305
span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, get_is_streaming())
304306

305-
_set_agent_data(span, agent)
306-
_set_model_data(span, model, model_settings)
307+
if span is not None:
308+
_set_agent_data(span, agent)
309+
_set_model_data(span, model, model_settings)
307310

308-
# Add available tools if agent is available
309-
agent_obj = agent or get_current_agent()
310-
_set_available_tools(span, agent_obj)
311+
# Add available tools if agent is available
312+
agent_obj = agent or get_current_agent()
313+
_set_available_tools(span, agent_obj)
311314

312-
# Set input messages (full conversation history)
313-
if messages:
314-
_set_input_messages(span, messages)
315+
# Set input messages (full conversation history)
316+
if messages:
317+
_set_input_messages(span, messages)
315318

316319
return span
317320

sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,19 @@ def execute_tool_span(
3131
"""
3232
span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options)
3333
if span_streaming:
34-
span = sentry_sdk.traces.start_span(
35-
name=f"execute_tool {tool_name}",
36-
attributes={
37-
"sentry.op": OP.GEN_AI_EXECUTE_TOOL,
38-
"sentry.origin": SPAN_ORIGIN,
39-
SPANDATA.GEN_AI_OPERATION_NAME: "execute_tool",
40-
SPANDATA.GEN_AI_TOOL_NAME: tool_name,
41-
},
42-
)
43-
44-
set_on_span = span.set_attribute
34+
span = None
35+
if sentry_sdk.traces.get_current_span() is not None:
36+
span = sentry_sdk.traces.start_span(
37+
name=f"execute_tool {tool_name}",
38+
attributes={
39+
"sentry.op": OP.GEN_AI_EXECUTE_TOOL,
40+
"sentry.origin": SPAN_ORIGIN,
41+
SPANDATA.GEN_AI_OPERATION_NAME: "execute_tool",
42+
SPANDATA.GEN_AI_TOOL_NAME: tool_name,
43+
},
44+
)
45+
46+
set_on_span = span.set_attribute if span is not None else None
4547
else:
4648
span = sentry_sdk.start_span(
4749
op=OP.GEN_AI_EXECUTE_TOOL,
@@ -54,16 +56,17 @@ def execute_tool_span(
5456

5557
set_on_span = span.set_data
5658

57-
if tool_definition is not None and hasattr(tool_definition, "description"):
58-
set_on_span(
59-
SPANDATA.GEN_AI_TOOL_DESCRIPTION,
60-
tool_definition.description,
61-
)
59+
if span is not None:
60+
if tool_definition is not None and hasattr(tool_definition, "description"):
61+
set_on_span(
62+
SPANDATA.GEN_AI_TOOL_DESCRIPTION,
63+
tool_definition.description,
64+
)
6265

63-
_set_agent_data(span, agent)
66+
_set_agent_data(span, agent)
6467

65-
if _should_send_prompts() and tool_args is not None:
66-
set_on_span(SPANDATA.GEN_AI_TOOL_INPUT, safe_serialize(tool_args))
68+
if _should_send_prompts() and tool_args is not None:
69+
set_on_span(SPANDATA.GEN_AI_TOOL_INPUT, safe_serialize(tool_args))
6770

6871
return span
6972

0 commit comments

Comments
 (0)