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
4 changes: 2 additions & 2 deletions .size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
Expand Down
36 changes: 35 additions & 1 deletion dev-packages/deno-integration-tests/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { TransactionEvent } from '@sentry/core';
import type { Event, TransactionEvent } from '@sentry/core';
import { getAsyncContextStrategy, getMainCarrier, setAsyncContextStrategy } from '@sentry/core';

/**
Expand Down Expand Up @@ -51,6 +51,40 @@ export function transactionSink(): TransactionSink {
};
}

export interface ErrorSink {
beforeSend: (event: Event) => null;
waitFor: (predicate: (event: Event) => boolean) => Promise<Event>;
}

/**
* 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<Event>(resolve => {
waiters.push({ predicate, resolve });
});
},
};
}

/** Reject with a descriptive message if `p` does not settle within `ms`. */
export function withTimeout<T>(p: Promise<T>, ms: number, what: string): Promise<T> {
let timer: ReturnType<typeof setTimeout> | undefined;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// <reference lib="deno.ns" />

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);
});
10 changes: 10 additions & 0 deletions packages/bun/src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(),
Expand Down
2 changes: 2 additions & 0 deletions packages/deno/src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
anthropicAIIntegration,
awsIntegration,
expressIntegration,
fastifyIntegration,
firebaseIntegration,
genericPoolIntegration,
googleGenAIIntegration,
Expand Down Expand Up @@ -77,6 +78,7 @@ export function getDefaultIntegrations(_options: Options): Integration[] {
anthropicAIIntegration(),
awsIntegration(),
expressIntegration(),
fastifyIntegration(),
Comment thread
cursor[bot] marked this conversation as resolved.
firebaseIntegration(),
genericPoolIntegration(),
googleGenAIIntegration(),
Expand Down
3 changes: 3 additions & 0 deletions packages/deno/test/__snapshots__/mod.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ snapshot[`captureMessage 1`] = `
"Anthropic_AI",
"Aws",
"Express",
"Fastify",
"Firebase",
"GenericPool",
"Google_GenAI",
Expand Down Expand Up @@ -168,6 +169,7 @@ snapshot[`captureMessage twice 1`] = `
"Anthropic_AI",
"Aws",
"Express",
"Fastify",
"Firebase",
"GenericPool",
"Google_GenAI",
Expand Down Expand Up @@ -280,6 +282,7 @@ snapshot[`captureMessage twice 2`] = `
"Anthropic_AI",
"Aws",
"Express",
"Fastify",
"Firebase",
"GenericPool",
"Google_GenAI",
Expand Down
12 changes: 4 additions & 8 deletions packages/node/src/integrations/tracing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,11 @@ import { prismaIntegration } from '@sentry/server-utils';
import {
amqplibIntegration,
anthropicAIIntegration,
expressIntegration,
firebaseIntegration,
genericPoolIntegration,
googleGenAIIntegration,
graphqlIntegration,
hapiIntegration,
kafkaIntegration,
koaIntegration,
langChainIntegration,
langGraphIntegration,
lruMemoizerIntegration,
Expand All @@ -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(),
Expand All @@ -39,8 +37,6 @@ export function getAutoPerformanceIntegrations(): Integration[] {
redisIntegration(),
postgresIntegration(),
prismaIntegration(),
hapiIntegration(),
koaIntegration(),
Comment thread
cursor[bot] marked this conversation as resolved.
tediousIntegration(),
genericPoolIntegration(),
kafkaIntegration(),
Expand Down
31 changes: 21 additions & 10 deletions packages/node/src/sdk/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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.
Expand Down Expand Up @@ -67,6 +73,11 @@ function getBaseDefaultIntegrations(): Integration[] {
childProcessIntegration(),
processSessionIntegration(),
modulesIntegration(),
// Framework-level integrations
expressIntegration(),
fastifyIntegration(),
hapiIntegration(),
koaIntegration(),
];
}

Expand Down Expand Up @@ -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();
}
Expand Down
12 changes: 12 additions & 0 deletions packages/node/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
30 changes: 24 additions & 6 deletions packages/node/test/sdk/diagnosticsChannelInjection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
});
Comment thread
cursor[bot] marked this conversation as resolved.
});
2 changes: 2 additions & 0 deletions packages/server-utils/src/orchestrion/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -62,6 +63,7 @@ export {
tediousIntegration,
vercelAIIntegration,
expressIntegration,
fastifyIntegration,
firebaseIntegration,
};
export type { InstrumentationConfig } from './apmTypes';