diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts b/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts index e1949751bae4..07cd93d331b7 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts @@ -169,6 +169,23 @@ describe('LangChain integration', () => { .start() .completed(); }); + + test('does not create duplicate spans from double module patching', async () => { + await createRunner() + .ignore('event') + .expect({ + transaction: event => { + const spans = event.spans || []; + const genAiChatSpans = spans.filter(span => span.op === 'gen_ai.chat'); + // The scenario makes 3 LangChain calls (2 successful + 1 error). + // Without the dedup guard, the file-level and module-level hooks + // both patch the same prototype, producing 6 spans instead of 3. + expect(genAiChatSpans).toHaveLength(3); + }, + }) + .start() + .completed(); + }); }); createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument-with-pii.mjs', (createRunner, test) => { diff --git a/packages/node/src/integrations/tracing/langchain/instrumentation.ts b/packages/node/src/integrations/tracing/langchain/instrumentation.ts index efa487ddce4f..057778af3a08 100644 --- a/packages/node/src/integrations/tracing/langchain/instrumentation.ts +++ b/packages/node/src/integrations/tracing/langchain/instrumentation.ts @@ -228,6 +228,12 @@ export class SentryLangChainInstrumentation extends InstrumentationBase; + // Skip if already patched (both file-level and module-level hooks resolve to the same prototype) + if (targetProto.__sentry_patched__) { + return; + } + targetProto.__sentry_patched__ = true; + // Patch the methods (invoke, stream, batch) // All chat model instances will inherit these patched methods const methodsToPatch = ['invoke', 'stream', 'batch'] as const;