How to use Sentry with your Remix apps

Roger Stringer
July 28, 2023
5 min read

Sentry 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.

SaaS vs Self-Hosted

Sentry offers both a SaaS solution and self-hosted solution.

You can also use Glitchtip which has full support for the Senty API.

Signup

You can sign up for Sentry and create a Remix project from visting this url and filling out the signup form.

Onboarding

Once you see the onboarding page which has the DSN, copy that somewhere (this becomes SENTRY_DSN).

Install the Sentry SDK

To start, we'll have to install the @sentry/remix package:

bash
1pnpm add @sentry/remix
2

Sentry'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.

// .env:
1SENTRY_DSN=<your_dsn> 
2

Set up monitoring files

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:

// app/utils/monitoring.client.tsx:
1import { useLocation, useMatches } from '@remix-run/react'
2import * as Sentry from '@sentry/remix'
3import { useEffect } from 'react'
4
5export function init() {
6	Sentry.init({
7		dsn: process.env.SENTRY_DSN,
8		integrations: [
9			new Sentry.BrowserTracing({
10				routingInstrumentation: Sentry.remixRouterInstrumentation(
11					useEffect,
12					useLocation,
13					useMatches,
14				),
15			}),
16			new Sentry.Replay(),
17		],
18		tracesSampleRate: 1.0,
19		replaysSessionSampleRate: 0.1,
20		replaysOnErrorSampleRate: 1.0,
21	})
22}
23

Second, create a file called app/utils/monitoring.server.ts for the server side monitoring:

// app/utils/monitoring.server.ts:
1import * as Sentry from '@sentry/remix'
2
3export function init() {
4	Sentry.init({
5		dsn: process.env.SENTRY_DSN,
6		tracesSampleRate: 1,
7	})
8}
9

Add monitoring to our entry files

We'll add our monitoring files to the appropriate entry files, so first let's edit entry.client.tsx:

// entry.client.tsx:
1import { RemixBrowser } from '@remix-run/react'
2import { startTransition } from 'react'
3import { hydrateRoot } from 'react-dom/client'
4
5if (process.env.NODE_ENV === 'production' && process.env.SENTRY_DSN) {
6	import('~/utils/monitoring.client').then(({ init }) => init())
7}
8startTransition(() => {
9	hydrateRoot(document, <RemixBrowser />)
10})
11

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:

// entry.server.tsx:
1import { PassThrough } from "node:stream";
2
3import type { EntryContext } from "@remix-run/node";
4import { Response } from "@remix-run/node";
5import { RemixServer } from "@remix-run/react";
6import isbot from "isbot";
7import { renderToPipeableStream } from "react-dom/server";
8
9const ABORT_DELAY = 5_000;
10
11if (process.env.NODE_ENV === 'production' && process.env.SENTRY_DSN) {
12	import('~/utils/monitoring.server').then(({ init }) => init())
13}
14
15export default function handleRequest(
16  request: Request,
17  responseStatusCode: number,
18  responseHeaders: Headers,
19  remixContext: EntryContext
20) {
21  return isbot(request.headers.get("user-agent"))
22    ? handleBotRequest(
23        request,
24        responseStatusCode,
25        responseHeaders,
26        remixContext
27      )
28    : handleBrowserRequest(
29        request,
30        responseStatusCode,
31        responseHeaders,
32        remixContext
33      );
34}
35
36function handleBotRequest(
37  request: Request,
38  responseStatusCode: number,
39  responseHeaders: Headers,
40  remixContext: EntryContext
41) {
42  return new Promise((resolve, reject) => {
43    const { pipe, abort } = renderToPipeableStream(
44      <RemixServer
45        context={remixContext}
46        url={request.url}
47        abortDelay={ABORT_DELAY}
48      />,
49      {
50        onAllReady() {
51          const body = new PassThrough();
52
53          responseHeaders.set("Content-Type", "text/html");
54
55          resolve(
56            new Response(body, {
57              headers: responseHeaders,
58              status: responseStatusCode,
59            })
60          );
61
62          pipe(body);
63        },
64        onShellError(error: unknown) {
65          reject(error);
66        },
67        onError(error: unknown) {
68          responseStatusCode = 500;
69          console.error(error);
70        },
71      }
72    );
73
74    setTimeout(abort, ABORT_DELAY);
75  });
76}
77
78function handleBrowserRequest(
79  request: Request,
80  responseStatusCode: number,
81  responseHeaders: Headers,
82  remixContext: EntryContext
83) {
84  return new Promise((resolve, reject) => {
85    const { pipe, abort } = renderToPipeableStream(
86      <RemixServer
87        context={remixContext}
88        url={request.url}
89        abortDelay={ABORT_DELAY}
90      />,
91      {
92        onShellReady() {
93          const body = new PassThrough();
94
95          responseHeaders.set("Content-Type", "text/html");
96
97          resolve(
98            new Response(body, {
99              headers: responseHeaders,
100              status: responseStatusCode,
101            })
102          );
103
104          pipe(body);
105        },
106        onShellError(error: unknown) {
107          reject(error);
108        },
109        onError(error: unknown) {
110          console.error(error);
111          responseStatusCode = 500;
112        },
113      }
114    );
115
116    setTimeout(abort, ABORT_DELAY);
117  });
118}
119

💡FYI, I'm just using the entry.server and entry.client files from the Remix Indie Stack in this example. You may have other modifications in your entry files but the only ones that matter here are the sentry imports.

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.

Do you like my content?

Sponsor Me On Github