fix(server-utils): Stop reporting caller-handled errors from the OpenAI, Anthropic & Google GenAI instrumentations - #23024
Conversation
|
👋 @isaacs, @mydea, @nicohrubec, @andreiborza, @getsentry/team-javascript-sdks — Please review this PR when you get a chance! |
c7facfc to
4c52da8
Compare
|
👋 @isaacs, @mydea, @nicohrubec, @andreiborza, @getsentry/team-javascript-sdks — Please review this PR when you get a chance! |
… instrumentation level The exported instrumentOpenAiClient, instrumentAnthropicAiClient and instrumentGoogleGenAIClient wrappers captured provider errors with mechanism.handled = false and then rethrew, so the SDK classified the error as an unhandled crash before the application's retry or fallback logic ran. A call that succeeded on retry still produced an unhandled event, and each retry produced another one. Applies the convention established for the channel-based OpenAI integration in getsentry#21877 to the manual client instrumentation, which is the only available path on the edge and serverless runtimes. Error span status and the original error identity are unchanged. The anthropic suite asserted this divergence directly, expecting the model-error event only while orchestrion was disabled. Both paths agree now, so that branch and its expectation are gone. Captures are kept where a provider reports an error as data on an otherwise successful call, since the caller never sees those as a thrown error. The AI integration suites no longer mask these events with .ignore('event'), so they fail if the capture returns. Dropping the capture also left the .catch() in createWithResponseWrapper rethrowing into a promise nothing observes, which is not a handler at all: awaiting the two promises in sequence orphans the second whenever the first rejects. They are awaited together now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Removing `captureException` put the import at 111 chars, so oxfmt collapsed it to a single line. Pin the previous layout with `oxfmt-ignore` per review feedback. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
31e179c to
28d6e59
Compare
|
👋 @isaacs, @mydea, @nicohrubec, @andreiborza, @getsentry/team-javascript-sdks — Please review this PR when you get a chance! |
2 similar comments
|
👋 @isaacs, @mydea, @nicohrubec, @andreiborza, @getsentry/team-javascript-sdks — Please review this PR when you get a chance! |
|
👋 @isaacs, @mydea, @nicohrubec, @andreiborza, @getsentry/team-javascript-sdks — Please review this PR when you get a chance! |
# Conflicts: # dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts
0af60ab to
d08b788
Compare
Anthropic stream errors — connection drops and `error`-type stream events — are
surfaced to the caller (the async iterator rejects, or their `error` listener
fires), so recording them as `handled: false` at the instrumentation level makes
the same premature "unhandled crash" decision that was fixed for the client
wrappers. Mark the span failed and leave the error for the caller.
Also assert the genuine error-as-data captures instead of masking them with
`.ignore('event')`: Google GenAI blocked content and Anthropic error-shaped
responses are returned as data on a resolved value and never thrown, so the
instrumentation intentionally captures them. Those cases now have explicit event
assertions, and the synthetic captureException-mock unit test is dropped in
favour of this integration coverage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 41f07bc. Configure here.
andreiborza
left a comment
There was a problem hiding this comment.
LGTM, thanks for raising and fixing this!
LangChain's callback error handlers (handleLLMError, handleChainError, handleToolError) and the embeddings wrapper called captureException with handled: false. These errors reject invoke()/embedQuery() to the caller, so the instrumentation recorded an "unhandled crash" for an error the application handles, the same issue fixed for the OpenAI, Anthropic and Google GenAI client wrappers in #23024. The span is still marked failed and the error still propagates; only the event goes away. The integration tests masked these with .ignore('event'); those masks are removed so the tests no longer hide instrumentation-level captures. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
LangGraph's compiled-graph invoke and tool-node wrappers called captureException with handled: false and then rethrew. The error rejects invoke() to the caller, so the instrumentation recorded an "unhandled crash" for an error the application handles, the same issue fixed for the client wrappers (#23024) and LangChain. The span is still marked failed and the error still propagates; only the event goes away. The integration tests masked these with .ignore('event'); those masks are removed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…g them
Attaching the MessageStream `error` listener stops the error being raised as an
unhandled rejection, so not capturing it swallows the error for callers that
don't await or iterate the stream. Restore the capture in the stream.on('error')
handler.
The type: 'error' stream-event path (isErrorEvent) stays uncaptured: the SDK
throws that to the caller and never delivers it as a stream event, so it is a
caller-handled error rather than error-as-data.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
LangGraph's compiled-graph invoke and tool-node wrappers called captureException with handled: false and then rethrew. The error rejects invoke() to the caller, so the instrumentation recorded an "unhandled crash" for an error the application handles, the same issue fixed for the client wrappers (#23024) and LangChain. The span is still marked failed and the error still propagates; only the event goes away. The integration tests masked these with .ignore('event'); those masks are removed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
) LangChain's callback error handlers (`handleLLMError`, `handleChainError`, `handleToolError`) and the embeddings wrapper called `captureException(error, { handled: false })`. These errors reject `invoke()`/`embedQuery()` to the caller, so the instrumentation recorded an "unhandled crash" for an error the application handles. This is the same problem fixed for the OpenAI, Anthropic, and Google GenAI client wrappers in #23024. Related to #23023 --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
) LangGraph's compiled-graph invoke wrapper and tool-node wrapper called `captureException(error, { handled: false })` and then rethrew. The error rejects `invoke()` to the caller, so the instrumentation recorded an "unhandled crash" for an error the application handles. This is the same problem fixed for the OpenAI, Anthropic, and Google GenAI client wrappers in #23024, and for LangChain in #23593. Related to #23023 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…Anthropic & Google GenAI instrumentations (#23638) Backport of: #23024 ## Differences to the original PR - On v10 the AI instrumentation lives in `packages/core/src/tracing/` (it moved to `@sentry/server-utils` on v11), so the same change is applied there with v10's internal imports and `GEN_AI_*_ATTRIBUTE` names. - Anthropic test only: v10 also captured the handled `error-model` rejection on the non-orchestrion path, so its tests asserted that event. The fix stops that capture, so those `EXPECTED_MODEL_ERROR` expectations are removed. v10's existing truncation test is kept next to the new error-shaped-response test. Co-authored-by: Abhilaksh <abhilaksh@nox.today>
… fixes (#23641) Adds an Important Changes changelog entry explaining that the AI integrations no longer capture provider errors thrown back to the caller, and credits @lux-in-tenebris-lucet for the original contribution (#23024).
Two conflicts, both in the google-genai instrumentation that develop moved under us. packages/server-utils/src/ai/google-genai/index.ts: develop added low-cardinality span names when span streaming is on (getsentry#23573) and dropped the captureException calls from the instrumentation (getsentry#23024), touching the same lines where this branch threads the chat create-time params through. Kept both: the merged apply() computes attributeParams via mergeChatCreateParams, feeds those to extractRequestAttributes plus addPrivateRequestAttributes, then derives model with develop's || 'unknown' and its spanName sentinel logic. packages/server-utils/test/ai/lib/tracing/google-genai.test.ts: add/add, develop landed a span-names suite at the same path this branch used for the config suite. Took develop's file whole and re-added the config suite beside it, hoisting the shared setupClient to module scope and switching the config assertions from spanToJSON to spanToStaticSpanJSON, since spanToJSON now returns the streamed shape (attributes, no data). Also updated the Cloudflare integration test. Its chat span assertion matches attributes exhaustively with toEqual, while that scenario passes a config to chats.create, so propagating it adds temperature, top_p and max_tokens to the chat span. Verified the assertion fails without those three keys and passes with them. The node suites assert per key, so they were unaffected. Verified: @sentry/server-utils 432 tests passing (44 files), node google-genai and google-genai-v2 integration 24 passing, cloudflare google-genai integration 1 passing, oxfmt --check, oxlint --type-aware, oxlint src --type-aware --type-check. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

The exported AI client instrumentations (
instrumentOpenAiClient,instrumentAnthropicAiClient,instrumentGoogleGenAIClient) calledcaptureException(error, { handled: false })and rethrew. The wrapper runs inside the provider call, before any caller retry or fallback, so it reports an "unhandled crash" for an error the app may handle. A 429 the app retried and recovered from got reported as a crash, once per attempt.This drops the capture everywhere the error reaches the caller, keeping it only where the provider returns the error as data on a resolved value (Anthropic error-shaped 200 responses, Google GenAI blocked content). The span is still marked
internal_errorand the error still propagates, so only the event goes away, leaving thehandleddecision to the application boundary.This also covers the Anthropic streaming paths (
stream.on('error')anderror-type events), which surfaced to the caller but were still captured. LangChain and LangGraph have the same problem in their callback handlers, which will be handled in a follow-up.The suites previously masked these events with
.ignore('event'); those are replaced with explicit expectations (no event on caller-handled paths, event still captured on error-as-data paths), and the syntheticcaptureException-mock unit test is removed in favour of that coverage.Fixes #23023