From 31cce578263d76b2ba756f9001aee702ad40dca7 Mon Sep 17 00:00:00 2001 From: RulaKhaled Date: Thu, 23 Oct 2025 11:13:15 +0200 Subject: [PATCH 1/8] update with disabling mechanisim --- .../tracing/langchain/instrument-with-pii.mjs | 2 - .../suites/tracing/langchain/instrument.mjs | 2 - packages/node-core/src/index.ts | 1 + .../src/otel/disabledIntegrations.ts | 45 +++++++++++ .../test/otel/disabledIntegrations.test.ts | 74 +++++++++++++++++++ .../tracing/anthropic-ai/index.ts | 6 +- .../tracing/google-genai/index.ts | 6 +- .../integrations/tracing/langchain/index.ts | 18 ++++- .../src/integrations/tracing/openai/index.ts | 6 +- 9 files changed, 151 insertions(+), 9 deletions(-) create mode 100644 packages/node-core/src/otel/disabledIntegrations.ts create mode 100644 packages/node-core/test/otel/disabledIntegrations.test.ts diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-with-pii.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-with-pii.mjs index 85b2a963d977..cb68a6f7683e 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-with-pii.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-with-pii.mjs @@ -7,8 +7,6 @@ Sentry.init({ tracesSampleRate: 1.0, sendDefaultPii: true, transport: loggingTransport, - // Filter out Anthropic integration to avoid duplicate spans with LangChain - integrations: integrations => integrations.filter(integration => integration.name !== 'Anthropic_AI'), beforeSendTransaction: event => { // Filter out mock express server transactions if (event.transaction.includes('/v1/messages')) { diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/instrument.mjs index 524d19f4b995..b4ce44f3e91a 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/instrument.mjs @@ -7,8 +7,6 @@ Sentry.init({ tracesSampleRate: 1.0, sendDefaultPii: false, transport: loggingTransport, - // Filter out Anthropic integration to avoid duplicate spans with LangChain - integrations: integrations => integrations.filter(integration => integration.name !== 'Anthropic_AI'), beforeSendTransaction: event => { // Filter out mock express server transactions if (event.transaction.includes('/v1/messages')) { diff --git a/packages/node-core/src/index.ts b/packages/node-core/src/index.ts index 7557d73c74a2..bca7b727c699 100644 --- a/packages/node-core/src/index.ts +++ b/packages/node-core/src/index.ts @@ -32,6 +32,7 @@ export { pinoIntegration } from './integrations/pino'; export { SentryContextManager } from './otel/contextManager'; export { setupOpenTelemetryLogger } from './otel/logger'; export { generateInstrumentOnce, instrumentWhenWrapped, INSTRUMENTED } from './otel/instrument'; +export { disableIntegrations, isIntegrationDisabled, enableIntegration } from './otel/disabledIntegrations'; export { init, getDefaultIntegrations, initWithoutDefaultIntegrations, validateOpenTelemetrySetup } from './sdk'; export { setIsolationScope } from './sdk/scope'; diff --git a/packages/node-core/src/otel/disabledIntegrations.ts b/packages/node-core/src/otel/disabledIntegrations.ts new file mode 100644 index 000000000000..459846eabcae --- /dev/null +++ b/packages/node-core/src/otel/disabledIntegrations.ts @@ -0,0 +1,45 @@ +/** + * Registry to track disabled integrations. + * This is used to prevent duplicate instrumentation when higher-level integrations + * (like LangChain) already instrument the underlying libraries (like OpenAI, Anthropic, etc.) + */ + +const DISABLED_INTEGRATIONS = new Set(); + +/** + * Mark one or more integrations as disabled to prevent their instrumentation from being set up. + * @param integrationName The name(s) of the integration(s) to disable + */ +export function disableIntegrations(integrationName: string | string[]): void { + if (Array.isArray(integrationName)) { + integrationName.forEach(name => DISABLED_INTEGRATIONS.add(name)); + } else { + DISABLED_INTEGRATIONS.add(integrationName); + } +} + +/** + * Check if an integration has been disabled. + * @param integrationName The name of the integration to check + * @returns true if the integration is disabled + */ +export function isIntegrationDisabled(integrationName: string): boolean { + return DISABLED_INTEGRATIONS.has(integrationName); +} + +/** + * Remove one or more integrations from the disabled list. + * @param integrationName The name(s) of the integration(s) to enable + */ +export function enableIntegration(integrationName: string | string[]): void { + if (Array.isArray(integrationName)) { + integrationName.forEach(name => DISABLED_INTEGRATIONS.delete(name)); + } else { + DISABLED_INTEGRATIONS.delete(integrationName); + } +} + +/** Exported only for tests. */ +export function clearDisabledIntegrations(): void { + DISABLED_INTEGRATIONS.clear(); +} diff --git a/packages/node-core/test/otel/disabledIntegrations.test.ts b/packages/node-core/test/otel/disabledIntegrations.test.ts new file mode 100644 index 000000000000..bbe3b1116035 --- /dev/null +++ b/packages/node-core/test/otel/disabledIntegrations.test.ts @@ -0,0 +1,74 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import { + clearDisabledIntegrations, + disableIntegrations, + enableIntegration, + isIntegrationDisabled, +} from '../../src/otel/disabledIntegrations'; + +describe('disabledIntegrations', () => { + beforeEach(() => { + clearDisabledIntegrations(); + }); + + it('should mark an integration as disabled', () => { + expect(isIntegrationDisabled('TestIntegration')).toBe(false); + disableIntegrations('TestIntegration'); + expect(isIntegrationDisabled('TestIntegration')).toBe(true); + }); + + it('should enable a disabled integration', () => { + disableIntegrations('TestIntegration'); + expect(isIntegrationDisabled('TestIntegration')).toBe(true); + enableIntegration('TestIntegration'); + expect(isIntegrationDisabled('TestIntegration')).toBe(false); + }); + + it('should handle multiple integrations', () => { + disableIntegrations('Integration1'); + disableIntegrations('Integration2'); + + expect(isIntegrationDisabled('Integration1')).toBe(true); + expect(isIntegrationDisabled('Integration2')).toBe(true); + expect(isIntegrationDisabled('Integration3')).toBe(false); + }); + + it('should clear all disabled integrations', () => { + disableIntegrations('Integration1'); + disableIntegrations('Integration2'); + + expect(isIntegrationDisabled('Integration1')).toBe(true); + expect(isIntegrationDisabled('Integration2')).toBe(true); + + clearDisabledIntegrations(); + + expect(isIntegrationDisabled('Integration1')).toBe(false); + expect(isIntegrationDisabled('Integration2')).toBe(false); + }); + + it('should disable multiple integrations at once using an array', () => { + expect(isIntegrationDisabled('Integration1')).toBe(false); + expect(isIntegrationDisabled('Integration2')).toBe(false); + expect(isIntegrationDisabled('Integration3')).toBe(false); + + disableIntegrations(['Integration1', 'Integration2', 'Integration3']); + + expect(isIntegrationDisabled('Integration1')).toBe(true); + expect(isIntegrationDisabled('Integration2')).toBe(true); + expect(isIntegrationDisabled('Integration3')).toBe(true); + }); + + it('should enable multiple integrations at once using an array', () => { + disableIntegrations(['Integration1', 'Integration2', 'Integration3']); + + expect(isIntegrationDisabled('Integration1')).toBe(true); + expect(isIntegrationDisabled('Integration2')).toBe(true); + expect(isIntegrationDisabled('Integration3')).toBe(true); + + enableIntegration(['Integration1', 'Integration2']); + + expect(isIntegrationDisabled('Integration1')).toBe(false); + expect(isIntegrationDisabled('Integration2')).toBe(false); + expect(isIntegrationDisabled('Integration3')).toBe(true); + }); +}); diff --git a/packages/node/src/integrations/tracing/anthropic-ai/index.ts b/packages/node/src/integrations/tracing/anthropic-ai/index.ts index 65b7d72a869a..a5ef985fada9 100644 --- a/packages/node/src/integrations/tracing/anthropic-ai/index.ts +++ b/packages/node/src/integrations/tracing/anthropic-ai/index.ts @@ -1,6 +1,6 @@ import type { AnthropicAiOptions, IntegrationFn } from '@sentry/core'; import { ANTHROPIC_AI_INTEGRATION_NAME, defineIntegration } from '@sentry/core'; -import { generateInstrumentOnce } from '@sentry/node-core'; +import { generateInstrumentOnce, isIntegrationDisabled } from '@sentry/node-core'; import { SentryAnthropicAiInstrumentation } from './instrumentation'; export const instrumentAnthropicAi = generateInstrumentOnce( @@ -13,6 +13,10 @@ const _anthropicAIIntegration = ((options: AnthropicAiOptions = {}) => { name: ANTHROPIC_AI_INTEGRATION_NAME, options, setupOnce() { + // Skip instrumentation if disabled (e.g., when LangChain integration is active) + if (isIntegrationDisabled(ANTHROPIC_AI_INTEGRATION_NAME)) { + return; + } instrumentAnthropicAi(options); }, }; diff --git a/packages/node/src/integrations/tracing/google-genai/index.ts b/packages/node/src/integrations/tracing/google-genai/index.ts index 5c1ad09d2fcd..8e3d61a6641a 100644 --- a/packages/node/src/integrations/tracing/google-genai/index.ts +++ b/packages/node/src/integrations/tracing/google-genai/index.ts @@ -1,6 +1,6 @@ import type { GoogleGenAIOptions, IntegrationFn } from '@sentry/core'; import { defineIntegration, GOOGLE_GENAI_INTEGRATION_NAME } from '@sentry/core'; -import { generateInstrumentOnce } from '@sentry/node-core'; +import { generateInstrumentOnce, isIntegrationDisabled } from '@sentry/node-core'; import { SentryGoogleGenAiInstrumentation } from './instrumentation'; export const instrumentGoogleGenAI = generateInstrumentOnce( @@ -12,6 +12,10 @@ const _googleGenAIIntegration = ((options: GoogleGenAIOptions = {}) => { return { name: GOOGLE_GENAI_INTEGRATION_NAME, setupOnce() { + // Skip instrumentation if disabled (e.g., when LangChain integration is active) + if (isIntegrationDisabled(GOOGLE_GENAI_INTEGRATION_NAME)) { + return; + } instrumentGoogleGenAI(options); }, }; diff --git a/packages/node/src/integrations/tracing/langchain/index.ts b/packages/node/src/integrations/tracing/langchain/index.ts index e575691b930f..df8eb95aaf61 100644 --- a/packages/node/src/integrations/tracing/langchain/index.ts +++ b/packages/node/src/integrations/tracing/langchain/index.ts @@ -1,6 +1,12 @@ import type { IntegrationFn, LangChainOptions } from '@sentry/core'; -import { defineIntegration, LANGCHAIN_INTEGRATION_NAME } from '@sentry/core'; -import { generateInstrumentOnce } from '@sentry/node-core'; +import { + ANTHROPIC_AI_INTEGRATION_NAME, + defineIntegration, + GOOGLE_GENAI_INTEGRATION_NAME, + LANGCHAIN_INTEGRATION_NAME, + OPENAI_INTEGRATION_NAME, +} from '@sentry/core'; +import { disableIntegrations, generateInstrumentOnce } from '@sentry/node-core'; import { SentryLangChainInstrumentation } from './instrumentation'; export const instrumentLangChain = generateInstrumentOnce( @@ -12,6 +18,10 @@ const _langChainIntegration = ((options: LangChainOptions = {}) => { return { name: LANGCHAIN_INTEGRATION_NAME, setupOnce() { + // Disable AI provider integrations to prevent duplicate spans + // LangChain integration handles instrumentation for all underlying AI providers + disableIntegrations([OPENAI_INTEGRATION_NAME, ANTHROPIC_AI_INTEGRATION_NAME, GOOGLE_GENAI_INTEGRATION_NAME]); + instrumentLangChain(options); }, }; @@ -25,6 +35,10 @@ const _langChainIntegration = ((options: LangChainOptions = {}) => { * When configured, this integration automatically instruments LangChain runnable instances * to capture telemetry data by injecting Sentry callback handlers into all LangChain calls. * + * **Important:** This integration automatically disables the OpenAI, Anthropic, and Google GenAI + * integrations to prevent duplicate spans when using LangChain with these providers. LangChain + * handles the instrumentation for all underlying AI providers. + * * @example * ```javascript * import * as Sentry from '@sentry/node'; diff --git a/packages/node/src/integrations/tracing/openai/index.ts b/packages/node/src/integrations/tracing/openai/index.ts index 0e88d2b315cc..38e724ed40e3 100644 --- a/packages/node/src/integrations/tracing/openai/index.ts +++ b/packages/node/src/integrations/tracing/openai/index.ts @@ -1,6 +1,6 @@ import type { IntegrationFn, OpenAiOptions } from '@sentry/core'; import { defineIntegration, OPENAI_INTEGRATION_NAME } from '@sentry/core'; -import { generateInstrumentOnce } from '@sentry/node-core'; +import { generateInstrumentOnce, isIntegrationDisabled } from '@sentry/node-core'; import { SentryOpenAiInstrumentation } from './instrumentation'; export const instrumentOpenAi = generateInstrumentOnce( @@ -13,6 +13,10 @@ const _openAiIntegration = ((options: OpenAiOptions = {}) => { name: OPENAI_INTEGRATION_NAME, options, setupOnce() { + // Skip instrumentation if disabled (e.g., when LangChain integration is active) + if (isIntegrationDisabled(OPENAI_INTEGRATION_NAME)) { + return; + } instrumentOpenAi(); }, }; From e34fa3d91167d2c5033767d5616df8012b1ce17c Mon Sep 17 00:00:00 2001 From: RulaKhaled Date: Thu, 23 Oct 2025 11:22:09 +0200 Subject: [PATCH 2/8] reorder integrations --- packages/node/src/integrations/tracing/index.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/node/src/integrations/tracing/index.ts b/packages/node/src/integrations/tracing/index.ts index 2782d7907349..b586941d6530 100644 --- a/packages/node/src/integrations/tracing/index.ts +++ b/packages/node/src/integrations/tracing/index.ts @@ -51,13 +51,15 @@ export function getAutoPerformanceIntegrations(): Integration[] { kafkaIntegration(), amqplibIntegration(), lruMemoizerIntegration(), + // AI providers + // LangChain must come first to disable AI provider integrations before they instrument + langChainIntegration(), vercelAIIntegration(), openAIIntegration(), - postgresJsIntegration(), - firebaseIntegration(), anthropicAIIntegration(), googleGenAIIntegration(), - langChainIntegration(), + postgresJsIntegration(), + firebaseIntegration(), ]; } @@ -89,12 +91,12 @@ export function getOpenTelemetryInstrumentationToPreload(): (((options?: any) => instrumentTedious, instrumentGenericPool, instrumentAmqplib, + instrumentLangChain, instrumentVercelAi, instrumentOpenAi, instrumentPostgresJs, instrumentFirebase, instrumentAnthropicAi, instrumentGoogleGenAI, - instrumentLangChain, ]; } From d65dd6e01ec8ec7710141d11c2732e32ebecede2 Mon Sep 17 00:00:00 2001 From: RulaKhaled Date: Thu, 23 Oct 2025 16:26:16 +0200 Subject: [PATCH 3/8] deisable integration --- packages/core/src/index.ts | 11 ++- packages/core/src/integration.ts | 66 ++++++++++++++++- packages/node-core/src/index.ts | 1 - .../src/otel/disabledIntegrations.ts | 45 ----------- packages/node-core/src/sdk/client.ts | 18 ++++- .../test/otel/disabledIntegrations.test.ts | 74 ------------------- .../tracing/anthropic-ai/index.ts | 6 +- .../tracing/google-genai/index.ts | 6 +- .../integrations/tracing/langchain/index.ts | 17 ++++- .../src/integrations/tracing/openai/index.ts | 6 +- 10 files changed, 109 insertions(+), 141 deletions(-) delete mode 100644 packages/node-core/src/otel/disabledIntegrations.ts delete mode 100644 packages/node-core/test/otel/disabledIntegrations.test.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index f3b29009b9ce..e535dae6feb9 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -55,7 +55,16 @@ export { initAndBind, setCurrentClient } from './sdk'; export { createTransport } from './transports/base'; export { makeOfflineTransport } from './transports/offline'; export { makeMultiplexedTransport } from './transports/multiplexed'; -export { getIntegrationsToSetup, addIntegration, defineIntegration } from './integration'; +export { + getIntegrationsToSetup, + addIntegration, + defineIntegration, + installedIntegrations, + disableIntegrations, + isIntegrationDisabled, + enableIntegration, + clearDisabledIntegrations, +} from './integration'; export { applyScopeDataToEvent, mergeScopeData } from './utils/applyScopeDataToEvent'; export { prepareEvent } from './utils/prepareEvent'; export { createCheckInEnvelope } from './checkin'; diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index 5cba3ff3dfb8..28a88d9d5611 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -8,6 +8,65 @@ import { debug } from './utils/debug-logger'; export const installedIntegrations: string[] = []; +/** + * Registry to track disabled integrations. + * This is used to prevent duplicate instrumentation when higher-level integrations + * (like LangChain) already instrument the underlying libraries (like OpenAI, Anthropic, etc.) + */ +const DISABLED_INTEGRATIONS = new Set(); + +/** + * Mark one or more integrations as disabled to prevent their instrumentation from being set up. + * @param integrationName The name(s) of the integration(s) to disable + */ +export function disableIntegrations(integrationName: string | string[]): void { + if (Array.isArray(integrationName)) { + integrationName.forEach(name => DISABLED_INTEGRATIONS.add(name)); + } else { + DISABLED_INTEGRATIONS.add(integrationName); + } +} + +/** + * Check if an integration has been disabled. + * @param integrationName The name of the integration to check + * @returns true if the integration is disabled + */ +export function isIntegrationDisabled(integrationName: string): boolean { + return DISABLED_INTEGRATIONS.has(integrationName); +} + +/** + * Remove one or more integrations from the disabled list. + * @param integrationName The name(s) of the integration(s) to enable + */ +export function enableIntegration(integrationName: string | string[]): void { + if (Array.isArray(integrationName)) { + integrationName.forEach(name => DISABLED_INTEGRATIONS.delete(name)); + } else { + DISABLED_INTEGRATIONS.delete(integrationName); + } +} + +/** + * Clear all disabled integrations. + * This is automatically called during Sentry.init() to ensure a clean state. + * + * This also removes the disabled integrations from the global installedIntegrations list, + * allowing them to run setupOnce() again if they're included in a new client. + */ +export function clearDisabledIntegrations(): void { + // Remove disabled integrations from the installed list so they can setup again + DISABLED_INTEGRATIONS.forEach(integrationName => { + const index = installedIntegrations.indexOf(integrationName); + if (index !== -1) { + installedIntegrations.splice(index, 1); + } + }); + + DISABLED_INTEGRATIONS.clear(); +} + /** Map of integrations assigned to a client */ export type IntegrationIndex = { [key: string]: Integration; @@ -108,8 +167,11 @@ export function setupIntegration(client: Client, integration: Integration, integ // `setupOnce` is only called the first time if (installedIntegrations.indexOf(integration.name) === -1 && typeof integration.setupOnce === 'function') { - integration.setupOnce(); - installedIntegrations.push(integration.name); + // Skip setup if integration is disabled + if (!isIntegrationDisabled(integration.name)) { + integration.setupOnce(); + installedIntegrations.push(integration.name); + } } // `setup` is run for each client diff --git a/packages/node-core/src/index.ts b/packages/node-core/src/index.ts index bca7b727c699..7557d73c74a2 100644 --- a/packages/node-core/src/index.ts +++ b/packages/node-core/src/index.ts @@ -32,7 +32,6 @@ export { pinoIntegration } from './integrations/pino'; export { SentryContextManager } from './otel/contextManager'; export { setupOpenTelemetryLogger } from './otel/logger'; export { generateInstrumentOnce, instrumentWhenWrapped, INSTRUMENTED } from './otel/instrument'; -export { disableIntegrations, isIntegrationDisabled, enableIntegration } from './otel/disabledIntegrations'; export { init, getDefaultIntegrations, initWithoutDefaultIntegrations, validateOpenTelemetrySetup } from './sdk'; export { setIsolationScope } from './sdk/scope'; diff --git a/packages/node-core/src/otel/disabledIntegrations.ts b/packages/node-core/src/otel/disabledIntegrations.ts deleted file mode 100644 index 459846eabcae..000000000000 --- a/packages/node-core/src/otel/disabledIntegrations.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Registry to track disabled integrations. - * This is used to prevent duplicate instrumentation when higher-level integrations - * (like LangChain) already instrument the underlying libraries (like OpenAI, Anthropic, etc.) - */ - -const DISABLED_INTEGRATIONS = new Set(); - -/** - * Mark one or more integrations as disabled to prevent their instrumentation from being set up. - * @param integrationName The name(s) of the integration(s) to disable - */ -export function disableIntegrations(integrationName: string | string[]): void { - if (Array.isArray(integrationName)) { - integrationName.forEach(name => DISABLED_INTEGRATIONS.add(name)); - } else { - DISABLED_INTEGRATIONS.add(integrationName); - } -} - -/** - * Check if an integration has been disabled. - * @param integrationName The name of the integration to check - * @returns true if the integration is disabled - */ -export function isIntegrationDisabled(integrationName: string): boolean { - return DISABLED_INTEGRATIONS.has(integrationName); -} - -/** - * Remove one or more integrations from the disabled list. - * @param integrationName The name(s) of the integration(s) to enable - */ -export function enableIntegration(integrationName: string | string[]): void { - if (Array.isArray(integrationName)) { - integrationName.forEach(name => DISABLED_INTEGRATIONS.delete(name)); - } else { - DISABLED_INTEGRATIONS.delete(integrationName); - } -} - -/** Exported only for tests. */ -export function clearDisabledIntegrations(): void { - DISABLED_INTEGRATIONS.clear(); -} diff --git a/packages/node-core/src/sdk/client.ts b/packages/node-core/src/sdk/client.ts index e631508c7392..962cb2c197b4 100644 --- a/packages/node-core/src/sdk/client.ts +++ b/packages/node-core/src/sdk/client.ts @@ -4,7 +4,14 @@ import { trace } from '@opentelemetry/api'; import { registerInstrumentations } from '@opentelemetry/instrumentation'; import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; import type { DynamicSamplingContext, Scope, ServerRuntimeClientOptions, TraceContext } from '@sentry/core'; -import { _INTERNAL_flushLogsBuffer, applySdkMetadata, debug, SDK_VERSION, ServerRuntimeClient } from '@sentry/core'; +import { + _INTERNAL_flushLogsBuffer, + applySdkMetadata, + clearDisabledIntegrations, + debug, + SDK_VERSION, + ServerRuntimeClient, +} from '@sentry/core'; import { getTraceContextForScope } from '@sentry/opentelemetry'; import { isMainThread, threadId } from 'worker_threads'; import { DEBUG_BUILD } from '../debug-build'; @@ -145,6 +152,15 @@ export class NodeClient extends ServerRuntimeClient { } } + /** @inheritDoc */ + protected _setupIntegrations(): void { + // Clear disabled integrations before setting up integrations + // This ensures that integrations work correctly when not all default integrations are used + // (e.g., when LangChain disables OpenAI, but a subsequent client doesn't use LangChain) + clearDisabledIntegrations(); + super._setupIntegrations(); + } + /** Custom implementation for OTEL, so we can handle scope-span linking. */ protected _getTraceInfoFromScope( scope: Scope | undefined, diff --git a/packages/node-core/test/otel/disabledIntegrations.test.ts b/packages/node-core/test/otel/disabledIntegrations.test.ts deleted file mode 100644 index bbe3b1116035..000000000000 --- a/packages/node-core/test/otel/disabledIntegrations.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { beforeEach, describe, expect, it } from 'vitest'; -import { - clearDisabledIntegrations, - disableIntegrations, - enableIntegration, - isIntegrationDisabled, -} from '../../src/otel/disabledIntegrations'; - -describe('disabledIntegrations', () => { - beforeEach(() => { - clearDisabledIntegrations(); - }); - - it('should mark an integration as disabled', () => { - expect(isIntegrationDisabled('TestIntegration')).toBe(false); - disableIntegrations('TestIntegration'); - expect(isIntegrationDisabled('TestIntegration')).toBe(true); - }); - - it('should enable a disabled integration', () => { - disableIntegrations('TestIntegration'); - expect(isIntegrationDisabled('TestIntegration')).toBe(true); - enableIntegration('TestIntegration'); - expect(isIntegrationDisabled('TestIntegration')).toBe(false); - }); - - it('should handle multiple integrations', () => { - disableIntegrations('Integration1'); - disableIntegrations('Integration2'); - - expect(isIntegrationDisabled('Integration1')).toBe(true); - expect(isIntegrationDisabled('Integration2')).toBe(true); - expect(isIntegrationDisabled('Integration3')).toBe(false); - }); - - it('should clear all disabled integrations', () => { - disableIntegrations('Integration1'); - disableIntegrations('Integration2'); - - expect(isIntegrationDisabled('Integration1')).toBe(true); - expect(isIntegrationDisabled('Integration2')).toBe(true); - - clearDisabledIntegrations(); - - expect(isIntegrationDisabled('Integration1')).toBe(false); - expect(isIntegrationDisabled('Integration2')).toBe(false); - }); - - it('should disable multiple integrations at once using an array', () => { - expect(isIntegrationDisabled('Integration1')).toBe(false); - expect(isIntegrationDisabled('Integration2')).toBe(false); - expect(isIntegrationDisabled('Integration3')).toBe(false); - - disableIntegrations(['Integration1', 'Integration2', 'Integration3']); - - expect(isIntegrationDisabled('Integration1')).toBe(true); - expect(isIntegrationDisabled('Integration2')).toBe(true); - expect(isIntegrationDisabled('Integration3')).toBe(true); - }); - - it('should enable multiple integrations at once using an array', () => { - disableIntegrations(['Integration1', 'Integration2', 'Integration3']); - - expect(isIntegrationDisabled('Integration1')).toBe(true); - expect(isIntegrationDisabled('Integration2')).toBe(true); - expect(isIntegrationDisabled('Integration3')).toBe(true); - - enableIntegration(['Integration1', 'Integration2']); - - expect(isIntegrationDisabled('Integration1')).toBe(false); - expect(isIntegrationDisabled('Integration2')).toBe(false); - expect(isIntegrationDisabled('Integration3')).toBe(true); - }); -}); diff --git a/packages/node/src/integrations/tracing/anthropic-ai/index.ts b/packages/node/src/integrations/tracing/anthropic-ai/index.ts index a5ef985fada9..65b7d72a869a 100644 --- a/packages/node/src/integrations/tracing/anthropic-ai/index.ts +++ b/packages/node/src/integrations/tracing/anthropic-ai/index.ts @@ -1,6 +1,6 @@ import type { AnthropicAiOptions, IntegrationFn } from '@sentry/core'; import { ANTHROPIC_AI_INTEGRATION_NAME, defineIntegration } from '@sentry/core'; -import { generateInstrumentOnce, isIntegrationDisabled } from '@sentry/node-core'; +import { generateInstrumentOnce } from '@sentry/node-core'; import { SentryAnthropicAiInstrumentation } from './instrumentation'; export const instrumentAnthropicAi = generateInstrumentOnce( @@ -13,10 +13,6 @@ const _anthropicAIIntegration = ((options: AnthropicAiOptions = {}) => { name: ANTHROPIC_AI_INTEGRATION_NAME, options, setupOnce() { - // Skip instrumentation if disabled (e.g., when LangChain integration is active) - if (isIntegrationDisabled(ANTHROPIC_AI_INTEGRATION_NAME)) { - return; - } instrumentAnthropicAi(options); }, }; diff --git a/packages/node/src/integrations/tracing/google-genai/index.ts b/packages/node/src/integrations/tracing/google-genai/index.ts index 8e3d61a6641a..5c1ad09d2fcd 100644 --- a/packages/node/src/integrations/tracing/google-genai/index.ts +++ b/packages/node/src/integrations/tracing/google-genai/index.ts @@ -1,6 +1,6 @@ import type { GoogleGenAIOptions, IntegrationFn } from '@sentry/core'; import { defineIntegration, GOOGLE_GENAI_INTEGRATION_NAME } from '@sentry/core'; -import { generateInstrumentOnce, isIntegrationDisabled } from '@sentry/node-core'; +import { generateInstrumentOnce } from '@sentry/node-core'; import { SentryGoogleGenAiInstrumentation } from './instrumentation'; export const instrumentGoogleGenAI = generateInstrumentOnce( @@ -12,10 +12,6 @@ const _googleGenAIIntegration = ((options: GoogleGenAIOptions = {}) => { return { name: GOOGLE_GENAI_INTEGRATION_NAME, setupOnce() { - // Skip instrumentation if disabled (e.g., when LangChain integration is active) - if (isIntegrationDisabled(GOOGLE_GENAI_INTEGRATION_NAME)) { - return; - } instrumentGoogleGenAI(options); }, }; diff --git a/packages/node/src/integrations/tracing/langchain/index.ts b/packages/node/src/integrations/tracing/langchain/index.ts index df8eb95aaf61..1c975a8f552f 100644 --- a/packages/node/src/integrations/tracing/langchain/index.ts +++ b/packages/node/src/integrations/tracing/langchain/index.ts @@ -2,6 +2,7 @@ import type { IntegrationFn, LangChainOptions } from '@sentry/core'; import { ANTHROPIC_AI_INTEGRATION_NAME, defineIntegration, + getClient, GOOGLE_GENAI_INTEGRATION_NAME, LANGCHAIN_INTEGRATION_NAME, OPENAI_INTEGRATION_NAME, @@ -18,9 +19,21 @@ const _langChainIntegration = ((options: LangChainOptions = {}) => { return { name: LANGCHAIN_INTEGRATION_NAME, setupOnce() { - // Disable AI provider integrations to prevent duplicate spans + // Only disable AI provider integrations if they weren't explicitly requested by the user // LangChain integration handles instrumentation for all underlying AI providers - disableIntegrations([OPENAI_INTEGRATION_NAME, ANTHROPIC_AI_INTEGRATION_NAME, GOOGLE_GENAI_INTEGRATION_NAME]); + const client = getClient(); + const clientIntegrations = client?.getOptions().integrations || []; + const explicitIntegrationNames = clientIntegrations.map(i => i.name); + + const integrationsToDisable = [ + OPENAI_INTEGRATION_NAME, + ANTHROPIC_AI_INTEGRATION_NAME, + GOOGLE_GENAI_INTEGRATION_NAME, + ].filter(name => !explicitIntegrationNames.includes(name)); + + if (integrationsToDisable.length > 0) { + disableIntegrations(integrationsToDisable); + } instrumentLangChain(options); }, diff --git a/packages/node/src/integrations/tracing/openai/index.ts b/packages/node/src/integrations/tracing/openai/index.ts index 38e724ed40e3..0e88d2b315cc 100644 --- a/packages/node/src/integrations/tracing/openai/index.ts +++ b/packages/node/src/integrations/tracing/openai/index.ts @@ -1,6 +1,6 @@ import type { IntegrationFn, OpenAiOptions } from '@sentry/core'; import { defineIntegration, OPENAI_INTEGRATION_NAME } from '@sentry/core'; -import { generateInstrumentOnce, isIntegrationDisabled } from '@sentry/node-core'; +import { generateInstrumentOnce } from '@sentry/node-core'; import { SentryOpenAiInstrumentation } from './instrumentation'; export const instrumentOpenAi = generateInstrumentOnce( @@ -13,10 +13,6 @@ const _openAiIntegration = ((options: OpenAiOptions = {}) => { name: OPENAI_INTEGRATION_NAME, options, setupOnce() { - // Skip instrumentation if disabled (e.g., when LangChain integration is active) - if (isIntegrationDisabled(OPENAI_INTEGRATION_NAME)) { - return; - } instrumentOpenAi(); }, }; From 3fba71ef9365a69acd80a202cfc4893a2936cadb Mon Sep 17 00:00:00 2001 From: RulaKhaled Date: Thu, 30 Oct 2025 09:49:12 +0100 Subject: [PATCH 4/8] refactor --- .../integrations/tracing/langchain/index.ts | 20 ++++--------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/packages/node/src/integrations/tracing/langchain/index.ts b/packages/node/src/integrations/tracing/langchain/index.ts index 1c975a8f552f..9d8ff0694189 100644 --- a/packages/node/src/integrations/tracing/langchain/index.ts +++ b/packages/node/src/integrations/tracing/langchain/index.ts @@ -2,12 +2,12 @@ import type { IntegrationFn, LangChainOptions } from '@sentry/core'; import { ANTHROPIC_AI_INTEGRATION_NAME, defineIntegration, - getClient, + disableIntegrations, GOOGLE_GENAI_INTEGRATION_NAME, LANGCHAIN_INTEGRATION_NAME, OPENAI_INTEGRATION_NAME, } from '@sentry/core'; -import { disableIntegrations, generateInstrumentOnce } from '@sentry/node-core'; +import { generateInstrumentOnce } from '@sentry/node-core'; import { SentryLangChainInstrumentation } from './instrumentation'; export const instrumentLangChain = generateInstrumentOnce( @@ -19,21 +19,9 @@ const _langChainIntegration = ((options: LangChainOptions = {}) => { return { name: LANGCHAIN_INTEGRATION_NAME, setupOnce() { - // Only disable AI provider integrations if they weren't explicitly requested by the user + // Disable AI provider integrations to prevent duplicate spans // LangChain integration handles instrumentation for all underlying AI providers - const client = getClient(); - const clientIntegrations = client?.getOptions().integrations || []; - const explicitIntegrationNames = clientIntegrations.map(i => i.name); - - const integrationsToDisable = [ - OPENAI_INTEGRATION_NAME, - ANTHROPIC_AI_INTEGRATION_NAME, - GOOGLE_GENAI_INTEGRATION_NAME, - ].filter(name => !explicitIntegrationNames.includes(name)); - - if (integrationsToDisable.length > 0) { - disableIntegrations(integrationsToDisable); - } + disableIntegrations([OPENAI_INTEGRATION_NAME, ANTHROPIC_AI_INTEGRATION_NAME, GOOGLE_GENAI_INTEGRATION_NAME]); instrumentLangChain(options); }, From df823f425b477dc58bc4e73f885b0af87c2df913 Mon Sep 17 00:00:00 2001 From: RulaKhaled Date: Tue, 4 Nov 2025 16:41:12 +0100 Subject: [PATCH 5/8] add missing import --- packages/node-core/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/node-core/src/index.ts b/packages/node-core/src/index.ts index 7557d73c74a2..c242386fa2e7 100644 --- a/packages/node-core/src/index.ts +++ b/packages/node-core/src/index.ts @@ -113,6 +113,7 @@ export { captureSession, endSession, addIntegration, + disableIntegrations, startSpan, startSpanManual, startInactiveSpan, From 461b8edf9d8b87be074034162bad7483ca1f65d2 Mon Sep 17 00:00:00 2001 From: RulaKhaled Date: Fri, 7 Nov 2025 11:41:11 +0100 Subject: [PATCH 6/8] update names --- packages/core/src/index.ts | 7 +- packages/core/src/integration.ts | 67 +++++++++---------- packages/node-core/src/index.ts | 1 - packages/node-core/src/sdk/client.ts | 8 +-- .../integrations/tracing/langchain/index.ts | 16 +++-- 5 files changed, 47 insertions(+), 52 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index e535dae6feb9..34f83d1b5f9a 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -60,10 +60,9 @@ export { addIntegration, defineIntegration, installedIntegrations, - disableIntegrations, - isIntegrationDisabled, - enableIntegration, - clearDisabledIntegrations, + _markIntegrationsDisabled, + _isIntegrationMarkedDisabled, + _clearDisabledIntegrationsMarks, } from './integration'; export { applyScopeDataToEvent, mergeScopeData } from './utils/applyScopeDataToEvent'; export { prepareEvent } from './utils/prepareEvent'; diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index 28a88d9d5611..64cb4f05d382 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -9,62 +9,55 @@ import { debug } from './utils/debug-logger'; export const installedIntegrations: string[] = []; /** - * Registry to track disabled integrations. + * Registry to track integrations marked as disabled. * This is used to prevent duplicate instrumentation when higher-level integrations * (like LangChain) already instrument the underlying libraries (like OpenAI, Anthropic, etc.) */ -const DISABLED_INTEGRATIONS = new Set(); +const MARKED_DISABLED_INTEGRATIONS = new Set(); /** * Mark one or more integrations as disabled to prevent their instrumentation from being set up. - * @param integrationName The name(s) of the integration(s) to disable + * This should be called during an integration's setupOnce() phase. + * The marked integrations will be skipped when their own setupOnce() is called. + * + * @internal This is an internal API for coordination between integrations, not for public use. + * @param integrationName The name(s) of the integration(s) to mark as disabled */ -export function disableIntegrations(integrationName: string | string[]): void { +export function _markIntegrationsDisabled(integrationName: string | string[]): void { if (Array.isArray(integrationName)) { - integrationName.forEach(name => DISABLED_INTEGRATIONS.add(name)); + integrationName.forEach(name => MARKED_DISABLED_INTEGRATIONS.add(name)); } else { - DISABLED_INTEGRATIONS.add(integrationName); + MARKED_DISABLED_INTEGRATIONS.add(integrationName); } } /** - * Check if an integration has been disabled. + * Check if an integration has been marked as disabled. + * + * @internal This is an internal API for coordination between integrations, not for public use. * @param integrationName The name of the integration to check - * @returns true if the integration is disabled - */ -export function isIntegrationDisabled(integrationName: string): boolean { - return DISABLED_INTEGRATIONS.has(integrationName); -} - -/** - * Remove one or more integrations from the disabled list. - * @param integrationName The name(s) of the integration(s) to enable + * @returns true if the integration is marked as disabled */ -export function enableIntegration(integrationName: string | string[]): void { - if (Array.isArray(integrationName)) { - integrationName.forEach(name => DISABLED_INTEGRATIONS.delete(name)); - } else { - DISABLED_INTEGRATIONS.delete(integrationName); - } +export function _isIntegrationMarkedDisabled(integrationName: string): boolean { + return MARKED_DISABLED_INTEGRATIONS.has(integrationName); } /** - * Clear all disabled integrations. - * This is automatically called during Sentry.init() to ensure a clean state. + * Clear all integration marks and remove marked integrations from the installed list. + * This is automatically called at the start of Sentry.init() to ensure a clean state + * between different client initializations. * - * This also removes the disabled integrations from the global installedIntegrations list, + * This also removes the marked integrations from the global installedIntegrations list, * allowing them to run setupOnce() again if they're included in a new client. + * + * @internal This is an internal API for coordination between integrations, not for public use. */ -export function clearDisabledIntegrations(): void { - // Remove disabled integrations from the installed list so they can setup again - DISABLED_INTEGRATIONS.forEach(integrationName => { - const index = installedIntegrations.indexOf(integrationName); - if (index !== -1) { - installedIntegrations.splice(index, 1); - } - }); +export function _clearDisabledIntegrationsMarks(): void { + // Remove marked integrations from the installed list so they can setup again + const filtered = installedIntegrations.filter(integration => !MARKED_DISABLED_INTEGRATIONS.has(integration)); + installedIntegrations.splice(0, installedIntegrations.length, ...filtered); - DISABLED_INTEGRATIONS.clear(); + MARKED_DISABLED_INTEGRATIONS.clear(); } /** Map of integrations assigned to a client */ @@ -166,9 +159,9 @@ export function setupIntegration(client: Client, integration: Integration, integ integrationIndex[integration.name] = integration; // `setupOnce` is only called the first time - if (installedIntegrations.indexOf(integration.name) === -1 && typeof integration.setupOnce === 'function') { - // Skip setup if integration is disabled - if (!isIntegrationDisabled(integration.name)) { + if (!installedIntegrations.includes(integration.name) && typeof integration.setupOnce === 'function') { + // Skip setup if integration is marked as disabled + if (!_isIntegrationMarkedDisabled(integration.name)) { integration.setupOnce(); installedIntegrations.push(integration.name); } diff --git a/packages/node-core/src/index.ts b/packages/node-core/src/index.ts index c242386fa2e7..7557d73c74a2 100644 --- a/packages/node-core/src/index.ts +++ b/packages/node-core/src/index.ts @@ -113,7 +113,6 @@ export { captureSession, endSession, addIntegration, - disableIntegrations, startSpan, startSpanManual, startInactiveSpan, diff --git a/packages/node-core/src/sdk/client.ts b/packages/node-core/src/sdk/client.ts index 962cb2c197b4..2cd8810e5c98 100644 --- a/packages/node-core/src/sdk/client.ts +++ b/packages/node-core/src/sdk/client.ts @@ -5,9 +5,9 @@ import { registerInstrumentations } from '@opentelemetry/instrumentation'; import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; import type { DynamicSamplingContext, Scope, ServerRuntimeClientOptions, TraceContext } from '@sentry/core'; import { + _clearDisabledIntegrationsMarks, _INTERNAL_flushLogsBuffer, applySdkMetadata, - clearDisabledIntegrations, debug, SDK_VERSION, ServerRuntimeClient, @@ -154,10 +154,10 @@ export class NodeClient extends ServerRuntimeClient { /** @inheritDoc */ protected _setupIntegrations(): void { - // Clear disabled integrations before setting up integrations + // Clear integration marks before setting up integrations // This ensures that integrations work correctly when not all default integrations are used - // (e.g., when LangChain disables OpenAI, but a subsequent client doesn't use LangChain) - clearDisabledIntegrations(); + // (e.g., when LangChain marks OpenAI as disabled, but a subsequent client doesn't use LangChain) + _clearDisabledIntegrationsMarks(); super._setupIntegrations(); } diff --git a/packages/node/src/integrations/tracing/langchain/index.ts b/packages/node/src/integrations/tracing/langchain/index.ts index 9d8ff0694189..07b261a3a306 100644 --- a/packages/node/src/integrations/tracing/langchain/index.ts +++ b/packages/node/src/integrations/tracing/langchain/index.ts @@ -1,8 +1,8 @@ import type { IntegrationFn, LangChainOptions } from '@sentry/core'; import { + _markIntegrationsDisabled, ANTHROPIC_AI_INTEGRATION_NAME, defineIntegration, - disableIntegrations, GOOGLE_GENAI_INTEGRATION_NAME, LANGCHAIN_INTEGRATION_NAME, OPENAI_INTEGRATION_NAME, @@ -19,9 +19,13 @@ const _langChainIntegration = ((options: LangChainOptions = {}) => { return { name: LANGCHAIN_INTEGRATION_NAME, setupOnce() { - // Disable AI provider integrations to prevent duplicate spans + // Mark AI provider integrations as disabled to prevent duplicate spans // LangChain integration handles instrumentation for all underlying AI providers - disableIntegrations([OPENAI_INTEGRATION_NAME, ANTHROPIC_AI_INTEGRATION_NAME, GOOGLE_GENAI_INTEGRATION_NAME]); + _markIntegrationsDisabled([ + OPENAI_INTEGRATION_NAME, + ANTHROPIC_AI_INTEGRATION_NAME, + GOOGLE_GENAI_INTEGRATION_NAME, + ]); instrumentLangChain(options); }, @@ -36,9 +40,9 @@ const _langChainIntegration = ((options: LangChainOptions = {}) => { * When configured, this integration automatically instruments LangChain runnable instances * to capture telemetry data by injecting Sentry callback handlers into all LangChain calls. * - * **Important:** This integration automatically disables the OpenAI, Anthropic, and Google GenAI - * integrations to prevent duplicate spans when using LangChain with these providers. LangChain - * handles the instrumentation for all underlying AI providers. + * **Important:** This integration automatically marks the OpenAI, Anthropic, and Google GenAI + * integrations as disabled to prevent duplicate spans when using LangChain with these providers. + * LangChain handles the instrumentation for all underlying AI providers. * * @example * ```javascript From 846b445061534d1abb0732aa8a3132ffbb013536 Mon Sep 17 00:00:00 2001 From: RulaKhaled Date: Fri, 7 Nov 2025 12:35:51 +0100 Subject: [PATCH 7/8] update with tests --- packages/core/test/lib/integration.test.ts | 134 +++++++++++++++++- .../integrations/tracing/langchain/index.ts | 19 +-- .../tracing/langchain/instrumentation.ts | 14 +- 3 files changed, 149 insertions(+), 18 deletions(-) diff --git a/packages/core/test/lib/integration.test.ts b/packages/core/test/lib/integration.test.ts index 5b7554f261b1..efe92571df16 100644 --- a/packages/core/test/lib/integration.test.ts +++ b/packages/core/test/lib/integration.test.ts @@ -1,6 +1,14 @@ import { afterEach, beforeEach, describe, expect, it, test, vi } from 'vitest'; import { getCurrentScope } from '../../src/currentScopes'; -import { addIntegration, getIntegrationsToSetup, installedIntegrations, setupIntegration } from '../../src/integration'; +import { + _clearDisabledIntegrationsMarks, + _isIntegrationMarkedDisabled, + _markIntegrationsDisabled, + addIntegration, + getIntegrationsToSetup, + installedIntegrations, + setupIntegration, +} from '../../src/integration'; import { setCurrentClient } from '../../src/sdk'; import type { Integration } from '../../src/types-hoist/integration'; import type { Options } from '../../src/types-hoist/options'; @@ -683,3 +691,127 @@ describe('addIntegration', () => { expect(logs).toHaveBeenCalledWith('Integration skipped because it was already installed: test'); }); }); + +describe('Integration marking system', () => { + beforeEach(() => { + // Clear marks before each test + _clearDisabledIntegrationsMarks(); + // Reset installed integrations + installedIntegrations.splice(0, installedIntegrations.length); + }); + + afterEach(() => { + // Clean up after tests + _clearDisabledIntegrationsMarks(); + }); + + describe('_markIntegrationsDisabled', () => { + it('marks a single integration as disabled', () => { + _markIntegrationsDisabled('TestIntegration'); + expect(_isIntegrationMarkedDisabled('TestIntegration')).toBe(true); + }); + + it('marks multiple integrations as disabled using array', () => { + _markIntegrationsDisabled(['Integration1', 'Integration2', 'Integration3']); + expect(_isIntegrationMarkedDisabled('Integration1')).toBe(true); + expect(_isIntegrationMarkedDisabled('Integration2')).toBe(true); + expect(_isIntegrationMarkedDisabled('Integration3')).toBe(true); + }); + + it('does not affect unmarked integrations', () => { + _markIntegrationsDisabled('Integration1'); + expect(_isIntegrationMarkedDisabled('Integration1')).toBe(true); + expect(_isIntegrationMarkedDisabled('Integration2')).toBe(false); + }); + }); + + describe('_isIntegrationMarkedDisabled', () => { + it('returns false for integrations that are not marked', () => { + expect(_isIntegrationMarkedDisabled('SomeIntegration')).toBe(false); + }); + + it('returns true for integrations that are marked', () => { + _markIntegrationsDisabled('MarkedIntegration'); + expect(_isIntegrationMarkedDisabled('MarkedIntegration')).toBe(true); + }); + }); + + describe('_clearDisabledIntegrationsMarks', () => { + it('clears all marks', () => { + _markIntegrationsDisabled(['Integration1', 'Integration2']); + expect(_isIntegrationMarkedDisabled('Integration1')).toBe(true); + expect(_isIntegrationMarkedDisabled('Integration2')).toBe(true); + + _clearDisabledIntegrationsMarks(); + + expect(_isIntegrationMarkedDisabled('Integration1')).toBe(false); + expect(_isIntegrationMarkedDisabled('Integration2')).toBe(false); + }); + + it('removes marked integrations from installedIntegrations list', () => { + // Simulate integrations being installed + installedIntegrations.push('Integration1', 'Integration2', 'Integration3'); + + // Mark some as disabled + _markIntegrationsDisabled(['Integration1', 'Integration3']); + + // Clear should remove marked ones from installed list + _clearDisabledIntegrationsMarks(); + + expect(installedIntegrations).toEqual(['Integration2']); + }); + + it('preserves integrations that are not marked when clearing', () => { + installedIntegrations.push('Integration1', 'Integration2', 'Integration3'); + _markIntegrationsDisabled('Integration2'); + + _clearDisabledIntegrationsMarks(); + + expect(installedIntegrations).toEqual(['Integration1', 'Integration3']); + }); + }); + + describe('setupIntegration with marked integrations', () => { + it('skips setupOnce for marked integrations', () => { + const client = getTestClient(); + const integration = new MockIntegration('MarkedIntegration'); + + // Mark the integration as disabled before setup + _markIntegrationsDisabled('MarkedIntegration'); + + setupIntegration(client, integration, {}); + + // setupOnce should not be called + expect(integration.setupOnce).not.toHaveBeenCalled(); + // Integration should not be in installed list + expect(installedIntegrations).not.toContain('MarkedIntegration'); + }); + + it('calls setupOnce for non-marked integrations', () => { + const client = getTestClient(); + const integration = new MockIntegration('NormalIntegration'); + + setupIntegration(client, integration, {}); + + // setupOnce should be called + expect(integration.setupOnce).toHaveBeenCalledTimes(1); + // Integration should be in installed list + expect(installedIntegrations).toContain('NormalIntegration'); + }); + + it('allows setup after clearing marks', () => { + const client = getTestClient(); + const integration = new MockIntegration('TestIntegration'); + + // Mark, clear, then setup + _markIntegrationsDisabled('TestIntegration'); + _clearDisabledIntegrationsMarks(); + + setupIntegration(client, integration, {}); + + // setupOnce should be called after marks are cleared + expect(integration.setupOnce).toHaveBeenCalledTimes(1); + expect(installedIntegrations).toContain('TestIntegration'); + }); + }); +}); diff --git a/packages/node/src/integrations/tracing/langchain/index.ts b/packages/node/src/integrations/tracing/langchain/index.ts index 07b261a3a306..c374e390a0b5 100644 --- a/packages/node/src/integrations/tracing/langchain/index.ts +++ b/packages/node/src/integrations/tracing/langchain/index.ts @@ -1,12 +1,5 @@ import type { IntegrationFn, LangChainOptions } from '@sentry/core'; -import { - _markIntegrationsDisabled, - ANTHROPIC_AI_INTEGRATION_NAME, - defineIntegration, - GOOGLE_GENAI_INTEGRATION_NAME, - LANGCHAIN_INTEGRATION_NAME, - OPENAI_INTEGRATION_NAME, -} from '@sentry/core'; +import { defineIntegration, LANGCHAIN_INTEGRATION_NAME } from '@sentry/core'; import { generateInstrumentOnce } from '@sentry/node-core'; import { SentryLangChainInstrumentation } from './instrumentation'; @@ -19,14 +12,8 @@ const _langChainIntegration = ((options: LangChainOptions = {}) => { return { name: LANGCHAIN_INTEGRATION_NAME, setupOnce() { - // Mark AI provider integrations as disabled to prevent duplicate spans - // LangChain integration handles instrumentation for all underlying AI providers - _markIntegrationsDisabled([ - OPENAI_INTEGRATION_NAME, - ANTHROPIC_AI_INTEGRATION_NAME, - GOOGLE_GENAI_INTEGRATION_NAME, - ]); - + // Register the instrumentation + // The instrumentation will mark AI providers as disabled when LangChain modules are actually loaded instrumentLangChain(options); }, }; diff --git a/packages/node/src/integrations/tracing/langchain/instrumentation.ts b/packages/node/src/integrations/tracing/langchain/instrumentation.ts index f171a2dfb022..7d352183ba14 100644 --- a/packages/node/src/integrations/tracing/langchain/instrumentation.ts +++ b/packages/node/src/integrations/tracing/langchain/instrumentation.ts @@ -6,7 +6,15 @@ import { InstrumentationNodeModuleFile, } from '@opentelemetry/instrumentation'; import type { LangChainOptions } from '@sentry/core'; -import { createLangChainCallbackHandler, getClient, SDK_VERSION } from '@sentry/core'; +import { + _markIntegrationsDisabled, + ANTHROPIC_AI_INTEGRATION_NAME, + createLangChainCallbackHandler, + getClient, + GOOGLE_GENAI_INTEGRATION_NAME, + OPENAI_INTEGRATION_NAME, + SDK_VERSION, +} from '@sentry/core'; const supportedVersions = ['>=0.1.0 <1.0.0']; @@ -143,6 +151,10 @@ export class SentryLangChainInstrumentation extends InstrumentationBase Date: Fri, 7 Nov 2025 15:28:36 +0100 Subject: [PATCH 8/8] update to catch within patch --- packages/core/src/integration.ts | 17 +-- packages/core/test/lib/integration.test.ts | 133 +++++------------- .../tracing/anthropic-ai/instrumentation.ts | 14 +- .../tracing/google-genai/instrumentation.ts | 17 ++- .../tracing/langchain/instrumentation.ts | 2 - .../tracing/openai/instrumentation.ts | 14 +- 6 files changed, 80 insertions(+), 117 deletions(-) diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index 64cb4f05d382..1bca45ad9321 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -24,11 +24,11 @@ const MARKED_DISABLED_INTEGRATIONS = new Set(); * @param integrationName The name(s) of the integration(s) to mark as disabled */ export function _markIntegrationsDisabled(integrationName: string | string[]): void { - if (Array.isArray(integrationName)) { - integrationName.forEach(name => MARKED_DISABLED_INTEGRATIONS.add(name)); - } else { - MARKED_DISABLED_INTEGRATIONS.add(integrationName); - } + const names = Array.isArray(integrationName) ? integrationName : [integrationName]; + names.forEach(name => { + MARKED_DISABLED_INTEGRATIONS.add(name); + DEBUG_BUILD && debug.log(`Integration marked as disabled: ${name}`); + }); } /** @@ -160,11 +160,8 @@ export function setupIntegration(client: Client, integration: Integration, integ // `setupOnce` is only called the first time if (!installedIntegrations.includes(integration.name) && typeof integration.setupOnce === 'function') { - // Skip setup if integration is marked as disabled - if (!_isIntegrationMarkedDisabled(integration.name)) { - integration.setupOnce(); - installedIntegrations.push(integration.name); - } + integration.setupOnce(); + installedIntegrations.push(integration.name); } // `setup` is run for each client diff --git a/packages/core/test/lib/integration.test.ts b/packages/core/test/lib/integration.test.ts index efe92571df16..9a3c1ba98f2c 100644 --- a/packages/core/test/lib/integration.test.ts +++ b/packages/core/test/lib/integration.test.ts @@ -11,7 +11,7 @@ import { } from '../../src/integration'; import { setCurrentClient } from '../../src/sdk'; import type { Integration } from '../../src/types-hoist/integration'; -import type { Options } from '../../src/types-hoist/options'; +import type { CoreOptions } from '../../src/types-hoist/options'; import { debug } from '../../src/utils/debug-logger'; import { getDefaultTestClientOptions, TestClient } from '../mocks/client'; @@ -40,8 +40,8 @@ class MockIntegration implements Integration { type TestCase = [ string, // test name - Options['defaultIntegrations'], // default integrations - Options['integrations'], // user-provided integrations + CoreOptions['defaultIntegrations'], // default integrations + CoreOptions['integrations'], // user-provided integrations Array, // expected results ]; @@ -694,124 +694,57 @@ describe('addIntegration', () => { describe('Integration marking system', () => { beforeEach(() => { - // Clear marks before each test _clearDisabledIntegrationsMarks(); - // Reset installed integrations installedIntegrations.splice(0, installedIntegrations.length); }); afterEach(() => { - // Clean up after tests _clearDisabledIntegrationsMarks(); }); - describe('_markIntegrationsDisabled', () => { - it('marks a single integration as disabled', () => { - _markIntegrationsDisabled('TestIntegration'); - expect(_isIntegrationMarkedDisabled('TestIntegration')).toBe(true); - }); + it('marks and checks single integration', () => { + expect(_isIntegrationMarkedDisabled('TestIntegration')).toBe(false); - it('marks multiple integrations as disabled using array', () => { - _markIntegrationsDisabled(['Integration1', 'Integration2', 'Integration3']); - expect(_isIntegrationMarkedDisabled('Integration1')).toBe(true); - expect(_isIntegrationMarkedDisabled('Integration2')).toBe(true); - expect(_isIntegrationMarkedDisabled('Integration3')).toBe(true); - }); + _markIntegrationsDisabled('TestIntegration'); - it('does not affect unmarked integrations', () => { - _markIntegrationsDisabled('Integration1'); - expect(_isIntegrationMarkedDisabled('Integration1')).toBe(true); - expect(_isIntegrationMarkedDisabled('Integration2')).toBe(false); - }); + expect(_isIntegrationMarkedDisabled('TestIntegration')).toBe(true); }); - describe('_isIntegrationMarkedDisabled', () => { - it('returns false for integrations that are not marked', () => { - expect(_isIntegrationMarkedDisabled('SomeIntegration')).toBe(false); - }); + it('marks and checks multiple integrations', () => { + _markIntegrationsDisabled(['OpenAI', 'Anthropic', 'GoogleGenAI']); - it('returns true for integrations that are marked', () => { - _markIntegrationsDisabled('MarkedIntegration'); - expect(_isIntegrationMarkedDisabled('MarkedIntegration')).toBe(true); - }); + expect(_isIntegrationMarkedDisabled('OpenAI')).toBe(true); + expect(_isIntegrationMarkedDisabled('Anthropic')).toBe(true); + expect(_isIntegrationMarkedDisabled('GoogleGenAI')).toBe(true); + expect(_isIntegrationMarkedDisabled('Other')).toBe(false); }); - describe('_clearDisabledIntegrationsMarks', () => { - it('clears all marks', () => { - _markIntegrationsDisabled(['Integration1', 'Integration2']); - expect(_isIntegrationMarkedDisabled('Integration1')).toBe(true); - expect(_isIntegrationMarkedDisabled('Integration2')).toBe(true); - - _clearDisabledIntegrationsMarks(); - - expect(_isIntegrationMarkedDisabled('Integration1')).toBe(false); - expect(_isIntegrationMarkedDisabled('Integration2')).toBe(false); - }); - - it('removes marked integrations from installedIntegrations list', () => { - // Simulate integrations being installed - installedIntegrations.push('Integration1', 'Integration2', 'Integration3'); - - // Mark some as disabled - _markIntegrationsDisabled(['Integration1', 'Integration3']); - - // Clear should remove marked ones from installed list - _clearDisabledIntegrationsMarks(); + it('clears marks and removes from installedIntegrations', () => { + // Simulate scenario: integrations installed, some marked for cleanup + installedIntegrations.push('LangChain', 'OpenAI', 'Anthropic', 'Normal'); + _markIntegrationsDisabled(['OpenAI', 'Anthropic']); - expect(installedIntegrations).toEqual(['Integration2']); - }); - - it('preserves integrations that are not marked when clearing', () => { - installedIntegrations.push('Integration1', 'Integration2', 'Integration3'); - _markIntegrationsDisabled('Integration2'); + _clearDisabledIntegrationsMarks(); - _clearDisabledIntegrationsMarks(); + // Marks are cleared + expect(_isIntegrationMarkedDisabled('OpenAI')).toBe(false); + expect(_isIntegrationMarkedDisabled('Anthropic')).toBe(false); - expect(installedIntegrations).toEqual(['Integration1', 'Integration3']); - }); + // Marked integrations removed from installed list (can setup again in new client) + expect(installedIntegrations).toEqual(['LangChain', 'Normal']); }); - describe('setupIntegration with marked integrations', () => { - it('skips setupOnce for marked integrations', () => { - const client = getTestClient(); - const integration = new MockIntegration('MarkedIntegration'); - - // Mark the integration as disabled before setup - _markIntegrationsDisabled('MarkedIntegration'); - - setupIntegration(client, integration, {}); - - // setupOnce should not be called - expect(integration.setupOnce).not.toHaveBeenCalled(); - // Integration should not be in installed list - expect(installedIntegrations).not.toContain('MarkedIntegration'); - }); - - it('calls setupOnce for non-marked integrations', () => { - const client = getTestClient(); - const integration = new MockIntegration('NormalIntegration'); - - setupIntegration(client, integration, {}); + it('handles multi-client scenario correctly', () => { + // First client with LangChain + installedIntegrations.push('LangChain', 'OpenAI'); + _markIntegrationsDisabled('OpenAI'); + expect(_isIntegrationMarkedDisabled('OpenAI')).toBe(true); - // setupOnce should be called - expect(integration.setupOnce).toHaveBeenCalledTimes(1); - // Integration should be in installed list - expect(installedIntegrations).toContain('NormalIntegration'); - }); - - it('allows setup after clearing marks', () => { - const client = getTestClient(); - const integration = new MockIntegration('TestIntegration'); - - // Mark, clear, then setup - _markIntegrationsDisabled('TestIntegration'); - _clearDisabledIntegrationsMarks(); - - setupIntegration(client, integration, {}); + // Second client initialization clears marks + _clearDisabledIntegrationsMarks(); - // setupOnce should be called after marks are cleared - expect(integration.setupOnce).toHaveBeenCalledTimes(1); - expect(installedIntegrations).toContain('TestIntegration'); - }); + // OpenAI can be used standalone in second client + expect(_isIntegrationMarkedDisabled('OpenAI')).toBe(false); + expect(installedIntegrations).toEqual(['LangChain']); // OpenAI removed, can setup fresh }); }); diff --git a/packages/node/src/integrations/tracing/anthropic-ai/instrumentation.ts b/packages/node/src/integrations/tracing/anthropic-ai/instrumentation.ts index d55689415aee..dd5b2c405776 100644 --- a/packages/node/src/integrations/tracing/anthropic-ai/instrumentation.ts +++ b/packages/node/src/integrations/tracing/anthropic-ai/instrumentation.ts @@ -5,7 +5,13 @@ import { InstrumentationNodeModuleDefinition, } from '@opentelemetry/instrumentation'; import type { AnthropicAiClient, AnthropicAiOptions } from '@sentry/core'; -import { getClient, instrumentAnthropicAiClient, SDK_VERSION } from '@sentry/core'; +import { + _isIntegrationMarkedDisabled, + ANTHROPIC_AI_INTEGRATION_NAME, + getClient, + instrumentAnthropicAiClient, + SDK_VERSION, +} from '@sentry/core'; const supportedVersions = ['>=0.19.2 <1.0.0']; @@ -48,6 +54,12 @@ export class SentryAnthropicAiInstrumentation extends InstrumentationBase=0.10.0 <2']; @@ -65,14 +72,18 @@ export class SentryGoogleGenAiInstrumentation extends InstrumentationBase=4.0.0 <6']; @@ -56,6 +62,12 @@ export class SentryOpenAiInstrumentation extends InstrumentationBase(OPENAI_INTEGRATION_NAME);