<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Germán Massello</title>
    <description>The latest articles on DEV Community by Germán Massello (@gmassello).</description>
    <link>https://dev.to/gmassello</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4090217%2F7b46bfec-587d-49b7-b2b7-34fb8916fc76.png</url>
      <title>DEV Community: Germán Massello</title>
      <link>https://dev.to/gmassello</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gmassello"/>
    <language>en</language>
    <item>
      <title>The bug report was wrong, and that was the interesting part</title>
      <dc:creator>Germán Massello</dc:creator>
      <pubDate>Sat, 22 Aug 2026 22:46:18 +0000</pubDate>
      <link>https://dev.to/gmassello/the-bug-report-was-wrong-and-that-was-the-interesting-part-328h</link>
      <guid>https://dev.to/gmassello/the-bug-report-was-wrong-and-that-was-the-interesting-part-328h</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Clear the Lineup&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Overview
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/getsentry/sentry-python" rel="noopener noreferrer"&gt;&lt;code&gt;getsentry/sentry-python&lt;/code&gt;&lt;/a&gt; is the official Sentry SDK for Python. You call &lt;code&gt;sentry_sdk.init()&lt;/code&gt; once, and it auto-instruments whatever libraries it finds installed — Django, Celery, Redis, and a growing set of AI integrations: OpenAI, Anthropic, LangChain, LangGraph, LiteLLM, and MCP.&lt;/p&gt;

&lt;p&gt;When one of those integrations catches an error, it attaches a &lt;strong&gt;mechanism&lt;/strong&gt; to the event before sending it. The mechanism carries two fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;type&lt;/code&gt; — which integration captured this&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;handled&lt;/code&gt; — whether the error was caught, or escaped into the user's code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sentry uses both. &lt;code&gt;type&lt;/code&gt; drives attribution and integration adoption metrics. &lt;code&gt;handled&lt;/code&gt; decides whether an error counts toward your crash-free rate and whether it trips unhandled-issue alerts.&lt;/p&gt;

&lt;p&gt;I picked &lt;a href="https://github.com/getsentry/sentry-python/issues/5242" rel="noopener noreferrer"&gt;issue #5242&lt;/a&gt;: &lt;em&gt;"No mechanism on MCP and LangChain exceptions"&lt;/em&gt;, opened by a Sentry engineer, labeled &lt;code&gt;Bug&lt;/code&gt;, unassigned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug Fix or Performance Improvement
&lt;/h2&gt;

&lt;p&gt;The report says the mechanism is missing. Before touching anything, I wrote the assertions that would prove it and ran them against the existing test suite. The repo already spins up real MCP servers over stdio and makes tools fail, so the reproduction was two lines added to tests that already existed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exception&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;values&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mechanism&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mcp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exception&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;values&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mechanism&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;handled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ten tests failed. But not the way the issue implied:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AssertionError: assert 'generic' == 'mcp'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not &lt;code&gt;KeyError&lt;/code&gt;. The mechanism was &lt;strong&gt;there&lt;/strong&gt; — with the default value from &lt;code&gt;sentry_sdk/utils.py:692&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;exception_value&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mechanism&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;mechanism&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;mechanism&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;generic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;handled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So this is two defects, not one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;type: "generic"&lt;/code&gt;&lt;/strong&gt; is the one the issue describes: the error carries no fingerprint of the integration that captured it. In the UI, an error from an MCP tool is indistinguishable from one your own code raised.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;handled: True&lt;/code&gt;&lt;/strong&gt; is the one that isn't in the issue, and it's the one that actually costs you something. Every MCP capture site looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;sentry_sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;capture_exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;raise&lt;/code&gt; means the exception keeps going and lands in your application. Nothing handled it. But with no mechanism supplied, Sentry defaults to &lt;code&gt;handled: True&lt;/code&gt; — so these errors don't count as unhandled, don't move your crash-free rate, and don't trip the alerts built on that signal. Your service is breaking and the dashboard says it's fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;PR:&lt;/strong&gt; &lt;a href="https://github.com/getsentry/sentry-python/pull/7226" rel="noopener noreferrer"&gt;getsentry/sentry-python#7226&lt;/a&gt; (draft)&lt;/p&gt;

