Skip to content

Commit

Permalink
feat(remix): Add Worker Runtime support to Remix SDK.
Browse files Browse the repository at this point in the history
  • Loading branch information
onurtemizkan committed Oct 11, 2023
1 parent 3c98e45 commit 9103c6b
Show file tree
Hide file tree
Showing 17 changed files with 421 additions and 107 deletions.
9 changes: 6 additions & 3 deletions packages/remix/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,13 @@
"@sentry/types": "7.73.0",
"@sentry/utils": "7.73.0",
"glob": "^10.3.4",
"is-ip": "^3.1.0",
"tslib": "^2.4.1 || ^1.9.3",
"yargs": "^17.6.0"
},
"devDependencies": {
"@remix-run/node": "^1.4.3",
"@remix-run/react": "^1.4.3",
"@remix-run/node": "^1.19.3",
"@remix-run/react": "^1.19.3",
"@types/express": "^4.17.14"
},
"peerDependencies": {
Expand Down Expand Up @@ -86,6 +87,8 @@
},
"sideEffects": [
"./esm/index.server.js",
"./src/index.server.ts"
"./src/index.server.ts",
"./src/index.worker.ts",
"./esm/index.worker.js"
]
}
2 changes: 1 addition & 1 deletion packages/remix/rollup.npm.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'

