Skip to content

Commit 6fab0cb

Browse files
committed
Prevent multiple wrappings when using a manual wrapper.
1 parent 083f2a3 commit 6fab0cb

File tree

6 files changed

+51
-16
lines changed

6 files changed

+51
-16
lines changed

packages/remix/src/utils/instrumentServer.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/* eslint-disable max-lines */
22
import { captureException, getCurrentHub } from '@sentry/node';
33
import { getActiveTransaction, hasTracingEnabled, Span } from '@sentry/tracing';
4+
import { WrappedFunction } from '@sentry/types';
45
import { addExceptionMechanism, fill, isNodeEnv, loadModule, logger, serializeBaggage } from '@sentry/utils';
56

67
import {
@@ -17,6 +18,9 @@ import {
1718
ServerRouteManifest,
1819
} from './types';
1920

21+
// Flag to track if the core request handler is instrumented.
22+
export let isRequestHandlerWrapped = false;
23+
2024
// Taken from Remix Implementation
2125
// https://github.com/remix-run/remix/blob/32300ec6e6e8025602cea63e17a2201989589eab/packages/remix-server-runtime/responses.ts#L60-L77
2226
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -365,16 +369,22 @@ export function instrumentBuild(build: ServerBuild): ServerBuild {
365369

366370
const wrappedEntry = { ...build.entry, module: { ...build.entry.module } };
367371

368-
fill(wrappedEntry.module, 'default', makeWrappedDocumentRequestFunction);
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__) {
377+
fill(wrappedEntry.module, 'default', makeWrappedDocumentRequestFunction);
378+
}
369379

370380
for (const [id, route] of Object.entries(build.routes)) {
371381
const wrappedRoute = { ...route, module: { ...route.module } };
372382

373-
if (wrappedRoute.module.action) {
383+
if (wrappedRoute.module.action && !(wrappedRoute.module.action as WrappedFunction).__sentry_original__) {
374384
fill(wrappedRoute.module, 'action', makeWrappedAction(id));
375385
}
376386

377-
if (wrappedRoute.module.loader) {
387+
if (wrappedRoute.module.loader && !(wrappedRoute.module.loader as WrappedFunction).__sentry_original__) {
378388
fill(wrappedRoute.module, 'loader', makeWrappedLoader(id));
379389
}
380390

@@ -385,6 +395,7 @@ export function instrumentBuild(build: ServerBuild): ServerBuild {
385395
wrappedRoute.module.loader = () => ({});
386396
}
387397

398+
// We want to wrap the root loader regardless of whether it's already wrapped before.
388399
fill(wrappedRoute.module, 'loader', makeWrappedRootLoader);
389400
}
390401

@@ -397,6 +408,10 @@ export function instrumentBuild(build: ServerBuild): ServerBuild {
397408
function makeWrappedCreateRequestHandler(
398409
origCreateRequestHandler: CreateRequestHandlerFunction,
399410
): 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;
414+
400415
return function (this: unknown, build: ServerBuild, ...args: unknown[]): RequestHandler {
401416
const newBuild = instrumentBuild(build);
402417
const requestHandler = origCreateRequestHandler.call(this, newBuild, ...args);

packages/remix/src/utils/serverAdapters/express.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { extractRequestData, loadModule } from '@sentry/utils';
22

3-
import { createRoutes, instrumentBuild, startRequestHandlerTransaction } from '../instrumentServer';
3+
import {
4+
createRoutes,
5+
instrumentBuild,
6+
isRequestHandlerWrapped,
7+
startRequestHandlerTransaction,
8+
} from '../instrumentServer';
49
import {
510
ExpressCreateRequestHandler,
611
ExpressCreateRequestHandlerOptions,
@@ -19,6 +24,11 @@ function wrapExpressRequestHandler(
1924
const routes = createRoutes(build.routes);
2025
const pkg = loadModule<ReactRouterDomPkg>('react-router-dom');
2126

27+
// If the core request handler is already wrapped, don't wrap Express handler which uses it.
28+
if (isRequestHandlerWrapped) {
29+
return origRequestHandler;
30+
}
31+
2232
return async function (
2333
this: unknown,
2434
req: ExpressRequest,

packages/remix/test/integration/app/routes/action-json-response/$id.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export const action: ActionFunction = async ({ params: { id } }) => {
1313
}
1414

1515
if (id === '-2') {
16-
// Note: This GET request triggers to the `Loader` of the URL, not the `Action`.
16+
// Note: This GET request triggers the `Loader` of the URL, not the `Action`.
1717
throw redirect('/action-json-response/-1');
1818
}
1919

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ import {
88

99
jest.spyOn(console, 'error').mockImplementation();
1010

11-
describe('Remix API Actions', () => {
11+
// Repeat tests for each adapter
12+
describe.each(['builtin', 'express'])('Remix API Actions with adapter = %s', adapter => {
1213
it('correctly instruments a parameterized Remix API action', async () => {
13-
const baseURL = await runServer();
14+
const baseURL = await runServer(adapter);
1415
const url = `${baseURL}/action-json-response/123123`;
1516
const envelope = await getEnvelopeRequest(url, 'post');
1617
const transaction = envelope[2];
@@ -39,7 +40,7 @@ describe('Remix API Actions', () => {
3940
});
4041

4142
it('reports an error thrown from the action', async () => {
42-
const baseURL = await runServer();
43+
const baseURL = await runServer(adapter);
4344
const url = `${baseURL}/action-json-response/-1`;
4445

4546
const [transaction, event] = await getMultipleEnvelopeRequest(url, 2, 'post');
@@ -76,7 +77,7 @@ describe('Remix API Actions', () => {
7677
});
7778

7879
it('handles a thrown 500 response', async () => {
79-
const baseURL = await runServer();
80+
const baseURL = await runServer(adapter);
8081
const url = `${baseURL}/action-json-response/-2`;
8182

8283
const [transaction_1, event, transaction_2] = await getMultipleEnvelopeRequest(url, 3, 'post');

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,17 @@ import {
88

99
jest.spyOn(console, 'error').mockImplementation();
1010

11-
describe('Remix API Loaders', () => {
11+
// Repeat tests for each adapter
12+
describe.each(['builtin', 'express'])('Remix API Loaders with adapter = %s', adapter => {
1213
it('reports an error thrown from the loader', async () => {
13-
const baseURL = await runServer();
14+
const baseURL = await runServer(adapter);
1415
const url = `${baseURL}/loader-json-response/-2`;
1516

16-
const [transaction, event] = await getMultipleEnvelopeRequest(url, 2);
17+
let [transaction, event] = await getMultipleEnvelopeRequest(url, 2);
18+
19+
// The event envelope is returned before the transaction envelope when using express adapter.
20+
// We can update this when we merge the envelope filtering utility.
21+
adapter === 'express' && ([event, transaction] = [transaction, event]);
1722

1823
assertSentryTransaction(transaction[2], {
1924
contexts: {
@@ -47,7 +52,7 @@ describe('Remix API Loaders', () => {
4752
});
4853

4954
it('correctly instruments a parameterized Remix API loader', async () => {
50-
const baseURL = await runServer();
55+
const baseURL = await runServer(adapter);
5156
const url = `${baseURL}/loader-json-response/123123`;
5257
const envelope = await getEnvelopeRequest(url);
5358
const transaction = envelope[2];
@@ -75,7 +80,7 @@ describe('Remix API Loaders', () => {
7580
});
7681

7782
it('handles a thrown 500 response', async () => {
78-
const baseURL = await runServer();
83+
const baseURL = await runServer(adapter);
7984
const url = `${baseURL}/loader-json-response/-1`;
8085

8186
const [transaction_1, event, transaction_2] = await getMultipleEnvelopeRequest(url, 3);

packages/remix/test/integration/test/server/utils/helpers.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
import express from 'express';
22
import { createRequestHandler } from '@remix-run/express';
33
import { getPortPromise } from 'portfinder';
4+
import { wrapExpressCreateRequestHandler } from '@sentry/remix';
45

56
export * from '../../../../../../node-integration-tests/utils';
67

78
/**
89
* Runs a test server
910
* @returns URL
1011
*/
11-
export async function runServer(): Promise<string> {
12+
export async function runServer(adapter: string = 'builtin'): Promise<string> {
13+
const requestHandlerFactory =
14+
adapter === 'express' ? wrapExpressCreateRequestHandler(createRequestHandler) : createRequestHandler;
15+
1216
const app = express();
1317
const port = await getPortPromise();
1418

15-
app.all('*', createRequestHandler({ build: require('../../../build') }));
19+
app.all('*', requestHandlerFactory({ build: require('../../../build') }));
1620

1721
const server = app.listen(port, () => {
1822
setTimeout(() => {

0 commit comments

Comments
 (0)