Skip to content

fix(server-utils): Record Anthropic chat spans for thread and tool-runner streams on 1.2+ - #25931

Merged
RulaKhaled merged 5 commits into
developfrom
fix/anthropic-1x-thread-chat-spans
Nov 10, 2026
Merged

RulaKhaled merged 5 commits into
developfrom
fix/anthropic-1x-thread-chat-spans

Conversation

@RulaKhaled

@RulaKhaled RulaKhaled commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator

On @anthropic-ai/sdk 1.2.0+, streamed turns from beta.messages.toolRunner({ stream: true }) and beta.messages.thread().stream() had no gen_ai.chat spans. Every tool runner runs over a thread since 1.2.0, including on Bedrock and Vertex.

The thread calls beta.messages.create(), which opens our span, but parses the response through a new APIPromise our hook never sees, so the span never ended. This hooks BetaMessageStream._start(), matches each request to its stream by abort signal, and ends the span from the stream's events with usage and output.

instrumentMessageStream now also ends the span on end, so aborted or truncated streams, messages.stream() included, no longer leave it open. An abort before the response is not marked as an error.

Fixes #25816

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

size-limit report 📦

Compared: 61349ce (PR test merge using develop at 2f3c8a9)

Against: 2f3c8a9 (develop) · base run #65503

✅ Baseline matches: this diff contains only this PR's changes.

Path Size % Change Change
@sentry/browser 29.92 kB - -
@sentry/browser - with treeshaking flags 28.08 kB - -
@sentry/browser - with treeshaking flags tracing without tracing 27.97 kB - -
@sentry/browser (incl. Tracing) 52.29 kB - -
@sentry/browser (incl. Tracing + Span Streaming) 52.3 kB - -
@sentry/browser (incl. Tracing, Profiling) 55.27 kB - -
@sentry/browser (incl. Tracing, Replay) 92.61 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 81.29 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 97.34 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 110.34 kB - -
@sentry/browser (incl. Feedback) 47.52 kB - -
@sentry/browser (incl. sendFeedback) 35.02 kB - -
@sentry/browser (incl. FeedbackAsync) 40.13 kB - -
@sentry/browser (incl. Metrics) 31.48 kB - -
@sentry/browser (incl. Logs) 31.77 kB - -
@sentry/browser (incl. Metrics & Logs) 32.45 kB - -
@sentry/react 31.78 kB - -
@sentry/react (incl. Tracing) 54.76 kB - -
@sentry/vue 38.33 kB - -
@sentry/vue (incl. Tracing) 55.33 kB - -
@sentry/svelte 29.95 kB - -
@sentry/remix (Remix 3 client bundle) 57.92 kB - -
CDN Bundle 31.72 kB - -
CDN Bundle (incl. Tracing) 52.89 kB - -
CDN Bundle (incl. Logs, Metrics) 34.52 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 55.39 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 75.9 kB - -
CDN Bundle (incl. Tracing, Replay) 91.06 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 93.62 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 97.19 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 99.8 kB - -
CDN Bundle - uncompressed 92.76 kB - -
CDN Bundle (incl. Tracing) - uncompressed 156.02 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 101.29 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 163.79 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 232.85 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 277.71 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 285.47 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 291.41 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 299.17 kB - -
@sentry/nextjs (client) 57.73 kB - -
@sentry/sveltekit (client) 52.82 kB - -
@sentry/core/server 44.29 kB - -
@sentry/core/browser 14.02 kB - -
@sentry/node 157.37 kB +0.17% +260 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 82.31 kB +0.05% +35 B 🔺
@sentry/node - without tracing 96.95 kB +0.07% +59 B 🔺
@sentry/node - without channel injection 137.65 kB +0.19% +259 B 🔺
@sentry/aws-serverless 105.62 kB +0.06% +61 B 🔺
@sentry/cloudflare (withSentry) - minified 203.34 kB - -
@sentry/cloudflare (withSentry) 505.12 kB - -
@sentry/nextjs/cloudflare (withSentry) - minified 225.19 kB - -

View base workflow run

@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Optional E2E tests

Some optional checks were not successful. They do not block the merge. 6 not successful, 49 successful. See the run.

