From 0b7a24dfed2bd01478ddfe5da0f673e16c7d2e8b Mon Sep 17 00:00:00 2001 From: Fabian Schindler Date: Thu, 4 Sep 2025 12:08:06 +0200 Subject: [PATCH] feat(ai): automatically wrap the root span for several AI/Agent frameworks in a transaction This includes: - anthropic: message.create - langchain: 'invoke_agent' spans - openai_agent: 'invoke_agent' spans + agent workflow (which was already like that) --- sentry_sdk/ai/utils.py | 12 +++++++++++- sentry_sdk/integrations/anthropic.py | 4 ++-- sentry_sdk/integrations/langchain.py | 8 +++++--- .../openai_agents/spans/agent_workflow.py | 4 ++-- .../integrations/openai_agents/spans/invoke_agent.py | 4 +++- sentry_sdk/integrations/openai_agents/utils.py | 10 ---------- 6 files changed, 23 insertions(+), 19 deletions(-) diff --git a/sentry_sdk/ai/utils.py b/sentry_sdk/ai/utils.py index cf52cba6e8..bc588dde8a 100644 --- a/sentry_sdk/ai/utils.py +++ b/sentry_sdk/ai/utils.py @@ -1,8 +1,9 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from typing import Any + from typing import Any, Callable +import sentry_sdk from sentry_sdk.tracing import Span from sentry_sdk.utils import logger @@ -34,3 +35,12 @@ def set_data_normalized(span, key, value, unpack=True): span.set_data(key, normalized) else: span.set_data(key, str(normalized)) + + +def get_start_span_function(): + # type: () -> Callable[..., Any] + current_span = sentry_sdk.get_current_span() + transaction_exists = ( + current_span is not None and current_span.containing_transaction == current_span + ) + return sentry_sdk.start_span if transaction_exists else sentry_sdk.start_transaction diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index 05d45ef62f..ff3d9a3388 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -4,7 +4,7 @@ import sentry_sdk from sentry_sdk.ai.monitoring import record_token_usage -from sentry_sdk.ai.utils import set_data_normalized +from sentry_sdk.ai.utils import set_data_normalized, get_start_span_function from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.integrations import _check_minimum_version, DidNotEnable, Integration from sentry_sdk.scope import should_send_default_pii @@ -194,7 +194,7 @@ def _sentry_patched_create_common(f, *args, **kwargs): model = kwargs.get("model", "") - span = sentry_sdk.start_span( + span = get_start_span_function()( op=OP.GEN_AI_CHAT, name=f"chat {model}".strip(), origin=AnthropicIntegration.origin, diff --git a/sentry_sdk/integrations/langchain.py b/sentry_sdk/integrations/langchain.py index 7e04a740ed..1381d0b7b4 100644 --- a/sentry_sdk/integrations/langchain.py +++ b/sentry_sdk/integrations/langchain.py @@ -4,7 +4,7 @@ import sentry_sdk from sentry_sdk.ai.monitoring import set_ai_pipeline_name -from sentry_sdk.ai.utils import set_data_normalized +from sentry_sdk.ai.utils import set_data_normalized, get_start_span_function from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.integrations import DidNotEnable, Integration from sentry_sdk.scope import should_send_default_pii @@ -626,8 +626,9 @@ def new_invoke(self, *args, **kwargs): return f(self, *args, **kwargs) agent_name, tools = _get_request_data(self, args, kwargs) + start_span_function = get_start_span_function() - with sentry_sdk.start_span( + with start_span_function( op=OP.GEN_AI_INVOKE_AGENT, name=f"invoke_agent {agent_name}" if agent_name else "invoke_agent", origin=LangchainIntegration.origin, @@ -684,8 +685,9 @@ def new_stream(self, *args, **kwargs): return f(self, *args, **kwargs) agent_name, tools = _get_request_data(self, args, kwargs) + start_span_function = get_start_span_function() - span = sentry_sdk.start_span( + span = start_span_function( op=OP.GEN_AI_INVOKE_AGENT, name=f"invoke_agent {agent_name}".strip(), origin=LangchainIntegration.origin, diff --git a/sentry_sdk/integrations/openai_agents/spans/agent_workflow.py b/sentry_sdk/integrations/openai_agents/spans/agent_workflow.py index de2f28d41e..ef69b856e3 100644 --- a/sentry_sdk/integrations/openai_agents/spans/agent_workflow.py +++ b/sentry_sdk/integrations/openai_agents/spans/agent_workflow.py @@ -1,7 +1,7 @@ import sentry_sdk +from sentry_sdk.ai.utils import get_start_span_function from ..consts import SPAN_ORIGIN -from ..utils import _get_start_span_function from typing import TYPE_CHECKING @@ -13,7 +13,7 @@ def agent_workflow_span(agent): # type: (agents.Agent) -> sentry_sdk.tracing.Span # Create a transaction or a span if an transaction is already active - span = _get_start_span_function()( + span = get_start_span_function()( name=f"{agent.name} workflow", origin=SPAN_ORIGIN, ) diff --git a/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py b/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py index 549ade1246..6b45f7dd46 100644 --- a/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +++ b/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py @@ -1,5 +1,6 @@ import sentry_sdk from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.ai.utils import get_start_span_function from ..consts import SPAN_ORIGIN from ..utils import _set_agent_data @@ -13,7 +14,8 @@ def invoke_agent_span(context, agent): # type: (agents.RunContextWrapper, agents.Agent) -> sentry_sdk.tracing.Span - span = sentry_sdk.start_span( + start_span_function = get_start_span_function() + span = start_span_function( op=OP.GEN_AI_INVOKE_AGENT, name=f"invoke_agent {agent.name}", origin=SPAN_ORIGIN, diff --git a/sentry_sdk/integrations/openai_agents/utils.py b/sentry_sdk/integrations/openai_agents/utils.py index 1525346726..a54c41af94 100644 --- a/sentry_sdk/integrations/openai_agents/utils.py +++ b/sentry_sdk/integrations/openai_agents/utils.py @@ -8,7 +8,6 @@ if TYPE_CHECKING: from typing import Any - from typing import Callable from agents import Usage try: @@ -28,15 +27,6 @@ def _capture_exception(exc): sentry_sdk.capture_event(event, hint=hint) -def _get_start_span_function(): - # type: () -> Callable[..., Any] - current_span = sentry_sdk.get_current_span() - transaction_exists = ( - current_span is not None and current_span.containing_transaction == current_span - ) - return sentry_sdk.start_span if transaction_exists else sentry_sdk.start_transaction - - def _set_agent_data(span, agent): # type: (sentry_sdk.tracing.Span, agents.Agent) -> None span.set_data(