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
Install the Package
Install @sentry/remix using your preferred package manager:npm install @sentry/remix
Current Version: 10.42.0 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 />);
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);
});
}
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);
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>
);
}
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,
});
Breadcrumbs
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
-
Build your app with source maps:
-
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
- Ensure both
entry.client.tsx and entry.server.tsx are configured
- Check that
withSentry wraps your root component
- Verify DSN is correct in both client and server configs
Source Maps Not Working
- Ensure you’re building with
--sourcemap flag
- Check that source maps are being uploaded correctly
- Verify release version matches between build and upload
- Ensure
tracesSampleRate is greater than 0
- Check that integrations are configured properly
- 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