diff --git a/packages/bun/package.json b/packages/bun/package.json index 5283164b287d..f55b9d8637f9 100644 --- a/packages/bun/package.json +++ b/packages/bun/package.json @@ -44,7 +44,7 @@ "@sentry/opentelemetry": "9.12.0" }, "devDependencies": { - "bun-types": "latest" + "bun-types": "^1.2.9" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/bun/src/integrations/bunserver.ts b/packages/bun/src/integrations/bunserver.ts index 1f1974839455..89a86d827ea0 100644 --- a/packages/bun/src/integrations/bunserver.ts +++ b/packages/bun/src/integrations/bunserver.ts @@ -1,17 +1,17 @@ +import type { ServeOptions } from 'bun'; import type { IntegrationFn, RequestEventData, SpanAttributes } from '@sentry/core'; import { SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, captureException, - continueTrace, - defineIntegration, - extractQueryParamsFromUrl, - getSanitizedUrlString, - parseUrl, + isURLObjectRelative, setHttpStatus, + defineIntegration, + continueTrace, startSpan, withIsolationScope, + parseStringToURLObject, } from '@sentry/core'; const INTEGRATION_NAME = 'BunServer'; @@ -28,6 +28,8 @@ const _bunServerIntegration = (() => { /** * Instruments `Bun.serve` to automatically create transactions and capture errors. * + * Does not support instrumenting static routes. + * * Enabled by default in the Bun SDK. * * ```js @@ -40,10 +42,18 @@ const _bunServerIntegration = (() => { */ export const bunServerIntegration = defineIntegration(_bunServerIntegration); +let hasPatchedBunServe = false; + /** * Instruments Bun.serve by patching it's options. + * + * Only exported for tests. */ export function instrumentBunServe(): void { + if (hasPatchedBunServe) { + return; + } + Bun.serve = new Proxy(Bun.serve, { apply(serveTarget, serveThisArg, serveArgs: Parameters) { instrumentBunServeOptions(serveArgs[0]); @@ -53,7 +63,7 @@ export function instrumentBunServe(): void { // 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]) => { + server.reload = (serveOptions: ServeOptions) => { instrumentBunServeOptions(serveOptions); return originalReload(serveOptions); }; @@ -61,81 +71,223 @@ export function instrumentBunServe(): void { return server; }, }); + + hasPatchedBunServe = true; } /** - * Instruments Bun.serve `fetch` option to automatically create spans and capture errors. + * Instruments Bun.serve options. + * + * @param serveOptions - The options for the Bun.serve function. */ function instrumentBunServeOptions(serveOptions: Parameters[0]): void { + // First handle fetch + instrumentBunServeOptionFetch(serveOptions); + // then handle routes + instrumentBunServeOptionRoutes(serveOptions); +} + +/** + * Instruments the `fetch` option of Bun.serve. + * + * @param serveOptions - The options for the Bun.serve function. + */ +function instrumentBunServeOptionFetch(serveOptions: Parameters[0]): void { + if (typeof serveOptions.fetch !== 'function') { + return; + } + serveOptions.fetch = new Proxy(serveOptions.fetch, { apply(fetchTarget, fetchThisArg, fetchArgs: Parameters) { - return withIsolationScope(isolationScope => { - const request = fetchArgs[0]; - const upperCaseMethod = request.method.toUpperCase(); - if (upperCaseMethod === 'OPTIONS' || upperCaseMethod === 'HEAD') { - return fetchTarget.apply(fetchThisArg, fetchArgs); - } + return wrapRequestHandler(fetchTarget, fetchThisArg, fetchArgs); + }, + }); +} - const parsedUrl = parseUrl(request.url); - const attributes: SpanAttributes = { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.bun.serve', - [SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD]: request.method || 'GET', - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', - }; - if (parsedUrl.search) { - attributes['http.query'] = parsedUrl.search; - } +/** + * Instruments the `routes` option of Bun.serve. + * + * @param serveOptions - The options for the Bun.serve function. + */ +function instrumentBunServeOptionRoutes(serveOptions: Parameters[0]): void { + if (!serveOptions.routes) { + return; + } - const url = getSanitizedUrlString(parsedUrl); - - isolationScope.setSDKProcessingMetadata({ - normalizedRequest: { - url, - method: request.method, - headers: request.headers.toJSON(), - query_string: extractQueryParamsFromUrl(url), - } satisfies RequestEventData, - }); - - return continueTrace( - { sentryTrace: request.headers.get('sentry-trace') || '', baggage: request.headers.get('baggage') }, - () => { - return startSpan( - { - attributes, - op: 'http.server', - name: `${request.method} ${parsedUrl.path || '/'}`, - }, - async span => { - try { - const response = await (fetchTarget.apply(fetchThisArg, fetchArgs) as ReturnType< - typeof serveOptions.fetch - >); - if (response?.status) { - setHttpStatus(span, response.status); - isolationScope.setContext('response', { - headers: response.headers.toJSON(), - status_code: response.status, - }); - } - return response; - } catch (e) { - captureException(e, { - mechanism: { - type: 'bun', - handled: false, - data: { - function: 'serve', - }, - }, - }); - throw e; - } + if (typeof serveOptions.routes !== 'object') { + return; + } + + Object.keys(serveOptions.routes).forEach(route => { + const routeHandler = serveOptions.routes[route]; + + // Handle route handlers that are an object + if (typeof routeHandler === 'function') { + serveOptions.routes[route] = new Proxy(routeHandler, { + apply: (routeHandlerTarget, routeHandlerThisArg, routeHandlerArgs: Parameters) => { + return wrapRequestHandler(routeHandlerTarget, routeHandlerThisArg, routeHandlerArgs, route); + }, + }); + } + + // Static routes are not instrumented + if (routeHandler instanceof Response) { + return; + } + + // Handle the route handlers that are an object. This means they define a route handler for each method. + if (typeof routeHandler === 'object') { + Object.entries(routeHandler).forEach(([routeHandlerObjectHandlerKey, routeHandlerObjectHandler]) => { + if (typeof routeHandlerObjectHandler === 'function') { + (serveOptions.routes[route] as Record)[routeHandlerObjectHandlerKey] = new Proxy( + routeHandlerObjectHandler, + { + apply: ( + routeHandlerObjectHandlerTarget, + routeHandlerObjectHandlerThisArg, + routeHandlerObjectHandlerArgs: Parameters, + ) => { + return wrapRequestHandler( + routeHandlerObjectHandlerTarget, + routeHandlerObjectHandlerThisArg, + routeHandlerObjectHandlerArgs, + route, + ); }, - ); - }, - ); + }, + ); + } }); - }, + } }); } + +type RouteHandler = Extract< + NonNullable[0]['routes']>[string], + // eslint-disable-next-line @typescript-eslint/ban-types + Function +>; + +function wrapRequestHandler( + target: T, + thisArg: unknown, + args: Parameters, + route?: string, +): ReturnType { + return withIsolationScope(isolationScope => { + const request = args[0]; + const upperCaseMethod = request.method.toUpperCase(); + if (upperCaseMethod === 'OPTIONS' || upperCaseMethod === 'HEAD') { + return target.apply(thisArg, args); + } + + const parsedUrl = parseStringToURLObject(request.url); + const attributes = getSpanAttributesFromParsedUrl(parsedUrl, request); + + let routeName = parsedUrl?.pathname || '/'; + if (request.params) { + Object.keys(request.params).forEach(key => { + attributes[`url.path.parameter.${key}`] = (request.params as Record)[key]; + }); + + // If a route has parameters, it's a parameterized route + if (route) { + attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] = 'route'; + attributes['url.template'] = route; + routeName = route; + } + } + + // Handle wildcard routes + if (route?.endsWith('/*')) { + attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] = 'route'; + attributes['url.template'] = route; + routeName = route; + } + + isolationScope.setSDKProcessingMetadata({ + normalizedRequest: { + url: request.url, + method: request.method, + headers: request.headers.toJSON(), + query_string: parsedUrl?.search, + } satisfies RequestEventData, + }); + + return continueTrace( + { + sentryTrace: request.headers.get('sentry-trace') ?? '', + baggage: request.headers.get('baggage'), + }, + () => + startSpan( + { + attributes, + op: 'http.server', + name: `${request.method} ${routeName}`, + }, + async span => { + try { + const response = (await target.apply(thisArg, args)) as Response | undefined; + if (response?.status) { + setHttpStatus(span, response.status); + isolationScope.setContext('response', { + headers: response.headers.toJSON(), + status_code: response.status, + }); + } + return response; + } catch (e) { + captureException(e, { + mechanism: { + type: 'bun', + handled: false, + data: { + function: 'serve', + }, + }, + }); + throw e; + } + }, + ), + ); + }); +} + +function getSpanAttributesFromParsedUrl( + parsedUrl: ReturnType, + request: Request, +): SpanAttributes { + const attributes: SpanAttributes = { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.bun.serve', + [SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD]: request.method || 'GET', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + }; + + if (parsedUrl) { + if (parsedUrl.search) { + attributes['url.query'] = parsedUrl.search; + } + if (parsedUrl.hash) { + attributes['url.fragment'] = parsedUrl.hash; + } + if (parsedUrl.pathname) { + attributes['url.path'] = parsedUrl.pathname; + } + if (!isURLObjectRelative(parsedUrl)) { + attributes['url.full'] = parsedUrl.href; + if (parsedUrl.port) { + attributes['url.port'] = parsedUrl.port; + } + if (parsedUrl.protocol) { + attributes['url.scheme'] = parsedUrl.protocol; + } + if (parsedUrl.hostname) { + attributes['url.domain'] = parsedUrl.hostname; + } + } + } + + return attributes; +} diff --git a/packages/bun/test/integrations/bunserver.test.ts b/packages/bun/test/integrations/bunserver.test.ts index 66a66476f78d..29e8241917ba 100644 --- a/packages/bun/test/integrations/bunserver.test.ts +++ b/packages/bun/test/integrations/bunserver.test.ts @@ -1,26 +1,24 @@ -import { afterEach, beforeAll, beforeEach, describe, expect, test } from 'bun:test'; -import type { Span } from '@sentry/core'; -import { getDynamicSamplingContextFromSpan, spanIsSampled, spanToJSON } from '@sentry/core'; +import { afterEach, beforeEach, beforeAll, describe, expect, test, spyOn } from 'bun:test'; +import * as SentryCore from '@sentry/core'; -import { init } from '../../src'; -import type { NodeClient } from '../../src'; import { instrumentBunServe } from '../../src/integrations/bunserver'; -import { getDefaultBunClientOptions } from '../helpers'; describe('Bun Serve Integration', () => { - let client: NodeClient | undefined; - // Fun fact: Bun = 2 21 14 :) - let port: number = 22114; + const continueTraceSpy = spyOn(SentryCore, 'continueTrace'); + const startSpanSpy = spyOn(SentryCore, 'startSpan'); beforeAll(() => { instrumentBunServe(); }); beforeEach(() => { - const options = getDefaultBunClientOptions({ tracesSampleRate: 1 }); - client = init(options); + startSpanSpy.mockClear(); + continueTraceSpy.mockClear(); }); + // Fun fact: Bun = 2 21 14 :) + let port: number = 22114; + afterEach(() => { // Don't reuse the port; Bun server stops lazily so tests may accidentally hit a server still closing from a // previous test @@ -28,12 +26,6 @@ describe('Bun Serve Integration', () => { }); test('generates a transaction around a request', async () => { - let generatedSpan: Span | undefined; - - client?.on('spanEnd', span => { - generatedSpan = span; - }); - const server = Bun.serve({ async fetch(_req) { return new Response('Bun!'); @@ -41,34 +33,30 @@ describe('Bun Serve Integration', () => { port, }); await fetch(`http://localhost:${port}/users?id=123`); - server.stop(); - - if (!generatedSpan) { - throw 'No span was generated in the test'; - } - - const spanJson = spanToJSON(generatedSpan); - expect(spanJson.status).toBe('ok'); - expect(spanJson.op).toEqual('http.server'); - expect(spanJson.description).toEqual('GET /users'); - expect(spanJson.data).toEqual({ - 'http.query': '?id=123', - 'http.request.method': 'GET', - 'http.response.status_code': 200, - 'sentry.op': 'http.server', - 'sentry.origin': 'auto.http.bun.serve', - 'sentry.sample_rate': 1, - 'sentry.source': 'url', - }); + await server.stop(); + + expect(startSpanSpy).toHaveBeenCalledTimes(1); + expect(startSpanSpy).toHaveBeenLastCalledWith( + { + attributes: { + 'sentry.origin': 'auto.http.bun.serve', + 'http.request.method': 'GET', + 'sentry.source': 'url', + 'url.query': '?id=123', + 'url.path': '/users', + 'url.full': `http://localhost:${port}/users?id=123`, + 'url.port': port.toString(), + 'url.scheme': 'http:', + 'url.domain': 'localhost', + }, + op: 'http.server', + name: 'GET /users', + }, + expect.any(Function), + ); }); test('generates a post transaction', async () => { - let generatedSpan: Span | undefined; - - client?.on('spanEnd', span => { - generatedSpan = span; - }); - const server = Bun.serve({ async fetch(_req) { return new Response('Bun!'); @@ -80,16 +68,26 @@ describe('Bun Serve Integration', () => { 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 /'); + await server.stop(); + + expect(startSpanSpy).toHaveBeenCalledTimes(1); + expect(startSpanSpy).toHaveBeenLastCalledWith( + { + attributes: { + 'sentry.origin': 'auto.http.bun.serve', + 'http.request.method': 'POST', + 'sentry.source': 'url', + 'url.path': '/', + 'url.full': `http://localhost:${port}/`, + 'url.port': port.toString(), + 'url.scheme': 'http:', + 'url.domain': 'localhost', + }, + op: 'http.server', + name: 'POST /', + }, + expect.any(Function), + ); }); test('continues a trace', async () => { @@ -98,13 +96,7 @@ 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-sample_rand=0.42,sentry-environment=production'; - - let generatedSpan: Span | undefined; - - client?.on('spanEnd', span => { - generatedSpan = span; - }); + const SENTRY_BAGGAGE_HEADER = 'sentry-sample_rand=0.42,sentry-environment=production'; const server = Bun.serve({ async fetch(_req) { @@ -113,35 +105,31 @@ describe('Bun Serve Integration', () => { port, }); + // Make request with trace headers await fetch(`http://localhost:${port}/`, { - headers: { 'sentry-trace': SENTRY_TRACE_HEADER, baggage: SENTRY_BAGGAGE_HEADER }, + headers: { + 'sentry-trace': SENTRY_TRACE_HEADER, + baggage: SENTRY_BAGGAGE_HEADER, + }, }); - server.stop(); + await 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); + // Verify continueTrace was called with the correct headers + expect(continueTraceSpy).toHaveBeenCalledTimes(1); + expect(continueTraceSpy).toHaveBeenCalledWith( + { + sentryTrace: SENTRY_TRACE_HEADER, + baggage: SENTRY_BAGGAGE_HEADER, + }, + expect.any(Function), + ); - expect(getDynamicSamplingContextFromSpan(generatedSpan)).toStrictEqual({ - version: '1.0', - sample_rand: '0.42', - environment: 'production', - }); + // Verify a span was created + expect(startSpanSpy).toHaveBeenCalledTimes(1); }); - test('does not create transactions for OPTIONS or HEAD requests', async () => { - let generatedSpan: Span | undefined; - - client?.on('spanEnd', span => { - generatedSpan = span; - }); - + test('skips span creation for OPTIONS and HEAD requests', async () => { const server = Bun.serve({ async fetch(_req) { return new Response('Bun!'); @@ -149,42 +137,265 @@ describe('Bun Serve Integration', () => { port, }); - await fetch(`http://localhost:${port}/`, { + // Make OPTIONS request + const optionsResponse = await fetch(`http://localhost:${port}/`, { method: 'OPTIONS', }); + expect(await optionsResponse.text()).toBe('Bun!'); - await fetch(`http://localhost:${port}/`, { + // Make HEAD request + const headResponse = await fetch(`http://localhost:${port}/`, { method: 'HEAD', }); + expect(await headResponse.text()).toBe(''); - server.stop(); + // Verify no spans were created + expect(startSpanSpy).not.toHaveBeenCalled(); - expect(generatedSpan).toBeUndefined(); + // Make a GET request to verify spans are still created for other methods + const getResponse = await fetch(`http://localhost:${port}/`); + expect(await getResponse.text()).toBe('Bun!'); + expect(startSpanSpy).toHaveBeenCalledTimes(1); + + await server.stop(); }); - test('intruments the server again if it is reloaded', async () => { - let serverWasInstrumented = false; - client?.on('spanEnd', () => { - serverWasInstrumented = true; + test('handles route parameters correctly', async () => { + const server = Bun.serve({ + routes: { + '/users/:id': req => { + return new Response(`User ${req.params.id}`); + }, + }, + port, }); + // Make request to parameterized route + const response = await fetch(`http://localhost:${port}/users/123`); + expect(await response.text()).toBe('User 123'); + + // Verify span was created with correct attributes + expect(startSpanSpy).toHaveBeenCalledTimes(1); + expect(startSpanSpy).toHaveBeenLastCalledWith( + expect.objectContaining({ + attributes: expect.objectContaining({ + 'sentry.origin': 'auto.http.bun.serve', + 'http.request.method': 'GET', + 'sentry.source': 'route', + 'url.template': '/users/:id', + 'url.path.parameter.id': '123', + 'url.path': '/users/123', + 'url.full': `http://localhost:${port}/users/123`, + 'url.port': port.toString(), + 'url.scheme': 'http:', + 'url.domain': 'localhost', + }), + op: 'http.server', + name: 'GET /users/:id', + }), + expect.any(Function), + ); + + await server.stop(); + }); + + test('handles wildcard routes correctly', async () => { + const server = Bun.serve({ + routes: { + '/api/*': req => { + return new Response(`API route: ${req.url}`); + }, + }, + port, + }); + + // Make request to wildcard route + const response = await fetch(`http://localhost:${port}/api/users/123`); + expect(await response.text()).toBe(`API route: http://localhost:${port}/api/users/123`); + + // Verify span was created with correct attributes + expect(startSpanSpy).toHaveBeenCalledTimes(1); + expect(startSpanSpy).toHaveBeenLastCalledWith( + expect.objectContaining({ + attributes: expect.objectContaining({ + 'sentry.origin': 'auto.http.bun.serve', + 'http.request.method': 'GET', + 'sentry.source': 'route', + 'url.template': '/api/*', + 'url.path': '/api/users/123', + 'url.full': `http://localhost:${port}/api/users/123`, + 'url.port': port.toString(), + 'url.scheme': 'http:', + 'url.domain': 'localhost', + }), + op: 'http.server', + name: 'GET /api/*', + }), + expect.any(Function), + ); + + await server.stop(); + }); + + test('reapplies instrumentation after server reload', async () => { const server = Bun.serve({ async fetch(_req) { - return new Response('Bun!'); + return new Response('Initial handler'); }, port, }); + // Verify initial handler works + const initialResponse = await fetch(`http://localhost:${port}/`); + expect(await initialResponse.text()).toBe('Initial handler'); + expect(startSpanSpy).toHaveBeenCalledTimes(1); + startSpanSpy.mockClear(); + + // Reload server with new handler server.reload({ async fetch(_req) { - return new Response('Reloaded Bun!'); + return new Response('Reloaded handler'); }, }); - await fetch(`http://localhost:${port}/`); + // Verify new handler works and is instrumented + const reloadedResponse = await fetch(`http://localhost:${port}/`); + expect(await reloadedResponse.text()).toBe('Reloaded handler'); + expect(startSpanSpy).toHaveBeenCalledTimes(1); + + await server.stop(); + }); + + describe('per-HTTP method routes', () => { + test('handles GET method correctly', async () => { + const server = Bun.serve({ + routes: { + '/api/posts': { + GET: () => new Response('List posts'), + }, + }, + port, + }); + + const response = await fetch(`http://localhost:${port}/api/posts`); + expect(await response.text()).toBe('List posts'); + expect(startSpanSpy).toHaveBeenCalledTimes(1); + expect(startSpanSpy).toHaveBeenLastCalledWith( + expect.objectContaining({ + attributes: expect.objectContaining({ + 'sentry.origin': 'auto.http.bun.serve', + 'http.request.method': 'GET', + 'sentry.source': 'route', + 'url.path': '/api/posts', + }), + op: 'http.server', + name: 'GET /api/posts', + }), + expect.any(Function), + ); + + await server.stop(); + }); + + test('handles POST method correctly', async () => { + const server = Bun.serve({ + routes: { + '/api/posts': { + POST: async req => { + const body = (await req.json()) as Record; + return Response.json({ created: true, ...body }); + }, + }, + }, + port, + }); + + const response = await fetch(`http://localhost:${port}/api/posts`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ title: 'New Post' }), + }); + expect(await response.json()).toEqual({ created: true, title: 'New Post' }); + expect(startSpanSpy).toHaveBeenCalledTimes(1); + expect(startSpanSpy).toHaveBeenLastCalledWith( + expect.objectContaining({ + attributes: expect.objectContaining({ + 'sentry.origin': 'auto.http.bun.serve', + 'http.request.method': 'POST', + 'sentry.source': 'route', + 'url.path': '/api/posts', + }), + op: 'http.server', + name: 'POST /api/posts', + }), + expect.any(Function), + ); + + await server.stop(); + }); - server.stop(); + test('handles PUT method correctly', async () => { + const server = Bun.serve({ + routes: { + '/api/posts': { + PUT: () => new Response('Update post'), + }, + }, + port, + }); + + const response = await fetch(`http://localhost:${port}/api/posts`, { + method: 'PUT', + }); + expect(await response.text()).toBe('Update post'); + expect(startSpanSpy).toHaveBeenCalledTimes(1); + expect(startSpanSpy).toHaveBeenLastCalledWith( + expect.objectContaining({ + attributes: expect.objectContaining({ + 'sentry.origin': 'auto.http.bun.serve', + 'http.request.method': 'PUT', + 'sentry.source': 'route', + 'url.path': '/api/posts', + }), + op: 'http.server', + name: 'PUT /api/posts', + }), + expect.any(Function), + ); + + await server.stop(); + }); - expect(serverWasInstrumented).toBeTrue(); + test('handles DELETE method correctly', async () => { + const server = Bun.serve({ + routes: { + '/api/posts': { + DELETE: () => new Response('Delete post'), + }, + }, + port, + }); + + const response = await fetch(`http://localhost:${port}/api/posts`, { + method: 'DELETE', + }); + expect(await response.text()).toBe('Delete post'); + expect(startSpanSpy).toHaveBeenCalledTimes(1); + expect(startSpanSpy).toHaveBeenLastCalledWith( + expect.objectContaining({ + attributes: expect.objectContaining({ + 'sentry.origin': 'auto.http.bun.serve', + 'http.request.method': 'DELETE', + 'sentry.source': 'route', + 'url.path': '/api/posts', + }), + op: 'http.server', + name: 'DELETE /api/posts', + }), + expect.any(Function), + ); + + await server.stop(); + }); }); }); diff --git a/yarn.lock b/yarn.lock index 346dae607107..9160308494b2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8256,10 +8256,10 @@ dependencies: "@types/webidl-conversions" "*" -"@types/ws@^8.5.1": - version "8.5.10" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.10.tgz#4acfb517970853fa6574a3a6886791d04a396787" - integrity sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A== +"@types/ws@*", "@types/ws@^8.5.1": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.18.1.tgz#48464e4bf2ddfd17db13d845467f6070ffea4aa9" + integrity sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg== dependencies: "@types/node" "*" @@ -11216,10 +11216,13 @@ builtins@^5.0.0, builtins@^5.0.1: dependencies: semver "^7.0.0" -bun-types@latest: - version "1.0.1" - resolved "https://registry.yarnpkg.com/bun-types/-/bun-types-1.0.1.tgz#8bcb10ae3a1548a39f0932fdb365f4b3a649efba" - integrity sha512-7NrXqhMIaNKmWn2dSWEQ50znMZqrN/5Z0NBMXvQTRu/+Y1CvoXRznFy0pnqLe024CeZgVdXoEpARNO1JZLAPGw== +bun-types@^1.2.9: + version "1.2.9" + resolved "https://registry.yarnpkg.com/bun-types/-/bun-types-1.2.9.tgz#e0208ba62f534eb64284c1f347f73bde7105c0f0" + integrity sha512-dk/kOEfQbajENN/D6FyiSgOKEuUi9PWfqKQJEgwKrCMWbjS/S6tEXp178mWvWAcUSYm9ArDlWHZKO3T/4cLXiw== + dependencies: + "@types/node" "*" + "@types/ws" "*" bundle-name@^3.0.0: version "3.0.0" @@ -27017,7 +27020,6 @@ stylus@0.59.0, stylus@^0.59.0: sucrase@^3.27.0, sucrase@^3.35.0, sucrase@getsentry/sucrase#es2020-polyfills: version "3.36.0" - uid fd682f6129e507c00bb4e6319cc5d6b767e36061 resolved "https://codeload.github.com/getsentry/sucrase/tar.gz/fd682f6129e507c00bb4e6319cc5d6b767e36061" dependencies: "@jridgewell/gen-mapping" "^0.3.2"