|
1 | 1 | /* eslint-disable max-lines */
|
2 | 2 | import { captureException, getCurrentHub } from '@sentry/node';
|
3 |
| -import { getActiveTransaction, hasTracingEnabled } from '@sentry/tracing'; |
4 |
| -import { addExceptionMechanism, fill, isNodeEnv, loadModule, logger, serializeBaggage } from '@sentry/utils'; |
5 |
| - |
6 |
| -// Types vendored from @remix-run/[email protected]: |
7 |
| -// https://github.com/remix-run/remix/blob/f3691d51027b93caa3fd2cdfe146d7b62a6eb8f2/packages/remix-server-runtime/server.ts |
8 |
| -type AppLoadContext = unknown; |
9 |
| -type AppData = unknown; |
10 |
| -type RequestHandler = (request: Request, loadContext?: AppLoadContext) => Promise<Response>; |
11 |
| -type CreateRequestHandlerFunction = (build: ServerBuild, mode?: string) => RequestHandler; |
12 |
| -type ServerRouteManifest = RouteManifest<Omit<ServerRoute, 'children'>>; |
13 |
| -type Params<Key extends string = string> = { |
14 |
| - readonly [key in Key]: string | undefined; |
15 |
| -}; |
16 |
| - |
17 |
| -interface Route { |
18 |
| - index?: boolean; |
19 |
| - caseSensitive?: boolean; |
20 |
| - id: string; |
21 |
| - parentId?: string; |
22 |
| - path?: string; |
23 |
| -} |
24 |
| -interface RouteData { |
25 |
| - [routeId: string]: AppData; |
26 |
| -} |
27 |
| - |
28 |
| -interface MetaFunction { |
29 |
| - (args: { data: AppData; parentsData: RouteData; params: Params; location: Location }): HtmlMetaDescriptor; |
30 |
| -} |
31 |
| - |
32 |
| -interface HtmlMetaDescriptor { |
33 |
| - [name: string]: null | string | undefined | Record<string, string> | Array<Record<string, string> | string>; |
34 |
| - charset?: 'utf-8'; |
35 |
| - charSet?: 'utf-8'; |
36 |
| - title?: string; |
37 |
| -} |
38 |
| - |
39 |
| -interface ServerRouteModule { |
40 |
| - action?: DataFunction; |
41 |
| - headers?: unknown; |
42 |
| - loader?: DataFunction; |
43 |
| - meta?: MetaFunction | HtmlMetaDescriptor; |
44 |
| -} |
45 |
| - |
46 |
| -interface ServerRoute extends Route { |
47 |
| - children: ServerRoute[]; |
48 |
| - module: ServerRouteModule; |
49 |
| -} |
50 |
| - |
51 |
| -interface RouteManifest<Route> { |
52 |
| - [routeId: string]: Route; |
53 |
| -} |
54 |
| - |
55 |
| -interface ServerBuild { |
56 |
| - entry: { |
57 |
| - module: ServerEntryModule; |
58 |
| - }; |
59 |
| - routes: ServerRouteManifest; |
60 |
| - assets: unknown; |
61 |
| -} |
62 |
| - |
63 |
| -interface HandleDocumentRequestFunction { |
64 |
| - (request: Request, responseStatusCode: number, responseHeaders: Headers, context: Record<symbol, unknown>): |
65 |
| - | Promise<Response> |
66 |
| - | Response; |
67 |
| -} |
68 |
| - |
69 |
| -interface HandleDataRequestFunction { |
70 |
| - (response: Response, args: DataFunctionArgs): Promise<Response> | Response; |
71 |
| -} |
72 |
| - |
73 |
| -interface ServerEntryModule { |
74 |
| - default: HandleDocumentRequestFunction; |
75 |
| - meta: MetaFunction; |
76 |
| - loader: DataFunction; |
77 |
| - handleDataRequest?: HandleDataRequestFunction; |
78 |
| -} |
79 |
| - |
80 |
| -interface DataFunctionArgs { |
81 |
| - request: Request; |
82 |
| - context: AppLoadContext; |
83 |
| - params: Params; |
84 |
| -} |
85 |
| - |
86 |
| -interface DataFunction { |
87 |
| - (args: DataFunctionArgs): Promise<Response> | Response | Promise<AppData> | AppData; |
88 |
| -} |
89 |
| - |
90 |
| -interface ReactRouterDomPkg { |
91 |
| - matchRoutes: (routes: ServerRoute[], pathname: string) => RouteMatch<ServerRoute>[] | null; |
92 |
| -} |
93 |
| - |
94 |
| -// Taken from Remix Implementation |
95 |
| -// https://github.com/remix-run/remix/blob/97999d02493e8114c39d48b76944069d58526e8d/packages/remix-server-runtime/routeMatching.ts#L6-L10 |
96 |
| -export interface RouteMatch<Route> { |
97 |
| - params: Params; |
98 |
| - pathname: string; |
99 |
| - route: Route; |
100 |
| -} |
| 3 | +import { getActiveTransaction, hasTracingEnabled, Span } from '@sentry/tracing'; |
| 4 | +import { |
| 5 | + addExceptionMechanism, |
| 6 | + CrossPlatformRequest, |
| 7 | + extractRequestData, |
| 8 | + fill, |
| 9 | + isNodeEnv, |
| 10 | + loadModule, |
| 11 | + logger, |
| 12 | + serializeBaggage, |
| 13 | +} from '@sentry/utils'; |
| 14 | +import type { Request as ExpressRequest } from 'express'; |
| 15 | + |
| 16 | +import { |
| 17 | + AppData, |
| 18 | + CreateRequestHandlerFunction, |
| 19 | + DataFunction, |
| 20 | + DataFunctionArgs, |
| 21 | + HandleDocumentRequestFunction, |
| 22 | + ReactRouterDomPkg, |
| 23 | + RequestHandler, |
| 24 | + RouteMatch, |
| 25 | + ServerBuild, |
| 26 | + ServerRoute, |
| 27 | + ServerRouteManifest, |
| 28 | +} from './types'; |
101 | 29 |
|
102 | 30 | // Taken from Remix Implementation
|
103 | 31 | // https://github.com/remix-run/remix/blob/32300ec6e6e8025602cea63e17a2201989589eab/packages/remix-server-runtime/responses.ts#L60-L77
|
@@ -290,7 +218,13 @@ function makeWrappedRootLoader(origLoader: DataFunction): DataFunction {
|
290 | 218 | };
|
291 | 219 | }
|
292 | 220 |
|
293 |
| -function createRoutes(manifest: ServerRouteManifest, parentId?: string): ServerRoute[] { |
| 221 | +/** |
| 222 | + * Creates routes from the server route manifest |
| 223 | + * |
| 224 | + * @param manifest |
| 225 | + * @param parentId |
| 226 | + */ |
| 227 | +export function createRoutes(manifest: ServerRouteManifest, parentId?: string): ServerRoute[] { |
294 | 228 | return Object.entries(manifest)
|
295 | 229 | .filter(([, route]) => route.parentId === parentId)
|
296 | 230 | .map(([id, route]) => ({
|
@@ -324,33 +258,55 @@ function matchServerRoutes(
|
324 | 258 | }));
|
325 | 259 | }
|
326 | 260 |
|
| 261 | +/** |
| 262 | + * Starts a new transaction for the given request to be used by different `RequestHandler` wrappers. |
| 263 | + * |
| 264 | + * @param request |
| 265 | + * @param routes |
| 266 | + * @param pkg |
| 267 | + */ |
| 268 | +export function startRequestHandlerTransaction( |
| 269 | + request: Request | ExpressRequest, |
| 270 | + routes: ServerRoute[], |
| 271 | + pkg?: ReactRouterDomPkg, |
| 272 | +): Span | undefined { |
| 273 | + const hub = getCurrentHub(); |
| 274 | + const currentScope = hub.getScope(); |
| 275 | + |
| 276 | + const reqData = extractRequestData(request as CrossPlatformRequest); |
| 277 | + |
| 278 | + if (!reqData.url) { |
| 279 | + return; |
| 280 | + } |
| 281 | + |
| 282 | + const url = new URL(reqData.url); |
| 283 | + const matches = matchServerRoutes(routes, url.pathname, pkg); |
| 284 | + |
| 285 | + const match = matches && getRequestMatch(url, matches); |
| 286 | + const name = match === null ? url.pathname : match.route.id; |
| 287 | + const source = match === null ? 'url' : 'route'; |
| 288 | + const transaction = hub.startTransaction({ |
| 289 | + name, |
| 290 | + op: 'http.server', |
| 291 | + tags: { |
| 292 | + method: reqData.method, |
| 293 | + }, |
| 294 | + metadata: { |
| 295 | + source, |
| 296 | + }, |
| 297 | + }); |
| 298 | + |
| 299 | + if (transaction) { |
| 300 | + currentScope?.setSpan(transaction); |
| 301 | + } |
| 302 | + return transaction; |
| 303 | +} |
| 304 | + |
327 | 305 | function wrapRequestHandler(origRequestHandler: RequestHandler, build: ServerBuild): RequestHandler {
|
328 | 306 | const routes = createRoutes(build.routes);
|
329 | 307 | const pkg = loadModule<ReactRouterDomPkg>('react-router-dom');
|
330 | 308 | return async function (this: unknown, request: Request, loadContext?: unknown): Promise<Response> {
|
331 |
| - const hub = getCurrentHub(); |
332 |
| - const currentScope = hub.getScope(); |
333 |
| - |
334 |
| - const url = new URL(request.url); |
335 |
| - const matches = matchServerRoutes(routes, url.pathname, pkg); |
336 |
| - |
337 |
| - const match = matches && getRequestMatch(url, matches); |
338 |
| - const name = match === null ? url.pathname : match.route.id; |
339 |
| - const source = match === null ? 'url' : 'route'; |
340 |
| - const transaction = hub.startTransaction({ |
341 |
| - name, |
342 |
| - op: 'http.server', |
343 |
| - tags: { |
344 |
| - method: request.method, |
345 |
| - }, |
346 |
| - metadata: { |
347 |
| - source, |
348 |
| - }, |
349 |
| - }); |
350 |
| - |
351 |
| - if (transaction) { |
352 |
| - currentScope?.setSpan(transaction); |
353 |
| - } |
| 309 | + const transaction = startRequestHandlerTransaction(request, routes, pkg); |
354 | 310 |
|
355 | 311 | const res = (await origRequestHandler.call(this, request, loadContext)) as Response;
|
356 | 312 |
|
@@ -388,43 +344,49 @@ function getRequestMatch(url: URL, matches: RouteMatch<ServerRoute>[]): RouteMat
|
388 | 344 | return match;
|
389 | 345 | }
|
390 | 346 |
|
391 |
| -function makeWrappedCreateRequestHandler( |
392 |
| - origCreateRequestHandler: CreateRequestHandlerFunction, |
393 |
| -): CreateRequestHandlerFunction { |
394 |
| - return function (this: unknown, build: ServerBuild, mode: string | undefined): RequestHandler { |
395 |
| - const routes: ServerRouteManifest = {}; |
396 |
| - |
397 |
| - const wrappedEntry = { ...build.entry, module: { ...build.entry.module } }; |
| 347 | +/** |
| 348 | + * |
| 349 | + */ |
| 350 | +export function instrumentBuild(build: ServerBuild): ServerBuild { |
| 351 | + const routes: ServerRouteManifest = {}; |
398 | 352 |
|
399 |
| - fill(wrappedEntry.module, 'default', makeWrappedDocumentRequestFunction); |
| 353 | + const wrappedEntry = { ...build.entry, module: { ...build.entry.module } }; |
400 | 354 |
|
401 |
| - for (const [id, route] of Object.entries(build.routes)) { |
402 |
| - const wrappedRoute = { ...route, module: { ...route.module } }; |
| 355 | + fill(wrappedEntry.module, 'default', makeWrappedDocumentRequestFunction); |
403 | 356 |
|
404 |
| - if (wrappedRoute.module.action) { |
405 |
| - fill(wrappedRoute.module, 'action', makeWrappedAction); |
406 |
| - } |
| 357 | + for (const [id, route] of Object.entries(build.routes)) { |
| 358 | + const wrappedRoute = { ...route, module: { ...route.module } }; |
407 | 359 |
|
408 |
| - if (wrappedRoute.module.loader) { |
409 |
| - fill(wrappedRoute.module, 'loader', makeWrappedLoader); |
410 |
| - } |
| 360 | + if (wrappedRoute.module.action) { |
| 361 | + fill(wrappedRoute.module, 'action', makeWrappedAction); |
| 362 | + } |
411 | 363 |
|
412 |
| - // Entry module should have a loader function to provide `sentry-trace` and `baggage` |
413 |
| - // They will be available for the root `meta` function as `data.sentryTrace` and `data.sentryBaggage` |
414 |
| - if (!wrappedRoute.parentId) { |
415 |
| - if (!wrappedRoute.module.loader) { |
416 |
| - wrappedRoute.module.loader = () => ({}); |
417 |
| - } |
| 364 | + if (wrappedRoute.module.loader) { |
| 365 | + fill(wrappedRoute.module, 'loader', makeWrappedLoader); |
| 366 | + } |
418 | 367 |
|
419 |
| - fill(wrappedRoute.module, 'loader', makeWrappedRootLoader); |
| 368 | + // Entry module should have a loader function to provide `sentry-trace` and `baggage` |
| 369 | + // They will be available for the root `meta` function as `data.sentryTrace` and `data.sentryBaggage` |
| 370 | + if (!wrappedRoute.parentId) { |
| 371 | + if (!wrappedRoute.module.loader) { |
| 372 | + wrappedRoute.module.loader = () => ({}); |
420 | 373 | }
|
421 | 374 |
|
422 |
| - routes[id] = wrappedRoute; |
| 375 | + fill(wrappedRoute.module, 'loader', makeWrappedRootLoader); |
423 | 376 | }
|
424 | 377 |
|
425 |
| - const newBuild = { ...build, routes, entry: wrappedEntry }; |
| 378 | + routes[id] = wrappedRoute; |
| 379 | + } |
| 380 | + |
| 381 | + return { ...build, routes, entry: wrappedEntry }; |
| 382 | +} |
426 | 383 |
|
427 |
| - const requestHandler = origCreateRequestHandler.call(this, newBuild, mode); |
| 384 | +function makeWrappedCreateRequestHandler( |
| 385 | + origCreateRequestHandler: CreateRequestHandlerFunction, |
| 386 | +): CreateRequestHandlerFunction { |
| 387 | + return function (this: unknown, build: ServerBuild, ...args: unknown[]): RequestHandler { |
| 388 | + const newBuild = instrumentBuild(build); |
| 389 | + const requestHandler = origCreateRequestHandler.call(this, newBuild, ...args); |
428 | 390 |
|
429 | 391 | return wrapRequestHandler(requestHandler, newBuild);
|
430 | 392 | };
|
|
0 commit comments