Skip to content

Commit d410f29

Browse files
mydeaclaude
andauthored
feat(node): Always set up express, fastify, koa, hapi integrations (#23473)
These integrations may also do non-tracing related things (e.g. error capturing etc), so they should be added by default. This also means that we always want to add the orchestrion runtime, even in non-tracing mode. Instead, I added a flag `enableRuntimeChannelInjection` to opt-out of this behavior if needed. Also added these three as defaults to deno and bun, to mirror this. Additionally, this unifies the integrations we add into `getErrorIntegrations()` and `getTracingIntegrations()`, so we can re-use this in node, deno and bun. It also adds awsIntegration to the tracing set so this is also added in all scenarios now. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent ee5a772 commit d410f29

15 files changed

Lines changed: 300 additions & 236 deletions

File tree

.size-limit.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -430,7 +430,7 @@ module.exports = [
430430
path: 'packages/node/build/esm/index.js',
431431
import: createImport('initWithoutDefaultIntegrations', 'getDefaultIntegrationsWithoutPerformance'),
432432
gzip: true,
433-
limit: '87 KB',
433+
limit: '92 KB',
434434
disablePlugins: ['@size-limit/esbuild'],
435435
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
436436
modifyWebpackConfig: function (config) {
@@ -454,7 +454,7 @@ module.exports = [
454454
import: createImport('init'),
455455
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
456456
gzip: true,
457-
limit: '97 KB',
457+
limit: '99 KB',
458458
disablePlugins: ['@size-limit/esbuild'],
459459
},
460460
// Cloudflare SDK (ESM) - compressed, minified to match `wrangler deploy --dry-run --minify` output

dev-packages/deno-integration-tests/src/index.ts

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { TransactionEvent } from '@sentry/core';
1+
import type { Event, TransactionEvent } from '@sentry/core';
22
import { getAsyncContextStrategy, getMainCarrier, setAsyncContextStrategy } from '@sentry/core';
33

44
/**
@@ -16,41 +16,64 @@ export function resetGlobals(): void {
1616
setAsyncContextStrategy(acs);
1717
}
1818

19-
export interface TransactionSink {
20-
beforeSendTransaction: (event: TransactionEvent) => null;
21-
waitFor: (predicate: (event: TransactionEvent) => boolean) => Promise<TransactionEvent>;
19+
interface EventSink<T> {
20+
beforeSend: (event: T) => null;
21+
waitFor: (predicate: (event: T) => boolean) => Promise<T>;
2222
}
2323

24-
/**
25-
* A `beforeSendTransaction` hook that records every transaction and lets a test
26-
* `await` the first one matching a predicate. `waitFor` resolves immediately if
27-
* a match already arrived, so there is no ordering race with the hook.
28-
*/
29-
export function transactionSink(): TransactionSink {
30-
const transactions: TransactionEvent[] = [];
31-
const waiters: { predicate: (e: TransactionEvent) => boolean; resolve: (e: TransactionEvent) => void }[] = [];
24+
function eventSink<T>(): EventSink<T> {
25+
const events: T[] = [];
26+
const waiters: { predicate: (e: T) => boolean; resolve: (e: T) => void }[] = [];
3227
return {
33-
beforeSendTransaction(event) {
34-
transactions.push(event);
28+
beforeSend(event) {
29+
events.push(event);
30+
3531
for (let i = waiters.length - 1; i >= 0; i--) {
3632
const w = waiters[i]!;
3733
if (w.predicate(event)) {
3834
waiters.splice(i, 1);
3935
w.resolve(event);
4036
}
4137
}
38+
4239
return null;
4340
},
4441
waitFor(predicate) {
45-
const already = transactions.find(predicate);
42+
const already = events.find(predicate);
4643
if (already) return Promise.resolve(already);
47-
return new Promise<TransactionEvent>(resolve => {
44+
return new Promise<T>(resolve => {
4845
waiters.push({ predicate, resolve });
4946
});
5047
},
5148
};
5249
}
5350

51+
/**
52+
* A `beforeSend` hook that records every transaction event and lets a test
53+
* `await` the first one matching a predicate. `waitFor` resolves immediately if
54+
* a match already arrived, so there is no ordering race with the hook.
55+
*/
56+
export function transactionSink(): {
57+
waitFor: (predicate: (event: TransactionEvent) => boolean) => Promise<TransactionEvent>;
58+
beforeSendTransaction: (event: TransactionEvent) => null;
59+
} {
60+
const sink = eventSink<TransactionEvent>();
61+
62+
return {
63+
waitFor: sink.waitFor,
64+
beforeSendTransaction: sink.beforeSend,
65+
};
66+
}
67+
68+
/**
69+
* A `beforeSend` hook that records every error and lets a test
70+
* `await` the first one matching a predicate. `waitFor` resolves immediately if
71+
* a match already arrived, so there is no ordering race with the hook.
72+
*/
73+
export function errorSink(): EventSink<Event> {
74+
return eventSink<Event>();
75+
}
76+
5477
/** Reject with a descriptive message if `p` does not settle within `ms`. */
5578
export function withTimeout<T>(p: Promise<T>, ms: number, what: string): Promise<T> {
5679
let timer: ReturnType<typeof setTimeout> | undefined;
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// <reference lib="deno.ns" />
2+
3+
import { channel } from 'node:diagnostics_channel';
4+
import type { DenoClient } from '@sentry/deno';
5+
import { init } from '@sentry/deno';
6+
import { assert } from 'https://deno.land/std@0.212.0/assert/assert.ts';
7+
import { assertEquals } from 'https://deno.land/std@0.212.0/assert/assert_equals.ts';
8+
import { assertExists } from 'https://deno.land/std@0.212.0/assert/assert_exists.ts';
9+
import { errorSink, resetGlobals, withTimeout } from '../../src/index.ts';
10+
11+
Deno.test('fastify instrumentation: included in default integrations (Deno 2.8.0+)', () => {
12+
resetGlobals();
13+
const client = init({ traceLifecycle: 'static', dsn: 'https://username@domain/123' }) as DenoClient;
14+
const names = client.getOptions().integrations.map(i => i.name);
15+
assert(names.includes('Fastify'), `Fastify should be in defaults, got ${names.join(', ')}`);
16+
});
17+
18+
Deno.test('fastify instrumentation: tracing:fastify.request.handler:error channel captures the error', async () => {
19+
resetGlobals();
20+
const sink = errorSink();
21+
init({
22+
traceLifecycle: 'static',
23+
dsn: 'https://username@domain/123',
24+
beforeSend: sink.beforeSend,
25+
});
26+
27+
const error = new Error('fastify boom');
28+
29+
// Fastify v5 publishes this native diagnostics channel when a request handler errors; the
30+
// integration subscribes to it directly (no orchestrion injection needed). A 5xx reply passes the
31+
// default `shouldHandleError`, so the error is captured.
32+
channel('tracing:fastify.request.handler:error').publish({
33+
error,
34+
request: { method: 'GET', routeOptions: { url: '/boom' } },
35+
reply: { statusCode: 500 },
36+
});
37+
38+
const event = await withTimeout(
39+
sink.waitFor(e => e.exception?.values?.[0]?.value === 'fastify boom'),
40+
5000,
41+
"the captured 'fastify boom' error",
42+
);
43+
44+
assertExists(event.exception?.values?.[0]);
45+
assertEquals(event.exception?.values?.[0]?.mechanism?.type, 'auto.function.fastify');
46+
assertEquals(event.exception?.values?.[0]?.mechanism?.handled, false);
47+
});

dev-packages/node-integration-tests/suites/anr/stop-and-start.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const Sentry = require('@sentry/node');
2+
const { waitForDebuggerReady } = require('@sentry-internal/test-utils');
23

34
setTimeout(() => {
45
process.exit();
@@ -52,7 +53,9 @@ setTimeout(() => {
5253
setTimeout(() => {
5354
anr.startWorker();
5455

55-
setTimeout(() => {
56+
// Wait for the restarted worker's debugger session to reconnect before blocking the event
57+
// loop, otherwise on slow CI the worker isn't ready to sample and the ANR is missed entirely.
58+
waitForDebuggerReady(() => {
5659
longWork();
5760
});
5861
}, 2000);

packages/bun/src/sdk.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import type { NodeClient } from '@sentry/node';
1212
import {
1313
consoleIntegration,
1414
contextLinesIntegration,
15-
getAutoPerformanceIntegrations,
1615
httpIntegration,
1716
init as initNode,
1817
modulesIntegration,
@@ -26,6 +25,7 @@ import { fetchIntegration } from './integrations/fetch';
2625
import { makeFetchTransport } from './transports';
2726
import type { BunOptions } from './types';
2827
import { bunHttpServerIntegration } from './integrations/bunHttpServer';
28+
import { getErrorIntegrations, getTracingIntegrations } from '@sentry/server-utils';
2929

3030
/**
3131
* The performance integrations for bun: the OTel auto-performance set, but with
@@ -40,7 +40,7 @@ function getPerformanceIntegrations(options: Options): Integration[] {
4040
return [];
4141
}
4242

43-
return getAutoPerformanceIntegrations();
43+
return getTracingIntegrations();
4444
}
4545

4646
/** Get the default integrations for the Bun SDK, excluding performance integrations. */
@@ -64,6 +64,9 @@ export function getDefaultIntegrationsWithoutPerformance(): Integration[] {
6464
nodeContextIntegration(),
6565
modulesIntegration(),
6666
processSessionIntegration(),
67+
// Framework-level integrations. These are not performance-only: they also handle error capture, so
68+
// they are added by default rather than gated behind tracing
69+
...getErrorIntegrations(),
6770
// Bun Specific
6871
bunServerIntegration(),
6972
bunHttpServerIntegration(),

packages/bun/test/init.test.ts

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { type Integration } from '@sentry/core';
2-
import * as sentryNode from '@sentry/node';
2+
import * as sentryServerUtils from '@sentry/server-utils';
33
import type { Mock } from 'bun:test';
44
import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from 'bun:test';
55
import {
@@ -22,15 +22,14 @@ class MockIntegration implements Integration {
2222
}
2323

2424
describe('init()', () => {
25-
let mockAutoPerformanceIntegrations: Mock<() => Integration[]>;
25+
let mockGetTracingIntegrations: Mock<() => Integration[]>;
2626

2727
beforeEach(() => {
28-
// @ts-expect-error weird
29-
mockAutoPerformanceIntegrations = spyOn(sentryNode, 'getAutoPerformanceIntegrations');
28+
mockGetTracingIntegrations = spyOn(sentryServerUtils, 'getTracingIntegrations');
3029
});
3130

3231
afterEach(() => {
33-
mockAutoPerformanceIntegrations.mockRestore();
32+
mockGetTracingIntegrations.mockRestore();
3433
});
3534

3635
describe('integrations', () => {
@@ -41,7 +40,7 @@ describe('init()', () => {
4140

4241
expect(client?.getOptions().integrations).toEqual([]);
4342

44-
expect(mockAutoPerformanceIntegrations).toHaveBeenCalledTimes(0);
43+
expect(mockGetTracingIntegrations).toHaveBeenCalledTimes(0);
4544
});
4645

4746
it('enables spotlight with default URL from config `true`', () => {
@@ -75,7 +74,7 @@ describe('init()', () => {
7574
expect(mockDefaultIntegrations[1]?.setupOnce).toHaveBeenCalledTimes(1);
7675
expect(mockIntegrations[0]?.setupOnce).toHaveBeenCalledTimes(1);
7776
expect(mockIntegrations[1]?.setupOnce).toHaveBeenCalledTimes(1);
78-
expect(mockAutoPerformanceIntegrations).toHaveBeenCalledTimes(0);
77+
expect(mockGetTracingIntegrations).toHaveBeenCalledTimes(0);
7978
});
8079

8180
it('installs integrations returned from a callback function', () => {
@@ -99,12 +98,12 @@ describe('init()', () => {
9998
expect(mockDefaultIntegrations[0]?.setupOnce).toHaveBeenCalledTimes(1);
10099
expect(mockDefaultIntegrations[1]?.setupOnce).toHaveBeenCalledTimes(0);
101100
expect(newIntegration.setupOnce).toHaveBeenCalledTimes(1);
102-
expect(mockAutoPerformanceIntegrations).toHaveBeenCalledTimes(0);
101+
expect(mockGetTracingIntegrations).toHaveBeenCalledTimes(0);
103102
});
104103

105104
it('installs performance default instrumentations if tracing is enabled', () => {
106105
const autoPerformanceIntegrations = [new MockIntegration('Performance integration')];
107-
mockAutoPerformanceIntegrations.mockImplementation(() => autoPerformanceIntegrations);
106+
mockGetTracingIntegrations.mockImplementation(() => autoPerformanceIntegrations);
108107

109108
const mockIntegrations = [
110109
new MockIntegration('Some mock integration 4.1'),
@@ -120,7 +119,7 @@ describe('init()', () => {
120119
expect(mockIntegrations[0]?.setupOnce).toHaveBeenCalledTimes(1);
121120
expect(mockIntegrations[1]?.setupOnce).toHaveBeenCalledTimes(1);
122121
expect(autoPerformanceIntegrations[0]?.setupOnce).toHaveBeenCalledTimes(1);
123-
expect(mockAutoPerformanceIntegrations).toHaveBeenCalledTimes(1);
122+
expect(mockGetTracingIntegrations).toHaveBeenCalledTimes(1);
124123

125124
const integrations = getClient()?.getOptions().integrations;
126125
expect(integrations).toBeArray();
@@ -137,7 +136,7 @@ describe('init()', () => {
137136
const client = getClient();
138137

139138
expect(client?.getOptions().integrations).toEqual([]);
140-
expect(mockAutoPerformanceIntegrations).toHaveBeenCalledTimes(0);
139+
expect(mockGetTracingIntegrations).toHaveBeenCalledTimes(0);
141140
});
142141

143142
it('still installs user-provided integrations', () => {
@@ -162,12 +161,12 @@ describe('init()', () => {
162161
const full = getDefaultIntegrations({}).map(({ name }) => name);
163162

164163
expect(withoutPerformance).toEqual(full);
165-
expect(mockAutoPerformanceIntegrations).toHaveBeenCalledTimes(0);
164+
expect(mockGetTracingIntegrations).toHaveBeenCalledTimes(0);
166165
});
167166

168167
it('omits the performance integrations that the full set adds when tracing is enabled', () => {
169168
const performanceIntegration = new MockIntegration('Performance integration');
170-
mockAutoPerformanceIntegrations.mockImplementation(() => [performanceIntegration]);
169+
mockGetTracingIntegrations.mockImplementation(() => [performanceIntegration]);
171170

172171
const withoutPerformance = getDefaultIntegrationsWithoutPerformance().map(({ name }) => name);
173172
const full = getDefaultIntegrations({ tracesSampleRate: 1 }).map(({ name }) => name);

packages/deno/src/sdk.ts

Lines changed: 4 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -11,32 +11,7 @@ import {
1111
requestDataIntegration,
1212
stackParserFromStackParserOptions,
1313
} from '@sentry/core';
14-
import {
15-
amqplibIntegration,
16-
anthropicAIIntegration,
17-
awsIntegration,
18-
expressIntegration,
19-
firebaseIntegration,
20-
genericPoolIntegration,
21-
googleGenAIIntegration,
22-
graphqlIntegration,
23-
hapiIntegration,
24-
kafkaIntegration,
25-
koaIntegration,
26-
langChainIntegration,
27-
langGraphIntegration,
28-
lruMemoizerIntegration,
29-
mongoIntegration,
30-
mongooseIntegration,
31-
mysqlIntegration,
32-
mysql2Integration,
33-
openAIIntegration,
34-
postgresIntegration,
35-
postgresJsIntegration,
36-
tediousIntegration,
37-
vercelAIIntegration,
38-
redisIntegration,
39-
} from '@sentry/server-utils';
14+
import { getTracingIntegrations, getErrorIntegrations } from '@sentry/server-utils';
4015
import { DenoClient } from './client';
4116
import { breadcrumbsIntegration } from './integrations/breadcrumbs';
4217
import { denoContextIntegration } from './integrations/context';
@@ -64,39 +39,12 @@ export function getDefaultIntegrations(_options: Options): Integration[] {
6439
denoContextIntegration(),
6540
denoServeIntegration(),
6641
denoHttpIntegration(),
67-
redisIntegration(),
68-
graphqlIntegration(),
69-
vercelAIIntegration(),
70-
// orchestrion-based instrumentations. We add a deliberate list here rather
71-
// than every channel integration: each one needs a Deno test proving it
72-
// records spans.
73-
//
74-
// The orchestrion channels may be injected after (or while) the SDK loads.
75-
// If they never load, these are no-ops.
76-
amqplibIntegration(),
77-
anthropicAIIntegration(),
78-
awsIntegration(),
79-
expressIntegration(),
80-
firebaseIntegration(),
81-
genericPoolIntegration(),
82-
googleGenAIIntegration(),
83-
hapiIntegration(),
84-
kafkaIntegration(),
85-
koaIntegration(),
86-
langChainIntegration(),
87-
langGraphIntegration(),
88-
lruMemoizerIntegration(),
89-
mongoIntegration(),
90-
mongooseIntegration(),
91-
mysqlIntegration(),
92-
mysql2Integration(),
93-
openAIIntegration(),
94-
postgresIntegration(),
95-
postgresJsIntegration(),
96-
tediousIntegration(),
9742
contextLinesIntegration(),
9843
normalizePathsIntegration(),
9944
globalHandlersIntegration(),
45+
// server-utils integrations
46+
...getErrorIntegrations(),
47+
...getTracingIntegrations(),
10048
];
10149
}
10250

0 commit comments

Comments
 (0)