Skip to content

Commit fd71786

Browse files
committed
feat(node-core): Add integration disabling mechanism to prevent instrumentation conflicts
1 parent acf2929 commit fd71786

File tree

13 files changed

+147
-12
lines changed

13 files changed

+147
-12
lines changed

dev-packages/node-integration-tests/suites/tracing/langchain/instrument-with-pii.mjs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ Sentry.init({
77
tracesSampleRate: 1.0,
88
sendDefaultPii: true,
99
transport: loggingTransport,
10-
// Filter out Anthropic integration to avoid duplicate spans with LangChain
11-
integrations: integrations => integrations.filter(integration => integration.name !== 'Anthropic_AI'),
1210
beforeSendTransaction: event => {
1311
// Filter out mock express server transactions
1412
if (event.transaction.includes('/v1/messages')) {

dev-packages/node-integration-tests/suites/tracing/langchain/instrument.mjs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ Sentry.init({
77
tracesSampleRate: 1.0,
88
sendDefaultPii: false,
99
transport: loggingTransport,
10-
// Filter out Anthropic integration to avoid duplicate spans with LangChain
11-
integrations: integrations => integrations.filter(integration => integration.name !== 'Anthropic_AI'),
1210
beforeSendTransaction: event => {
1311
// Filter out mock express server transactions
1412
if (event.transaction.includes('/v1/messages')) {

packages/node-core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export { pinoIntegration } from './integrations/pino';
3232
export { SentryContextManager } from './otel/contextManager';
3333
export { setupOpenTelemetryLogger } from './otel/logger';
3434
export { generateInstrumentOnce, instrumentWhenWrapped, INSTRUMENTED } from './otel/instrument';
35+
export { disableIntegration, isIntegrationDisabled, enableIntegration } from './otel/disabledIntegrations';
3536

3637
export { init, getDefaultIntegrations, initWithoutDefaultIntegrations, validateOpenTelemetrySetup } from './sdk';
3738
export { setIsolationScope } from './sdk/scope';
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* Registry to track disabled integrations.
3+
* This is used to prevent duplicate instrumentation when higher-level integrations
4+
* (like LangChain) already instrument the underlying libraries (like OpenAI, Anthropic, etc.)
5+
*/
6+
7+
const DISABLED_INTEGRATIONS = new Set<string>();
8+
9+
/**
10+
* Mark an integration as disabled to prevent its instrumentation from being set up.
11+
* @param integrationName The name of the integration to disable
12+
*/
13+
export function disableIntegration(integrationName: string): void {
14+
DISABLED_INTEGRATIONS.add(integrationName);
15+
}
16+
17+
/**
18+
* Check if an integration has been disabled.
19+
* @param integrationName The name of the integration to check
20+
* @returns true if the integration is disabled
21+
*/
22+
export function isIntegrationDisabled(integrationName: string): boolean {
23+
return DISABLED_INTEGRATIONS.has(integrationName);
24+
}
25+
26+
/**
27+
* Remove an integration from the disabled list.
28+
* @param integrationName The name of the integration to enable
29+
*/
30+
export function enableIntegration(integrationName: string): void {
31+
DISABLED_INTEGRATIONS.delete(integrationName);
32+
}
33+
34+
/** Exported only for tests. */
35+
export function clearDisabledIntegrations(): void {
36+
DISABLED_INTEGRATIONS.clear();
37+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { beforeEach, describe, expect, it } from 'vitest';
2+
import {
3+
clearDisabledIntegrations,
4+
disableIntegration,
5+
enableIntegration,
6+
isIntegrationDisabled,
7+
} from '../../src/otel/disabledIntegrations';
8+
9+
describe('disabledIntegrations', () => {
10+
beforeEach(() => {
11+
clearDisabledIntegrations();
12+
});
13+
14+
it('should mark an integration as disabled', () => {
15+
expect(isIntegrationDisabled('TestIntegration')).toBe(false);
16+
disableIntegration('TestIntegration');
17+
expect(isIntegrationDisabled('TestIntegration')).toBe(true);
18+
});
19+
20+
it('should enable a disabled integration', () => {
21+
disableIntegration('TestIntegration');
22+
expect(isIntegrationDisabled('TestIntegration')).toBe(true);
23+
enableIntegration('TestIntegration');
24+
expect(isIntegrationDisabled('TestIntegration')).toBe(false);
25+
});
26+
27+
it('should handle multiple integrations', () => {
28+
disableIntegration('Integration1');
29+
disableIntegration('Integration2');
30+
31+
expect(isIntegrationDisabled('Integration1')).toBe(true);
32+
expect(isIntegrationDisabled('Integration2')).toBe(true);
33+
expect(isIntegrationDisabled('Integration3')).toBe(false);
34+
});
35+
36+
it('should clear all disabled integrations', () => {
37+
disableIntegration('Integration1');
38+
disableIntegration('Integration2');
39+
40+
expect(isIntegrationDisabled('Integration1')).toBe(true);
41+
expect(isIntegrationDisabled('Integration2')).toBe(true);
42+
43+
clearDisabledIntegrations();
44+
45+
expect(isIntegrationDisabled('Integration1')).toBe(false);
46+
expect(isIntegrationDisabled('Integration2')).toBe(false);
47+
});
48+
});

packages/node/src/integrations/tracing/anthropic-ai/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { AnthropicAiOptions, IntegrationFn } from '@sentry/core';
22
import { ANTHROPIC_AI_INTEGRATION_NAME, defineIntegration } from '@sentry/core';
3-
import { generateInstrumentOnce } from '@sentry/node-core';
3+
import { generateInstrumentOnce, isIntegrationDisabled } from '@sentry/node-core';
44
import { SentryAnthropicAiInstrumentation } from './instrumentation';
55

66
export const instrumentAnthropicAi = generateInstrumentOnce<AnthropicAiOptions>(
@@ -13,6 +13,10 @@ const _anthropicAIIntegration = ((options: AnthropicAiOptions = {}) => {
1313
name: ANTHROPIC_AI_INTEGRATION_NAME,
1414
options,
1515
setupOnce() {
16+
// Skip instrumentation if disabled (e.g., when LangChain integration is active)
17+
if (isIntegrationDisabled(ANTHROPIC_AI_INTEGRATION_NAME)) {
18+
return;
19+
}
1620
instrumentAnthropicAi(options);
1721
},
1822
};

packages/node/src/integrations/tracing/anthropic-ai/instrumentation.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import {
55
InstrumentationNodeModuleDefinition,
66
} from '@opentelemetry/instrumentation';
77
import type { AnthropicAiClient, AnthropicAiOptions } from '@sentry/core';
8-
import { getClient, instrumentAnthropicAiClient, SDK_VERSION } from '@sentry/core';
8+
import { ANTHROPIC_AI_INTEGRATION_NAME, getClient, instrumentAnthropicAiClient, SDK_VERSION } from '@sentry/core';
9+
import { isIntegrationDisabled } from '@sentry/node-core';
910

1011
const supportedVersions = ['>=0.19.2 <1.0.0'];
1112

@@ -43,6 +44,11 @@ export class SentryAnthropicAiInstrumentation extends InstrumentationBase<Anthro
4344
* Core patch logic applying instrumentation to the Anthropic AI client constructor.
4445
*/
4546
private _patch(exports: PatchedModuleExports): PatchedModuleExports | void {
47+
// Skip patching if this integration has been disabled (e.g., by LangChain integration)
48+
if (isIntegrationDisabled(ANTHROPIC_AI_INTEGRATION_NAME)) {
49+
return exports;
50+
}
51+
4652
const Original = exports.Anthropic;
4753

4854
const config = this.getConfig();

packages/node/src/integrations/tracing/google-genai/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { GoogleGenAIOptions, IntegrationFn } from '@sentry/core';
22
import { defineIntegration, GOOGLE_GENAI_INTEGRATION_NAME } from '@sentry/core';
3-
import { generateInstrumentOnce } from '@sentry/node-core';
3+
import { generateInstrumentOnce, isIntegrationDisabled } from '@sentry/node-core';
44
import { SentryGoogleGenAiInstrumentation } from './instrumentation';
55

66
export const instrumentGoogleGenAI = generateInstrumentOnce<GoogleGenAIOptions>(
@@ -12,6 +12,10 @@ const _googleGenAIIntegration = ((options: GoogleGenAIOptions = {}) => {
1212
return {
1313
name: GOOGLE_GENAI_INTEGRATION_NAME,
1414
setupOnce() {
15+
// Skip instrumentation if disabled (e.g., when LangChain integration is active)
16+
if (isIntegrationDisabled(GOOGLE_GENAI_INTEGRATION_NAME)) {
17+
return;
18+
}
1519
instrumentGoogleGenAI(options);
1620
},
1721
};

packages/node/src/integrations/tracing/google-genai/instrumentation.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,14 @@ import {
66
InstrumentationNodeModuleFile,
77
} from '@opentelemetry/instrumentation';
88
import type { GoogleGenAIClient, GoogleGenAIOptions } from '@sentry/core';
9-
import { getClient, instrumentGoogleGenAIClient, replaceExports, SDK_VERSION } from '@sentry/core';
9+
import {
10+
getClient,
11+
GOOGLE_GENAI_INTEGRATION_NAME,
12+
instrumentGoogleGenAIClient,
13+
replaceExports,
14+
SDK_VERSION,
15+
} from '@sentry/core';
16+
import { isIntegrationDisabled } from '@sentry/node-core';
1017

1118
const supportedVersions = ['>=0.10.0 <2'];
1219

@@ -57,6 +64,11 @@ export class SentryGoogleGenAiInstrumentation extends InstrumentationBase<Google
5764
* Core patch logic applying instrumentation to the Google GenAI client constructor.
5865
*/
5966
private _patch(exports: PatchedModuleExports): PatchedModuleExports | void {
67+
// Skip patching if this integration has been disabled (e.g., by LangChain integration)
68+
if (isIntegrationDisabled(GOOGLE_GENAI_INTEGRATION_NAME)) {
69+
return exports;
70+
}
71+
6072
const Original = exports.GoogleGenAI;
6173
const config = this.getConfig();
6274

packages/node/src/integrations/tracing/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ import { instrumentVercelAi, vercelAIIntegration } from './vercelai';
3232
*/
3333
export function getAutoPerformanceIntegrations(): Integration[] {
3434
return [
35+
// LangChain must come first to disable AI provider integrations before they instrument
36+
langChainIntegration(),
3537
expressIntegration(),
3638
fastifyIntegration(),
3739
graphqlIntegration(),
@@ -57,7 +59,6 @@ export function getAutoPerformanceIntegrations(): Integration[] {
5759
firebaseIntegration(),
5860
anthropicAIIntegration(),
5961
googleGenAIIntegration(),
60-
langChainIntegration(),
6162
];
6263
}
6364

0 commit comments

Comments
 (0)