@@ -36,7 +36,7 @@ type Router = {
3636/* Extend the CrossPlatformRequest type with a patched parameter to build a reconstructed route */
3737type PatchedRequest = CrossPlatformRequest & { _reconstructedRoute ?: string } ;
3838
39- /* Type used for pathing the express router prototype */
39+ /* Types used for patching the express router prototype */
4040type ExpressRouter = Router & {
4141 _router ?: ExpressRouter ;
4242 stack ?: Layer [ ] ;
@@ -51,14 +51,15 @@ type ExpressRouter = Router & {
5151 ) => unknown ;
5252} ;
5353
54- /* Type used for pathing the express router prototype */
5554type Layer = {
5655 match : ( path : string ) => boolean ;
5756 handle_request : ( req : PatchedRequest , res : ExpressResponse , next : ( ) => void ) => void ;
58- route ?: { path : string | RegExp } ;
57+ route ?: { path : RouteType | RouteType [ ] } ;
5958 path ?: string ;
6059} ;
6160
61+ type RouteType = string | RegExp ;
62+
6263interface ExpressResponse {
6364 once ( name : string , callback : ( ) => void ) : void ;
6465}
@@ -273,11 +274,8 @@ function instrumentRouter(appOrRouter: ExpressRouter): void {
273274 req . _reconstructedRoute = '' ;
274275 }
275276
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 ) ;
281279
282280 // Otherwise, the hardcoded path (i.e. a partial route without params) is stored in layer.path
283281 const partialRoute = layerRoutePath || layer . path || '' ;
@@ -289,7 +287,7 @@ function instrumentRouter(appOrRouter: ExpressRouter): void {
289287 // We want to end up with the parameterized URL of the incoming request without any extraneous path segments.
290288 const finalPartialRoute = partialRoute
291289 . split ( '/' )
292- . filter ( segment => segment . length > 0 && ! segment . includes ( '*' ) )
290+ . filter ( segment => segment . length > 0 && ( isRegex || isArray || ! segment . includes ( '*' ) ) )
293291 . join ( '/' ) ;
294292
295293 // If we found a valid partial URL, we append it to the reconstructed route
@@ -301,7 +299,7 @@ function instrumentRouter(appOrRouter: ExpressRouter): void {
301299 // Now we check if we are in the "last" part of the route. We determine this by comparing the
302300 // number of URL segments from the original URL to that of our reconstructed parameterized URL.
303301 // 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 ;
305303 const routeLength = getNumberOfUrlSegments ( req . _reconstructedRoute ) ;
306304
307305 if ( urlLength === routeLength ) {
@@ -319,7 +317,73 @@ function instrumentRouter(appOrRouter: ExpressRouter): void {
319317 } ;
320318}
321319
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+ */
322373function getNumberOfUrlSegments ( url : string ) : number {
323374 // 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 ( ) ;
325389}
0 commit comments