[]): void {
* @param OrigApp The Remix root to wrap
* @param options The options for ErrorBoundary wrapper.
*/
-export function withSentry, R extends React.ComponentType
>(OrigApp: R): R {
+export function withSentry
, R extends React.ComponentType
>(
+ OrigApp: R,
+ useEffect?: UseEffect,
+ useLocation?: UseLocation,
+ useMatches?: UseMatches,
+ instrumentNavigation?: boolean,
+): R {
const SentryRoot: React.FC
= (props: P) => {
+ setGlobals({ useEffect, useLocation, useMatches, instrumentNavigation: instrumentNavigation || true });
+
// Early return when any of the required functions is not available.
if (!_useEffect || !_useLocation || !_useMatches) {
DEBUG_BUILD &&
@@ -184,8 +192,8 @@ export function setGlobals({
useMatches?: UseMatches;
instrumentNavigation?: boolean;
}): void {
- _useEffect = useEffect;
- _useLocation = useLocation;
- _useMatches = useMatches;
- _instrumentNavigation = instrumentNavigation;
+ _useEffect = useEffect || _useEffect;
+ _useLocation = useLocation || _useLocation;
+ _useMatches = useMatches || _useMatches;
+ _instrumentNavigation = instrumentNavigation ?? _instrumentNavigation;
}
diff --git a/packages/remix/src/client/sdk.ts b/packages/remix/src/client/sdk.ts
new file mode 100644
index 000000000000..21b19e1aeb24
--- /dev/null
+++ b/packages/remix/src/client/sdk.ts
@@ -0,0 +1,21 @@
+/* eslint-enable @typescript-eslint/no-unused-vars */
+import type { Client } from '@sentry/core';
+import { applySdkMetadata } from '@sentry/core';
+import { init as reactInit } from '@sentry/react';
+import type { RemixOptions } from '../utils/remixOptions';
+
+/**
+ * Initializes the Remix SDK.
+ * @param options The configuration options.
+ * @returns The initialized SDK.
+ */
+export function init(options: RemixOptions): Client | undefined {
+ const opts = {
+ ...options,
+ environment: options.environment || process.env.NODE_ENV,
+ };
+
+ applySdkMetadata(opts, 'remix', ['remix', 'react']);
+
+ return reactInit(opts);
+}
diff --git a/packages/remix/src/cloudflare/index.ts b/packages/remix/src/cloudflare/index.ts
new file mode 100644
index 000000000000..c86548a39aee
--- /dev/null
+++ b/packages/remix/src/cloudflare/index.ts
@@ -0,0 +1,103 @@
+export * from '@sentry/react';
+
+export { captureRemixErrorBoundaryError } from '../client/errors';
+export { withSentry } from '../client/performance';
+
+import { instrumentBuild as instrumentRemixBuild, makeWrappedCreateRequestHandler } from '../server/instrumentServer';
+export { makeWrappedCreateRequestHandler };
+
+/**
+ * Instruments a Remix build to capture errors and performance data.
+ * @param build The Remix build to instrument.
+ * @returns The instrumented Remix build.
+ */
+export const instrumentBuild: typeof instrumentRemixBuild = build => {
+ return instrumentRemixBuild(build, {
+ instrumentTracing: true,
+ });
+};
+
+export type {
+ Breadcrumb,
+ BreadcrumbHint,
+ PolymorphicRequest,
+ RequestEventData,
+ SdkInfo,
+ Event,
+ EventHint,
+ ErrorEvent,
+ Exception,
+ Session,
+ SeverityLevel,
+ Span,
+ StackFrame,
+ Stacktrace,
+ Thread,
+ User,
+} from '@sentry/core';
+
+export {
+ addEventProcessor,
+ addBreadcrumb,
+ addIntegration,
+ captureException,
+ captureEvent,
+ captureMessage,
+ captureFeedback,
+ close,
+ createTransport,
+ lastEventId,
+ flush,
+ getClient,
+ isInitialized,
+ getCurrentScope,
+ getGlobalScope,
+ getIsolationScope,
+ setCurrentClient,
+ Scope,
+ SDK_VERSION,
+ setContext,
+ setExtra,
+ setExtras,
+ setTag,
+ setTags,
+ setUser,
+ getSpanStatusFromHttpCode,
+ setHttpStatus,
+ withScope,
+ withIsolationScope,
+ captureCheckIn,
+ withMonitor,
+ setMeasurement,
+ getActiveSpan,
+ getRootSpan,
+ getTraceData,
+ getTraceMetaTags,
+ startSpan,
+ startInactiveSpan,
+ startSpanManual,
+ startNewTrace,
+ suppressTracing,
+ withActiveSpan,
+ getSpanDescendants,
+ continueTrace,
+ functionToStringIntegration,
+ inboundFiltersIntegration,
+ linkedErrorsIntegration,
+ requestDataIntegration,
+ extraErrorDataIntegration,
+ dedupeIntegration,
+ rewriteFramesIntegration,
+ captureConsoleIntegration,
+ moduleMetadataIntegration,
+ zodErrorsIntegration,
+ SEMANTIC_ATTRIBUTE_SENTRY_OP,
+ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
+ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
+ SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE,
+ trpcMiddleware,
+ spanToJSON,
+ spanToTraceHeader,
+ spanToBaggageHeader,
+ updateSpanName,
+} from '@sentry/core';
diff --git a/packages/remix/src/index.client.ts b/packages/remix/src/index.client.ts
new file mode 100644
index 000000000000..4f1cce44fa36
--- /dev/null
+++ b/packages/remix/src/index.client.ts
@@ -0,0 +1 @@
+export * from './client';
diff --git a/packages/remix/src/index.client.tsx b/packages/remix/src/index.client.tsx
deleted file mode 100644
index 3f6d14294978..000000000000
--- a/packages/remix/src/index.client.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-/* eslint-enable @typescript-eslint/no-unused-vars */
-
-import type { Client } from '@sentry/core';
-import { applySdkMetadata, logger } from '@sentry/core';
-import { init as reactInit } from '@sentry/react';
-import { DEBUG_BUILD } from './utils/debug-build';
-import type { RemixOptions } from './utils/remixOptions';
-
-export { browserTracingIntegration } from './client/browserTracingIntegration';
-export { captureRemixErrorBoundaryError } from './client/errors';
-export { withSentry } from './client/performance';
-export * from '@sentry/react';
-
-// This is a no-op function that does nothing. It's here to make sure that the
-// function signature is the same as in the server SDK.
-// See issue: https://github.com/getsentry/sentry-javascript/issues/9594
-/* eslint-disable @typescript-eslint/no-unused-vars */
-export async function captureRemixServerException(err: unknown, name: string, request: Request): Promise {
- DEBUG_BUILD &&
- logger.warn(
- '`captureRemixServerException` is a server-only function and should not be called in the browser. ' +
- 'This function is a no-op in the browser environment.',
- );
-}
-
-export function init(options: RemixOptions): Client | undefined {
- const opts = {
- ...options,
- environment: options.environment || process.env.NODE_ENV,
- };
-
- applySdkMetadata(opts, 'remix', ['remix', 'react']);
-
- return reactInit(opts);
-}
diff --git a/packages/remix/src/index.server.ts b/packages/remix/src/index.server.ts
index 9084284217a9..e6c9610df417 100644
--- a/packages/remix/src/index.server.ts
+++ b/packages/remix/src/index.server.ts
@@ -1,173 +1,7 @@
-import { applySdkMetadata, logger } from '@sentry/core';
-import type { Integration } from '@sentry/core';
-import type { NodeClient, NodeOptions } from '@sentry/node';
-import { getDefaultIntegrations as getDefaultNodeIntegrations, init as nodeInit, isInitialized } from '@sentry/node';
-
-import { DEBUG_BUILD } from './utils/debug-build';
-import { instrumentServer } from './utils/instrumentServer';
-import { httpIntegration } from './utils/integrations/http';
-import { remixIntegration } from './utils/integrations/opentelemetry';
-import type { RemixOptions } from './utils/remixOptions';
-
-// We need to explicitly export @sentry/node as they end up under `default` in ESM builds
-// See: https://github.com/getsentry/sentry-javascript/issues/8474
-export {
- addBreadcrumb,
- addEventProcessor,
- addIntegration,
- amqplibIntegration,
- anrIntegration,
- disableAnrDetectionForCallback,
- captureCheckIn,
- captureConsoleIntegration,
- captureEvent,
- captureException,
- captureFeedback,
- captureMessage,
- captureSession,
- close,
- connectIntegration,
- consoleIntegration,
- contextLinesIntegration,
- continueTrace,
- createGetModuleFromFilename,
- createTransport,
- cron,
- dedupeIntegration,
- defaultStackParser,
- endSession,
- expressErrorHandler,
- expressIntegration,
- extraErrorDataIntegration,
- fastifyIntegration,
- flush,
- functionToStringIntegration,
- generateInstrumentOnce,
- genericPoolIntegration,
- getActiveSpan,
- getAutoPerformanceIntegrations,
- getClient,
- getCurrentScope,
- getDefaultIntegrations,
- getGlobalScope,
- getIsolationScope,
- getRootSpan,
- getSentryRelease,
- getSpanDescendants,
- getSpanStatusFromHttpCode,
- graphqlIntegration,
- hapiIntegration,
- httpIntegration,
- inboundFiltersIntegration,
- initOpenTelemetry,
- isInitialized,
- knexIntegration,
- kafkaIntegration,
- koaIntegration,
- lastEventId,
- linkedErrorsIntegration,
- localVariablesIntegration,
- makeNodeTransport,
- modulesIntegration,
- mongoIntegration,
- mongooseIntegration,
- mysql2Integration,
- mysqlIntegration,
- nativeNodeFetchIntegration,
- NodeClient,
- nodeContextIntegration,
- onUncaughtExceptionIntegration,
- onUnhandledRejectionIntegration,
- parameterize,
- postgresIntegration,
- prismaIntegration,
- redisIntegration,
- requestDataIntegration,
- rewriteFramesIntegration,
- Scope,
- SDK_VERSION,
- SEMANTIC_ATTRIBUTE_SENTRY_OP,
- SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
- SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE,
- SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
- setContext,
- setCurrentClient,
- setExtra,
- setExtras,
- setHttpStatus,
- setMeasurement,
- setTag,
- setTags,
- setupConnectErrorHandler,
- setupExpressErrorHandler,
- setupHapiErrorHandler,
- setupKoaErrorHandler,
- setUser,
- spanToBaggageHeader,
- spanToJSON,
- spanToTraceHeader,
- spotlightIntegration,
- startInactiveSpan,
- startNewTrace,
- suppressTracing,
- startSession,
- startSpan,
- startSpanManual,
- tediousIntegration,
- trpcMiddleware,
- updateSpanName,
- withActiveSpan,
- withIsolationScope,
- withMonitor,
- withScope,
- zodErrorsIntegration,
-} from '@sentry/node';
-
-// Keeping the `*` exports for backwards compatibility and types
-export * from '@sentry/node';
-
+export * from './server';
export {
- sentryHandleError,
- wrapHandleErrorWithSentry,
-} from './utils/instrumentServer';
-
-export { captureRemixServerException } from './utils/errors';
-
-export { ErrorBoundary, withErrorBoundary } from '@sentry/react';
-export { withSentry } from './client/performance';
-export { captureRemixErrorBoundaryError } from './client/errors';
-export { browserTracingIntegration } from './client/browserTracingIntegration';
+ captureRemixErrorBoundaryError,
+ withSentry,
+} from './client';
export type { SentryMetaArgs } from './utils/types';
-
-/**
- * Returns the default Remix integrations.
- *
- * @param options The options for the SDK.
- */
-export function getRemixDefaultIntegrations(options: RemixOptions): Integration[] {
- return [
- ...getDefaultNodeIntegrations(options as NodeOptions).filter(integration => integration.name !== 'Http'),
- httpIntegration(),
- remixIntegration(),
- ].filter(int => int) as Integration[];
-}
-
-/** Initializes Sentry Remix SDK on Node. */
-export function init(options: RemixOptions): NodeClient | undefined {
- applySdkMetadata(options, 'remix', ['remix', 'node']);
-
- if (isInitialized()) {
- DEBUG_BUILD && logger.log('SDK already initialized');
-
- return;
- }
-
- options.defaultIntegrations = getRemixDefaultIntegrations(options as NodeOptions);
-
- const client = nodeInit(options as NodeOptions);
-
- instrumentServer();
-
- return client;
-}
diff --git a/packages/remix/src/index.types.ts b/packages/remix/src/index.types.ts
index 5cfb7114bbbc..763a2747f69e 100644
--- a/packages/remix/src/index.types.ts
+++ b/packages/remix/src/index.types.ts
@@ -12,6 +12,7 @@ import type { RemixOptions } from './utils/remixOptions';
/** Initializes Sentry Remix SDK */
export declare function init(options: RemixOptions): Client | undefined;
+export declare const browserTracingIntegration: typeof clientSdk.browserTracingIntegration;
export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsIntegration;
export declare const contextLinesIntegration: typeof clientSdk.contextLinesIntegration;
diff --git a/packages/remix/src/utils/errors.ts b/packages/remix/src/server/errors.ts
similarity index 94%
rename from packages/remix/src/utils/errors.ts
rename to packages/remix/src/server/errors.ts
index 1921ee2a37f2..0a5e9f4918bc 100644
--- a/packages/remix/src/utils/errors.ts
+++ b/packages/remix/src/server/errors.ts
@@ -14,11 +14,11 @@ import {
winterCGRequestToRequestData,
} from '@sentry/core';
import type { RequestEventData, Span } from '@sentry/core';
-import { DEBUG_BUILD } from './debug-build';
-import type { RemixOptions } from './remixOptions';
-import { storeFormDataKeys } from './utils';
-import { extractData, isResponse, isRouteErrorResponse } from './vendor/response';
-import type { DataFunction, RemixRequest } from './vendor/types';
+import { DEBUG_BUILD } from '../utils/debug-build';
+import type { RemixOptions } from '../utils/remixOptions';
+import { storeFormDataKeys } from '../utils/utils';
+import { extractData, isResponse, isRouteErrorResponse } from '../utils/vendor/response';
+import type { DataFunction, RemixRequest } from '../utils/vendor/types';
/**
* Captures an exception happened in the Remix server.
diff --git a/packages/remix/src/server/index.ts b/packages/remix/src/server/index.ts
new file mode 100644
index 000000000000..44c616de3a30
--- /dev/null
+++ b/packages/remix/src/server/index.ts
@@ -0,0 +1,120 @@
+// We need to explicitly export @sentry/node as they end up under `default` in ESM builds
+// See: https://github.com/getsentry/sentry-javascript/issues/8474
+export {
+ addBreadcrumb,
+ addEventProcessor,
+ addIntegration,
+ amqplibIntegration,
+ anrIntegration,
+ disableAnrDetectionForCallback,
+ captureCheckIn,
+ captureConsoleIntegration,
+ captureEvent,
+ captureException,
+ captureFeedback,
+ captureMessage,
+ captureSession,
+ close,
+ connectIntegration,
+ consoleIntegration,
+ contextLinesIntegration,
+ continueTrace,
+ createGetModuleFromFilename,
+ createTransport,
+ cron,
+ dedupeIntegration,
+ defaultStackParser,
+ endSession,
+ expressErrorHandler,
+ expressIntegration,
+ extraErrorDataIntegration,
+ fastifyIntegration,
+ flush,
+ functionToStringIntegration,
+ generateInstrumentOnce,
+ genericPoolIntegration,
+ getActiveSpan,
+ getAutoPerformanceIntegrations,
+ getClient,
+ getCurrentScope,
+ getDefaultIntegrations,
+ getGlobalScope,
+ getIsolationScope,
+ getRootSpan,
+ getSentryRelease,
+ getSpanDescendants,
+ getSpanStatusFromHttpCode,
+ graphqlIntegration,
+ hapiIntegration,
+ httpIntegration,
+ inboundFiltersIntegration,
+ initOpenTelemetry,
+ isInitialized,
+ knexIntegration,
+ kafkaIntegration,
+ koaIntegration,
+ lastEventId,
+ linkedErrorsIntegration,
+ localVariablesIntegration,
+ makeNodeTransport,
+ modulesIntegration,
+ mongoIntegration,
+ mongooseIntegration,
+ mysql2Integration,
+ mysqlIntegration,
+ nativeNodeFetchIntegration,
+ NodeClient,
+ nodeContextIntegration,
+ onUncaughtExceptionIntegration,
+ onUnhandledRejectionIntegration,
+ parameterize,
+ postgresIntegration,
+ prismaIntegration,
+ redisIntegration,
+ requestDataIntegration,
+ rewriteFramesIntegration,
+ Scope,
+ SDK_VERSION,
+ SEMANTIC_ATTRIBUTE_SENTRY_OP,
+ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
+ SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE,
+ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
+ setContext,
+ setCurrentClient,
+ setExtra,
+ setExtras,
+ setHttpStatus,
+ setMeasurement,
+ setTag,
+ setTags,
+ setupConnectErrorHandler,
+ setupExpressErrorHandler,
+ setupHapiErrorHandler,
+ setupKoaErrorHandler,
+ setUser,
+ spanToBaggageHeader,
+ spanToJSON,
+ spanToTraceHeader,
+ spotlightIntegration,
+ startInactiveSpan,
+ startNewTrace,
+ suppressTracing,
+ startSession,
+ startSpan,
+ startSpanManual,
+ tediousIntegration,
+ trpcMiddleware,
+ updateSpanName,
+ withActiveSpan,
+ withIsolationScope,
+ withMonitor,
+ withScope,
+ zodErrorsIntegration,
+} from '@sentry/node';
+
+// Keeping the `*` exports for backwards compatibility and types
+export * from '@sentry/node';
+
+export { init, getRemixDefaultIntegrations } from './sdk';
+export { captureRemixServerException } from './errors';
+export { sentryHandleError, wrapHandleErrorWithSentry, instrumentBuild } from './instrumentServer';
diff --git a/packages/remix/src/utils/instrumentServer.ts b/packages/remix/src/server/instrumentServer.ts
similarity index 59%
rename from packages/remix/src/utils/instrumentServer.ts
rename to packages/remix/src/server/instrumentServer.ts
index 25878becb82d..f4634805e1e1 100644
--- a/packages/remix/src/utils/instrumentServer.ts
+++ b/packages/remix/src/server/instrumentServer.ts
@@ -1,19 +1,28 @@
-import type { RequestEventData, WrappedFunction } from '@sentry/core';
+/* eslint-disable max-lines */
+import type { RequestEventData, Span, TransactionSource, WrappedFunction } from '@sentry/core';
import {
+ SEMANTIC_ATTRIBUTE_SENTRY_OP,
+ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
+ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
continueTrace,
fill,
+ getActiveSpan,
getClient,
+ getRootSpan,
getTraceData,
hasSpansEnabled,
isNodeEnv,
loadModule,
logger,
+ setHttpStatus,
+ spanToJSON,
+ startSpan,
winterCGRequestToRequestData,
withIsolationScope,
} from '@sentry/core';
-import { DEBUG_BUILD } from './debug-build';
-import { captureRemixServerException, errorHandleDataFunction, errorHandleDocumentRequestFunction } from './errors';
-import { extractData, isDeferredData, isResponse, isRouteErrorResponse, json } from './vendor/response';
+import { DEBUG_BUILD } from '../utils/debug-build';
+import { createRoutes, getTransactionName } from '../utils/utils';
+import { extractData, isDeferredData, isResponse, isRouteErrorResponse, json } from '../utils/vendor/response';
import type {
AppData,
AppLoadContext,
@@ -25,8 +34,10 @@ import type {
RemixRequest,
RequestHandler,
ServerBuild,
+ ServerRoute,
ServerRouteManifest,
-} from './vendor/types';
+} from '../utils/vendor/types';
+import { captureRemixServerException, errorHandleDataFunction, errorHandleDocumentRequestFunction } from './errors';
const redirectStatusCodes = new Set([301, 302, 303, 307, 308]);
function isRedirectResponse(response: Response): boolean {
@@ -77,11 +88,16 @@ export function wrapHandleErrorWithSentry(
};
}
+function isCloudflareEnv(): boolean {
+ // eslint-disable-next-line no-restricted-globals
+ return navigator?.userAgent?.includes('Cloudflare');
+}
+
function getTraceAndBaggage(): {
sentryTrace?: string;
sentryBaggage?: string;
} {
- if (isNodeEnv()) {
+ if (isNodeEnv() || isCloudflareEnv()) {
const traceData = getTraceData();
return {
@@ -93,7 +109,7 @@ function getTraceAndBaggage(): {
return {};
}
-function makeWrappedDocumentRequestFunction() {
+function makeWrappedDocumentRequestFunction(instrumentTracing?: boolean) {
return function (origDocumentRequestFunction: HandleDocumentRequestFunction): HandleDocumentRequestFunction {
return async function (
this: unknown,
@@ -103,33 +119,81 @@ function makeWrappedDocumentRequestFunction() {
context: EntryContext,
loadContext?: Record,
): Promise {
- return errorHandleDocumentRequestFunction.call(this, origDocumentRequestFunction, {
+ const documentRequestContext = {
request,
responseStatusCode,
responseHeaders,
context,
loadContext,
- });
+ };
+
+ if (instrumentTracing) {
+ const activeSpan = getActiveSpan();
+ const rootSpan = activeSpan && getRootSpan(activeSpan);
+
+ const name = rootSpan ? spanToJSON(rootSpan).description : undefined;
+
+ return startSpan(
+ {
+ // If we don't have a root span, `onlyIfParent` will lead to the span not being created anyhow
+ // So we don't need to care too much about the fallback name, it's just for typing purposes....
+ name: name || '',
+ onlyIfParent: true,
+ attributes: {
+ method: request.method,
+ url: request.url,
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.remix',
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function.remix.document_request',
+ },
+ },
+ () => {
+ return errorHandleDocumentRequestFunction.call(this, origDocumentRequestFunction, documentRequestContext);
+ },
+ );
+ } else {
+ return errorHandleDocumentRequestFunction.call(this, origDocumentRequestFunction, documentRequestContext);
+ }
};
};
}
-function makeWrappedDataFunction(origFn: DataFunction, id: string, name: 'action' | 'loader'): DataFunction {
+function makeWrappedDataFunction(
+ origFn: DataFunction,
+ id: string,
+ name: 'action' | 'loader',
+ instrumentTracing?: boolean,
+): DataFunction {
return async function (this: unknown, args: DataFunctionArgs): Promise {
- return errorHandleDataFunction.call(this, origFn, name, args);
+ if (instrumentTracing) {
+ return startSpan(
+ {
+ op: `function.remix.${name}`,
+ name: id,
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.remix',
+ name,
+ },
+ },
+ (span: Span) => {
+ return errorHandleDataFunction.call(this, origFn, name, args, span);
+ },
+ );
+ } else {
+ return errorHandleDataFunction.call(this, origFn, name, args);
+ }
};
}
const makeWrappedAction =
- (id: string) =>
+ (id: string, instrumentTracing?: boolean) =>
(origAction: DataFunction): DataFunction => {
- return makeWrappedDataFunction(origAction, id, 'action');
+ return makeWrappedDataFunction(origAction, id, 'action', instrumentTracing);
};
const makeWrappedLoader =
- (id: string) =>
+ (id: string, instrumentTracing?: boolean) =>
(origLoader: DataFunction): DataFunction => {
- return makeWrappedDataFunction(origLoader, id, 'loader');
+ return makeWrappedDataFunction(origLoader, id, 'loader', instrumentTracing);
};
function makeWrappedRootLoader() {
@@ -176,7 +240,20 @@ function makeWrappedRootLoader() {
};
}
-function wrapRequestHandler(origRequestHandler: RequestHandler): RequestHandler {
+function wrapRequestHandler(
+ origRequestHandler: RequestHandler,
+ build:
+ | ServerBuild
+ | { build: ServerBuild }
+ | (() => ServerBuild | { build: ServerBuild } | Promise),
+ options?: {
+ instrumentTracing?: boolean;
+ },
+): RequestHandler {
+ let resolvedBuild: ServerBuild | { build: ServerBuild };
+ let name: string;
+ let source: TransactionSource;
+
return async function (this: unknown, request: RemixRequest, loadContext?: AppLoadContext): Promise {
const upperCaseMethod = request.method.toUpperCase();
// We don't want to wrap OPTIONS and HEAD requests
@@ -184,8 +261,25 @@ function wrapRequestHandler(origRequestHandler: RequestHandler): RequestHandler
return origRequestHandler.call(this, request, loadContext);
}
+ let resolvedRoutes: ServerRoute[] | undefined;
+
+ if (options?.instrumentTracing) {
+ if (typeof build === 'function') {
+ resolvedBuild = await build();
+ } else {
+ resolvedBuild = build;
+ }
+
+ // check if the build is nested under `build` key
+ if ('build' in resolvedBuild) {
+ resolvedRoutes = createRoutes(resolvedBuild.build.routes);
+ } else {
+ resolvedRoutes = createRoutes(resolvedBuild.routes);
+ }
+ }
+
return withIsolationScope(async isolationScope => {
- const options = getClient()?.getOptions();
+ const clientOptions = getClient()?.getOptions();
let normalizedRequest: RequestEventData = {};
@@ -195,9 +289,16 @@ function wrapRequestHandler(origRequestHandler: RequestHandler): RequestHandler
DEBUG_BUILD && logger.warn('Failed to normalize Remix request');
}
+ if (options?.instrumentTracing && resolvedRoutes) {
+ const url = new URL(request.url);
+ [name, source] = getTransactionName(resolvedRoutes, url);
+
+ isolationScope.setTransactionName(name);
+ }
+
isolationScope.setSDKProcessingMetadata({ normalizedRequest });
- if (!options || !hasSpansEnabled(options)) {
+ if (!clientOptions || !hasSpansEnabled(clientOptions)) {
return origRequestHandler.call(this, request, loadContext);
}
@@ -207,6 +308,33 @@ function wrapRequestHandler(origRequestHandler: RequestHandler): RequestHandler
baggage: request.headers.get('baggage') || '',
},
async () => {
+ if (options?.instrumentTracing) {
+ const parentSpan = getActiveSpan();
+ const rootSpan = parentSpan && getRootSpan(parentSpan);
+ rootSpan?.updateName(name);
+
+ return startSpan(
+ {
+ name,
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.remix',
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source,
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server',
+ method: request.method,
+ },
+ },
+ async span => {
+ const res = (await origRequestHandler.call(this, request, loadContext)) as Response;
+
+ if (isResponse(res)) {
+ setHttpStatus(span, res.status);
+ }
+
+ return res;
+ },
+ );
+ }
+
return (await origRequestHandler.call(this, request, loadContext)) as Response;
},
);
@@ -214,8 +342,14 @@ function wrapRequestHandler(origRequestHandler: RequestHandler): RequestHandler
};
}
-function instrumentBuildCallback(build: ServerBuild): ServerBuild {
- const routes: ServerRouteManifest = {};
+function instrumentBuildCallback(
+ build: ServerBuild,
+ options?: {
+ instrumentTracing?: boolean;
+ },
+): ServerBuild {
+ const routes: ServerRouteManifest = build.routes;
+
const wrappedEntry = { ...build.entry, module: { ...build.entry.module } };
// Not keeping boolean flags like it's done for `requestHandler` functions,
@@ -224,7 +358,7 @@ function instrumentBuildCallback(build: ServerBuild): ServerBuild {
// We should be able to wrap them, as they may not be wrapped before.
const defaultExport = wrappedEntry.module.default as undefined | WrappedFunction;
if (defaultExport && !defaultExport.__sentry_original__) {
- fill(wrappedEntry.module, 'default', makeWrappedDocumentRequestFunction());
+ fill(wrappedEntry.module, 'default', makeWrappedDocumentRequestFunction(options?.instrumentTracing));
}
for (const [id, route] of Object.entries(build.routes)) {
@@ -232,12 +366,12 @@ function instrumentBuildCallback(build: ServerBuild): ServerBuild {
const routeAction = wrappedRoute.module.action as undefined | WrappedFunction;
if (routeAction && !routeAction.__sentry_original__) {
- fill(wrappedRoute.module, 'action', makeWrappedAction(id));
+ fill(wrappedRoute.module, 'action', makeWrappedAction(id, options?.instrumentTracing));
}
const routeLoader = wrappedRoute.module.loader as undefined | WrappedFunction;
if (routeLoader && !routeLoader.__sentry_original__) {
- fill(wrappedRoute.module, 'loader', makeWrappedLoader(id));
+ fill(wrappedRoute.module, 'loader', makeWrappedLoader(id, options?.instrumentTracing));
}
// Entry module should have a loader function to provide `sentry-trace` and `baggage`
@@ -254,7 +388,13 @@ function instrumentBuildCallback(build: ServerBuild): ServerBuild {
routes[id] = wrappedRoute;
}
- return { ...build, routes, entry: wrappedEntry };
+ const instrumentedBuild = { ...build, routes };
+
+ if (wrappedEntry) {
+ instrumentedBuild.entry = wrappedEntry;
+ }
+
+ return instrumentedBuild;
}
/**
@@ -262,6 +402,9 @@ function instrumentBuildCallback(build: ServerBuild): ServerBuild {
*/
export function instrumentBuild(
build: ServerBuild | (() => ServerBuild | Promise),
+ options?: {
+ instrumentTracing?: boolean;
+ },
): ServerBuild | (() => ServerBuild | Promise) {
if (typeof build === 'function') {
return function () {
@@ -269,28 +412,28 @@ export function instrumentBuild(
if (resolvedBuild instanceof Promise) {
return resolvedBuild.then(build => {
- return instrumentBuildCallback(build);
+ return instrumentBuildCallback(build, options);
});
} else {
- return instrumentBuildCallback(resolvedBuild);
+ return instrumentBuildCallback(resolvedBuild, options);
}
};
} else {
- return instrumentBuildCallback(build);
+ return instrumentBuildCallback(build, options);
}
}
-const makeWrappedCreateRequestHandler = () =>
+export const makeWrappedCreateRequestHandler = (options?: { instrumentTracing?: boolean }) =>
function (origCreateRequestHandler: CreateRequestHandlerFunction): CreateRequestHandlerFunction {
return function (
this: unknown,
build: ServerBuild | (() => Promise),
...args: unknown[]
): RequestHandler {
- const newBuild = instrumentBuild(build);
+ const newBuild = instrumentBuild(build, options);
const requestHandler = origCreateRequestHandler.call(this, newBuild, ...args);
- return wrapRequestHandler(requestHandler);
+ return wrapRequestHandler(requestHandler, newBuild, options);
};
};
@@ -298,7 +441,7 @@ const makeWrappedCreateRequestHandler = () =>
* Monkey-patch Remix's `createRequestHandler` from `@remix-run/server-runtime`
* which Remix Adapters (https://remix.run/docs/en/v1/api/remix) use underneath.
*/
-export function instrumentServer(): void {
+export function instrumentServer(options?: { instrumentTracing?: boolean }): void {
const pkg = loadModule<{
createRequestHandler: CreateRequestHandlerFunction;
}>('@remix-run/server-runtime', module);
@@ -309,5 +452,5 @@ export function instrumentServer(): void {
return;
}
- fill(pkg, 'createRequestHandler', makeWrappedCreateRequestHandler());
+ fill(pkg, 'createRequestHandler', makeWrappedCreateRequestHandler(options));
}
diff --git a/packages/remix/src/utils/integrations/http.ts b/packages/remix/src/server/integrations/http.ts
similarity index 100%
rename from packages/remix/src/utils/integrations/http.ts
rename to packages/remix/src/server/integrations/http.ts
diff --git a/packages/remix/src/utils/integrations/opentelemetry.ts b/packages/remix/src/server/integrations/opentelemetry.ts
similarity index 97%
rename from packages/remix/src/utils/integrations/opentelemetry.ts
rename to packages/remix/src/server/integrations/opentelemetry.ts
index e14d5671bb4d..7ba99421c82f 100644
--- a/packages/remix/src/utils/integrations/opentelemetry.ts
+++ b/packages/remix/src/server/integrations/opentelemetry.ts
@@ -3,7 +3,7 @@ import { RemixInstrumentation } from 'opentelemetry-instrumentation-remix';
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, defineIntegration } from '@sentry/core';
import type { Client, IntegrationFn, Span } from '@sentry/core';
import { generateInstrumentOnce, getClient, spanToJSON } from '@sentry/node';
-import type { RemixOptions } from '../remixOptions';
+import type { RemixOptions } from '../../utils/remixOptions';
const INTEGRATION_NAME = 'Remix';
diff --git a/packages/remix/src/server/sdk.ts b/packages/remix/src/server/sdk.ts
new file mode 100644
index 000000000000..65a19bebdd28
--- /dev/null
+++ b/packages/remix/src/server/sdk.ts
@@ -0,0 +1,42 @@
+import { applySdkMetadata, logger } from '@sentry/core';
+import type { Integration } from '@sentry/core';
+import type { NodeClient, NodeOptions } from '@sentry/node';
+import { getDefaultIntegrations as getDefaultNodeIntegrations, init as nodeInit, isInitialized } from '@sentry/node';
+
+import { DEBUG_BUILD } from '../utils/debug-build';
+import type { RemixOptions } from '../utils/remixOptions';
+import { instrumentServer } from './instrumentServer';
+import { httpIntegration } from './integrations/http';
+import { remixIntegration } from './integrations/opentelemetry';
+
+/**
+ * Returns the default Remix integrations.
+ *
+ * @param options The options for the SDK.
+ */
+export function getRemixDefaultIntegrations(options: RemixOptions): Integration[] {
+ return [
+ ...getDefaultNodeIntegrations(options as NodeOptions).filter(integration => integration.name !== 'Http'),
+ httpIntegration(),
+ remixIntegration(),
+ ].filter(int => int) as Integration[];
+}
+
+/** Initializes Sentry Remix SDK on Node. */
+export function init(options: RemixOptions): NodeClient | undefined {
+ applySdkMetadata(options, 'remix', ['remix', 'node']);
+
+ if (isInitialized()) {
+ DEBUG_BUILD && logger.log('SDK already initialized');
+
+ return;
+ }
+
+ options.defaultIntegrations = getRemixDefaultIntegrations(options as NodeOptions);
+
+ const client = nodeInit(options as NodeOptions);
+
+ instrumentServer();
+
+ return client;
+}
diff --git a/packages/remix/src/utils/vendor/types.ts b/packages/remix/src/utils/vendor/types.ts
index 19e30b1a78e1..ea80085d0780 100644
--- a/packages/remix/src/utils/vendor/types.ts
+++ b/packages/remix/src/utils/vendor/types.ts
@@ -1,6 +1,5 @@
import type { Agent } from 'https';
/* eslint-disable @typescript-eslint/no-explicit-any */
-/* eslint-disable max-lines */
/* eslint-disable @typescript-eslint/ban-types */
// Types vendored from @remix-run/server-runtime@1.6.0:
// https://github.com/remix-run/remix/blob/f3691d51027b93caa3fd2cdfe146d7b62a6eb8f2/packages/remix-server-runtime/server.ts