diff --git a/.size-limit.js b/.size-limit.js index cad73f5f5ceb..288258df7c06 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -430,7 +430,7 @@ module.exports = [ path: 'packages/node/build/esm/index.js', import: createImport('initWithoutDefaultIntegrations', 'getDefaultIntegrationsWithoutPerformance'), gzip: true, - limit: '87 KB', + limit: '92 KB', disablePlugins: ['@size-limit/esbuild'], ignore: [...builtinModules, ...nodePrefixedBuiltinModules], modifyWebpackConfig: function (config) { @@ -454,7 +454,7 @@ module.exports = [ import: createImport('init'), ignore: [...builtinModules, ...nodePrefixedBuiltinModules], gzip: true, - limit: '97 KB', + limit: '99 KB', disablePlugins: ['@size-limit/esbuild'], }, // Cloudflare SDK (ESM) - compressed, minified to match `wrangler deploy --dry-run --minify` output diff --git a/dev-packages/deno-integration-tests/src/index.ts b/dev-packages/deno-integration-tests/src/index.ts index bc6b80087901..cef98cfc8ee5 100644 --- a/dev-packages/deno-integration-tests/src/index.ts +++ b/dev-packages/deno-integration-tests/src/index.ts @@ -1,4 +1,4 @@ -import type { TransactionEvent } from '@sentry/core'; +import type { Event, TransactionEvent } from '@sentry/core'; import { getAsyncContextStrategy, getMainCarrier, setAsyncContextStrategy } from '@sentry/core'; /** @@ -16,22 +16,18 @@ export function resetGlobals(): void { setAsyncContextStrategy(acs); } -export interface TransactionSink { - beforeSendTransaction: (event: TransactionEvent) => null; - waitFor: (predicate: (event: TransactionEvent) => boolean) => Promise; +interface EventSink { + beforeSend: (event: T) => null; + waitFor: (predicate: (event: T) => boolean) => Promise; } -/** - * A `beforeSendTransaction` hook that records every transaction and lets a test - * `await` the first one matching a predicate. `waitFor` resolves immediately if - * a match already arrived, so there is no ordering race with the hook. - */ -export function transactionSink(): TransactionSink { - const transactions: TransactionEvent[] = []; - const waiters: { predicate: (e: TransactionEvent) => boolean; resolve: (e: TransactionEvent) => void }[] = []; +function eventSink(): EventSink { + const events: T[] = []; + const waiters: { predicate: (e: T) => boolean; resolve: (e: T) => void }[] = []; return { - beforeSendTransaction(event) { - transactions.push(event); + beforeSend(event) { + events.push(event); + for (let i = waiters.length - 1; i >= 0; i--) { const w = waiters[i]!; if (w.predicate(event)) { @@ -39,18 +35,45 @@ export function transactionSink(): TransactionSink { w.resolve(event); } } + return null; }, waitFor(predicate) { - const already = transactions.find(predicate); + const already = events.find(predicate); if (already) return Promise.resolve(already); - return new Promise(resolve => { + return new Promise(resolve => { waiters.push({ predicate, resolve }); }); }, }; } +/** + * A `beforeSend` hook that records every transaction event and lets a test + * `await` the first one matching a predicate. `waitFor` resolves immediately if + * a match already arrived, so there is no ordering race with the hook. + */ +export function transactionSink(): { + waitFor: (predicate: (event: TransactionEvent) => boolean) => Promise; + beforeSendTransaction: (event: TransactionEvent) => null; +} { + const sink = eventSink(); + + return { + waitFor: sink.waitFor, + beforeSendTransaction: sink.beforeSend, + }; +} + +/** + * A `beforeSend` hook that records every error and lets a test + * `await` the first one matching a predicate. `waitFor` resolves immediately if + * a match already arrived, so there is no ordering race with the hook. + */ +export function errorSink(): EventSink { + return eventSink(); +} + /** Reject with a descriptive message if `p` does not settle within `ms`. */ export function withTimeout(p: Promise, ms: number, what: string): Promise { let timer: ReturnType | undefined; diff --git a/dev-packages/deno-integration-tests/suites/orchestrion-fastify/test.ts b/dev-packages/deno-integration-tests/suites/orchestrion-fastify/test.ts new file mode 100644 index 000000000000..276a74eda91c --- /dev/null +++ b/dev-packages/deno-integration-tests/suites/orchestrion-fastify/test.ts @@ -0,0 +1,47 @@ +// + +import { channel } from 'node:diagnostics_channel'; +import type { DenoClient } from '@sentry/deno'; +import { init } from '@sentry/deno'; +import { assert } from 'https://deno.land/std@0.212.0/assert/assert.ts'; +import { assertEquals } from 'https://deno.land/std@0.212.0/assert/assert_equals.ts'; +import { assertExists } from 'https://deno.land/std@0.212.0/assert/assert_exists.ts'; +import { errorSink, resetGlobals, withTimeout } from '../../src/index.ts'; + +Deno.test('fastify instrumentation: included in default integrations (Deno 2.8.0+)', () => { + resetGlobals(); + const client = init({ traceLifecycle: 'static', dsn: 'https://username@domain/123' }) as DenoClient; + const names = client.getOptions().integrations.map(i => i.name); + assert(names.includes('Fastify'), `Fastify should be in defaults, got ${names.join(', ')}`); +}); + +Deno.test('fastify instrumentation: tracing:fastify.request.handler:error channel captures the error', async () => { + resetGlobals(); + const sink = errorSink(); + init({ + traceLifecycle: 'static', + dsn: 'https://username@domain/123', + beforeSend: sink.beforeSend, + }); + + const error = new Error('fastify boom'); + + // Fastify v5 publishes this native diagnostics channel when a request handler errors; the + // integration subscribes to it directly (no orchestrion injection needed). A 5xx reply passes the + // default `shouldHandleError`, so the error is captured. + channel('tracing:fastify.request.handler:error').publish({ + error, + request: { method: 'GET', routeOptions: { url: '/boom' } }, + reply: { statusCode: 500 }, + }); + + const event = await withTimeout( + sink.waitFor(e => e.exception?.values?.[0]?.value === 'fastify boom'), + 5000, + "the captured 'fastify boom' error", + ); + + assertExists(event.exception?.values?.[0]); + assertEquals(event.exception?.values?.[0]?.mechanism?.type, 'auto.function.fastify'); + assertEquals(event.exception?.values?.[0]?.mechanism?.handled, false); +}); diff --git a/dev-packages/node-integration-tests/suites/anr/stop-and-start.js b/dev-packages/node-integration-tests/suites/anr/stop-and-start.js index 6f1e4a7d6339..966f3b227e87 100644 --- a/dev-packages/node-integration-tests/suites/anr/stop-and-start.js +++ b/dev-packages/node-integration-tests/suites/anr/stop-and-start.js @@ -1,4 +1,5 @@ const Sentry = require('@sentry/node'); +const { waitForDebuggerReady } = require('@sentry-internal/test-utils'); setTimeout(() => { process.exit(); @@ -52,7 +53,9 @@ setTimeout(() => { setTimeout(() => { anr.startWorker(); - setTimeout(() => { + // Wait for the restarted worker's debugger session to reconnect before blocking the event + // loop, otherwise on slow CI the worker isn't ready to sample and the ANR is missed entirely. + waitForDebuggerReady(() => { longWork(); }); }, 2000); diff --git a/packages/bun/src/sdk.ts b/packages/bun/src/sdk.ts index ce5e08872542..ba77f87ef9a9 100644 --- a/packages/bun/src/sdk.ts +++ b/packages/bun/src/sdk.ts @@ -12,7 +12,6 @@ import type { NodeClient } from '@sentry/node'; import { consoleIntegration, contextLinesIntegration, - getAutoPerformanceIntegrations, httpIntegration, init as initNode, modulesIntegration, @@ -26,6 +25,7 @@ import { fetchIntegration } from './integrations/fetch'; import { makeFetchTransport } from './transports'; import type { BunOptions } from './types'; import { bunHttpServerIntegration } from './integrations/bunHttpServer'; +import { getErrorIntegrations, getTracingIntegrations } from '@sentry/server-utils'; /** * The performance integrations for bun: the OTel auto-performance set, but with @@ -40,7 +40,7 @@ function getPerformanceIntegrations(options: Options): Integration[] { return []; } - return getAutoPerformanceIntegrations(); + return getTracingIntegrations(); } /** Get the default integrations for the Bun SDK, excluding performance integrations. */ @@ -64,6 +64,9 @@ export function getDefaultIntegrationsWithoutPerformance(): Integration[] { nodeContextIntegration(), modulesIntegration(), processSessionIntegration(), + // Framework-level integrations. These are not performance-only: they also handle error capture, so + // they are added by default rather than gated behind tracing + ...getErrorIntegrations(), // Bun Specific bunServerIntegration(), bunHttpServerIntegration(), diff --git a/packages/bun/test/init.test.ts b/packages/bun/test/init.test.ts index 022d7a95043f..abf3aabf060e 100644 --- a/packages/bun/test/init.test.ts +++ b/packages/bun/test/init.test.ts @@ -1,5 +1,5 @@ import { type Integration } from '@sentry/core'; -import * as sentryNode from '@sentry/node'; +import * as sentryServerUtils from '@sentry/server-utils'; import type { Mock } from 'bun:test'; import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from 'bun:test'; import { @@ -22,15 +22,14 @@ class MockIntegration implements Integration { } describe('init()', () => { - let mockAutoPerformanceIntegrations: Mock<() => Integration[]>; + let mockGetTracingIntegrations: Mock<() => Integration[]>; beforeEach(() => { - // @ts-expect-error weird - mockAutoPerformanceIntegrations = spyOn(sentryNode, 'getAutoPerformanceIntegrations'); + mockGetTracingIntegrations = spyOn(sentryServerUtils, 'getTracingIntegrations'); }); afterEach(() => { - mockAutoPerformanceIntegrations.mockRestore(); + mockGetTracingIntegrations.mockRestore(); }); describe('integrations', () => { @@ -41,7 +40,7 @@ describe('init()', () => { expect(client?.getOptions().integrations).toEqual([]); - expect(mockAutoPerformanceIntegrations).toHaveBeenCalledTimes(0); + expect(mockGetTracingIntegrations).toHaveBeenCalledTimes(0); }); it('enables spotlight with default URL from config `true`', () => { @@ -75,7 +74,7 @@ describe('init()', () => { expect(mockDefaultIntegrations[1]?.setupOnce).toHaveBeenCalledTimes(1); expect(mockIntegrations[0]?.setupOnce).toHaveBeenCalledTimes(1); expect(mockIntegrations[1]?.setupOnce).toHaveBeenCalledTimes(1); - expect(mockAutoPerformanceIntegrations).toHaveBeenCalledTimes(0); + expect(mockGetTracingIntegrations).toHaveBeenCalledTimes(0); }); it('installs integrations returned from a callback function', () => { @@ -99,12 +98,12 @@ describe('init()', () => { expect(mockDefaultIntegrations[0]?.setupOnce).toHaveBeenCalledTimes(1); expect(mockDefaultIntegrations[1]?.setupOnce).toHaveBeenCalledTimes(0); expect(newIntegration.setupOnce).toHaveBeenCalledTimes(1); - expect(mockAutoPerformanceIntegrations).toHaveBeenCalledTimes(0); + expect(mockGetTracingIntegrations).toHaveBeenCalledTimes(0); }); it('installs performance default instrumentations if tracing is enabled', () => { const autoPerformanceIntegrations = [new MockIntegration('Performance integration')]; - mockAutoPerformanceIntegrations.mockImplementation(() => autoPerformanceIntegrations); + mockGetTracingIntegrations.mockImplementation(() => autoPerformanceIntegrations); const mockIntegrations = [ new MockIntegration('Some mock integration 4.1'), @@ -120,7 +119,7 @@ describe('init()', () => { expect(mockIntegrations[0]?.setupOnce).toHaveBeenCalledTimes(1); expect(mockIntegrations[1]?.setupOnce).toHaveBeenCalledTimes(1); expect(autoPerformanceIntegrations[0]?.setupOnce).toHaveBeenCalledTimes(1); - expect(mockAutoPerformanceIntegrations).toHaveBeenCalledTimes(1); + expect(mockGetTracingIntegrations).toHaveBeenCalledTimes(1); const integrations = getClient()?.getOptions().integrations; expect(integrations).toBeArray(); @@ -137,7 +136,7 @@ describe('init()', () => { const client = getClient(); expect(client?.getOptions().integrations).toEqual([]); - expect(mockAutoPerformanceIntegrations).toHaveBeenCalledTimes(0); + expect(mockGetTracingIntegrations).toHaveBeenCalledTimes(0); }); it('still installs user-provided integrations', () => { @@ -162,12 +161,12 @@ describe('init()', () => { const full = getDefaultIntegrations({}).map(({ name }) => name); expect(withoutPerformance).toEqual(full); - expect(mockAutoPerformanceIntegrations).toHaveBeenCalledTimes(0); + expect(mockGetTracingIntegrations).toHaveBeenCalledTimes(0); }); it('omits the performance integrations that the full set adds when tracing is enabled', () => { const performanceIntegration = new MockIntegration('Performance integration'); - mockAutoPerformanceIntegrations.mockImplementation(() => [performanceIntegration]); + mockGetTracingIntegrations.mockImplementation(() => [performanceIntegration]); const withoutPerformance = getDefaultIntegrationsWithoutPerformance().map(({ name }) => name); const full = getDefaultIntegrations({ tracesSampleRate: 1 }).map(({ name }) => name); diff --git a/packages/deno/src/sdk.ts b/packages/deno/src/sdk.ts index b4867e6f61b4..0b33f933e337 100644 --- a/packages/deno/src/sdk.ts +++ b/packages/deno/src/sdk.ts @@ -11,32 +11,7 @@ import { requestDataIntegration, stackParserFromStackParserOptions, } from '@sentry/core'; -import { - amqplibIntegration, - anthropicAIIntegration, - awsIntegration, - expressIntegration, - firebaseIntegration, - genericPoolIntegration, - googleGenAIIntegration, - graphqlIntegration, - hapiIntegration, - kafkaIntegration, - koaIntegration, - langChainIntegration, - langGraphIntegration, - lruMemoizerIntegration, - mongoIntegration, - mongooseIntegration, - mysqlIntegration, - mysql2Integration, - openAIIntegration, - postgresIntegration, - postgresJsIntegration, - tediousIntegration, - vercelAIIntegration, - redisIntegration, -} from '@sentry/server-utils'; +import { getTracingIntegrations, getErrorIntegrations } from '@sentry/server-utils'; import { DenoClient } from './client'; import { breadcrumbsIntegration } from './integrations/breadcrumbs'; import { denoContextIntegration } from './integrations/context'; @@ -64,39 +39,12 @@ export function getDefaultIntegrations(_options: Options): Integration[] { denoContextIntegration(), denoServeIntegration(), denoHttpIntegration(), - redisIntegration(), - graphqlIntegration(), - vercelAIIntegration(), - // orchestrion-based instrumentations. We add a deliberate list here rather - // than every channel integration: each one needs a Deno test proving it - // records spans. - // - // The orchestrion channels may be injected after (or while) the SDK loads. - // If they never load, these are no-ops. - amqplibIntegration(), - anthropicAIIntegration(), - awsIntegration(), - expressIntegration(), - firebaseIntegration(), - genericPoolIntegration(), - googleGenAIIntegration(), - hapiIntegration(), - kafkaIntegration(), - koaIntegration(), - langChainIntegration(), - langGraphIntegration(), - lruMemoizerIntegration(), - mongoIntegration(), - mongooseIntegration(), - mysqlIntegration(), - mysql2Integration(), - openAIIntegration(), - postgresIntegration(), - postgresJsIntegration(), - tediousIntegration(), contextLinesIntegration(), normalizePathsIntegration(), globalHandlersIntegration(), + // server-utils integrations + ...getErrorIntegrations(), + ...getTracingIntegrations(), ]; } diff --git a/packages/deno/test/__snapshots__/mod.test.ts.snap b/packages/deno/test/__snapshots__/mod.test.ts.snap index 5e6a7f16eee3..4ce1aea47cd0 100644 --- a/packages/deno/test/__snapshots__/mod.test.ts.snap +++ b/packages/deno/test/__snapshots__/mod.test.ts.snap @@ -56,33 +56,35 @@ snapshot[`captureMessage 1`] = ` "DenoContext", "DenoServe", "DenoHttp", - "Redis", - "Graphql", - "VercelAI", - "Amqplib", - "Anthropic_AI", - "Aws", + "ContextLines", + "NormalizePaths", + "GlobalHandlers", "Express", - "Firebase", - "GenericPool", - "Google_GenAI", + "Fastify", "Hapi", - "Kafka", "Koa", - "LangChain", - "LangGraph", - "LruMemoizer", + "Graphql", "Mongo", "Mongoose", "Mysql", "Mysql2", - "OpenAI", + "Redis", "Postgres", - "PostgresJs", + "Prisma", "Tedious", - "ContextLines", - "NormalizePaths", - "GlobalHandlers", + "GenericPool", + "Kafka", + "Amqplib", + "LruMemoizer", + "Aws", + "LangChain", + "LangGraph", + "VercelAI", + "OpenAI", + "Anthropic_AI", + "Google_GenAI", + "PostgresJs", + "Firebase", ], name: "sentry.javascript.deno", packages: [ @@ -161,33 +163,35 @@ snapshot[`captureMessage twice 1`] = ` "DenoContext", "DenoServe", "DenoHttp", - "Redis", - "Graphql", - "VercelAI", - "Amqplib", - "Anthropic_AI", - "Aws", + "ContextLines", + "NormalizePaths", + "GlobalHandlers", "Express", - "Firebase", - "GenericPool", - "Google_GenAI", + "Fastify", "Hapi", - "Kafka", "Koa", - "LangChain", - "LangGraph", - "LruMemoizer", + "Graphql", "Mongo", "Mongoose", "Mysql", "Mysql2", - "OpenAI", + "Redis", "Postgres", - "PostgresJs", + "Prisma", "Tedious", - "ContextLines", - "NormalizePaths", - "GlobalHandlers", + "GenericPool", + "Kafka", + "Amqplib", + "LruMemoizer", + "Aws", + "LangChain", + "LangGraph", + "VercelAI", + "OpenAI", + "Anthropic_AI", + "Google_GenAI", + "PostgresJs", + "Firebase", ], name: "sentry.javascript.deno", packages: [ @@ -273,33 +277,35 @@ snapshot[`captureMessage twice 2`] = ` "DenoContext", "DenoServe", "DenoHttp", - "Redis", - "Graphql", - "VercelAI", - "Amqplib", - "Anthropic_AI", - "Aws", + "ContextLines", + "NormalizePaths", + "GlobalHandlers", "Express", - "Firebase", - "GenericPool", - "Google_GenAI", + "Fastify", "Hapi", - "Kafka", "Koa", - "LangChain", - "LangGraph", - "LruMemoizer", + "Graphql", "Mongo", "Mongoose", "Mysql", "Mysql2", - "OpenAI", + "Redis", "Postgres", - "PostgresJs", + "Prisma", "Tedious", - "ContextLines", - "NormalizePaths", - "GlobalHandlers", + "GenericPool", + "Kafka", + "Amqplib", + "LruMemoizer", + "Aws", + "LangChain", + "LangGraph", + "VercelAI", + "OpenAI", + "Anthropic_AI", + "Google_GenAI", + "PostgresJs", + "Firebase", ], name: "sentry.javascript.deno", packages: [ diff --git a/packages/node/src/integrations/tracing/index.ts b/packages/node/src/integrations/tracing/index.ts index e01d0d36bd38..c22d22451271 100644 --- a/packages/node/src/integrations/tracing/index.ts +++ b/packages/node/src/integrations/tracing/index.ts @@ -1,60 +1,6 @@ import type { Integration } from '@sentry/core'; -import { - prismaIntegration, - amqplibIntegration, - anthropicAIIntegration, - expressIntegration, - firebaseIntegration, - genericPoolIntegration, - googleGenAIIntegration, - graphqlIntegration, - hapiIntegration, - kafkaIntegration, - koaIntegration, - langChainIntegration, - langGraphIntegration, - lruMemoizerIntegration, - mongoIntegration, - mongooseIntegration, - mysqlIntegration, - mysql2Integration, - openAIIntegration, - postgresIntegration, - postgresJsIntegration, - redisIntegration, - tediousIntegration, - vercelAIIntegration, -} from '@sentry/server-utils'; -import { fastifyIntegration } from './fastify'; +import { getTracingIntegrations } from '@sentry/server-utils'; export function getAutoPerformanceIntegrations(): Integration[] { - return [ - expressIntegration(), - fastifyIntegration(), - graphqlIntegration(), - mongoIntegration(), - mongooseIntegration(), - mysqlIntegration(), - mysql2Integration(), - redisIntegration(), - postgresIntegration(), - prismaIntegration(), - hapiIntegration(), - koaIntegration(), - tediousIntegration(), - genericPoolIntegration(), - kafkaIntegration(), - amqplibIntegration(), - lruMemoizerIntegration(), - // AI providers - // LangChain must come first to disable AI provider integrations before they instrument - langChainIntegration(), - langGraphIntegration(), - vercelAIIntegration(), - openAIIntegration(), - anthropicAIIntegration(), - googleGenAIIntegration(), - postgresJsIntegration(), - firebaseIntegration(), - ]; + return getTracingIntegrations(); } diff --git a/packages/node/src/sdk/index.ts b/packages/node/src/sdk/index.ts index badfe207bc8c..b4249a0ec89a 100644 --- a/packages/node/src/sdk/index.ts +++ b/packages/node/src/sdk/index.ts @@ -16,7 +16,7 @@ import { stackParserFromStackParserOptions, } from '@sentry/core'; import { isMainThread, parentPort } from 'node:worker_threads'; -import { detectOrchestrionSetup } from '@sentry/server-utils'; +import { detectOrchestrionSetup, getErrorIntegrations, getTracingIntegrations } from '@sentry/server-utils'; import { registerDiagnosticsChannelInjection } from '@sentry/server-utils/orchestrion/register'; import { DEBUG_BUILD } from '../debug-build'; import { childProcessIntegration } from '../integrations/childProcess'; @@ -32,7 +32,6 @@ 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 { workerThreadsIntegration } from '../integrations/workerThreads'; import { makeNodeTransport } from '../transports'; import type { NodeClientOptions, NodeOptions } from '../types'; @@ -69,6 +68,8 @@ function getBaseDefaultIntegrations(): Integration[] { workerThreadsIntegration(), processSessionIntegration(), modulesIntegration(), + // Framework-level integrations + ...getErrorIntegrations(), ]; } @@ -83,11 +84,8 @@ export function getDefaultIntegrationsWithoutPerformance(): Integration[] { export function getDefaultIntegrations(options: Options): Integration[] { return [ ...getDefaultIntegrationsWithoutPerformance(), - // We only add performance integrations if tracing is enabled - // Note that this means that without tracing enabled, e.g. `expressIntegration()` will not be added - // This means that generally request isolation will work (because that is done by httpIntegration) - // But `transactionName` will not be set automatically - ...(hasSpansEnabled(options) ? getAutoPerformanceIntegrations() : []), + // We only add tracing-only integrations if tracing is enabled + ...(hasSpansEnabled(options) ? getTracingIntegrations() : []), ]; } @@ -147,20 +145,20 @@ function _init( } } - // Resolve the tracing-affecting options (e.g. `SENTRY_TRACES_SAMPLE_RATE`) up front so that both - // the span-enablement gate below and default-integration selection see the final values. Without - // this, enabling tracing purely via env would leave `hasSpansEnabled` false at this point and skip - // the performance integrations. `getClientOptions` resolves the remaining options later. + // Resolve the tracing-affecting options (e.g. `SENTRY_TRACES_SAMPLE_RATE`) up front so that + // default-integration selection sees the final values. Without this, enabling tracing purely via + // env would leave `hasSpansEnabled` false at this point and skip the performance integrations. + // `getClientOptions` resolves the remaining options later. const optionsWithResolvedTracing = { ...options, 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); + // Install the channel-based (orchestrion diagnostics-channel) instrumentation hooks by default, + // independent of tracing — the channel integrations also capture errors, not just spans. Opt out + // with `enableRuntimeChannelInjection: false`. Install as early as possible, before the app imports + // its instrumented modules. + const useChannelInjection = options.enableRuntimeChannelInjection !== false; if (useChannelInjection) { registerDiagnosticsChannelInjection(); } @@ -216,9 +214,7 @@ function _init( // Warn about missing or doubled channel injection. Runs after the client // is created so the debug logger is enabled and the warning is emitted. - if (useChannelInjection) { - detectOrchestrionSetup(); - } + detectOrchestrionSetup(); return client; } diff --git a/packages/node/src/types.ts b/packages/node/src/types.ts index c15ba570b141..42c78ea5b556 100644 --- a/packages/node/src/types.ts +++ b/packages/node/src/types.ts @@ -22,6 +22,18 @@ export interface BaseNodeOptions extends ServerRuntimeOptions { */ enableOpenTelemetrySetup?: boolean; + /** + * Controls whether the SDK installs its runtime diagnostics-channel injection hooks. These hooks + * transform supported modules (e.g. Express) at load time so they emit the diagnostics channels + * that the channel-based integrations subscribe to. + * + * Set this to `false` to opt out — for example when the channels are injected at build + * time via the bundler plugin, or when the runtime module hooks are unavailable. + * + * @default true + */ + enableRuntimeChannelInjection?: boolean; + /** * Override the runtime name reported in events. * Defaults to 'node' with the current process version if not specified. diff --git a/packages/node/test/sdk/diagnosticsChannelInjection.test.ts b/packages/node/test/sdk/diagnosticsChannelInjection.test.ts index cbeee7171f7f..5132f8cf6544 100644 --- a/packages/node/test/sdk/diagnosticsChannelInjection.test.ts +++ b/packages/node/test/sdk/diagnosticsChannelInjection.test.ts @@ -22,10 +22,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). -describe('diagnostics-channel injection default', () => { +// Runtime diagnostics-channel injection is installed by default, independent of tracing (the channel +// integrations capture errors as well as spans). It can be turned off via `enableRuntimeChannelInjection: false`. +describe('diagnostics-channel injection', () => { beforeEach(() => { global.__SENTRY__ = {}; vi.spyOn(debug, 'enable').mockImplementation(() => undefined); @@ -37,17 +36,36 @@ describe('diagnostics-channel injection default', () => { vi.clearAllMocks(); }); - it('registers the injection hooks and runs detection when span recording is enabled', () => { + it('registers the injection hooks and runs detection by default with tracing enabled', () => { init({ dsn: PUBLIC_DSN, tracesSampleRate: 1, enableOpenTelemetrySetup: false }); expect(registerDiagnosticsChannelInjection).toHaveBeenCalledTimes(1); expect(detectOrchestrionSetup).toHaveBeenCalledTimes(1); }); - it('does not register the injection hooks when tracing is disabled', () => { + it('registers the injection hooks by default even when tracing is disabled', () => { init({ dsn: PUBLIC_DSN, enableOpenTelemetrySetup: false }); + expect(registerDiagnosticsChannelInjection).toHaveBeenCalledTimes(1); + expect(detectOrchestrionSetup).toHaveBeenCalledTimes(1); + }); + + it('does not register the injection hooks but still runs detection when `enableRuntimeChannelInjection` is false', () => { + init({ + dsn: PUBLIC_DSN, + tracesSampleRate: 1, + enableRuntimeChannelInjection: false, + enableOpenTelemetrySetup: false, + }); + expect(registerDiagnosticsChannelInjection).not.toHaveBeenCalled(); - expect(detectOrchestrionSetup).not.toHaveBeenCalled(); + expect(detectOrchestrionSetup).toHaveBeenCalledTimes(1); + }); + + it('registers the injection hooks when `enableRuntimeChannelInjection` is true and tracing is disabled', () => { + init({ dsn: PUBLIC_DSN, enableRuntimeChannelInjection: true, enableOpenTelemetrySetup: false }); + + expect(registerDiagnosticsChannelInjection).toHaveBeenCalledTimes(1); + expect(detectOrchestrionSetup).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/node/test/sdk/init.test.ts b/packages/node/test/sdk/init.test.ts index b16ad41c41b4..3a9af657e3d9 100644 --- a/packages/node/test/sdk/init.test.ts +++ b/packages/node/test/sdk/init.test.ts @@ -4,7 +4,6 @@ import * as SentryOpentelemetry from '@sentry/opentelemetry'; import * as SentryServerUtils from '@sentry/server-utils'; import { afterEach, beforeEach, describe, expect, it, type Mock, type MockInstance, vi } from 'vitest'; import { getClient, NodeClient } from '../../src/'; -import * as auto from '../../src/integrations/tracing'; import { init } from '../../src/sdk'; import { cleanupOtel } from '../helpers/mockSdkInit'; @@ -24,7 +23,7 @@ class MockIntegration implements Integration { } describe('init()', () => { - let mockAutoPerformanceIntegrations: MockInstance = vi.fn(() => []); + let mockGetTracingIntegrations: MockInstance = vi.fn(() => []); beforeEach(() => { global.__SENTRY__ = {}; @@ -32,7 +31,7 @@ describe('init()', () => { // prevent the debug from being enabled, resulting in console.log calls vi.spyOn(debug, 'enable').mockImplementation(() => {}); - mockAutoPerformanceIntegrations = vi.spyOn(auto, 'getAutoPerformanceIntegrations').mockImplementation(() => []); + mockGetTracingIntegrations = vi.spyOn(SentryServerUtils, 'getTracingIntegrations').mockImplementation(() => []); }); afterEach(() => { @@ -67,7 +66,7 @@ describe('init()', () => { expect(client?.getOptions().integrations.map(integration => integration.name)).toEqual(['SpanStreaming']); - expect(mockAutoPerformanceIntegrations).toHaveBeenCalledTimes(0); + expect(mockGetTracingIntegrations).toHaveBeenCalledTimes(0); }); it('installs merged default integrations, with overrides provided through options', () => { @@ -87,7 +86,7 @@ describe('init()', () => { expect(mockDefaultIntegrations[1]?.setupOnce as Mock).toHaveBeenCalledTimes(1); expect(mockIntegrations[0]?.setupOnce as Mock).toHaveBeenCalledTimes(1); expect(mockIntegrations[1]?.setupOnce as Mock).toHaveBeenCalledTimes(1); - expect(mockAutoPerformanceIntegrations).toHaveBeenCalledTimes(0); + expect(mockGetTracingIntegrations).toHaveBeenCalledTimes(0); }); it('installs integrations returned from a callback function', () => { @@ -111,13 +110,13 @@ describe('init()', () => { expect(mockDefaultIntegrations[0]?.setupOnce as Mock).toHaveBeenCalledTimes(1); expect(mockDefaultIntegrations[1]?.setupOnce as Mock).toHaveBeenCalledTimes(0); expect(newIntegration.setupOnce).toHaveBeenCalledTimes(1); - expect(mockAutoPerformanceIntegrations).toHaveBeenCalledTimes(0); + expect(mockGetTracingIntegrations).toHaveBeenCalledTimes(0); }); it('installs performance default instrumentations if tracing is enabled', () => { const autoPerformanceIntegration = new MockIntegration('Some mock integration 4.4'); - mockAutoPerformanceIntegrations.mockReset().mockImplementation(() => [autoPerformanceIntegration]); + mockGetTracingIntegrations.mockReset().mockImplementation(() => [autoPerformanceIntegration]); const mockIntegrations = [ new MockIntegration('Some mock integration 4.1'), @@ -133,7 +132,7 @@ describe('init()', () => { expect(mockIntegrations[0]?.setupOnce as Mock).toHaveBeenCalledTimes(1); expect(mockIntegrations[1]?.setupOnce as Mock).toHaveBeenCalledTimes(1); expect(autoPerformanceIntegration.setupOnce).toHaveBeenCalledTimes(1); - expect(mockAutoPerformanceIntegrations).toHaveBeenCalledTimes(1); + expect(mockGetTracingIntegrations).toHaveBeenCalledTimes(1); const client = getClient(); expect(client?.getOptions()).toEqual( @@ -145,7 +144,7 @@ describe('init()', () => { it('installs performance default instrumentations if tracing is enabled via `SENTRY_TRACES_SAMPLE_RATE`', () => { const autoPerformanceIntegration = new MockIntegration('Some mock integration 4.5'); - mockAutoPerformanceIntegrations.mockReset().mockImplementation(() => [autoPerformanceIntegration]); + mockGetTracingIntegrations.mockReset().mockImplementation(() => [autoPerformanceIntegration]); process.env.SENTRY_TRACES_SAMPLE_RATE = '1'; @@ -156,7 +155,7 @@ describe('init()', () => { } expect(autoPerformanceIntegration.setupOnce).toHaveBeenCalledTimes(1); - expect(mockAutoPerformanceIntegrations).toHaveBeenCalledTimes(1); + expect(mockGetTracingIntegrations).toHaveBeenCalledTimes(1); const client = getClient(); expect(client?.getOptions()).toEqual( diff --git a/packages/server-utils/src/index.ts b/packages/server-utils/src/index.ts index 0ad0598dedfd..4309a6f1b859 100644 --- a/packages/server-utils/src/index.ts +++ b/packages/server-utils/src/index.ts @@ -46,3 +46,5 @@ export { tediousIntegration } from './integrations/tedious'; export { vercelAIIntegration } from './integrations/vercel-ai'; export { expressIntegration } from './integrations/express'; export { firebaseIntegration } from './integrations/firebase'; + +export { getTracingIntegrations, getErrorIntegrations } from './integrations'; diff --git a/packages/server-utils/src/integrations/index.ts b/packages/server-utils/src/integrations/index.ts new file mode 100644 index 000000000000..22bd0f0f1664 --- /dev/null +++ b/packages/server-utils/src/integrations/index.ts @@ -0,0 +1,62 @@ +import { amqplibIntegration } from './amqplib'; +import { mongoIntegration } from './mongodb'; +import { graphqlIntegration } from './graphql'; +import { redisIntegration } from './redis'; +import { mysqlIntegration } from './mysql'; +import { mysql2Integration } from './mysql2'; +import { postgresIntegration } from './postgres'; +import { prismaIntegration } from './prisma'; +import { tediousIntegration } from './tedious'; +import { genericPoolIntegration } from './generic-pool'; +import { kafkaIntegration } from './kafkajs'; +import { mongooseIntegration } from './mongoose'; +import { lruMemoizerIntegration } from './lru-memoizer'; +import { langChainIntegration } from './langchain'; +import { langGraphIntegration } from './langgraph'; +import { vercelAIIntegration } from './vercel-ai'; +import { openAIIntegration } from './openai'; +import { anthropicAIIntegration } from './anthropic'; +import { googleGenAIIntegration } from './google-genai'; +import { postgresJsIntegration } from './postgres-js'; +import { firebaseIntegration } from './firebase'; +import { expressIntegration } from './express'; +import { fastifyIntegration } from './fastify'; +import { hapiIntegration } from './hapi'; +import { koaIntegration } from './koa'; +import type { Integration } from '@sentry/core'; +import { awsIntegration } from './aws-sdk'; + +/** These are integrations that are tracing-only integrations. */ +export function getTracingIntegrations(): Integration[] { + return [ + graphqlIntegration(), + mongoIntegration(), + mongooseIntegration(), + mysqlIntegration(), + mysql2Integration(), + redisIntegration(), + postgresIntegration(), + prismaIntegration(), + tediousIntegration(), + genericPoolIntegration(), + kafkaIntegration(), + amqplibIntegration(), + lruMemoizerIntegration(), + awsIntegration(), + // AI providers + // LangChain must come first to disable AI provider integrations before they instrument + langChainIntegration(), + langGraphIntegration(), + vercelAIIntegration(), + openAIIntegration(), + anthropicAIIntegration(), + googleGenAIIntegration(), + postgresJsIntegration(), + firebaseIntegration(), + ]; +} + +/** These are integrations that cover error capture, in addition to tracing. */ +export function getErrorIntegrations(): Integration[] { + return [expressIntegration(), fastifyIntegration(), hapiIntegration(), koaIntegration()]; +}