fix(mcp,langchain): Add mechanism to captured exceptions by gmassello · Pull Request #7226 · getsentry/sentry-python · GitHub
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion sentry_sdk/integrations/langchain.py
29 changes: 23 additions & 6 deletions sentry_sdk/integrations/mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
from sentry_sdk.traces import StreamedSpan
from sentry_sdk.tracing_utils import has_span_streaming_enabled
from sentry_sdk.utils import (
capture_internal_exceptions,
event_from_exception,
has_data_collection_enabled,
nullcontext,
package_version,
Expand Down Expand Up @@ -100,6 +102,15 @@ def setup_once() -> None:
_patch_fastmcp()


def _capture_exception(exc: "Any") -> None:
event, hint = event_from_exception(
exc,
client_options=sentry_sdk.get_client().options,
mechanism={"type": "mcp", "handled": False},
)
sentry_sdk.capture_event(event, hint=hint)


@contextmanager
def _active_http_scopes(
ctx: "Any",
Expand Down Expand Up @@ -394,7 +405,8 @@ async def _tool_handler_wrapper(
result = await result

except Exception as e:
sentry_sdk.capture_exception(e)
with capture_internal_exceptions():
_capture_exception(e)
raise

if result is None:
Expand Down Expand Up @@ -490,7 +502,8 @@ async def _instrument_v2_tool_call(
result = await call_next(ctx)

except Exception as e:
sentry_sdk.capture_exception(e)
with capture_internal_exceptions():
_capture_exception(e)
raise

if not isinstance(result, dict):
Expand Down Expand Up @@ -615,7 +628,8 @@ async def _prompt_handler_wrapper(
result = await result

except Exception as e:
sentry_sdk.capture_exception(e)
with capture_internal_exceptions():
_capture_exception(e)
raise

if result is None:
Expand Down Expand Up @@ -764,7 +778,8 @@ async def _instrument_v2_prompt_get(
try:
result = await call_next(ctx)
except Exception as e:
sentry_sdk.capture_exception(e)
with capture_internal_exceptions():
_capture_exception(e)
raise

if not isinstance(result, dict):
Expand Down Expand Up @@ -919,7 +934,8 @@ async def _resource_handler_wrapper(
result = await result

except Exception as e:
sentry_sdk.capture_exception(e)
with capture_internal_exceptions():
_capture_exception(e)
raise

return result
Expand Down Expand Up @@ -984,7 +1000,8 @@ async def _instrument_v2_resource_read(
result = await call_next(ctx)

except Exception as e:
sentry_sdk.capture_exception(e)
with capture_internal_exceptions():
_capture_exception(e)
raise

return result
Expand Down
83 changes: 83 additions & 0 deletions tests/integrations/langchain/test_langchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -3621,6 +3621,8 @@ def _llm_type(self) -> str:

error = events[0]
assert error["level"] == "error"
assert error["exception"]["values"][0]["mechanism"]["type"] == "langchain"
assert not error["exception"]["values"][0]["mechanism"]["handled"]


@pytest.mark.parametrize("span_streaming", [True, False])
Expand Down Expand Up @@ -3691,6 +3693,8 @@ def _llm_type(self) -> str:

(error,) = (item.payload for item in items if item.type == "event")
assert error["level"] == "error"
assert error["exception"]["values"][0]["mechanism"]["type"] == "langchain"
assert not error["exception"]["values"][0]["mechanism"]["handled"]
sentry_sdk.flush()
spans = [item.payload for item in items if item.type == "span"]
assert spans[0]["status"] == "error"
Expand Down Expand Up @@ -3728,6 +3732,8 @@ def _llm_type(self) -> str:

(error,) = (item.payload for item in items if item.type == "event")
assert error["level"] == "error"
assert error["exception"]["values"][0]["mechanism"]["type"] == "langchain"
assert not error["exception"]["values"][0]["mechanism"]["handled"]
sentry_sdk.flush()
spans = [item.payload for item in items if item.type == "span"]
assert spans[0]["status"] == "error"
Expand Down Expand Up @@ -3766,10 +3772,87 @@ def _llm_type(self) -> str:

(error, transaction) = events
assert error["level"] == "error"
assert error["exception"]["values"][0]["mechanism"]["type"] == "langchain"
assert not error["exception"]["values"][0]["mechanism"]["handled"]
assert transaction["spans"][0]["status"] == "internal_error"
assert transaction["spans"][0]["tags"]["status"] == "internal_error"


@tool
def failing_tool(word: str) -> int:
"""Raises instead of returning a length."""
raise ValueError("Tool execution failed")


@pytest.mark.skipif(
LANGCHAIN_VERSION < (1,),
reason="LangChain 1.0+ required (ONE AGENT refactor)",
)
def test_langchain_tool_error(
sentry_init,
capture_events,
get_model_response,
nonstreaming_responses_tool_call_model_responses,
):
sentry_init(
integrations=[LangchainIntegration(include_prompts=True)],
disabled_integrations=[StdlibIntegration],
traces_sample_rate=1.0,
)

responses = nonstreaming_responses_tool_call_model_responses(
tool_name="failing_tool",
arguments='{"word": "eudca"}',
response_model="gpt-4-0613",
response_text="",
response_ids=iter(["resp_1"]),
usages=iter(
[
ResponseUsage(
input_tokens=0,
input_tokens_details=InputTokensDetails(
cached_tokens=0,
cache_write_tokens=0,
),
output_tokens=0,
output_tokens_details=OutputTokensDetails(
reasoning_tokens=0,
),
total_tokens=0,
),
]
),
)
tool_response = get_model_response(
next(responses),
serialize_pydantic=True,
request_headers={
"X-Stainless-Raw-Response": "True",
},
)

llm = ChatOpenAI(
model_name="gpt-4",
temperature=0,
openai_api_key="badkey",
use_responses_api=True,
)
agent = create_agent(model=llm, tools=[failing_tool], name="failing_agent")

events = capture_events()

with patch.object(
llm.client._client._client, "send", side_effect=[tool_response]
), start_transaction(name="tx"), pytest.raises(ValueError):
agent.invoke({"messages": [HumanMessage(content="hi")]})

error_events = [event for event in events if event.get("level") == "error"]
assert len(error_events) == 1
assert error_events[0]["exception"]["values"][0]["type"] == "ValueError"
assert error_events[0]["exception"]["values"][0]["mechanism"]["type"] == "langchain"
assert not error_events[0]["exception"]["values"][0]["mechanism"]["handled"]
Comment thread
cursor[bot] marked this conversation as resolved.


def test_manual_callback_no_duplication(sentry_init):
"""
Test that when a user manually provides a SentryLangchainCallback,
Expand Down
14 changes: 14 additions & 0 deletions tests/integrations/mcp/test_mcp.py