Skip to main content
The @sentry/remix package provides comprehensive error tracking and performance monitoring for Remix applications, with support for both client and server environments.

Prerequisites

  • Node.js 18 or newer
  • Remix 2.x
  • React 18.x
  • A Sentry account and project DSN

Installation

1

Install the Package

Install @sentry/remix using your preferred package manager:
npm install @sentry/remix
Current Version: 10.42.0
2

Initialize Client-Side

Create or update app/entry.client.tsx:
import { useLocation, useMatches } from '@remix-run/react';
import * as Sentry from '@sentry/remix';
import { useEffect } from 'react';
import { hydrateRoot } from 'react-dom/client';
import { RemixBrowser } from '@remix-run/react';

Sentry.init({
  dsn: 'YOUR_DSN_HERE',
  
  tracesSampleRate: 1.0,
  
  replaysSessionSampleRate: 0.1,
  replaysOnErrorSampleRate: 1.0,
  
  integrations: [
    Sentry.browserTracingIntegration({
      useEffect,
      useLocation,
      useMatches,
    }),
    Sentry.replayIntegration(),
  ],
});

hydrateRoot(document, <RemixBrowser />);
3

Initialize Server-Side

Create or update app/entry.server.tsx:
import { PassThrough } from 'stream';
import type { EntryContext } from '@remix-run/node';
import { Response } from '@remix-run/node';
import { RemixServer } from '@remix-run/react';
import { renderToPipeableStream } from 'react-dom/server';
import * as Sentry from '@sentry/remix';

Sentry.init({
  dsn: 'YOUR_DSN_HERE',
  tracesSampleRate: 1.0,
});

export default function handleRequest(
  request: Request,
  responseStatusCode: number,
  responseHeaders: Headers,
  remixContext: EntryContext
) {
  return new Promise((resolve, reject) => {
    const { pipe, abort } = renderToPipeableStream(
      <RemixServer context={remixContext} url={request.url} />,
      {
        onShellReady: () => {
          const body = new PassThrough();
          responseHeaders.set('Content-Type', 'text/html');
          
          resolve(
            new Response(body as any, {
              headers: responseHeaders,
              status: responseStatusCode,
            })
          );
          
          pipe(body);
        },
        onShellError: (error: unknown) => {
          reject(error);
        },
        onError: (error: unknown) => {
          responseStatusCode = 500;
          Sentry.captureException(error);
        },
      }
    );
    
    setTimeout(abort, 5000);
  });
}
4

Wrap Root Component

Wrap your root component with withSentry in app/root.tsx:
import {
  Links,
  LiveReload,
  Meta,
  Outlet,
  Scripts,
  ScrollRestoration,
} from '@remix-run/react';
import { withSentry } from '@sentry/remix';

function App() {
  return (
    <html lang="en">
      <head>
        <Meta />
        <Links />
      </head>
      <body>
        <Outlet />
        <ScrollRestoration />
        <Scripts />
        <LiveReload />
      </body>
    </html>
  );
}

export default withSentry(App);
5

Verify Installation

Create a test route to verify Sentry is working:
// app/routes/sentry-test.tsx
import * as Sentry from '@sentry/remix';

export default function SentryTest() {
  return (
    <button
      onClick={() => {
        Sentry.captureException(new Error('Test error'));
      }}
    >
      Trigger Test Error
    </button>
  );
}
Visit the route and click the button. Check your Sentry dashboard to see the error.

Loaders and Actions

Loader Functions

Errors in loaders are automatically captured:
// app/routes/users.$id.tsx
import type { LoaderFunctionArgs } from '@remix-run/node';
import { json } from '@remix-run/node';
import * as Sentry from '@sentry/remix';

export async function loader({ params }: LoaderFunctionArgs) {
  try {
    const user = await fetchUser(params.id);
    return json({ user });
  } catch (error) {
    Sentry.captureException(error);
    throw new Response('User not found', { status: 404 });
  }
}

Action Functions

// app/routes/contact.tsx
import type { ActionFunctionArgs } from '@remix-run/node';
import { json } from '@remix-run/node';
import * as Sentry from '@sentry/remix';

export async function action({ request }: ActionFunctionArgs) {
  const formData = await request.formData();
  const email = formData.get('email');
  
  try {
    await sendEmail(email);
    return json({ success: true });
  } catch (error) {
    Sentry.captureException(error);
    return json({ success: false, error: 'Failed to send email' }, { status: 500 });
  }
}

Error Boundaries

Create custom error boundaries with Sentry integration:
// app/routes/_index.tsx
import { useRouteError, isRouteErrorResponse } from '@remix-run/react';
import * as Sentry from '@sentry/remix';
import { useEffect } from 'react';

export function ErrorBoundary() {
  const error = useRouteError();
  
  useEffect(() => {
    if (!isRouteErrorResponse(error)) {
      Sentry.captureException(error);
    }
  }, [error]);
  
  if (isRouteErrorResponse(error)) {
    return (
      <div>
        <h1>{error.status} {error.statusText}</h1>
        <p>{error.data}</p>
      </div>
    );
  }
  
  return (
    <div>
      <h1>Error</h1>
      <p>Something went wrong</p>
    </div>
  );
}

Performance Monitoring

Automatic Instrumentation

The SDK automatically creates spans for:
  • Page loads and navigation
  • Loaders and actions
  • Server requests
  • Database queries (with integrations)

