Skip to content

Commit 51ee37d

Browse files
committed
feat(remix): Add Worker Runtime support to Remix SDK.
1 parent 3c98e45 commit 51ee37d

17 files changed

+419
-107
lines changed

packages/remix/package.json

+6-3
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,13 @@
3434
"@sentry/types": "7.73.0",
3535
"@sentry/utils": "7.73.0",
3636
"glob": "^10.3.4",
37+
"is-ip": "^3.1.0",
3738
"tslib": "^2.4.1 || ^1.9.3",
3839
"yargs": "^17.6.0"
3940
},
4041
"devDependencies": {
41-
"@remix-run/node": "^1.4.3",
42-
"@remix-run/react": "^1.4.3",
42+
"@remix-run/node": "^1.19.3",
43+
"@remix-run/react": "^1.19.3",
4344
"@types/express": "^4.17.14"
4445
},
4546
"peerDependencies": {
@@ -86,6 +87,8 @@
8687
},
8788
"sideEffects": [
8889
"./esm/index.server.js",
89-
"./src/index.server.ts"
90+
"./src/index.server.ts",
91+
"./src/index.worker.ts",
92+
"./esm/index.worker.js"
9093
]
9194
}

packages/remix/rollup.npm.config.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'
22

33
export default makeNPMConfigVariants(
44
makeBaseNPMConfig({
5-
entrypoints: ['src/index.server.ts', 'src/index.client.tsx'],
5+
entrypoints: ['src/index.server.ts', 'src/index.client.tsx', 'src/index.worker.ts'],
66
packageSpecificConfig: {
77
external: ['react-router', 'react-router-dom'],
88
output: {

packages/remix/src/client/errors.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { captureException, withScope } from '@sentry/core';
2-
import { addExceptionMechanism, isNodeEnv, isString } from '@sentry/utils';
2+
import { addExceptionMechanism, isBrowser, isString } from '@sentry/utils';
33

44
import { isRouteErrorResponse } from '../utils/vendor/response';
55

@@ -11,7 +11,7 @@ import { isRouteErrorResponse } from '../utils/vendor/response';
1111
*/
1212
export function captureRemixErrorBoundaryError(error: unknown): string | undefined {
1313
let eventId: string | undefined;
14-
const isClientSideRuntimeError = !isNodeEnv() && error instanceof Error;
14+
const isClientSideRuntimeError = isBrowser() && error instanceof Error;
1515
const isRemixErrorResponse = isRouteErrorResponse(error);
1616
// Server-side errors apart from `ErrorResponse`s also appear here without their stacktraces.
1717
// So, we only capture:

packages/remix/src/client/performance.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { ErrorBoundaryProps } from '@sentry/react';
22
import { WINDOW, withErrorBoundary } from '@sentry/react';
33
import type { Transaction, TransactionContext } from '@sentry/types';
4-
import { isNodeEnv, logger } from '@sentry/utils';
4+
import { isBrowser, logger } from '@sentry/utils';
55
import * as React from 'react';
66

77
import { getFutureFlagsBrowser, readRemixVersionFromLoader } from '../utils/futureFlags';
@@ -109,7 +109,7 @@ export function withSentry<P extends Record<string, unknown>, R extends React.Co
109109
// Early return when any of the required functions is not available.
110110
if (!_useEffect || !_useLocation || !_useMatches || !_customStartTransaction) {
111111
__DEBUG_BUILD__ &&
112-
!isNodeEnv() &&
112+
isBrowser() &&
113113
logger.warn('Remix SDK was unable to wrap your root because of one or more missing parameters.');
114114

115115
// @ts-expect-error Setting more specific React Component typing for `R` generic above

packages/remix/src/index.client.tsx

+56-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,28 @@
11
/* eslint-disable import/export */
2-
import { configureScope, init as reactInit } from '@sentry/react';
2+
import { init as reactInit } from '@sentry/react';
33

44
import { buildMetadata } from './utils/metadata';
55
import type { RemixOptions } from './utils/remixOptions';
66
export { remixRouterInstrumentation, withSentry } from './client/performance';
77
export { captureRemixErrorBoundaryError } from './client/errors';
88
export * from '@sentry/react';
9+
import type { ServerRuntimeClientOptions } from '@sentry/core';
10+
import {
11+
configureScope,
12+
getCurrentHub,
13+
getIntegrationsToSetup,
14+
initAndBind,
15+
ServerRuntimeClient,
16+
startTransaction,
17+
} from '@sentry/core';
18+
import { createStackParser, logger, nodeStackLineParser, stackParserFromStackParserOptions } from '@sentry/utils';
19+
20+
import { makeEdgeTransport } from './worker/transport';
21+
22+
export { captureRemixServerException } from './utils/instrumentServer';
23+
export { ErrorBoundary, withErrorBoundary } from '@sentry/react';
24+
// export { wrapExpressCreateRequestHandler } from './utils/serverAdapters/express';
25+
export { wrapWorkerCreateRequestHandler } from './utils/serverAdapters/worker';
926

1027
export function init(options: RemixOptions): void {
1128
buildMetadata(options, ['remix', 'react']);
@@ -17,3 +34,41 @@ export function init(options: RemixOptions): void {
1734
scope.setTag('runtime', 'browser');
1835
});
1936
}
37+
38+
const nodeStackParser = createStackParser(nodeStackLineParser());
39+
40+
function sdkAlreadyInitialized(): boolean {
41+
const hub = getCurrentHub();
42+
return !!hub.getClient();
43+
}
44+
45+
/** Initializes Sentry Remix SDK on Node. */
46+
export function workerInit(options: RemixOptions): void {
47+
buildMetadata(options, ['remix', 'node']);
48+
49+
if (sdkAlreadyInitialized()) {
50+
__DEBUG_BUILD__ && logger.log('SDK already initialized');
51+
52+
return;
53+
}
54+
55+
const clientOptions: ServerRuntimeClientOptions = {
56+
...options,
57+
stackParser: stackParserFromStackParserOptions(options.stackParser || nodeStackParser),
58+
integrations: getIntegrationsToSetup(options),
59+
transport: options.transport || makeEdgeTransport,
60+
};
61+
62+
initAndBind(ServerRuntimeClient, clientOptions);
63+
64+
configureScope(scope => {
65+
scope.setTag('runtime', 'worker');
66+
});
67+
68+
const transaction = startTransaction({
69+
name: 'remix-main',
70+
op: 'init',
71+
});
72+
73+
transaction.finish();
74+
}

packages/remix/src/index.server.ts

+1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export { ErrorBoundary, withErrorBoundary } from '@sentry/react';
6060
export { remixRouterInstrumentation, withSentry } from './client/performance';
6161
export { captureRemixErrorBoundaryError } from './client/errors';
6262
export { wrapExpressCreateRequestHandler } from './utils/serverAdapters/express';
63+
export { wrapWorkerCreateRequestHandler } from './utils/serverAdapters/worker';
6364

6465
function sdkAlreadyInitialized(): boolean {
6566
const hub = getCurrentHub();

packages/remix/src/index.worker.ts

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import type { ServerRuntimeClientOptions } from '@sentry/core';
2+
import { configureScope, getCurrentHub, getIntegrationsToSetup, initAndBind, ServerRuntimeClient } from '@sentry/core';
3+
import { createStackParser, logger, nodeStackLineParser, stackParserFromStackParserOptions } from '@sentry/utils';
4+
5+
import { instrumentServer } from './utils/instrumentServer';
6+
import { buildMetadata } from './utils/metadata';
7+
import type { RemixOptions } from './utils/remixOptions';
8+
import { makeEdgeTransport } from './worker/transport';
9+
10+
export { captureRemixServerException } from './utils/instrumentServer';
11+
export { ErrorBoundary, withErrorBoundary } from '@sentry/react';
12+
export { remixRouterInstrumentation, withSentry } from './client/performance';
13+
export { captureRemixErrorBoundaryError } from './client/errors';
14+
export { wrapExpressCreateRequestHandler } from './utils/serverAdapters/express';
15+
16+
const nodeStackParser = createStackParser(nodeStackLineParser());
17+
18+
function sdkAlreadyInitialized(): boolean {
19+
const hub = getCurrentHub();
20+
return !!hub.getClient();
21+
}
22+
23+
/** Initializes Sentry Remix SDK on Node. */
24+
export function init(options: RemixOptions): void {
25+
buildMetadata(options, ['remix', 'node']);
26+
27+
if (sdkAlreadyInitialized()) {
28+
__DEBUG_BUILD__ && logger.log('SDK already initialized');
29+
30+
return;
31+
}
32+
33+
const clientOptions: ServerRuntimeClientOptions = {
34+
...options,
35+
stackParser: stackParserFromStackParserOptions(options.stackParser || nodeStackParser),
36+
integrations: getIntegrationsToSetup(options),
37+
transport: options.transport || makeEdgeTransport,
38+
};
39+
40+
initAndBind(ServerRuntimeClient, clientOptions);
41+
instrumentServer();
42+
43+
configureScope(scope => {
44+
scope.setTag('runtime', 'worker');
45+
});
46+
}

packages/remix/src/utils/instrumentServer.ts

+18-14
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
/* eslint-disable max-lines */
2-
import { getActiveTransaction, hasTracingEnabled, runWithAsyncContext } from '@sentry/core';
3-
import type { Hub } from '@sentry/node';
4-
import { captureException, getCurrentHub } from '@sentry/node';
2+
import type { Hub } from '@sentry/core';
3+
import {
4+
captureException,
5+
getActiveTransaction,
6+
getCurrentHub,
7+
hasTracingEnabled,
8+
runWithAsyncContext,
9+
} from '@sentry/core';
510
import type { Transaction, TransactionSource, WrappedFunction } from '@sentry/types';
611
import {
712
addExceptionMechanism,
813
dynamicSamplingContextToSentryBaggageHeader,
914
fill,
10-
isNodeEnv,
15+
isBrowser,
1116
loadModule,
1217
logger,
1318
tracingContextFromHeaders,
@@ -236,7 +241,7 @@ function getTraceAndBaggage(): { sentryTrace?: string; sentryBaggage?: string }
236241
const transaction = getActiveTransaction();
237242
const currentScope = getCurrentHub().getScope();
238243

239-
if (isNodeEnv() && hasTracingEnabled()) {
244+
if (!isBrowser() && hasTracingEnabled()) {
240245
const span = currentScope.getSpan();
241246

242247
if (span && transaction) {
@@ -252,18 +257,17 @@ function getTraceAndBaggage(): { sentryTrace?: string; sentryBaggage?: string }
252257
return {};
253258
}
254259

255-
function makeWrappedRootLoader(remixVersion: number) {
260+
function makeWrappedRootLoader() {
256261
return function (origLoader: DataFunction): DataFunction {
257262
return async function (this: unknown, args: DataFunctionArgs): Promise<Response | AppData> {
258263
const res = await origLoader.call(this, args);
259264
const traceAndBaggage = getTraceAndBaggage();
260265

261266
if (isDeferredData(res)) {
262-
return {
263-
...res.data,
264-
...traceAndBaggage,
265-
remixVersion,
266-
};
267+
res.data['sentryTrace'] = traceAndBaggage.sentryTrace;
268+
res.data['sentryBaggage'] = traceAndBaggage.sentryBaggage;
269+
270+
return res;
267271
}
268272

269273
if (isResponse(res)) {
@@ -279,7 +283,7 @@ function makeWrappedRootLoader(remixVersion: number) {
279283

280284
if (typeof data === 'object') {
281285
return json(
282-
{ ...data, ...traceAndBaggage, remixVersion },
286+
{ ...data, ...traceAndBaggage },
283287
{ headers: res.headers, statusText: res.statusText, status: res.status },
284288
);
285289
} else {
@@ -290,7 +294,7 @@ function makeWrappedRootLoader(remixVersion: number) {
290294
}
291295
}
292296

293-
return { ...res, ...traceAndBaggage, remixVersion };
297+
return { ...res, ...traceAndBaggage };
294298
};
295299
};
296300
}
@@ -462,7 +466,7 @@ export function instrumentBuild(build: ServerBuild): ServerBuild {
462466
}
463467

464468
// We want to wrap the root loader regardless of whether it's already wrapped before.
465-
fill(wrappedRoute.module, 'loader', makeWrappedRootLoader(remixVersion));
469+
fill(wrappedRoute.module, 'loader', makeWrappedRootLoader());
466470
}
467471

468472
routes[id] = wrappedRoute;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { getCurrentHub, hasTracingEnabled } from '@sentry/core';
2+
import { isString, logger } from '@sentry/utils';
3+
4+
import {
5+
createRoutes,
6+
getTransactionName,
7+
instrumentBuild,
8+
isRequestHandlerWrapped,
9+
startRequestHandlerTransaction,
10+
} from '../instrumentServer';
11+
import type { ReactRouterDomPkg, ServerBuild } from '../vendor/types';
12+
13+
type WorkerRequestHandler = (request: Request) => Promise<Response>;
14+
export type WorkerCreateRequestHandler = (this: unknown, options: any) => WorkerRequestHandler;
15+
type WorkerRequestHandlerOptions = {
16+
build: ServerBuild;
17+
mode?: string;
18+
poweredByHeader?: boolean;
19+
getLoadContext?: (request: Request) => Promise<unknown> | unknown;
20+
};
21+
22+
let pkg: ReactRouterDomPkg;
23+
24+
function wrapWorkerRequestHandler(origRequestHandler: WorkerRequestHandler, build: ServerBuild): WorkerRequestHandler {
25+
const routes = createRoutes(build.routes);
26+
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+
32+
return async function (this: unknown, request: Request): Promise<Response> {
33+
if (!pkg) {
34+
try {
35+
pkg = await import('react-router-dom');
36+
} finally {
37+
if (!pkg) {
38+
__DEBUG_BUILD__ && logger.error('Could not find `react-router-dom` package.');
39+
}
40+
}
41+
}
42+
43+
const hub = getCurrentHub();
44+
const options = hub.getClient()?.getOptions();
45+
const scope = hub.getScope();
46+
47+
scope.setSDKProcessingMetadata({ request });
48+
49+
if (!options || !hasTracingEnabled(options) || !request.url || !request.method) {
50+
return origRequestHandler.call(this, request);
51+
}
52+
53+
const url = new URL(request.url);
54+
const [name, source] = getTransactionName(routes, url, pkg);
55+
startRequestHandlerTransaction(hub, name, source, {
56+
headers: {
57+
'sentry-trace':
58+
(request.headers && isString(request.headers.get('sentry-trace')) && request.headers.get('sentry-trace')) ||
59+
'',
60+
baggage: (request.headers && isString(request.headers.get('baggage')) && request.headers.get('baggage')) || '',
61+
},
62+
method: request.method,
63+
});
64+
65+
return origRequestHandler.call(this, request);
66+
};
67+
}
68+
69+
/**
70+
* Instruments `createRequestHandler` from `@remix-run/express`
71+
*/
72+
export function wrapWorkerCreateRequestHandler(
73+
origCreateRequestHandler: WorkerCreateRequestHandler,
74+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
75+
): (options: any) => WorkerRequestHandler {
76+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
77+
return function (this: unknown, options: any): WorkerRequestHandler {
78+
const newBuild = instrumentBuild((options as WorkerRequestHandlerOptions).build);
79+
const requestHandler = origCreateRequestHandler.call(this, { ...options, build: newBuild });
80+
81+
return wrapWorkerRequestHandler(requestHandler, newBuild);
82+
};
83+
}

packages/remix/src/utils/vendor/getIpAddress.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@
2323
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2424
// SOFTWARE.
2525

26-
import { isIP } from 'net';
27-
26+
import isIP from 'is-ip';
2827
/**
2928
* Get the IP address of the client sending a request.
3029
*

packages/remix/src/utils/vendor/types.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ export interface DataFunction {
202202
}
203203

204204
export interface ReactRouterDomPkg {
205-
matchRoutes: (routes: ServerRoute[], pathname: string) => RouteMatch<ServerRoute>[] | null;
205+
matchRoutes: (routes: any[], pathname: string) => RouteMatch<ServerRoute>[] | null;
206206
}
207207

208208
// Taken from Remix Implementation

0 commit comments

Comments
 (0)