Skip to content

Commit f25eee7

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 27c4421 commit f25eee7

13 files changed

Lines changed: 250 additions & 73 deletions

File tree

.github/workflows/build.yml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -895,12 +895,25 @@ jobs:
895895
needs: [job_get_metadata, job_build]
896896
if: needs.job_build.outputs.changed_bun_integration == 'true' || github.event_name != 'pull_request'
897897
runs-on: ubuntu-24.04
898-
timeout-minutes: 15
898+
timeout-minutes: 20
899899
steps:
900900
- name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }})
901901
uses: actions/checkout@v7
902902
with:
903903
ref: ${{ env.HEAD_COMMIT }}
904+
- name: Free up disk space if low
905+
# The bundled Node suites pull several DB docker images (mssql alone is ~1.5GB)
906+
# Available disk space is not consistent, if we detect low space this cleans up some unused toolchains
907+
run: |
908+
df -h /
909+
avail_kb=$(df -k --output=avail / | tail -1)
910+
if [ "$avail_kb" -lt $((40 * 1024 * 1024)) ]; then
911+
echo "Low disk space (<40GB free), reclaiming unused toolchains"
912+
sudo rm -rf /usr/local/lib/android /usr/share/dotnet /opt/ghc /usr/local/.ghcup /opt/hostedtoolcache/CodeQL
913+
df -h /
914+
else
915+
echo "Sufficient disk space, skipping cleanup"
916+
fi
904917
- name: Set up Node
905918
uses: actions/setup-node@v7
906919
with:
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: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ const NO_OUTGOING_HTTP_INSTRUMENTATION = [
5252

5353
// JS-3508: `bun run` cannot inject the diagnostics channels into libraries, so framework,
5454
// database and AI instrumentation creates no spans. Apps must be built with `@sentry/bun/plugin`.
55-
const NO_AUTO_INSTRUMENTATION = [
55+
export const NO_AUTO_INSTRUMENTATION = [
5656
'suites/express/**',
5757
'suites/tracing/httpIntegration-streamed/test.ts',
5858
'suites/fs-instrumentation/test.ts',
@@ -136,3 +136,44 @@ export const NODE_SUITES_EXCLUDE = [
136136
...NO_AUTO_INSTRUMENTATION,
137137
...NOT_TRIAGED,
138138
];
139+
140+
// The build project (`node-suites-bun-build`) runs the suites of `NO_AUTO_INSTRUMENTATION` with the
141+
// scenarios bundled by `@sentry/bun/plugin`. These do not run there.
142+
143+
// On Bun the channel integrations subscribe at `init()` by design, and this suite checks that
144+
// they wait until their module loads.
145+
const BUN_BUILD_EAGER_SUBSCRIPTION = ['suites/tracing/orchestrion-lazy-registration/test.ts'];
146+
147+
// Some or all tests fail with the bundled scenarios, cause not investigated yet. In
148+
// `express/tracing` only the request data tests fail: they set `httpIntegration` options, and with
149+
// `@sentry/bun` the request body comes from `bunHttpServerIntegration`.
150+
const BUN_BUILD_NOT_TRIAGED = [
151+
'suites/express/sentry-trace/test.ts',
152+
'suites/express/tracing/test.ts',
153+
'suites/express/with-http/**',
154+
'suites/hono-sdk/test.ts',
155+
'suites/pino/test.ts',
156+
'suites/tracing/google-genai-v2/test.ts',
157+
'suites/tracing/google-genai/test.ts',
158+
'suites/tracing/langchain/v1/test.ts',
159+
'suites/tracing/mcp-handler-exact-once/test.ts',
160+
'suites/tracing/mcp-server-streamed/test.ts',
161+
'suites/tracing/mongodb-v4/test.ts',
162+
'suites/tracing/mongodb-v5/test.ts',
163+
'suites/tracing/mongodb-v6/test.ts',
164+
'suites/tracing/mongodb-v7/test.ts',
165+
'suites/tracing/mongodb/test.ts',
166+
'suites/tracing/mongoose-tracing-channel/test.ts',
167+
'suites/tracing/mongoose-v5/test.ts',
168+
'suites/tracing/mongoose-v7/test.ts',
169+
'suites/tracing/mongoose-v8/test.ts',
170+
'suites/tracing/mongoose-v9/test.ts',
171+
'suites/tracing/mongoose/test.ts',
172+
'suites/tracing/mysql/test.ts',
173+
'suites/tracing/openai/test.ts',
174+
'suites/tracing/together-ai/test.ts',
175+
'suites/tracing/vercelai/test.ts',
176+
'suites/tracing/vercelai/v6_v7/test.ts',
177+
];
178+
179+
export const BUN_BUILD_EXCLUDE = ['**/node_modules/**', ...BUN_BUILD_EAGER_SUBSCRIPTION, ...BUN_BUILD_NOT_TRIAGED];

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { fileURLToPath } from 'node:url';
22
import { defineConfig } from 'vitest/config';
33
import baseConfig from '../../vite/vite.config';
4-
import { NODE_SUITES_EXCLUDE } from './node-suites/excludes';
4+
import { BUN_BUILD_EXCLUDE, NO_AUTO_INSTRUMENTATION, NODE_SUITES_EXCLUDE } from './node-suites/excludes';
55

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

@@ -77,6 +77,23 @@ export default defineConfig({
7777
},
7878
},
7979
},
80+
{
81+
extends: true,
82+
test: {
83+
...nodeSuitesTest,
84+
// The auto-instrumentation suites, with each scenario bundled by `@sentry/bun/plugin` before
85+
// it starts, as Bun apps must be built to get these spans (JS-3508).
86+
name: 'node-suites-bun-build',
87+
include: NO_AUTO_INSTRUMENTATION.map(glob => (glob.endsWith('/**') ? `${glob}/test.ts` : glob)),
88+
exclude: BUN_BUILD_EXCLUDE,
89+
env: {
90+
RUNTIME: 'bun',
91+
RUNTIME_PRELOAD: fileURLToPath(new URL('./node-suites/alias-sentry-bun.ts', import.meta.url)),
92+
RUNTIME_BUILD_SCRIPT: fileURLToPath(new URL('./node-suites/bun-build.ts', import.meta.url)),
93+
EXPECTED_SDK_NAME: 'sentry.javascript.bun',
94+
},
95+
},
96+
},
8097
],
8198
},
8299
});

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, RUNTIME } from '../../../utils';
5+
import { conditionalTest, EXPECTED_SDK_NAME, RUNTIME } 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-
(RUNTIME === 'deno' ? describe.skip : conditionalTest({ max: 25 }))('pg-native', () => {
231+
// Bun: the `libpq` addon needs the Node symbol `node::EmitAsyncInit`, which Bun does not provide.
232+
(RUNTIME !== 'node' ? describe.skip : conditionalTest({ max: 25 }))('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-
(RUNTIME === 'deno' ? describe.skip : conditionalTest({ max: 25 }))('pg-native', () => {
280+
// Bun: the `libpq` addon needs the Node symbol `node::EmitAsyncInit`, which Bun does not provide.
281+
(RUNTIME !== 'node' ? describe.skip : conditionalTest({ max: 25 }))('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)