Skip to content

Commit

Permalink
fix(express): Support multiple routers with common paths. (#6253)
Browse files Browse the repository at this point in the history
Handles the case when _at the end of reconstruction_ the original URL and the reconstructed URL don't match and there are no parameters.
  • Loading branch information
onurtemizkan committed Nov 22, 2022
1 parent daacd38 commit 72c6855
Show file tree
Hide file tree
Showing 16 changed files with 329 additions and 1 deletion.
3 changes: 3 additions & 0 deletions packages/node-integration-tests/.eslintrc.js
Expand Up @@ -18,6 +18,9 @@ module.exports = {
project: ['tsconfig.test.json'],
sourceType: 'module',
},
rules: {
'@typescript-eslint/typedef': 'off',
},
},
],
};
@@ -0,0 +1,34 @@
import * as Sentry from '@sentry/node';
import * as Tracing from '@sentry/tracing';
import cors from 'cors';
import express from 'express';

const app = express();

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
integrations: [new Sentry.Integrations.Http({ tracing: true }), new Tracing.Integrations.Express({ app })],
tracesSampleRate: 1.0,
});

app.use(Sentry.Handlers.requestHandler());
app.use(Sentry.Handlers.tracingHandler());

app.use(cors());

const APIv1 = express.Router();

APIv1.get('/user/:userId', function (_req, res) {
Sentry.captureMessage('Custom Message');
res.send('Success');
});

const root = express.Router();

app.use('/api2/v1', root);
app.use('/api/v1', APIv1);

app.use(Sentry.Handlers.errorHandler());

export default app;
@@ -0,0 +1,11 @@
import { assertSentryEvent, TestEnv } from '../../../../utils/index';

test('should construct correct url with common infixes with multiple parameterized routers.', async () => {
const env = await TestEnv.init(__dirname, `${__dirname}/server.ts`);
const event = await env.getEnvelopeRequest({ url: env.url.replace('test', 'api/v1/user/3212') });

assertSentryEvent(event[2] as any, {
message: 'Custom Message',
transaction: 'GET /api/v1/user/:userId',
});
});
@@ -0,0 +1,34 @@
import * as Sentry from '@sentry/node';
import * as Tracing from '@sentry/tracing';
import cors from 'cors';
import express from 'express';

const app = express();

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
integrations: [new Sentry.Integrations.Http({ tracing: true }), new Tracing.Integrations.Express({ app })],
tracesSampleRate: 1.0,
});

app.use(Sentry.Handlers.requestHandler());
app.use(Sentry.Handlers.tracingHandler());

app.use(cors());

const APIv1 = express.Router();

APIv1.get('/test', function (_req, res) {
Sentry.captureMessage('Custom Message');
res.send('Success');
});

const root = express.Router();

app.use('/api/v1', root);
app.use('/api2/v1', APIv1);

app.use(Sentry.Handlers.errorHandler());

export default app;
@@ -0,0 +1,11 @@
import { assertSentryEvent, TestEnv } from '../../../../utils/index';

test('should construct correct url with common infixes with multiple routers.', async () => {
const env = await TestEnv.init(__dirname, `${__dirname}/server.ts`);
const event = await env.getEnvelopeRequest({ url: env.url.replace('test', 'api2/v1/test/') });

assertSentryEvent(event[2] as any, {
message: 'Custom Message',
transaction: 'GET /api2/v1/test',
});
});
@@ -0,0 +1,34 @@
import * as Sentry from '@sentry/node';
import * as Tracing from '@sentry/tracing';
import cors from 'cors';
import express from 'express';

const app = express();

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
integrations: [new Sentry.Integrations.Http({ tracing: true }), new Tracing.Integrations.Express({ app })],
tracesSampleRate: 1.0,
});

app.use(Sentry.Handlers.requestHandler());
app.use(Sentry.Handlers.tracingHandler());

app.use(cors());

const APIv1 = express.Router();

APIv1.get('/user/:userId', function (_req, res) {
Sentry.captureMessage('Custom Message');
res.send('Success');
});

const root = express.Router();

app.use('/api/v1', APIv1);
app.use('/api', root);

app.use(Sentry.Handlers.errorHandler());

export default app;
@@ -0,0 +1,11 @@
import { assertSentryEvent, TestEnv } from '../../../../utils/index';

test('should construct correct urls with multiple parameterized routers (use order reversed).', async () => {
const env = await TestEnv.init(__dirname, `${__dirname}/server.ts`);
const event = await env.getEnvelopeRequest({ url: env.url.replace('test', 'api/v1/user/1234/') });

assertSentryEvent(event[2] as any, {
message: 'Custom Message',
transaction: 'GET /api/v1/user/:userId',
});
});
@@ -0,0 +1,34 @@
import * as Sentry from '@sentry/node';
import * as Tracing from '@sentry/tracing';
import cors from 'cors';
import express from 'express';

const app = express();

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
integrations: [new Sentry.Integrations.Http({ tracing: true }), new Tracing.Integrations.Express({ app })],
tracesSampleRate: 1.0,
});

app.use(Sentry.Handlers.requestHandler());
app.use(Sentry.Handlers.tracingHandler());

app.use(cors());

const APIv1 = express.Router();

APIv1.get('/user/:userId', function (_req, res) {
Sentry.captureMessage('Custom Message');
res.send('Success');
});

const root = express.Router();

