Skip to content
Open
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
24 changes: 19 additions & 5 deletions sentry_sdk/integrations/openai_agents/patches/agent_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from sentry_sdk.consts import SPANDATA
from sentry_sdk.integrations import DidNotEnable
from sentry_sdk.traces import StreamedSpan
from sentry_sdk.utils import capture_internal_exceptions, reraise

from ..spans import (
Expand All @@ -12,7 +13,7 @@
)

if TYPE_CHECKING:
from typing import Any, Awaitable, Callable, Optional
from typing import Any, Awaitable, Callable, Optional, Union

from agents.run_internal.run_steps import SingleStepResult

Expand Down Expand Up @@ -50,7 +51,7 @@ def _maybe_start_agent_span(
should_run_agent_start_hooks: bool,
span_kwargs: "dict[str, Any]",
is_streaming: bool = False,
) -> "Optional[Span]":
) -> "Optional[Union[Span, StreamedSpan]]":
"""
Start an agent invocation span if conditions are met.
Handles ending any existing span for a different agent.
Expand Down Expand Up @@ -78,7 +79,12 @@ def _maybe_start_agent_span(
context_wrapper._sentry_agent_span = span
agent._sentry_agent_span = span

if is_streaming:
if not is_streaming:
return span

if isinstance(span, StreamedSpan):
span.set_attribute(SPANDATA.GEN_AI_RESPONSE_STREAMING, True)
else:
span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True)

return span
Expand Down Expand Up @@ -108,7 +114,11 @@ async def _run_single_turn(
context_wrapper, agent, should_run_agent_start_hooks, kwargs
)

if span is None or span.timestamp is not None:
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:
Expand Down Expand Up @@ -188,7 +198,11 @@ async def _run_single_turn_streamed(
is_streaming=True,
)

if span is None or span.timestamp is not None:
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:
Expand Down
21 changes: 14 additions & 7 deletions sentry_sdk/integrations/openai_agents/patches/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import sentry_sdk
from sentry_sdk.consts import SPANDATA
from sentry_sdk.integrations import DidNotEnable
from sentry_sdk.traces import StreamedSpan
from sentry_sdk.tracing import BAGGAGE_HEADER_NAME
from sentry_sdk.tracing_utils import (
add_sentry_baggage_to_headers,
Expand All @@ -16,7 +17,7 @@
from ..spans import ai_client_span, update_ai_client_span

if TYPE_CHECKING:
from typing import Any, Callable, Optional
from typing import Any, Callable, Optional, Union

from sentry_sdk.tracing import Span

Expand All @@ -34,11 +35,14 @@ def _set_response_model_on_agent_span(
if response_model:
agent_span = getattr(agent, "_sentry_agent_span", None)
if agent_span:
agent_span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model)
if isinstance(agent_span, StreamedSpan):
agent_span.set_attribute(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model)
else:
agent_span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model)


def _inject_trace_propagation_headers(
hosted_tool: "HostedMCPTool", span: "Span"
hosted_tool: "HostedMCPTool", span: "Union[Span, StreamedSpan]"
) -> None:
headers = hosted_tool.tool_config.get("headers")
if headers is None:
Expand Down Expand Up @@ -151,7 +155,12 @@ async def wrapped_stream_response(*args: "Any", **kwargs: "Any") -> "Any":
for hosted_tool in hosted_tools:
_inject_trace_propagation_headers(hosted_tool, span=span)

span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True)
set_on_span = (
span.set_attribute
if isinstance(span, StreamedSpan)
else span.set_data
)
set_on_span(SPANDATA.GEN_AI_RESPONSE_STREAMING, True)

streaming_response = None
ttft_recorded = False
Expand All @@ -162,9 +171,7 @@ async def wrapped_stream_response(*args: "Any", **kwargs: "Any") -> "Any":
# Detect first content token (text delta event)
if not ttft_recorded and hasattr(event, "delta"):
ttft = time.perf_counter() - start_time
span.set_data(
SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft
)
set_on_span(SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft)
ttft_recorded = True

# Capture the full response from ResponseCompletedEvent
Expand Down
32 changes: 25 additions & 7 deletions sentry_sdk/integrations/openai_agents/patches/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import sentry_sdk
from sentry_sdk.consts import SPANDATA
from sentry_sdk.integrations import DidNotEnable
from sentry_sdk.traces import StreamedSpan
from sentry_sdk.utils import capture_internal_exceptions, reraise

from ..spans import agent_workflow_span, update_invoke_agent_span
Expand Down Expand Up @@ -43,9 +44,15 @@ async def wrapper(*args: "Any", **kwargs: "Any") -> "Any":
conversation_id = kwargs.get("conversation_id")
if conversation_id:
agent._sentry_conversation_id = conversation_id
workflow_span.set_data(
SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id
)

if isinstance(workflow_span, StreamedSpan):
workflow_span.set_attribute(
SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id
)
else:
workflow_span.set_data(
SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id
)
Comment thread
cursor[bot] marked this conversation as resolved.

args = (agent, *args[1:])
try:
Expand All @@ -61,9 +68,15 @@ async def wrapper(*args: "Any", **kwargs: "Any") -> "Any":
context_wrapper, "_sentry_agent_span", None
)

if (
invoke_agent_span is not None
and invoke_agent_span.timestamp is None
if invoke_agent_span is not None and (
(
isinstance(invoke_agent_span, StreamedSpan)
and invoke_agent_span.end_timestamp is None
)
or (
not isinstance(invoke_agent_span, StreamedSpan)
and invoke_agent_span.timestamp is None
)
):
update_invoke_agent_span(
span=invoke_agent_span,
Expand Down Expand Up @@ -135,7 +148,12 @@ def wrapper(*args: "Any", **kwargs: "Any") -> "Any":

# Set conversation ID on workflow span early so it's captured even on errors
if conversation_id:
workflow_span.set_data(SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id)
if isinstance(workflow_span, StreamedSpan):
workflow_span.set_attribute(
SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id
)
else:
workflow_span.set_data(SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id)

# Store span on agent for cleanup
agent._sentry_workflow_span = workflow_span
Expand Down
15 changes: 14 additions & 1 deletion sentry_sdk/integrations/openai_agents/spans/agent_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,28 @@

import sentry_sdk
from sentry_sdk.ai.utils import get_start_span_function
from sentry_sdk.tracing_utils import has_span_streaming_enabled

from ..consts import SPAN_ORIGIN

if TYPE_CHECKING:
from typing import Union

import agents


def agent_workflow_span(agent: "agents.Agent") -> "sentry_sdk.tracing.Span":
def agent_workflow_span(
agent: "agents.Agent",
) -> "Union[sentry_sdk.tracing.Span, sentry_sdk.traces.StreamedSpan]":
# 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}
)

return span

span = get_start_span_function()(
name=f"{agent.name} workflow",
origin=SPAN_ORIGIN,
Expand Down
43 changes: 30 additions & 13 deletions sentry_sdk/integrations/openai_agents/spans/ai_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import sentry_sdk
from sentry_sdk.consts import OP, SPANDATA
from sentry_sdk.traces import StreamedSpan
from sentry_sdk.tracing_utils import has_span_streaming_enabled

from ..consts import SPAN_ORIGIN
from ..utils import (
Expand All @@ -12,14 +14,14 @@
)

if TYPE_CHECKING:
from typing import Any, Optional
from typing import Any, Optional, Union

from agents import Agent


def ai_client_span(
agent: "Agent", get_response_kwargs: "dict[str, Any]"
) -> "sentry_sdk.tracing.Span":
) -> "Union[sentry_sdk.tracing.Span, StreamedSpan]":
# TODO-anton: implement other types of operations. Now "chat" is hardcoded.
# Get model name from agent.model or fall back to request model (for when agent.model is None/default)
model_name = None
Expand All @@ -28,13 +30,24 @@ def ai_client_span(
elif hasattr(agent, "_sentry_request_model"):
model_name = agent._sentry_request_model

span = sentry_sdk.start_span(
op=OP.GEN_AI_CHAT,
name=f"chat {model_name}",
origin=SPAN_ORIGIN,
)
# 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")
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}",
Comment thread
alexander-alderman-webb marked this conversation as resolved.
attributes={
"sentry.op": OP.GEN_AI_CHAT,
"sentry.origin": SPAN_ORIGIN,
SPANDATA.GEN_AI_OPERATION_NAME: "chat",
},
)
else:
span = sentry_sdk.start_span(
op=OP.GEN_AI_CHAT,
name=f"chat {model_name}",
origin=SPAN_ORIGIN,
)
# TODO-anton: remove hardcoded stuff and replace something that also works for embedding and so on
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how useful this comment is anymore - any chance we can remove it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is still a bug 😬.
Created #6417 to track.

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

_set_agent_data(span, agent)
_set_input_data(span, get_response_kwargs)
Expand All @@ -43,7 +56,7 @@ def ai_client_span(


def update_ai_client_span(
span: "sentry_sdk.tracing.Span",
span: "Union[sentry_sdk.tracing.Span, StreamedSpan]",
response: "Any",
response_model: "Optional[str]" = None,
agent: "Optional[Agent]" = None,
Expand All @@ -55,13 +68,17 @@ def update_ai_client_span(
if hasattr(response, "output") and response.output:
_set_output_data(span, response)

set_on_span = (
span.set_attribute if isinstance(span, StreamedSpan) else span.set_data
)

if response_model is not None:
span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model)
set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model)
elif hasattr(response, "model") and response.model:
span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, str(response.model))
set_on_span(SPANDATA.GEN_AI_RESPONSE_MODEL, str(response.model))

# Set conversation ID from agent if available
if agent:
conv_id = getattr(agent, "_sentry_conversation_id", None)
if conv_id:
span.set_data(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id)
set_on_span(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id)
56 changes: 41 additions & 15 deletions sentry_sdk/integrations/openai_agents/spans/execute_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,58 @@
import sentry_sdk
from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS
from sentry_sdk.scope import should_send_default_pii
from sentry_sdk.traces import SpanStatus, StreamedSpan
from sentry_sdk.tracing_utils import has_span_streaming_enabled

from ..consts import SPAN_ORIGIN
from ..utils import _set_agent_data

if TYPE_CHECKING:
from typing import Any
from typing import Any, Union

import agents


def execute_tool_span(
tool: "agents.Tool", *args: "Any", **kwargs: "Any"
) -> "sentry_sdk.tracing.Span":
span = sentry_sdk.start_span(
op=OP.GEN_AI_EXECUTE_TOOL,
name=f"execute_tool {tool.name}",
origin=SPAN_ORIGIN,
)
) -> "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"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
else:
span = sentry_sdk.start_span(
op=OP.GEN_AI_EXECUTE_TOOL,
name=f"execute_tool {tool.name}",
origin=SPAN_ORIGIN,
)

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

span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "execute_tool")
span.set_data(SPANDATA.GEN_AI_TOOL_NAME, tool.name)
span.set_data(SPANDATA.GEN_AI_TOOL_DESCRIPTION, tool.description)

span.set_data(SPANDATA.GEN_AI_TOOL_NAME, tool.name)
span.set_data(SPANDATA.GEN_AI_TOOL_DESCRIPTION, tool.description)
set_on_span = span.set_data

if should_send_default_pii():
input = args[1]
span.set_data(SPANDATA.GEN_AI_TOOL_INPUT, input)
set_on_span(SPANDATA.GEN_AI_TOOL_INPUT, input)

return span


def update_execute_tool_span(
span: "sentry_sdk.tracing.Span",
span: "Union[sentry_sdk.tracing.Span, StreamedSpan]",
agent: "agents.Agent",
tool: "agents.Tool",
result: "Any",
Expand All @@ -45,12 +64,19 @@ def update_execute_tool_span(
if isinstance(result, str) and result.startswith(
"An error occurred while running the tool"
):
span.set_status(SPANSTATUS.INTERNAL_ERROR)
if isinstance(span, StreamedSpan):
span.status = SpanStatus.ERROR
else:
span.set_status(SPANSTATUS.INTERNAL_ERROR)

set_on_span = (
span.set_attribute if isinstance(span, StreamedSpan) else span.set_data
)

if should_send_default_pii():
span.set_data(SPANDATA.GEN_AI_TOOL_OUTPUT, result)
set_on_span(SPANDATA.GEN_AI_TOOL_OUTPUT, result)

# Add conversation ID from agent
conv_id = getattr(agent, "_sentry_conversation_id", None)
if conv_id:
span.set_data(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id)
set_on_span(SPANDATA.GEN_AI_CONVERSATION_ID, conv_id)
Loading
Loading