# React Router Framework | Sentry for React Router Framework

##### Important

This SDK is currently in **beta**. Beta features are still in progress and may have bugs. Please reach out on [GitHub](https://github.com/getsentry/sentry-javascript/issues/new/choose) if you have any feedback or concerns.

##### Looking for library mode?

If you are using React Router in library mode, you can follow the instructions in the [React guide here](https://docs.sentry.io/platforms/javascript/guides/react/features/react-router/v7.md).

## [Prerequisites](https://docs.sentry.io/platforms/javascript/guides/react-router.md#prerequisites)

You need:

* A Sentry [account](https://sentry.io/signup/) and [project](https://docs.sentry.io/product/projects.md)
* Your application up and running

## [Install](https://docs.sentry.io/platforms/javascript/guides/react-router.md#install)

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

Error Monitoring\[ ]Tracing\[ ]Session Replay\[ ]User Feedback\[ ]Logs (Beta)\[ ]Profiling

```bash
npm install @sentry/react-router @sentry/profiling-node
```

```bash
npm install @sentry/react-router
```

## [Configure](https://docs.sentry.io/platforms/javascript/guides/react-router.md#configure)

### [Expose Hooks](https://docs.sentry.io/platforms/javascript/guides/react-router.md#expose-hooks)

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:

```bash
npx react-router reveal
```

### [Client-Side Setup](https://docs.sentry.io/platforms/javascript/guides/react-router.md#client-side-setup)

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

`entry.client.tsx`

```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: "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.reactRouterTracingIntegration(),
+    //  performance
+    //  session-replay
+    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
+  _experiments: { enableLogs: true },
+  //  logs
+  //  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`

```tsx
+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>
  );
}
// ...
```

### [Server-Side Setup](https://docs.sentry.io/platforms/javascript/guides/react-router.md#server-side-setup)

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 are on a different version please make use of our manual server wrappers.

For server loaders use `wrapServerLoader`:

```ts
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`:

```ts
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
  }
);
```

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

`instrument.server.mjs`

```js
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,
  //  logs

  // Enable logs to be sent to Sentry
  _experiments: { enableLogs: true },
  //  logs
  //  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`

```tsx
+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:

```tsx
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
```

### [Update Scripts](https://docs.sentry.io/platforms/javascript/guides/react-router.md#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. Update the `start` and `dev` script to include the instrumentation file:

Are you using Windows?

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

```bash
set NODE_OPTIONS=--import ./instrument.server.mjs
```

Read more about [environment variables](https://learn.microsoft.com/en-us/windows/win32/procthread/environment-variables).

`package.json`

```json
"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",
}
```

#### [Alternative Setup for Hosting Platforms](https://docs.sentry.io/platforms/javascript/guides/react-router.md#alternative-setup-for-hosting-platforms)

If you're deploying to platforms like **Vercel** or **Netlify** where setting the `NODE_OPTIONS` import flag is not possible, you can import the instrumentation file directly at the top of your `entry.server.tsx`:

`entry.server.tsx`

```tsx
+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
```

##### Incomplete Auto-instrumentation

When importing the instrumentation file directly in `entry.server.tsx` instead of using the `--import` flag, automatic instrumentation will be incomplete and you'll miss automatically captured spans and traces for some of your application's server-side operations. This approach should only be used when the `NODE_OPTIONS` approach is not available on your hosting platform.

## [Source Maps Upload](https://docs.sentry.io/platforms/javascript/guides/react-router.md#source-maps-upload)

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

`vite.config.ts`

```typescript
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;
  // 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`

```bash
SENTRY_AUTH_TOKEN=sntrys_YOUR_TOKEN_HERE
```

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

`react-router.config.ts`

```typescript
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;
```

## [Verify](https://docs.sentry.io/platforms/javascript/guides/react-router.md#verify)

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`

```tsx
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>;
}
```

Learn more about manually capturing an error or message in our [Usage documentation](https://docs.sentry.io/platforms/javascript/guides/react-router/usage.md).

To view and resolve the recorded error, log into [sentry.io](https://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.
