diff --git a/sentry_sdk/integrations/langchain.py b/sentry_sdk/integrations/langchain.py index 054a953a01..9dcbb189ce 100644 --- a/sentry_sdk/integrations/langchain.py +++ b/sentry_sdk/integrations/langchain.py @@ -210,6 +210,8 @@ class LangchainIntegration(Integration): identifier = "langchain" origin = f"auto.ai.{identifier}" + _ignored_exceptions: "set[type[Exception]]" = set() + def __init__( self: "LangchainIntegration", include_prompts: bool = True, @@ -262,17 +264,22 @@ def gc_span_map(self) -> None: self._exit_span(span, run_id) def _handle_error(self, run_id: "UUID", error: "Any") -> None: + is_ignored = isinstance(error, tuple(LangchainIntegration._ignored_exceptions)) + with capture_internal_exceptions(): if not run_id or run_id not in self.span_map: return span = self.span_map[run_id] - sentry_sdk.capture_exception( - error, span._scope if isinstance(span, StreamedSpan) else span.scope - ) + if is_ignored: + span.__exit__(None, None, None) + else: + sentry_sdk.capture_exception( + error, span._scope if isinstance(span, StreamedSpan) else span.scope + ) + span.__exit__(type(error), error, error.__traceback__) - span.__exit__(type(error), error, error.__traceback__) del self.span_map[run_id] def _normalize_langchain_message(self, message: "BaseMessage") -> "Any": diff --git a/sentry_sdk/integrations/langgraph.py b/sentry_sdk/integrations/langgraph.py index 8039aec6a9..3d3856a913 100644 --- a/sentry_sdk/integrations/langgraph.py +++ b/sentry_sdk/integrations/langgraph.py @@ -10,6 +10,9 @@ ) from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.integrations import DidNotEnable, Integration + +# This is fine because langgraph depends on langchain-base, and LangchainIntegration only imports from langchain-base. +from sentry_sdk.integrations.langchain import LangchainIntegration from sentry_sdk.scope import should_send_default_pii from sentry_sdk.traces import StreamedSpan from sentry_sdk.tracing_utils import ( @@ -19,6 +22,7 @@ from sentry_sdk.utils import safe_serialize try: + from langgraph.errors import GraphBubbleUp from langgraph.graph import StateGraph from langgraph.pregel import Pregel except ImportError: @@ -34,6 +38,7 @@ def __init__(self: "LanggraphIntegration", include_prompts: bool = True) -> None @staticmethod def setup_once() -> None: + LangchainIntegration._ignored_exceptions.add(GraphBubbleUp) # LangGraph lets users create agents using a StateGraph or the Functional API. # StateGraphs are then compiled to a CompiledStateGraph. Both CompiledStateGraph and # the functional API execute on a Pregel instance. Pregel is the runtime for the graph diff --git a/tests/integrations/langgraph/test_langgraph.py b/tests/integrations/langgraph/test_langgraph.py index 0b85cca3cc..200bd58838 100644 --- a/tests/integrations/langgraph/test_langgraph.py +++ b/tests/integrations/langgraph/test_langgraph.py @@ -3,6 +3,10 @@ from unittest.mock import MagicMock, patch import pytest +from langchain_core.language_models.chat_models import BaseChatModel +from langchain_core.messages import HumanMessage +from langchain_core.outputs import ChatResult +from langgraph.errors import GraphBubbleUp import sentry_sdk from sentry_sdk import start_transaction @@ -125,6 +129,15 @@ async def ainvoke(self, state, config=None): return {"messages": [MockMessage("Async Pregel response")]} +class InterruptingChatModel(BaseChatModel): + @property + def _llm_type(self) -> str: + return "interrupting-chat-model" + + def _generate(self, messages, stop=None, run_manager=None, **kwargs) -> ChatResult: + raise GraphBubbleUp("interrupt") + + def test_langgraph_integration_init(): """Test LanggraphIntegration initialization with different parameters.""" integration = LanggraphIntegration() @@ -2104,3 +2117,17 @@ def original_invoke(self, *args, **kwargs): assert len(parsed_messages) == 1 assert "small message 5" in str(parsed_messages[0]) assert tx["_meta"]["spans"]["0"]["data"]["gen_ai.request.messages"][""]["len"] == 5 + + +def test_graph_bubble_up_ignored(sentry_init, capture_items): + sentry_init( + integrations=[LanggraphIntegration()], + ) + + events = capture_items("event") + + model = InterruptingChatModel() + with pytest.raises(GraphBubbleUp): + model.invoke([HumanMessage(content="hi")]) + + assert len(events) == 0