export default makeNPMConfigVariants(
makeBaseNPMConfig({
entrypoints: ['src/index.server.ts', 'src/index.client.tsx'],
entrypoints: ['src/index.server.ts', 'src/index.client.tsx', 'src/index.worker.ts'],
packageSpecificConfig: {
external: ['react-router', 'react-router-dom'],
output: {
Expand Down
4 changes: 2 additions & 2 deletions packages/remix/src/client/errors.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { captureException, withScope } from '@sentry/core';
import { addExceptionMechanism, isNodeEnv, isString } from '@sentry/utils';
import { addExceptionMechanism, isBrowser, isString } from '@sentry/utils';

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

Expand All @@ -11,7 +11,7 @@ import { isRouteErrorResponse } from '../utils/vendor/response';
*/
export function captureRemixErrorBoundaryError(error: unknown): string | undefined {
let eventId: string | undefined;
const isClientSideRuntimeError = !isNodeEnv() && error instanceof Error;
const isClientSideRuntimeError = isBrowser() && error instanceof Error;
const isRemixErrorResponse = isRouteErrorResponse(error);
// Server-side errors apart from `ErrorResponse`s also appear here without their stacktraces.
// So, we only capture:
Expand Down
4 changes: 2 additions & 2 deletions packages/remix/src/client/performance.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ErrorBoundaryProps } from '@sentry/react';
import { WINDOW, withErrorBoundary } from '@sentry/react';
import type { Transaction, TransactionContext } from '@sentry/types';
import { isNodeEnv, logger } from '@sentry/utils';
import { isBrowser, logger } from '@sentry/utils';
import * as React from 'react';

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

// @ts-expect-error Setting more specific React Component typing for `R` generic above
Expand Down
57 changes: 56 additions & 1 deletion packages/remix/src/index.client.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,28 @@
/* eslint-disable import/export */
import { configureScope, init as reactInit } from '@sentry/react';
import { init as reactInit } from '@sentry/react';

import { buildMetadata } from './utils/metadata';
import type { RemixOptions } from './utils/remixOptions';
export { remixRouterInstrumentation, withSentry } from './client/performance';
export { captureRemixErrorBoundaryError } from './client/errors';
export * from '@sentry/react';
import type { ServerRuntimeClientOptions } from '@sentry/core';
import {
configureScope,
getCurrentHub,
getIntegrationsToSetup,
initAndBind,
ServerRuntimeClient,
startTransaction,
} from '@sentry/core';
import { createStackParser, logger, nodeStackLineParser, stackParserFromStackParserOptions } from '@sentry/utils';

import { makeEdgeTransport } from './worker/transport';

export { captureRemixServerException } from './utils/instrumentServer';
export { ErrorBoundary, withErrorBoundary } from '@sentry/react';
// export { wrapExpressCreateRequestHandler } from './utils/serverAdapters/express';
export { wrapWorkerCreateRequestHandler } from './utils/serverAdapters/worker';

export function init(options: RemixOptions): void {
buildMetadata(options, ['remix', 'react']);
Expand All @@ -17,3 +34,41 @@ export function init(options: RemixOptions): void {
scope.setTag('runtime', 'browser');
});
}

const nodeStackParser = createStackParser(nodeStackLineParser());

function sdkAlreadyInitialized(): boolean {
const hub = getCurrentHub();
return !!hub.getClient();
}

/** Initializes Sentry Remix SDK on Node. */
export function workerInit(options: RemixOptions): void {
buildMetadata(options, ['remix', 'node']);

if (sdkAlreadyInitialized()) {
__DEBUG_BUILD__ && logger.log('SDK already initialized');

return;
}

const clientOptions: ServerRuntimeClientOptions = {
...options,
stackParser: stackParserFromStackParserOptions(options.stackParser || nodeStackParser),
integrations: getIntegrationsToSetup(options),
transport: options.transport || makeEdgeTransport,
};

initAndBind(ServerRuntimeClient, clientOptions);

configureScope(scope => {
scope.setTag('runtime', 'worker');
});

const transaction = startTransaction({
name: 'remix-main',
op: 'init',
});

transaction.finish();
}
1 change: 1 addition & 0 deletions packages/remix/src/index.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export { ErrorBoundary, withErrorBoundary } from '@sentry/react';
export { remixRouterInstrumentation, withSentry } from './client/performance';
export { captureRemixErrorBoundaryError } from './client/errors';
export { wrapExpressCreateRequestHandler } from './utils/serverAdapters/express';
export { wrapWorkerCreateRequestHandler } from './utils/serverAdapters/worker';

function sdkAlreadyInitialized(): boolean {
const hub = getCurrentHub();
Expand Down
46 changes: 46 additions & 0 deletions packages/remix/src/index.worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { ServerRuntimeClientOptions } from '@sentry/core';
import { configureScope, getCurrentHub, getIntegrationsToSetup, initAndBind, ServerRuntimeClient } from '@sentry/core';
import { createStackParser, logger, nodeStackLineParser, stackParserFromStackParserOptions } from '@sentry/utils';

import { instrumentServer } from './utils/instrumentServer';
import { buildMetadata } from './utils/metadata';
import type { RemixOptions } from './utils/remixOptions';
import { makeEdgeTransport } from './worker/transport';

export { captureRemixServerException } from './utils/instrumentServer';
export { ErrorBoundary, withErrorBoundary } from '@sentry/react';
export { remixRouterInstrumentation, withSentry } from './client/performance';
export { captureRemixErrorBoundaryError } from './client/errors';
export { wrapExpressCreateRequestHandler } from './utils/serverAdapters/express';

const nodeStackParser = createStackParser(nodeStackLineParser());

function sdkAlreadyInitialized(): boolean {
const hub = getCurrentHub();
return !!hub.getClient();
}

/** Initializes Sentry Remix SDK on Node. */
export function init(options: RemixOptions): void {
buildMetadata(options, ['remix', 'node']);

if (sdkAlreadyInitialized()) {
__DEBUG_BUILD__ && logger.log('SDK already initialized');

return;
}

const clientOptions: ServerRuntimeClientOptions = {
...options,
stackParser: stackParserFromStackParserOptions(options.stackParser || nodeStackParser),
integrations: getIntegrationsToSetup(options),
transport: options.transport || makeEdgeTransport,
};

initAndBind(ServerRuntimeClient, clientOptions);
instrumentServer();

configureScope(scope => {
scope.setTag('runtime', 'worker');
});
}
32 changes: 18 additions & 14 deletions packages/remix/src/utils/instrumentServer.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
/* eslint-disable max-lines */
import { getActiveTransaction, hasTracingEnabled, runWithAsyncContext } from '@sentry/core';
import type { Hub } from '@sentry/node';
import { captureException, getCurrentHub } from '@sentry/node';
import type { Hub } from '@sentry/core';
import {
captureException,
getActiveTransaction,
getCurrentHub,
hasTracingEnabled,
runWithAsyncContext,
} from '@sentry/core';
import type { Transaction, TransactionSource, WrappedFunction } from '@sentry/types';
import {
addExceptionMechanism,
dynamicSamplingContextToSentryBaggageHeader,
fill,
isNodeEnv,
isBrowser,
loadModule,
logger,
tracingContextFromHeaders,
Expand Down Expand Up @@ -236,7 +241,7 @@ function getTraceAndBaggage(): { sentryTrace?: string; sentryBaggage?: string }
const transaction = getActiveTransaction();
const currentScope = getCurrentHub().getScope();

if (isNodeEnv() && hasTracingEnabled()) {
if (!isBrowser() && hasTracingEnabled()) {
const span = currentScope.getSpan();

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

function makeWrappedRootLoader(remixVersion: number) {
function makeWrappedRootLoader() {
return function (origLoader: DataFunction): DataFunction {
return async function (this: unknown, args: DataFunctionArgs): Promise<Response | AppData> {
const res = await origLoader.call(this, args);
const traceAndBaggage = getTraceAndBaggage();

if (isDeferredData(res)) {
return {
...res.data,
...traceAndBaggage,
remixVersion,
};
res.data['sentryTrace'] = traceAndBaggage.sentryTrace;
res.data['sentryBaggage'] = traceAndBaggage.sentryBaggage;

return res;
}

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

if (typeof data === 'object') {
return json(
{ ...data, ...traceAndBaggage, remixVersion },
{ ...data, ...traceAndBaggage },
{ headers: res.headers, statusText: res.statusText, status: res.status },
);
} else {
Expand All @@ -290,7 +294,7 @@ function makeWrappedRootLoader(remixVersion: number) {
}
}

return { ...res, ...traceAndBaggage, remixVersion };
return { ...res, ...traceAndBaggage };
};
};
}
Expand Down Expand Up @@ -462,7 +466,7 @@ export function instrumentBuild(build: ServerBuild): ServerBuild {
}

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

routes[id] = wrappedRoute;
Expand Down
83 changes: 83 additions & 0 deletions packages/remix/src/utils/serverAdapters/worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { getCurrentHub, hasTracingEnabled } from '@sentry/core';
import { isString, logger } from '@sentry/utils';

import {
createRoutes,
getTransactionName,
instrumentBuild,
isRequestHandlerWrapped,
startRequestHandlerTransaction,
} from '../instrumentServer';
import type { ReactRouterDomPkg, ServerBuild } from '../vendor/types';

type WorkerRequestHandler = (request: Request) => Promise<Response>;
export type WorkerCreateRequestHandler = (this: unknown, options: any) => WorkerRequestHandler;
type WorkerRequestHandlerOptions = {
build: ServerBuild;
mode?: string;
poweredByHeader?: boolean;
getLoadContext?: (request: Request) => Promise<unknown> | unknown;
};

let pkg: ReactRouterDomPkg;

function wrapWorkerRequestHandler(origRequestHandler: WorkerRequestHandler, build: ServerBuild): WorkerRequestHandler {
const routes = createRoutes(build.routes);

// If the core request handler is already wrapped, don't wrap Express handler which uses it.
if (isRequestHandlerWrapped) {
return origRequestHandler;
}

return async function (this: unknown, request: Request): Promise<Response> {
if (!pkg) {
try {
pkg = await import('react-router-dom');
} finally {
if (!pkg) {
__DEBUG_BUILD__ && logger.error('Could not find `react-router-dom` package.');
}
}
}

const hub = getCurrentHub();
const options = hub.getClient()?.getOptions();
const scope = hub.getScope();

scope.setSDKProcessingMetadata({ request });

if (!options || !hasTracingEnabled(options) || !request.url || !request.method) {
return origRequestHandler.call(this, request);
}

const url = new URL(request.url);
const [name, source] = getTransactionName(routes, url, pkg);
startRequestHandlerTransaction(hub, name, source, {
headers: {
'sentry-trace':
(request.headers && isString(request.headers.get('sentry-trace')) && request.headers.get('sentry-trace')) ||
'',
baggage: (request.headers && isString(request.headers.get('baggage')) && request.headers.get('baggage')) || '',
},
method: request.method,
});

return origRequestHandler.call(this, request);
};
}

/**
* Instruments `createRequestHandler` from `@remix-run/express`
*/
export function wrapWorkerCreateRequestHandler(
origCreateRequestHandler: WorkerCreateRequestHandler,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): (options: any) => WorkerRequestHandler {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return function (this: unknown, options: any): WorkerRequestHandler {
const newBuild = instrumentBuild((options as WorkerRequestHandlerOptions).build);
const requestHandler = origCreateRequestHandler.call(this, { ...options, build: newBuild });

return wrapWorkerRequestHandler(requestHandler, newBuild);
};
}
3 changes: 1 addition & 2 deletions packages/remix/src/utils/vendor/getIpAddress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

import { isIP } from 'net';

import isIP from 'is-ip';
/**
* Get the IP address of the client sending a request.
*
Expand Down
2 changes: 1 addition & 1 deletion packages/remix/src/utils/vendor/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ export interface DataFunction {
}

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

// Taken from Remix Implementation
Expand Down
Loading

0 comments on commit 9103c6b

Please sign in to comment.