React Router Framework

Learn how to set up and configure Sentry in your React Router v7 application, capture your first errors, and view them in Sentry.

You need:

Choose the features you want to configure, and this guide will show you how:

Want to learn more about these features?
  • Issues (always enabled): Sentry's core error monitoring product that automatically reports errors, uncaught exceptions, and unhandled rejections. If you have something that looks like an exception, Sentry can capture it.
  • Tracing: Track software performance while seeing the impact of errors across multiple systems. For example, distributed tracing allows you to follow a request from the frontend to the backend and back.
  • Session Replay: Get to the root cause of an issue faster by viewing a video-like reproduction of what was happening in the user's browser before, during, and after the problem.
  • Profiling: Gain deeper insight than traditional tracing without custom instrumentation, letting you discover slow-to-execute or resource-intensive functions in your app.
  • Logs: Centralize and analyze your application logs to correlate them with errors and performance issues. Search, filter, and visualize log data to understand what's happening in your applications.

Run the command for your preferred package manager to add the SDK package to your application:

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

Before configuring Sentry, you need to make React Router's entry files (entry.client.tsx and entry.server.tsx) visible in your project. Run this command to expose them:

Copied
npx react-router reveal

Initialize Sentry 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: "",
+
+  // 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
+    // Registers and configures the Tracing integration,
+    // which automatically instruments your application to monitor its
+    // performance, including custom Angular routing instrumentation
+    Sentry.reactRouterTracingIntegration(),
+    //  performance
+    //  session-replay
+    // Registers the Replay integration,
+    // which automatically captures Session Replays
+    Sentry.replayIntegration(),
+    //  session-replay
+    //  user-feedback
+    Sentry.feedbackIntegration({
+      // Additional SDK configuration goes in here, for example:
+      colorScheme: "system",
+    }),
+    //  user-feedback
+  ],
+  //  logs
+
+  // Enable logs to be sent to Sentry
+  enableLogs: true,
+  //  logs
+  //  performance
+
+  // Set tracesSampleRate to 1.0 to capture 100%
+  // of transactions for tracing.
+  // We recommend adjusting this value in production
+  // Learn more at
+  // https://docs.sentry.io/platforms/javascript/guides/react-router/configuration/options/#traces-sample-rate
+  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
+  // Learn more at
+  // https://docs.sentry.io/platforms/javascript/guides/react-router/session-replay/configuration/#general-integration-configuration
+  replaysSessionSampleRate: 0.1,
+  replaysOnErrorSampleRate: 1.0,
+  //  session-replay
+});

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

Update your app/root.tsx file to capture unhandled errors in 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>
  );
}
// ...

Limited Node support for auto-instrumentation

Automatic server-side instrumentation is currently only supported on:

  • Node 20: Version <20.19
  • Node 22: Version <22.12

If you're on a different version, use our manual server wrappers.

For server loaders use wrapServerLoader:

Copied
import * as Sentry from "@sentry/react-router";

export const loader = Sentry.wrapServerLoader(
  {
    name: "Load Some Data",
    description: "Loads some data from the db",
  },
  async ({ params }) => {
    // ... your loader logic
  }
);

For server actions use wrapServerAction:

Copied
import * as Sentry from "@sentry/react-router";

export const action = Sentry.wrapServerAction(
  {
    name: "Submit Form Data",
    description: "Processes form submission data",
  },
  async ({ request }) => {
    // ... your action logic
  }
);

First, create a file called instrument.server.mjs in the root of your project to initialize Sentry:

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

Sentry.init({
  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,
  //  logs

  // Enable logs to be sent to Sentry
  enableLogs: true,
  //  logs
  //  profiling

  // Add our Profiling integration
  integrations: [nodeProfilingIntegration()],
  //  profiling
  //  performance
  // Set tracesSampleRate to 1.0 to capture 100%
  // of transactions for tracing.
  // We recommend adjusting this value in production
  // Learn more at
  // https://docs.sentry.io/platforms/javascript/guides/react-router/configuration/options/#tracesSampleRate
  tracesSampleRate: 1.0,
  //  performance
  //  profiling
  // Set profilesSampleRate to 1.0 to profile 100%
  // of sampled transactions.
  // This is relative to tracesSampleRate
  // Learn more at
  // https://docs.sentry.io/platforms/javascript/guides/react-router/configuration/options/#profilesSampleRate
  profilesSampleRate: 1.0,
  //  profiling
});

