Skip to content

Conversation

andreiborza
Copy link
Member

@andreiborza andreiborza commented Sep 24, 2025

Previously, we completely walked over anthropic's SDK and replaced message.stream with our own method that returns an async generator. This breaks the SDK as MessageStream has further user callable api, such as adding event handlers.

This fix proxies message.stream instead of replacing it with our own method. Instead of returning an async generator, we now hook into various events to do our instrumentation.

Streams requested via stream: true are expected to return async generators, so the current approach still holds, the only change is that we proxy instead of overwrite.

Fixes: #17734

@andreiborza andreiborza requested a review from mydea September 24, 2025 12:41
cursor[bot]

This comment was marked as outdated.

Copy link
Contributor

github-actions bot commented Sep 24, 2025

size-limit report 📦

⚠️ Warning: Base artifact is not the latest one, because the latest workflow run is not done yet. This may lead to incorrect results. Try to re-run all tests to get up to date results.

Path Size % Change Change
@sentry/browser 24.26 kB - -
@sentry/browser - with treeshaking flags 22.77 kB - -
@sentry/browser (incl. Tracing) 40.43 kB - -
@sentry/browser (incl. Tracing, Replay) 78.81 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 68.48 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 83.48 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 95.69 kB - -
@sentry/browser (incl. Feedback) 40.97 kB - -
@sentry/browser (incl. sendFeedback) 28.91 kB - -
@sentry/browser (incl. FeedbackAsync) 33.84 kB - -
@sentry/react 25.98 kB - -
@sentry/react (incl. Tracing) 42.41 kB - -
@sentry/vue 28.78 kB - -
@sentry/vue (incl. Tracing) 42.24 kB - -
@sentry/svelte 24.29 kB - -
CDN Bundle 25.77 kB - -
CDN Bundle (incl. Tracing) 40.33 kB - -
CDN Bundle (incl. Tracing, Replay) 76.56 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 82.05 kB - -
CDN Bundle - uncompressed 75.37 kB - -
CDN Bundle (incl. Tracing) - uncompressed 119.39 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 234.51 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 247.27 kB - -
@sentry/nextjs (client) 44.42 kB - -
@sentry/sveltekit (client) 40.85 kB - -
@sentry/node-core 50.02 kB -0.01% -1 B 🔽
@sentry/node 153.06 kB +0.15% +228 B 🔺
@sentry/node - without tracing 91.94 kB -0.01% -1 B 🔽
@sentry/aws-serverless 105.39 kB -0.01% -1 B 🔽

View base workflow run

Copy link
Contributor

github-actions bot commented Sep 24, 2025

node-overhead report 🧳

Note: This is a synthetic benchmark with a minimal express app and does not necessarily reflect the real-world performance impact in an application.
⚠️ Warning: Base artifact is not the latest one, because the latest workflow run is not done yet. This may lead to incorrect results. Try to re-run all tests to get up to date results.

Scenario Requests/s % of Baseline Prev. Requests/s Change %
GET Baseline 8,877 - 9,257 -4%
GET With Sentry 1,383 16% 1,365 +1%
GET With Sentry (error only) 6,060 68% 6,221 -3%
POST Baseline 1,190 - 1,215 -2%
POST With Sentry 507 43% 553 -8%
POST With Sentry (error only) 1,051 88% 1,068 -2%
MYSQL Baseline 3,304 - 3,342 -1%
MYSQL With Sentry 439 13% 444 -1%
MYSQL With Sentry (error only) 2,671 81% 2,719 -2%

View base workflow run

if (isStreamRequested || isStreamingMethod) {
return startSpanManual(
if (isStreamRequested || isStreamingMethod) {
const messageStream = target.apply(context, args);
Copy link
Member

Choose a reason for hiding this comment

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

m: I think (this is also what cursor is raising I believe) that this should move into the startSpanManual callback, so this is properly encapsulated by that span.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah yea, this should move down. Thanks.

},
},
});
span.end();
Copy link
Member

Choose a reason for hiding this comment

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

l/m: We should check if the span is not yet ended here, because it could be ended already from the main code path and throw afterwards in theory, I suppose?

*/

function processEvent(
export function processEvent(
Copy link
Member

Choose a reason for hiding this comment

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

does this need to be exported?

Copy link
Member Author

Choose a reason for hiding this comment

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

Nope, leftover. Removed.

*/

interface StreamingState {
export interface StreamingState {
Copy link
Member

Choose a reason for hiding this comment

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

does this need to be exported?

Copy link
Member Author

Choose a reason for hiding this comment

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

Nope, leftover from a previous approach 😅

* each event from the input stream unchanged.
* Finalizes span attributes when stream processing completes
*/
export function finalizeStreamSpan(state: StreamingState, span: Span, recordOutputs: boolean): void {
Copy link
Member

Choose a reason for hiding this comment

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

does this need to be exported?

Copy link
Member Author

Choose a reason for hiding this comment

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

Nope, leftover. Removed.

cursor[bot]

This comment was marked as outdated.

cursor[bot]

This comment was marked as outdated.

cursor[bot]

This comment was marked as outdated.

@andreiborza andreiborza requested a review from mydea September 25, 2025 07:55
});
}

if (span.isRecording()) {
Copy link
Member

Choose a reason for hiding this comment

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

l: We could also just check isRecording() at the top of the function and early return if not, setting attributes is a noop in that case anyhow :)

Copy link
Member Author

Choose a reason for hiding this comment

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

Updated, thanks!

Copy link
Member

@mydea mydea left a comment

Choose a reason for hiding this comment

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

nice, great catch and improvement here that there are these two paths, and nice tests 🚀

cursor[bot]

This comment was marked as outdated.

@andreiborza andreiborza force-pushed the ab/fix-anthropic-streaming branch from 9bf91c6 to 894aa11 Compare September 25, 2025 11:42
op: 'gen_ai.messages',
origin: 'auto.ai.anthropic',
status: 'ok',
}),
Copy link

Choose a reason for hiding this comment

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

Bug: Stream Attribute Mismatch in Test

The test for the messages.stream span incorrectly expects gen_ai.request.stream: true. This attribute is only set when an explicit stream parameter is passed, which messages.stream does not use as it's inherently streaming. The instrumentation correctly reflects this by omitting the attribute for messages.stream calls.

Fix in Cursor Fix in Web

Copy link
Member Author

Choose a reason for hiding this comment

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

Not true, we add this in both cases.

@andreiborza andreiborza merged commit f4df972 into develop Sep 25, 2025
188 checks passed
@andreiborza andreiborza deleted the ab/fix-anthropic-streaming branch September 25, 2025 13:46
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 message stream events unhandled
2 participants