From bc05e63590a8c5f97ccec92e82a7dcfaddb64f08 Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Wed, 23 Sep 2026 17:47:10 +0200 Subject: [PATCH 1/3] test(bun): Run the Bun-only suites with the shared Node runner The Bun package had its own copy of the scenario runner and envelope assertions. The Bun-only suites now use the runner of `node-integration-tests`, so one runner serves all scenarios. The `captureMessage` test in `basic` is removed, because the shared `public-api` suites cover it with `@sentry/bun`. Co-Authored-By: Claude Opus 5.5 --- dev-packages/bun-integration-tests/expect.ts | 97 ------ dev-packages/bun-integration-tests/runner.ts | 282 ------------------ .../suites/basic/index.ts | 8 +- .../suites/basic/test.ts | 103 ++++--- .../suites/cjs/index.cjs | 3 +- .../bun-integration-tests/suites/cjs/test.ts | 59 ++-- .../suites/fetch/index.ts | 3 +- .../suites/fetch/test.ts | 121 ++++---- .../suites/hono-sdk/index.ts | 3 +- .../suites/hono-sdk/test.ts | 209 +++++++------ .../suites/is-localhost/index.ts | 3 +- .../suites/is-localhost/test.ts | 35 +-- .../suites/request-bodies/index.ts | 3 +- .../suites/request-bodies/test.ts | 69 +++-- .../bun-integration-tests/vite.config.mts | 1 + 15 files changed, 336 insertions(+), 663 deletions(-) delete mode 100644 dev-packages/bun-integration-tests/expect.ts delete mode 100644 dev-packages/bun-integration-tests/runner.ts diff --git a/dev-packages/bun-integration-tests/expect.ts b/dev-packages/bun-integration-tests/expect.ts deleted file mode 100644 index 6f1add5ffeae..000000000000 --- a/dev-packages/bun-integration-tests/expect.ts +++ /dev/null @@ -1,97 +0,0 @@ -import type { Contexts, Envelope, Event, SdkInfo } from '@sentry/core'; -import { SDK_VERSION } from '@sentry/core'; -import { expect } from 'vitest'; - -export const UUID_MATCHER = expect.stringMatching(/^[\da-f]{32}$/); -export const UUID_V4_MATCHER = expect.stringMatching( - /^[\da-f]{8}-[\da-f]{4}-4[\da-f]{3}-[89ab][\da-f]{3}-[\da-f]{12}$/, -); -export const SHORT_UUID_MATCHER = expect.stringMatching(/^[\da-f]{16}$/); -export const ISO_DATE_MATCHER = expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/); - -function dropUndefinedKeys>(obj: T): T { - for (const [key, value] of Object.entries(obj)) { - if (value === undefined) { - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete obj[key]; - } - } - return obj; -} - -function getSdk(sdk: 'bun' | 'hono'): SdkInfo { - return { - integrations: expect.any(Array), - name: `sentry.javascript.${sdk}`, - packages: [ - { - name: `npm:@sentry/${sdk}`, - version: SDK_VERSION, - }, - ...(sdk === 'hono' ? [{ name: 'npm:@sentry/bun', version: SDK_VERSION }] : []), - ], - version: SDK_VERSION, - }; -} - -function defaultContexts(eventContexts: Contexts = {}): Contexts { - return dropUndefinedKeys({ - app: { app_memory: expect.any(Number), app_start_time: expect.any(String), free_memory: expect.any(Number) }, - cloud_resource: expect.any(Object), - trace: { - trace_id: UUID_MATCHER, - span_id: SHORT_UUID_MATCHER, - }, - culture: { locale: expect.any(String), timezone: expect.any(String) }, - device: expect.any(Object), - os: expect.any(Object), - runtime: { name: 'bun', version: expect.any(String) }, - ...eventContexts, - }); -} - -export function expectedEvent(event: Event, { sdk }: { sdk: 'bun' | 'hono' }): Event { - return dropUndefinedKeys({ - event_id: UUID_MATCHER, - timestamp: expect.any(Number), - environment: 'production', - platform: 'node', - modules: expect.any(Object), - sdk: getSdk(sdk), - server_name: expect.any(String), - // release is auto-detected from GitHub CI env vars, so only expect it if we know it will be there - ...(process.env.GITHUB_SHA ? { release: expect.any(String) } : {}), - ...event, - contexts: defaultContexts(event.contexts), - }); -} - -export function eventEnvelope( - event: Event, - { - includeSampleRand = false, - includeTransaction = true, - sdk = 'bun', - }: { includeSampleRand?: boolean; includeTransaction?: boolean; sdk?: 'bun' | 'hono' } = {}, -): Envelope { - return [ - { - event_id: UUID_MATCHER, - sent_at: ISO_DATE_MATCHER, - sdk: { name: `sentry.javascript.${sdk}`, version: SDK_VERSION }, - trace: { - environment: event.environment || 'production', - public_key: 'public', - trace_id: UUID_MATCHER, - - sample_rate: expect.any(String), - sampled: expect.any(String), - // release is auto-detected from GitHub CI env vars, so only expect it if we know it will be there - ...(process.env.GITHUB_SHA ? { release: expect.any(String) } : {}), - ...(includeSampleRand && { sample_rand: expect.stringMatching(/^[01](\.\d+)?$/) }), - ...(includeTransaction && { transaction: expect.any(String) }), - }, - }, - [[{ type: 'event' }, expectedEvent(event, { sdk })]], - ]; -} diff --git a/dev-packages/bun-integration-tests/runner.ts b/dev-packages/bun-integration-tests/runner.ts deleted file mode 100644 index 51f9e4e7fa60..000000000000 --- a/dev-packages/bun-integration-tests/runner.ts +++ /dev/null @@ -1,282 +0,0 @@ -import type { Envelope, EnvelopeItemType } from '@sentry/core'; -import { normalize } from '@sentry/core'; -import { createBasicSentryServer } from '@sentry-internal/test-utils'; -import { spawn } from 'child_process'; -import { existsSync, statSync } from 'fs'; -import { join } from 'path'; -import { inspect } from 'util'; -import { expect } from 'vitest'; - -const CLEANUP_STEPS = new Set<() => void>(); - -export function cleanupChildProcesses(): void { - for (const step of CLEANUP_STEPS) { - step(); - } - CLEANUP_STEPS.clear(); -} - -process.on('exit', cleanupChildProcesses); - -function deferredPromise( - done?: () => void, -): { resolve: (val: T) => void; reject: (reason?: unknown) => void; promise: Promise } { - let resolve; - let reject; - const promise = new Promise((res, rej) => { - resolve = (val: T) => { - res(val); - }; - reject = (reason: Error) => { - rej(reason); - }; - }); - if (!resolve || !reject) { - throw new Error('Failed to create deferred promise'); - } - return { - resolve, - reject, - promise: promise.finally(() => done?.()), - }; -} - -type Expected = Envelope | ((envelope: Envelope) => void); - -type StartResult = { - completed(): Promise; - makeRequest( - method: 'get' | 'post', - path: string, - options?: { headers?: Record; data?: BodyInit; expectError?: boolean }, - ): Promise; -}; - -/** Creates a test runner that spawns a Bun child process */ -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export function createRunner(...paths: string[]) { - const testPath = join(...paths); - - let unordered = false; - - if (!existsSync(testPath)) { - throw new Error(`Test scenario not found: ${testPath}`); - } - - const entryFile = statSync(testPath).isDirectory() ? join(testPath, 'index.ts') : testPath; - - const expectedEnvelopes: Expected[] = []; - const ignored: Set = new Set(['session', 'sessions', 'client_report']); - const envVars: Record = {}; - - return { - withEnv: function (env: Record) { - Object.assign(envVars, env); - return this; - }, - expect: function (expected: Expected) { - expectedEnvelopes.push(expected); - return this; - }, - expectN: function (n: number, expected: Expected) { - for (let i = 0; i < n; i++) { - expectedEnvelopes.push(expected); - } - return this; - }, - unordered: function () { - unordered = true; - return this; - }, - ignore: function (...types: EnvelopeItemType[]) { - types.forEach(t => ignored.add(t)); - return this; - }, - unignore: function (...types: EnvelopeItemType[]) { - for (const t of types) { - ignored.delete(t); - } - return this; - }, - start: function (signal?: AbortSignal): StartResult { - const { resolve, reject, promise: isComplete } = deferredPromise(cleanupChildProcesses); - const expectedEnvelopeCount = expectedEnvelopes.length; - - let envelopeCount = 0; - const { resolve: setServerPort, promise: serverPortPromise } = deferredPromise(); - let child: ReturnType | undefined; - - function expectCallbackCalled(): void { - envelopeCount++; - if (envelopeCount === expectedEnvelopeCount) { - resolve(); - } - } - - function assertEnvelopeMatches(expected: Expected, envelope: Envelope): void { - if (typeof expected === 'function') { - expected(envelope); - } else { - expect(envelope).toEqual(expected); - } - } - - function newEnvelope(envelope: Envelope): void { - if (process.env.DEBUG) log('newEnvelope', inspect(envelope, false, null, true)); - - const envelopeItemType = envelope[1][0][0].type; - - if (ignored.has(envelopeItemType)) { - return; - } - - try { - if (unordered) { - const matchIndex = expectedEnvelopes.findIndex(candidate => { - try { - assertEnvelopeMatches(candidate, envelope); - return true; - } catch { - return false; - } - }); - - if (matchIndex < 0) { - return; - } - - expectedEnvelopes.splice(matchIndex, 1); - } else { - const expected = expectedEnvelopes.shift(); - - if (!expected) { - return; - } - - assertEnvelopeMatches(expected, envelope); - } - - expectCallbackCalled(); - } catch (e) { - reject(e); - } - } - - createBasicSentryServer(newEnvelope) - .then(([mockServerPort, mockServerClose]) => { - if (mockServerClose) { - CLEANUP_STEPS.add(() => { - mockServerClose(); - }); - } - - if (process.env.DEBUG) log('Starting scenario', testPath); - - if (!existsSync(entryFile)) { - reject(new Error(`Entry file not found: ${entryFile}`)); - return; - } - - const stdio: ('inherit' | 'ipc' | 'ignore' | 'pipe')[] = process.env.DEBUG - ? ['inherit', 'inherit', 'inherit', 'ipc'] - : ['ignore', 'pipe', 'pipe', 'ipc']; - - child = spawn('bun', ['run', entryFile], { - stdio, - signal, - env: { - ...process.env, - SENTRY_DSN: `http://public@localhost:${mockServerPort}/1337`, - ...envVars, - }, - }); - - CLEANUP_STEPS.add(() => { - child?.kill(); - }); - - child.on('error', e => { - // eslint-disable-next-line no-console - console.error('Error starting Bun child process:', e); - reject(e); - }); - - if (!process.env.DEBUG && child.stderr) { - let stderrData = ''; - child.stderr.on('data', (chunk: Buffer) => { - stderrData += chunk.toString(); - }); - child.on('exit', code => { - if (code !== 0 && code !== null && stderrData) { - // eslint-disable-next-line no-console - console.error('Bun process stderr:', stderrData); - } - }); - } - - child.on('message', (message: string) => { - const msg = JSON.parse(message) as { event: string; port?: number }; - if (msg.event === 'READY' && typeof msg.port === 'number') { - if (process.env.DEBUG) log('Bun server ready on port', msg.port); - setServerPort(msg.port); - } - }); - }) - .catch(e => reject(e)); - - return { - completed: async function (): Promise { - return isComplete; - }, - makeRequest: async function ( - method: 'get' | 'post', - path: string, - options: { headers?: Record; data?: BodyInit; expectError?: boolean } = {}, - ): Promise { - const url = `http://localhost:${await serverPortPromise}${path}`; - const body = options.data; - const headers = options.headers || {}; - const expectError = options.expectError || false; - - if (process.env.DEBUG) log('making request', method, url, headers, body); - - try { - const init: RequestInit = { headers, method, body }; - const res = await fetch(url, init); - - if (!res.ok) { - if (!expectError) { - reject(new Error(`Expected request to "${path}" to succeed, but got a ${res.status} response`)); - } - - return; - } - - if (expectError) { - reject(new Error(`Expected request to "${path}" to fail, but got a ${res.status} response`)); - return; - } - - if (res.headers.get('content-type')?.includes('application/json')) { - return await res.json(); - } - - return (await res.text()) as T; - } catch (e) { - if (expectError) { - return; - } - - reject(e); - return; - } - }, - }; - }, - }; -} - -function log(...args: unknown[]): void { - // eslint-disable-next-line no-console - console.log(...args.map(arg => normalize(arg))); -} diff --git a/dev-packages/bun-integration-tests/suites/basic/index.ts b/dev-packages/bun-integration-tests/suites/basic/index.ts index c760e61d6ba5..4d0e7a6cb9b3 100644 --- a/dev-packages/bun-integration-tests/suites/basic/index.ts +++ b/dev-packages/bun-integration-tests/suites/basic/index.ts @@ -1,3 +1,4 @@ +import { sendPortToRunner } from '@sentry-internal/node-integration-tests'; import * as Sentry from '@sentry/bun'; Sentry.init({ @@ -14,11 +15,6 @@ const server = Bun.serve({ throw new Error('This is a test error from the Bun integration tests'); } - if (url.pathname === '/message') { - Sentry.captureMessage('Hello from Bun'); - return new Response('OK'); - } - return new Response('Hello from Bun!'); }, error(_err) { @@ -26,4 +22,4 @@ const server = Bun.serve({ }, }); -process.send?.(JSON.stringify({ event: 'READY', port: server.port })); +sendPortToRunner(server.port!); diff --git a/dev-packages/bun-integration-tests/suites/basic/test.ts b/dev-packages/bun-integration-tests/suites/basic/test.ts index 9a9da240e373..68a30794ce70 100644 --- a/dev-packages/bun-integration-tests/suites/basic/test.ts +++ b/dev-packages/bun-integration-tests/suites/basic/test.ts @@ -1,54 +1,69 @@ -import { expect, it } from 'vitest'; -import { eventEnvelope } from '../../expect'; -import { createRunner } from '../../runner'; +import { SDK_VERSION } from '@sentry/core'; +import { afterAll, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../node-integration-tests/utils/runner'; -it('captures an error thrown in Bun.serve fetch handler', async ({ signal }) => { - const runner = createRunner(__dirname) - .expect( - eventEnvelope( - { - level: 'error', - exception: { - values: [ - { - type: 'Error', - value: 'This is a test error from the Bun integration tests', - stacktrace: { - frames: expect.any(Array), - }, - mechanism: { type: 'auto.http.bun.serve', handled: false }, +afterAll(() => { + cleanupChildProcesses(); +}); + +test('captures an error thrown in Bun.serve fetch handler', async () => { + const runner = createRunner(__dirname, 'index.ts') + .withMockSentryServer() + .ignore('span') + .expect({ + event: { + level: 'error', + platform: 'node', + exception: { + values: [ + { + type: 'Error', + value: 'This is a test error from the Bun integration tests', + stacktrace: { + frames: expect.any(Array), }, - ], - }, - request: expect.objectContaining({ - method: 'GET', - url: expect.stringContaining('/error'), - }), + mechanism: { type: 'auto.http.bun.serve', handled: false }, + }, + ], }, - { includeSampleRand: true, includeTransaction: false }, - ), - ) - .ignore('span') - .start(signal); + request: expect.objectContaining({ + method: 'GET', + url: expect.stringContaining('/error'), + }), + sdk: expect.objectContaining({ + name: 'sentry.javascript.bun', + packages: [{ name: 'npm:@sentry/bun', version: SDK_VERSION }], + }), + contexts: expect.objectContaining({ + runtime: { name: 'bun', version: expect.any(String) }, + }), + }, + }) + .start(); + await runner.makeRequest('get', '/error', { expectError: true }); await runner.completed(); }); -it('captures a manually sent message', async ({ signal }) => { - const runner = createRunner(__dirname) - .expect(envelope => { - const [, envelopeItems] = envelope; - const [itemHeader, itemPayload] = envelopeItems[0]; - - expect(itemHeader.type).toBe('event'); - - expect(itemPayload).toMatchObject({ - level: 'info', - message: 'Hello from Bun', - }); - }) +test('sends the error with a sampled trace envelope header', async () => { + const runner = createRunner(__dirname, 'index.ts') + .withMockSentryServer() .ignore('span') - .start(signal); - await runner.makeRequest('get', '/message'); + .expectHeader({ + event: { + sdk: { name: 'sentry.javascript.bun', version: SDK_VERSION }, + trace: expect.objectContaining({ + environment: 'production', + public_key: 'public', + trace_id: expect.stringMatching(/^[\da-f]{32}$/), + sample_rate: '1', + sampled: 'true', + sample_rand: expect.stringMatching(/^[01](\.\d+)?$/), + }), + }, + }) + .start(); + + await runner.makeRequest('get', '/error', { expectError: true }); await runner.completed(); }); diff --git a/dev-packages/bun-integration-tests/suites/cjs/index.cjs b/dev-packages/bun-integration-tests/suites/cjs/index.cjs index d21203678e20..6efe24b8b7ad 100644 --- a/dev-packages/bun-integration-tests/suites/cjs/index.cjs +++ b/dev-packages/bun-integration-tests/suites/cjs/index.cjs @@ -1,4 +1,5 @@ const Sentry = require('@sentry/bun'); +const { sendPortToRunner } = require('@sentry-internal/node-integration-tests'); Sentry.init({ dsn: process.env.SENTRY_DSN, @@ -15,4 +16,4 @@ const server = Bun.serve({ }, }); -process.send?.(JSON.stringify({ event: 'READY', port: server.port })); +sendPortToRunner(server.port); diff --git a/dev-packages/bun-integration-tests/suites/cjs/test.ts b/dev-packages/bun-integration-tests/suites/cjs/test.ts index e70f0ddb8971..1523c4fd104c 100644 --- a/dev-packages/bun-integration-tests/suites/cjs/test.ts +++ b/dev-packages/bun-integration-tests/suites/cjs/test.ts @@ -1,35 +1,38 @@ -import { expect, it } from 'vitest'; -import { eventEnvelope } from '../../expect'; -import { createRunner } from '../../runner'; +import { afterAll, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../node-integration-tests/utils/runner'; -it('initializes when @sentry/bun is loaded with require()', async ({ signal }) => { +afterAll(() => { + cleanupChildProcesses(); +}); + +test('initializes when @sentry/bun is loaded with require()', async () => { const runner = createRunner(__dirname, 'index.cjs') - .expect( - eventEnvelope( - { - level: 'error', - exception: { - values: [ - { - type: 'Error', - value: 'This is a test error from a CommonJS Bun app', - stacktrace: { - frames: expect.any(Array), - }, - mechanism: { type: 'auto.http.bun.serve', handled: false }, + .withMockSentryServer() + .ignore('span') + .expect({ + event: { + level: 'error', + exception: { + values: [ + { + type: 'Error', + value: 'This is a test error from a CommonJS Bun app', + stacktrace: { + frames: expect.any(Array), }, - ], - }, - request: expect.objectContaining({ - method: 'GET', - url: expect.stringContaining('/error'), - }), + mechanism: { type: 'auto.http.bun.serve', handled: false }, + }, + ], }, - { includeSampleRand: true, includeTransaction: false }, - ), - ) - .ignore('span') - .start(signal); + request: expect.objectContaining({ + method: 'GET', + url: expect.stringContaining('/error'), + }), + sdk: expect.objectContaining({ name: 'sentry.javascript.bun' }), + }, + }) + .start(); + await runner.makeRequest('get', '/error', { expectError: true }); await runner.completed(); }); diff --git a/dev-packages/bun-integration-tests/suites/fetch/index.ts b/dev-packages/bun-integration-tests/suites/fetch/index.ts index f378ee6c786d..ede289da0818 100644 --- a/dev-packages/bun-integration-tests/suites/fetch/index.ts +++ b/dev-packages/bun-integration-tests/suites/fetch/index.ts @@ -1,3 +1,4 @@ +import { sendPortToRunner } from '@sentry-internal/node-integration-tests'; import * as Sentry from '@sentry/bun'; // The target server the instrumented app makes outgoing fetch requests to. It @@ -48,4 +49,4 @@ const server = Bun.serve({ }, }); -process.send?.(JSON.stringify({ event: 'READY', port: server.port })); +sendPortToRunner(server.port!); diff --git a/dev-packages/bun-integration-tests/suites/fetch/test.ts b/dev-packages/bun-integration-tests/suites/fetch/test.ts index e503f78519a5..53bc4ac9e841 100644 --- a/dev-packages/bun-integration-tests/suites/fetch/test.ts +++ b/dev-packages/bun-integration-tests/suites/fetch/test.ts @@ -1,49 +1,48 @@ -import type { Envelope, Event, SerializedStreamedSpan, SerializedStreamedSpanContainer } from '@sentry/core'; -import { expect, it } from 'vitest'; -import { createRunner } from '../../runner'; +import { afterAll, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../node-integration-tests/utils/runner'; -function getSpans(envelope: Envelope): SerializedStreamedSpan[] { - return (envelope[1][0][1] as SerializedStreamedSpanContainer).items; -} - -it('creates an http.client span for outgoing fetch requests', async ({ signal }) => { - const runner = createRunner(__dirname) - .expect(envelope => { - const spans = getSpans(envelope); - - const segmentSpan = spans.find(span => span.is_segment); - expect(segmentSpan).toMatchObject({ - // `Bun.serve` without `routes` has no parameterized route, so the streamed segment is - // named after the method only; the path lives in `url.path`. - name: 'GET', - attributes: expect.objectContaining({ - 'sentry.op': { value: 'http.server', type: 'string' }, - 'url.path': { value: '/outgoing-fetch', type: 'string' }, - }), - }); - - const httpClientSpan = spans.find(span => span.attributes['sentry.op']?.value === 'http.client'); +afterAll(() => { + cleanupChildProcesses(); +}); - expect(httpClientSpan).toBeDefined(); - expect(httpClientSpan).toMatchObject({ - name: 'GET localhost', - parent_span_id: segmentSpan!.span_id, - attributes: expect.objectContaining({ - 'sentry.op': { value: 'http.client', type: 'string' }, - 'sentry.origin': { value: 'auto.http.fetch', type: 'string' }, - 'http.request.method': { value: 'GET', type: 'string' }, - type: { value: 'fetch', type: 'string' }, - }), - }); +test('creates an http.client span for outgoing fetch requests', async () => { + const runner = createRunner(__dirname, 'index.ts') + .withMockSentryServer() + .expect({ + span: container => { + const segmentSpan = container.items.find(span => span.is_segment); + expect(segmentSpan).toMatchObject({ + // `Bun.serve` without `routes` has no parameterized route, so the streamed segment is + // named after the method only; the path lives in `url.path`. + name: 'GET', + attributes: expect.objectContaining({ + 'sentry.op': { value: 'http.server', type: 'string' }, + 'url.path': { value: '/outgoing-fetch', type: 'string' }, + }), + }); + + const httpClientSpan = container.items.find(span => span.attributes['sentry.op']?.value === 'http.client'); + + expect(httpClientSpan).toMatchObject({ + name: 'GET localhost', + parent_span_id: segmentSpan!.span_id, + attributes: expect.objectContaining({ + 'sentry.op': { value: 'http.client', type: 'string' }, + 'sentry.origin': { value: 'auto.http.fetch', type: 'string' }, + 'http.request.method': { value: 'GET', type: 'string' }, + type: { value: 'fetch', type: 'string' }, + }), + }); + }, }) - .start(signal); + .start(); await runner.makeRequest('get', '/outgoing-fetch'); await runner.completed(); }); -it('propagates sentry-trace and baggage headers to allowed outgoing fetch requests', async ({ signal }) => { - const runner = createRunner(__dirname).start(signal); +test('propagates sentry-trace and baggage headers to allowed outgoing fetch requests', async () => { + const runner = createRunner(__dirname, 'index.ts').withMockSentryServer().start(); const response = await runner.makeRequest<{ headers: Record }>('get', '/outgoing-fetch'); @@ -54,40 +53,40 @@ it('propagates sentry-trace and baggage headers to allowed outgoing fetch reques expect(response?.headers.baggage).toContain(`sentry-trace_id=${traceId}`); }); -it('does not propagate headers to outgoing fetch requests outside tracePropagationTargets', async ({ signal }) => { - const runner = createRunner(__dirname).start(signal); +test('does not propagate headers to outgoing fetch requests outside tracePropagationTargets', async () => { + const runner = createRunner(__dirname, 'index.ts').withMockSentryServer().start(); const response = await runner.makeRequest<{ headers: Record }>('get', '/outgoing-fetch-disallowed'); + expect(response?.headers).toBeDefined(); expect(response?.headers['sentry-trace']).toBeUndefined(); expect(response?.headers.baggage).toBeUndefined(); }); -it('records a breadcrumb for outgoing fetch requests', async ({ signal }) => { +test('records a breadcrumb for outgoing fetch requests', async () => { // Streamed spans carry no breadcrumbs, so the breadcrumb is asserted on an error // captured right after the fetch instead. - const runner = createRunner(__dirname) - .expect(envelope => { - const [, envelopeItems] = envelope; - const [itemHeader, event] = envelopeItems[0] as [{ type: string }, Event]; - - expect(itemHeader.type).toBe('event'); - expect(event.exception?.values?.[0]?.value).toBe('fetch done'); - - expect(event.breadcrumbs).toContainEqual( - expect.objectContaining({ - category: 'fetch', - type: 'http', - data: expect.objectContaining({ - method: 'GET', - status_code: 200, - url: expect.stringMatching(/\/allowed$/), + const runner = createRunner(__dirname, 'index.ts') + .withMockSentryServer() + .ignore('span') + .expect({ + event: event => { + expect(event.exception?.values?.[0]?.value).toBe('fetch done'); + + expect(event.breadcrumbs).toContainEqual( + expect.objectContaining({ + category: 'fetch', + type: 'http', + data: expect.objectContaining({ + method: 'GET', + status_code: 200, + url: expect.stringMatching(/\/allowed$/), + }), }), - }), - ); + ); + }, }) - .ignore('span') - .start(signal); + .start(); await runner.makeRequest('get', '/outgoing-fetch-error'); await runner.completed(); diff --git a/dev-packages/bun-integration-tests/suites/hono-sdk/index.ts b/dev-packages/bun-integration-tests/suites/hono-sdk/index.ts index 075fc896618b..d45505e479cf 100644 --- a/dev-packages/bun-integration-tests/suites/hono-sdk/index.ts +++ b/dev-packages/bun-integration-tests/suites/hono-sdk/index.ts @@ -1,5 +1,6 @@ import { sentry } from '@sentry/hono/bun'; import { Hono } from 'hono'; +import { sendPortToRunner } from '@sentry-internal/node-integration-tests'; const app = new Hono(); @@ -28,4 +29,4 @@ const server = Bun.serve({ fetch: app.fetch, }); -process.send?.(JSON.stringify({ event: 'READY', port: server.port })); +sendPortToRunner(server.port!); diff --git a/dev-packages/bun-integration-tests/suites/hono-sdk/test.ts b/dev-packages/bun-integration-tests/suites/hono-sdk/test.ts index 785cffb85589..7b9dbb0a1d6a 100644 --- a/dev-packages/bun-integration-tests/suites/hono-sdk/test.ts +++ b/dev-packages/bun-integration-tests/suites/hono-sdk/test.ts @@ -1,110 +1,139 @@ -import type { SerializedStreamedSpanContainer } from '@sentry/core'; -import { expect, it } from 'vitest'; -import { eventEnvelope, SHORT_UUID_MATCHER, UUID_MATCHER } from '../../expect'; -import { createRunner } from '../../runner'; +import { SDK_VERSION } from '@sentry/core'; +import { afterAll, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../node-integration-tests/utils/runner'; -it('Hono app captures parametrized errors (Hono SDK on Bun)', async ({ signal }) => { - const runner = createRunner(__dirname) - .expect(envelope => { - const [, envelopeItems] = envelope; - const [itemHeader, itemPayload] = envelopeItems[0]; - - expect(itemHeader.type).toBe('span'); +afterAll(() => { + cleanupChildProcesses(); +}); - const segmentSpan = (itemPayload as SerializedStreamedSpanContainer).items.find(span => span.is_segment); +test('Hono app captures parametrized errors (Hono SDK on Bun)', async () => { + const runner = createRunner(__dirname, 'index.ts') + .withMockSentryServer() + .unordered() + .expect({ + span: container => { + const segmentSpan = container.items.find(span => span.is_segment); - expect(segmentSpan).toMatchObject({ - name: 'GET /error/:param', - is_segment: true, - span_id: expect.any(String), - trace_id: expect.any(String), - status: 'error', - attributes: expect.objectContaining({ - 'sentry.op': { value: 'http.server', type: 'string' }, - 'sentry.origin': { value: 'auto.http.bun.serve', type: 'string' }, - 'sentry.segment.name.source': { value: 'route', type: 'string' }, - 'http.route': { value: '/error/:param', type: 'string' }, - 'http.request.method': { value: 'GET', type: 'string' }, - 'http.response.status_code': { value: 500, type: 'integer' }, - 'url.path': { value: '/error/param-123', type: 'string' }, - }), - }); + expect(segmentSpan).toMatchObject({ + name: 'GET /error/:param', + is_segment: true, + span_id: expect.any(String), + trace_id: expect.any(String), + status: 'error', + attributes: expect.objectContaining({ + 'sentry.op': { value: 'http.server', type: 'string' }, + 'sentry.origin': { value: 'auto.http.bun.serve', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'http.route': { value: '/error/:param', type: 'string' }, + 'http.request.method': { value: 'GET', type: 'string' }, + 'http.response.status_code': { value: 500, type: 'integer' }, + 'url.path': { value: '/error/param-123', type: 'string' }, + }), + }); + }, }) - - .expect( - eventEnvelope( - { - level: 'error', - transaction: 'GET /error/:param', - exception: { - values: [ - { - type: 'Error', - value: 'Test error from Hono app', - stacktrace: { - frames: expect.any(Array), - }, - mechanism: { type: 'auto.http.hono.context_error', handled: false }, - }, - ], - }, - request: { - cookies: {}, - headers: expect.any(Object), - method: 'GET', - url: expect.stringContaining('/error/param-123'), - }, - user: { ip_address: expect.any(String) }, - breadcrumbs: [ + .expect({ + event: { + level: 'error', + transaction: 'GET /error/:param', + exception: { + values: [ { - timestamp: expect.any(Number), - category: 'console', - level: 'error', - message: 'Error: Test error from Hono app', - data: expect.objectContaining({ - logger: 'console', - arguments: [{ message: 'Test error from Hono app', name: 'Error', stack: expect.any(String) }], - }), + type: 'Error', + value: 'Test error from Hono app', + stacktrace: { + frames: expect.any(Array), + }, + mechanism: { type: 'auto.http.hono.context_error', handled: false }, }, ], }, - { sdk: 'hono', includeSampleRand: true, includeTransaction: true }, - ), - ) - .unordered() - .start(signal); + request: { + cookies: {}, + headers: expect.any(Object), + method: 'GET', + url: expect.stringContaining('/error/param-123'), + }, + user: { ip_address: expect.any(String) }, + // The runner reads the server port from a `console.log`, which adds a console breadcrumb too. + breadcrumbs: expect.arrayContaining([ + { + timestamp: expect.any(Number), + category: 'console', + level: 'error', + message: 'Error: Test error from Hono app', + data: expect.objectContaining({ + logger: 'console', + arguments: [{ message: 'Test error from Hono app', name: 'Error', stack: expect.any(String) }], + }), + }, + ]), + sdk: expect.objectContaining({ + name: 'sentry.javascript.hono', + packages: [ + { name: 'npm:@sentry/hono', version: SDK_VERSION }, + { name: 'npm:@sentry/bun', version: SDK_VERSION }, + ], + }), + contexts: expect.objectContaining({ + runtime: { name: 'bun', version: expect.any(String) }, + }), + }, + }) + .start(); await runner.makeRequest('get', '/error/param-123', { expectError: true }); await runner.completed(); }); -it('Hono app captures parametrized route names on Bun', async ({ signal }) => { - const runner = createRunner(__dirname) - .expect(envelope => { - const [, envelopeItems] = envelope; - const [itemHeader, itemPayload] = envelopeItems[0]; +test('sends the Hono error with the transaction name in the trace envelope header', async () => { + const runner = createRunner(__dirname, 'index.ts') + .withMockSentryServer() + .ignore('span') + .expectHeader({ + event: { + sdk: { name: 'sentry.javascript.hono', version: SDK_VERSION }, + trace: expect.objectContaining({ + environment: 'production', + public_key: 'public', + trace_id: expect.stringMatching(/^[\da-f]{32}$/), + sampled: 'true', + sample_rand: expect.stringMatching(/^[01](\.\d+)?$/), + transaction: 'GET /error/:param', + }), + }, + }) + .start(); - expect(itemHeader.type).toBe('span'); + await runner.makeRequest('get', '/error/param-123', { expectError: true }); + await runner.completed(); +}); - const segmentSpan = (itemPayload as SerializedStreamedSpanContainer).items.find(span => span.is_segment); +test('Hono app captures parametrized route names on Bun', async () => { + const runner = createRunner(__dirname, 'index.ts') + .withMockSentryServer() + .expect({ + span: container => { + const segmentSpan = container.items.find(span => span.is_segment); - expect(segmentSpan).toMatchObject({ - name: 'GET /hello/:name', - is_segment: true, - span_id: SHORT_UUID_MATCHER, - trace_id: UUID_MATCHER, - status: 'ok', - attributes: expect.objectContaining({ - 'sentry.op': { value: 'http.server', type: 'string' }, - 'sentry.origin': { value: 'auto.http.bun.serve', type: 'string' }, - 'sentry.segment.name.source': { value: 'route', type: 'string' }, - 'http.route': { value: '/hello/:name', type: 'string' }, - 'http.request.method': { value: 'GET', type: 'string' }, - 'url.path': { value: '/hello/world', type: 'string' }, - }), - }); + expect(segmentSpan).toMatchObject({ + name: 'GET /hello/:name', + is_segment: true, + span_id: expect.stringMatching(/^[\da-f]{16}$/), + trace_id: expect.stringMatching(/^[\da-f]{32}$/), + status: 'ok', + attributes: expect.objectContaining({ + 'sentry.op': { value: 'http.server', type: 'string' }, + 'sentry.origin': { value: 'auto.http.bun.serve', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'http.route': { value: '/hello/:name', type: 'string' }, + 'http.request.method': { value: 'GET', type: 'string' }, + 'url.path': { value: '/hello/world', type: 'string' }, + }), + }); + }, }) - .start(signal); + .start(); await runner.makeRequest('get', '/hello/world'); await runner.completed(); diff --git a/dev-packages/bun-integration-tests/suites/is-localhost/index.ts b/dev-packages/bun-integration-tests/suites/is-localhost/index.ts index 0f02bee605ee..5c323f972ffb 100644 --- a/dev-packages/bun-integration-tests/suites/is-localhost/index.ts +++ b/dev-packages/bun-integration-tests/suites/is-localhost/index.ts @@ -1,3 +1,4 @@ +import { sendPortToRunner } from '@sentry-internal/node-integration-tests'; import * as Sentry from '@sentry/bun'; Sentry.init({ @@ -15,4 +16,4 @@ const server = Bun.serve({ }, }); -process.send?.(JSON.stringify({ event: 'READY', port: server.port })); +sendPortToRunner(server.port!); diff --git a/dev-packages/bun-integration-tests/suites/is-localhost/test.ts b/dev-packages/bun-integration-tests/suites/is-localhost/test.ts index 8250220b6891..8904b0e0a574 100644 --- a/dev-packages/bun-integration-tests/suites/is-localhost/test.ts +++ b/dev-packages/bun-integration-tests/suites/is-localhost/test.ts @@ -1,26 +1,27 @@ -import type { Envelope, SerializedStreamedSpan, SerializedStreamedSpanContainer } from '@sentry/core'; -import { expect, it } from 'vitest'; -import { createRunner } from '../../runner'; +import { afterAll, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../node-integration-tests/utils/runner'; -function getSpans(envelope: Envelope): SerializedStreamedSpan[] { - return (envelope[1][0][1] as SerializedStreamedSpanContainer).items; -} +afterAll(() => { + cleanupChildProcesses(); +}); // The runner always requests `http://localhost:`, so only the `true` case is reachable here. // The `false` case is covered by the unit tests for `isLocalhostRequest`. -it('sets sentry.is_localhost on every streamed span', async ({ signal }) => { - const runner = createRunner(__dirname) - .expect(envelope => { - const spans = getSpans(envelope); - - expect(spans.some(span => span.is_segment)).toBe(true); - expect(spans.some(span => span.name === 'child-span')).toBe(true); +test('sets sentry.is_localhost on every streamed span', async () => { + const runner = createRunner(__dirname, 'index.ts') + .withMockSentryServer() + .unordered() + .expect({ + span: container => { + expect(container.items.some(span => span.is_segment)).toBe(true); + expect(container.items.some(span => span.name === 'child-span')).toBe(true); - for (const span of spans) { - expect(span.attributes['sentry.is_localhost']).toEqual({ value: true, type: 'boolean' }); - } + for (const span of container.items) { + expect(span.attributes['sentry.is_localhost']).toEqual({ value: true, type: 'boolean' }); + } + }, }) - .start(signal); + .start(); await runner.makeRequest('get', '/'); await runner.completed(); diff --git a/dev-packages/bun-integration-tests/suites/request-bodies/index.ts b/dev-packages/bun-integration-tests/suites/request-bodies/index.ts index d3bf4d5417a5..dbd9ecfd5dd2 100644 --- a/dev-packages/bun-integration-tests/suites/request-bodies/index.ts +++ b/dev-packages/bun-integration-tests/suites/request-bodies/index.ts @@ -1,3 +1,4 @@ +import { sendPortToRunner } from '@sentry-internal/node-integration-tests'; import * as Sentry from '@sentry/bun'; // One scenario per process; the test picks the SDK setup through this variable. @@ -32,4 +33,4 @@ const server = Bun.serve({ }, }); -process.send?.(JSON.stringify({ event: 'READY', port: server.port })); +sendPortToRunner(server.port!); diff --git a/dev-packages/bun-integration-tests/suites/request-bodies/test.ts b/dev-packages/bun-integration-tests/suites/request-bodies/test.ts index da08cbc248c7..0134022d2b29 100644 --- a/dev-packages/bun-integration-tests/suites/request-bodies/test.ts +++ b/dev-packages/bun-integration-tests/suites/request-bodies/test.ts @@ -1,26 +1,25 @@ -import type { Envelope, TransactionEvent } from '@sentry/core'; -import { expect, it } from 'vitest'; -import { createRunner } from '../../runner'; +import { afterAll, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../node-integration-tests/utils/runner'; -function getTransaction(envelope: Envelope): TransactionEvent { - const [itemHeader, itemPayload] = envelope[1][0]; - expect(itemHeader.type).toBe('transaction'); - return itemPayload as TransactionEvent; -} +afterAll(() => { + cleanupChildProcesses(); +}); -it('captures incoming request bodies by default', async ({ signal }) => { - const runner = createRunner(__dirname) - .expect(envelope => { - const transaction = getTransaction(envelope); - expect(transaction.request).toMatchObject({ - method: 'POST', - url: expect.stringContaining('/default'), - query_string: 'source=test', - headers: expect.objectContaining({ 'content-type': 'text/plain' }), - data: 'captured-by-default', - }); +test('captures incoming request bodies by default', async () => { + const runner = createRunner(__dirname, 'index.ts') + .withMockSentryServer() + .expect({ + transaction: transaction => { + expect(transaction.request).toMatchObject({ + method: 'POST', + url: expect.stringContaining('/default'), + query_string: 'source=test', + headers: expect.objectContaining({ 'content-type': 'text/plain' }), + data: 'captured-by-default', + }); + }, }) - .start(signal); + .start(); const response = await runner.makeRequest('post', '/default?source=test', { headers: { 'content-type': 'text/plain' }, @@ -30,14 +29,16 @@ it('captures incoming request bodies by default', async ({ signal }) => { await runner.completed(); }); -it('an explicit small size overrides disabled body collection', async ({ signal }) => { - const runner = createRunner(__dirname) +test('an explicit small size overrides disabled body collection', async () => { + const runner = createRunner(__dirname, 'index.ts') + .withMockSentryServer() .withEnv({ BODY_MODE: 'explicit-small' }) - .expect(envelope => { - const transaction = getTransaction(envelope); - expect(transaction.request?.data).toBe(`${'a'.repeat(997)}...`); + .expect({ + transaction: transaction => { + expect(transaction.request?.data).toBe(`${'a'.repeat(997)}...`); + }, }) - .start(signal); + .start(); const body = 'a'.repeat(1_001); const response = await runner.makeRequest('post', '/explicit-small', { @@ -48,15 +49,17 @@ it('an explicit small size overrides disabled body collection', async ({ signal await runner.completed(); }); -it('an explicit none overrides enabled body collection', async ({ signal }) => { - const runner = createRunner(__dirname) +test('an explicit none overrides enabled body collection', async () => { + const runner = createRunner(__dirname, 'index.ts') + .withMockSentryServer() .withEnv({ BODY_MODE: 'explicit-none' }) - .expect(envelope => { - const transaction = getTransaction(envelope); - expect(transaction.request?.method).toBe('POST'); - expect(transaction.request?.data).toBeUndefined(); + .expect({ + transaction: transaction => { + expect(transaction.request?.method).toBe('POST'); + expect(transaction.request?.data).toBeUndefined(); + }, }) - .start(signal); + .start(); const response = await runner.makeRequest('post', '/explicit-none', { headers: { 'content-type': 'text/plain' }, diff --git a/dev-packages/bun-integration-tests/vite.config.mts b/dev-packages/bun-integration-tests/vite.config.mts index 0147c9abd394..8621cd5b6af1 100644 --- a/dev-packages/bun-integration-tests/vite.config.mts +++ b/dev-packages/bun-integration-tests/vite.config.mts @@ -49,6 +49,7 @@ export default defineConfig({ test: { name: 'bun', include: ['./suites/**/test.ts'], + env: { RUNTIME: 'bun' }, poolOptions: { threads: { singleThread: true, From 413b4d1839229ddba900e8a09f6b89c56b8aedb1 Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Thu, 24 Sep 2026 11:11:58 +0200 Subject: [PATCH 2/3] fixup! test(bun): Run the Bun-only suites with the shared Node runner Co-Authored-By: Claude Opus 5.5 --- dev-packages/bun-integration-tests/vite.config.mts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dev-packages/bun-integration-tests/vite.config.mts b/dev-packages/bun-integration-tests/vite.config.mts index 8621cd5b6af1..8aed2702be3a 100644 --- a/dev-packages/bun-integration-tests/vite.config.mts +++ b/dev-packages/bun-integration-tests/vite.config.mts @@ -50,6 +50,8 @@ export default defineConfig({ name: 'bun', include: ['./suites/**/test.ts'], env: { RUNTIME: 'bun' }, + // Above the 30 second port timeout of the runner on Bun, so a slow start can still pass. + testTimeout: 45_000, poolOptions: { threads: { singleThread: true, From f2f90c0af87478c3dffb40fb24da04ce1a0cf8ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Peer=20St=C3=B6cklmair?= Date: Thu, 24 Sep 2026 11:47:45 +0200 Subject: [PATCH 3/3] fixup! test(bun): Run the Bun-only suites with the shared Node runner Co-authored-by: isaacs --- dev-packages/bun-integration-tests/suites/is-localhost/test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/dev-packages/bun-integration-tests/suites/is-localhost/test.ts b/dev-packages/bun-integration-tests/suites/is-localhost/test.ts index 8904b0e0a574..2dfa918630ea 100644 --- a/dev-packages/bun-integration-tests/suites/is-localhost/test.ts +++ b/dev-packages/bun-integration-tests/suites/is-localhost/test.ts @@ -10,7 +10,6 @@ afterAll(() => { test('sets sentry.is_localhost on every streamed span', async () => { const runner = createRunner(__dirname, 'index.ts') .withMockSentryServer() - .unordered() .expect({ span: container => { expect(container.items.some(span => span.is_segment)).toBe(true);