&lt;p&gt;The fix is a module-level helper in each integration, matching the pattern the other eight AI integrations already use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_capture_exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Any&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;event_from_exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;client_options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sentry_sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_client&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;mechanism&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mcp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;handled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sentry_sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;capture_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;hint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seven call sites route through it — six in &lt;code&gt;mcp.py&lt;/code&gt;, one in &lt;code&gt;langchain.py&lt;/code&gt;. About 30 lines of production code.&lt;/p&gt;

&lt;p&gt;A note on the PR's state, since it's visible on GitHub: it's open as a &lt;strong&gt;draft&lt;/strong&gt;, because the repo converts non-draft PRs automatically. The upstream test matrix hasn't run yet — GitHub gates workflows on pull requests from first-time contributors until a maintainer approves them. The three checks that don't need approval (Socket Security ×2, Semgrep) pass. Everything I could verify myself, I ran locally and listed below.&lt;/p&gt;

&lt;p&gt;CONTRIBUTING also asks contributors to discuss the approach with a maintainer before opening a PR. The challenge deadline didn't allow for that round trip, so I opened the PR as a draft and left a comment on the issue explaining what I found. If the approach doesn't fit, it costs the maintainers one comment to close it.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Improvements
&lt;/h2&gt;

&lt;p&gt;The fix is small. Everything interesting is in what I had to verify to be sure it was right.&lt;/p&gt;

&lt;h3&gt;
  
  
  The reproduction is the regression test
&lt;/h3&gt;

&lt;p&gt;I didn't build a separate harness. The suite already drives real MCP servers, so the assertions that prove the bug are the same ones that will guard against it. Red before, green after, one artifact.&lt;/p&gt;

&lt;h3&gt;
  
  
  Coverage measured, not assumed
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;mcp.py&lt;/code&gt; has six capture sites. The obvious assumption is that the three error tests cover them. They do — but only across &lt;strong&gt;two versions of the &lt;code&gt;mcp&lt;/code&gt; package&lt;/strong&gt;, which are mutually exclusive code paths: v1 patches decorators, v2 installs middleware.&lt;/p&gt;

&lt;p&gt;I ran the suite under &lt;code&gt;--cov&lt;/code&gt; and checked which lines actually executed:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;call site&lt;/th&gt;
&lt;th&gt;mcp v1.29.0&lt;/th&gt;
&lt;th&gt;mcp v2.0.0&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;397 &lt;code&gt;_tool_handler_wrapper&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;493 &lt;code&gt;_instrument_v2_tool_call&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;618 &lt;code&gt;_prompt_handler_wrapper&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;767 &lt;code&gt;_instrument_v2_prompt_get&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;922 &lt;code&gt;_resource_handler_wrapper&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;987 &lt;code&gt;_instrument_v2_resource_read&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Run one version and half the fix is untested. Worth knowing before claiming the fix works.&lt;/p&gt;

&lt;p&gt;What I ran locally, all green:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;env&lt;/th&gt;
&lt;th&gt;result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;py3.14-mcp-v1.29.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;100 passed, 7 skipped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;py3.14-mcp-v2.0.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;105 passed, 2 skipped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;py3.14-langchain-base-v1.3.14&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;626 passed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;py3.14-langgraph-v0.6.11&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;146 passed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;py3.14-fastmcp-v1.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;88 passed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;py3.13-fastmcp-v4.0.0b2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;42 passed, 3 skipped&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Plus &lt;code&gt;mypy sentry_sdk&lt;/code&gt; (strict, 193 files) and &lt;code&gt;ruff&lt;/code&gt; clean.&lt;/p&gt;

&lt;p&gt;LangGraph is in there for a reason: &lt;code&gt;LangchainIntegration._ignored_exceptions&lt;/code&gt; is populated only by the LangGraph integration, and &lt;code&gt;test_graph_bubble_up_ignored&lt;/code&gt; asserts that ignored exceptions produce &lt;strong&gt;zero&lt;/strong&gt; events. The &lt;code&gt;is_ignored&lt;/code&gt; branch never reaches the capture, so the fix shouldn't touch it — but "shouldn't" isn't "doesn't", so I ran it.&lt;/p&gt;

&lt;h3&gt;
  
  
  A path with zero coverage
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;langchain.py&lt;/code&gt; has three entry points into its error handler. Two were exercised. &lt;code&gt;on_tool_error&lt;/code&gt; — the one that fires when an agent's tool raises — had &lt;strong&gt;no test at all&lt;/strong&gt;: the only tool defined in the 7,000-line test file always succeeds.&lt;/p&gt;

