From d4c4cd4513c54e725f5917be32080cdc820dabf5 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Wed, 25 Mar 2026 14:03:18 +0100 Subject: [PATCH 1/6] fix(langchain): Set agent name as gen_ai.agent.name --- sentry_sdk/integrations/langchain.py | 154 +++++++----------- .../integrations/langchain/test_langchain.py | 6 + 2 files changed, 66 insertions(+), 94 deletions(-) diff --git a/sentry_sdk/integrations/langchain.py b/sentry_sdk/integrations/langchain.py index f4ec75310d..11b44b7096 100644 --- a/sentry_sdk/integrations/langchain.py +++ b/sentry_sdk/integrations/langchain.py @@ -1,4 +1,3 @@ -import contextvars import itertools import sys import json @@ -153,44 +152,6 @@ def _transform_langchain_message_content(content: "Any") -> "Any": return content -# Contextvar to track agent names in a stack for re-entrant agent support -_agent_stack: "contextvars.ContextVar[Optional[List[Optional[str]]]]" = ( - contextvars.ContextVar("langchain_agent_stack", default=None) -) - - -def _push_agent(agent_name: "Optional[str]") -> None: - """Push an agent name onto the stack.""" - stack = _agent_stack.get() - if stack is None: - stack = [] - else: - # Copy the list to maintain contextvar isolation across async contexts - stack = stack.copy() - stack.append(agent_name) - _agent_stack.set(stack) - - -def _pop_agent() -> "Optional[str]": - """Pop an agent name from the stack and return it.""" - stack = _agent_stack.get() - if stack: - # Copy the list to maintain contextvar isolation across async contexts - stack = stack.copy() - agent_name = stack.pop() - _agent_stack.set(stack) - return agent_name - return None - - -def _get_current_agent() -> "Optional[str]": - """Get the current agent name (top of stack) without removing it.""" - stack = _agent_stack.get() - if stack: - return stack[-1] - return None - - def _get_system_instructions(messages: "List[List[BaseMessage]]") -> "List[str]": system_instructions = [] @@ -454,8 +415,8 @@ def on_chat_model_start( elif "openai" in ai_type: span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai") - agent_name = _get_current_agent() - if agent_name: + agent_name = kwargs.get("metadata", {}).get("lc_agent_name") + if agent_name is not None: span.set_data(SPANDATA.GEN_AI_AGENT_NAME, agent_name) for key, attribute in DATA_FIELDS.items(): @@ -654,8 +615,8 @@ def on_tool_start( if tool_description is not None: span.set_data(SPANDATA.GEN_AI_TOOL_DESCRIPTION, tool_description) - agent_name = _get_current_agent() - if agent_name: + agent_name = kwargs.get("metadata", {}).get("lc_agent_name") + if agent_name is not None: span.set_data(SPANDATA.GEN_AI_AGENT_NAME, agent_name) if should_send_default_pii() and self.include_prompts: @@ -782,9 +743,7 @@ def _record_token_usage(span: "Span", response: "Any") -> None: span.set_data(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, total_tokens) -def _get_request_data( - obj: "Any", args: "Any", kwargs: "Any" -) -> "tuple[Optional[str], Optional[List[Any]]]": +def _get_available_tools(obj: "Any") -> "tuple[Optional[str], Optional[List[Any]]]": """ Get the agent name and available tools for the agent. """ @@ -799,6 +758,13 @@ def _get_request_data( ) tools = tools if tools and len(tools) > 0 else None + return tools + + +def _get_run_name(obj: "Any", args: "Any"): + agent = getattr(obj, "agent", None) + runnable = getattr(agent, "runnable", None) + runnable_config = getattr(runnable, "config", {}) try: agent_name = None if len(args) > 1: @@ -808,7 +774,7 @@ def _get_request_data( except Exception: pass - return (agent_name, tools) + return agent_name def _simplify_langchain_tools(tools: "Any") -> "Optional[List[Any]]": @@ -976,58 +942,53 @@ def new_invoke(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": if integration is None: return f(self, *args, **kwargs) - agent_name, tools = _get_request_data(self, args, kwargs) start_span_function = get_start_span_function() - + run_name = _get_run_name(self, args) with start_span_function( op=OP.GEN_AI_INVOKE_AGENT, - name=f"invoke_agent {agent_name}" if agent_name else "invoke_agent", + name=run_name, origin=LangchainIntegration.origin, ) as span: - _push_agent(agent_name) - try: - if agent_name: - span.set_data(SPANDATA.GEN_AI_AGENT_NAME, agent_name) + if run_name: + span.set_data(SPANDATA.GEN_AI_AGENT_NAME, run_name) - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") - span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, False) + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") + span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, False) - _set_tools_on_span(span, tools) + tools = _get_available_tools(self) + _set_tools_on_span(span, tools) - # Run the agent - result = f(self, *args, **kwargs) + # Run the agent + result = f(self, *args, **kwargs) - input = result.get("input") - if ( - input is not None - and should_send_default_pii() - and integration.include_prompts - ): - normalized_messages = normalize_message_roles([input]) - scope = sentry_sdk.get_current_scope() - messages_data = truncate_and_annotate_messages( - normalized_messages, span, scope + input = result.get("input") + if ( + input is not None + and should_send_default_pii() + and integration.include_prompts + ): + normalized_messages = normalize_message_roles([input]) + scope = sentry_sdk.get_current_scope() + messages_data = truncate_and_annotate_messages( + normalized_messages, span, scope + ) + if messages_data is not None: + set_data_normalized( + span, + SPANDATA.GEN_AI_REQUEST_MESSAGES, + messages_data, + unpack=False, ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, - ) - output = result.get("output") - if ( - output is not None - and should_send_default_pii() - and integration.include_prompts - ): - set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output) + output = result.get("output") + if ( + output is not None + and should_send_default_pii() + and integration.include_prompts + ): + set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output) - return result - finally: - # Ensure agent is popped even if an exception occurs - _pop_agent() + return result return new_invoke @@ -1039,24 +1000,31 @@ def new_stream(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": if integration is None: return f(self, *args, **kwargs) - agent_name, tools = _get_request_data(self, args, kwargs) start_span_function = get_start_span_function() + agent_name = kwargs.get("metadata", {}).get("lc_agent_name") + run_name = _get_run_name(self, args) + + span_name = "invoke_agent" + if agent_name is not None: + span_name = f"invoke_agent {agent_name}" + elif run_name: + span_name = f"invoke_agent {run_name}" + span = start_span_function( op=OP.GEN_AI_INVOKE_AGENT, - name=f"invoke_agent {agent_name}" if agent_name else "invoke_agent", + name=span_name, origin=LangchainIntegration.origin, ) span.__enter__() - _push_agent(agent_name) - - if agent_name: + if agent_name is not None: span.set_data(SPANDATA.GEN_AI_AGENT_NAME, agent_name) span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) + tools = _get_available_tools(self) _set_tools_on_span(span, tools) input = args[0].get("input") if len(args) >= 1 else None @@ -1106,7 +1074,6 @@ def new_iterator() -> "Iterator[Any]": raise finally: # Ensure cleanup happens even if iterator is abandoned or fails - _pop_agent() span.__exit__(*exc_info) async def new_iterator_async() -> "AsyncIterator[Any]": @@ -1132,7 +1099,6 @@ async def new_iterator_async() -> "AsyncIterator[Any]": raise finally: # Ensure cleanup happens even if iterator is abandoned or fails - _pop_agent() span.__exit__(*exc_info) if str(type(result)) == "": diff --git a/tests/integrations/langchain/test_langchain.py b/tests/integrations/langchain/test_langchain.py index 46d831a3ea..7e5041c4be 100644 --- a/tests/integrations/langchain/test_langchain.py +++ b/tests/integrations/langchain/test_langchain.py @@ -254,6 +254,8 @@ def test_langchain_create_agent( assert len(chat_spans) == 1 assert chat_spans[0]["origin"] == "auto.ai.langchain" + assert chat_spans[0]["data"]["gen_ai.agent.name"] == "word_length_agent" + assert chat_spans[0]["data"]["gen_ai.usage.input_tokens"] == 10 assert chat_spans[0]["data"]["gen_ai.usage.output_tokens"] == 20 assert chat_spans[0]["data"]["gen_ai.usage.total_tokens"] == 30 @@ -410,6 +412,10 @@ def test_tool_execution_span( assert chat_spans[1]["origin"] == "auto.ai.langchain" assert tool_exec_span["origin"] == "auto.ai.langchain" + assert chat_spans[0]["data"]["gen_ai.agent.name"] == "word_length_agent" + assert chat_spans[1]["data"]["gen_ai.agent.name"] == "word_length_agent" + assert tool_exec_span["data"]["gen_ai.agent.name"] == "word_length_agent" + assert chat_spans[0]["data"]["gen_ai.usage.input_tokens"] == 142 assert chat_spans[0]["data"]["gen_ai.usage.output_tokens"] == 50 assert chat_spans[0]["data"]["gen_ai.usage.total_tokens"] == 192 From 8cf3f81a59a1458ff5695418e03cc66582a8a612 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Tue, 31 Mar 2026 09:14:30 +0200 Subject: [PATCH 2/6] typing --- sentry_sdk/integrations/langchain.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/langchain.py b/sentry_sdk/integrations/langchain.py index 6430477e5a..b685a838aa 100644 --- a/sentry_sdk/integrations/langchain.py +++ b/sentry_sdk/integrations/langchain.py @@ -754,7 +754,7 @@ def _record_token_usage(span: "Span", response: "Any") -> None: span.set_data(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, total_tokens) -def _get_available_tools(obj: "Any") -> "tuple[Optional[str], Optional[List[Any]]]": +def _get_available_tools(obj: "Any") -> "Optional[List[Any]]": """ Get the agent name and available tools for the agent. """ @@ -772,7 +772,7 @@ def _get_available_tools(obj: "Any") -> "tuple[Optional[str], Optional[List[Any] return tools -def _get_run_name(obj: "Any", args: "Any"): +def _get_run_name(obj: "Any", args: "Any") -> "Optional[str]": agent = getattr(obj, "agent", None) runnable = getattr(agent, "runnable", None) runnable_config = getattr(runnable, "config", {}) From 56ec48f644714f9a1f2645257985efe5880a2826 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Tue, 31 Mar 2026 09:19:51 +0200 Subject: [PATCH 3/6] fix span description --- sentry_sdk/integrations/langchain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/langchain.py b/sentry_sdk/integrations/langchain.py index b685a838aa..c6e7346d7b 100644 --- a/sentry_sdk/integrations/langchain.py +++ b/sentry_sdk/integrations/langchain.py @@ -957,7 +957,7 @@ def new_invoke(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": run_name = _get_run_name(self, args) with start_span_function( op=OP.GEN_AI_INVOKE_AGENT, - name=run_name, + name=f"invoke_agent {run_name}" if run_name else "invoke_agent", origin=LangchainIntegration.origin, ) as span: if run_name: From d8c06f8cce3fffe728ccc38966c4901777e8f3a8 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Tue, 31 Mar 2026 09:29:08 +0200 Subject: [PATCH 4/6] defensive check --- sentry_sdk/integrations/langchain.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/sentry_sdk/integrations/langchain.py b/sentry_sdk/integrations/langchain.py index c6e7346d7b..6279a29d6b 100644 --- a/sentry_sdk/integrations/langchain.py +++ b/sentry_sdk/integrations/langchain.py @@ -426,9 +426,11 @@ def on_chat_model_start( if ai_system: span.set_data(SPANDATA.GEN_AI_SYSTEM, ai_system) - agent_name = kwargs.get("metadata", {}).get("lc_agent_name") - if agent_name is not None: - span.set_data(SPANDATA.GEN_AI_AGENT_NAME, agent_name) + agent_metadata = kwargs.get("metadata") + if isinstance(agent_metadata, dict) and "lc_agent_name" in agent_metadata: + span.set_data( + SPANDATA.GEN_AI_AGENT_NAME, agent_metadata["lc_agent_name"] + ) for key, attribute in DATA_FIELDS.items(): if key in all_params and all_params[key] is not None: @@ -626,9 +628,11 @@ def on_tool_start( if tool_description is not None: span.set_data(SPANDATA.GEN_AI_TOOL_DESCRIPTION, tool_description) - agent_name = kwargs.get("metadata", {}).get("lc_agent_name") - if agent_name is not None: - span.set_data(SPANDATA.GEN_AI_AGENT_NAME, agent_name) + agent_metadata = kwargs.get("metadata") + if isinstance(agent_metadata, dict) and "lc_agent_name" in agent_metadata: + span.set_data( + SPANDATA.GEN_AI_AGENT_NAME, agent_metadata["lc_agent_name"] + ) if should_send_default_pii() and self.include_prompts: set_data_normalized( From 36ca817269ff88a1f144402732e2be43cbde7fbd Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Tue, 31 Mar 2026 09:34:06 +0200 Subject: [PATCH 5/6] no agent name in stream --- sentry_sdk/integrations/langchain.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/sentry_sdk/integrations/langchain.py b/sentry_sdk/integrations/langchain.py index 6279a29d6b..c786b22562 100644 --- a/sentry_sdk/integrations/langchain.py +++ b/sentry_sdk/integrations/langchain.py @@ -1017,25 +1017,15 @@ def new_stream(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": start_span_function = get_start_span_function() - agent_name = kwargs.get("metadata", {}).get("lc_agent_name") run_name = _get_run_name(self, args) - span_name = "invoke_agent" - if agent_name is not None: - span_name = f"invoke_agent {agent_name}" - elif run_name: - span_name = f"invoke_agent {run_name}" - span = start_span_function( op=OP.GEN_AI_INVOKE_AGENT, - name=span_name, + name=f"invoke_agent {run_name}" if run_name else "invoke_agent", origin=LangchainIntegration.origin, ) span.__enter__() - if agent_name is not None: - span.set_data(SPANDATA.GEN_AI_AGENT_NAME, agent_name) - span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) From 0d43616ca8e4ff4888d52cbc8d2fddbe199d41c9 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Tue, 31 Mar 2026 15:18:12 +0200 Subject: [PATCH 6/6] simplify --- sentry_sdk/integrations/langchain.py | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/sentry_sdk/integrations/langchain.py b/sentry_sdk/integrations/langchain.py index c786b22562..87c01ff326 100644 --- a/sentry_sdk/integrations/langchain.py +++ b/sentry_sdk/integrations/langchain.py @@ -758,7 +758,9 @@ def _record_token_usage(span: "Span", response: "Any") -> None: span.set_data(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, total_tokens) -def _get_available_tools(obj: "Any") -> "Optional[List[Any]]": +def _get_request_data( + obj: "Any", args: "Any", kwargs: "Any" +) -> "tuple[Optional[str], Optional[List[Any]]]": """ Get the agent name and available tools for the agent. """ @@ -773,13 +775,6 @@ def _get_available_tools(obj: "Any") -> "Optional[List[Any]]": ) tools = tools if tools and len(tools) > 0 else None - return tools - - -def _get_run_name(obj: "Any", args: "Any") -> "Optional[str]": - agent = getattr(obj, "agent", None) - runnable = getattr(agent, "runnable", None) - runnable_config = getattr(runnable, "config", {}) try: agent_name = None if len(args) > 1: @@ -789,7 +784,7 @@ def _get_run_name(obj: "Any", args: "Any") -> "Optional[str]": except Exception: pass - return agent_name + return (agent_name, tools) def _simplify_langchain_tools(tools: "Any") -> "Optional[List[Any]]": @@ -957,8 +952,9 @@ def new_invoke(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": if integration is None: return f(self, *args, **kwargs) + run_name, tools = _get_request_data(self, args, kwargs) start_span_function = get_start_span_function() - run_name = _get_run_name(self, args) + with start_span_function( op=OP.GEN_AI_INVOKE_AGENT, name=f"invoke_agent {run_name}" if run_name else "invoke_agent", @@ -970,7 +966,6 @@ def new_invoke(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, False) - tools = _get_available_tools(self) _set_tools_on_span(span, tools) # Run the agent @@ -1015,10 +1010,9 @@ def new_stream(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": if integration is None: return f(self, *args, **kwargs) + run_name, tools = _get_request_data(self, args, kwargs) start_span_function = get_start_span_function() - run_name = _get_run_name(self, args) - span = start_span_function( op=OP.GEN_AI_INVOKE_AGENT, name=f"invoke_agent {run_name}" if run_name else "invoke_agent", @@ -1029,7 +1023,6 @@ def new_stream(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True) - tools = _get_available_tools(self) _set_tools_on_span(span, tools) input = args[0].get("input") if len(args) >= 1 else None