From 4a964533e43b5ef58684fc43d58450e3b4797cc0 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 21 Aug 2026 11:23:46 +0200 Subject: [PATCH 1/5] feat(node): Always set up express, fastify, koa, hapi integrations --- .../node/src/integrations/tracing/index.ts | 12 +++---- packages/node/src/sdk/index.ts | 31 +++++++++++++------ packages/node/src/types.ts | 12 +++++++ .../sdk/diagnosticsChannelInjection.test.ts | 30 ++++++++++++++---- 4 files changed, 61 insertions(+), 24 deletions(-) diff --git a/packages/node/src/integrations/tracing/index.ts b/packages/node/src/integrations/tracing/index.ts index 2d7274326a40..d92a18a04d71 100644 --- a/packages/node/src/integrations/tracing/index.ts +++ b/packages/node/src/integrations/tracing/index.ts @@ -3,14 +3,11 @@ import { prismaIntegration } from '@sentry/server-utils'; import { amqplibIntegration, anthropicAIIntegration, - expressIntegration, firebaseIntegration, genericPoolIntegration, googleGenAIIntegration, graphqlIntegration, - hapiIntegration, kafkaIntegration, - koaIntegration, langChainIntegration, langGraphIntegration, lruMemoizerIntegration, @@ -25,12 +22,13 @@ import { tediousIntegration, vercelAIIntegration, } from '@sentry/server-utils/orchestrion'; -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 2bb381588581..5323592842b3 100644 --- a/packages/node/src/sdk/index.ts +++ b/packages/node/src/sdk/index.ts @@ -16,7 +16,12 @@ import { stackParserFromStackParserOptions, } from '@sentry/core'; import { isMainThread, parentPort } from 'node:worker_threads'; -import { detectOrchestrionSetup } from '@sentry/server-utils/orchestrion'; +import { + detectOrchestrionSetup, + expressIntegration, + hapiIntegration, + koaIntegration, +} from '@sentry/server-utils/orchestrion'; import { registerDiagnosticsChannelInjection } from '@sentry/server-utils/orchestrion/register'; import { DEBUG_BUILD } from '../debug-build'; import { childProcessIntegration } from '../integrations/childProcess'; @@ -40,6 +45,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. @@ -67,6 +73,11 @@ function getBaseDefaultIntegrations(): Integration[] { childProcessIntegration(), processSessionIntegration(), modulesIntegration(), + // Framework-level integrations + expressIntegration(), + fastifyIntegration(), + hapiIntegration(), + koaIntegration(), ]; } @@ -145,20 +156,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 e06405efaae9..90bb27e77f17 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 58b08fb9248740a5484b0dfa65333e83841b93d5 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 21 Aug 2026 12:02:58 +0200 Subject: [PATCH 2/5] 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 ++ packages/server-utils/src/orchestrion/index.ts | 2 ++ 3 files changed, 14 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 629d96f914e0..e3b8fa76a575 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(), diff --git a/packages/server-utils/src/orchestrion/index.ts b/packages/server-utils/src/orchestrion/index.ts index 0e6c926ad936..3f06c10011c2 100644 --- a/packages/server-utils/src/orchestrion/index.ts +++ b/packages/server-utils/src/orchestrion/index.ts @@ -23,6 +23,7 @@ import { postgresJsIntegration } from '../integrations/postgres-js'; import { tediousIntegration } from '../integrations/tedious'; import { vercelAIIntegration } from '../integrations/vercel-ai'; import { expressIntegration } from '../integrations/express'; +import { fastifyIntegration } from '../integrations/fastify'; import { firebaseIntegration } from '../integrations/firebase'; export { detectOrchestrionSetup, isOrchestrionInjected } from './detect'; @@ -62,6 +63,7 @@ export { tediousIntegration, vercelAIIntegration, expressIntegration, + fastifyIntegration, firebaseIntegration, }; export type { InstrumentationConfig } from './apmTypes'; From abdfca8b3a65d70e8c5a1c53b7271fd547e3f6dd Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 21 Aug 2026 12:49:14 +0200 Subject: [PATCH 3/5] 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 d4fcdc87bf75..440816d17423 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: '96 KB', + limit: '99 KB', disablePlugins: ['@size-limit/esbuild'], }, // Cloudflare SDK (ESM) - compressed, minified to match `wrangler deploy --dry-run --minify` output From ab767ef79ba5289f3ed657b899daf52013cdc648 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 21 Aug 2026 12:56:47 +0200 Subject: [PATCH 4/5] 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 d405a64641b0fb68fd12d2fde0f422e47bc6fc1c Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 21 Aug 2026 13:00:25 +0200 Subject: [PATCH 5/5] 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); +});