&lt;p&gt;I wrote &lt;code&gt;test_langchain_tool_error&lt;/code&gt;, and confirmed with coverage that it hits &lt;code&gt;on_tool_error&lt;/code&gt; and the capture, and neither of the other two entry points. That one goes into the PR on its own merit — it covers a real path nothing covered before.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;handled=False&lt;/code&gt; at all seven sites
&lt;/h3&gt;

&lt;p&gt;The MCP sites re-raise, so the exception escapes. &lt;code&gt;flask.py:240&lt;/code&gt; sets the precedent: errors a framework later converts into a response are still unhandled.&lt;/p&gt;

&lt;p&gt;For LangChain, &lt;code&gt;_handle_error&lt;/code&gt; is a notification callback, not a swallow point — the existing test wraps the whole thing in &lt;code&gt;pytest.raises(ValueError)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I deliberately did &lt;strong&gt;not&lt;/strong&gt; add a &lt;code&gt;handled&lt;/code&gt; parameter. The only &lt;code&gt;handled=True&lt;/code&gt; among the AI integrations lives in &lt;code&gt;pydantic_ai&lt;/code&gt;, gated behind an option that has no equivalent here. YAGNI.&lt;/p&gt;

&lt;h3&gt;
  
  
  The regression I introduced, and caught
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;sentry_sdk.capture_exception&lt;/code&gt; wraps its internal &lt;code&gt;capture_event&lt;/code&gt; in a safety net (&lt;code&gt;scope.py:1593-1596&lt;/code&gt;). If Sentry's own serialization blows up, it swallows that quietly rather than replacing your exception with its own.&lt;/p&gt;

&lt;p&gt;My helper dropped that net. The sibling integrations restore it at the call site (&lt;code&gt;openai.py:845-846&lt;/code&gt;); mine didn't. Inside &lt;code&gt;except Exception as e:&lt;/code&gt; followed by &lt;code&gt;raise&lt;/code&gt;, that's a real regression: an internal SDK failure would surface to the user instead of their own error. The repo's contract is explicit — &lt;em&gt;"Don't crash applications."&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;capture_internal_exceptions&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="nf"&gt;_capture_exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;raise&lt;/code&gt; stays outside the &lt;code&gt;with&lt;/code&gt;, so the original exception always propagates. I verified it by forcing &lt;code&gt;_capture_exception&lt;/code&gt; to throw and checking the user's &lt;code&gt;ValueError&lt;/code&gt; still came through on both MCP versions.&lt;/p&gt;

&lt;h3&gt;
  
  
  One thing I found and did not fix
&lt;/h3&gt;

&lt;p&gt;While extending the tests to FastMCP, one environment failed differently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AssertionError: assert 'logging' == 'mcp'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;fastmcp==4.0.0b2&lt;/code&gt; + &lt;code&gt;mcp==2.0.0&lt;/code&gt;, a failing tool &lt;strong&gt;never reaches any MCP capture site&lt;/strong&gt; — I measured it, zero of the six executed. FastMCP catches and logs the error itself, and the only reason Sentry sees it at all is &lt;code&gt;LoggingIntegration&lt;/code&gt; picking up that log line.&lt;/p&gt;

&lt;p&gt;Which means &lt;code&gt;test_fastmcp_tool_with_error&lt;/code&gt; isn't testing MCP error capture on that environment. It passes by accident, and its &lt;code&gt;assert len(error_events) &amp;gt;= 1&lt;/code&gt; hides it.&lt;/p&gt;

&lt;p&gt;I left &lt;code&gt;test_fastmcp.py&lt;/code&gt; alone. Adding the mechanism assertions there would go permanently red on that env for a cause unrelated to this bug. It's documented in the PR as a separate finding.&lt;/p&gt;

&lt;p&gt;That's the decision I'd defend hardest: a static reading said "FastMCP routes through MCP, so it gets the mechanism." The measurement said otherwise, and the measurement wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Use of Sentry
&lt;/h2&gt;

&lt;p&gt;I instrumented before fixing, so the broken state is on the record.&lt;/p&gt;

