Skip to content

Commit f5caf00

Browse files
JPeer264claude
andcommitted
test(bun): Run the auto-instrumentation suites with bundled scenarios
Under `bun run` the SDK cannot inject the diagnostics channels into libraries (JS-3508), so Bun apps must be built with `@sentry/bun/plugin` to get framework, database and AI spans. The new `node-suites-bun-build` project tests that path: the runner builds each scenario with the plugin right before it starts (`RUNTIME_BUILD_SCRIPT`), and runs the bundle with `@sentry/bun`. The Docker suites run as well, so the Bun CI job gets the disk space step of the Node and Deno jobs. `pg-native` and the CommonJS `postgresjs` tests are skipped on Bun: the `libpq` addon needs a Node symbol that Bun does not have, and through the `bun` export condition `require('postgres')` returns the ES module namespace. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 0ed13e4 commit f5caf00

13 files changed

Lines changed: 256 additions & 73 deletions

File tree

‎.github/workflows/build.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -907,7 +907,7 @@ jobs:
907907
needs: [job_get_metadata, job_build]
908908
if: needs.job_build.outputs.changed_bun_integration == 'true' || github.event_name != 'pull_request'
909909
runs-on: ubuntu-24.04
910-
timeout-minutes: 15
910+
timeout-minutes: 20
911911
steps:
912912
- name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }})
913913
uses: actions/checkout@v7
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Bundles one Node suite scenario with `@sentry/bun/plugin`, so the plugin can inject the
2+
// diagnostics channels into the libraries the scenario uses. The runner calls this script with the
3+
// scenario path when `RUNTIME_BUILD_SCRIPT` points to it, and runs the printed output file instead.
4+
import { sentryBunPlugin } from '@sentry/bun/plugin';
5+
import { dirname, join, relative } from 'node:path';
6+
7+
const NODE_SUITES_ROOT = join(import.meta.dir, '..', '..', 'node-integration-tests');
8+
const BUILD_ROOT = join(import.meta.dir, '..', 'build');
9+
10+
const entry = process.argv[2];
11+
if (!entry) {
12+
// eslint-disable-next-line no-console
13+
console.error('BUILD_FAILED no scenario path');
14+
process.exit(1);
15+
}
16+
17+
const result = await Bun.build({
18+
entrypoints: [entry],
19+
target: 'bun',
20+
// Mirrors the scenario's folder, so relative paths between scenarios keep their shape.
21+
outdir: join(BUILD_ROOT, relative(NODE_SUITES_ROOT, dirname(entry))),
22+
sourcemap: 'linked',
23+
// The instrument file is preloaded unbundled, so the bundle must share its `@sentry/*` packages.
24+
// knex requires the drivers of all its dialects, and the ones that are not installed must stay
25+
// external so the bundle still builds.
26+
external: ['@sentry/*', '@sentry-internal/*', 'better-sqlite3', 'oracledb', 'pg-query-stream', 'sqlite3'],
27+
plugins: [sentryBunPlugin()],
28+
});
29+
30+
const output = result.outputs.find(file => file.kind === 'entry-point');
31+
if (!result.success || !output) {
32+
// eslint-disable-next-line no-console
33+
console.error('BUILD_FAILED', result.logs);
34+
process.exit(1);
35+
}
36+
37+
// eslint-disable-next-line no-console
38+
console.log(`BUILD_OK ${output.path}`);

