@@ -36,7 +36,7 @@ type Router = {
36
36
/* Extend the CrossPlatformRequest type with a patched parameter to build a reconstructed route */
37
37
type PatchedRequest = CrossPlatformRequest & { _reconstructedRoute ?: string } ;
38
38
39
- /* Type used for pathing the express router prototype */
39
+ /* Types used for patching the express router prototype */
40
40
type ExpressRouter = Router & {
41
41
_router ?: ExpressRouter ;
42
42
stack ?: Layer [ ] ;
@@ -51,14 +51,15 @@ type ExpressRouter = Router & {
51
51
) => unknown ;
52
52
} ;
53
53
54
- /* Type used for pathing the express router prototype */
55
54
type Layer = {
56
55
match : ( path : string ) => boolean ;
57
56
handle_request : ( req : PatchedRequest , res : ExpressResponse , next : ( ) => void ) => void ;
58
- route ?: { path : string | RegExp } ;
57
+ route ?: { path : RouteType | RouteType [ ] } ;
59
58
path ?: string ;
60
59
} ;
61
60
61
+ type RouteType = string | RegExp ;
62
+
62
63
interface ExpressResponse {
63
64
once ( name : string , callback : ( ) => void ) : void ;
64
65
}
@@ -273,11 +274,8 @@ function instrumentRouter(appOrRouter: ExpressRouter): void {
273
274
req . _reconstructedRoute = '' ;
274
275
}
275
276
276
- // If the layer's partial route has params, the route is stored in layer.route.
277
- // Since a route might be defined with a RegExp, we convert it toString to make sure we end up with a string
278
- const lrp = layer . route ?. path ;
279
- const isRegex = isRegExp ( lrp ) ;
280
- const layerRoutePath = isRegex ? lrp ?. toString ( ) : ( lrp as string ) ;
277
+ // If the layer's partial route has params, is a regex or an array, the route is stored in layer.route.
278
+ const { layerRoutePath, isRegex, isArray, numExtraSegments } : LayerRoutePathInfo = getLayerRoutePathInfo ( layer ) ;
281
279
282
280
// Otherwise, the hardcoded path (i.e. a partial route without params) is stored in layer.path
283
281
const partialRoute = layerRoutePath || layer . path || '' ;
@@ -289,7 +287,7 @@ function instrumentRouter(appOrRouter: ExpressRouter): void {
289
287
// We want to end up with the parameterized URL of the incoming request without any extraneous path segments.
290
288
const finalPartialRoute = partialRoute
291
289
. split ( '/' )
292
- . filter ( segment => segment . length > 0 && ! segment . includes ( '*' ) )
290
+ . filter ( segment => segment . length > 0 && ( isRegex || isArray || ! segment . includes ( '*' ) ) )
293
291
. join ( '/' ) ;
294
292
295
293
// If we found a valid partial URL, we append it to the reconstructed route
@@ -301,7 +299,7 @@ function instrumentRouter(appOrRouter: ExpressRouter): void {
301
299
// Now we check if we are in the "last" part of the route. We determine this by comparing the
302
300
// number of URL segments from the original URL to that of our reconstructed parameterized URL.
303
301
// If we've reached our final destination, we update the transaction name.
304
- const urlLength = getNumberOfUrlSegments ( req . originalUrl || '' ) ;
302
+ const urlLength = getNumberOfUrlSegments ( req . originalUrl || '' ) + numExtraSegments ;
305
303
const routeLength = getNumberOfUrlSegments ( req . _reconstructedRoute ) ;
306
304
307
305
if ( urlLength === routeLength ) {
@@ -319,7 +317,73 @@ function instrumentRouter(appOrRouter: ExpressRouter): void {
319
317
} ;
320
318
}
321
319
320
+ type LayerRoutePathInfo = {
321
+ layerRoutePath ?: string ;
322
+ isRegex : boolean ;
323
+ isArray : boolean ;
324
+ numExtraSegments : number ;
325
+ } ;
326
+
327
+ /**
328
+ * Extracts and stringifies the layer's route which can either be a string with parameters (`users/:id`),
329
+ * a RegEx (`/test/`) or an array of strings and regexes (`['/path1', /\/path[2-5]/, /path/:id]`). Additionally
330
+ * returns extra information about the route, such as if the route is defined as regex or as an array.
331
+ *
332
+ * @param layer the layer to extract the stringified route from
333
+ *
334
+ * @returns an object containing the stringified route, a flag determining if the route was a regex
335
+ * and the number of extra segments to the matched path that are additionally in the route,
336
+ * if the route was an array (defaults to 0).
337
+ */
338
+ function getLayerRoutePathInfo ( layer : Layer ) : LayerRoutePathInfo {
339
+ const lrp = layer . route ?. path ;
340
+
341
+ const isRegex = isRegExp ( lrp ) ;
342
+ const isArray = Array . isArray ( lrp ) ;
343
+
344
+ if ( ! lrp ) {
345
+ return { isRegex, isArray, numExtraSegments : 0 } ;
346
+ }
347
+
348
+ const numExtraSegments = isArray
349
+ ? Math . max ( getNumberOfArrayUrlSegments ( lrp as RouteType [ ] ) - getNumberOfUrlSegments ( layer . path || '' ) , 0 )
350
+ : 0 ;
351
+
352
+ const layerRoutePath = getLayerRoutePathString ( isArray , lrp ) ;
353
+
354
+ return { layerRoutePath, isRegex, isArray, numExtraSegments } ;
355
+ }
356
+
357
+ /**
358
+ * Returns the number of URL segments in an array of routes
359
+ *
360
+ * Example: ['/api/test', /\/api\/post[0-9]/, '/users/:id/details`] -> 7
361
+ */
362
+ function getNumberOfArrayUrlSegments ( routesArray : RouteType [ ] ) : number {
363
+ return routesArray . reduce ( ( accNumSegments : number , currentRoute : RouteType ) => {
364
+ // array members can be a RegEx -> convert them toString
365
+ return accNumSegments + getNumberOfUrlSegments ( currentRoute . toString ( ) ) ;
366
+ } , 0 ) ;
367
+ }
368
+
369
+ /**
370
+ * Returns number of URL segments of a passed URL.
371
+ * Also handles URLs of type RegExp
372
+ */
322
373
function getNumberOfUrlSegments ( url : string ) : number {
323
374
// split at '/' or at '\/' to split regex urls correctly
324
- return url . split ( / \\ ? \/ / ) . filter ( s => s . length > 0 ) . length ;
375
+ return url . split ( / \\ ? \/ / ) . filter ( s => s . length > 0 && s !== ',' ) . length ;
376
+ }
377
+
378
+ /**
379
+ * Extracts and returns the stringified version of the layers route path
380
+ * Handles route arrays (by joining the paths together) as well as RegExp and normal
381
+ * string values (in the latter case the toString conversion is technically unnecessary but
382
+ * it doesn't hurt us either).
383
+ */
384
+ function getLayerRoutePathString ( isArray : boolean , lrp ?: RouteType | RouteType [ ] ) : string | undefined {
385
+ if ( isArray ) {
386
+ return ( lrp as RouteType [ ] ) . map ( r => r . toString ( ) ) . join ( ',' ) ;
387
+ }
388
+ return lrp && lrp . toString ( ) ;
325
389
}
0 commit comments