Skip to content
Closed
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
62 changes: 61 additions & 1 deletion packages/node/src/integrations/tracing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ import {
} from '@sentry/server-utils/orchestrion';
import { fastifyIntegration } from './fastify';

export function getAutoPerformanceIntegrations(): Integration[] {
/**
* This explicitly has no return type to ensure this is inferred properly.
* We use this to ensure that AUTO_PERFORMANCE_INTEGRATION_NAMES is in sync with the integrations returned by this function.
*/
function _getAutoPerformanceIntegrations() {
return [
expressIntegration(),
fastifyIntegration(),
Expand Down Expand Up @@ -58,3 +62,59 @@ export function getAutoPerformanceIntegrations(): Integration[] {
firebaseIntegration(),
];
}

export function getAutoPerformanceIntegrations(): Integration[] {
return _getAutoPerformanceIntegrations();
}

/**
* Union of the `name` of every integration returned by {@link _getAutoPerformanceIntegrations}.
* Derived from that function's inferred (intentionally un-annotated) return type, so it stays in
* sync automatically as integrations are added or removed.
*/
type AutoPerformanceIntegrationName = ReturnType<typeof _getAutoPerformanceIntegrations>[number]['name'];

/**
* Builds a readonly tuple that must list **every** member of `T` exactly. A missing member makes the
* call fail to compile — the error names the missing member(s) — while an unknown/misspelled member
* is rejected by the element constraint. This enforces exhaustiveness at the declaration itself, so
* no separate assertion is needed.
*/
const tupleOfAllNames =
<T extends string>() =>
<const U extends readonly T[]>(names: [T] extends [U[number]] ? U : Exclude<T, U[number]>): U =>
names as U;

/**
* The names of all auto performance integrations, as a runtime constant so callers can check for
* these integrations (e.g. to gate channel-based instrumentation) without instantiating them. Typed
* so that adding an integration to {@link _getAutoPerformanceIntegrations} without listing it here
* (or vice versa) is a compile error.
*/
export const AUTO_PERFORMANCE_INTEGRATION_NAMES = tupleOfAllNames<AutoPerformanceIntegrationName>()([
'Express',
'Fastify',
'Graphql',
'Mongo',
'Mongoose',
'Mysql',
'Mysql2',
'Redis',
'Postgres',
'Prisma',
'Hapi',
'Koa',
'Tedious',
'GenericPool',
'Kafka',
'Amqplib',
'LruMemoizer',
'LangChain',
'LangGraph',
'VercelAI',
'OpenAI',
'Anthropic_AI',
'Google_GenAI',
'PostgresJs',
'Firebase',
]);
36 changes: 26 additions & 10 deletions packages/node/src/sdk/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import { onUnhandledRejectionIntegration } from '../integrations/onunhandledreje
import { processSessionIntegration } from '../integrations/processSession';
import { INTEGRATION_NAME as SPOTLIGHT_INTEGRATION_NAME, spotlightIntegration } from '../integrations/spotlight';
import { systemErrorIntegration } from '../integrations/systemError';
import { getAutoPerformanceIntegrations } from '../integrations/tracing';
import { AUTO_PERFORMANCE_INTEGRATION_NAMES, getAutoPerformanceIntegrations } from '../integrations/tracing';
import { makeNodeTransport } from '../transports';
import type { NodeClientOptions, NodeOptions } from '../types';
import { getEntryPointType } from '../utils/entry-point';
Expand Down Expand Up @@ -89,6 +89,20 @@ export function getDefaultIntegrations(options: Options): Integration[] {
];
}

/**
* Whether the user explicitly configured a channel-based (orchestrion) integration via the
* `integrations` option. These integrations (e.g. `expressIntegration()`) can capture errors even
* with tracing off, so their diagnostics-channel module hooks must be installed regardless of spans.
*/
function hasUserConfiguredChannelIntegration(options: NodeClientOptions): boolean {
if (!Array.isArray(options.integrations)) {
return false;
}
return options.integrations.some(integration =>
(AUTO_PERFORMANCE_INTEGRATION_NAMES as readonly string[]).includes(integration.name),
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-orchestrion names gate injection

Low Severity

hasUserConfiguredChannelIntegration treats every AUTO_PERFORMANCE_INTEGRATION_NAMES entry as orchestrion-backed, but Prisma and Fastify are not. Passing only those with tracing off still calls registerDiagnosticsChannelInjection, installing module hooks that those integrations never use.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0af2bbc. Configure here.


/**
* Initialize Sentry for Node.
*/
Expand Down Expand Up @@ -154,20 +168,22 @@ function _init(
tracesSampleRate: getTracesSampleRate(options.tracesSampleRate),
};

// Gate channel-based (orchestrion diagnostics-channel) instrumentation on span recording: the
// channel integrations only produce spans, so with tracing off there are no subscribers and
// injecting the module hooks would be pointless work. Install the hooks as early as possible,
// before the app imports its instrumented modules.
const useChannelInjection = hasSpansEnabled(optionsWithResolvedTracing);
if (useChannelInjection) {
registerDiagnosticsChannelInjection();
}

// Only use Node SDK defaults if none provided.
const defaultIntegrations = options.defaultIntegrations ?? getDefaultIntegrationsImpl(optionsWithResolvedTracing);

const clientOptions = getClientOptions({ ...options, defaultIntegrations }, getDefaultIntegrationsImpl);

// Gate channel-based (orchestrion diagnostics-channel) instrumentation. Register the module hooks
// when tracing is on (the channel integrations produce spans) OR when the user explicitly added a
// channel-based integration (e.g. `expressIntegration()`) via `integrations` — those can capture
// errors even with tracing off, so their subscribers must fire regardless of spans. Install the
// hooks as early as possible, before the app imports its instrumented modules.
const useChannelInjection =
hasSpansEnabled(optionsWithResolvedTracing) || hasUserConfiguredChannelIntegration(clientOptions);
if (useChannelInjection) {
registerDiagnosticsChannelInjection();
}

const scope = getCurrentScope();
scope.update(clientOptions.initialScope);

Expand Down
35 changes: 33 additions & 2 deletions packages/node/test/sdk/diagnosticsChannelInjection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ vi.mock('@sentry/server-utils/orchestrion', async importOriginal => {
return { ...actual, detectOrchestrionSetup };
});

import { expressIntegration } from '../../src';
import { init } from '../../src/sdk';
import { cleanupOtel, resetGlobals } from '../helpers/mockSdkInit';

Expand All @@ -23,8 +24,9 @@ declare var global: any;
const PUBLIC_DSN = 'https://username@domain/123';

// Channel-based (orchestrion diagnostics-channel) instrumentation is the default in v11: `init()`
// installs the injection hooks unconditionally when span recording is enabled, and skips them when
// tracing is off (there would be no channel subscribers to feed).
// installs the injection hooks when span recording is enabled, or when a channel-based integration
// (e.g. `expressIntegration()`) is configured — those can capture errors even with tracing off. With
// tracing off and no such integration there are no channel subscribers, so the hooks are skipped.
describe('diagnostics-channel injection default', () => {
beforeEach(() => {
global.__SENTRY__ = {};
Expand All @@ -50,4 +52,33 @@ describe('diagnostics-channel injection default', () => {
expect(registerDiagnosticsChannelInjection).not.toHaveBeenCalled();
expect(detectOrchestrionSetup).not.toHaveBeenCalled();
});

it('registers the injection hooks when a channel-based integration is configured, even with tracing disabled', () => {
init({ dsn: PUBLIC_DSN, enableOpenTelemetrySetup: false, integrations: [expressIntegration()] });

expect(registerDiagnosticsChannelInjection).toHaveBeenCalledTimes(1);
expect(detectOrchestrionSetup).toHaveBeenCalledTimes(1);
});

it('registers the injection hooks when a channel-based integration is added via an `integrations` function', () => {
init({
dsn: PUBLIC_DSN,
enableOpenTelemetrySetup: false,
integrations: defaults => [...defaults, expressIntegration()],
});

expect(registerDiagnosticsChannelInjection).toHaveBeenCalledTimes(1);
expect(detectOrchestrionSetup).toHaveBeenCalledTimes(1);
});

it('does not register the injection hooks when only non-channel integrations are configured and tracing is disabled', () => {
init({
dsn: PUBLIC_DSN,
enableOpenTelemetrySetup: false,
integrations: [{ name: 'CustomNonChannelIntegration', setup: () => undefined }],
});

expect(registerDiagnosticsChannelInjection).not.toHaveBeenCalled();
expect(detectOrchestrionSetup).not.toHaveBeenCalled();
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing integration or E2E test

Medium Severity

This is a feat PR, but coverage is only mocked unit tests around registerDiagnosticsChannelInjection. Per the PR review guidelines, a feat needs at least one integration or E2E test. An integration test that init with a channel integration and tracing off, then asserts real error capture, would lock in the production path this change is meant to fix.

Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit 0af2bbc. Configure here.

});
Loading