‎dev-packages/bun-integration-tests/node-suites/excludes.ts‎

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ const NO_OUTGOING_HTTP_INSTRUMENTATION = [
7575
// `bun run` cannot inject the diagnostics channels into libraries, so framework, database and AI
7676
// instrumentation creates no spans. Apps must be built with `@sentry/bun/plugin`.
7777
// See https://github.com/getsentry/sentry-javascript/issues/23882
78-
const NO_AUTO_INSTRUMENTATION = [
78+
export const NO_AUTO_INSTRUMENTATION = [
7979
'suites/express/**',
8080
'suites/fs-instrumentation/test.ts',
8181
'suites/hono-sdk/test.ts',
@@ -215,3 +215,56 @@ export const SENTRY_BUN_EXCLUDE = [
215215
...NO_NATIVE_NODE_FETCH_INTEGRATION,
216216
...FETCH_INTEGRATION_DIFFERS,
217217
];
218+
219+
// The build project (`node-suites-bun-build`) runs the suites of `NO_AUTO_INSTRUMENTATION` with the
220+
// scenarios bundled by `@sentry/bun/plugin`. These do not run there.
221+
222+
// On Bun the channel integrations subscribe at `init()` by design, and this suite checks that
223+
// they wait until their module loads.
224+
const BUN_BUILD_EAGER_SUBSCRIPTION = ['suites/tracing/orchestrion-lazy-registration/test.ts'];
225+
226+
// The first `init()` has no DSN, so `bunHttpServerIntegration` is not set up, and the suite then
227+
// adds only `httpIntegration`. On Bun that does not isolate requests, because Bun does not publish
228+
// `http.server.request.start`.
229+
const BUN_BUILD_NO_HTTP_SERVER_INTEGRATION = ['suites/express/multiple-init/test.ts'];
230+
231+
// Some or all tests fail with the bundled scenarios, cause not investigated yet. In
232+
// `express/tracing` only the request data tests fail: they set `httpIntegration` options, and with
233+
// `@sentry/bun` the request body comes from `bunHttpServerIntegration`.
234+
const BUN_BUILD_NOT_TRIAGED = [
235+
'suites/express/sentry-trace/test.ts',
236+
'suites/express/tracing/test.ts',
237+
'suites/express/with-http/**',
238+
'suites/hono-sdk/test.ts',
239+
'suites/pino/test.ts',
240+
'suites/tracing/google-genai-v2/test.ts',
241+
'suites/tracing/google-genai/test.ts',
242+
'suites/tracing/langchain/v1/test.ts',
243+
'suites/tracing/mastra/test.ts',
244+
'suites/tracing/mcp-handler-exact-once/test.ts',
245+
'suites/tracing/mcp-server-streamed/test.ts',
246+
'suites/tracing/mongodb-v4/test.ts',
247+
'suites/tracing/mongodb-v5/test.ts',
248+
'suites/tracing/mongodb-v6/test.ts',
249+
'suites/tracing/mongodb-v7/test.ts',
250+
'suites/tracing/mongodb/test.ts',
251+
'suites/tracing/mongoose-tracing-channel/test.ts',
252+
'suites/tracing/mongoose-v5/test.ts',
253+
'suites/tracing/mongoose-v7/test.ts',
254+
'suites/tracing/mongoose-v8/test.ts',
255+
'suites/tracing/mongoose-v9/test.ts',
256+
'suites/tracing/mongoose/test.ts',
257+
'suites/tracing/mysql/test.ts',
258+
'suites/tracing/openai/test.ts',
259+
'suites/tracing/prisma-orm-v8/test.ts',
260+
'suites/tracing/together-ai/test.ts',
261+
'suites/tracing/vercelai/test.ts',
262+
'suites/tracing/vercelai/v6_v7/test.ts',
263+
];
264+
265+
export const BUN_BUILD_EXCLUDE = [
266+
'**/node_modules/**',
267+
...BUN_BUILD_EAGER_SUBSCRIPTION,
268+
...BUN_BUILD_NO_HTTP_SERVER_INTEGRATION,
269+
...BUN_BUILD_NOT_TRIAGED,
270+
];

‎dev-packages/bun-integration-tests/vite.config.mts‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { fileURLToPath } from 'node:url';
22
import { defineConfig } from 'vitest/config';
33
import baseConfig from '../../vite/vite.config';
4-
import { NODE_SUITES_EXCLUDE, SENTRY_BUN_EXCLUDE, SENTRY_NODE_EXCLUDE } from './node-suites/excludes';
4+
import {
5+
BUN_BUILD_EXCLUDE,
6+
NO_AUTO_INSTRUMENTATION,
7+
NODE_SUITES_EXCLUDE,
8+
SENTRY_BUN_EXCLUDE,
9+
SENTRY_NODE_EXCLUDE,
10+
} from './node-suites/excludes';
511

612
const NODE_SUITES_ROOT = fileURLToPath(new URL('../node-integration-tests', import.meta.url));
713

@@ -82,6 +88,24 @@ export default defineConfig({
8288
},
8389
},
8490
},
91+
{
92+
extends: true,
93+
test: {
94+
...nodeSuitesTest,
95+
// The auto-instrumentation suites, with each scenario bundled by `@sentry/bun/plugin` before
96+
// it starts, as Bun apps must be built to get these spans.
97+
// See https://github.com/getsentry/sentry-javascript/issues/23882
98+
name: 'node-suites-bun-build',
99+
include: NO_AUTO_INSTRUMENTATION.map(glob => (glob.endsWith('/**') ? `${glob}/test.ts` : glob)),
100+
exclude: BUN_BUILD_EXCLUDE,
101+
env: {
102+
RUNTIME: 'bun',
103+
RUNTIME_PRELOAD: fileURLToPath(new URL('./node-suites/alias-sentry-bun.ts', import.meta.url)),
104+
RUNTIME_BUILD_SCRIPT: fileURLToPath(new URL('./node-suites/bun-build.ts', import.meta.url)),
105+
EXPECTED_SDK_NAME: 'sentry.javascript.bun',
106+
},
107+
},
108+
},
85109
],
86110
},
87111
});