app.use('/api', root);
app.use('/api/v1', APIv1);

app.use(Sentry.Handlers.errorHandler());

export default app;
@@ -0,0 +1,11 @@
import { assertSentryEvent, TestEnv } from '../../../../utils/index';

test('should construct correct urls with multiple parameterized routers.', async () => {
const env = await TestEnv.init(__dirname, `${__dirname}/server.ts`);
const event = await env.getEnvelopeRequest({ url: env.url.replace('test', 'api/v1/user/1234/') });

assertSentryEvent(event[2] as any, {
message: 'Custom Message',
transaction: 'GET /api/v1/user/:userId',
});
});
@@ -0,0 +1,34 @@
import * as Sentry from '@sentry/node';
import * as Tracing from '@sentry/tracing';
import cors from 'cors';
import express from 'express';

const app = express();

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
integrations: [new Sentry.Integrations.Http({ tracing: true }), new Tracing.Integrations.Express({ app })],
tracesSampleRate: 1.0,
});

app.use(Sentry.Handlers.requestHandler());
app.use(Sentry.Handlers.tracingHandler());

app.use(cors());

const APIv1 = express.Router();

APIv1.get('/:userId', function (_req, res) {
Sentry.captureMessage('Custom Message');
res.send('Success');
});

const root = express.Router();

app.use('/api/v1', APIv1);
app.use('/api', root);

app.use(Sentry.Handlers.errorHandler());

export default app;
@@ -0,0 +1,11 @@
import { assertSentryEvent, TestEnv } from '../../../../utils/index';

test('should construct correct url with multiple parameterized routers of the same length (use order reversed).', async () => {
const env = await TestEnv.init(__dirname, `${__dirname}/server.ts`);
const event = await env.getEnvelopeRequest({ url: env.url.replace('test', 'api/v1/1234/') });

assertSentryEvent(event[2] as any, {
message: 'Custom Message',
transaction: 'GET /api/v1/:userId',
});
});
@@ -0,0 +1,34 @@
import * as Sentry from '@sentry/node';
import * as Tracing from '@sentry/tracing';
import cors from 'cors';
import express from 'express';

const app = express();

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
integrations: [new Sentry.Integrations.Http({ tracing: true }), new Tracing.Integrations.Express({ app })],
tracesSampleRate: 1.0,
});

app.use(Sentry.Handlers.requestHandler());
app.use(Sentry.Handlers.tracingHandler());

app.use(cors());

const APIv1 = express.Router();

APIv1.get('/:userId', function (_req, res) {
Sentry.captureMessage('Custom Message');
res.send('Success');
});

const root = express.Router();

app.use('/api', root);
app.use('/api/v1', APIv1);

app.use(Sentry.Handlers.errorHandler());

export default app;
@@ -0,0 +1,11 @@
import { assertSentryEvent, TestEnv } from '../../../../utils/index';

test('should construct correct url with multiple parameterized routers of the same length.', async () => {
const env = await TestEnv.init(__dirname, `${__dirname}/server.ts`);
const event = await env.getEnvelopeRequest({ url: env.url.replace('test', 'api/v1/1234/') });

assertSentryEvent(event[2] as any, {
message: 'Custom Message',
transaction: 'GET /api/v1/:userId',
});
});
@@ -0,0 +1,34 @@
import * as Sentry from '@sentry/node';
import * as Tracing from '@sentry/tracing';
import cors from 'cors';
import express from 'express';

const app = express();

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
integrations: [new Sentry.Integrations.Http({ tracing: true }), new Tracing.Integrations.Express({ app })],
tracesSampleRate: 1.0,
});

app.use(Sentry.Handlers.requestHandler());
app.use(Sentry.Handlers.tracingHandler());

app.use(cors());

const APIv1 = express.Router();

APIv1.get('/test', function (_req, res) {
Sentry.captureMessage('Custom Message');
res.send('Success');
});

const root = express.Router();

app.use('/api', root);
app.use('/api/v1', APIv1);

app.use(Sentry.Handlers.errorHandler());

export default app;
@@ -0,0 +1,11 @@
import { assertSentryEvent, TestEnv } from '../../../../utils/index';

test('should construct correct urls with multiple routers.', async () => {
const env = await TestEnv.init(__dirname, `${__dirname}/server.ts`);
const event = await env.getEnvelopeRequest({ url: env.url.replace('test', 'api/v1/test/') });

assertSentryEvent(event[2] as any, {
message: 'Custom Message',
transaction: 'GET /api/v1/test',
});
});
12 changes: 11 additions & 1 deletion packages/tracing/src/integrations/node/express.ts
Expand Up @@ -36,7 +36,7 @@ type Router = {
};

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

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

if (layerRoutePath || isRegex || isArray) {
req._hasParameters = true;
}

// Otherwise, the hardcoded path (i.e. a partial route without params) is stored in layer.path
const partialRoute = layerRoutePath || layer.path || '';

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

if (urlLength === routeLength) {
if (!req._hasParameters) {
if (req._reconstructedRoute !== req.originalUrl) {
req._reconstructedRoute = req.originalUrl;
}
}

const transaction = res.__sentry_transaction;
if (transaction && transaction.metadata.source !== 'custom') {
// If the request URL is '/' or empty, the reconstructed route will be empty.
Expand Down

0 comments on commit 72c6855

Please sign in to comment.