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/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 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, 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/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, ]; } diff --git a/packages/node/src/integrations/tracing/langchain/index.ts b/packages/node/src/integrations/tracing/langchain/index.ts index e575691b930f..9d8ff0694189 100644 --- a/packages/node/src/integrations/tracing/langchain/index.ts +++ b/packages/node/src/integrations/tracing/langchain/index.ts @@ -1,5 +1,12 @@ import type { IntegrationFn, LangChainOptions } from '@sentry/core'; -import { defineIntegration, LANGCHAIN_INTEGRATION_NAME } from '@sentry/core'; +import { + ANTHROPIC_AI_INTEGRATION_NAME, + defineIntegration, + disableIntegrations, + GOOGLE_GENAI_INTEGRATION_NAME, + LANGCHAIN_INTEGRATION_NAME, + OPENAI_INTEGRATION_NAME, +} from '@sentry/core'; import { generateInstrumentOnce } from '@sentry/node-core'; import { SentryLangChainInstrumentation } from './instrumentation'; @@ -12,6 +19,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 +36,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';