1+ import type { ServeOptions } from 'bun' ;
12import type { IntegrationFn , RequestEventData , SpanAttributes } from '@sentry/core' ;
23import {
34 SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD ,
45 SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN ,
56 SEMANTIC_ATTRIBUTE_SENTRY_SOURCE ,
67 captureException ,
7- continueTrace ,
8- defineIntegration ,
9- extractQueryParamsFromUrl ,
10- getSanitizedUrlString ,
11- parseUrl ,
8+ isURLObjectRelative ,
129 setHttpStatus ,
10+ defineIntegration ,
11+ continueTrace ,
1312 startSpan ,
1413 withIsolationScope ,
14+ parseStringToURLObject ,
1515} from '@sentry/core' ;
1616
1717const INTEGRATION_NAME = 'BunServer' ;
@@ -28,6 +28,8 @@ const _bunServerIntegration = (() => {
2828/**
2929 * Instruments `Bun.serve` to automatically create transactions and capture errors.
3030 *
31+ * Does not support instrumenting static routes.
32+ *
3133 * Enabled by default in the Bun SDK.
3234 *
3335 * ```js
@@ -40,10 +42,18 @@ const _bunServerIntegration = (() => {
4042 */
4143export const bunServerIntegration = defineIntegration ( _bunServerIntegration ) ;
4244
45+ let hasPatchedBunServe = false ;
46+
4347/**
4448 * Instruments Bun.serve by patching it's options.
49+ *
50+ * Only exported for tests.
4551 */
4652export function instrumentBunServe ( ) : void {
53+ if ( hasPatchedBunServe ) {
54+ return ;
55+ }
56+
4757 Bun . serve = new Proxy ( Bun . serve , {
4858 apply ( serveTarget , serveThisArg , serveArgs : Parameters < typeof Bun . serve > ) {
4959 instrumentBunServeOptions ( serveArgs [ 0 ] ) ;
@@ -53,89 +63,231 @@ export function instrumentBunServe(): void {
5363 // We can't use a Proxy for this as Bun does `instanceof` checks internally that fail if we
5464 // wrap the Server instance.
5565 const originalReload : typeof server . reload = server . reload . bind ( server ) ;
56- server . reload = ( serveOptions : Parameters < typeof Bun . serve > [ 0 ] ) => {
66+ server . reload = ( serveOptions : ServeOptions ) => {
5767 instrumentBunServeOptions ( serveOptions ) ;
5868 return originalReload ( serveOptions ) ;
5969 } ;
6070
6171 return server ;
6272 } ,
6373 } ) ;
74+
75+ hasPatchedBunServe = true ;
6476}
6577
6678/**
67- * Instruments Bun.serve `fetch` option to automatically create spans and capture errors.
79+ * Instruments Bun.serve options.
80+ *
81+ * @param serveOptions - The options for the Bun.serve function.
6882 */
6983function instrumentBunServeOptions ( serveOptions : Parameters < typeof Bun . serve > [ 0 ] ) : void {
84+ // First handle fetch
85+ instrumentBunServeOptionFetch ( serveOptions ) ;
86+ // then handle routes
87+ instrumentBunServeOptionRoutes ( serveOptions ) ;
88+ }
89+
90+ /**
91+ * Instruments the `fetch` option of Bun.serve.
92+ *
93+ * @param serveOptions - The options for the Bun.serve function.
94+ */
95+ function instrumentBunServeOptionFetch ( serveOptions : Parameters < typeof Bun . serve > [ 0 ] ) : void {
96+ if ( typeof serveOptions . fetch !== 'function' ) {
97+ return ;
98+ }
99+
70100 serveOptions . fetch = new Proxy ( serveOptions . fetch , {
71101 apply ( fetchTarget , fetchThisArg , fetchArgs : Parameters < typeof serveOptions . fetch > ) {
72- return withIsolationScope ( isolationScope => {
73- const request = fetchArgs [ 0 ] ;
74- const upperCaseMethod = request . method . toUpperCase ( ) ;
75- if ( upperCaseMethod === 'OPTIONS' || upperCaseMethod === 'HEAD' ) {
76- return fetchTarget . apply ( fetchThisArg , fetchArgs ) ;
77- }
102+ return wrapRequestHandler ( fetchTarget , fetchThisArg , fetchArgs ) ;
103+ } ,
104+ } ) ;
105+ }
78106
79- const parsedUrl = parseUrl ( request . url ) ;
80- const attributes : SpanAttributes = {
81- [ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN ] : 'auto.http.bun.serve' ,
82- [ SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD ] : request . method || 'GET' ,
83- [ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE ] : 'url' ,
84- } ;
85- if ( parsedUrl . search ) {
86- attributes [ 'http.query' ] = parsedUrl . search ;
87- }
107+ /**
108+ * Instruments the `routes` option of Bun.serve.
109+ *
110+ * @param serveOptions - The options for the Bun.serve function.
111+ */
112+ function instrumentBunServeOptionRoutes ( serveOptions : Parameters < typeof Bun . serve > [ 0 ] ) : void {
113+ if ( ! serveOptions . routes ) {
114+ return ;
115+ }
88116
89- const url = getSanitizedUrlString ( parsedUrl ) ;
90-
91- isolationScope . setSDKProcessingMetadata ( {
92- normalizedRequest : {
93- url,
94- method : request . method ,
95- headers : request . headers . toJSON ( ) ,
96- query_string : extractQueryParamsFromUrl ( url ) ,
97- } satisfies RequestEventData ,
98- } ) ;
99-
100- return continueTrace (
101- { sentryTrace : request . headers . get ( 'sentry-trace' ) || '' , baggage : request . headers . get ( 'baggage' ) } ,
102- ( ) => {
103- return startSpan (
104- {
105- attributes,
106- op : 'http.server' ,
107- name : `${ request . method } ${ parsedUrl . path || '/' } ` ,
108- } ,
109- async span => {
110- try {
111- const response = await ( fetchTarget . apply ( fetchThisArg , fetchArgs ) as ReturnType <
112- typeof serveOptions . fetch
113- > ) ;
114- if ( response ?. status ) {
115- setHttpStatus ( span , response . status ) ;
116- isolationScope . setContext ( 'response' , {
117- headers : response . headers . toJSON ( ) ,
118- status_code : response . status ,
119- } ) ;
120- }
121- return response ;
122- } catch ( e ) {
123- captureException ( e , {
124- mechanism : {
125- type : 'bun' ,
126- handled : false ,
127- data : {
128- function : 'serve' ,
129- } ,
130- } ,
131- } ) ;
132- throw e ;
133- }
117+ if ( typeof serveOptions . routes !== 'object' ) {
118+ return ;
119+ }
120+
121+ Object . keys ( serveOptions . routes ) . forEach ( route => {
122+ const routeHandler = serveOptions . routes [ route ] ;
123+
124+ // Handle route handlers that are an object
125+ if ( typeof routeHandler === 'function' ) {
126+ serveOptions . routes [ route ] = new Proxy ( routeHandler , {
127+ apply : ( routeHandlerTarget , routeHandlerThisArg , routeHandlerArgs : Parameters < typeof routeHandler > ) => {
128+ return wrapRequestHandler ( routeHandlerTarget , routeHandlerThisArg , routeHandlerArgs , route ) ;
129+ } ,
130+ } ) ;
131+ }
132+
133+ // Static routes are not instrumented
134+ if ( routeHandler instanceof Response ) {
135+ return ;
136+ }
137+
138+ // Handle the route handlers that are an object. This means they define a route handler for each method.
139+ if ( typeof routeHandler === 'object' ) {
140+ Object . entries ( routeHandler ) . forEach ( ( [ routeHandlerObjectHandlerKey , routeHandlerObjectHandler ] ) => {
141+ if ( typeof routeHandlerObjectHandler === 'function' ) {
142+ ( serveOptions . routes [ route ] as Record < string , RouteHandler > ) [ routeHandlerObjectHandlerKey ] = new Proxy (
143+ routeHandlerObjectHandler ,
144+ {
145+ apply : (
146+ routeHandlerObjectHandlerTarget ,
147+ routeHandlerObjectHandlerThisArg ,
148+ routeHandlerObjectHandlerArgs : Parameters < typeof routeHandlerObjectHandler > ,
149+ ) => {
150+ return wrapRequestHandler (
151+ routeHandlerObjectHandlerTarget ,
152+ routeHandlerObjectHandlerThisArg ,
153+ routeHandlerObjectHandlerArgs ,
154+ route ,
155+ ) ;
134156 } ,
135- ) ;
136- } ,
137- ) ;
157+ } ,
158+ ) ;
159+ }
138160 } ) ;
139- } ,
161+ }
140162 } ) ;
141163}
164+
165+ type RouteHandler = Extract <
166+ NonNullable < Parameters < typeof Bun . serve > [ 0 ] [ 'routes' ] > [ string ] ,
167+ // eslint-disable-next-line @typescript-eslint/ban-types
168+ Function
169+ > ;
170+
171+ function wrapRequestHandler < T extends RouteHandler = RouteHandler > (
172+ target : T ,
173+ thisArg : unknown ,
174+ args : Parameters < T > ,
175+ route ?: string ,
176+ ) : ReturnType < T > {
177+ return withIsolationScope ( isolationScope => {
178+ const request = args [ 0 ] ;
179+ const upperCaseMethod = request . method . toUpperCase ( ) ;
180+ if ( upperCaseMethod === 'OPTIONS' || upperCaseMethod === 'HEAD' ) {
181+ return target . apply ( thisArg , args ) ;
182+ }
183+
184+ const parsedUrl = parseStringToURLObject ( request . url ) ;
185+ const attributes = getSpanAttributesFromParsedUrl ( parsedUrl , request ) ;
186+
187+ let routeName = parsedUrl ?. pathname || '/' ;
188+ if ( request . params ) {
189+ Object . keys ( request . params ) . forEach ( key => {
190+ attributes [ `url.path.parameter.${ key } ` ] = ( request . params as Record < string , string > ) [ key ] ;
191+ } ) ;
192+
193+ // If a route has parameters, it's a parameterized route
194+ if ( route ) {
195+ attributes [ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE ] = 'route' ;
196+ attributes [ 'url.template' ] = route ;
197+ routeName = route ;
198+ }
199+ }
200+
201+ // Handle wildcard routes
202+ if ( route ?. endsWith ( '/*' ) ) {
203+ attributes [ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE ] = 'route' ;
204+ attributes [ 'url.template' ] = route ;
205+ routeName = route ;
206+ }
207+
208+ isolationScope . setSDKProcessingMetadata ( {
209+ normalizedRequest : {
210+ url : request . url ,
211+ method : request . method ,
212+ headers : request . headers . toJSON ( ) ,
213+ query_string : parsedUrl ?. search ,
214+ } satisfies RequestEventData ,
215+ } ) ;
216+
217+ return continueTrace (
218+ {
219+ sentryTrace : request . headers . get ( 'sentry-trace' ) ?? '' ,
220+ baggage : request . headers . get ( 'baggage' ) ,
221+ } ,
222+ ( ) =>
223+ startSpan (
224+ {
225+ attributes,
226+ op : 'http.server' ,
227+ name : `${ request . method } ${ routeName } ` ,
228+ } ,
229+ async span => {
230+ try {
231+ const response = ( await target . apply ( thisArg , args ) ) as Response | undefined ;
232+ if ( response ?. status ) {
233+ setHttpStatus ( span , response . status ) ;
234+ isolationScope . setContext ( 'response' , {
235+ headers : response . headers . toJSON ( ) ,
236+ status_code : response . status ,
237+ } ) ;
238+ }
239+ return response ;
240+ } catch ( e ) {
241+ captureException ( e , {
242+ mechanism : {
243+ type : 'bun' ,
244+ handled : false ,
245+ data : {
246+ function : 'serve' ,
247+ } ,
248+ } ,
249+ } ) ;
250+ throw e ;
251+ }
252+ } ,
253+ ) ,
254+ ) ;
255+ } ) ;
256+ }
257+
258+ function getSpanAttributesFromParsedUrl (
259+ parsedUrl : ReturnType < typeof parseStringToURLObject > ,
260+ request : Request ,
261+ ) : SpanAttributes {
262+ const attributes : SpanAttributes = {
263+ [ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN ] : 'auto.http.bun.serve' ,
264+ [ SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD ] : request . method || 'GET' ,
265+ [ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE ] : 'url' ,
266+ } ;
267+
268+ if ( parsedUrl ) {
269+ if ( parsedUrl . search ) {
270+ attributes [ 'url.query' ] = parsedUrl . search ;
271+ }
272+ if ( parsedUrl . hash ) {
273+ attributes [ 'url.fragment' ] = parsedUrl . hash ;
274+ }
275+ if ( parsedUrl . pathname ) {
276+ attributes [ 'url.path' ] = parsedUrl . pathname ;
277+ }
278+ if ( ! isURLObjectRelative ( parsedUrl ) ) {
279+ attributes [ 'url.full' ] = parsedUrl . href ;
280+ if ( parsedUrl . port ) {
281+ attributes [ 'url.port' ] = parsedUrl . port ;
282+ }
283+ if ( parsedUrl . protocol ) {
284+ attributes [ 'url.scheme' ] = parsedUrl . protocol ;
285+ }
286+ if ( parsedUrl . hostname ) {
287+ attributes [ 'url.domain' ] = parsedUrl . hostname ;
288+ }
289+ }
290+ }
291+
292+ return attributes ;
293+ }
0 commit comments