Skip to content

fix(node): Use context-scoped suppression for LangChain + AI provider span deduplication#22366

Open
nicohrubec wants to merge 2 commits into
developfrom
feat/langchain-scoped-ai-suppression
Open

fix(node): Use context-scoped suppression for LangChain + AI provider span deduplication#22366
nicohrubec wants to merge 2 commits into
developfrom
feat/langchain-scoped-ai-suppression

Conversation

@nicohrubec

@nicohrubec nicohrubec commented Jul 17, 2026

Copy link
Copy Markdown
Member

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

Comment thread packages/node/src/integrations/tracing/langchain/instrumentation.ts
Comment thread packages/core/src/tracing/ai/suppression.ts
@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size % Change Change
@sentry/browser 27.75 kB - -
@sentry/browser - with treeshaking flags 26.18 kB - -
@sentry/browser (incl. Tracing) 46.56 kB - -
@sentry/browser (incl. Tracing + Span Streaming) 48.37 kB - -
@sentry/browser (incl. Tracing, Profiling) 51.36 kB - -
@sentry/browser (incl. Tracing, Replay) 85.84 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 75.46 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 90.55 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 103.21 kB - -
@sentry/browser (incl. Feedback) 44.93 kB - -
@sentry/browser (incl. sendFeedback) 32.55 kB - -
@sentry/browser (incl. FeedbackAsync) 37.69 kB - -
@sentry/browser (incl. Metrics) 28.83 kB - -
@sentry/browser (incl. Logs) 29.05 kB - -
@sentry/browser (incl. Metrics & Logs) 29.76 kB - -
@sentry/react 29.54 kB - -
@sentry/react (incl. Tracing) 48.84 kB - -
@sentry/vue 33.17 kB - -
@sentry/vue (incl. Tracing) 48.52 kB - -
@sentry/svelte 27.78 kB - -
CDN Bundle 30.14 kB - -
CDN Bundle (incl. Tracing) 48.53 kB - -
CDN Bundle (incl. Logs, Metrics) 31.73 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 49.83 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 70.99 kB - -
CDN Bundle (incl. Tracing, Replay) 86.05 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 87.35 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 91.85 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 93.12 kB - -
CDN Bundle - uncompressed 89.88 kB - -
CDN Bundle (incl. Tracing) - uncompressed 146.73 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 94.59 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 150.71 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 219.35 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 265.96 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 269.93 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 279.67 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 283.62 kB - -
@sentry/nextjs (client) 51.36 kB - -
@sentry/sveltekit (client) 46.99 kB - -
@sentry/core/server 80.37 kB +0.15% +117 B 🔺
@sentry/core/browser 66.79 kB +0.15% +99 B 🔺
@sentry/node-core 63.21 kB - -
@sentry/node 125.7 kB -0.04% -39 B 🔽
@sentry/node (incl. diagnostics channel injection) 149.53 kB +0.01% +12 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 70.03 kB -0.01% -2 B 🔽
@sentry/node/light 51.32 kB -0.01% -1 B 🔽
@sentry/node - without tracing 74.92 kB - -
@sentry/aws-serverless 84.15 kB - -
@sentry/cloudflare (withSentry) - minified 197.51 kB - -
@sentry/cloudflare (withSentry) 485.87 kB - -

View base workflow run

@nicohrubec
nicohrubec marked this pull request as ready for review July 17, 2026 14:21
@nicohrubec
nicohrubec requested review from a team as code owners July 17, 2026 14:21
@nicohrubec
nicohrubec requested review from andreiborza and isaacs and removed request for a team July 17, 2026 14:21
@nicohrubec
nicohrubec marked this pull request as draft July 17, 2026 15:24

@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.

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.

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.

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?

Suggested change
* 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

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.

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

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 one, too. Not openai?

}

/** @inheritDoc */
protected _setupIntegrations(): void {

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.

Nice. Removing this is a good cleanup.

@nicohrubec
nicohrubec marked this pull request as ready for review July 21, 2026 08:17
@nicohrubec

Copy link
Copy Markdown
Member Author

I noticed that this solution doesn't work as is for the orchestrion path. I will fix that in a follow up but need to put some of the global skip logic for that one back in for now.

@nicohrubec
nicohrubec force-pushed the feat/langchain-scoped-ai-suppression branch from f522c23 to 099efb6 Compare July 21, 2026 08:41

@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.

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 099efb6. Configure here.

nicohrubec and others added 2 commits July 21, 2026 10:56
… span deduplication

Replaces the global AI-provider skip flag with context-scoped suppression for the
OTel LangChain path: LangChain invoke/stream/batch (and embeddings) run inside
`_INTERNAL_withSuppressedAiProviderSpans`, and the core provider instrumentations
skip span creation when `_INTERNAL_isAiProviderSpanSuppressed()`. Direct provider
SDK calls outside LangChain keep their spans, fixing globally-dropped provider spans.

The orchestrion provider channels continue to use the global skip flag for now; a
follow-up converts them to scoped suppression.

Fixes #19687

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@nicohrubec
nicohrubec force-pushed the feat/langchain-scoped-ai-suppression branch from 099efb6 to 7eed185 Compare July 21, 2026 08:56
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.

AI provider spans are suppressed globally when LangChain integration is active

2 participants