Skip to content

Commit 6f4ad56

Browse files
authored
fix(remix): Paramaterize server side transactions (#5491)
This makes sure server side transactions are parameterized correctly in Remix. We do this by matching the server routes the same way Remix does under the hood, by reconciling the routes and URL pathname - https://github.com/remix-run/remix/blob/97999d02493e8114c39d48b76944069d58526e8d/packages/remix-server-runtime/server.ts#L36
1 parent cb925e3 commit 6f4ad56

File tree

2 files changed

+96
-15
lines changed

2 files changed

+96
-15
lines changed

packages/remix/src/utils/instrumentServer.ts

Lines changed: 90 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,7 @@
11
/* eslint-disable max-lines */
22
import { captureException, getCurrentHub } from '@sentry/node';
33
import { getActiveTransaction } from '@sentry/tracing';
4-
import {
5-
addExceptionMechanism,
6-
fill,
7-
loadModule,
8-
logger,
9-
serializeBaggage,
10-
stripUrlQueryAndFragment,
11-
} from '@sentry/utils';
4+
import { addExceptionMechanism, fill, loadModule, logger, serializeBaggage } from '@sentry/utils';
125

136
// Types vendored from @remix-run/[email protected]:
147
// https://github.com/remix-run/remix/blob/f3691d51027b93caa3fd2cdfe146d7b62a6eb8f2/packages/remix-server-runtime/server.ts
@@ -92,6 +85,18 @@ interface DataFunction {
9285
(args: DataFunctionArgs): Promise<Response> | Response | Promise<AppData> | AppData;
9386
}
9487

88+
interface ReactRouterDomPkg {
89+
matchRoutes: (routes: ServerRoute[], pathname: string) => RouteMatch<ServerRoute>[] | null;
90+
}
91+
92+
// Taken from Remix Implementation
93+
// https://github.com/remix-run/remix/blob/97999d02493e8114c39d48b76944069d58526e8d/packages/remix-server-runtime/routeMatching.ts#L6-L10
94+
export interface RouteMatch<Route> {
95+
params: Params;
96+
pathname: string;
97+
route: Route;
98+
}
99+
95100
// Taken from Remix Implementation
96101
// https://github.com/remix-run/remix/blob/7688da5c75190a2e29496c78721456d6e12e3abe/packages/remix-server-runtime/responses.ts#L54-L62
97102
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -262,18 +267,61 @@ function makeWrappedMeta(origMeta: MetaFunction | HtmlMetaDescriptor = {}): Meta
262267
};
263268
}
264269

265-
function wrapRequestHandler(origRequestHandler: RequestHandler): RequestHandler {
270+
function createRoutes(manifest: ServerRouteManifest, parentId?: string): ServerRoute[] {
271+
return Object.entries(manifest)
272+
.filter(([, route]) => route.parentId === parentId)
273+
.map(([id, route]) => ({
274+
...route,
275+
children: createRoutes(manifest, id),
276+
}));
277+
}
278+
279+
// Remix Implementation:
280+
// https://github.com/remix-run/remix/blob/38e127b1d97485900b9c220d93503de0deb1fc81/packages/remix-server-runtime/routeMatching.ts#L12-L24
281+
//
282+
// Changed so that `matchRoutes` function is passed in.
283+
function matchServerRoutes(
284+
routes: ServerRoute[],
285+
pathname: string,
286+
pkg?: ReactRouterDomPkg,
287+
): RouteMatch<ServerRoute>[] | null {
288+
if (!pkg) {
289+
return null;
290+
}
291+
292+
const matches = pkg.matchRoutes(routes, pathname);
293+
if (!matches) {
294+
return null;
295+
}
296+
297+
return matches.map(match => ({
298+
params: match.params,
299+
pathname: match.pathname,
300+
route: match.route,
301+
}));
302+
}
303+
304+
function wrapRequestHandler(origRequestHandler: RequestHandler, build: ServerBuild): RequestHandler {
305+
const routes = createRoutes(build.routes);
306+
const pkg = loadModule<ReactRouterDomPkg>('react-router-dom');
266307
return async function (this: unknown, request: Request, loadContext?: unknown): Promise<Response> {
267308
const hub = getCurrentHub();
268309
const currentScope = hub.getScope();
310+
311+
const url = new URL(request.url);
312+
const matches = matchServerRoutes(routes, url.pathname, pkg);
313+
314+
const match = matches && getRequestMatch(url, matches);
315+
const name = match === null ? url.pathname : match.route.id;
316+
const source = match === null ? 'url' : 'route';
269317
const transaction = hub.startTransaction({
270-
name: stripUrlQueryAndFragment(request.url),
318+
name,
271319
op: 'http.server',
272320
tags: {
273321
method: request.method,
274322
},
275323
metadata: {
276-
source: 'url',
324+
source,
277325
},
278326
});
279327

@@ -290,6 +338,33 @@ function wrapRequestHandler(origRequestHandler: RequestHandler): RequestHandler
290338
};
291339
}
292340

341+
// https://github.com/remix-run/remix/blob/97999d02493e8114c39d48b76944069d58526e8d/packages/remix-server-runtime/server.ts#L573-L586
342+
function isIndexRequestUrl(url: URL): boolean {
343+
for (const param of url.searchParams.getAll('index')) {
344+
// only use bare `?index` params without a value
345+
// ✅ /foo?index
346+
// ✅ /foo?index&index=123
347+
// ✅ /foo?index=123&index
348+
// ❌ /foo?index=123
349+
if (param === '') {
350+
return true;
351+
}
352+
}
353+
354+
return false;
355+
}
356+
357+
// https://github.com/remix-run/remix/blob/97999d02493e8114c39d48b76944069d58526e8d/packages/remix-server-runtime/server.ts#L588-L596
358+
function getRequestMatch(url: URL, matches: RouteMatch<ServerRoute>[]): RouteMatch<ServerRoute> {
359+
const match = matches.slice(-1)[0];
360+
361+
if (!isIndexRequestUrl(url) && match.route.id.endsWith('/index')) {
362+
return matches.slice(-2)[0];
363+
}
364+
365+
return match;
366+
}
367+
293368
function makeWrappedCreateRequestHandler(
294369
origCreateRequestHandler: CreateRequestHandlerFunction,
295370
): CreateRequestHandlerFunction {
@@ -316,9 +391,11 @@ function makeWrappedCreateRequestHandler(
316391
routes[id] = wrappedRoute;
317392
}
318393

319-
const requestHandler = origCreateRequestHandler.call(this, { ...build, routes, entry: wrappedEntry }, mode);
394+
const newBuild = { ...build, routes, entry: wrappedEntry };
395+
396+
const requestHandler = origCreateRequestHandler.call(this, newBuild, mode);
320397

321-
return wrapRequestHandler(requestHandler);
398+
return wrapRequestHandler(requestHandler, newBuild);
322399
};
323400
}
324401

packages/remix/test/integration/test/server/loader.test.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,17 @@ describe('Remix API Loaders', () => {
88
const transaction = envelope[2];
99

1010
assertSentryTransaction(transaction, {
11+
transaction: 'routes/loader-json-response/$id',
12+
transaction_info: {
13+
source: 'route',
14+
},
1115
spans: [
1216
{
13-
description: url,
17+
description: 'routes/loader-json-response/$id',
1418
op: 'remix.server.loader',
1519
},
1620
{
17-
description: url,
21+
description: 'routes/loader-json-response/$id',
1822
op: 'remix.server.documentRequest',
1923
},
2024
],

0 commit comments

Comments
 (0)