Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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')) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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')) {
Expand Down
11 changes: 10 additions & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
66 changes: 64 additions & 2 deletions packages/core/src/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>();

/**
* 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;
Expand Down Expand Up @@ -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
Expand Down
18 changes: 17 additions & 1 deletion packages/node-core/src/sdk/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -145,6 +152,15 @@ export class NodeClient extends ServerRuntimeClient<NodeClientOptions> {
}
}

/** @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,
Expand Down
10 changes: 6 additions & 4 deletions packages/node/src/integrations/tracing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
];
}

Expand Down Expand Up @@ -89,12 +91,12 @@ export function getOpenTelemetryInstrumentationToPreload(): (((options?: any) =>
instrumentTedious,
instrumentGenericPool,
instrumentAmqplib,
instrumentLangChain,
instrumentVercelAi,
instrumentOpenAi,
instrumentPostgresJs,
instrumentFirebase,
instrumentAnthropicAi,
instrumentGoogleGenAI,
instrumentLangChain,
];
}
31 changes: 29 additions & 2 deletions packages/node/src/integrations/tracing/langchain/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
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,
getClient,
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<LangChainOptions>(
Expand All @@ -12,6 +19,22 @@ const _langChainIntegration = ((options: LangChainOptions = {}) => {
return {
name: LANGCHAIN_INTEGRATION_NAME,
setupOnce() {
// Only disable AI provider integrations if they weren't explicitly requested by the user
// 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);
}

instrumentLangChain(options);
},
};
Expand All @@ -25,6 +48,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';
Expand Down
Loading