|
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