Skip to content

Commit 72c6855

Browse files
authored
fix(express): Support multiple routers with common paths. (#6253)
Handles the case when _at the end of reconstruction_ the original URL and the reconstructed URL don't match and there are no parameters.
1 parent daacd38 commit 72c6855

File tree

16 files changed

+329
-1
lines changed

16 files changed

+329
-1
lines changed

packages/node-integration-tests/.eslintrc.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ module.exports = {
1818
project: ['tsconfig.test.json'],
1919
sourceType: 'module',
2020
},
21+
rules: {
22+
'@typescript-eslint/typedef': 'off',
23+
},
2124
},
2225
],
2326
};
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import * as Sentry from '@sentry/node';
2+
import * as Tracing from '@sentry/tracing';
3+
import cors from 'cors';
4+
import express from 'express';
5+
6+
const app = express();
7+
8+
Sentry.init({
9+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
10+
release: '1.0',
11+
integrations: [new Sentry.Integrations.Http({ tracing: true }), new Tracing.Integrations.Express({ app })],
12+
tracesSampleRate: 1.0,
13+
});
14+
15+
app.use(Sentry.Handlers.requestHandler());
16+
app.use(Sentry.Handlers.tracingHandler());
17+
18+
app.use(cors());
19+
20+
const APIv1 = express.Router();
21+
22+
APIv1.get('/user/:userId', function (_req, res) {
23+
Sentry.captureMessage('Custom Message');
24+
res.send('Success');
25+
});
26+
27+
const root = express.Router();
28+
29+
app.use('/api2/v1', root);
30+
app.use('/api/v1', APIv1);
31+
32+
app.use(Sentry.Handlers.errorHandler());
33+
34+
export default app;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { assertSentryEvent, TestEnv } from '../../../../utils/index';
2+
3+
test('should construct correct url with common infixes with multiple parameterized routers.', async () => {
4+
const env = await TestEnv.init(__dirname, `${__dirname}/server.ts`);
5+
const event = await env.getEnvelopeRequest({ url: env.url.replace('test', 'api/v1/user/3212') });
6+
7+
assertSentryEvent(event[2] as any, {
8+
message: 'Custom Message',
9+
transaction: 'GET /api/v1/user/:userId',
10+
});
11+
});
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import * as Sentry from '@sentry/node';
2+
import * as Tracing from '@sentry/tracing';
3+
import cors from 'cors';
4+
import express from 'express';
5+
6+
const app = express();
7+
8+
Sentry.init({
9+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
10+
release: '1.0',
11+
integrations: [new Sentry.Integrations.Http({ tracing: true }), new Tracing.Integrations.Express({ app })],
12+
tracesSampleRate: 1.0,
13+
});
14+
15+
app.use(Sentry.Handlers.requestHandler());
16+
app.use(Sentry.Handlers.tracingHandler());
17+
18+
app.use(cors());
19+
20+
const APIv1 = express.Router();
21+
22+
APIv1.get('/test', function (_req, res) {
23+
Sentry.captureMessage('Custom Message');
24+
res.send('Success');
25+
});
26+
27+
const root = express.Router();
28+
29+
app.use('/api/v1', root);
30+
app.use('/api2/v1', APIv1);
31+
32+
app.use(Sentry.Handlers.errorHandler());
33+
34+
export default app;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { assertSentryEvent, TestEnv } from '../../../../utils/index';
2+
3+
test('should construct correct url with common infixes with multiple routers.', async () => {
4+
const env = await TestEnv.init(__dirname, `${__dirname}/server.ts`);
5+
const event = await env.getEnvelopeRequest({ url: env.url.replace('test', 'api2/v1/test/') });
6+
7+
assertSentryEvent(event[2] as any, {
8+
message: 'Custom Message',
9+
transaction: 'GET /api2/v1/test',
10+
});
11+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import * as Sentry from '@sentry/node';
2+
import * as Tracing from '@sentry/tracing';
3+
import cors from 'cors';
4+
import express from 'express';
5+
6+
const app = express();
7+
8+
Sentry.init({
9+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
10+
release: '1.0',
11+
integrations: [new Sentry.Integrations.Http({ tracing: true }), new Tracing.Integrations.Express({ app })],
12+
tracesSampleRate: 1.0,
13+
});
14+
15+
app.use(Sentry.Handlers.requestHandler());
16+
app.use(Sentry.Handlers.tracingHandler());
17+
18+
app.use(cors());
19+
20+
const APIv1 = express.Router();
21+
22+
APIv1.get('/user/:userId', function (_req, res) {
23+
Sentry.captureMessage('Custom Message');
24+
res.send('Success');
25+
});
26+
27+
const root = express.Router();
28+
29+
app.use('/api/v1', APIv1);
30+
app.use('/api', root);
31+
32+
app.use(Sentry.Handlers.errorHandler());
33+
34+
export default app;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { assertSentryEvent, TestEnv } from '../../../../utils/index';
2+
3+
test('should construct correct urls with multiple parameterized routers (use order reversed).', async () => {
4+
const env = await TestEnv.init(__dirname, `${__dirname}/server.ts`);
5+
const event = await env.getEnvelopeRequest({ url: env.url.replace('test', 'api/v1/user/1234/') });
6+
7+
assertSentryEvent(event[2] as any, {
8+
message: 'Custom Message',
9+
transaction: 'GET /api/v1/user/:userId',
10+
});
11+
});
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import * as Sentry from '@sentry/node';
2+
import * as Tracing from '@sentry/tracing';
3+
import cors from 'cors';
4+
import express from 'express';
5+
6+
const app = express();
7+
8+
Sentry.init({
9+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
10+
release: '1.0',
11+
integrations: [new Sentry.Integrations.Http({ tracing: true }), new Tracing.Integrations.Express({ app })],
12+
tracesSampleRate: 1.0,
13+
});
14+
15+
app.use(Sentry.Handlers.requestHandler());
16+
app.use(Sentry.Handlers.tracingHandler());
17+
18+
app.use(cors());
19+
20+
const APIv1 = express.Router();
21+
22+
APIv1.get('/user/:userId', function (_req, res) {
23+
Sentry.captureMessage('Custom Message');
24+
res.send('Success');
25+
});
26+
27+
const root = express.Router();
28+
29+
app.use('/api', root);
30+
app.use('/api/v1', APIv1);
31+
32+
app.use(Sentry.Handlers.errorHandler());
33+
34+
export default app;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { assertSentryEvent, TestEnv } from '../../../../utils/index';
2+
3+
test('should construct correct urls with multiple parameterized routers.', async () => {
4+
const env = await TestEnv.init(__dirname, `${__dirname}/server.ts`);
5+
const event = await env.getEnvelopeRequest({ url: env.url.replace('test', 'api/v1/user/1234/') });
6+
7+
assertSentryEvent(event[2] as any, {
8+
message: 'Custom Message',
9+
transaction: 'GET /api/v1/user/:userId',
10+
});
11+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import * as Sentry from '@sentry/node';
2+
import * as Tracing from '@sentry/tracing';
3+
import cors from 'cors';
4+
import express from 'express';
5+
6+
const app = express();
7+
8+
Sentry.init({
9+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
10+
release: '1.0',
11+
integrations: [new Sentry.Integrations.Http({ tracing: true }), new Tracing.Integrations.Express({ app })],
12+
tracesSampleRate: 1.0,
13+
});
14+
15+
app.use(Sentry.Handlers.requestHandler());
16+
app.use(Sentry.Handlers.tracingHandler());
17+
18+
app.use(cors());
19+
20+
const APIv1 = express.Router();
21+
22+
APIv1.get('/:userId', function (_req, res) {
23+
Sentry.captureMessage('Custom Message');
24+
res.send('Success');
25+
});
26+
27+
const root = express.Router();
28+
29+
app.use('/api/v1', APIv1);
30+
app.use('/api', root);
31+
32+
app.use(Sentry.Handlers.errorHandler());
33+
34+
export default app;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { assertSentryEvent, TestEnv } from '../../../../utils/index';
2+
3+
test('should construct correct url with multiple parameterized routers of the same length (use order reversed).', async () => {
4+
const env = await TestEnv.init(__dirname, `${__dirname}/server.ts`);
5+
const event = await env.getEnvelopeRequest({ url: env.url.replace('test', 'api/v1/1234/') });
6+
7+
assertSentryEvent(event[2] as any, {
8+
message: 'Custom Message',
9+
transaction: 'GET /api/v1/:userId',
10+
});
11+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import * as Sentry from '@sentry/node';
2+
import * as Tracing from '@sentry/tracing';
3+
import cors from 'cors';
4+
import express from 'express';
5+
6+
const app = express();
7+
8+
Sentry.init({
9+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
10+
release: '1.0',
11+
integrations: [new Sentry.Integrations.Http({ tracing: true }), new Tracing.Integrations.Express({ app })],
12+
tracesSampleRate: 1.0,
13+
});
14+
15+
app.use(Sentry.Handlers.requestHandler());
16+
app.use(Sentry.Handlers.tracingHandler());
17+
18+
app.use(cors());
19+
20+
const APIv1 = express.Router();
21+
22+
APIv1.get('/:userId', function (_req, res) {
23+
Sentry.captureMessage('Custom Message');
24+
res.send('Success');
25+
});
26+
27+
const root = express.Router();
28+
29+
app.use('/api', root);
30+
app.use('/api/v1', APIv1);
31+
32+
app.use(Sentry.Handlers.errorHandler());
33+
34+
export default app;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { assertSentryEvent, TestEnv } from '../../../../utils/index';
2+
3+
test('should construct correct url with multiple parameterized routers of the same length.', async () => {
4+
const env = await TestEnv.init(__dirname, `${__dirname}/server.ts`);
5+
const event = await env.getEnvelopeRequest({ url: env.url.replace('test', 'api/v1/1234/') });
6+
7+
assertSentryEvent(event[2] as any, {
8+
message: 'Custom Message',
9+
transaction: 'GET /api/v1/:userId',
10+
});
11+
});
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import * as Sentry from '@sentry/node';
2+
import * as Tracing from '@sentry/tracing';
3+
import cors from 'cors';
4+
import express from 'express';
5+
6+
const app = express();
7+
8+
Sentry.init({
9+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
10+
release: '1.0',
11+
integrations: [new Sentry.Integrations.Http({ tracing: true }), new Tracing.Integrations.Express({ app })],
12+
tracesSampleRate: 1.0,
13+
});
14+
15+
app.use(Sentry.Handlers.requestHandler());
16+
app.use(Sentry.Handlers.tracingHandler());
17+
18+
app.use(cors());
19+
20+
const APIv1 = express.Router();
21+
22+
APIv1.get('/test', function (_req, res) {
23+
Sentry.captureMessage('Custom Message');
24+
res.send('Success');
25+
});
26+
27+
const root = express.Router();
28+
29+
app.use('/api', root);
30+
app.use('/api/v1', APIv1);
31+
32+
app.use(Sentry.Handlers.errorHandler());
33+
34+
export default app;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { assertSentryEvent, TestEnv } from '../../../../utils/index';
2+
3+
test('should construct correct urls with multiple routers.', async () => {
4+
const env = await TestEnv.init(__dirname, `${__dirname}/server.ts`);
5+
const event = await env.getEnvelopeRequest({ url: env.url.replace('test', 'api/v1/test/') });
6+
7+
assertSentryEvent(event[2] as any, {
8+
message: 'Custom Message',
9+
transaction: 'GET /api/v1/test',
10+
});
11+
});

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ type Router = {
3636
};
3737

3838
/* Extend the PolymorphicRequest type with a patched parameter to build a reconstructed route */
39-
type PatchedRequest = PolymorphicRequest & { _reconstructedRoute?: string };
39+
type PatchedRequest = PolymorphicRequest & { _reconstructedRoute?: string; _hasParameters?: boolean };
4040

4141
/* Types used for patching the express router prototype */
4242
type ExpressRouter = Router & {
@@ -303,6 +303,10 @@ function instrumentRouter(appOrRouter: ExpressRouter): void {
303303
// If the layer's partial route has params, is a regex or an array, the route is stored in layer.route.
304304
const { layerRoutePath, isRegex, isArray, numExtraSegments }: LayerRoutePathInfo = getLayerRoutePathInfo(layer);
305305

306+
if (layerRoutePath || isRegex || isArray) {
307+
req._hasParameters = true;
308+
}
309+
306310
// Otherwise, the hardcoded path (i.e. a partial route without params) is stored in layer.path
307311
const partialRoute = layerRoutePath || layer.path || '';
308312

@@ -329,6 +333,12 @@ function instrumentRouter(appOrRouter: ExpressRouter): void {
329333
const routeLength = getNumberOfUrlSegments(req._reconstructedRoute);
330334

331335
if (urlLength === routeLength) {
336+
if (!req._hasParameters) {
337+
if (req._reconstructedRoute !== req.originalUrl) {
338+
req._reconstructedRoute = req.originalUrl;
339+
}
340+
}
341+
332342
const transaction = res.__sentry_transaction;
333343
if (transaction && transaction.metadata.source !== 'custom') {
334344
// If the request URL is '/' or empty, the reconstructed route will be empty.

0 commit comments

Comments
 (0)