Custom Spans

import * as Sentry from '@sentry/remix';
import type { LoaderFunctionArgs } from '@remix-run/node';

export async function loader({ params }: LoaderFunctionArgs) {
  const data = await Sentry.startSpan(
    { name: 'fetch-user-data', op: 'db.query' },
    async () => {
      return await db.user.findUnique({
        where: { id: params.id },
      });
    }
  );
  
  return json({ data });
}

Database Integrations

Prisma

// app/entry.server.tsx
import * as Sentry from '@sentry/remix';

Sentry.init({
  dsn: 'YOUR_DSN_HERE',
  integrations: [
    Sentry.prismaIntegration(),
  ],
  tracesSampleRate: 1.0,
});
Then use Prisma normally:
import { prisma } from '~/db.server';

export async function loader() {
  // Prisma queries are automatically instrumented
  const users = await prisma.user.findMany();
  return json({ users });
}

MongoDB

// app/entry.server.tsx
import * as Sentry from '@sentry/remix';

Sentry.init({
  dsn: 'YOUR_DSN_HERE',
  integrations: [
    Sentry.mongoIntegration(),
  ],
  tracesSampleRate: 1.0,
});

Setting Context

User Context

// app/root.tsx
import { useLoaderData } from '@remix-run/react';
import * as Sentry from '@sentry/remix';
import { useEffect } from 'react';

export default function App() {
  const { user } = useLoaderData<typeof loader>();
  
  useEffect(() => {
    if (user) {
      Sentry.setUser({
        id: user.id,
        email: user.email,
        username: user.username,
      });
    } else {
      Sentry.setUser(null);
    }
  }, [user]);
  
  return (
    // ... rest of your app
  );
}

Tags and Extra Context

import * as Sentry from '@sentry/remix';

// Set tags
Sentry.setTag('route', location.pathname);
Sentry.setTag('user_tier', 'premium');

// Set extra context
Sentry.setExtra('api_version', 'v2');
Sentry.setContext('request', {
  method: request.method,
  url: request.url,
});
import * as Sentry from '@sentry/remix';

Sentry.addBreadcrumb({
  category: 'action',
  message: 'User submitted form',
  level: 'info',
  data: {
    formId: 'contact-form',
  },
});

Session Replay

Configure Session Replay in your client entry:
// app/entry.client.tsx
import * as Sentry from '@sentry/remix';

Sentry.init({
  dsn: 'YOUR_DSN_HERE',
  
  integrations: [
    Sentry.replayIntegration({
      maskAllText: true,
      blockAllMedia: true,
      maskAllInputs: true,
    }),
  ],
  
  replaysSessionSampleRate: 0.1,
  replaysOnErrorSampleRate: 1.0,
});

Source Maps

Sentry provides a CLI tool for uploading source maps:

Using the Upload Script

  1. Build your app with source maps:
    remix build --sourcemap
    
  2. Upload source maps:
    npx sentry-upload-sourcemaps \
      --org your-org \
      --project your-project \
      --auth-token YOUR_AUTH_TOKEN \
      --release VERSION
    

Automated with package.json

{
  "scripts": {
    "build": "remix build --sourcemap",
    "postbuild": "sentry-upload-sourcemaps --org your-org --project your-project"
  }
}

Using Sentry CLI Directly

For more control, use sentry-cli directly:
sentry-cli releases new VERSION
sentry-cli releases files VERSION upload-sourcemaps ./build
sentry-cli releases finalize VERSION

Cloudflare Workers

For Cloudflare Workers deployment:
// app/entry.server.tsx
import * as Sentry from '@sentry/remix/cloudflare';

Sentry.init({
  dsn: 'YOUR_DSN_HERE',
  tracesSampleRate: 1.0,
});
Import from @sentry/remix/cloudflare instead of @sentry/remix for edge runtime support.

Advanced Configuration

Sampling

Sentry.init({
  dsn: 'YOUR_DSN_HERE',
  
  tracesSampler: (samplingContext) => {
    // Don't sample health checks
    if (samplingContext.request?.url?.includes('/health')) {
      return 0;
    }
    
    // Sample API routes at 50%
    if (samplingContext.request?.url?.includes('/api')) {
      return 0.5;
    }
    
    return 0.1;
  },
});

Environment Detection

Sentry.init({
  dsn: 'YOUR_DSN_HERE',
  environment: process.env.NODE_ENV,
  enabled: process.env.NODE_ENV === 'production',
});

Custom Transport

import { makeFetchTransport } from '@sentry/remix';

Sentry.init({
  dsn: 'YOUR_DSN_HERE',
  transport: makeFetchTransport,
});

Troubleshooting

Errors Not Captured

  1. Ensure both entry.client.tsx and entry.server.tsx are configured
  2. Check that withSentry wraps your root component
  3. Verify DSN is correct in both client and server configs

Source Maps Not Working

  1. Ensure you’re building with --sourcemap flag
  2. Check that source maps are being uploaded correctly
  3. Verify release version matches between build and upload

Performance Data Missing

  1. Ensure tracesSampleRate is greater than 0
  2. Check that integrations are configured properly
  3. Verify Sentry is initialized before app runs
Make sure to call Sentry.init() in both entry.client.tsx and entry.server.tsx with appropriate configurations for each environment.

Next Steps

Build docs developers (and LLMs) love