React Router Framework

React Router v7 is a framework for building full-stack web apps and websites. Learn how to set it up with Sentry.

You need:

Sentry captures data by using an SDK within your application’s runtime.

Copied
npm install @sentry/react-router @sentry/profiling-node

React Router exposes two hooks in your app folder (entry.client.tsx and entry.server.tsx). If you do not see these two files, expose them with the following command:

Copied
npx react-router reveal

Initialize the Sentry React SDK in your entry.client.tsx file:

entry.client.tsx
Copied
+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: "https://examplePublicKey@o0.ingest.sentry.io/0",
+  
+  // 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: [
+    //  performance
+    Sentry.browserTracingIntegration(),
+    //  performance
+    //  session-replay
+    Sentry.replayIntegration(),
+    //  session-replay
+  ],
+  //  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/],
+  //  performance
+  //  session-replay
+
+  // Capture Replay for 10% of all sessions,
+  // plus 100% of sessions with an error
+  replaysSessionSampleRate: 0.1,
+  replaysOnErrorSampleRate: 1.0,
+  //  session-replay
+});

startTransition(() => {
  hydrateRoot(
    document,
    <StrictMode>
      <HydratedRouter />
    </StrictMode>
  );
});

Now, update your app/root.tsx file to report any unhandled errors from your error boundary:

app/root.tsx
Copied
+import * as Sentry from "@sentry/react-router";

export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
  let message = "Oops!";
  let details = "An unexpected error occurred.";
  let stack: string | undefined;

  if (isRouteErrorResponse(error)) {
    message = error.status === 404 ? "404" : "Error";
    details =
      error.status === 404
        ? "The requested page could not be found."
        : error.statusText || details;
  } else if (error && error instanceof Error) {
    // you only want to capture non 404-errors that reach the boundary
+   Sentry.captureException(error);
    if (import.meta.env.DEV) {
      details = error.message;
      stack = error.stack;
    }
  }

  return (
    <main>
      <h1>{message}</h1>
      <p>{details}</p>
      {stack && (
        <pre>
          <code>{stack}</code>
        </pre>
      )}
    </main>
  );
}
// ...

Create an instrument.server.mjs file in the root of your app:

instrument.server.mjs
Copied
import * as Sentry from "@sentry/react-router";
//  profiling
import { nodeProfilingIntegration } from '@sentry/profiling-node';
//  profiling

Sentry.init({
  dsn: "https://examplePublicKey@o0.ingest.sentry.io/0",
  
  // 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,
  //  profiling
  
  integrations: [nodeProfilingIntegration()],
  //  profiling
  //  performance
  tracesSampleRate: 1.0, // Capture 100% of the transactions
  //  performance
  //  profiling
  profilesSampleRate: 1.0, // profile every transaction
  //  profiling
});

Update your entry.server.tsx file:

entry.server.tsx
Copied
+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, don't log those
  if (!request.signal.aborted) {
+   Sentry.captureException(error);
    // optionally log the error so you can see it
    console.error(error);
  }
};


// ... rest of your server entry
Do you need to customize your handleRequest function?

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:

Copied
import { getMetaTagTransformer, wrapSentryHandleRequest, } from "@sentry/react-router";
// ... other imports const handleRequest = function handleRequest( request: Request, responseStatusCode: number, responseHeaders: Headers, routerContext: EntryContext, _loadContext: AppLoadContext, ): Promise<Response> { 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( <ServerRouter context={routerContext} url={request.url} />, { [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

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. Update the start and dev script to include the instrumentation file:

package.json
Copied
"scripts": {
  "dev": "NODE_OPTIONS='--import ./instrument.server.mjs' react-router dev",
  "start": "NODE_OPTIONS='--import ./instrument.server.mjs' react-router-serve ./build/server/index.js",
}

Update vite.config.ts to include the sentryReactRouter plugin. Also add your SentryReactRouterBuildOptions config options to the Vite config (this is required for uploading source maps at the end of the build):

vite.config.ts
Copied
import { reactRouter } from '@react-router/dev/vite';
import { sentryReactRouter, type SentryReactRouterBuildOptions } from '@sentry/react-router';
import { defineConfig } from 'vite';

const sentryConfig: SentryReactRouterBuildOptions = {
  org: "example-org",
  project: "example-project",

  // An auth token is required for uploading source maps.
  authToken: "sntrys_YOUR_TOKEN_HERE"
  // ...
};

export default defineConfig(config => {
  return {
+   plugins: [reactRouter(),sentryReactRouter(sentryConfig, config)],
+   sentryConfig, // Also pass the config here!
  };
});

Include the sentryOnBuildEnd hook in react-router.config.ts:

react-router.config.ts
Copied
import type { Config } from '@react-router/dev/config';
import { sentryOnBuildEnd } from '@sentry/react-router';

export default {
  ssr: true,
  buildEnd: async ({ viteConfig, reactRouterConfig, buildManifest }) => {
    // ...
    // Call this at the end of the hook
+   await sentryOnBuildEnd({ viteConfig, reactRouterConfig, buildManifest });
  },
} satisfies Config;

This snippet includes an intentional error, so you can test that everything is working as soon as you set it up.

Throw an error in a loader to verify that Sentry is working. After opening this route in your browser, you should see two errors in the Sentry issue stream, one captured from the server and one captured from the client.

error.tsx
Copied
import type { Route } from "./+types/example-page";

export async function loader() {
  throw new Error("some error thrown in a loader");
}

export default function ExamplePage() {
  return <div>Loading this page will throw an error</div>;
}

To view and resolve the recorded error, log into sentry.io and select your project. Clicking on the error's title will open a page where you can see detailed information and mark it as resolved.

Was this helpful?
Help improve this content
Our documentation is open source and available on GitHub. Your contributions are welcome, whether fixing a typo (drat!) or suggesting an update ("yeah, this would be better").