|
1 | 1 | /* eslint-disable max-lines */
|
2 | 2 | import { captureException, getCurrentHub } from '@sentry/node';
|
3 |
| -import { getActiveTransaction, hasTracingEnabled } from '@sentry/tracing'; |
| 3 | +import { getActiveTransaction, hasTracingEnabled, Span } from '@sentry/tracing'; |
| 4 | +import { WrappedFunction } from '@sentry/types'; |
4 | 5 | import { addExceptionMechanism, fill, isNodeEnv, loadModule, logger, serializeBaggage } from '@sentry/utils';
|
5 | 6 |
|
6 |
| -// Types vendored from @remix-run/server-runtime@1.6.0: |
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 |
| -} |
| 7 | +import { |
| 8 | + AppData, |
| 9 | + CreateRequestHandlerFunction, |
| 10 | + DataFunction, |
| 11 | + DataFunctionArgs, |
| 12 | + HandleDocumentRequestFunction, |
| 13 | + ReactRouterDomPkg, |
| 14 | + RequestHandler, |
| 15 | + RouteMatch, |
| 16 | + ServerBuild, |
| 17 | + ServerRoute, |
| 18 | + ServerRouteManifest, |
| 19 | +} from './types'; |
| 20 | + |
| 21 | +// Flag to track if the core request handler is instrumented. |
| 22 | +export let isRequestHandlerWrapped = false; |
101 | 23 |
|
102 | 24 | // Taken from Remix Implementation
|
103 | 25 | // https://github.com/remix-run/remix/blob/32300ec6e6e8025602cea63e17a2201989589eab/packages/remix-server-runtime/responses.ts#L60-L77
|
@@ -318,7 +240,13 @@ function makeWrappedRootLoader(origLoader: DataFunction): DataFunction {
|
318 | 240 | };
|
319 | 241 | }
|
320 | 242 |
|
321 |
| -function createRoutes(manifest: ServerRouteManifest, parentId?: string): ServerRoute[] { |
| 243 | +/** |
| 244 | + * Creates routes from the server route manifest |
| 245 | + * |
| 246 | + * @param manifest |
| 247 | + * @param parentId |
| 248 | + */ |
| 249 | +export function createRoutes(manifest: ServerRouteManifest, parentId?: string): ServerRoute[] { |
322 | 250 | return Object.entries(manifest)
|
323 | 251 | .filter(([, route]) => route.parentId === parentId)
|
324 | 252 | .map(([id, route]) => ({
|
@@ -352,33 +280,50 @@ function matchServerRoutes(
|
352 | 280 | }));
|
353 | 281 | }
|
354 | 282 |
|
| 283 | +/** |
| 284 | + * Starts a new transaction for the given request to be used by different `RequestHandler` wrappers. |
| 285 | + * |
| 286 | + * @param request |
| 287 | + * @param routes |
| 288 | + * @param pkg |
| 289 | + */ |
| 290 | +export function startRequestHandlerTransaction( |
| 291 | + url: URL, |
| 292 | + method: string, |
| 293 | + routes: ServerRoute[], |
| 294 | + pkg?: ReactRouterDomPkg, |
| 295 | +): Span | undefined { |
| 296 | + const hub = getCurrentHub(); |
| 297 | + const currentScope = hub.getScope(); |
| 298 | + const matches = matchServerRoutes(routes, url.pathname, pkg); |
| 299 | + |
| 300 | + const match = matches && getRequestMatch(url, matches); |
| 301 | + const name = match === null ? url.pathname : match.route.id; |
| 302 | + const source = match === null ? 'url' : 'route'; |
| 303 | + const transaction = hub.startTransaction({ |
| 304 | + name, |
| 305 | + op: 'http.server', |
| 306 | + tags: { |
| 307 | + method: method, |
| 308 | + }, |
| 309 | + metadata: { |
| 310 | + source, |
| 311 | + }, |
| 312 | + }); |
| 313 | + |
| 314 | + if (transaction) { |
| 315 | + currentScope?.setSpan(transaction); |
| 316 | + } |
| 317 | + return transaction; |
| 318 | +} |
| 319 | + |
355 | 320 | function wrapRequestHandler(origRequestHandler: RequestHandler, build: ServerBuild): RequestHandler {
|
356 | 321 | const routes = createRoutes(build.routes);
|
357 | 322 | const pkg = loadModule<ReactRouterDomPkg>('react-router-dom');
|
358 | 323 | return async function (this: unknown, request: Request, loadContext?: unknown): Promise<Response> {
|
359 |
| - const hub = getCurrentHub(); |
360 |
| - const currentScope = hub.getScope(); |
361 |
| - |
362 | 324 | const url = new URL(request.url);
|
363 |
| - const matches = matchServerRoutes(routes, url.pathname, pkg); |
364 |
| - |
365 |
| - const match = matches && getRequestMatch(url, matches); |
366 |
| - const name = match === null ? url.pathname : match.route.id; |
367 |
| - const source = match === null ? 'url' : 'route'; |
368 |
| - const transaction = hub.startTransaction({ |
369 |
| - name, |
370 |
| - op: 'http.server', |
371 |
| - tags: { |
372 |
| - method: request.method, |
373 |
| - }, |
374 |
| - metadata: { |
375 |
| - source, |
376 |
| - }, |
377 |
| - }); |
378 | 325 |
|
379 |
| - if (transaction) { |
380 |
| - currentScope?.setSpan(transaction); |
381 |
| - } |
| 326 | + const transaction = startRequestHandlerTransaction(url, request.method, routes, pkg); |
382 | 327 |
|
383 | 328 | const res = (await origRequestHandler.call(this, request, loadContext)) as Response;
|
384 | 329 |
|
@@ -416,43 +361,60 @@ function getRequestMatch(url: URL, matches: RouteMatch<ServerRoute>[]): RouteMat
|
416 | 361 | return match;
|
417 | 362 | }
|
418 | 363 |
|
419 |
| -function makeWrappedCreateRequestHandler( |
420 |
| - origCreateRequestHandler: CreateRequestHandlerFunction, |
421 |
| -): CreateRequestHandlerFunction { |
422 |
| - return function (this: unknown, build: ServerBuild, mode: string | undefined): RequestHandler { |
423 |
| - const routes: ServerRouteManifest = {}; |
| 364 | +/** |
| 365 | + * Instruments `remix` ServerBuild for performance tracing and error tracking. |
| 366 | + */ |
| 367 | +export function instrumentBuild(build: ServerBuild): ServerBuild { |
| 368 | + const routes: ServerRouteManifest = {}; |
424 | 369 |
|
425 |
| - const wrappedEntry = { ...build.entry, module: { ...build.entry.module } }; |
| 370 | + const wrappedEntry = { ...build.entry, module: { ...build.entry.module } }; |
426 | 371 |
|
| 372 | + // Not keeping boolean flags like it's done for `requestHandler` functions, |
| 373 | + // Because the build can change between build and runtime. |
| 374 | + // So if there is a new `loader` or`action` or `documentRequest` after build. |
| 375 | + // We should be able to wrap them, as they may not be wrapped before. |
| 376 | + if (!(wrappedEntry.module.default as WrappedFunction).__sentry_original__) { |
427 | 377 | fill(wrappedEntry.module, 'default', makeWrappedDocumentRequestFunction);
|
| 378 | + } |
428 | 379 |
|
429 |
| - for (const [id, route] of Object.entries(build.routes)) { |
430 |
| - const wrappedRoute = { ...route, module: { ...route.module } }; |
431 |
| - |
432 |
| - if (wrappedRoute.module.action) { |
433 |
| - fill(wrappedRoute.module, 'action', makeWrappedAction(id)); |
434 |
| - } |
| 380 | + for (const [id, route] of Object.entries(build.routes)) { |
| 381 | + const wrappedRoute = { ...route, module: { ...route.module } }; |
435 | 382 |
|
436 |
| - if (wrappedRoute.module.loader) { |
437 |
| - fill(wrappedRoute.module, 'loader', makeWrappedLoader(id)); |
438 |
| - } |
| 383 | + if (wrappedRoute.module.action && !(wrappedRoute.module.action as WrappedFunction).__sentry_original__) { |
| 384 | + fill(wrappedRoute.module, 'action', makeWrappedAction(id)); |
| 385 | + } |
439 | 386 |
|
440 |
| - // Entry module should have a loader function to provide `sentry-trace` and `baggage` |
441 |
| - // They will be available for the root `meta` function as `data.sentryTrace` and `data.sentryBaggage` |
442 |
| - if (!wrappedRoute.parentId) { |
443 |
| - if (!wrappedRoute.module.loader) { |
444 |
| - wrappedRoute.module.loader = () => ({}); |
445 |
| - } |
| 387 | + if (wrappedRoute.module.loader && !(wrappedRoute.module.loader as WrappedFunction).__sentry_original__) { |
| 388 | + fill(wrappedRoute.module, 'loader', makeWrappedLoader(id)); |
| 389 | + } |
446 | 390 |
|
447 |
| - fill(wrappedRoute.module, 'loader', makeWrappedRootLoader); |
| 391 | + // Entry module should have a loader function to provide `sentry-trace` and `baggage` |
| 392 | + // They will be available for the root `meta` function as `data.sentryTrace` and `data.sentryBaggage` |
| 393 | + if (!wrappedRoute.parentId) { |
| 394 | + if (!wrappedRoute.module.loader) { |
| 395 | + wrappedRoute.module.loader = () => ({}); |
448 | 396 | }
|
449 | 397 |
|
450 |
| - routes[id] = wrappedRoute; |
| 398 | + // We want to wrap the root loader regardless of whether it's already wrapped before. |
| 399 | + fill(wrappedRoute.module, 'loader', makeWrappedRootLoader); |
451 | 400 | }
|
452 | 401 |
|
453 |
| - const newBuild = { ...build, routes, entry: wrappedEntry }; |
| 402 | + routes[id] = wrappedRoute; |
| 403 | + } |
| 404 | + |
| 405 | + return { ...build, routes, entry: wrappedEntry }; |
| 406 | +} |
| 407 | + |
| 408 | +function makeWrappedCreateRequestHandler( |
| 409 | + origCreateRequestHandler: CreateRequestHandlerFunction, |
| 410 | +): CreateRequestHandlerFunction { |
| 411 | + // To track if this wrapper has been applied, before other wrappers. |
| 412 | + // Can't track `__sentry_original__` because it's not the same function as the potentially manually wrapped one. |
| 413 | + isRequestHandlerWrapped = true; |
454 | 414 |
|
455 |
| - const requestHandler = origCreateRequestHandler.call(this, newBuild, mode); |
| 415 | + return function (this: unknown, build: ServerBuild, ...args: unknown[]): RequestHandler { |
| 416 | + const newBuild = instrumentBuild(build); |
| 417 | + const requestHandler = origCreateRequestHandler.call(this, newBuild, ...args); |
456 | 418 |
|
457 | 419 | return wrapRequestHandler(requestHandler, newBuild);
|
458 | 420 | };
|
|
0 commit comments