Skip to content

Commit e849076

Browse files
authored
fix(node): Adjust Express URL parameterization for RegEx routes (#5483)
make our URL parameterization for Express (introduced in #5450) compatible with RegEx-defined routes. Previously, as reported in #5481, our parameterization logic would cause a crash because instead of a string, the matched route would be of type `RegExp`. This PR adjusts our logic so that we detect if we get a matched string our regex. In the latter case, we also append a `'/'` to the reconstructed partial route name so that the regex is closed.
1 parent 174433a commit e849076

File tree

3 files changed

+51
-9
lines changed

3 files changed

+51
-9
lines changed

packages/node-integration-tests/suites/express/tracing/server.ts

+4
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ app.get('/test/express', (_req, res) => {
2121
res.send({ response: 'response 1' });
2222
});
2323

24+
app.get(/\/test\/regex/, (_req, res) => {
25+
res.send({ response: 'response 2' });
26+
});
27+
2428
app.use(Sentry.Handlers.errorHandler());
2529

2630
export default app;

packages/node-integration-tests/suites/express/tracing/test.ts

+26
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,29 @@ test('should create and send transactions for Express routes and spans for middl
2727
],
2828
});
2929
});
30+
31+
test('should set a correct transaction name for routes specified in RegEx', async () => {
32+
const url = await runServer(__dirname, `${__dirname}/server.ts`);
33+
const envelope = await getEnvelopeRequest(`${url}/regex`);
34+
35+
expect(envelope).toHaveLength(3);
36+
37+
assertSentryTransaction(envelope[2], {
38+
transaction: 'GET /\\/test\\/regex/',
39+
transaction_info: {
40+
source: 'route',
41+
},
42+
contexts: {
43+
trace: {
44+
data: {
45+
url: '/test/regex',
46+
},
47+
op: 'http.server',
48+
status: 'ok',
49+
tags: {
50+
'http.status_code': '200',
51+
},
52+
},
53+
},
54+
});
55+
});

packages/tracing/src/integrations/node/express.ts

+21-9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable max-lines */
22
import { Integration, Transaction } from '@sentry/types';
3-
import { CrossPlatformRequest, extractPathForTransaction, logger } from '@sentry/utils';
3+
import { CrossPlatformRequest, extractPathForTransaction, isRegExp, logger } from '@sentry/utils';
44

55
type Method =
66
| 'all'
@@ -55,7 +55,7 @@ type ExpressRouter = Router & {
5555
type Layer = {
5656
match: (path: string) => boolean;
5757
handle_request: (req: PatchedRequest, res: ExpressResponse, next: () => void) => void;
58-
route?: { path: string };
58+
route?: { path: string | RegExp };
5959
path?: string;
6060
};
6161

@@ -273,9 +273,14 @@ function instrumentRouter(appOrRouter: ExpressRouter): void {
273273
req._reconstructedRoute = '';
274274
}
275275

276-
// If the layer's partial route has params, the route is stored in layer.route. Otherwise, the hardcoded path
277-
// (i.e. a partial route without params) is stored in layer.path
278-
const partialRoute = layer.route?.path || layer.path || '';
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);
281+
282+
// Otherwise, the hardcoded path (i.e. a partial route without params) is stored in layer.path
283+
const partialRoute = layerRoutePath || layer.path || '';
279284

280285
// Normalize the partial route so that it doesn't contain leading or trailing slashes
281286
// and exclude empty or '*' wildcard routes.
@@ -288,15 +293,17 @@ function instrumentRouter(appOrRouter: ExpressRouter): void {
288293
.join('/');
289294

290295
// If we found a valid partial URL, we append it to the reconstructed route
291-
if (finalPartialRoute.length > 0) {
292-
req._reconstructedRoute += `/${finalPartialRoute}`;
296+
if (finalPartialRoute && finalPartialRoute.length > 0) {
297+
// If the partial route is from a regex route, we append a '/' to close the regex
298+
req._reconstructedRoute += `/${finalPartialRoute}${isRegex ? '/' : ''}`;
293299
}
294300

295301
// Now we check if we are in the "last" part of the route. We determine this by comparing the
296302
// number of URL segments from the original URL to that of our reconstructed parameterized URL.
297303
// If we've reached our final destination, we update the transaction name.
298-
const urlLength = req.originalUrl?.split('/').filter(s => s.length > 0).length;
299-
const routeLength = req._reconstructedRoute.split('/').filter(s => s.length > 0).length;
304+
const urlLength = getNumberOfUrlSegments(req.originalUrl || '');
305+
const routeLength = getNumberOfUrlSegments(req._reconstructedRoute);
306+
300307
if (urlLength === routeLength) {
301308
const transaction = res.__sentry_transaction;
302309
if (transaction && transaction.metadata.source !== 'custom') {
@@ -311,3 +318,8 @@ function instrumentRouter(appOrRouter: ExpressRouter): void {
311318
return originalProcessParams.call(this, layer, called, req, res, done);
312319
};
313320
}
321+
322+
function getNumberOfUrlSegments(url: string): number {
323+
// split at '/' or at '\/' to split regex urls correctly
324+
return url.split(/\\?\//).filter(s => s.length > 0).length;
325+
}

0 commit comments

Comments
 (0)