Check Status
❌ CI: Optional E2E Tests / E2E nextjs-16-cacheComponents (cloudflare) Test (optional) Failing after 10m Details
❌ CI: Optional E2E Tests / E2E node-flue (latest) Test (optional) Failing after 29s Details
❌ CI: Optional E2E Tests / E2E node-eve (latest) Test (optional) Failing after 57s Details
❌ CI: Optional E2E Tests / E2E node-eve Test (optional) Failing after 2m Details
❌ CI: Optional E2E Tests / E2E solidstart-2 Test (optional) Failing after 45s Details
❌ CI: Optional E2E Tests / E2E node-eve (orchestrion) Test (optional) Failing after 2m Details
Successful checks
Check Status
✅ CI: Optional E2E Tests / E2E cloudflare-workers-send-to-sentry Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E nextjs-14 (latest) Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E cloudflare-flue-send-to-sentry Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E node-express-mcp-v1 Test (optional) Successful in 43s Details
✅ CI: Optional E2E Tests / E2E nextjs-16-tunnel (cloudflare) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E nextjs-15 (latest) Test (optional) Successful in 3m Details
✅ CI: Optional E2E Tests / E2E cloudflare-think (ai v6) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E gen-ai-libraries (cloudflare) Test (optional) Successful in 47s Details
✅ CI: Optional E2E Tests / E2E nextjs-orpc Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E nextjs-15 (turbo) Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E cloudflare-pi-durable Test (optional) Successful in 49s Details
✅ CI: Optional E2E Tests / E2E cloudflare-pi-durable (latest) Test (optional) Successful in 41s Details
✅ CI: Optional E2E Tests / E2E cloudflare-agent Test (optional) Successful in 43s Details
✅ CI: Optional E2E Tests / E2E nextjs-16 (vinext, bun) Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E gen-ai-libraries Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E cloudflare-think Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E nestjs-12 (latest) Test (optional) Successful in 45s Details
✅ CI: Optional E2E Tests / E2E debug-id-sourcemaps Test (optional) Successful in 47s Details
✅ CI: Optional E2E Tests / E2E astro-7-static Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E gen-ai-libraries (latest) Test (optional) Successful in 49s Details
✅ CI: Optional E2E Tests / E2E deno (latest) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E cloudflare-mastra Test (optional) Successful in 52s Details
✅ CI: Optional E2E Tests / E2E astro-7 Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E nuxt-5-sourcemaps (client source maps kept) Test (optional) Successful in 38s Details
✅ CI: Optional E2E Tests / E2E node-fastify-6 (tracer provider) Test (optional) Successful in 45s Details
✅ CI: Optional E2E Tests / E2E node-pi-durable Test (optional) Successful in 46s Details
✅ CI: Optional E2E Tests / E2E nextjs-16 (deno) Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E node-mastra Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E nextjs-16 (vinext) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E nuxt-5 (tunnel-dev) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E node-openai-decisions (latest) Test (optional) Successful in 30s Details
✅ CI: Optional E2E Tests / E2E node-flue Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E nuxt-5 Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E nuxt-5-sourcemaps Test (optional) Successful in 42s Details
✅ CI: Optional E2E Tests / E2E react-router-7-framework-instrumentation Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E tanstackstart-react-cloudflare Test (optional) Successful in 46s Details
✅ CI: Optional E2E Tests / E2E node-express-send-to-sentry Test (optional) Successful in 59s Details
✅ CI: Optional E2E Tests / E2E node-fastify-6 Test (optional) Successful in 36s Details
✅ CI: Optional E2E Tests / E2E nextjs-16 (cloudflare, latest) Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E node-express-mcp-v2 Test (optional) Successful in 33s Details
✅ CI: Optional E2E Tests / E2E node-express-send-to-sentry-static Test (optional) Successful in 54s Details
✅ CI: Optional E2E Tests / E2E tanstackstart-react (latest) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E node-pi-durable (latest) Test (optional) Successful in 38s Details
✅ CI: Optional E2E Tests / E2E node-mastra (latest) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E node-openai-decisions Test (optional) Successful in 28s Details
✅ CI: Optional E2E Tests / E2E tanstackstart-react-sourcemaps Test (optional) Successful in 34s Details
✅ CI: Optional E2E Tests / E2E nuxt-5-cloudflare Test (optional) Successful in 34s Details
✅ CI: Optional E2E Tests / E2E supabase-nextjs Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E nuxt-5-sourcemaps (static / nuxt generate) Test (optional) Successful in 43s Details

@RulaKhaled
RulaKhaled added this pull request to stack #25993 November 2, 2026 11:53
@RulaKhaled
RulaKhaled force-pushed the fix/anthropic-1x-thread-chat-spans branch 2 times, most recently from b953034 to 4198705 Compare November 2, 2026 14:30
@RulaKhaled

Copy link
Copy Markdown
Collaborator Author

