Skip to content

Commit d73e2e5

Browse files
committed
feat(remix): Export a manual wrapper for custom Express servers.
1 parent 9d38065 commit d73e2e5

File tree

4 files changed

+314
-148
lines changed

4 files changed

+314
-148
lines changed

packages/remix/src/index.server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export { ErrorBoundary, withErrorBoundary } from '@sentry/react';
1010
export { remixRouterInstrumentation, withSentry } from './performance/client';
1111
export { BrowserTracing, Integrations } from '@sentry/tracing';
1212
export * from '@sentry/node';
13+
export { wrapExpressCreateRequestHandler } from './utils/serverAdapters/express';
1314

1415
function sdkAlreadyInitialized(): boolean {
1516
const hub = getCurrentHub();

packages/remix/src/utils/instrumentServer.ts

Lines changed: 110 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -1,103 +1,31 @@
11
/* eslint-disable max-lines */
22
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';
10129

10230
// Taken from Remix Implementation
10331
// 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 {
290218
};
291219
}
292220

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[] {
294228
return Object.entries(manifest)
295229
.filter(([, route]) => route.parentId === parentId)
296230
.map(([id, route]) => ({
@@ -324,33 +258,55 @@ function matchServerRoutes(
324258
}));
325259
}
326260

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+
327305
function wrapRequestHandler(origRequestHandler: RequestHandler, build: ServerBuild): RequestHandler {
328306
const routes = createRoutes(build.routes);
329307
const pkg = loadModule<ReactRouterDomPkg>('react-router-dom');
330308
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);
354310

355311
const res = (await origRequestHandler.call(this, request, loadContext)) as Response;
356312

@@ -388,43 +344,49 @@ function getRequestMatch(url: URL, matches: RouteMatch<ServerRoute>[]): RouteMat
388344
return match;
389345
}
390346

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 = {};
398352

399-
fill(wrappedEntry.module, 'default', makeWrappedDocumentRequestFunction);
353+
const wrappedEntry = { ...build.entry, module: { ...build.entry.module } };
400354

401-
for (const [id, route] of Object.entries(build.routes)) {
402-
const wrappedRoute = { ...route, module: { ...route.module } };
355+
fill(wrappedEntry.module, 'default', makeWrappedDocumentRequestFunction);
403356

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 } };
407359

408-
if (wrappedRoute.module.loader) {
409-
fill(wrappedRoute.module, 'loader', makeWrappedLoader);
410-
}
360+
if (wrappedRoute.module.action) {
361+
fill(wrappedRoute.module, 'action', makeWrappedAction);
362+
}
411363

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+
}
418367

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 = () => ({});
420373
}
421374

422-
routes[id] = wrappedRoute;
375+
fill(wrappedRoute.module, 'loader', makeWrappedRootLoader);
423376
}
424377

425-
const newBuild = { ...build, routes, entry: wrappedEntry };
378+
routes[id] = wrappedRoute;
379+
}
380+
381+
return { ...build, routes, entry: wrappedEntry };
382+
}
426383

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);
428390

429391
return wrapRequestHandler(requestHandler, newBuild);
430392
};
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { loadModule } from '@sentry/utils';
2+
import type * as Express from 'express';
3+
4+
import { createRoutes, instrumentBuild, startRequestHandlerTransaction } from '../instrumentServer';
5+
import { ExpressCreateRequestHandler, ExpressRequestHandler, ReactRouterDomPkg, ServerBuild } from '../types';
6+
7+
interface ExpressCreateRequestHandlerOptions {
8+
build: ServerBuild;
9+
getLoadContext?: GetLoadContextFunction;
10+
mode?: string;
11+
}
12+
13+
type GetLoadContextFunction = (req: any, res: any) => any;
14+
15+
function wrapExpressRequestHandler(
16+
origRequestHandler: ExpressRequestHandler,
17+
build: ServerBuild,
18+
): ExpressRequestHandler {
19+
const routes = createRoutes(build.routes);
20+
const pkg = loadModule<ReactRouterDomPkg>('react-router-dom');
21+
22+
return async function (
23+
this: unknown,
24+
req: Express.Request,
25+
res: Express.Response,
26+
next: Express.NextFunction,
27+
): Promise<void> {
28+
const transaction = startRequestHandlerTransaction(req, routes, pkg);
29+
30+
await origRequestHandler.call(this, req, res, next);
31+
32+
transaction?.setHttpStatus(res.statusCode);
33+
transaction?.finish();
34+
};
35+
}
36+
37+
/**
38+
*
39+
*/
40+
export function wrapExpressCreateRequestHandler(
41+
origCreateRequestHandler: ExpressCreateRequestHandler,
42+
): (options: any) => ExpressRequestHandler {
43+
return function (this: unknown, options: any): ExpressRequestHandler {
44+
const newBuild = instrumentBuild((options as ExpressCreateRequestHandlerOptions).build);
45+
const requestHandler = origCreateRequestHandler.call(this, { ...options /* :, build newBuild */ });
46+
47+
return wrapExpressRequestHandler(requestHandler, newBuild);
48+
};
49+
}

0 commit comments

Comments
 (0)