fix(node): Use context-scoped suppression for LangChain + AI provider span deduplication#22366
fix(node): Use context-scoped suppression for LangChain + AI provider span deduplication#22366nicohrubec wants to merge 3 commits into
Conversation
… span deduplication 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.
Reviewed by Cursor Bugbot for commit c96914f. Configure here.
size-limit report 📦
|
Adapts the add-ai-integration skill to require lowest-level provider integrations to check _INTERNAL_isAiProviderSpanSuppressed(), and adds a Bugbot rule to lint for missing suppression checks / global suppression. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
isaacs
left a comment
There was a problem hiding this comment.
LGTM! Some tiny nits / suggestions for a doc comment, but an improvement for sure.
| /** | ||
| * Execute a callback with AI provider spans suppressed in the current scope. | ||
| * This is used by higher-level integrations (like LangChain) to prevent | ||
| * duplicate spans from underlying AI provider instrumentations. |
There was a problem hiding this comment.
low: I think this approach means that anything that calls getCurrentScope() will get this child scope instead of the original? It seems like it might have side effects if anything in the callback calls Sentry.setTag/setContext/setUser, since it'll be attached to a discarded scope.
I don't really see a way around it, tbh, but maybe a doc comment could be worthwhile here?
| * duplicate spans from underlying AI provider instrumentations. | |
| * duplicate spans from underlying AI provider instrumentations. | |
| * | |
| * Suppression rides on a forked scope (as `suppressTracing` does) because a | |
| * forked scope is the only Sentry-native carrier that both survives async | |
| * boundaries and stays isolated from concurrent, unrelated work. | |
| * | |
| * Trade-off: current-scope mutations made inside `callback` land on the fork | |
| * and do not propagate to the outer scope. |
| // EDGE CASE: Import and instantiate Anthropic client BEFORE LangChain is imported | ||
| // This simulates the timing issue where a user creates an Anthropic client in one file | ||
| // before importing LangChain in another file | ||
| // Direct Anthropic call made BEFORE LangChain is imported/used |
There was a problem hiding this comment.
nit: this file is named scenario-openai-before-langchain.mjs but it is using anthropic? Should it be renamed scenario-anthropic-before-langchain.mjs? It doesn't change it functionally, of course, but just seems a little weird 😅
| // EDGE CASE: Import and instantiate Anthropic client BEFORE LangChain is imported | ||
| // This simulates the timing issue where a user creates an Anthropic client in one file | ||
| // before importing LangChain in another file | ||
| // Direct Anthropic call made BEFORE LangChain is imported/used |
| } | ||
|
|
||
| /** @inheritDoc */ | ||
| protected _setupIntegrations(): void { |
There was a problem hiding this comment.
Nice. Removing this is a good cleanup.

When both LangChain and an AI provider integration (OpenAI, Anthropic, Google GenAI) are active, a single LangChain invoke() call produces duplicate gen_ai.chat spans — one from LangChain's callback handler and one from the underlying provider instrumentation.
The previous fix globally disabled provider instrumentations when LangChain was detected, but this broke direct provider SDK calls unrelated to LangChain.
This PR sets a suppression flag on the current scope during LangChain invoke/stream/batch execution. Provider instrumentations check this flag and skip span creation if set.
Closes #19687