|
| 1 | +--- |
| 2 | +title: Hydrogen with React Router |
| 3 | +description: Learn how to use the Sentry React Router SDK to instrument your Hydrogen app (versions 2025.5.0+). |
| 4 | +--- |
| 5 | + |
| 6 | +<Alert level="info" title="Hydrogen Version"> |
| 7 | + |
| 8 | +This guide applies to Hydrogen versions **2025.5.0 and later** that use React Router 7 (framework mode). For older versions of Hydrogen that use Remix v2, see the [Remix guide](/platforms/javascript/guides/cloudflare/frameworks/hydrogen-remix/). |
| 9 | + |
| 10 | +</Alert> |
| 11 | + |
| 12 | +Starting from Hydrogen version 2025.5.0, Shopify switched from Remix v2 to React Router 7 (framework mode). You can use the Sentry React Router SDK with Cloudflare support to add Sentry instrumentation to your Hydrogen app. |
| 13 | + |
| 14 | +## Installing Sentry React Router and Cloudflare SDKs |
| 15 | + |
| 16 | +First, install the Sentry React Router and Cloudflare SDKs with your package manager: |
| 17 | + |
| 18 | +```bash {tabTitle:npm} |
| 19 | +npm install @sentry/react-router @sentry/cloudflare --save |
| 20 | +``` |
| 21 | + |
| 22 | +```bash {tabTitle:yarn} |
| 23 | +yarn add @sentry/react-router @sentry/cloudflare |
| 24 | +``` |
| 25 | + |
| 26 | +```bash {tabTitle:pnpm} |
| 27 | +pnpm add @sentry/react-router @sentry/cloudflare |
| 28 | +``` |
| 29 | + |
| 30 | +## Instrumenting Your Server |
| 31 | + |
| 32 | +Create an `instrument.server.mjs` file to initialize Sentry on the server: |
| 33 | + |
| 34 | +```js {filename:instrument.server.mjs} |
| 35 | +import * as Sentry from "@sentry/react-router"; |
| 36 | + |
| 37 | +Sentry.init({ |
| 38 | + dsn: "YOUR_DSN_HERE", |
| 39 | + // Adds request headers and IP for users, for more info visit: |
| 40 | + // https://docs.sentry.io/platforms/javascript/guides/react-router/configuration/options/#sendDefaultPii |
| 41 | + sendDefaultPii: true, |
| 42 | + tracesSampleRate: 1.0, |
| 43 | +}); |
| 44 | +``` |
| 45 | + |
| 46 | +Update your `server.ts` file to use the `wrapRequestHandler` method from `@sentry/cloudflare`: |
| 47 | + |
| 48 | +```ts {filename:server.ts} |
| 49 | +import { wrapRequestHandler } from '@sentry/cloudflare'; |
| 50 | +// ...other imports |
| 51 | + |
| 52 | +/** |
| 53 | + * Export a fetch handler in module format. |
| 54 | + */ |
| 55 | +export default { |
| 56 | + async fetch( |
| 57 | + request: Request, |
| 58 | + env: Env, |
| 59 | + executionContext: ExecutionContext |
| 60 | + ): Promise<Response> { |
| 61 | + return wrapRequestHandler( |
| 62 | + { |
| 63 | + options: { |
| 64 | + dsn: "YOUR_DSN_HERE", |
| 65 | + tracesSampleRate: 1.0, |
| 66 | + }, |
| 67 | + request: request as any, |
| 68 | + context: executionContext, |
| 69 | + }, |
| 70 | + async () => { |
| 71 | + // Your existing Hydrogen server logic |
| 72 | + const handleRequest = createRequestHandler({ |
| 73 | + // @ts-ignore |
| 74 | + build: await import('virtual:react-router/server-build'), |
| 75 | + mode: process.env.NODE_ENV, |
| 76 | + getLoadContext: (): AppLoadContext => ({ |
| 77 | + // your load context |
| 78 | + }), |
| 79 | + }); |
| 80 | + |
| 81 | + return handleRequest(request); |
| 82 | + } |
| 83 | + ); |
| 84 | + }, |
| 85 | +}; |
| 86 | +``` |
| 87 | + |
| 88 | +## Instrumenting Your Client |
| 89 | + |
| 90 | +Initialize Sentry in your `entry.client.tsx` file: |
| 91 | + |
| 92 | +```tsx {filename:app/entry.client.tsx} |
| 93 | +import { HydratedRouter } from 'react-router/dom'; |
| 94 | +import * as Sentry from '@sentry/react-router/cloudflare'; |
| 95 | +import { StrictMode, startTransition } from 'react'; |
| 96 | +import { hydrateRoot } from 'react-dom/client'; |
| 97 | + |
| 98 | +Sentry.init({ |
| 99 | + dsn: "___PUBLIC_DSN___", |
| 100 | + integrations: [Sentry.reactRouterTracingIntegration()], |
| 101 | + tracesSampleRate: 1.0, |
| 102 | +}); |
| 103 | + |
| 104 | +startTransition(() => { |
| 105 | + hydrateRoot( |
| 106 | + document, |
| 107 | + <StrictMode> |
| 108 | + <HydratedRouter /> |
| 109 | + </StrictMode>, |
| 110 | + ); |
| 111 | +}); |
| 112 | +``` |
| 113 | + |
| 114 | +## Server-Side Rendering with Trace Injection |
| 115 | + |
| 116 | +To enable distributed tracing, wrap your `handleRequest` function in your `entry.server.tsx` file and inject trace meta tags: |
| 117 | + |
| 118 | +```tsx {filename:app/entry.server.tsx} |
| 119 | +import './instrument.server'; |
| 120 | +import { HandleErrorFunction, ServerRouter } from 'react-router'; |
| 121 | +import type { EntryContext } from '@shopify/remix-oxygen'; |
| 122 | +import { renderToReadableStream } from 'react-dom/server'; |
| 123 | +import * as Sentry from '@sentry/react-router/cloudflare'; |
| 124 | + |
| 125 | +async function handleRequest( |
| 126 | + request: Request, |
| 127 | + responseStatusCode: number, |
| 128 | + responseHeaders: Headers, |
| 129 | + reactRouterContext: EntryContext, |
| 130 | +) { |
| 131 | + const body = Sentry.injectTraceMetaTags(await renderToReadableStream( |
| 132 | + <ServerRouter context={reactRouterContext} url={request.url} />, |
| 133 | + { |
| 134 | + signal: request.signal, |
| 135 | + }, |
| 136 | + )); |
| 137 | + |
| 138 | + responseHeaders.set('Content-Type', 'text/html'); |
| 139 | + |
| 140 | + return new Response(body, { |
| 141 | + headers: responseHeaders, |
| 142 | + status: responseStatusCode, |
| 143 | + }); |
| 144 | +} |
| 145 | + |
| 146 | +export const handleError: HandleErrorFunction = (error, { request }) => { |
| 147 | + // React Router may abort some interrupted requests, don't log those |
| 148 | + if (!request.signal.aborted) { |
| 149 | + Sentry.captureException(error); |
| 150 | + console.error(error); |
| 151 | + } |
| 152 | +}; |
| 153 | + |
| 154 | +export default Sentry.wrapSentryHandleRequest(handleRequest); |
| 155 | +``` |
| 156 | + |
| 157 | +## Configuration |
| 158 | + |
| 159 | +### Vite Configuration |
| 160 | + |
| 161 | +Add the Sentry plugin to your `vite.config.ts`: |
| 162 | + |
| 163 | +```ts {filename:vite.config.ts} |
| 164 | +import { reactRouter } from '@react-router/dev/vite'; |
| 165 | +import { hydrogen } from '@shopify/hydrogen/vite'; |
| 166 | +import { oxygen } from '@shopify/mini-oxygen/vite'; |
| 167 | +import { defineConfig } from 'vite'; |
| 168 | +import { sentryReactRouter } from '@sentry/react-router'; |
| 169 | + |
| 170 | +export default defineConfig(config => ({ |
| 171 | + plugins: [ |
| 172 | + hydrogen(), |
| 173 | + oxygen(), |
| 174 | + reactRouter(), |
| 175 | + sentryReactRouter({ |
| 176 | + org: "your-org-slug", |
| 177 | + project: "your-project-slug", |
| 178 | + authToken: process.env.SENTRY_AUTH_TOKEN, |
| 179 | + }, config), |
| 180 | + // ... other plugins |
| 181 | + ], |
| 182 | +})); |
| 183 | +``` |
| 184 | + |
| 185 | +### Build Configuration |
| 186 | + |
| 187 | +Add the `buildEnd` hook to your `react-router.config.ts`: |
| 188 | + |
| 189 | +```ts {filename:react-router.config.ts} |
| 190 | +import type {Config} from '@react-router/dev/config'; |
| 191 | +import { sentryOnBuildEnd } from '@sentry/react-router'; |
| 192 | + |
| 193 | +export default { |
| 194 | + appDirectory: 'app', |
| 195 | + buildDirectory: 'dist', |
| 196 | + ssr: true, |
| 197 | + buildEnd: async ({ viteConfig, reactRouterConfig, buildManifest }) => { |
| 198 | + // Call this at the end of the hook |
| 199 | + (await sentryOnBuildEnd({ viteConfig, reactRouterConfig, buildManifest })); |
| 200 | + } |
| 201 | +} satisfies Config; |
| 202 | +``` |
0 commit comments