Skip to content

Commit 3011d0c

Browse files
committed
wrap methods to get parameterized names
1 parent 5395bf0 commit 3011d0c

File tree

1 file changed

+78
-26
lines changed

1 file changed

+78
-26
lines changed

packages/nextjs/src/utils/instrumentServer.ts

Lines changed: 78 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { deepReadDirSync } from '@sentry/node';
2-
import { hasTracingEnabled } from '@sentry/tracing';
32
import { Transaction } from '@sentry/types';
3+
import { getActiveTransaction, hasTracingEnabled } from '@sentry/tracing';
44
import { fill, logger } from '@sentry/utils';
55
import * as domain from 'domain';
66
import * as http from 'http';
77
import { default as createNextServer } from 'next';
8+
import * as querystring from 'querystring';
89
import * as url from 'url';
910

1011
import * as Sentry from '../index.server';
@@ -40,11 +41,19 @@ interface NextResponse extends http.ServerResponse {
4041
type HandlerGetter = () => Promise<ReqHandler>;
4142
type ReqHandler = (req: NextRequest, res: NextResponse, parsedUrl?: url.UrlWithParsedQuery) => Promise<void>;
4243
type ErrorLogger = (err: Error) => void;
44+
type ApiPageEnsurer = (path: string) => Promise<void>;
45+
type PageComponentFinder = (
46+
pathname: string,
47+
query: querystring.ParsedUrlQuery,
48+
params: { [key: string]: any } | null,
49+
) => Promise<{ [key: string]: any } | null>;
4350

4451
// these aliases are purely to make the function signatures more easily understandable
4552
type WrappedHandlerGetter = HandlerGetter;
4653
type WrappedErrorLogger = ErrorLogger;
4754
type WrappedReqHandler = ReqHandler;
55+
type WrappedApiPageEnsurer = ApiPageEnsurer;
56+
type WrappedPageComponentFinder = PageComponentFinder;
4857

4958
// TODO is it necessary for this to be an object?
5059
const closure: PlainObject = {};
@@ -125,6 +134,12 @@ function makeWrappedHandlerGetter(origHandlerGetter: HandlerGetter): WrappedHand
125134
// to the appropriate handlers)
126135
fill(serverPrototype, 'handleRequest', makeWrappedReqHandler);
127136

137+
// Wrap as a way to grab the parameterized request URL to use as the transaction name for API requests and page
138+
// requests, respectively. These methods are chosen because they're the first spot in the request-handling process
139+
// where the parameterized path is provided as an argument, so it's easy to grab.
140+
fill(serverPrototype, 'ensureApiPage', makeWrappedMethodForGettingParameterizedPath);
141+
fill(serverPrototype, 'findPageComponents', makeWrappedMethodForGettingParameterizedPath);
142+
128143
sdkSetupComplete = true;
129144
}
130145

@@ -182,40 +197,77 @@ function makeWrappedReqHandler(origReqHandler: ReqHandler): WrappedReqHandler {
182197
// local.on('error', Sentry.captureException);
183198

184199
local.run(() => {
185-
// We only want to record page and API requests
186-
if (hasTracingEnabled() && shouldTraceRequest(req.url, publicDirFiles)) {
187-
const transaction = Sentry.startTransaction({
188-
name: `${(req.method || 'GET').toUpperCase()} ${req.url}`,
189-
op: 'http.server',
190-
});
191-
Sentry.getCurrentHub()
192-
.getScope()
193-
?.setSpan(transaction);
194-
195-
res.__sentry__ = { transaction };
196-
197-
res.once('finish', () => {
198-
const transaction = res.__sentry__?.transaction;
199-
if (transaction) {
200-
// Push `transaction.finish` to the next event loop so open spans have a chance to finish before the transaction
201-
// closes
202-
setImmediate(() => {
203-
// TODO
204-
// addExpressReqToTransaction(transaction, req);
200+
const currentScope = Sentry.getCurrentHub().getScope();
201+
202+
if (currentScope) {
203+
// We only want to record page and API requests
204+
if (hasTracingEnabled() && shouldTraceRequest(req.url, publicDirFiles)) {
205+
// pull off query string, if any
206+
const reqPath = req.url.split('?')[0];
207+
208+
// requests for pages will only ever be GET requests, so don't bother to include the method in the transaction
209+
// name; requests to API routes could be GET, POST, PUT, etc, so do include it there
210+
const namePrefix = req.url.startsWith('/api') ? `${(req.method || 'GET').toUpperCase()} ` : '';
211+
212+
const transaction = Sentry.startTransaction({
213+
name: `${namePrefix}${reqPath}`,
214+
op: 'http.server',
215+
metadata: { request: req },
216+
});
217+
218+
currentScope.setSpan(transaction);
219+
220+
res.once('finish', () => {
221+
const transaction = getActiveTransaction();
222+
if (transaction) {
205223
transaction.setHttpStatus(res.statusCode);
206-
transaction.finish();
207-
});
208-
}
209-
});
210224

211-
return origReqHandler.call(this, req, res, parsedUrl);
225+
delete transaction.metadata.request;
226+
227+
// Push `transaction.finish` to the next event loop so open spans have a chance to finish before the
228+
// transaction closes
229+
setImmediate(() => {
230+
transaction.finish();
231+
});
232+
}
233+
});
234+
}
212235
}
236+
237+
return origReqHandler.call(this, req, res, parsedUrl);
213238
});
214239
};
215240

216241
return wrappedReqHandler;
217242
}
218243

244+
/**
245+
* Wrap the given method in order to use the parameterized path passed to it in the transaction name.
246+
*
247+
* @param origMethod Either `ensureApiPage` (called for every API request) or `findPageComponents` (called for every
248+
* page request), both from the `Server` class
249+
* @returns A wrapped version of the given method
250+
*/
251+
function makeWrappedMethodForGettingParameterizedPath(
252+
origMethod: ApiPageEnsurer | PageComponentFinder,
253+
): WrappedApiPageEnsurer | WrappedPageComponentFinder {
254+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
255+
const wrappedMethod = async function(this: Server, parameterizedPath: string, ...args: any[]): Promise<any> {
256+
const transaction = getActiveTransaction();
257+
258+
// replace specific URL with parameterized version
259+
if (transaction && transaction.metadata.request) {
260+
// strip query string, if any
261+
const origPath = transaction.metadata.request.url.split('?')[0];
262+
transaction.name = transaction.name.replace(origPath, parameterizedPath);
263+
}
264+
265+
return origMethod.call(this, parameterizedPath, ...args);
266+
};
267+
268+
return wrappedMethod;
269+
}
270+
219271
/**
220272
* Determine if the request should be traced, by filtering out requests for internal next files and static resources.
221273
*

0 commit comments

Comments
 (0)