bugbot run

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

@RulaKhaled

Copy link
Copy Markdown
Collaborator Author

bugbot run

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

@RulaKhaled
RulaKhaled marked this pull request as ready for review November 2, 2026 15:06
@RulaKhaled
RulaKhaled requested a review from a team as a code owner November 2, 2026 15:06
@RulaKhaled
RulaKhaled requested review from andreiborza, isaacs and nicohrubec and removed request for a team November 2, 2026 15:06
Base automatically changed from fix/anthropic-sdk-1x to develop November 4, 2026 13:35
@RulaKhaled
RulaKhaled requested review from a team as code owners November 4, 2026 13:35
@RulaKhaled
RulaKhaled requested review from Lms24, logaretm and s1gr1d and removed request for a team November 4, 2026 13:35
@RulaKhaled
RulaKhaled force-pushed the fix/anthropic-1x-thread-chat-spans branch 2 times, most recently from 7fe1266 to 2e6e182 Compare November 5, 2026 12:57
@RulaKhaled

Copy link
Copy Markdown
Collaborator Author

bugbot run

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

RulaKhaled and others added 3 commits November 10, 2026 11:58
…nner streams on 1.2+

On @anthropic-ai/sdk 1.2.0+, streamed turns from
beta.messages.toolRunner({ stream: true }) and beta.messages.thread().stream()
had no gen_ai.chat spans. The thread parses the response of each create() call
through a new APIPromise our hook never sees, so the span never ended. Hook
BetaMessageStream._start(), match each request to its stream by abort signal,
and end the span from the stream's events.

instrumentMessageStream now also ends the span on 'end', so aborted or truncated
streams no longer leave it open.

Fixes #25816

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A later create() sent with a finished thread stream's signal was wired to that
stream and its span never ended. Drop the entry once an attempt succeeds, since
only rejected attempts are retried. Also cover the 'end' listener with a unit
test and update the wrapStreamResult JSDoc.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Each mode and turn is now its own test.each case, with no conditionals, so a
failure points at one path. The cases share one scenario run per env, since
every run is a Node process.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A create() sent with the signal of a thread stream whose request failed was
still matched to that stream, so its span never ended. A finished stream sends
no more requests, so skip it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@isaacs isaacs left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should align with the changes in 26023 (either here or in a follow-up), and we should make a deliberate choice about what to do with recovered errors (even if that's just adding a test to pin the current behavior).


const { recordOutputs } = resolveAIRecordingOptions(options);
// Not `asResponse()`, which on 1.x also ends the SDK's own span when the user turned its tracing on.
void request._responseProps().then(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a little weird. An attempt that the thread recovers from can end as an error span, which is confusing.

_createWithRecovery() re-sends a turn when ThreadState.recoversFrom(error) is true (for example a lost thread, which triggers a replay). Each rejected attempt gets its own gen_ai.chat span, ended with end(error). So a turn that the SDK recovered from without the user's knowledge shows an errored chat span next to the successful one. That can be accurate (a real 4xx happened), but it is a product decision.

I created a test to debug it, and it shows 2 spans being received, plus the error event. If that's the way we want this to be represented, that's defensible, but probably worth having a test to pin it as an intentional choice: https://gist.github.com/isaacs/5539045945a6881fdaf99cb0a9281743

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's a great catch, I pinned it with a test and kept it on purpose. the rejected attempt was a real 4xx, same as an http.client span a retry recovers from. hiding it means copying the SDK's private recovery rules (only some codes, only on a continue), and a copy that drifts would hide real errors. didn't get an error event in my run though, only the span. which one did you see? 🤔

@isaacs
isaacs force-pushed the fix/anthropic-1x-thread-chat-spans branch from 5fcb668 to 7df1252 Compare November 10, 2026 12:05
@isaacs
isaacs requested a review from a team November 10, 2026 12:05

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

An aborted thread or tool-runner stream ended its chat span ok with nothing to
show it was cut short. Mark it cancelled, as Google GenAI does since #26023,
both mid-stream and before the response arrives. Also pin the error span of an
attempt the thread recovered from, which is deliberate.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 7c140a0. Configure here.

@RulaKhaled
RulaKhaled merged commit bcf7aea into develop Nov 10, 2026
337 of 343 checks passed
@RulaKhaled
RulaKhaled deleted the fix/anthropic-1x-thread-chat-spans branch November 10, 2026 17:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Anthropic tool runner and message threads drop gen_ai.chat spans on @anthropic-ai/sdk >=1.2.0

2 participants