1
+ import type { ServeOptions } from 'bun' ;
1
2
import type { IntegrationFn , RequestEventData , SpanAttributes } from '@sentry/core' ;
2
3
import {
3
4
SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD ,
4
5
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN ,
5
6
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE ,
6
7
captureException ,
7
- continueTrace ,
8
- defineIntegration ,
9
- extractQueryParamsFromUrl ,
10
- getSanitizedUrlString ,
11
- parseUrl ,
8
+ isURLObjectRelative ,
12
9
setHttpStatus ,
10
+ defineIntegration ,
11
+ continueTrace ,
13
12
startSpan ,
14
13
withIsolationScope ,
14
+ parseStringToURLObject ,
15
15
} from '@sentry/core' ;
16
16
17
17
const INTEGRATION_NAME = 'BunServer' ;
@@ -28,6 +28,8 @@ const _bunServerIntegration = (() => {
28
28
/**
29
29
* Instruments `Bun.serve` to automatically create transactions and capture errors.
30
30
*
31
+ * Does not support instrumenting static routes.
32
+ *
31
33
* Enabled by default in the Bun SDK.
32
34
*
33
35
* ```js
@@ -40,10 +42,18 @@ const _bunServerIntegration = (() => {
40
42
*/
41
43
export const bunServerIntegration = defineIntegration ( _bunServerIntegration ) ;
42
44
45
+ let hasPatchedBunServe = false ;
46
+
43
47
/**
44
48
* Instruments Bun.serve by patching it's options.
49
+ *
50
+ * Only exported for tests.
45
51
*/
46
52
export function instrumentBunServe ( ) : void {
53
+ if ( hasPatchedBunServe ) {
54
+ return ;
55
+ }
56
+
47
57
Bun . serve = new Proxy ( Bun . serve , {
48
58
apply ( serveTarget , serveThisArg , serveArgs : Parameters < typeof Bun . serve > ) {
49
59
instrumentBunServeOptions ( serveArgs [ 0 ] ) ;
@@ -53,89 +63,231 @@ export function instrumentBunServe(): void {
53
63
// We can't use a Proxy for this as Bun does `instanceof` checks internally that fail if we
54
64
// wrap the Server instance.
55
65
const originalReload : typeof server . reload = server . reload . bind ( server ) ;
56
- server . reload = ( serveOptions : Parameters < typeof Bun . serve > [ 0 ] ) => {
66
+ server . reload = ( serveOptions : ServeOptions ) => {
57
67
instrumentBunServeOptions ( serveOptions ) ;
58
68
return originalReload ( serveOptions ) ;
59
69
} ;
60
70
61
71
return server ;
62
72
} ,
63
73
} ) ;
74
+
75
+ hasPatchedBunServe = true ;
64
76
}
65
77
66
78
/**
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.
68
82
*/
69
83
function 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
+
70
100
serveOptions . fetch = new Proxy ( serveOptions . fetch , {
71
101
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
+ }
78
106
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
+ }
88
116
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
+ ) ;
134
156
} ,
135
- ) ;
136
- } ,
137
- ) ;
157
+ } ,
158
+ ) ;
159
+ }
138
160
} ) ;
139
- } ,
161
+ }
140
162
} ) ;
141
163
}
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