‎dev-packages/node-integration-tests/suites/tracing/ioredis-dc/test.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { SENTRY_TRACE_LIFECYCLE } from '@sentry/conventions/attributes';
22
import type { SerializedStreamedSpanContainer } from '@sentry/core';
33
import { afterAll, describe, expect } from 'vitest';
4+
import { EXPECTED_SDK_NAME } from '../../../utils';
45
import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner';
56

67
describeWithDockerCompose(
@@ -135,7 +136,7 @@ describeWithDockerCompose(
135136
'sentry.op': op,
136137
'sentry.origin': ORIGIN,
137138
'sentry.release': '1.0',
138-
'sentry.sdk.name': 'sentry.javascript.node',
139+
'sentry.sdk.name': EXPECTED_SDK_NAME,
139140
'sentry.segment.name': SEGMENT_NAME,
140141
'server.address': HOST,
141142
'server.port': PORT,

‎dev-packages/node-integration-tests/suites/tracing/postgres-streamed/test.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_OP } from '@sentry/core';
22
import type { SerializedStreamedSpanContainer } from '@sentry/core';
33
import { SENTRY_TRACE_LIFECYCLE } from '@sentry/conventions/attributes';
44
import { afterAll, describe, expect } from 'vitest';
5-
import { conditionalTest } from '../../../utils';
5+
import { conditionalTest, EXPECTED_SDK_NAME } from '../../../utils';
66
import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner';
77

88
// Query-span origin depends on which instrumentation is active. Blocks driving the SDK's default
@@ -58,7 +58,7 @@ const COMMON_DB_ATTRIBUTES = {
5858
},
5959
'sentry.sdk.name': {
6060
type: 'string',
61-
value: 'sentry.javascript.node',
61+
value: EXPECTED_SDK_NAME,
6262
},
6363
'sentry.sdk.version': {
6464
type: 'string',
@@ -228,7 +228,8 @@ describeWithDockerCompose('postgres auto instrumentation (streamed)', { workingD
228228
});
229229

230230
// Deno: with a module load hook installed, Deno compiles a native addon (`libpq`) as JavaScript.
231-
conditionalTest({ max: 25, skipRuntimes: ['deno'] })('pg-native', () => {
231+
// Bun: the `libpq` addon needs the Node symbol `node::EmitAsyncInit`, which Bun does not provide.
232+
conditionalTest({ max: 25, skipRuntimes: ['bun', 'deno'] })('pg-native', () => {
232233
createEsmAndCjsTests(
233234
__dirname,
234235
'scenario-native.mjs',

‎dev-packages/node-integration-tests/suites/tracing/postgres/test.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,8 @@ describeWithDockerCompose('postgres auto instrumentation', { workingDirectory: [
277277
});
278278

279279
// Deno: with a module load hook installed, Deno compiles a native addon (`libpq`) as JavaScript.
280-
conditionalTest({ max: 25, skipRuntimes: ['deno'] })('pg-native', () => {
280+
// Bun: the `libpq` addon needs the Node symbol `node::EmitAsyncInit`, which Bun does not provide.
281+
conditionalTest({ max: 25, skipRuntimes: ['bun', 'deno'] })('pg-native', () => {
281282
const EXPECTED_TRANSACTION = {
282283
transaction: 'Test Transaction',
283284
spans: expect.arrayContaining([

‎dev-packages/node-integration-tests/suites/tracing/postgresjs-streamed/test.ts‎

Lines changed: 55 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import type { SerializedStreamedSpanContainer } from '@sentry/core';
22
import { afterAll, describe, expect } from 'vitest';
3+
import { RUNTIME } from '../../../utils';
34
import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner';
45

6+
// On Bun, `postgres` resolves to its ESM build through the `bun` export condition, so
7+
// `require('postgres')` returns the module namespace instead of the `postgres` function.
8+
59
/**
610
* Streamed span attributes are `{ value, type }` objects, unlike transaction span `data`,
711
* which stores values directly.
@@ -168,30 +172,34 @@ describeWithDockerCompose('postgresjs auto instrumentation (streamed)', { workin
168172
},
169173
};
170174

171-
createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createTestRunner, test) => {
172-
test('should auto-instrument `postgres` package', { timeout: 90_000 }, async () => {
173-
await createTestRunner()
174-
.expect({
175-
span: container => {
176-
expect(container).toMatchObject(EXPECTED_SPANS);
175+
createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createTestRunner, test, mode) => {
176+
test.skipIf(RUNTIME === 'bun' && mode === 'cjs')(
177+
'should auto-instrument `postgres` package',
178+
{ timeout: 90_000 },
179+
async () => {
180+
await createTestRunner()
181+
.expect({
182+
span: container => {
183+
expect(container).toMatchObject(EXPECTED_SPANS);
177184

178-
// The assertions above only cover the queries the scenario issues itself. postgres.js
179-
// also runs internal ones (e.g. the `pg_catalog` type lookup), so guard the invariant
180-
// across every query span: the name is the summary, never the statement.
181-
const dbSpans = getDbSpans(container);
182-
expect(dbSpans.length).toBeGreaterThan(0);
183-
for (const span of dbSpans) {
184-
expect(span.name).toBe(span.attributes['db.query.summary']?.value);
185-
}
186-
},
187-
})
188-
.expect({ event: EXPECTED_ERROR_EVENT })
189-
// The error event is captured via an unhandled rejection processed on a later tick than
190-
// the spans, so the two envelopes can reach the transport in either order.
191-
.unordered()
192-
.start()
193-
.completed();
194-
});
185+
// The assertions above only cover the queries the scenario issues itself. postgres.js
186+
// also runs internal ones (e.g. the `pg_catalog` type lookup), so guard the invariant
187+
// across every query span: the name is the summary, never the statement.
188+
const dbSpans = getDbSpans(container);
189+
expect(dbSpans.length).toBeGreaterThan(0);
190+
for (const span of dbSpans) {
191+
expect(span.name).toBe(span.attributes['db.query.summary']?.value);
192+
}
193+
},
194+
})
195+
.expect({ event: EXPECTED_ERROR_EVENT })
196+
// The error event is captured via an unhandled rejection processed on a later tick than
197+
// the spans, so the two envelopes can reach the transport in either order.
198+
.unordered()
199+
.start()
200+
.completed();
201+
},
202+
);
195203
});
196204
});
197205

@@ -228,10 +236,14 @@ describeWithDockerCompose('postgresjs auto instrumentation (streamed)', { workin
228236
__dirname,
229237
'scenario-requestHook.mjs',
230238
'instrument-requestHook.mjs',
231-
(createTestRunner, test) => {
232-
test('should call requestHook when provided', { timeout: 90_000 }, async () => {
233-
await createTestRunner().expect({ span: EXPECTED_SPANS }).start().completed();
234-
});
239+
(createTestRunner, test, mode) => {
240+
test.skipIf(RUNTIME === 'bun' && mode === 'cjs')(
241+
'should call requestHook when provided',
242+
{ timeout: 90_000 },
243+
async () => {
244+
await createTestRunner().expect({ span: EXPECTED_SPANS }).start().completed();
245+
},
246+
);
235247
},
236248
);
237249
});
@@ -262,10 +274,14 @@ describeWithDockerCompose('postgresjs auto instrumentation (streamed)', { workin
262274
]),
263275
};
264276

265-
createEsmAndCjsTests(__dirname, 'scenario-url.mjs', 'instrument.mjs', (createTestRunner, test) => {
266-
test('should instrument postgres package with URL initialization', { timeout: 90_000 }, async () => {
267-
await createTestRunner().ignore('event').expect({ span: EXPECTED_SPANS }).start().completed();
268-
});
277+
createEsmAndCjsTests(__dirname, 'scenario-url.mjs', 'instrument.mjs', (createTestRunner, test, mode) => {
278+
test.skipIf(RUNTIME === 'bun' && mode === 'cjs')(
279+
'should instrument postgres package with URL initialization',
280+
{ timeout: 90_000 },
281+
async () => {
282+
await createTestRunner().ignore('event').expect({ span: EXPECTED_SPANS }).start().completed();
283+
},
284+
);
269285
});
270286
});
271287

@@ -296,10 +312,14 @@ describeWithDockerCompose('postgresjs auto instrumentation (streamed)', { workin
296312
]),
297313
};
298314

299-
createEsmAndCjsTests(__dirname, 'scenario-unsafe.mjs', 'instrument.mjs', (createTestRunner, test) => {
300-
test('should instrument sql.unsafe() queries', { timeout: 90_000 }, async () => {
301-
await createTestRunner().ignore('event').expect({ span: EXPECTED_SPANS }).start().completed();
302-
});
315+
createEsmAndCjsTests(__dirname, 'scenario-unsafe.mjs', 'instrument.mjs', (createTestRunner, test, mode) => {
316+
test.skipIf(RUNTIME === 'bun' && mode === 'cjs')(
317+
'should instrument sql.unsafe() queries',
318+
{ timeout: 90_000 },
319+
async () => {
320+
await createTestRunner().ignore('event').expect({ span: EXPECTED_SPANS }).start().completed();
321+
},
322+
);
303323
});
304324
});
305325
});

0 commit comments

Comments
 (0)