Repository navigation
AI client instrumentation reports caller-handled provider errors as unhandled, once per retry #23023
Description
Is there an existing issue for this?
- Searched open and closed issues; the closest is Ensure SDK-captured error events have a proper
mechanism#17212 (mechanism consistency), which is closed and does not cover the AI integrations.
How do you use Sentry?
Sentry Saas (sentry.io)
Which SDK are you using?
@sentry/node
SDK Version
10.69.0
Framework Version
Node 24, no framework
Link to Sentry event
n/a, reproduced locally with a recording transport
Reproduction Example/SDK Setup
import * as Sentry from '@sentry/node';
Sentry.init({ dsn: '...', tracesSampleRate: 1 });
const client = Sentry.instrumentOpenAiClient(openai);
// Ordinary application-level retry
async function withRetry(fn, attempts) {
let lastError;
for (let i = 0; i < attempts; i++) {
try { return await fn(); } catch (error) { lastError = error; }
}
throw lastError;
}
// First attempt 429s, second succeeds.
await withRetry(() => client.chat.completions.create(request), 3);Steps to Reproduce
- Instrument an OpenAI, Anthropic, or Google GenAI client with the exported
instrument*Clientfunction. - Make a call that fails once and succeeds on retry.
- Observe the events sent to Sentry.
Running the above against 10.69.0 with a recording transport:
A. transient failure, retry succeeds
outcome: recovered
events sent: 1
- "Rate limit exceeded" handled=false type=auto.ai.openai
B. all attempts fail
outcome: exhausted
events sent: 3
- "Rate limit exceeded" handled=false type=auto.ai.openai
- "Rate limit exceeded" handled=false type=auto.ai.openai
- "Rate limit exceeded" handled=false type=auto.ai.openai
All three exported client instrumentations behave the same way:
openai outcome=recovered events=1 handled=false (auto.ai.openai)
anthropic outcome=recovered events=1 handled=false (auto.ai.anthropic)
google-genai outcome=recovered events=1 handled=false (auto.ai.google_genai)
Expected Result
No error event for a call the application recovered from, and at most one event for a call that ultimately fails, carrying the caller's own handled status.
Actual Result
The instrumentation calls captureException(error, { mechanism: { handled: false } }) and then rethrows, so it decides the error is an unhandled crash before any application retry or fallback logic runs. Because the capture happens inside the wrapped method, every retry produces another event.
Concretely, this means:
- A transient failure that the application successfully recovered from still appears in Sentry as an unhandled crash.
- A request retried N times produces N duplicate events.
- When the call really is fatal, the application's own boundary captures it too, so the same failure is recorded twice with conflicting
handledvalues.dedupeIntegrationis not in the Node defaults, so both land.
The affected call sites all rethrow to the caller:
| File | Mechanism type |
|---|---|
ai/openai/index.ts (streaming + non-streaming) |
auto.ai.openai.stream, auto.ai.openai |
ai/anthropic-ai/index.ts (streaming + non-streaming) |
auto.ai.anthropic |
ai/google-genai/index.ts (streaming + non-streaming) |
auto.ai.google_genai |
ai/core/utils.ts (.withResponse() wrapper) |
caller-supplied |
These are reachable through instrumentOpenAiClient, instrumentAnthropicAiClient and instrumentGoogleGenAIClient, which are exported from @sentry/node, @sentry/bun, @sentry/cloudflare, @sentry/vercel-edge, @sentry/aws-serverless, @sentry/google-cloud-serverless and @sentry/elysia. On the edge and serverless runtimes this is the only available instrumentation path.
The AI integration tests currently mask this: the OpenAI scenario catches two provider errors with the comment // Error is expected and handled, while the test runner drops the resulting events via .ignore('event').
Impact
handled feeds release health, so the effect lands on numbers teams act on. An unhandled error marks the session crashed, so a retried 429 costs the same crash-free rate as a real crash. Deploy during a provider blip and release health flags the new version as a regression. Unhandled issues are also auto-prioritized, so recovered failures outrank real bugs in triage, and volume-threshold alerts fire on retry storms that never reached a user.
A provider outage compounds it. One retried request bills 3 to 5 events instead of 0, and orgs near quota start dropping real errors, so an OpenAI incident costs visibility into unrelated production bugs.
AI apps hit this harder than other integrations because the multipliers are standard practice: retry with backoff, fallback chains across models and vendors, agent loops making 10 to 30 calls per user action, and errors that are ordinary control flow rather than faults (context_length_exceeded handled by truncating and retrying, or a content-policy refusal rendered as a normal message).
The workaround today is to filter the auto-instrumented event and capture explicitly at the application boundary, which is what we're doing. The other option teams reach for is dropping the AI integration to stop the false crash reports, which gives up the gen-AI spans and token accounting with it.
Suggested direction
This looks like the same call that was already made for the channel-based OpenAI integration in #21877, whose merge commit reads:
Let errors bubble to the global handlers instead of capturing them as unhandled at the instrumentation level, matching the DB/cache channel subscribers.
Applying that convention to the exported client instrumentations would keep the error span status while leaving the handled decision to the application boundary that actually knows the outcome.
Worth keeping as-is, for contrast: the places where a provider reports an error as data rather than by rejecting. That covers Anthropic's error stream events and error-shaped responses, and Google GenAI's blocked-content signal. The caller never sees those as a thrown error, so dropping those captures would lose the signal entirely.
Happy to open a PR for this.
Metadata
Metadata
Assignees
Projects
- StatusShow more project fieldsNo status
Activity
linear-code commentedon Aug 4, 2026
JS-3275
Lms24 commentedon Aug 5, 2026
Hi @lux-in-tenebris-lucet thanks for writing in! We'll get the PR reviewed soon!
In the meantime, a word of advice for OSS contributions: Please do not overcrowd issues or PR descriptions with AI-generated text. I'd rather have a short summarized issue and PR description from a human, than 2 pages of AI content. Thanks for still using our bug template, this makes it a bit more digestable.
nicohrubec commentedon Aug 25, 2026
Hey, sorry for the delay. I reproduced this locally and the fix seems valid to me. I'll adjust your PR a bit and then get it merged!
fix(server-utils): Don't capture caller-handled LangChain errors (#23593
fix(server-utils): Don't capture caller-handled LangGraph errors (#23594
fix(server-utils): Stop reporting caller-handled errors from the Open…
github-actions commentedon Sep 23, 2026
A PR closing this issue has just been released 🚀
This issue was referenced by PR #23024, which was included in the 11.0.0 release.