Skip to content

Commit 24c5c28

Browse files
authored
Express integration span name change and path unification (#3078)
* ref: Change express span name to midddleware.method * ref: Change types to express Router instead of Application * fix: Use correct paths for express transactions and unify express integration
1 parent 10ed751 commit 24c5c28

File tree

2 files changed

+80
-101
lines changed

2 files changed

+80
-101
lines changed

packages/node/src/handlers.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
/* eslint-disable @typescript-eslint/no-explicit-any */
33
import { captureException, getCurrentHub, startTransaction, withScope } from '@sentry/core';
44
import { extractTraceparentData, Span } from '@sentry/tracing';
5-
import { Event } from '@sentry/types';
5+
import { Event, Transaction } from '@sentry/types';
66
import {
77
extractNodeRequestData,
88
forget,
@@ -21,6 +21,16 @@ import { flush } from './sdk';
2121

2222
const DEFAULT_SHUTDOWN_TIMEOUT = 2000;
2323

24+
interface ExpressRequest {
25+
route?: {
26+
path: string;
27+
};
28+
method: string;
29+
originalUrl: string;
30+
baseUrl: string;
31+
query: string;
32+
}
33+
2434
/**
2535
* Express-compatible tracing handler.
2636
* @see Exposed as `Handlers.tracingHandler`
@@ -65,6 +75,7 @@ export function tracingHandler(): (
6575
res.once('finish', () => {
6676
// We schedule the immediate execution of the `finish` to let all the spans being closed first.
6777
setImmediate(() => {
78+
addExpressReqToTransaction(transaction, (req as unknown) as ExpressRequest);
6879
transaction.setHttpStatus(res.statusCode);
6980
transaction.finish();
7081
});
@@ -74,6 +85,20 @@ export function tracingHandler(): (
7485
};
7586
}
7687

88+
/**
89+
* Set parameterized as transaction name e.g.: `GET /users/:id`
90+
* Also adds more context data on the transaction from the request
91+
*/
92+
function addExpressReqToTransaction(transaction: Transaction | undefined, req: ExpressRequest): void {
93+
if (!transaction) return;
94+
if (req.route) {
95+
transaction.name = `${req.method} ${req.baseUrl}${req.route.path}`;
96+
}
97+
transaction.setData('url', req.originalUrl);
98+
transaction.setData('baseUrl', req.baseUrl);
99+
transaction.setData('query', req.query);
100+
}
101+
77102
type TransactionTypes = 'path' | 'methodPath' | 'handler';
78103

79104
/** JSDoc */
Lines changed: 54 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
/* eslint-disable @typescript-eslint/no-explicit-any */
21
import { Integration, Transaction } from '@sentry/types';
32
import { logger } from '@sentry/utils';
43

5-
// Have to manually set types because we are using package-alias
64
type Method =
75
| 'all'
86
| 'get'
@@ -27,17 +25,14 @@ type Method =
2725
| 'subscribe'
2826
| 'trace'
2927
| 'unlock'
30-
| 'unsubscribe';
28+
| 'unsubscribe'
29+
| 'use';
3130

32-
type Application = {
33-
[method in Method | 'use']: (...args: any) => any;
31+
type Router = {
32+
[method in Method]: (...args: unknown[]) => unknown;
3433
};
3534

36-
type ErrorRequestHandler = (...args: any) => any;
37-
type RequestHandler = (...args: any) => any;
38-
type NextFunction = (...args: any) => any;
39-
40-
interface Response {
35+
interface ExpressResponse {
4136
once(name: string, callback: () => void): void;
4237
}
4338

@@ -52,8 +47,7 @@ interface SentryTracingResponse {
5247
/**
5348
* Express integration
5449
*
55-
* Provides an request and error handler for Express framework
56-
* as well as tracing capabilities
50+
* Provides an request and error handler for Express framework as well as tracing capabilities
5751
*/
5852
export class Express implements Integration {
5953
/**
@@ -69,27 +63,26 @@ export class Express implements Integration {
6963
/**
7064
* Express App instance
7165
*/
72-
private readonly _app?: Application;
66+
private readonly _router?: Router;
7367
private readonly _methods?: Method[];
7468

7569
/**
7670
* @inheritDoc
7771
*/
78-
public constructor(options: { app?: Application; methods?: Method[] } = {}) {
79-
this._app = options.app;
80-
this._methods = options.methods;
72+
public constructor(options: { app?: Router; router?: Router; methods?: Method[] } = {}) {
73+
this._router = options.router || options.app;
74+
this._methods = (Array.isArray(options.methods) ? options.methods : []).concat('use');
8175
}
8276

8377
/**
8478
* @inheritDoc
8579
*/
8680
public setupOnce(): void {
87-
if (!this._app) {
81+
if (!this._router) {
8882
logger.error('ExpressIntegration is missing an Express instance');
8983
return;
9084
}
91-
instrumentMiddlewares(this._app);
92-
routeMiddlewares(this._app, this._methods);
85+
instrumentMiddlewares(this._router, this._methods);
9386
}
9487
}
9588

@@ -104,75 +97,63 @@ export class Express implements Integration {
10497
* app.use(function (req, res, next) { ... })
10598
* // error handler
10699
* app.use(function (err, req, res, next) { ... })
100+
*
101+
* They all internally delegate to the `router[method]` of the given application instance.
107102
*/
108-
// eslint-disable-next-line @typescript-eslint/ban-types
109-
function wrap(fn: Function): RequestHandler | ErrorRequestHandler {
103+
// eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/no-explicit-any
104+
function wrap(fn: Function, method: Method): (...args: any[]) => void {
110105
const arity = fn.length;
111106

112107
switch (arity) {
113108
case 2: {
114-
return function(this: NodeJS.Global, req: Request, res: Response & SentryTracingResponse): any {
109+
return function(this: NodeJS.Global, req: unknown, res: ExpressResponse & SentryTracingResponse): void {
115110
const transaction = res.__sentry_transaction;
116-
addExpressReqToTransaction(transaction, req);
117111
if (transaction) {
118112
const span = transaction.startChild({
119113
description: fn.name,
120-
op: 'middleware',
114+
op: `middleware.${method}`,
121115
});
122116
res.once('finish', () => {
123117
span.finish();
124118
});
125119
}
126-
// eslint-disable-next-line prefer-rest-params
127-
return fn.apply(this, arguments);
120+
return fn.call(this, req, res);
128121
};
129122
}
130123
case 3: {
131124
return function(
132125
this: NodeJS.Global,
133-
req: Request,
134-
res: Response & SentryTracingResponse,
135-
next: NextFunction,
136-
): any {
126+
req: unknown,
127+
res: ExpressResponse & SentryTracingResponse,
128+
next: () => void,
129+
): void {
137130
const transaction = res.__sentry_transaction;
138-
addExpressReqToTransaction(transaction, req);
139-
const span =
140-
transaction &&
141-
transaction.startChild({
142-
description: fn.name,
143-
op: 'middleware',
144-
});
145-
fn.call(this, req, res, function(this: NodeJS.Global): any {
146-
if (span) {
147-
span.finish();
148-
}
149-
// eslint-disable-next-line prefer-rest-params
150-
return next.apply(this, arguments);
131+
const span = transaction?.startChild({
132+
description: fn.name,
133+
op: `middleware.${method}`,
134+
});
135+
fn.call(this, req, res, function(this: NodeJS.Global, ...args: unknown[]): void {
136+
span?.finish();
137+
next.call(this, ...args);
151138
});
152139
};
153140
}
154141
case 4: {
155142
return function(
156143
this: NodeJS.Global,
157-
err: any,
144+
err: Error,
158145
req: Request,
159146
res: Response & SentryTracingResponse,
160-
next: NextFunction,
161-
): any {
147+
next: () => void,
148+
): void {
162149
const transaction = res.__sentry_transaction;
163-
addExpressReqToTransaction(transaction, req);
164-
const span =
165-
transaction &&
166-
transaction.startChild({
167-
description: fn.name,
168-
op: 'middleware',
169-
});
170-
fn.call(this, err, req, res, function(this: NodeJS.Global): any {
171-
if (span) {
172-
span.finish();
173-
}
174-
// eslint-disable-next-line prefer-rest-params
175-
return next.apply(this, arguments);
150+
const span = transaction?.startChild({
151+
description: fn.name,
152+
op: `middleware.${method}`,
153+
});
154+
fn.call(this, err, req, res, function(this: NodeJS.Global, ...args: unknown[]): void {
155+
span?.finish();
156+
next.call(this, ...args);
176157
});
177158
};
178159
}
@@ -183,24 +164,7 @@ function wrap(fn: Function): RequestHandler | ErrorRequestHandler {
183164
}
184165

185166
/**
186-
* Set parameterized as transaction name e.g.: `GET /users/:id`
187-
* Also adds more context data on the transaction from the request
188-
*/
189-
function addExpressReqToTransaction(transaction: Transaction | undefined, req: any): void {
190-
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
191-
if (transaction) {
192-
if (req.route && req.route.path) {
193-
transaction.name = `${req.method} ${req.route.path}`;
194-
}
195-
transaction.setData('url', req.originalUrl);
196-
transaction.setData('baseUrl', req.baseUrl);
197-
transaction.setData('query', req.query);
198-
}
199-
/* eslint-enable @typescript-eslint/no-unsafe-member-access */
200-
}
201-
202-
/**
203-
* Takes all the function arguments passed to the original `app.use` call
167+
* Takes all the function arguments passed to the original `app` or `router` method, eg. `app.use` or `router.use`
204168
* and wraps every function, as well as array of functions with a call to our `wrap` method.
205169
* We have to take care of the arrays as well as iterate over all of the arguments,
206170
* as `app.use` can accept middlewares in few various forms.
@@ -209,16 +173,16 @@ function addExpressReqToTransaction(transaction: Transaction | undefined, req: a
209173
* app.use([<path>], <fn>, ...<fn>)
210174
* app.use([<path>], ...<fn>[])
211175
*/
212-
function wrapUseArgs(args: IArguments): unknown[] {
213-
return Array.from(args).map((arg: unknown) => {
176+
function wrapMiddlewareArgs(args: unknown[], method: Method): unknown[] {
177+
return args.map((arg: unknown) => {
214178
if (typeof arg === 'function') {
215-
return wrap(arg);
179+
return wrap(arg, method);
216180
}
217181

218182
if (Array.isArray(arg)) {
219183
return arg.map((a: unknown) => {
220184
if (typeof a === 'function') {
221-
return wrap(a);
185+
return wrap(a, method);
222186
}
223187
return a;
224188
});
@@ -229,31 +193,21 @@ function wrapUseArgs(args: IArguments): unknown[] {
229193
}
230194

231195
/**
232-
* Patches original App to utilize our tracing functionality
196+
* Patches original router to utilize our tracing functionality
233197
*/
234-
function patchMiddleware(app: Application, method: Method | 'use'): Application {
235-
const originalAppCallback = app[method];
198+
function patchMiddleware(router: Router, method: Method): Router {
199+
const originalCallback = router[method];
236200

237-
app[method] = function(): any {
238-
// eslint-disable-next-line prefer-rest-params
239-
return originalAppCallback.apply(this, wrapUseArgs(arguments));
201+
router[method] = function(...args: unknown[]): void {
202+
return originalCallback.call(this, ...wrapMiddlewareArgs(args, method));
240203
};
241204

242-
return app;
205+
return router;
243206
}
244207

245208
/**
246-
* Patches original app.use
209+
* Patches original router methods
247210
*/
248-
function instrumentMiddlewares(app: Application): void {
249-
patchMiddleware(app, 'use');
250-
}
251-
252-
/**
253-
* Patches original app.METHOD
254-
*/
255-
function routeMiddlewares(app: Application, methods: Method[] = []): void {
256-
methods.forEach(function(method: Method) {
257-
patchMiddleware(app, method);
258-
});
211+
function instrumentMiddlewares(router: Router, methods: Method[] = []): void {
212+
methods.forEach((method: Method) => patchMiddleware(router, method));
259213
}

0 commit comments

Comments
 (0)