Hydrogen with React Router
Learn how to use the Sentry React Router SDK to instrument your Hydrogen app (versions 2025.5.0+).
Hydrogen Version
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.
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.
First, install the Sentry React Router and Cloudflare SDKs with your package manager:
npm install @sentry/react-router @sentry/cloudflare --legacy-peer-deps --save
Create an instrument.server.mjs
file to initialize Sentry on the server:
instrument.server.mjs
import * as Sentry from "@sentry/react-router";
Sentry.init({
dsn: "https://examplePublicKey@o0.ingest.sentry.io/0example-org / example-project",
// Adds request headers and IP for users, for more info visit:
// https://docs.sentry.io/platforms/javascript/guides/react-router/configuration/options/#sendDefaultPii
sendDefaultPii: true,
tracesSampleRate: 1.0,
});
Update your server.ts
file to use the wrapRequestHandler
method from @sentry/cloudflare
:
server.ts
// Virtual entry point for the app
import { wrapRequestHandler } from '@sentry/cloudflare/request';
import { storefrontRedirect } from '@shopify/hydrogen';
import { createRequestHandler } from '@shopify/remix-oxygen';
import { createAppLoadContext } from '~/lib/context';
/**
* Export a fetch handler in module format.
*/
export default {
async fetch(
request: Request,
env: Env,
executionContext: ExecutionContext,
): Promise<Response> {
return wrapRequestHandler(
{
options: {
dsn: "https://examplePublicKey@o0.ingest.sentry.io/0example-org / example-project",
tracesSampleRate: 1.0,
},
request: request as any,
context: executionContext,
},
async () => {
try {
const appLoadContext = await createAppLoadContext(
request,
env,
executionContext,
);
/**
* Create a Remix request handler and pass
* Hydrogen's Storefront client to the loader context.
*/
const handleRequest = createRequestHandler({
// eslint-disable-next-line import/no-unresolved
build: await import('virtual:react-router/server-build'),
mode: process.env.NODE_ENV,
getLoadContext: () => appLoadContext,
});
const response = await handleRequest(request);
if (appLoadContext.session.isPending) {
response.headers.set(
'Set-Cookie',
await appLoadContext.session.commit(),
);
}
if (response.status === 404) {
/**
* Check for redirects only when there's a 404 from the app.
* If the redirect doesn't exist, then `storefrontRedirect`
* will pass through the 404 response.
*/
return storefrontRedirect({
request,
response,
storefront: appLoadContext.storefront,
});
}
return response;
} catch (error) {
console.error(error);
return new Response('An unexpected error occurred', {status: 500});
}
}
);
},
};
Initialize Sentry in your entry.client.tsx
file:
app/entry.client.tsx
import { HydratedRouter } from "react-router/dom";
import * as Sentry from "@sentry/react-router/cloudflare";
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
Sentry.init({
dsn: "https://examplePublicKey@o0.ingest.sentry.io/0example-org / example-project",
integrations: [Sentry.reactRouterTracingIntegration()],
tracesSampleRate: 1.0,
});
if (!window.location.origin.includes("webcache.googleusercontent.com")) {
startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<HydratedRouter />
</StrictMode>,
);
});
}
To enable distributed tracing, wrap your handleRequest
function in your entry.server.tsx
file and inject trace meta tags:
app/entry.server.tsx
import "../instrument.server.mjs";
import { type AppLoadContext } from "@shopify/remix-oxygen";
import {
type HandleErrorFunction,
type EntryContext,
ServerRouter,
} from "react-router";
import { renderToReadableStream } from "react-dom/server";
import { createContentSecurityPolicy } from "@shopify/hydrogen";
import * as Sentry from "@sentry/react-router/cloudflare";
async function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
reactRouterContext: EntryContext,
context: AppLoadContext,
) {
const { nonce, header, NonceProvider } = createContentSecurityPolicy({
shop: {
checkoutDomain: context.env.PUBLIC_CHECKOUT_DOMAIN,
storeDomain: context.env.PUBLIC_STORE_DOMAIN,
},
});
const body = Sentry.injectTraceMetaTags(
await renderToReadableStream(
<NonceProvider>
<ServerRouter
context={reactRouterContext}
url={request.url}
nonce={nonce}
/>
</NonceProvider>,
{
nonce,
signal: request.signal,
onError(error) {
console.error(error);
responseStatusCode = 500;
},
},
),
);
responseHeaders.set("Content-Type", "text/html");
responseHeaders.set("Content-Security-Policy", header);
return new Response(body, {
headers: responseHeaders,
status: responseStatusCode,
});
}
export const handleError: HandleErrorFunction = (error, { request }) => {
// React Router may abort some interrupted requests, don't log those
if (!request.signal.aborted) {
Sentry.captureException(error);
console.error(error);
}
};
export default Sentry.wrapSentryHandleRequest(handleRequest);
Add the Sentry plugin to your vite.config.ts
:
vite.config.ts
import { reactRouter } from '@react-router/dev/vite';
import { hydrogen } from '@shopify/hydrogen/vite';
import { oxygen } from '@shopify/mini-oxygen/vite';
import { defineConfig } from 'vite';
import { sentryReactRouter } from '@sentry/react-router';
export default defineConfig(config => ({
plugins: [
hydrogen(),
oxygen(),
reactRouter(),
sentryReactRouter({
org: "your-org-slug",
project: "your-project-slug",
authToken: process.env.SENTRY_AUTH_TOKEN,
}, config),
// ... other plugins
],
}));
Add the buildEnd
hook to your react-router.config.ts
:
react-router.config.ts
import type {Config} from '@react-router/dev/config';
import { sentryOnBuildEnd } from '@sentry/react-router';
export default {
appDirectory: 'app',
buildDirectory: 'dist',
ssr: true,
buildEnd: async ({ viteConfig, reactRouterConfig, buildManifest }) => {
// Call this at the end of the hook
(await sentryOnBuildEnd({ viteConfig, reactRouterConfig, buildManifest }));
}
} satisfies Config;
Our documentation is open source and available on GitHub. Your contributions are welcome, whether fixing a typo (drat!) or suggesting an update ("yeah, this would be better").