From 5f2cd778e58cf311aef6b330effdffef3ba009e0 Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Wed, 23 Sep 2026 11:17:41 +0200 Subject: [PATCH 1/4] 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 --- .github/workflows/build.yml | 15 +++- .../node-suites/bun-build.ts | 38 ++++++++ .../node-suites/excludes.ts | 43 ++++++++- .../bun-integration-tests/vite.config.mts | 19 +++- .../suites/tracing/ioredis-dc/test.ts | 3 +- .../suites/tracing/postgres-streamed/test.ts | 7 +- .../suites/tracing/postgres/test.ts | 3 +- .../tracing/postgresjs-streamed/test.ts | 90 +++++++++++-------- .../suites/tracing/postgresjs/test.ts | 70 +++++++++------ .../suites/tracing/redis-cache/test.ts | 3 +- .../suites/tracing/redis-dc/test.ts | 3 +- .../suites/tracing/redis/test.ts | 3 +- .../utils/runner/createRunner.ts | 26 +++++- 13 files changed, 250 insertions(+), 73 deletions(-) create mode 100644 dev-packages/bun-integration-tests/node-suites/bun-build.ts diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 45e298794f10..4e69eaf7fde6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -895,12 +895,25 @@ jobs: needs: [job_get_metadata, job_build] if: needs.job_build.outputs.changed_bun_integration == 'true' || github.event_name != 'pull_request' runs-on: ubuntu-24.04 - timeout-minutes: 15 + timeout-minutes: 20 steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) uses: actions/checkout@v7 with: ref: ${{ env.HEAD_COMMIT }} + - name: Free up disk space if low + # The bundled Node suites pull several DB docker images (mssql alone is ~1.5GB) + # Available disk space is not consistent, if we detect low space this cleans up some unused toolchains + run: | + df -h / + avail_kb=$(df -k --output=avail / | tail -1) + if [ "$avail_kb" -lt $((40 * 1024 * 1024)) ]; then + echo "Low disk space (<40GB free), reclaiming unused toolchains" + sudo rm -rf /usr/local/lib/android /usr/share/dotnet /opt/ghc /usr/local/.ghcup /opt/hostedtoolcache/CodeQL + df -h / + else + echo "Sufficient disk space, skipping cleanup" + fi - name: Set up Node uses: actions/setup-node@v7 with: diff --git a/dev-packages/bun-integration-tests/node-suites/bun-build.ts b/dev-packages/bun-integration-tests/node-suites/bun-build.ts new file mode 100644 index 000000000000..3c86daba1baf --- /dev/null +++ b/dev-packages/bun-integration-tests/node-suites/bun-build.ts @@ -0,0 +1,38 @@ +// Bundles one Node suite scenario with `@sentry/bun/plugin`, so the plugin can inject the +// diagnostics channels into the libraries the scenario uses. The runner calls this script with the +// scenario path when `RUNTIME_BUILD_SCRIPT` points to it, and runs the printed output file instead. +import { sentryBunPlugin } from '@sentry/bun/plugin'; +import { dirname, join, relative } from 'node:path'; + +const NODE_SUITES_ROOT = join(import.meta.dir, '..', '..', 'node-integration-tests'); +const BUILD_ROOT = join(import.meta.dir, '..', 'build'); + +const entry = process.argv[2]; +if (!entry) { + // eslint-disable-next-line no-console + console.error('BUILD_FAILED no scenario path'); + process.exit(1); +} + +const result = await Bun.build({ + entrypoints: [entry], + target: 'bun', + // Mirrors the scenario's folder, so relative paths between scenarios keep their shape. + outdir: join(BUILD_ROOT, relative(NODE_SUITES_ROOT, dirname(entry))), + sourcemap: 'linked', + // The instrument file is preloaded unbundled, so the bundle must share its `@sentry/*` packages. + // knex requires the drivers of all its dialects, and the ones that are not installed must stay + // external so the bundle still builds. + external: ['@sentry/*', '@sentry-internal/*', 'better-sqlite3', 'oracledb', 'pg-query-stream', 'sqlite3'], + plugins: [sentryBunPlugin()], +}); + +const output = result.outputs.find(file => file.kind === 'entry-point'); +if (!result.success || !output) { + // eslint-disable-next-line no-console + console.error('BUILD_FAILED', result.logs); + process.exit(1); +} + +// eslint-disable-next-line no-console +console.log(`BUILD_OK ${output.path}`); diff --git a/dev-packages/bun-integration-tests/node-suites/excludes.ts b/dev-packages/bun-integration-tests/node-suites/excludes.ts index 6d9ce5d37011..2e2b4f561447 100644 --- a/dev-packages/bun-integration-tests/node-suites/excludes.ts +++ b/dev-packages/bun-integration-tests/node-suites/excludes.ts @@ -71,7 +71,7 @@ const NO_OUTGOING_HTTP_INSTRUMENTATION = [ // `bun run` cannot inject the diagnostics channels into libraries, so framework, database and AI // instrumentation creates no spans. Apps must be built with `@sentry/bun/plugin`. // See https://github.com/getsentry/sentry-javascript/issues/23882 -const NO_AUTO_INSTRUMENTATION = [ +export const NO_AUTO_INSTRUMENTATION = [ 'suites/express/**', 'suites/fs-instrumentation/test.ts', 'suites/hono-sdk/test.ts', @@ -150,3 +150,44 @@ export const NODE_SUITES_EXCLUDE = [ ...NO_AUTO_INSTRUMENTATION, ...NOT_TRIAGED, ]; + +// The build project (`node-suites-bun-build`) runs the suites of `NO_AUTO_INSTRUMENTATION` with the +// scenarios bundled by `@sentry/bun/plugin`. These do not run there. + +// On Bun the channel integrations subscribe at `init()` by design, and this suite checks that +// they wait until their module loads. +const BUN_BUILD_EAGER_SUBSCRIPTION = ['suites/tracing/orchestrion-lazy-registration/test.ts']; + +// Some or all tests fail with the bundled scenarios, cause not investigated yet. In +// `express/tracing` only the request data tests fail: they set `httpIntegration` options, and with +// `@sentry/bun` the request body comes from `bunHttpServerIntegration`. +const BUN_BUILD_NOT_TRIAGED = [ + 'suites/express/sentry-trace/test.ts', + 'suites/express/tracing/test.ts', + 'suites/express/with-http/**', + 'suites/hono-sdk/test.ts', + 'suites/pino/test.ts', + 'suites/tracing/google-genai-v2/test.ts', + 'suites/tracing/google-genai/test.ts', + 'suites/tracing/langchain/v1/test.ts', + 'suites/tracing/mcp-handler-exact-once/test.ts', + 'suites/tracing/mcp-server-streamed/test.ts', + 'suites/tracing/mongodb-v4/test.ts', + 'suites/tracing/mongodb-v5/test.ts', + 'suites/tracing/mongodb-v6/test.ts', + 'suites/tracing/mongodb-v7/test.ts', + 'suites/tracing/mongodb/test.ts', + 'suites/tracing/mongoose-tracing-channel/test.ts', + 'suites/tracing/mongoose-v5/test.ts', + 'suites/tracing/mongoose-v7/test.ts', + 'suites/tracing/mongoose-v8/test.ts', + 'suites/tracing/mongoose-v9/test.ts', + 'suites/tracing/mongoose/test.ts', + 'suites/tracing/mysql/test.ts', + 'suites/tracing/openai/test.ts', + 'suites/tracing/together-ai/test.ts', + 'suites/tracing/vercelai/test.ts', + 'suites/tracing/vercelai/v6_v7/test.ts', +]; + +export const BUN_BUILD_EXCLUDE = ['**/node_modules/**', ...BUN_BUILD_EAGER_SUBSCRIPTION, ...BUN_BUILD_NOT_TRIAGED]; diff --git a/dev-packages/bun-integration-tests/vite.config.mts b/dev-packages/bun-integration-tests/vite.config.mts index 7e64518e836d..970dc9befe5b 100644 --- a/dev-packages/bun-integration-tests/vite.config.mts +++ b/dev-packages/bun-integration-tests/vite.config.mts @@ -1,7 +1,7 @@ import { fileURLToPath } from 'node:url'; import { defineConfig } from 'vitest/config'; import baseConfig from '../../vite/vite.config'; -import { NODE_SUITES_EXCLUDE } from './node-suites/excludes'; +import { BUN_BUILD_EXCLUDE, NO_AUTO_INSTRUMENTATION, NODE_SUITES_EXCLUDE } from './node-suites/excludes'; const NODE_SUITES_ROOT = fileURLToPath(new URL('../node-integration-tests', import.meta.url)); @@ -77,6 +77,23 @@ export default defineConfig({ }, }, }, + { + extends: true, + test: { + ...nodeSuitesTest, + // The auto-instrumentation suites, with each scenario bundled by `@sentry/bun/plugin` before + // it starts, as Bun apps must be built to get these spans (JS-3508). + name: 'node-suites-bun-build', + include: NO_AUTO_INSTRUMENTATION.map(glob => (glob.endsWith('/**') ? `${glob}/test.ts` : glob)), + exclude: BUN_BUILD_EXCLUDE, + env: { + RUNTIME: 'bun', + RUNTIME_PRELOAD: fileURLToPath(new URL('./node-suites/alias-sentry-bun.ts', import.meta.url)), + RUNTIME_BUILD_SCRIPT: fileURLToPath(new URL('./node-suites/bun-build.ts', import.meta.url)), + EXPECTED_SDK_NAME: 'sentry.javascript.bun', + }, + }, + }, ], }, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/ioredis-dc/test.ts b/dev-packages/node-integration-tests/suites/tracing/ioredis-dc/test.ts index e5332df32f76..e6866955ca5c 100644 --- a/dev-packages/node-integration-tests/suites/tracing/ioredis-dc/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/ioredis-dc/test.ts @@ -1,6 +1,7 @@ import { SENTRY_TRACE_LIFECYCLE } from '@sentry/conventions/attributes'; import type { SerializedStreamedSpanContainer } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; +import { EXPECTED_SDK_NAME } from '../../../utils'; import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner'; describeWithDockerCompose( @@ -135,7 +136,7 @@ describeWithDockerCompose( 'sentry.op': op, 'sentry.origin': ORIGIN, 'sentry.release': '1.0', - 'sentry.sdk.name': 'sentry.javascript.node', + 'sentry.sdk.name': EXPECTED_SDK_NAME, 'sentry.segment.name': SEGMENT_NAME, 'server.address': HOST, 'server.port': PORT, diff --git a/dev-packages/node-integration-tests/suites/tracing/postgres-streamed/test.ts b/dev-packages/node-integration-tests/suites/tracing/postgres-streamed/test.ts index c1bb8ef83855..abc3b2e0de45 100644 --- a/dev-packages/node-integration-tests/suites/tracing/postgres-streamed/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/postgres-streamed/test.ts @@ -2,7 +2,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_OP } from '@sentry/core'; import type { SerializedStreamedSpanContainer } from '@sentry/core'; import { SENTRY_TRACE_LIFECYCLE } from '@sentry/conventions/attributes'; import { afterAll, describe, expect } from 'vitest'; -import { conditionalTest, RUNTIME } from '../../../utils'; +import { conditionalTest, EXPECTED_SDK_NAME, RUNTIME } from '../../../utils'; import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner'; // Query-span origin depends on which instrumentation is active. Blocks driving the SDK's default @@ -58,7 +58,7 @@ const COMMON_DB_ATTRIBUTES = { }, 'sentry.sdk.name': { type: 'string', - value: 'sentry.javascript.node', + value: EXPECTED_SDK_NAME, }, 'sentry.sdk.version': { type: 'string', @@ -228,7 +228,8 @@ describeWithDockerCompose('postgres auto instrumentation (streamed)', { workingD }); // Deno: with a module load hook installed, Deno compiles a native addon (`libpq`) as JavaScript. - (RUNTIME === 'deno' ? describe.skip : conditionalTest({ max: 25 }))('pg-native', () => { + // Bun: the `libpq` addon needs the Node symbol `node::EmitAsyncInit`, which Bun does not provide. + (RUNTIME !== 'node' ? describe.skip : conditionalTest({ max: 25 }))('pg-native', () => { createEsmAndCjsTests( __dirname, 'scenario-native.mjs', diff --git a/dev-packages/node-integration-tests/suites/tracing/postgres/test.ts b/dev-packages/node-integration-tests/suites/tracing/postgres/test.ts index 0bba595a1c1d..fc7245ed5a48 100644 --- a/dev-packages/node-integration-tests/suites/tracing/postgres/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/postgres/test.ts @@ -277,7 +277,8 @@ describeWithDockerCompose('postgres auto instrumentation', { workingDirectory: [ }); // Deno: with a module load hook installed, Deno compiles a native addon (`libpq`) as JavaScript. - (RUNTIME === 'deno' ? describe.skip : conditionalTest({ max: 25 }))('pg-native', () => { + // Bun: the `libpq` addon needs the Node symbol `node::EmitAsyncInit`, which Bun does not provide. + (RUNTIME !== 'node' ? describe.skip : conditionalTest({ max: 25 }))('pg-native', () => { const EXPECTED_TRANSACTION = { transaction: 'Test Transaction', spans: expect.arrayContaining([ diff --git a/dev-packages/node-integration-tests/suites/tracing/postgresjs-streamed/test.ts b/dev-packages/node-integration-tests/suites/tracing/postgresjs-streamed/test.ts index e77702f8242b..5acbaba41e8f 100644 --- a/dev-packages/node-integration-tests/suites/tracing/postgresjs-streamed/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/postgresjs-streamed/test.ts @@ -1,7 +1,11 @@ import type { SerializedStreamedSpanContainer } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; +import { RUNTIME } from '../../../utils'; import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner'; +// On Bun, `postgres` resolves to its ESM build through the `bun` export condition, so +// `require('postgres')` returns the module namespace instead of the `postgres` function. + /** * Streamed span attributes are `{ value, type }` objects, unlike transaction span `data`, * which stores values directly. @@ -168,30 +172,34 @@ describeWithDockerCompose('postgresjs auto instrumentation (streamed)', { workin }, }; - createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createTestRunner, test) => { - test('should auto-instrument `postgres` package', { timeout: 90_000 }, async () => { - await createTestRunner() - .expect({ - span: container => { - expect(container).toMatchObject(EXPECTED_SPANS); + createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createTestRunner, test, mode) => { + test.skipIf(RUNTIME === 'bun' && mode === 'cjs')( + 'should auto-instrument `postgres` package', + { timeout: 90_000 }, + async () => { + await createTestRunner() + .expect({ + span: container => { + expect(container).toMatchObject(EXPECTED_SPANS); - // The assertions above only cover the queries the scenario issues itself. postgres.js - // also runs internal ones (e.g. the `pg_catalog` type lookup), so guard the invariant - // across every query span: the name is the summary, never the statement. - const dbSpans = getDbSpans(container); - expect(dbSpans.length).toBeGreaterThan(0); - for (const span of dbSpans) { - expect(span.name).toBe(span.attributes['db.query.summary']?.value); - } - }, - }) - .expect({ event: EXPECTED_ERROR_EVENT }) - // The error event is captured via an unhandled rejection processed on a later tick than - // the spans, so the two envelopes can reach the transport in either order. - .unordered() - .start() - .completed(); - }); + // The assertions above only cover the queries the scenario issues itself. postgres.js + // also runs internal ones (e.g. the `pg_catalog` type lookup), so guard the invariant + // across every query span: the name is the summary, never the statement. + const dbSpans = getDbSpans(container); + expect(dbSpans.length).toBeGreaterThan(0); + for (const span of dbSpans) { + expect(span.name).toBe(span.attributes['db.query.summary']?.value); + } + }, + }) + .expect({ event: EXPECTED_ERROR_EVENT }) + // The error event is captured via an unhandled rejection processed on a later tick than + // the spans, so the two envelopes can reach the transport in either order. + .unordered() + .start() + .completed(); + }, + ); }); }); @@ -228,10 +236,14 @@ describeWithDockerCompose('postgresjs auto instrumentation (streamed)', { workin __dirname, 'scenario-requestHook.mjs', 'instrument-requestHook.mjs', - (createTestRunner, test) => { - test('should call requestHook when provided', { timeout: 90_000 }, async () => { - await createTestRunner().expect({ span: EXPECTED_SPANS }).start().completed(); - }); + (createTestRunner, test, mode) => { + test.skipIf(RUNTIME === 'bun' && mode === 'cjs')( + 'should call requestHook when provided', + { timeout: 90_000 }, + async () => { + await createTestRunner().expect({ span: EXPECTED_SPANS }).start().completed(); + }, + ); }, ); }); @@ -262,10 +274,14 @@ describeWithDockerCompose('postgresjs auto instrumentation (streamed)', { workin ]), }; - createEsmAndCjsTests(__dirname, 'scenario-url.mjs', 'instrument.mjs', (createTestRunner, test) => { - test('should instrument postgres package with URL initialization', { timeout: 90_000 }, async () => { - await createTestRunner().ignore('event').expect({ span: EXPECTED_SPANS }).start().completed(); - }); + createEsmAndCjsTests(__dirname, 'scenario-url.mjs', 'instrument.mjs', (createTestRunner, test, mode) => { + test.skipIf(RUNTIME === 'bun' && mode === 'cjs')( + 'should instrument postgres package with URL initialization', + { timeout: 90_000 }, + async () => { + await createTestRunner().ignore('event').expect({ span: EXPECTED_SPANS }).start().completed(); + }, + ); }); }); @@ -296,10 +312,14 @@ describeWithDockerCompose('postgresjs auto instrumentation (streamed)', { workin ]), }; - createEsmAndCjsTests(__dirname, 'scenario-unsafe.mjs', 'instrument.mjs', (createTestRunner, test) => { - test('should instrument sql.unsafe() queries', { timeout: 90_000 }, async () => { - await createTestRunner().ignore('event').expect({ span: EXPECTED_SPANS }).start().completed(); - }); + createEsmAndCjsTests(__dirname, 'scenario-unsafe.mjs', 'instrument.mjs', (createTestRunner, test, mode) => { + test.skipIf(RUNTIME === 'bun' && mode === 'cjs')( + 'should instrument sql.unsafe() queries', + { timeout: 90_000 }, + async () => { + await createTestRunner().ignore('event').expect({ span: EXPECTED_SPANS }).start().completed(); + }, + ); }); }); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/postgresjs/test.ts b/dev-packages/node-integration-tests/suites/tracing/postgresjs/test.ts index d893e5200004..a0fffe40d453 100644 --- a/dev-packages/node-integration-tests/suites/tracing/postgresjs/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/postgresjs/test.ts @@ -1,6 +1,10 @@ import { afterAll, describe, expect } from 'vitest'; +import { RUNTIME } from '../../../utils'; import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner'; +// On Bun, `postgres` resolves to its ESM build through the `bun` export condition, so +// `require('postgres')` returns the module namespace instead of the `postgres` function. + describeWithDockerCompose('postgresjs auto instrumentation', { workingDirectory: [__dirname] }, () => { afterAll(() => { cleanupChildProcesses(); @@ -223,17 +227,21 @@ describeWithDockerCompose('postgresjs auto instrumentation', { workingDirectory: }, }; - createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createTestRunner, test) => { - test('should auto-instrument `postgres` package', { timeout: 60_000 }, async () => { - await createTestRunner() - .expect({ transaction: EXPECTED_TRANSACTION }) - .expect({ event: EXPECTED_ERROR_EVENT }) - // The error event is captured via an unhandled rejection processed on a later tick than - // the transaction, so the two envelopes can reach the transport in either order. - .unordered() - .start() - .completed(); - }); + createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createTestRunner, test, mode) => { + test.skipIf(RUNTIME === 'bun' && mode === 'cjs')( + 'should auto-instrument `postgres` package', + { timeout: 60_000 }, + async () => { + await createTestRunner() + .expect({ transaction: EXPECTED_TRANSACTION }) + .expect({ event: EXPECTED_ERROR_EVENT }) + // The error event is captured via an unhandled rejection processed on a later tick than + // the transaction, so the two envelopes can reach the transport in either order. + .unordered() + .start() + .completed(); + }, + ); }); }); @@ -326,10 +334,14 @@ describeWithDockerCompose('postgresjs auto instrumentation', { workingDirectory: __dirname, 'scenario-requestHook.mjs', 'instrument-requestHook.mjs', - (createTestRunner, test) => { - test('should call requestHook when provided', { timeout: 60_000 }, async () => { - await createTestRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed(); - }); + (createTestRunner, test, mode) => { + test.skipIf(RUNTIME === 'bun' && mode === 'cjs')( + 'should call requestHook when provided', + { timeout: 60_000 }, + async () => { + await createTestRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed(); + }, + ); }, ); }); @@ -407,10 +419,14 @@ describeWithDockerCompose('postgresjs auto instrumentation', { workingDirectory: ]), }; - createEsmAndCjsTests(__dirname, 'scenario-url.mjs', 'instrument.mjs', (createTestRunner, test) => { - test('should instrument postgres package with URL initialization', { timeout: 90_000 }, async () => { - await createTestRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed(); - }); + createEsmAndCjsTests(__dirname, 'scenario-url.mjs', 'instrument.mjs', (createTestRunner, test, mode) => { + test.skipIf(RUNTIME === 'bun' && mode === 'cjs')( + 'should instrument postgres package with URL initialization', + { timeout: 90_000 }, + async () => { + await createTestRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed(); + }, + ); }); }); @@ -486,12 +502,16 @@ describeWithDockerCompose('postgresjs auto instrumentation', { workingDirectory: ]), }; - createEsmAndCjsTests(__dirname, 'scenario-unsafe.mjs', 'instrument.mjs', (createTestRunner, test) => { - test('should instrument sql.unsafe() queries', { timeout: 90_000 }, async () => { - // The last query fails on purpose, and its unhandled rejection also sends an error event, which can - // arrive before the transaction. - await createTestRunner().ignore('event').expect({ transaction: EXPECTED_TRANSACTION }).start().completed(); - }); + createEsmAndCjsTests(__dirname, 'scenario-unsafe.mjs', 'instrument.mjs', (createTestRunner, test, mode) => { + test.skipIf(RUNTIME === 'bun' && mode === 'cjs')( + 'should instrument sql.unsafe() queries', + { timeout: 90_000 }, + async () => { + // The last query fails on purpose, and its unhandled rejection also sends an error event, which can + // arrive before the transaction. + await createTestRunner().ignore('event').expect({ transaction: EXPECTED_TRANSACTION }).start().completed(); + }, + ); }); }); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/redis-cache/test.ts b/dev-packages/node-integration-tests/suites/tracing/redis-cache/test.ts index a6c77cd01225..2e8de3443a55 100644 --- a/dev-packages/node-integration-tests/suites/tracing/redis-cache/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/redis-cache/test.ts @@ -1,6 +1,7 @@ import { SENTRY_TRACE_LIFECYCLE } from '@sentry/conventions/attributes'; import type { SerializedStreamedSpanContainer } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; +import { EXPECTED_SDK_NAME } from '../../../utils'; import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner'; describeWithDockerCompose('redis cache auto instrumentation', { workingDirectory: [__dirname] }, () => { @@ -705,7 +706,7 @@ describeWithDockerCompose('redis cache auto instrumentation', { workingDirectory 'sentry.kind': 'client', 'sentry.origin': redisOrigin, 'sentry.release': '1.0', - 'sentry.sdk.name': 'sentry.javascript.node', + 'sentry.sdk.name': EXPECTED_SDK_NAME, 'sentry.segment.name': segmentName, [SENTRY_TRACE_LIFECYCLE]: 'stream', }), diff --git a/dev-packages/node-integration-tests/suites/tracing/redis-dc/test.ts b/dev-packages/node-integration-tests/suites/tracing/redis-dc/test.ts index 77cf91663ed0..06d8cac5a465 100644 --- a/dev-packages/node-integration-tests/suites/tracing/redis-dc/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/redis-dc/test.ts @@ -1,6 +1,7 @@ import { SENTRY_TRACE_LIFECYCLE } from '@sentry/conventions/attributes'; import type { SerializedStreamedSpanContainer } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; +import { EXPECTED_SDK_NAME } from '../../../utils'; import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner'; describeWithDockerCompose( @@ -183,7 +184,7 @@ describeWithDockerCompose( 'sentry.op': op, 'sentry.origin': ORIGIN, 'sentry.release': '1.0', - 'sentry.sdk.name': 'sentry.javascript.node', + 'sentry.sdk.name': EXPECTED_SDK_NAME, 'sentry.segment.name': SEGMENT_NAME, 'server.address': HOST, 'server.port': PORT, diff --git a/dev-packages/node-integration-tests/suites/tracing/redis/test.ts b/dev-packages/node-integration-tests/suites/tracing/redis/test.ts index 8d5b9339ec33..50cd473957b8 100644 --- a/dev-packages/node-integration-tests/suites/tracing/redis/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/redis/test.ts @@ -1,6 +1,7 @@ import { SENTRY_TRACE_LIFECYCLE } from '@sentry/conventions/attributes'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, type SerializedStreamedSpanContainer } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; +import { EXPECTED_SDK_NAME } from '../../../utils'; import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner'; describeWithDockerCompose('redis auto instrumentation', { workingDirectory: [__dirname] }, () => { @@ -85,7 +86,7 @@ describeWithDockerCompose('redis auto instrumentation', { workingDirectory: [__d 'sentry.op': { type: 'string', value: redisSpanOp }, 'sentry.origin': { type: 'string', value: origin }, 'sentry.release': { type: 'string', value: '1.0' }, - 'sentry.sdk.name': { type: 'string', value: 'sentry.javascript.node' }, + 'sentry.sdk.name': { type: 'string', value: EXPECTED_SDK_NAME }, 'sentry.sdk.version': { type: 'string', value: expect.any(String) }, 'sentry.segment.id': { type: 'string', value: expect.stringMatching(/^[\da-f]{16}$/) }, 'sentry.segment.name': { type: 'string', value: 'Test Span' }, diff --git a/dev-packages/node-integration-tests/utils/runner/createRunner.ts b/dev-packages/node-integration-tests/utils/runner/createRunner.ts index e737cfa4be8e..ed94c1e1bd5a 100644 --- a/dev-packages/node-integration-tests/utils/runner/createRunner.ts +++ b/dev-packages/node-integration-tests/utils/runner/createRunner.ts @@ -14,7 +14,7 @@ import type { } from '@sentry/core'; import { normalize } from '@sentry/core'; import { createBasicSentryServer } from '@sentry-internal/test-utils'; -import { spawn } from 'child_process'; +import { spawn, spawnSync } from 'child_process'; import { existsSync } from 'fs'; import { tmpdir } from 'os'; import { join } from 'path'; @@ -501,8 +501,9 @@ export function createRunner(...paths: string[]) { !ensureNoErrorOutput && (expectedEnvelopes.length > 0 || (expectedEnvelopeHeaders?.length ?? 0) > 0); const runtime = getRuntime(); const childFlags = wantsAutoFlush ? [...buildAutoFlushFlags(flags, testPath, runtime), ...flags] : flags; + const entryPath = buildScenario(runtime, testPath); - child = spawn(runtime, buildRuntimeArgs(runtime, childFlags, testPath), { env, cwd: PACKAGE_ROOT }); + child = spawn(runtime, buildRuntimeArgs(runtime, childFlags, entryPath), { env, cwd: PACKAGE_ROOT }); spawnedAt = Date.now(); child.on('error', e => { @@ -794,6 +795,27 @@ function buildAutoFlushFlags(existingFlags: readonly string[], testPath: string, * `DENO_IMPORT_MAP`. Any other Node flag throws, so a suite that needs one fails with a clear * message instead of running with different behavior. */ +/** + * Returns the file the runtime runs for `testPath`. When `RUNTIME_BUILD_SCRIPT` is set, that + * script is run by the same runtime with `testPath`, builds the scenario, and prints + * `BUILD_OK `. The Bun package uses it to bundle scenarios with `@sentry/bun/plugin`. + * Scenarios are built right before they start, because `createEsmAndCjsTests` writes the CJS + * variant of a scenario only while the tests run. + */ +function buildScenario(runtime: Runtime, testPath: string): string { + const buildScript = process.env.RUNTIME_BUILD_SCRIPT; + if (!buildScript) { + return testPath; + } + + const result = spawnSync(runtime, [buildScript, testPath], { cwd: PACKAGE_ROOT, encoding: 'utf8' }); + const outputPath = result.stdout?.match(/^BUILD_OK (.+)$/m)?.[1]; + if (!outputPath) { + throw new Error(`Building ${testPath} with ${buildScript} failed:\n${result.stderr}${result.stdout}`); + } + return outputPath; +} + function buildRuntimeArgs(runtime: Runtime, flags: readonly string[], testPath: string): string[] { if (runtime === 'node') { return [...flags, testPath]; From 5ff56ba0c8d8e21218660adc9337b789a71674c7 Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Wed, 23 Sep 2026 18:39:52 +0200 Subject: [PATCH 2/4] fixup! test(bun): Run the auto-instrumentation suites with bundled scenarios Co-Authored-By: Claude Opus 5.5 --- dev-packages/bun-integration-tests/vite.config.mts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dev-packages/bun-integration-tests/vite.config.mts b/dev-packages/bun-integration-tests/vite.config.mts index 970dc9befe5b..90c1bd7f867e 100644 --- a/dev-packages/bun-integration-tests/vite.config.mts +++ b/dev-packages/bun-integration-tests/vite.config.mts @@ -82,7 +82,8 @@ export default defineConfig({ test: { ...nodeSuitesTest, // The auto-instrumentation suites, with each scenario bundled by `@sentry/bun/plugin` before - // it starts, as Bun apps must be built to get these spans (JS-3508). + // it starts, as Bun apps must be built to get these spans. + // See https://github.com/getsentry/sentry-javascript/issues/23882 name: 'node-suites-bun-build', include: NO_AUTO_INSTRUMENTATION.map(glob => (glob.endsWith('/**') ? `${glob}/test.ts` : glob)), exclude: BUN_BUILD_EXCLUDE, From f731e72f89bfac6617406f04b083b78480bb4dad Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Wed, 23 Sep 2026 18:52:00 +0200 Subject: [PATCH 3/4] fixup! test(bun): Run the auto-instrumentation suites with bundled scenarios Co-Authored-By: Claude Opus 5.5 --- .github/workflows/build.yml | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4e69eaf7fde6..729e22999414 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -901,19 +901,6 @@ jobs: uses: actions/checkout@v7 with: ref: ${{ env.HEAD_COMMIT }} - - name: Free up disk space if low - # The bundled Node suites pull several DB docker images (mssql alone is ~1.5GB) - # Available disk space is not consistent, if we detect low space this cleans up some unused toolchains - run: | - df -h / - avail_kb=$(df -k --output=avail / | tail -1) - if [ "$avail_kb" -lt $((40 * 1024 * 1024)) ]; then - echo "Low disk space (<40GB free), reclaiming unused toolchains" - sudo rm -rf /usr/local/lib/android /usr/share/dotnet /opt/ghc /usr/local/.ghcup /opt/hostedtoolcache/CodeQL - df -h / - else - echo "Sufficient disk space, skipping cleanup" - fi - name: Set up Node uses: actions/setup-node@v7 with: From 1a809bcc588223918e9fff54834ddcfc1eebe9fc Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Wed, 23 Sep 2026 19:26:18 +0200 Subject: [PATCH 4/4] fixup! test(bun): Run the auto-instrumentation suites with bundled scenarios Co-Authored-By: Claude Opus 5.5 --- .../bun-integration-tests/node-suites/excludes.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/dev-packages/bun-integration-tests/node-suites/excludes.ts b/dev-packages/bun-integration-tests/node-suites/excludes.ts index 2e2b4f561447..f2842da25318 100644 --- a/dev-packages/bun-integration-tests/node-suites/excludes.ts +++ b/dev-packages/bun-integration-tests/node-suites/excludes.ts @@ -158,6 +158,11 @@ export const NODE_SUITES_EXCLUDE = [ // they wait until their module loads. const BUN_BUILD_EAGER_SUBSCRIPTION = ['suites/tracing/orchestrion-lazy-registration/test.ts']; +// The first `init()` has no DSN, so `bunHttpServerIntegration` is not set up, and the suite then +// adds only `httpIntegration`. On Bun that does not isolate requests, because Bun does not publish +// `http.server.request.start`. +const BUN_BUILD_NO_HTTP_SERVER_INTEGRATION = ['suites/express/multiple-init/test.ts']; + // Some or all tests fail with the bundled scenarios, cause not investigated yet. In // `express/tracing` only the request data tests fail: they set `httpIntegration` options, and with // `@sentry/bun` the request body comes from `bunHttpServerIntegration`. @@ -190,4 +195,9 @@ const BUN_BUILD_NOT_TRIAGED = [ 'suites/tracing/vercelai/v6_v7/test.ts', ]; -export const BUN_BUILD_EXCLUDE = ['**/node_modules/**', ...BUN_BUILD_EAGER_SUBSCRIPTION, ...BUN_BUILD_NOT_TRIAGED]; +export const BUN_BUILD_EXCLUDE = [ + '**/node_modules/**', + ...BUN_BUILD_EAGER_SUBSCRIPTION, + ...BUN_BUILD_NO_HTTP_SERVER_INTEGRATION, + ...BUN_BUILD_NOT_TRIAGED, +];