Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(express): Support multiple routers with common paths. #6253

Merged
merged 3 commits into from Nov 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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);
Comment on lines +29 to +30
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at #6004, this is the order in which the parameterization was incorrect (so this is fixed, great!). Can we also verify that swapping the order still works (as the user originally reported)?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added tests 👍


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