@@ -8,8 +8,10 @@ import {
88 dynamicSamplingContextToSentryBaggageHeader ,
99 fill ,
1010 isNodeEnv ,
11+ isPrimitive ,
1112 loadModule ,
1213 logger ,
14+ objectify ,
1315 tracingContextFromHeaders ,
1416} from '@sentry/utils' ;
1517
@@ -70,6 +72,28 @@ async function extractResponseError(response: Response): Promise<unknown> {
7072 return responseData ;
7173}
7274
75+ /**
76+ * Sentry utility to be used in place of `handleError` function of Remix v2
77+ * Remix Docs: https://remix.run/docs/en/main/file-conventions/entry.server#handleerror
78+ *
79+ * Should be used in `entry.server` like:
80+ *
81+ * export const handleError = Sentry.wrapRemixHandleError
82+ */
83+ export function wrapRemixHandleError ( err : unknown , { request } : DataFunctionArgs ) : void {
84+ // We are skipping thrown responses here as they are handled by
85+ // `captureRemixServerException` at loader / action level
86+ // We don't want to capture them twice.
87+ // This function if only for capturing unhandled server-side exceptions.
88+ // https://remix.run/docs/en/main/file-conventions/entry.server#thrown-responses
89+ // https://remix.run/docs/en/v1/api/conventions#throwing-responses-in-loaders
90+ if ( isResponse ( err ) || isRouteErrorResponse ( err ) ) {
91+ return ;
92+ }
93+
94+ void captureRemixServerException ( err , 'remix.server.handleError' , request ) ;
95+ }
96+
7397/**
7498 * Captures an exception happened in the Remix server.
7599 *
@@ -107,7 +131,9 @@ export async function captureRemixServerException(err: unknown, name: string, re
107131 DEBUG_BUILD && logger . warn ( 'Failed to normalize Remix request' ) ;
108132 }
109133
110- captureException ( isResponse ( err ) ? await extractResponseError ( err ) : err , scope => {
134+ const objectifiedErr = objectify ( err ) ;
135+
136+ captureException ( isResponse ( objectifiedErr ) ? await extractResponseError ( objectifiedErr ) : objectifiedErr , scope => {
111137 const activeTransactionName = getActiveTransaction ( ) ?. name ;
112138
113139 scope . setSDKProcessingMetadata ( {
@@ -138,7 +164,7 @@ export async function captureRemixServerException(err: unknown, name: string, re
138164 } ) ;
139165}
140166
141- function makeWrappedDocumentRequestFunction ( remixVersion : number ) {
167+ function makeWrappedDocumentRequestFunction ( remixVersion ? : number ) {
142168 return function ( origDocumentRequestFunction : HandleDocumentRequestFunction ) : HandleDocumentRequestFunction {
143169 return async function (
144170 this : unknown ,
@@ -149,7 +175,6 @@ function makeWrappedDocumentRequestFunction(remixVersion: number) {
149175 loadContext ?: Record < string , unknown > ,
150176 ) : Promise < Response > {
151177 let res : Response ;
152-
153178 const activeTransaction = getActiveTransaction ( ) ;
154179
155180 try {
@@ -174,7 +199,12 @@ function makeWrappedDocumentRequestFunction(remixVersion: number) {
174199
175200 span ?. finish ( ) ;
176201 } catch ( err ) {
177- if ( ! FUTURE_FLAGS ?. v2_errorBoundary && remixVersion !== 2 ) {
202+ const isRemixV1 = ! FUTURE_FLAGS ?. v2_errorBoundary && remixVersion !== 2 ;
203+
204+ // This exists to capture the server-side rendering errors on Remix v1
205+ // On Remix v2, we capture SSR errors at `handleError`
206+ // We also skip primitives here, as we can't dedupe them, and also we don't expect any primitive SSR errors.
207+ if ( isRemixV1 && ! isPrimitive ( err ) ) {
178208 await captureRemixServerException ( err , 'documentRequest' , request ) ;
179209 }
180210
@@ -217,7 +247,12 @@ function makeWrappedDataFunction(
217247 currentScope . setSpan ( activeTransaction ) ;
218248 span ?. finish ( ) ;
219249 } catch ( err ) {
220- if ( ! FUTURE_FLAGS ?. v2_errorBoundary && remixVersion !== 2 ) {
250+ const isRemixV2 = FUTURE_FLAGS ?. v2_errorBoundary || remixVersion === 2 ;
251+
252+ // On Remix v2, we capture all unexpected errors (except the `Route Error Response`s / Thrown Responses) in `handleError` function.
253+ // This is both for consistency and also avoid duplicates such as primitives like `string` or `number` being captured twice.
254+ // Remix v1 does not have a `handleError` function, so we capture all errors here.
255+ if ( isRemixV2 ? isResponse ( err ) : true ) {
221256 await captureRemixServerException ( err , name , args . request ) ;
222257 }
223258
@@ -240,7 +275,10 @@ const makeWrappedLoader =
240275 return makeWrappedDataFunction ( origLoader , id , 'loader' , remixVersion ) ;
241276 } ;
242277
243- function getTraceAndBaggage ( ) : { sentryTrace ?: string ; sentryBaggage ?: string } {
278+ function getTraceAndBaggage ( ) : {
279+ sentryTrace ?: string ;
280+ sentryBaggage ?: string ;
281+ } {
244282 const transaction = getActiveTransaction ( ) ;
245283 const currentScope = getCurrentHub ( ) . getScope ( ) ;
246284
@@ -287,7 +325,11 @@ function makeWrappedRootLoader(remixVersion: number) {
287325 if ( typeof data === 'object' ) {
288326 return json (
289327 { ...data , ...traceAndBaggage , remixVersion } ,
290- { headers : res . headers , statusText : res . statusText , status : res . status } ,
328+ {
329+ headers : res . headers ,
330+ statusText : res . statusText ,
331+ status : res . status ,
332+ } ,
291333 ) ;
292334 } else {
293335 DEBUG_BUILD && logger . warn ( 'Skipping injection of trace and baggage as the response body is not an object' ) ;
@@ -498,7 +540,9 @@ function makeWrappedCreateRequestHandler(
498540 * which Remix Adapters (https://remix.run/docs/en/v1/api/remix) use underneath.
499541 */
500542export function instrumentServer ( ) : void {
501- const pkg = loadModule < { createRequestHandler : CreateRequestHandlerFunction } > ( '@remix-run/server-runtime' ) ;
543+ const pkg = loadModule < {
544+ createRequestHandler : CreateRequestHandlerFunction ;
545+ } > ( '@remix-run/server-runtime' ) ;
502546
503547 if ( ! pkg ) {
504548 DEBUG_BUILD && logger . warn ( 'Remix SDK was unable to require `@remix-run/server-runtime` package.' ) ;
0 commit comments