|
1 | 1 | import { deepReadDirSync } from '@sentry/node'; |
2 | | -import { hasTracingEnabled } from '@sentry/tracing'; |
3 | 2 | import { Transaction } from '@sentry/types'; |
| 3 | +import { getActiveTransaction, hasTracingEnabled } from '@sentry/tracing'; |
4 | 4 | import { fill, logger } from '@sentry/utils'; |
5 | 5 | import * as domain from 'domain'; |
6 | 6 | import * as http from 'http'; |
7 | 7 | import { default as createNextServer } from 'next'; |
| 8 | +import * as querystring from 'querystring'; |
8 | 9 | import * as url from 'url'; |
9 | 10 |
|
10 | 11 | import * as Sentry from '../index.server'; |
@@ -40,11 +41,19 @@ interface NextResponse extends http.ServerResponse { |
40 | 41 | type HandlerGetter = () => Promise<ReqHandler>; |
41 | 42 | type ReqHandler = (req: NextRequest, res: NextResponse, parsedUrl?: url.UrlWithParsedQuery) => Promise<void>; |
42 | 43 | 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>; |
43 | 50 |
|
44 | 51 | // these aliases are purely to make the function signatures more easily understandable |
45 | 52 | type WrappedHandlerGetter = HandlerGetter; |
46 | 53 | type WrappedErrorLogger = ErrorLogger; |
47 | 54 | type WrappedReqHandler = ReqHandler; |
| 55 | +type WrappedApiPageEnsurer = ApiPageEnsurer; |
| 56 | +type WrappedPageComponentFinder = PageComponentFinder; |
48 | 57 |
|
49 | 58 | // TODO is it necessary for this to be an object? |
50 | 59 | const closure: PlainObject = {}; |
@@ -125,6 +134,12 @@ function makeWrappedHandlerGetter(origHandlerGetter: HandlerGetter): WrappedHand |
125 | 134 | // to the appropriate handlers) |
126 | 135 | fill(serverPrototype, 'handleRequest', makeWrappedReqHandler); |
127 | 136 |
|
| 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 | + |
128 | 143 | sdkSetupComplete = true; |
129 | 144 | } |
130 | 145 |
|
@@ -182,40 +197,77 @@ function makeWrappedReqHandler(origReqHandler: ReqHandler): WrappedReqHandler { |
182 | 197 | // local.on('error', Sentry.captureException); |
183 | 198 |
|
184 | 199 | 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) { |
205 | 223 | transaction.setHttpStatus(res.statusCode); |
206 | | - transaction.finish(); |
207 | | - }); |
208 | | - } |
209 | | - }); |
210 | 224 |
|
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 | + } |
212 | 235 | } |
| 236 | + |
| 237 | + return origReqHandler.call(this, req, res, parsedUrl); |
213 | 238 | }); |
214 | 239 | }; |
215 | 240 |
|
216 | 241 | return wrappedReqHandler; |
217 | 242 | } |
218 | 243 |
|
| 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 | + |
219 | 271 | /** |
220 | 272 | * Determine if the request should be traced, by filtering out requests for internal next files and static resources. |
221 | 273 | * |
|
0 commit comments