Skip to content

Commit 05ab775

Browse files
JPeer264claude
andauthored
test(bun, deno): Run shared Node integration suites on Bun and Deno
The Node integration runner now reads a `RUNTIME` env var (`node`, `bun` or `deno`) and spawns the scenario with that binary. Node preload flags become `--preload` on Bun and Deno. Deno also gets an import map for the test helpers, CommonJS detection, and bare Node builtins, because the workspace symlinks put the SDK packages outside `node_modules`. The Bun and Deno integration packages select Node suites in their Vitest configs, so the same scenarios run on all 3 runtimes without copies. Bun runs them twice: with `@sentry/node`, and with `@sentry/node` mapped to `@sentry/bun` by a preload. Single tests that a runtime does not support use `test.skipIf` on `RUNTIME`. The runner also waits longer for a server port on Bun and Deno, which start the SDK more slowly than Node, and it follows `SIGTERM` with `SIGKILL`: on Deno a `SIGTERM` listener (the Vercel integration has one) keeps a scenario alive after its test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent ea27349 commit 05ab775

19 files changed

Lines changed: 350 additions & 82 deletions

File tree

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { plugin } from 'bun';
2+
import Module from 'node:module';
3+
4+
// Maps `@sentry/node` to `@sentry/bun` in the Node suite files. `@sentry/bun` imports
5+
// `@sentry/node` itself, so imports from outside the Node suite files are not touched.
6+
const NODE_SUITE_FILE = /[\\/]node-integration-tests[\\/](suites|utils)[\\/]/;
7+
8+
// Bun's runtime `onResolve` does not see bare package specifiers, so ES modules are rewritten on
9+
// load. `onLoad` output for a CommonJS file does not run, so those files keep their source.
10+
const NODE_SUITE_ESM_FILE = /[\\/]node-integration-tests[\\/](suites|utils)[\\/].*\.(mjs|ts)$/;
11+
const SENTRY_NODE_SPECIFIER = /(['"])@sentry\/node\1/g;
12+
13+
plugin({
14+
name: 'alias-sentry-node-to-sentry-bun',
15+
setup(build) {
16+
build.onLoad({ filter: NODE_SUITE_ESM_FILE }, async args => {
17+
const source = await Bun.file(args.path).text();
18+
return {
19+
contents: source.replace(SENTRY_NODE_SPECIFIER, '$1@sentry/bun$1'),
20+
loader: args.path.endsWith('.ts') ? 'ts' : 'js',
21+
};
22+
});
23+
},
24+
});
25+
26+
type ResolveFilename = (request: string, parent: { filename?: string } | undefined, ...rest: unknown[]) => string;
27+
const moduleWithResolve = Module as unknown as { _resolveFilename: ResolveFilename };
28+
const originalResolveFilename = moduleWithResolve._resolveFilename;
29+
moduleWithResolve._resolveFilename = function (request, parent, ...rest) {
30+
const aliased = request === '@sentry/node' && NODE_SUITE_FILE.test(parent?.filename ?? '') ? '@sentry/bun' : request;
31+
return originalResolveFilename.call(this, aliased, parent, ...rest);
32+
};

‎dev-packages/bun-integration-tests/package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"pg": "8.16.0"
2222
},
2323
"devDependencies": {
24+
"@sentry-internal/node-integration-tests": "10.67.0",
2425
"@sentry-internal/test-utils": "10.67.0",
2526
"bun-types": "^1.2.9",
2627
"vitest": "^3.2.7"

‎dev-packages/bun-integration-tests/tsconfig.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"extends": "../../tsconfig.json",
33

4-
"include": ["suites/**/*.ts", "*.ts"],
4+
"include": ["suites/**/*.ts", "node-suites/**/*.ts", "*.ts"],
55

66
"compilerOptions": {
77
"lib": ["ES2020"],
Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,26 @@
1+
import { fileURLToPath } from 'node:url';
12
import { defineConfig } from 'vitest/config';
23
import baseConfig from '../../vite/vite.config';
34

5+
const NODE_SUITES_ROOT = fileURLToPath(new URL('../node-integration-tests', import.meta.url));
6+
7+
// Node suites that also run on Bun. The scenarios stay in `node-integration-tests`.
8+
const NODE_SUITES = [
9+
'suites/public-api/**/test.ts',
10+
'suites/client-reports/**/test.ts',
11+
'suites/featureFlags/**/test.ts',
12+
];
13+
14+
// Single tests that fail on Bun are skipped with `test.skipIf` on `RUNTIME` in the Node suite.
15+
const NODE_SUITES_EXCLUDE = ['**/node_modules/**'];
16+
17+
const nodeSuitesTest = {
18+
root: NODE_SUITES_ROOT,
19+
include: NODE_SUITES,
20+
exclude: NODE_SUITES_EXCLUDE,
21+
testTimeout: 15_000,
22+
};
23+
424
export default defineConfig({
525
...baseConfig,
626
test: {
@@ -9,7 +29,6 @@ export default defineConfig({
929
enabled: false,
1030
},
1131
isolate: false,
12-
include: ['./suites/**/test.ts'],
1332
testTimeout: 20_000,
1433
...(process.env.DEBUG
1534
? {
@@ -18,15 +37,49 @@ export default defineConfig({
1837
}
1938
: {}),
2039
pool: 'threads',
21-
poolOptions: {
22-
threads: {
23-
singleThread: true,
24-
},
25-
},
2640
reporters: process.env.DEBUG
2741
? ['default', { summary: false }]
2842
: process.env.GITHUB_ACTIONS
2943
? ['dot', 'github-actions']
3044
: ['verbose'],
45+
projects: [
46+
{
47+
extends: true,
48+
test: {
49+
name: 'bun',
50+
include: ['./suites/**/test.ts'],
51+
poolOptions: {
52+
threads: {
53+
singleThread: true,
54+
},
55+
},
56+
},
57+
},
58+
{
59+
extends: true,
60+
test: {
61+
...nodeSuitesTest,
62+
name: 'node-suites',
63+
env: { RUNTIME: 'bun' },
64+
},
65+
},
66+
{
67+
extends: true,
68+
test: {
69+
...nodeSuitesTest,
70+
name: 'node-suites-sentry-bun',
71+
exclude: [
72+
...NODE_SUITES_EXCLUDE,
73+
// The scenario creates a `NodeClient` itself, which sends `sentry.javascript.node`.
74+
'suites/public-api/logs/test.ts',
75+
],
76+
env: {
77+
RUNTIME: 'bun',
78+
RUNTIME_PRELOAD: fileURLToPath(new URL('./node-suites/alias-sentry-bun.ts', import.meta.url)),
79+
EXPECTED_SDK_NAME: 'sentry.javascript.bun',
80+
},
81+
},
82+
},
83+
],
3184
},
3285
});
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"imports": {
3+
"@sentry-internal/node-integration-tests": "../../node-integration-tests/build/esm/index.js"
4+
}
5+
}

‎dev-packages/deno-integration-tests/package.json‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,20 @@
1111
"install:deno": "node ./scripts/install-deno.mjs",
1212
"lint": "oxlint . --type-aware",
1313
"lint:fix": "oxlint . --fix --type-aware",
14-
"test": "run-s install:deno deno-types test:unit",
15-
"test:unit": "deno test --allow-net --allow-read --allow-run --allow-env --no-check"
14+
"test": "run-s install:deno deno-types test:unit test:node-suites",
15+
"test:unit": "deno test --allow-net --allow-read --allow-run --allow-env --no-check",
16+
"test:node-suites": "vitest run"
1617
},
1718
"dependencies": {
1819
"@sentry/core": "10.67.0",
1920
"@sentry/deno": "10.67.0",
2021
"mysql": "^2.18.1",
2122
"pg": "^8.22.0"
2223
},
24+
"devDependencies": {
25+
"@sentry-internal/node-integration-tests": "10.67.0",
26+
"vitest": "^3.2.7"
27+
},
2328
"volta": {
2429
"extends": "../../package.json"
2530
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { fileURLToPath } from 'node:url';
2+
import { defineConfig } from 'vitest/config';
3+
import baseConfig from '../../vite/vite.config';
4+
5+
// Runs the Node suites below on Deno. The scenarios stay in `node-integration-tests`, and the
6+
// Deno-only suites in `suites/` run with `deno test`.
7+
export default defineConfig({
8+
...baseConfig,
9+
test: {
10+
...baseConfig.test,
11+
root: fileURLToPath(new URL('../node-integration-tests', import.meta.url)),
12+
coverage: {
13+
enabled: false,
14+
},
15+
isolate: false,
16+
include: [
17+
'suites/public-api/**/test.ts',
18+
'suites/client-reports/**/test.ts',
19+
'suites/featureFlags/**/test.ts',
20+
'suites/express/tracing/**/test.ts',
21+
'suites/tracing/httpIntegration/test.ts',
22+
'suites/tracing/httpIntegration-streamed/test.ts',
23+
],
24+
// Single tests that fail on Deno are skipped with `test.skipIf` on `RUNTIME` in the Node suite.
25+
exclude: ['**/node_modules/**'],
26+
env: {
27+
RUNTIME: 'deno',
28+
DENO_IMPORT_MAP: fileURLToPath(new URL('./node-suites/import-map.json', import.meta.url)),
29+
},
30+
testTimeout: 15_000,
31+
...(process.env.DEBUG
32+
? {
33+
disableConsoleIntercept: true,
34+
silent: false,
35+
}
36+
: {}),
37+
pool: 'threads',
38+
reporters: process.env.DEBUG
39+
? ['default', { summary: false }]
40+
: process.env.GITHUB_ACTIONS
41+
? ['dot', 'github-actions']
42+
: ['verbose'],
43+
},
44+
});

‎dev-packages/node-integration-tests/suites/public-api/LocalVariables/test.ts‎

Lines changed: 53 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { mkdirSync, rmdirSync, unlinkSync, writeFileSync } from 'fs';
22
import * as path from 'path';
33
import { afterAll, beforeAll, describe, expect, test } from 'vitest';
44
import { cleanupChildProcesses, createRunner } from '../../../utils/runner';
5+
import { RUNTIME } from '../../../utils';
56

67
const EXPECTED_LOCAL_VARIABLES_EVENT = {
78
exception: {
@@ -79,14 +80,15 @@ module.exports = { out_of_app_function };`,
7980
.completed();
8081
});
8182

82-
test('Should include local variables when enabled', async () => {
83+
// Bun and Deno: the error events have no local variables.
84+
test.skipIf(RUNTIME !== 'node')('Should include local variables when enabled', async () => {
8385
await createRunner(__dirname, 'local-variables.js')
8486
.expect({ event: EXPECTED_LOCAL_VARIABLES_EVENT })
8587
.start()
8688
.completed();
8789
});
8890

89-
test('Should include local variables when instrumenting via --import', async () => {
91+
test.skipIf(RUNTIME !== 'node')('Should include local variables when instrumenting via --import', async () => {
9092
const instrumentPath = path.resolve(__dirname, 'local-variables-instrument.cjs');
9193

9294
await createRunner(__dirname, 'local-variables-no-sentry.js')
@@ -96,7 +98,7 @@ module.exports = { out_of_app_function };`,
9698
.completed();
9799
});
98100

99-
test('Should include local variables with ESM', async () => {
101+
test.skipIf(RUNTIME !== 'node')('Should include local variables with ESM', async () => {
100102
await createRunner(__dirname, 'local-variables-caught.mjs')
101103
.expect({ event: EXPECTED_LOCAL_VARIABLES_EVENT })
102104
.start()
@@ -107,36 +109,39 @@ module.exports = { out_of_app_function };`,
107109
await createRunner(__dirname, 'deny-inspector.mjs').ensureNoErrorOutput().start().completed();
108110
});
109111

110-
test('Should retain original local variables when error is re-thrown', async () => {
112+
test.skipIf(RUNTIME !== 'node')('Should retain original local variables when error is re-thrown', async () => {
111113
await createRunner(__dirname, 'local-variables-rethrow.js')
112114
.expect({ event: EXPECTED_LOCAL_VARIABLES_EVENT })
113115
.start()
114116
.completed();
115117
});
116118

117-
test('Includes local variables for caught exceptions when enabled', async () => {
119+
test.skipIf(RUNTIME !== 'node')('Includes local variables for caught exceptions when enabled', async () => {
118120
await createRunner(__dirname, 'local-variables-caught.js')
119121
.expect({ event: EXPECTED_LOCAL_VARIABLES_EVENT })
120122
.start()
121123
.completed();
122124
});
123125

124-
test('Filters local variables by name via dataCollection.stackFrameVariables', async () => {
125-
await createRunner(__dirname, 'local-variables-filtered.js')
126-
.expect({
127-
event: event => {
128-
const frame = event.exception?.values?.[0]?.stacktrace?.frames?.find(frame => frame.function === 'one');
129-
130-
expect(frame?.vars).toEqual({
131-
name: 'some name',
132-
keepVar: 'keep me',
133-
secretVar: '[Filtered]',
134-
});
135-
},
136-
})
137-
.start()
138-
.completed();
139-
});
126+
test.skipIf(RUNTIME !== 'node')(
127+
'Filters local variables by name via dataCollection.stackFrameVariables',
128+
async () => {
129+
await createRunner(__dirname, 'local-variables-filtered.js')
130+
.expect({
131+
event: event => {
132+
const frame = event.exception?.values?.[0]?.stacktrace?.frames?.find(frame => frame.function === 'one');
133+
134+
expect(frame?.vars).toEqual({
135+
name: 'some name',
136+
keepVar: 'keep me',
137+
secretVar: '[Filtered]',
138+
});
139+
},
140+
})
141+
.start()
142+
.completed();
143+
},
144+
);
140145

141146
test('Does not attach local variables when dataCollection.stackFrameVariables is false', async () => {
142147
await createRunner(__dirname, 'local-variables-disabled.js')
@@ -151,7 +156,7 @@ module.exports = { out_of_app_function };`,
151156
.completed();
152157
});
153158

154-
test('Should handle different function name formats', async () => {
159+
test.skipIf(RUNTIME !== 'node')('Should handle different function name formats', async () => {
155160
await createRunner(__dirname, 'local-variables-name-matching.js')
156161
.expect({
157162
event: {
@@ -177,30 +182,33 @@ module.exports = { out_of_app_function };`,
177182
.completed();
178183
});
179184

180-
test('adds local variables to out of app frames when includeOutOfAppFrames is true', async () => {
181-
await createRunner(__dirname, 'local-variables-out-of-app.js')
182-
.expect({
183-
event: event => {
184-
const frames = event.exception?.values?.[0]?.stacktrace?.frames || [];
185-
186-
const inAppFrame = frames.find(frame => frame.function === 'in_app_function');
187-
const outOfAppFrame = frames.find(frame => frame.function === 'out_of_app_function');
188-
189-
expect(inAppFrame?.vars).toEqual({ inAppVar: 'in app value' });
190-
expect(inAppFrame?.in_app).toEqual(true);
191-
192-
expect(outOfAppFrame?.vars).toEqual({
193-
outOfAppVar: 'out of app value modified value',
194-
passedArg: 'in app value modified value',
195-
});
196-
expect(outOfAppFrame?.in_app).toEqual(false);
197-
},
198-
})
199-
.start()
200-
.completed();
201-
});
185+
test.skipIf(RUNTIME !== 'node')(
186+
'adds local variables to out of app frames when includeOutOfAppFrames is true',
187+
async () => {
188+
await createRunner(__dirname, 'local-variables-out-of-app.js')
189+
.expect({
190+
event: event => {
191+
const frames = event.exception?.values?.[0]?.stacktrace?.frames || [];
192+
193+
const inAppFrame = frames.find(frame => frame.function === 'in_app_function');
194+
const outOfAppFrame = frames.find(frame => frame.function === 'out_of_app_function');
195+
196+
expect(inAppFrame?.vars).toEqual({ inAppVar: 'in app value' });
197+
expect(inAppFrame?.in_app).toEqual(true);
198+
199+
expect(outOfAppFrame?.vars).toEqual({
200+
outOfAppVar: 'out of app value modified value',
201+
passedArg: 'in app value modified value',
202+
});
203+
expect(outOfAppFrame?.in_app).toEqual(false);
204+
},
205+
})
206+
.start()
207+
.completed();
208+
},
209+
);
202210

203-
test('does not add local variables to out of app frames by default', async () => {
211+
test.skipIf(RUNTIME !== 'node')('does not add local variables to out of app frames by default', async () => {
204212
await createRunner(__dirname, 'local-variables-out-of-app-default.js')
205213
.expect({
206214
event: event => {

‎dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/test.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as childProcess from 'child_process';
22
import * as path from 'path';
33
import { describe, expect, test } from 'vitest';
44
import { createRunner } from '../../../utils/runner';
5+
import { RUNTIME } from '../../../utils';
56

67
describe('OnUncaughtException integration', () => {
78
test('should close process on uncaught error with no additional listeners registered', () =>
@@ -122,7 +123,8 @@ describe('OnUncaughtException integration', () => {
122123
.completed();
123124
});
124125

125-
describe('Worker thread error handling', () => {
126+
// Bun and Deno: the worker thread errors are not handled as on Node.
127+
describe.skipIf(RUNTIME !== 'node')('Worker thread error handling', () => {
126128
test.each(['mjs', 'js'])('should not interfere with worker thread error handling ".%s"', async extension => {
127129
const runner = createRunner(__dirname, `worker-thread/caught-worker.${extension}`)
128130
.withFlags('--import', path.join(__dirname, `worker-thread/instrument.${extension}`))

0 commit comments

Comments
 (0)