
Migrating this site to EmDash
This site has changed CMS more times than I can count since 2003. This time I moved to EmDash on Cloudflare, and the reason is that most of my building now happens alongside an AI agent.
Read moreSentry is great for error monitoring, and flexible.
This post will show how to add Sentry to your remix apps so it's set up for error, performance, and replay monitoring.
Sentry offers both a SaaS solution and
You can also use Glitchtip which has full support for the Senty API.
You can sign up for Sentry and create a Remix project from visting
this url and filling out the signup form.
Once you see the onboarding page which has the DSN, copy that somewhere (this
becomes SENTRY_DSN).
To start, we'll have to install the @sentry/remix package:
pnpm add @sentry/remixSentry's remix package works well for our needs so we'll use that.
You'll also need to add an environment variable called SENTRY_DSN which is the client DSN sentry gave you when you created your project.
SENTRY_DSN=<your_dsn> We need to create two files, one for monitoring client side, and one for monitoring server side.
First, create a file called app/utils/monitoring.client.tsx for client-side monitoring:
import { useLocation, useMatches } from '@remix-run/react'
import * as Sentry from '@sentry/remix'
import { useEffect } from 'react'
export function init() {
Sentry.init({
dsn: process.env.SENTRY_DSN,
integrations: [
new Sentry.BrowserTracing({
routingInstrumentation: Sentry.remixRouterInstrumentation(
useEffect,
useLocation,
useMatches,
),
}),
new Sentry.Replay(),
],
tracesSampleRate: 1.0,
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
})
}Second, create a file called app/utils/monitoring.server.ts for the server side monitoring:
import * as Sentry from '@sentry/remix'
export function init() {
Sentry.init({
dsn: process.env.SENTRY_DSN,
tracesSampleRate: 1,
})
}We'll add our monitoring files to the appropriate entry files, so first let's edit entry.client.tsx:
import { RemixBrowser } from '@remix-run/react'
import { startTransition } from 'react'
import { hydrateRoot } from 'react-dom/client'
if (process.env.NODE_ENV === 'production' && process.env.SENTRY_DSN) {
import('~/utils/monitoring.client').then(({ init }) => init())
}
startTransition(() => {
hydrateRoot(document, <RemixBrowser />)
})This will load our monitoring.client file if we are in production mode and the SENTRY_DSN environment variable has been configured.
Next, let's edit our entry.server.tsx file:
import { PassThrough } from "node:stream";
import type { EntryContext } from "@remix-run/node";
import { Response } from "@remix-run/node";
import { RemixServer } from "@remix-run/react";
import isbot from "isbot";
import { renderToPipeableStream } from "react-dom/server";
const ABORT_DELAY = 5_000;
if (process.env.NODE_ENV === 'production' && process.env.SENTRY_DSN) {
import('~/utils/monitoring.server').then(({ init }) => init())
}
export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
) {
return isbot(request.headers.get("user-agent"))
? handleBotRequest(
request,
responseStatusCode,
responseHeaders,
remixContext
)
: handleBrowserRequest(
request,
responseStatusCode,
responseHeaders,
remixContext
);
}
function handleBotRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
) {
return new Promise((resolve, reject) => {
const { pipe, abort } = renderToPipeableStream(
<RemixServer
context={remixContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>,
{
onAllReady() {
const body = new PassThrough();
responseHeaders.set("Content-Type", "text/html");
resolve(
new Response(body, {
headers: responseHeaders,
status: responseStatusCode,
})
);
pipe(body);
},
onShellError(error: unknown) {
reject(error);
},
onError(error: unknown) {
responseStatusCode = 500;
console.error(error);
},
}
);
setTimeout(abort, ABORT_DELAY);
});
}
function handleBrowserRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
) {
return new Promise((resolve, reject) => {
const { pipe, abort } = renderToPipeableStream(
<RemixServer
context={remixContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>,
{
onShellReady() {
const body = new PassThrough();
responseHeaders.set("Content-Type", "text/html");
resolve(
new Response(body, {
headers: responseHeaders,
status: responseStatusCode,
})
);
pipe(body);
},
onShellError(error: unknown) {
reject(error);
},
onError(error: unknown) {
console.error(error);
responseStatusCode = 500;
},
}
);
setTimeout(abort, ABORT_DELAY);
});
}That's it, we've added sentry monitoring to both the server and client side of our app and now we'll start getting reports showing up in Sentry as errors happen.

This site has changed CMS more times than I can count since 2003. This time I moved to EmDash on Cloudflare, and the reason is that most of my building now happens alongside an AI agent.
Read more
I wanted a simple dashboard that showed me events as they happened, so I wrote a quick little Astro app. It grew into Get Butters: a real-time event feed with a REST API, a CLI, insight cards and push alerts.
Read more
Once four agents can edit the same repo, architecture stops being the hard problem and coordination starts. The five roles I run and what each one is forbidden to touch, why every handoff goes through a file, a worktree script so agents stop racing, and the failure modes worth naming.
Read more