-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
fix(node): Use context-scoped suppression for LangChain + AI provider span deduplication #22366
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -38,9 +38,7 @@ async function run() { | |
| const baseURL = `http://localhost:${server.address().port}`; | ||
|
|
||
| await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { | ||
| // 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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 😅 |
||
| const { default: Anthropic } = await import('@anthropic-ai/sdk'); | ||
| const anthropicClient = new Anthropic({ | ||
| apiKey: 'mock-api-key', | ||
|
|
@@ -55,8 +53,8 @@ async function run() { | |
| max_tokens: 100, | ||
| }); | ||
|
|
||
| // NOW import LangChain - at this point it will mark Anthropic to be skipped | ||
| // But the client created above is already instrumented | ||
| // Import and use LangChain - its own instrumentation records the span and suppresses the nested | ||
| // Anthropic SDK span only for the duration of the call. | ||
| const { ChatAnthropic } = await import('@langchain/anthropic'); | ||
|
|
||
| // Create a LangChain model - this uses Anthropic under the hood | ||
|
|
@@ -73,8 +71,8 @@ async function run() { | |
| // Use LangChain - this will be instrumented by LangChain integration | ||
| await langchainModel.invoke('LangChain Anthropic call'); | ||
|
|
||
| // Create ANOTHER Anthropic client after LangChain was imported | ||
| // This one should NOT be instrumented (skip mechanism works correctly) | ||
| // Direct Anthropic call made AFTER LangChain was used - still instrumented, since suppression | ||
| // is scoped to the LangChain call and does not leak to direct provider calls. | ||
| const anthropicClient2 = new Anthropic({ | ||
| apiKey: 'mock-api-key', | ||
| baseURL, | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,27 @@ | ||||||||||||||||||||
| import { getCurrentScope, withScope } from '../../currentScopes'; | ||||||||||||||||||||
| import type { Scope } from '../../scope'; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const SUPPRESS_AI_PROVIDER_SPANS_KEY = '__SENTRY_SUPPRESS_AI_PROVIDER_SPANS__'; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /** | ||||||||||||||||||||
| * Check if AI provider spans should be suppressed in the current scope. | ||||||||||||||||||||
| * | ||||||||||||||||||||
| * @internal | ||||||||||||||||||||
| */ | ||||||||||||||||||||
| export function _INTERNAL_isAiProviderSpanSuppressed(): boolean { | ||||||||||||||||||||
| return getCurrentScope().getScopeData().sdkProcessingMetadata[SUPPRESS_AI_PROVIDER_SPANS_KEY] === true; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /** | ||||||||||||||||||||
| * 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. | ||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. low: I think this approach means that anything that calls I don't really see a way around it, tbh, but maybe a doc comment could be worthwhile here?
Suggested change
|
||||||||||||||||||||
| * | ||||||||||||||||||||
| * @internal | ||||||||||||||||||||
| */ | ||||||||||||||||||||
| export function _INTERNAL_withSuppressedAiProviderSpans<T>(callback: () => T): T { | ||||||||||||||||||||
| return withScope((scope: Scope) => { | ||||||||||||||||||||
| scope.setSDKProcessingMetadata({ [SUPPRESS_AI_PROVIDER_SPANS_KEY]: true }); | ||||||||||||||||||||
| return callback(); | ||||||||||||||||||||
| }); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
nicohrubec marked this conversation as resolved.
|
||||||||||||||||||||
There was a problem hiding this comment.
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?