Next, replace the default handleRequest and handleError functions in your entry.server.tsx file with Sentry's wrapped versions:

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 = Sentry.createSentryHandleError({
+  logErrors: false
+});

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

If you need to customize the logic of your handleRequest function, you'll need to use Sentry's 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
Do you need to customize your handleError function?

If you have custom logic in your handleError function, you'll need to capture errors manually:

Copied
import {
  getMetaTagTransformer,
  wrapSentryHandleRequest,
} from "@sentry/react-router";
// ... other imports

export function handleError(
  error: unknown,
  { request, params, context }: LoaderFunctionArgs | ActionFunctionArgs,
) {
  if (!request.signal.aborted) {
Sentry.captureException(error);
console.error(formatErrorForJsonLogging(error)); } } // ... rest of your entry.server.ts file

React Router runs in ESM mode, which means you need to load the Sentry instrumentation file before the application starts. Update your package.json scripts:

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",
}
Are you using Windows?

If you're on Windows, set the NODE_OPTIONS environment variable manually before running your app.

Copied
set NODE_OPTIONS=--import ./instrument.server.mjs

Read more about environment variables.

Deploying to Vercel, Netlify, and similar platforms

If you're deploying to platforms where you can't set the NODE_OPTIONS flag, import the instrumentation file directly at the top of your entry.server.tsx:

entry.server.tsx
Copied
+import './instrument.server';
 import * as Sentry from '@sentry/react-router';
 import { createReadableStreamFromReadable } from '@react-router/node';
 import { renderToPipeableStream } from 'react-dom/server';
 // ... rest of your imports

The stack traces in your Sentry errors probably won't look like your actual code without unminifying them. To fix this, upload your source maps to Sentry.

First, update vite.config.ts to include the sentryReactRouter plugin, making sure to pass both the Vite and Sentry configurations to it:

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: "",
  project: "",

  // An auth token is required for uploading source maps;
  // store it in an environment variable to keep it secure.
  authToken: process.env.SENTRY_AUTH_TOKEN,
  // ...
};

export default defineConfig(config => {
  return {
+   plugins: [reactRouter(),sentryReactRouter(sentryConfig, config)],
  };
});

To keep your auth token secure, always store it in an environment variable instead of directly in your files:

.env
Copied
SENTRY_AUTH_TOKEN=sntrys_YOUR_TOKEN_HERE

Next, 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;

You can prevent ad blockers from blocking Sentry events using tunneling. Use the tunnel option to add an API endpoint in your application that forwards Sentry events to Sentry servers.

To enable tunneling, update Sentry.init with the following option:

Copied
Sentry.init({
  dsn: "",,
tunnel: "/tunnel",
});

This will send all events to the tunnel endpoint. However, the events need to be parsed and redirected to Sentry, so you'll need to do additional configuration on the server. You can find a detailed explanation on how to do this on our Troubleshooting page.

Let's test your setup and confirm that Sentry is working correctly and sending data to your Sentry project.

To verify that Sentry captures errors and creates issues in your Sentry project, throw an error in a loader:

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

export async function loader() {
  throw new Error("My first Sentry error!");
}

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

To test your tracing configuration, update the previous code snippet by starting a trace to measure the time it takes for the execution of your code:

error.tsx
Copied
import * as Sentry from "@sentry/react-router";
import type { Route } from "./+types/example-page";

export async function loader() {
  return Sentry.startSpan(
    {
      op: "test",
      name: "My First Test Transaction",
    },
    () => {
      throw new Error("My first Sentry error!");
    },
  );
}

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

Open the route in your browser. You should start a trace and trigger an error.

Now, head over to your project on Sentry.io to view the collected data (it takes a couple of moments for the data to appear).

Need help locating the captured errors in your Sentry project?
  1. Open the Issues page and select an error from the issues list to view the full details and context of this error. For more details, see this interactive walkthrough.
  2. Open the Traces page and select a trace to reveal more information about each span, its duration, and any errors. For an interactive UI walkthrough, click here.
  3. Open the Replays page and select an entry from the list to get a detailed view where you can replay the interaction and get more information to help you troubleshoot.
  4. Open the Logs page and filter by service, environment, or search keywords to view log entries from your application. For an interactive UI walkthrough, click here.

At this point, you should have integrated Sentry into your React Router Framework application and should already be sending data to your Sentry project.

Now's a good time to customize your setup and look into more advanced topics. Our next recommended steps for you are:

Are you having problems setting up the SDK?
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").