&lt;p&gt;The demo runs a real MCP server whose &lt;code&gt;lookup_order&lt;/code&gt; tool fails with &lt;code&gt;ValueError: upstream order service returned 503&lt;/code&gt;. I sent one event with the fix stashed (&lt;code&gt;release: before-fix&lt;/code&gt;) and one with it applied (&lt;code&gt;release: after-fix&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Both land in the same issue with the same stack trace, which makes the comparison read itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before&lt;/strong&gt; — Sentry renders the mechanism right under the exception:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9k46gfy9onxlo7dxyda5.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9k46gfy9onxlo7dxyda5.jpg" alt="Sentry showing the exception with mechanism generic and handled true"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After&lt;/strong&gt; — same error, same place in the UI:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi6fx0qx4k9s8xxxzod6c.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi6fx0qx4k9s8xxxzod6c.jpg" alt="Sentry showing the same exception with mechanism mcp and handled false"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The contrast that makes the case.&lt;/strong&gt; The same trace, two panels of it.&lt;/p&gt;

&lt;p&gt;The span the failing tool produced:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwhxazrpmdsh97767g106.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwhxazrpmdsh97767g106.jpg" alt="Sentry trace preview showing the mcp.server span for tools/call lookup_order"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And that span's own metadata:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwdqcgcxnkmju1udvqlyy.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwdqcgcxnkmju1udvqlyy.jpg" alt="Sentry trace details showing Operation Name mcp.server and Origin auto.ai.mcp"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The instrumentation always knew how to label its &lt;strong&gt;spans&lt;/strong&gt;. &lt;code&gt;auto.ai.mcp&lt;/code&gt; was sitting right there the whole time, on the very same tool call that produced the error. Its &lt;strong&gt;errors&lt;/strong&gt; went out anonymous. The fix just brings them up to the standard the spans already met.&lt;/p&gt;

&lt;p&gt;The issue view tells the story on its own too: &lt;code&gt;First seen in release before-fix&lt;/code&gt;, &lt;code&gt;Last seen in release after-fix&lt;/code&gt;, and the &lt;strong&gt;Unhandled&lt;/strong&gt; badge that only appears once &lt;code&gt;handled&lt;/code&gt; is correct.&lt;/p&gt;

&lt;p&gt;Sentry credits for the challenge: code &lt;code&gt;bugsmash26&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Use of Google AI
&lt;/h2&gt;

&lt;p&gt;I used Gemini during the investigation, mainly to pressure-test my own reading of the code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mapping the capture paths.&lt;/strong&gt; &lt;code&gt;mcp.py&lt;/code&gt; is ~1,100 lines with two parallel instrumentation strategies for &lt;code&gt;mcp&lt;/code&gt; v1 and v2. I had Gemini trace which functions own each capture site and which version reaches them, then verified the answer with &lt;code&gt;--cov&lt;/code&gt; rather than trusting it. The map was right; the measurement is what made it evidence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Challenging the &lt;code&gt;handled&lt;/code&gt; call.&lt;/strong&gt; I asked it to argue &lt;em&gt;against&lt;/em&gt; &lt;code&gt;handled=False&lt;/code&gt; — the strongest case being that MCP v1 turns a tool exception into an &lt;code&gt;isError&lt;/code&gt; result the host never sees, so arguably it &lt;em&gt;was&lt;/em&gt; handled. That's what sent me to &lt;code&gt;flask.py:240&lt;/code&gt; for the precedent, and it's why that decision is argued in the PR instead of asserted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edge cases for the test list.&lt;/strong&gt; Chained exceptions, &lt;code&gt;ExceptionGroup&lt;/code&gt; under &lt;code&gt;anyio&lt;/code&gt; task groups, errors with no active transaction. Most turned out to be already covered or out of scope, but the FastMCP question came out of that pass — and that's the one that found something real.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I'd tell the next person
&lt;/h2&gt;

&lt;p&gt;The valuable part wasn't the fix. Thirty lines, and the pattern was already in the repo eight times over.&lt;/p&gt;

&lt;p&gt;The valuable part was refusing to trust three things: the issue title (the mechanism wasn't missing, it was wrong), my own coverage assumption (measured it instead), and my own patch (a reviewer pass caught that I'd dropped a safety net the original code had).&lt;/p&gt;

&lt;p&gt;Two of the three findings that matter here weren't in the bug report at all.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
    </item>
  </channel>
</rss>
