Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -895,7 +895,7 @@ 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
Expand Down
38 changes: 38 additions & 0 deletions dev-packages/bun-integration-tests/node-suites/bun-build.ts
Original file line number Diff line number Diff line change
@@ -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}`);
53 changes: 52 additions & 1 deletion dev-packages/bun-integration-tests/node-suites/excludes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -150,3 +150,54 @@ 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'];

// 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`.
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_NO_HTTP_SERVER_INTEGRATION,
...BUN_BUILD_NOT_TRIAGED,
];
20 changes: 19 additions & 1 deletion dev-packages/bun-integration-tests/vite.config.mts
Original file line number Diff line number Diff line change
@@ -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));

Expand Down Expand Up @@ -77,6 +77,24 @@ 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.
// 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,
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',
},
},
},
],
},
});
Original file line number Diff line number Diff line change
@@ -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(
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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();
},
);
});
});

Expand Down Expand Up @@ -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();
},
);
},
);
});
Expand Down Expand Up @@ -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();
},
);
});
});

Expand Down Expand Up @@ -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();
},
);
});
});
});
Loading
Loading