Skip to content

Commit 31cce57

Browse files
committed
update with disabling mechanisim
1 parent e05acdd commit 31cce57

File tree

9 files changed

+151
-9
lines changed

9 files changed

+151
-9
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 { disableIntegrations, isIntegrationDisabled, enableIntegration } from './otel/disabledIntegrations';
3536

3637
export { init, getDefaultIntegrations, initWithoutDefaultIntegrations, validateOpenTelemetrySetup } from './sdk';
3738
export { setIsolationScope } from './sdk/scope';
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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 one or more integrations as disabled to prevent their instrumentation from being set up.
11+
* @param integrationName The name(s) of the integration(s) to disable
12+
*/
13+
export function disableIntegrations(integrationName: string | string[]): void {
14+
if (Array.isArray(integrationName)) {
15+
integrationName.forEach(name => DISABLED_INTEGRATIONS.add(name));
16+
} else {
17+
DISABLED_INTEGRATIONS.add(integrationName);
18+
}
19+
}
20+
21+
/**
22+
* Check if an integration has been disabled.
23+
* @param integrationName The name of the integration to check
24+
* @returns true if the integration is disabled
25+
*/
26+
export function isIntegrationDisabled(integrationName: string): boolean {
27+
return DISABLED_INTEGRATIONS.has(integrationName);
28+
}
29+
30+
/**
31+
* Remove one or more integrations from the disabled list.
32+
* @param integrationName The name(s) of the integration(s) to enable
33+
*/
34+
export function enableIntegration(integrationName: string | string[]): void {
35+
if (Array.isArray(integrationName)) {
36+
integrationName.forEach(name => DISABLED_INTEGRATIONS.delete(name));
37+
} else {
38+
DISABLED_INTEGRATIONS.delete(integrationName);
39+
}
40+
}
41+
42+
/** Exported only for tests. */
43+
export function clearDisabledIntegrations(): void {
44+
DISABLED_INTEGRATIONS.clear();
45+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { beforeEach, describe, expect, it } from 'vitest';
2+
import {
3+
clearDisabledIntegrations,
4+
disableIntegrations,
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+
disableIntegrations('TestIntegration');
17+
expect(isIntegrationDisabled('TestIntegration')).toBe(true);
18+
});
19+
20+
it('should enable a disabled integration', () => {
21+
disableIntegrations('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+
disableIntegrations('Integration1');
29+
disableIntegrations('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+
disableIntegrations('Integration1');
38+
disableIntegrations('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+
49+
it('should disable multiple integrations at once using an array', () => {
50+
expect(isIntegrationDisabled('Integration1')).toBe(false);
51+
expect(isIntegrationDisabled('Integration2')).toBe(false);
52+
expect(isIntegrationDisabled('Integration3')).toBe(false);
53+
54+
disableIntegrations(['Integration1', 'Integration2', 'Integration3']);
55+
56+
expect(isIntegrationDisabled('Integration1')).toBe(true);
57+
expect(isIntegrationDisabled('Integration2')).toBe(true);
58+
expect(isIntegrationDisabled('Integration3')).toBe(true);
59+
});
60+
61+
it('should enable multiple integrations at once using an array', () => {
62+
disableIntegrations(['Integration1', 'Integration2', 'Integration3']);
63+
64+
expect(isIntegrationDisabled('Integration1')).toBe(true);
65+
expect(isIntegrationDisabled('Integration2')).toBe(true);
66+
expect(isIntegrationDisabled('Integration3')).toBe(true);
67+
68+
enableIntegration(['Integration1', 'Integration2']);
69+
70+
expect(isIntegrationDisabled('Integration1')).toBe(false);
71+
expect(isIntegrationDisabled('Integration2')).toBe(false);
72+
expect(isIntegrationDisabled('Integration3')).toBe(true);
73+
});
74+
});

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/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/langchain/index.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import type { IntegrationFn, LangChainOptions } from '@sentry/core';
2-
import { defineIntegration, LANGCHAIN_INTEGRATION_NAME } from '@sentry/core';
3-
import { generateInstrumentOnce } from '@sentry/node-core';
2+
import {
3+
ANTHROPIC_AI_INTEGRATION_NAME,
4+
defineIntegration,
5+
GOOGLE_GENAI_INTEGRATION_NAME,
6+
LANGCHAIN_INTEGRATION_NAME,
7+
OPENAI_INTEGRATION_NAME,
8+
} from '@sentry/core';
9+
import { disableIntegrations, generateInstrumentOnce } from '@sentry/node-core';
410
import { SentryLangChainInstrumentation } from './instrumentation';
511

612
export const instrumentLangChain = generateInstrumentOnce<LangChainOptions>(
@@ -12,6 +18,10 @@ const _langChainIntegration = ((options: LangChainOptions = {}) => {
1218
return {
1319
name: LANGCHAIN_INTEGRATION_NAME,
1420
setupOnce() {
21+
// Disable AI provider integrations to prevent duplicate spans
22+
// LangChain integration handles instrumentation for all underlying AI providers
23+
disableIntegrations([OPENAI_INTEGRATION_NAME, ANTHROPIC_AI_INTEGRATION_NAME, GOOGLE_GENAI_INTEGRATION_NAME]);
24+
1525
instrumentLangChain(options);
1626
},
1727
};
@@ -25,6 +35,10 @@ const _langChainIntegration = ((options: LangChainOptions = {}) => {
2535
* When configured, this integration automatically instruments LangChain runnable instances
2636
* to capture telemetry data by injecting Sentry callback handlers into all LangChain calls.
2737
*
38+
* **Important:** This integration automatically disables the OpenAI, Anthropic, and Google GenAI
39+
* integrations to prevent duplicate spans when using LangChain with these providers. LangChain
40+
* handles the instrumentation for all underlying AI providers.
41+
*
2842
* @example
2943
* ```javascript
3044
* import * as Sentry from '@sentry/node';

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

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

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

0 commit comments

Comments
 (0)