From 8328ed430932eb99fa39c10dfcd5438ec4e0fd35 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 21 Aug 2026 11:23:46 +0200 Subject: [PATCH 1/7] feat(node): Always set up express, fastify, koa, hapi integrations --- .../node/src/integrations/tracing/index.ts | 12 +++----- packages/node/src/sdk/index.ts | 26 +++++++++------- packages/node/src/types.ts | 12 ++++++++ .../sdk/diagnosticsChannelInjection.test.ts | 30 +++++++++++++++---- 4 files changed, 56 insertions(+), 24 deletions(-) diff --git a/packages/node/src/integrations/tracing/index.ts b/packages/node/src/integrations/tracing/index.ts index e01d0d36bd38..925398d96a74 100644 --- a/packages/node/src/integrations/tracing/index.ts +++ b/packages/node/src/integrations/tracing/index.ts @@ -3,14 +3,11 @@ import { prismaIntegration, amqplibIntegration, anthropicAIIntegration, - expressIntegration, firebaseIntegration, genericPoolIntegration, googleGenAIIntegration, graphqlIntegration, - hapiIntegration, kafkaIntegration, - koaIntegration, langChainIntegration, langGraphIntegration, lruMemoizerIntegration, @@ -25,12 +22,13 @@ import { tediousIntegration, vercelAIIntegration, } from '@sentry/server-utils'; -import { fastifyIntegration } from './fastify'; export function getAutoPerformanceIntegrations(): Integration[] { + // The following integrations are not considered performance integrations because they are "framework"-level + // meaning they may also handle error capture and similar things. + // Thus, we add them by default: + // express, fastify, hapi, koa return [ - expressIntegration(), - fastifyIntegration(), graphqlIntegration(), mongoIntegration(), mongooseIntegration(), @@ -39,8 +37,6 @@ export function getAutoPerformanceIntegrations(): Integration[] { redisIntegration(), postgresIntegration(), prismaIntegration(), - hapiIntegration(), - koaIntegration(), tediousIntegration(), genericPoolIntegration(), kafkaIntegration(), diff --git a/packages/node/src/sdk/index.ts b/packages/node/src/sdk/index.ts index badfe207bc8c..7bc0231aed01 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, expressIntegration, hapiIntegration, koaIntegration } from '@sentry/server-utils'; import { registerDiagnosticsChannelInjection } from '@sentry/server-utils/orchestrion/register'; import { DEBUG_BUILD } from '../debug-build'; import { childProcessIntegration } from '../integrations/childProcess'; @@ -41,6 +41,7 @@ import { getSpotlightConfig } from '../utils/spotlight'; import { defaultStackParser, getSentryRelease } from './api'; import { NodeClient } from './client'; import { initOpenTelemetry } from './initOtel'; +import { fastifyIntegration } from '../integrations/tracing/fastify'; /** * Get the base default integrations shared by all Node SDK default-integration sets. @@ -69,6 +70,11 @@ function getBaseDefaultIntegrations(): Integration[] { workerThreadsIntegration(), processSessionIntegration(), modulesIntegration(), + // Framework-level integrations + expressIntegration(), + fastifyIntegration(), + hapiIntegration(), + koaIntegration(), ]; } @@ -147,20 +153,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(); } 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..fbcfa292f2a9 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 when `enableRuntimeChannelInjection` is false', () => { + init({ + dsn: PUBLIC_DSN, + tracesSampleRate: 1, + enableRuntimeChannelInjection: false, + enableOpenTelemetrySetup: false, + }); + expect(registerDiagnosticsChannelInjection).not.toHaveBeenCalled(); expect(detectOrchestrionSetup).not.toHaveBeenCalled(); }); + + 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); + }); }); From cff54b95b2e9e68746b830a7710f9ebea3c5dc64 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 21 Aug 2026 12:02:58 +0200 Subject: [PATCH 2/7] feat(bun,deno): Add express, fastify, koa, hapi to default integrations Mirror the Node SDK change promoting the framework integrations (express, fastify, hapi, koa) to always-on defaults. Bun gains all four; Deno (which already listed express, hapi, koa) gains fastify, now also re-exported from `@sentry/server-utils/orchestrion`. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/bun/src/sdk.ts | 10 ++++++++++ packages/deno/src/sdk.ts | 2 ++ 2 files changed, 12 insertions(+) diff --git a/packages/bun/src/sdk.ts b/packages/bun/src/sdk.ts index ce5e08872542..2256969cc86f 100644 --- a/packages/bun/src/sdk.ts +++ b/packages/bun/src/sdk.ts @@ -12,9 +12,13 @@ import type { NodeClient } from '@sentry/node'; import { consoleIntegration, contextLinesIntegration, + expressIntegration, + fastifyIntegration, getAutoPerformanceIntegrations, + hapiIntegration, httpIntegration, init as initNode, + koaIntegration, modulesIntegration, nodeContextIntegration, onUncaughtExceptionIntegration, @@ -64,6 +68,12 @@ 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 (matching the Node SDK). + expressIntegration(), + fastifyIntegration(), + hapiIntegration(), + koaIntegration(), // Bun Specific bunServerIntegration(), bunHttpServerIntegration(), diff --git a/packages/deno/src/sdk.ts b/packages/deno/src/sdk.ts index b4867e6f61b4..9266928ed917 100644 --- a/packages/deno/src/sdk.ts +++ b/packages/deno/src/sdk.ts @@ -16,6 +16,7 @@ import { anthropicAIIntegration, awsIntegration, expressIntegration, + fastifyIntegration, firebaseIntegration, genericPoolIntegration, googleGenAIIntegration, @@ -77,6 +78,7 @@ export function getDefaultIntegrations(_options: Options): Integration[] { anthropicAIIntegration(), awsIntegration(), expressIntegration(), + fastifyIntegration(), firebaseIntegration(), genericPoolIntegration(), googleGenAIIntegration(), From 3137104fe844a0b6c135caef83fad8ca7c586d91 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 21 Aug 2026 12:49:14 +0200 Subject: [PATCH 3/7] bump size limits --- .size-limit.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.size-limit.js b/.size-limit.js index becbd8285041..737b0a6ea977 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 From 54b98010578c3658864ed7014eb261f99410329c Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 21 Aug 2026 12:56:47 +0200 Subject: [PATCH 4/7] fix test --- packages/deno/test/__snapshots__/mod.test.ts.snap | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/deno/test/__snapshots__/mod.test.ts.snap b/packages/deno/test/__snapshots__/mod.test.ts.snap index 5e6a7f16eee3..78d37f153acb 100644 --- a/packages/deno/test/__snapshots__/mod.test.ts.snap +++ b/packages/deno/test/__snapshots__/mod.test.ts.snap @@ -63,6 +63,7 @@ snapshot[`captureMessage 1`] = ` "Anthropic_AI", "Aws", "Express", + "Fastify", "Firebase", "GenericPool", "Google_GenAI", @@ -168,6 +169,7 @@ snapshot[`captureMessage twice 1`] = ` "Anthropic_AI", "Aws", "Express", + "Fastify", "Firebase", "GenericPool", "Google_GenAI", @@ -280,6 +282,7 @@ snapshot[`captureMessage twice 2`] = ` "Anthropic_AI", "Aws", "Express", + "Fastify", "Firebase", "GenericPool", "Google_GenAI", From 55dbca35bd1061ed8cb587bc046f9f580a045a42 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 21 Aug 2026 13:00:25 +0200 Subject: [PATCH 5/7] test(deno): Add orchestrion-fastify integration test Mirror the other orchestrion Deno suites: assert the Fastify integration is in the defaults and that the native `tracing:fastify.request.handler:error` channel captures the error (with mechanism `auto.function.fastify`). Adds a shared `errorSink` helper alongside `transactionSink`. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../deno-integration-tests/src/index.ts | 36 +++++++++++++- .../suites/orchestrion-fastify/test.ts | 47 +++++++++++++++++++ 2 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 dev-packages/deno-integration-tests/suites/orchestrion-fastify/test.ts diff --git a/dev-packages/deno-integration-tests/src/index.ts b/dev-packages/deno-integration-tests/src/index.ts index bc6b80087901..224204fb2af2 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'; /** @@ -51,6 +51,40 @@ export function transactionSink(): TransactionSink { }; } +export interface ErrorSink { + beforeSend: (event: Event) => null; + waitFor: (predicate: (event: Event) => boolean) => Promise; +} + +/** + * A `beforeSend` hook that records every error event and lets a test `await` the + * first one matching a predicate. Mirrors {@link transactionSink} for error events. + */ +export function errorSink(): ErrorSink { + const events: Event[] = []; + const waiters: { predicate: (e: Event) => boolean; resolve: (e: Event) => void }[] = []; + return { + beforeSend(event) { + events.push(event); + for (let i = waiters.length - 1; i >= 0; i--) { + const w = waiters[i]!; + if (w.predicate(event)) { + waiters.splice(i, 1); + w.resolve(event); + } + } + return null; + }, + waitFor(predicate) { + const already = events.find(predicate); + if (already) return Promise.resolve(already); + return new Promise(resolve => { + waiters.push({ predicate, resolve }); + }); + }, + }; +} + /** 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); +}); From b733457293e88d224092b7c0d1b1ef6fed478180 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 24 Aug 2026 10:31:46 +0200 Subject: [PATCH 6/7] fix flake --- .../node-integration-tests/suites/anr/stop-and-start.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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); From 20ff2d217fbf3c2d25e913d46faa53e3c2420382 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 24 Aug 2026 13:24:13 +0200 Subject: [PATCH 7/7] better comment --- packages/node/src/sdk/index.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/node/src/sdk/index.ts b/packages/node/src/sdk/index.ts index 7bc0231aed01..6346379f1f5f 100644 --- a/packages/node/src/sdk/index.ts +++ b/packages/node/src/sdk/index.ts @@ -90,9 +90,8 @@ 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 + // Note that integrations like `httpIntegration` or `expressIntegration` are always added, + // because they also handle non-tracing related functionality. ...(hasSpansEnabled(options) ? getAutoPerformanceIntegrations() : []), ]; }