diff --git a/packages/bun/src/integrations/bunserver.ts b/packages/bun/src/integrations/bunserver.ts index d8ee46abae73..2e9efe25af62 100644 --- a/packages/bun/src/integrations/bunserver.ts +++ b/packages/bun/src/integrations/bunserver.ts @@ -47,7 +47,18 @@ export function instrumentBunServe(): void { Bun.serve = new Proxy(Bun.serve, { apply(serveTarget, serveThisArg, serveArgs: Parameters) { instrumentBunServeOptions(serveArgs[0]); - return serveTarget.apply(serveThisArg, serveArgs); + const server: ReturnType = serveTarget.apply(serveThisArg, serveArgs); + + // A Bun server can be reloaded, re-wrap any fetch function passed to it + // We can't use a Proxy for this as Bun does `instanceof` checks internally that fail if we + // wrap the Server instance. + const originalReload: typeof server.reload = server.reload.bind(server); + server.reload = (serveOptions: Parameters[0]) => { + instrumentBunServeOptions(serveOptions); + return originalReload(serveOptions); + } + + return server; }, }); } diff --git a/packages/bun/test/integrations/bunserver.test.ts b/packages/bun/test/integrations/bunserver.test.ts index b1dc17381ccb..e432cebb6f0c 100644 --- a/packages/bun/test/integrations/bunserver.test.ts +++ b/packages/bun/test/integrations/bunserver.test.ts @@ -1,67 +1,87 @@ -import { beforeAll, beforeEach, describe, expect, test } from 'bun:test'; +import { afterEach, beforeAll, beforeEach, describe, expect, test } from 'bun:test'; +import type { Span} from '@sentry/core'; import { getDynamicSamplingContextFromSpan, setCurrentClient, spanIsSampled, spanToJSON } from '@sentry/core'; import { BunClient } from '../../src/client'; import { instrumentBunServe } from '../../src/integrations/bunserver'; import { getDefaultBunClientOptions } from '../helpers'; -// Fun fact: Bun = 2 21 14 :) -const DEFAULT_PORT = 22114; - describe('Bun Serve Integration', () => { let client: BunClient; + // Fun fact: Bun = 2 21 14 :) + let port: number = 22114; beforeAll(() => { instrumentBunServe(); }); beforeEach(() => { - const options = getDefaultBunClientOptions({ tracesSampleRate: 1, debug: true }); + const options = getDefaultBunClientOptions({ tracesSampleRate: 1, }); client = new BunClient(options); setCurrentClient(client); client.init(); }); + afterEach(() => { + // Don't reuse the port; Bun server stops lazily so tests may accidentally hit a server still closing from a + // previous test + port += 1; + }); + test('generates a transaction around a request', async () => { + let generatedSpan: Span | undefined; + client.on('spanEnd', span => { - expect(spanToJSON(span).status).toBe('ok'); - expect(spanToJSON(span).data?.['http.response.status_code']).toEqual(200); - expect(spanToJSON(span).op).toEqual('http.server'); - expect(spanToJSON(span).description).toEqual('GET /'); + generatedSpan = span; }); const server = Bun.serve({ async fetch(_req) { return new Response('Bun!'); }, - port: DEFAULT_PORT, + port, }); + await fetch(`http://localhost:${port}/`); + server.stop(); - await fetch('http://localhost:22114/'); + if (!generatedSpan) { + throw 'No span was generated in the test'; + } - server.stop(); + expect(spanToJSON(generatedSpan).status).toBe('ok'); + expect(spanToJSON(generatedSpan).data?.['http.response.status_code']).toEqual(200); + expect(spanToJSON(generatedSpan).op).toEqual('http.server'); + expect(spanToJSON(generatedSpan).description).toEqual('GET /'); }); test('generates a post transaction', async () => { + let generatedSpan: Span | undefined; + client.on('spanEnd', span => { - expect(spanToJSON(span).status).toBe('ok'); - expect(spanToJSON(span).data?.['http.response.status_code']).toEqual(200); - expect(spanToJSON(span).op).toEqual('http.server'); - expect(spanToJSON(span).description).toEqual('POST /'); + generatedSpan = span; }); const server = Bun.serve({ async fetch(_req) { return new Response('Bun!'); }, - port: DEFAULT_PORT, + port, }); - await fetch('http://localhost:22114/', { + await fetch(`http://localhost:${port}/`, { method: 'POST', }); server.stop(); + + if (!generatedSpan) { + throw 'No span was generated in the test'; + } + + expect(spanToJSON(generatedSpan).status).toBe('ok'); + expect(spanToJSON(generatedSpan).data?.['http.response.status_code']).toEqual(200); + expect(spanToJSON(generatedSpan).op).toEqual('http.server'); + expect(spanToJSON(generatedSpan).description).toEqual('POST /'); }); test('continues a trace', async () => { @@ -70,55 +90,93 @@ describe('Bun Serve Integration', () => { const PARENT_SAMPLED = '1'; const SENTRY_TRACE_HEADER = `${TRACE_ID}-${PARENT_SPAN_ID}-${PARENT_SAMPLED}`; - const SENTRY_BAGGAGE_HEADER = 'sentry-version=1.0,sentry-environment=production'; + const SENTRY_BAGGAGE_HEADER = 'sentry-version=1.0,sentry-sample_rand=0.42,sentry-environment=production'; - client.on('spanEnd', span => { - expect(span.spanContext().traceId).toBe(TRACE_ID); - expect(spanToJSON(span).parent_span_id).toBe(PARENT_SPAN_ID); - expect(spanIsSampled(span)).toBe(true); - expect(span.isRecording()).toBe(false); + let generatedSpan: Span | undefined; - expect(getDynamicSamplingContextFromSpan(span)).toStrictEqual({ - version: '1.0', - environment: 'production', - }); + client.on('spanEnd', span => { + generatedSpan = span; }); const server = Bun.serve({ async fetch(_req) { return new Response('Bun!'); }, - port: DEFAULT_PORT, + port, }); - await fetch('http://localhost:22114/', { + await fetch(`http://localhost:${port}/`, { headers: { 'sentry-trace': SENTRY_TRACE_HEADER, baggage: SENTRY_BAGGAGE_HEADER }, }); server.stop(); + + if (!generatedSpan) { + throw 'No span was generated in the test'; + } + + expect(generatedSpan.spanContext().traceId).toBe(TRACE_ID); + expect(spanToJSON(generatedSpan).parent_span_id).toBe(PARENT_SPAN_ID); + expect(spanIsSampled(generatedSpan)).toBe(true); + expect(generatedSpan.isRecording()).toBe(false); + + expect(getDynamicSamplingContextFromSpan(generatedSpan)).toStrictEqual({ + version: '1.0', + sample_rand: '0.42', + environment: 'production', + }); }); test('does not create transactions for OPTIONS or HEAD requests', async () => { - client.on('spanEnd', () => { - // This will never run, but we want to make sure it doesn't run. - expect(false).toEqual(true); + let generatedSpan: Span | undefined; + + client.on('spanEnd', span => { + generatedSpan = span; }); const server = Bun.serve({ async fetch(_req) { return new Response('Bun!'); }, - port: DEFAULT_PORT, + port, }); - await fetch('http://localhost:22114/', { + await fetch(`http://localhost:${port}/`, { method: 'OPTIONS', }); - await fetch('http://localhost:22114/', { + await fetch(`http://localhost:${port}/`, { method: 'HEAD', }); server.stop(); + + expect(generatedSpan).toBeUndefined(); }); + + test('intruments the server again if it is reloaded', async () => { + let serverWasInstrumented = false; + client.on('spanEnd', () => { + serverWasInstrumented = true; + }); + + const server = Bun.serve({ + async fetch(_req) { + return new Response('Bun!'); + }, + port, + }); + + server.reload({ + async fetch(_req) { + return new Response('Reloaded Bun!'); + }, + }); + + await fetch(`http://localhost:${port}/`); + + server.stop(); + + expect(serverWasInstrumented).toBeTrue(); + }) }); diff --git a/packages/bun/test/sdk.test.ts b/packages/bun/test/sdk.test.ts index a548cc2614c7..11870f30c101 100644 --- a/packages/bun/test/sdk.test.ts +++ b/packages/bun/test/sdk.test.ts @@ -1,14 +1,20 @@ -import { expect, test } from 'bun:test'; +import { describe, expect, test } from 'bun:test'; import { init } from '../src/index'; -test("calling init shouldn't fail", () => { - init({ +describe('Bun SDK', () => { + const initOptions = { dsn: 'https://00000000000000000000000000000000@o000000.ingest.sentry.io/0000000', + tracesSampleRate: 1, + }; + + test("calling init shouldn't fail", () => { + expect(() => { + init(initOptions); + }).not.toThrow(); }); - expect(true).toBe(true); -}); -test('should return client from init', () => { - expect(init({})).not.toBeUndefined(); + test('should return client from init', () => { + expect(init(initOptions)).not.toBeUndefined(); + }); });