From bce8be6f2eda92ee75e78dea8ec1f7780567731b Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 19 Mar 2026 10:57:21 +0000 Subject: [PATCH 1/3] feat(langchain): Change LLM span operation to generate_text and capture agent name Update the LangChain integration to track LLM operations as gen_ai.generate_text instead of gen_ai.pipeline. This provides more specific operation semantics for LLM invocations. Additionally, capture the agent name when an LLM is invoked within an agent context using _push_agent/_pop_agent tracking. - Change span operation from OP.GEN_AI_PIPELINE to OP.GEN_AI_GENERATE_TEXT - Update span name to include model information - Add GEN_AI_OPERATION_NAME span data - Add GEN_AI_AGENT_NAME span data when agent context is active - Add GEN_AI_PIPELINE_NAME span data when pipeline name is provided - Update tests to check for new operation type and span data fields Co-Authored-By: Claude Haiku 4.5 --- sentry_sdk/integrations/langchain.py | 14 ++- .../integrations/langchain/test_langchain.py | 119 +++++++++++++++++- 2 files changed, 127 insertions(+), 6 deletions(-) diff --git a/sentry_sdk/integrations/langchain.py b/sentry_sdk/integrations/langchain.py index d19d9bbdd5..96e1863a03 100644 --- a/sentry_sdk/integrations/langchain.py +++ b/sentry_sdk/integrations/langchain.py @@ -366,15 +366,25 @@ def on_llm_start( or "" ) + pipeline_name = kwargs.get("name") watched_span = self._create_span( run_id, parent_run_id, - op=OP.GEN_AI_PIPELINE, - name=kwargs.get("name") or "Langchain LLM call", + op=OP.GEN_AI_GENERATE_TEXT, + name=f"generate_text {model}".strip(), origin=LangchainIntegration.origin, ) span = watched_span.span + span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "generate_text") + + agent_name = _get_current_agent() + if agent_name: + span.set_data(SPANDATA.GEN_AI_AGENT_NAME, agent_name) + + if pipeline_name: + span.set_data(SPANDATA.GEN_AI_PIPELINE_NAME, pipeline_name) + if model: span.set_data( SPANDATA.GEN_AI_REQUEST_MODEL, diff --git a/tests/integrations/langchain/test_langchain.py b/tests/integrations/langchain/test_langchain.py index 132da0a9a0..9e286126e8 100644 --- a/tests/integrations/langchain/test_langchain.py +++ b/tests/integrations/langchain/test_langchain.py @@ -27,6 +27,8 @@ SentryLangchainCallback, _transform_langchain_content_block, _transform_langchain_message_content, + _push_agent, + _pop_agent, ) try: @@ -851,12 +853,15 @@ def test_langchain_integration_with_langchain_core_only(sentry_init, capture_eve assert tx["type"] == "transaction" llm_spans = [ - span for span in tx.get("spans", []) if span.get("op") == "gen_ai.pipeline" + span + for span in tx.get("spans", []) + if span.get("op") == "gen_ai.generate_text" ] assert len(llm_spans) > 0 llm_span = llm_spans[0] - assert llm_span["description"] == "Langchain LLM call" + assert llm_span["description"] == "generate_text gpt-3.5-turbo" + assert llm_span["data"]["gen_ai.operation.name"] == "generate_text" assert llm_span["data"]["gen_ai.request.model"] == "gpt-3.5-turbo" assert ( llm_span["data"]["gen_ai.response.text"] @@ -867,6 +872,110 @@ def test_langchain_integration_with_langchain_core_only(sentry_init, capture_eve assert llm_span["data"]["gen_ai.usage.output_tokens"] == 15 +def test_langchain_llm_span_includes_agent_name(sentry_init, capture_events): + from langchain_core.outputs import LLMResult, Generation + + with patch("sentry_sdk.integrations.langchain.AgentExecutor", None): + from sentry_sdk.integrations.langchain import ( + LangchainIntegration, + SentryLangchainCallback, + ) + + sentry_init( + integrations=[LangchainIntegration(include_prompts=True)], + traces_sample_rate=1.0, + send_default_pii=True, + ) + events = capture_events() + + callback = SentryLangchainCallback(max_span_map_size=100, include_prompts=True) + + run_id = "12345678-1234-1234-1234-123456789abc" + serialized = {"_type": "openai", "model_name": "gpt-3.5-turbo"} + prompts = ["Hello"] + + with start_transaction(): + _push_agent("test-agent") + try: + callback.on_llm_start( + serialized=serialized, + prompts=prompts, + run_id=run_id, + invocation_params={"model": "gpt-3.5-turbo"}, + ) + + response = LLMResult( + generations=[[Generation(text="Hi")]], + llm_output={}, + ) + callback.on_llm_end(response=response, run_id=run_id) + finally: + _pop_agent() + + assert len(events) > 0 + tx = events[0] + + llm_spans = [ + span + for span in tx.get("spans", []) + if span.get("op") == "gen_ai.generate_text" + ] + assert len(llm_spans) == 1 + + llm_span = llm_spans[0] + assert llm_span["data"][SPANDATA.GEN_AI_AGENT_NAME] == "test-agent" + + +def test_langchain_llm_span_no_agent_name_when_no_agent(sentry_init, capture_events): + from langchain_core.outputs import LLMResult, Generation + + with patch("sentry_sdk.integrations.langchain.AgentExecutor", None): + from sentry_sdk.integrations.langchain import ( + LangchainIntegration, + SentryLangchainCallback, + ) + + sentry_init( + integrations=[LangchainIntegration(include_prompts=True)], + traces_sample_rate=1.0, + send_default_pii=True, + ) + events = capture_events() + + callback = SentryLangchainCallback(max_span_map_size=100, include_prompts=True) + + run_id = "12345678-1234-1234-1234-123456789def" + serialized = {"_type": "openai", "model_name": "gpt-3.5-turbo"} + prompts = ["Hello"] + + with start_transaction(): + callback.on_llm_start( + serialized=serialized, + prompts=prompts, + run_id=run_id, + invocation_params={"model": "gpt-3.5-turbo"}, + ) + + response = LLMResult( + generations=[[Generation(text="Hi")]], + llm_output={}, + ) + callback.on_llm_end(response=response, run_id=run_id) + + assert len(events) > 0 + tx = events[0] + + llm_spans = [ + span + for span in tx.get("spans", []) + if span.get("op") == "gen_ai.generate_text" + ] + assert len(llm_spans) == 1 + + llm_span = llm_spans[0] + assert SPANDATA.GEN_AI_AGENT_NAME not in llm_span["data"] + + def test_langchain_message_role_mapping(sentry_init, capture_events): """Test that message roles are properly normalized in langchain integration.""" global llm_type @@ -1062,11 +1171,12 @@ def test_langchain_message_truncation(sentry_init, capture_events): assert tx["type"] == "transaction" llm_spans = [ - span for span in tx.get("spans", []) if span.get("op") == "gen_ai.pipeline" + span for span in tx.get("spans", []) if span.get("op") == "gen_ai.generate_text" ] assert len(llm_spans) > 0 llm_span = llm_spans[0] + assert llm_span["data"]["gen_ai.operation.name"] == "generate_text" assert SPANDATA.GEN_AI_REQUEST_MESSAGES in llm_span["data"] messages_data = llm_span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES] @@ -1776,11 +1886,12 @@ def test_langchain_response_model_extraction( assert tx["type"] == "transaction" llm_spans = [ - span for span in tx.get("spans", []) if span.get("op") == "gen_ai.pipeline" + span for span in tx.get("spans", []) if span.get("op") == "gen_ai.generate_text" ] assert len(llm_spans) > 0 llm_span = llm_spans[0] + assert llm_span["data"]["gen_ai.operation.name"] == "generate_text" if expected_model is not None: assert SPANDATA.GEN_AI_RESPONSE_MODEL in llm_span["data"] From c4d5f03acf7a1f51dfbaebfc039546ab7e9389df Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 19 Mar 2026 11:08:27 +0000 Subject: [PATCH 2/3] ref(langchain): Remove redundant docstring from on_llm_start Co-Authored-By: Claude Sonnet 4.6 --- sentry_sdk/integrations/langchain.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sentry_sdk/integrations/langchain.py b/sentry_sdk/integrations/langchain.py index 96e1863a03..ad5c17a895 100644 --- a/sentry_sdk/integrations/langchain.py +++ b/sentry_sdk/integrations/langchain.py @@ -351,7 +351,6 @@ def on_llm_start( metadata: "Optional[Dict[str, Any]]" = None, **kwargs: "Any", ) -> "Any": - """Run when LLM starts running.""" with capture_internal_exceptions(): if not run_id: return From 30e22baf2ce20b119a7239a9123eb5aaddd5c04b Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Fri, 20 Mar 2026 09:53:23 +0000 Subject: [PATCH 3/3] ref(langchain): Remove agent name from on_llm_start and clean up tests Move agent name capture out of on_llm_start to be handled in follow-up PRs. Remove associated tests and unused imports. Co-Authored-By: Claude Opus 4.6 (1M context) --- sentry_sdk/integrations/langchain.py | 6 +- .../integrations/langchain/test_langchain.py | 106 ------------------ 2 files changed, 1 insertion(+), 111 deletions(-) diff --git a/sentry_sdk/integrations/langchain.py b/sentry_sdk/integrations/langchain.py index ad5c17a895..1d77001684 100644 --- a/sentry_sdk/integrations/langchain.py +++ b/sentry_sdk/integrations/langchain.py @@ -365,7 +365,6 @@ def on_llm_start( or "" ) - pipeline_name = kwargs.get("name") watched_span = self._create_span( run_id, parent_run_id, @@ -377,10 +376,7 @@ def on_llm_start( span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "generate_text") - agent_name = _get_current_agent() - if agent_name: - span.set_data(SPANDATA.GEN_AI_AGENT_NAME, agent_name) - + pipeline_name = kwargs.get("name") if pipeline_name: span.set_data(SPANDATA.GEN_AI_PIPELINE_NAME, pipeline_name) diff --git a/tests/integrations/langchain/test_langchain.py b/tests/integrations/langchain/test_langchain.py index 9e286126e8..00feb36a50 100644 --- a/tests/integrations/langchain/test_langchain.py +++ b/tests/integrations/langchain/test_langchain.py @@ -27,8 +27,6 @@ SentryLangchainCallback, _transform_langchain_content_block, _transform_langchain_message_content, - _push_agent, - _pop_agent, ) try: @@ -872,110 +870,6 @@ def test_langchain_integration_with_langchain_core_only(sentry_init, capture_eve assert llm_span["data"]["gen_ai.usage.output_tokens"] == 15 -def test_langchain_llm_span_includes_agent_name(sentry_init, capture_events): - from langchain_core.outputs import LLMResult, Generation - - with patch("sentry_sdk.integrations.langchain.AgentExecutor", None): - from sentry_sdk.integrations.langchain import ( - LangchainIntegration, - SentryLangchainCallback, - ) - - sentry_init( - integrations=[LangchainIntegration(include_prompts=True)], - traces_sample_rate=1.0, - send_default_pii=True, - ) - events = capture_events() - - callback = SentryLangchainCallback(max_span_map_size=100, include_prompts=True) - - run_id = "12345678-1234-1234-1234-123456789abc" - serialized = {"_type": "openai", "model_name": "gpt-3.5-turbo"} - prompts = ["Hello"] - - with start_transaction(): - _push_agent("test-agent") - try: - callback.on_llm_start( - serialized=serialized, - prompts=prompts, - run_id=run_id, - invocation_params={"model": "gpt-3.5-turbo"}, - ) - - response = LLMResult( - generations=[[Generation(text="Hi")]], - llm_output={}, - ) - callback.on_llm_end(response=response, run_id=run_id) - finally: - _pop_agent() - - assert len(events) > 0 - tx = events[0] - - llm_spans = [ - span - for span in tx.get("spans", []) - if span.get("op") == "gen_ai.generate_text" - ] - assert len(llm_spans) == 1 - - llm_span = llm_spans[0] - assert llm_span["data"][SPANDATA.GEN_AI_AGENT_NAME] == "test-agent" - - -def test_langchain_llm_span_no_agent_name_when_no_agent(sentry_init, capture_events): - from langchain_core.outputs import LLMResult, Generation - - with patch("sentry_sdk.integrations.langchain.AgentExecutor", None): - from sentry_sdk.integrations.langchain import ( - LangchainIntegration, - SentryLangchainCallback, - ) - - sentry_init( - integrations=[LangchainIntegration(include_prompts=True)], - traces_sample_rate=1.0, - send_default_pii=True, - ) - events = capture_events() - - callback = SentryLangchainCallback(max_span_map_size=100, include_prompts=True) - - run_id = "12345678-1234-1234-1234-123456789def" - serialized = {"_type": "openai", "model_name": "gpt-3.5-turbo"} - prompts = ["Hello"] - - with start_transaction(): - callback.on_llm_start( - serialized=serialized, - prompts=prompts, - run_id=run_id, - invocation_params={"model": "gpt-3.5-turbo"}, - ) - - response = LLMResult( - generations=[[Generation(text="Hi")]], - llm_output={}, - ) - callback.on_llm_end(response=response, run_id=run_id) - - assert len(events) > 0 - tx = events[0] - - llm_spans = [ - span - for span in tx.get("spans", []) - if span.get("op") == "gen_ai.generate_text" - ] - assert len(llm_spans) == 1 - - llm_span = llm_spans[0] - assert SPANDATA.GEN_AI_AGENT_NAME not in llm_span["data"] - - def test_langchain_message_role_mapping(sentry_init, capture_events): """Test that message roles are properly normalized in langchain integration.""" global llm_type