diff --git a/docs/platforms/javascript/guides/react-router/index.mdx b/docs/platforms/javascript/guides/react-router/index.mdx index 1a06ea0c904d30..6b8eb8236f1b45 100644 --- a/docs/platforms/javascript/guides/react-router/index.mdx +++ b/docs/platforms/javascript/guides/react-router/index.mdx @@ -83,42 +83,42 @@ npx react-router reveal Initialize the Sentry React SDK in your `entry.client.tsx` file: -```tsx {filename: entry.client.tsx} -import * as Sentry from "@sentry/react-router"; -import { startTransition, StrictMode } from "react"; -import { hydrateRoot } from "react-dom/client"; -import { HydratedRouter } from "react-router/dom"; - -Sentry.init({ - dsn: "___PUBLIC_DSN___", - - // Adds request headers and IP for users, for more info visit: - // https://docs.sentry.io/platforms/javascript/guides/react-router/configuration/options/#sendDefaultPii - sendDefaultPii: true, - - integrations: [ - // ___PRODUCT_OPTION_START___ performance - Sentry.browserTracingIntegration(), - // ___PRODUCT_OPTION_END___ performance - // ___PRODUCT_OPTION_START___ session-replay - Sentry.replayIntegration(), - // ___PRODUCT_OPTION_END___ session-replay - ], - // ___PRODUCT_OPTION_START___ performance - - tracesSampleRate: 1.0, // Capture 100% of the transactions - - // Set `tracePropagationTargets` to declare which URL(s) should have trace propagation enabled - tracePropagationTargets: [/^\//, /^https:\/\/yourserver\.io\/api/], - // ___PRODUCT_OPTION_END___ performance - // ___PRODUCT_OPTION_START___ session-replay - - // Capture Replay for 10% of all sessions, - // plus 100% of sessions with an error - replaysSessionSampleRate: 0.1, - replaysOnErrorSampleRate: 1.0, - // ___PRODUCT_OPTION_END___ session-replay -}); +```tsx {diff} {filename: entry.client.tsx} ++import * as Sentry from "@sentry/react-router"; + import { startTransition, StrictMode } from "react"; + import { hydrateRoot } from "react-dom/client"; + import { HydratedRouter } from "react-router/dom"; + ++Sentry.init({ ++ dsn: "___PUBLIC_DSN___", ++ ++ // Adds request headers and IP for users, for more info visit: ++ // https://docs.sentry.io/platforms/javascript/guides/react-router/configuration/options/#sendDefaultPii ++ sendDefaultPii: true, ++ ++ integrations: [ ++ // ___PRODUCT_OPTION_START___ performance ++ Sentry.browserTracingIntegration(), ++ // ___PRODUCT_OPTION_END___ performance ++ // ___PRODUCT_OPTION_START___ session-replay ++ Sentry.replayIntegration(), ++ // ___PRODUCT_OPTION_END___ session-replay ++ ], ++ // ___PRODUCT_OPTION_START___ performance ++ ++ tracesSampleRate: 1.0, // Capture 100% of the transactions ++ ++ // Set `tracePropagationTargets` to declare which URL(s) should have trace propagation enabled ++ tracePropagationTargets: [/^\//, /^https:\/\/yourserver\.io\/api/], ++ // ___PRODUCT_OPTION_END___ performance ++ // ___PRODUCT_OPTION_START___ session-replay ++ ++ // Capture Replay for 10% of all sessions, ++ // plus 100% of sessions with an error ++ replaysSessionSampleRate: 0.1, ++ replaysOnErrorSampleRate: 1.0, ++ // ___PRODUCT_OPTION_END___ session-replay ++}); startTransition(() => { hydrateRoot( @@ -133,7 +133,7 @@ startTransition(() => { Now, update your `app/root.tsx` file to report any unhandled errors from your error boundary: ```tsx {diff} {filename: app/root.tsx} -import * as Sentry from "@sentry/react-router"; ++import * as Sentry from "@sentry/react-router"; export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { let message = "Oops!"; @@ -199,40 +199,110 @@ Sentry.init({ }); ``` -In your `entry.server.tsx` file, export the `handleError` function: +Update your `entry.server.tsx` file: ```tsx {diff} {filename: entry.server.tsx} -import * as Sentry from "@sentry/react-router"; -import { type HandleErrorFunction } from "react-router"; ++import * as Sentry from '@sentry/react-router'; + import { createReadableStreamFromReadable } from '@react-router/node'; + import { renderToPipeableStream } from 'react-dom/server'; + import { ServerRouter } from 'react-router'; + import { type HandleErrorFunction } from 'react-router'; + ++const handleRequest = Sentry.createSentryHandleRequest({ ++ ServerRouter, ++ renderToPipeableStream, ++ createReadableStreamFromReadable, ++}); + +export default handleRequest; export const handleError: HandleErrorFunction = (error, { request }) => { - // React Router may abort some interrupted requests, report those + // React Router may abort some interrupted requests, don't log those if (!request.signal.aborted) { + Sentry.captureException(error); - - // make sure to still log the error so you can see it + // optionally log the error so you can see it console.error(error); } }; -- export default function handleRequest( -+ function handleRequest( - request: Request, - responseStatusCode: number, - responseHeaders: Headers, - routerContext: EntryContext, - loadContext: AppLoadContext, -) { - return new Promise((resolve, reject) => { - // ... - } -} - -+ export default Sentry.sentryHandleRequest(handleRequest); // ... rest of your server entry ``` + + If you need to update the logic of your `handleRequest` function you'll need to include the provided Sentry helper functions (`getMetaTagTransformer` and `wrapSentryHandleRequest`) manually: + + ```tsx {1-4, 44-45, 69-70} + import { getMetaTagTransformer, wrapSentryHandleRequest } from '@sentry/react-router'; + // ... other imports + + const handleRequest = function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + routerContext: EntryContext, + _loadContext: AppLoadContext, + ): Promise { + return new Promise((resolve, reject) => { + let shellRendered = false; + const userAgent = request.headers.get('user-agent'); + + // Determine if we should use onAllReady or onShellReady + const isBot = typeof userAgent === 'string' && botRegex.test(userAgent); + const isSpaMode = !!(routerContext as { isSpaMode?: boolean }).isSpaMode; + + const readyOption = isBot || isSpaMode ? 'onAllReady' : 'onShellReady'; + + const { pipe, abort } = renderToPipeableStream( + , + { + [readyOption]() { + shellRendered = true; + const body = new PassThrough(); + + const stream = createReadableStreamFromReadable(body); + + responseHeaders.set('Content-Type', 'text/html'); + + resolve( + new Response(stream, { + headers: responseHeaders, + status: responseStatusCode, + }), + ); + + // this enables distributed tracing between client and server + pipe(getMetaTagTransformer(body)); + }, + onShellError(error: unknown) { + reject(error); + }, + onError(error: unknown) { + // eslint-disable-next-line no-param-reassign + responseStatusCode = 500; + // Log streaming rendering errors from inside the shell. Don't log + // errors encountered during initial shell rendering since they'll + // reject and get logged in handleDocumentRequest. + if (shellRendered) { + // eslint-disable-next-line no-console + console.error(error); + } + }, + }, + ); + + // Abort the rendering stream after the `streamTimeout` + setTimeout(abort, streamTimeout); + }); + }; + + // wrap the default export + export default wrapSentryHandleRequest(handleRequest); + + // ... rest of your entry.server.ts file + ``` + + ### Update Scripts Since React Router is running in ESM mode, you need to use the `--import` command line options to load our server-side instrumentation module before the application starts.