From bd0b8161c54a825cd5d3d485f91fe3f15b56b863 Mon Sep 17 00:00:00 2001 From: harryautomazione Date: Fri, 20 Mar 2026 20:16:18 +0100 Subject: [PATCH 1/3] fix(ai): remove setting of GEN_AI_AGENT_NAME from langchain --- sentry_sdk/integrations/langchain.py | 55 ---------------------------- 1 file changed, 55 deletions(-) diff --git a/sentry_sdk/integrations/langchain.py b/sentry_sdk/integrations/langchain.py index d19d9bbdd5..28723058da 100644 --- a/sentry_sdk/integrations/langchain.py +++ b/sentry_sdk/integrations/langchain.py @@ -154,43 +154,6 @@ def _transform_langchain_message_content(content: "Any") -> "Any": # 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 = [] @@ -455,10 +418,6 @@ 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: - span.set_data(SPANDATA.GEN_AI_AGENT_NAME, agent_name) - for key, attribute in DATA_FIELDS.items(): if key in all_params and all_params[key] is not None: set_data_normalized(span, attribute, all_params[key], unpack=False) @@ -655,10 +614,6 @@ 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: - span.set_data(SPANDATA.GEN_AI_AGENT_NAME, agent_name) - if should_send_default_pii() and self.include_prompts: set_data_normalized( span, @@ -985,10 +940,7 @@ def new_invoke(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": name=f"invoke_agent {agent_name}" if agent_name else "invoke_agent", origin=LangchainIntegration.origin, ) as span: - _push_agent(agent_name) try: - if agent_name: - 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, False) @@ -1028,7 +980,6 @@ def new_invoke(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": return result finally: # Ensure agent is popped even if an exception occurs - _pop_agent() return new_invoke @@ -1050,10 +1001,6 @@ def new_stream(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": ) span.__enter__() - _push_agent(agent_name) - - if agent_name: - 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) @@ -1107,7 +1054,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]": @@ -1133,7 +1079,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)) == "": From 80c772d3acb3fa304295ce2b135a109d828dca8a Mon Sep 17 00:00:00 2001 From: harryautomazione Date: Fri, 20 Mar 2026 23:49:49 +0100 Subject: [PATCH 2/3] Fix Langchain GEN_AI_AGENT_NAME regression by adding contextvar propagation - Add CURRENT_LANGCHAIN_AGENT_NAME contextvar to track agent name across spans - Set agent name in agent executor wrappers (invoke/stream) - Propagate agent name to all child spans via _create_span - Add test to verify agent name is set on all spans --- sentry_sdk/integrations/langchain.py | 110 +++++++++++------- .../integrations/langchain/test_langchain.py | 86 ++++++++++++++ 2 files changed, 153 insertions(+), 43 deletions(-) diff --git a/sentry_sdk/integrations/langchain.py b/sentry_sdk/integrations/langchain.py index 28723058da..acd05805e5 100644 --- a/sentry_sdk/integrations/langchain.py +++ b/sentry_sdk/integrations/langchain.py @@ -23,6 +23,13 @@ from sentry_sdk.tracing_utils import _get_value, set_span_errored from sentry_sdk.utils import capture_internal_exceptions, logger +CURRENT_LANGCHAIN_AGENT_NAME = contextvars.ContextVar("CURRENT_LANGCHAIN_AGENT_NAME", default=None) + + +def _get_current_langchain_agent_name() -> "Optional[str]": + return CURRENT_LANGCHAIN_AGENT_NAME.get(None) + + if TYPE_CHECKING: from typing import ( Any, @@ -290,6 +297,11 @@ def _create_span( watched_span = WatchedSpan(sentry_sdk.start_span(**kwargs)) watched_span.span.__enter__() + + agent_name = _get_current_langchain_agent_name() + if agent_name: + watched_span.span.set_data(SPANDATA.GEN_AI_AGENT_NAME, agent_name) + self.span_map[run_id] = watched_span self.gc_span_map() return watched_span @@ -933,53 +945,60 @@ def new_invoke(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": return f(self, *args, **kwargs) agent_name, tools = _get_request_data(self, args, kwargs) + token = CURRENT_LANGCHAIN_AGENT_NAME.set(agent_name) start_span_function = get_start_span_function() - 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, - ) as span: - try: - - 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) - - # 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 - ) - if messages_data is not None: - set_data_normalized( - span, - SPANDATA.GEN_AI_REQUEST_MESSAGES, - messages_data, - unpack=False, + try: + 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, + ) as span: + try: + if agent_name: + 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, False) + + _set_tools_on_span(span, tools) + + # 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 ) + 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) - - return result - finally: - # Ensure agent is popped even if an exception occurs + 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 + pass + finally: + CURRENT_LANGCHAIN_AGENT_NAME.reset(token) return new_invoke @@ -992,6 +1011,7 @@ def new_stream(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": return f(self, *args, **kwargs) agent_name, tools = _get_request_data(self, args, kwargs) + token = CURRENT_LANGCHAIN_AGENT_NAME.set(agent_name) start_span_function = get_start_span_function() span = start_span_function( @@ -1001,6 +1021,8 @@ def new_stream(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": ) span.__enter__() + if agent_name: + 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) @@ -1055,6 +1077,7 @@ def new_iterator() -> "Iterator[Any]": finally: # Ensure cleanup happens even if iterator is abandoned or fails span.__exit__(*exc_info) + CURRENT_LANGCHAIN_AGENT_NAME.reset(token) async def new_iterator_async() -> "AsyncIterator[Any]": exc_info: "tuple[Any, Any, Any]" = (None, None, None) @@ -1080,6 +1103,7 @@ async def new_iterator_async() -> "AsyncIterator[Any]": finally: # Ensure cleanup happens even if iterator is abandoned or fails span.__exit__(*exc_info) + CURRENT_LANGCHAIN_AGENT_NAME.reset(token) if str(type(result)) == "": result = new_iterator_async() diff --git a/tests/integrations/langchain/test_langchain.py b/tests/integrations/langchain/test_langchain.py index 132da0a9a0..9b93ba152a 100644 --- a/tests/integrations/langchain/test_langchain.py +++ b/tests/integrations/langchain/test_langchain.py @@ -259,6 +259,92 @@ def test_langchain_agent( }, ] == json.loads(chat_spans[0]["data"][SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS]) + +def test_langchain_agent_name_propagation(sentry_init, capture_events, monkeypatch): + monkeypatch.setattr( + "sentry_sdk.integrations.langchain._get_request_data", + lambda obj, args, kwargs: ("test_agent_name", None), + ) + + sentry_init( + integrations=[LangchainIntegration(include_prompts=True)], + traces_sample_rate=1.0, + send_default_pii=True, + ) + events = capture_events() + + global llm_type, stream_result_mock + llm_type = "openai-chat" + stream_result_mock = Mock( + side_effect=[ + [ + ChatGenerationChunk( + type="ChatGenerationChunk", + message=AIMessageChunk( + content="", + additional_kwargs={ + "tool_calls": [ + { + "index": 0, + "id": "call_BbeyNhCKa6kYLYzrD40NGm3b", + "function": {"arguments": "", "name": "get_word_length"}, + "type": "function", + } + ] + }, + ), + ), + ChatGenerationChunk( + type="ChatGenerationChunk", + message=AIMessageChunk( + content="5", + usage_metadata={ + "input_tokens": 142, + "output_tokens": 50, + "total_tokens": 192, + "input_token_details": {"audio": 0, "cache_read": 0}, + "output_token_details": {"audio": 0, "reasoning": 0}, + }, + ), + generation_info={"finish_reason": "function_call"}, + ), + ], + [ + ChatGenerationChunk( + text="The word eudca has 5 letters.", + type="ChatGenerationChunk", + message=AIMessageChunk( + content="The word eudca has 5 letters.", + usage_metadata={ + "input_tokens": 89, + "output_tokens": 28, + "total_tokens": 117, + "input_token_details": {"audio": 0, "cache_read": 0}, + "output_token_details": {"audio": 0, "reasoning": 0}, + }, + ), + ), + ChatGenerationChunk( + type="ChatGenerationChunk", + generation_info={"finish_reason": "stop"}, + message=AIMessageChunk(content=""), + ), + ], + ] + ) + + llm = MockOpenAI(model_name="gpt-3.5-turbo", temperature=0, openai_api_key="badkey") + agent = create_openai_tools_agent(llm, [get_word_length], prompt=None) + agent_executor = AgentExecutor(agent=agent, tools=[get_word_length], verbose=True) + + with start_transaction(): + list(agent_executor.stream({"input": "How many letters in the word eudca"})) + + tx = events[0] + assert tx["type"] == "transaction" + for span in tx["spans"]: + assert span["data"].get(SPANDATA.GEN_AI_AGENT_NAME) == "test_agent_name" + assert "5" in chat_spans[1]["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] # Verify tool calls are recorded when PII is enabled From ce109824fae70edb5fb9d2adb85ecb5ca7e6ad8d Mon Sep 17 00:00:00 2001 From: harryautomazione Date: Sat, 21 Mar 2026 00:03:42 +0100 Subject: [PATCH 3/3] Fix test_langchain_agent_name_propagation placement - Correctly place the new test after test_langchain_agent - Remove misplaced assertions that broke the original test --- .../integrations/langchain/test_langchain.py | 126 ++++++------------ 1 file changed, 40 insertions(+), 86 deletions(-) diff --git a/tests/integrations/langchain/test_langchain.py b/tests/integrations/langchain/test_langchain.py index 9b93ba152a..1e73ef9634 100644 --- a/tests/integrations/langchain/test_langchain.py +++ b/tests/integrations/langchain/test_langchain.py @@ -259,92 +259,6 @@ def test_langchain_agent( }, ] == json.loads(chat_spans[0]["data"][SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS]) - -def test_langchain_agent_name_propagation(sentry_init, capture_events, monkeypatch): - monkeypatch.setattr( - "sentry_sdk.integrations.langchain._get_request_data", - lambda obj, args, kwargs: ("test_agent_name", None), - ) - - sentry_init( - integrations=[LangchainIntegration(include_prompts=True)], - traces_sample_rate=1.0, - send_default_pii=True, - ) - events = capture_events() - - global llm_type, stream_result_mock - llm_type = "openai-chat" - stream_result_mock = Mock( - side_effect=[ - [ - ChatGenerationChunk( - type="ChatGenerationChunk", - message=AIMessageChunk( - content="", - additional_kwargs={ - "tool_calls": [ - { - "index": 0, - "id": "call_BbeyNhCKa6kYLYzrD40NGm3b", - "function": {"arguments": "", "name": "get_word_length"}, - "type": "function", - } - ] - }, - ), - ), - ChatGenerationChunk( - type="ChatGenerationChunk", - message=AIMessageChunk( - content="5", - usage_metadata={ - "input_tokens": 142, - "output_tokens": 50, - "total_tokens": 192, - "input_token_details": {"audio": 0, "cache_read": 0}, - "output_token_details": {"audio": 0, "reasoning": 0}, - }, - ), - generation_info={"finish_reason": "function_call"}, - ), - ], - [ - ChatGenerationChunk( - text="The word eudca has 5 letters.", - type="ChatGenerationChunk", - message=AIMessageChunk( - content="The word eudca has 5 letters.", - usage_metadata={ - "input_tokens": 89, - "output_tokens": 28, - "total_tokens": 117, - "input_token_details": {"audio": 0, "cache_read": 0}, - "output_token_details": {"audio": 0, "reasoning": 0}, - }, - ), - ), - ChatGenerationChunk( - type="ChatGenerationChunk", - generation_info={"finish_reason": "stop"}, - message=AIMessageChunk(content=""), - ), - ], - ] - ) - - llm = MockOpenAI(model_name="gpt-3.5-turbo", temperature=0, openai_api_key="badkey") - agent = create_openai_tools_agent(llm, [get_word_length], prompt=None) - agent_executor = AgentExecutor(agent=agent, tools=[get_word_length], verbose=True) - - with start_transaction(): - list(agent_executor.stream({"input": "How many letters in the word eudca"})) - - tx = events[0] - assert tx["type"] == "transaction" - for span in tx["spans"]: - assert span["data"].get(SPANDATA.GEN_AI_AGENT_NAME) == "test_agent_name" - assert "5" in chat_spans[1]["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] # Verify tool calls are recorded when PII is enabled @@ -399,6 +313,46 @@ def test_langchain_agent_name_propagation(sentry_init, capture_events, monkeypat ) +def test_langchain_agent_name_propagation(sentry_init, capture_events, monkeypatch): + sentry_init( + integrations=[LangchainIntegration(include_prompts=True)], + traces_sample_rate=1.0, + send_default_pii=True, + ) + events = capture_events() + + prompt = ChatPromptTemplate.from_messages( + [ + ( + "system", + "You are very powerful assistant, but don't know current events", + ), + ("user", "{input}"), + MessagesPlaceholder(variable_name="agent_scratchpad"), + ] + ) + + llm = MockOpenAI(model_name="gpt-3.5-turbo", temperature=0, openai_api_key="badkey") + agent = create_openai_tools_agent(llm, [get_word_length], prompt) + agent_executor = AgentExecutor(agent=agent, tools=[get_word_length], verbose=True) + + # Mock _get_request_data to return a test agent name + def mock_get_request_data(self, args, kwargs): + return "test_agent_name", [get_word_length] + + monkeypatch.setattr( + "sentry_sdk.integrations.langchain._get_request_data", mock_get_request_data + ) + + with start_transaction(): + list(agent_executor.stream({"input": "How many letters in the word eudca"})) + + tx = events[0] + assert tx["type"] == "transaction" + for span in tx["spans"]: + assert span["data"].get(SPANDATA.GEN_AI_AGENT_NAME) == "test_agent_name" + + def test_langchain_error(sentry_init, capture_events): sentry_init( integrations=[LangchainIntegration(include_prompts=True)],