fix(core): Instrument Anthropic client in place instead of via a deep proxy#22305
fix(core): Instrument Anthropic client in place instead of via a deep proxy#22305logaretm wants to merge 2 commits into
Conversation
… proxy The Anthropic integration wrapped the client in a deep Proxy that ran every method with the raw object as `this`. That changed the client's observable semantics: internal delegation (`messages.stream()` calling `this.create()`) resolved against the raw object, so it escaped any outer wrapper and behaved differently than an uninstrumented client. Instrument the registered methods in place instead (own properties shadowing the prototype), invoking them with the caller's `this`. Instrumentation is now observationally transparent: private-field access and internal delegation work as they do on a plain client. A re-entrancy guard keyed on the active streaming helper span keeps `stream()`'s internal `create()` call from producing a duplicate span.
size-limit report 📦
|
nicohrubec
left a comment
There was a problem hiding this comment.
were you able to reproduce the broken behavior? we use the same deep proxy pattern in openai and google genai, if this approach is really inherently broken we should of course change it but we should first ensure that we are actually fixing anything imo. do we know what specifically causes the issues?
|
@nicohrubec Yep, reproduced it with a simulation of what braintrust does. The cause is our deep proxy binds every method to the raw underlying object, so when This kinda allows us to get out of the way while keeping the same capabilities we had. The other two instrumentations don't use |
chargome
left a comment
There was a problem hiding this comment.
+1 for adding integration tests, otherwise LGTM
Adds an integration test asserting that instrumenting the Anthropic client does not hide its own methods from an outer wrapper: `messages.stream()` delegates to `create` through `this`, so a co-instrumenting wrapper must still observe that internal call. Fails against the previous deep-proxy implementation and passes with in-place instrumentation.
|
Added a few contrived tests since this is a bit unconventional case. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ 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 76874ee. Configure here.
| if (activeSpan && STREAMING_HELPER_SPANS.has(activeSpan)) { | ||
| return target.apply(invocationThis, args); | ||
| } | ||
| } |
There was a problem hiding this comment.
Stream dedup over-suppresses later calls
Medium Severity
The duplicate-span gate skips instrumentation whenever the active span is a streaming-helper span. On Node, that helper span stays active across MessageStream's async continuations via async context, so later non-streaming calls such as messages.create from stream event handlers can be skipped for the whole stream lifetime, not only the internal sync create delegation.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 76874ee. Configure here.


Instruments the Anthropic client's methods in place instead of wrapping the client in a deep
Proxy, so our instrumentation stops changing the client's observable behavior.closes #20291