Hydrogen with Remix (Legacy)
Learn how to use the Sentry Remix SDK to instrument your Hydrogen app (versions before 2025.5.0).
Hydrogen Version
This guide applies to Hydrogen versions before 2025.5.0 that use Remix v2. For newer versions of Hydrogen (2025.5.0+) that use React Router 7, see the React Router guide.
If you're using Shopify's Hydrogen framework with Remix v2, you can use the Sentry Remix SDK to add Sentry instrumentation to your app.
First, install the Sentry Remix and Cloudflare SDKs with your package manager:
npm install @sentry/remix @sentry/cloudflare --save
npm install @sentry/remix @sentry/cloudflare --save
yarn add @sentry/remix @sentry/cloudflare
pnpm add @sentry/remix @sentry/cloudflare
Update your server.ts file to use the wrapRequestHandler method from @sentry/cloudflare/request and instrumentBuild from @sentry/remix/cloudflare:
server.tsimport { wrapRequestHandler } from "@sentry/cloudflare/request";
import { instrumentBuild } from "@sentry/remix/cloudflare";
import { createRequestHandler } from "@remix-run/cloudflare";
// Virtual entry point for the app
import * as remixBuild from "virtual:remix/server-build";
/**
* Export a fetch handler in module format.
*/
export default {
async fetch(
request: Request,
env: Env,
executionContext: ExecutionContext
): Promise<Response> {
return wrapRequestHandler(
{
options: {
dsn: "YOUR_DSN_HERE",
tracesSampleRate: 1.0,
},
// Need to cast to any because this is not on cloudflare
request: request as any,
context: executionContext,
},
async () => {
// Instrument your server build with Sentry
// and use the instrumented build inside the fetch handler
const instrumentedBuild = instrumentBuild(remixBuild);
const handleRequest = createRequestHandler({
build: instrumentedBuild,
mode: process.env.NODE_ENV,
getLoadContext: (): AppLoadContext => ({
// your load context
}),
});
return handleRequest(request);
}
);
},
};
import { wrapRequestHandler } from "@sentry/cloudflare/request";
import { instrumentBuild } from "@sentry/remix/cloudflare";
import { createRequestHandler } from "@remix-run/cloudflare";
// Virtual entry point for the app
import * as remixBuild from "virtual:remix/server-build";
/**
* Export a fetch handler in module format.
*/
export default {
async fetch(
request: Request,
env: Env,
executionContext: ExecutionContext
): Promise<Response> {
return wrapRequestHandler(
{
options: {
dsn: "YOUR_DSN_HERE",
tracesSampleRate: 1.0,
},
// Need to cast to any because this is not on cloudflare
request: request as any,
context: executionContext,
},
async () => {
// Instrument your server build with Sentry
// and use the instrumented build inside the fetch handler
const instrumentedBuild = instrumentBuild(remixBuild);
const handleRequest = createRequestHandler({
build: instrumentedBuild,
mode: process.env.NODE_ENV,
getLoadContext: (): AppLoadContext => ({
// your load context
}),
});
return handleRequest(request);
}
);
},
};
Wrap your Remix root component using withSentry:
app/root.tsximport * as Sentry from "@sentry/remix/cloudflare";
import { useEffect } from "react";
import { useLocation, useMatches } from "@remix-run/react";
function App() {
return (
// Your app content
);
}
// Pass `useEffect`, `useLocation` and `useMatches` hooks to `withSentry`
export default Sentry.withSentry(App, useEffect, useLocation, useMatches);
import * as Sentry from "@sentry/remix/cloudflare";
import { useEffect } from "react";
import { useLocation, useMatches } from "@remix-run/react";
function App() {
return (
// Your app content
);
}
// Pass `useEffect`, `useLocation` and `useMatches` hooks to `withSentry`
export default Sentry.withSentry(App, useEffect, useLocation, useMatches);
Finally, update your entry.client.tsx file to initialize Sentry SDK on the client:
app/entry.client.tsximport * as Sentry from "@sentry/remix/cloudflare";
import { useEffect } from "react";
import { useLocation, useMatches } from "@remix-run/react";
Sentry.init({
dsn: "___PUBLIC_DSN___",
integrations: [
Sentry.browserTracingIntegration({
useEffect,
useLocation,
useMatches,
}),
// Replay is only available in the client
Sentry.replayIntegration(),
],
// Set tracesSampleRate to 1.0 to capture 100%
// of transactions for tracing.
// We recommend adjusting this value in production
// Learn more at
// https://docs.sentry.io/platforms/javascript/configuration/options/#traces-sample-rate
tracesSampleRate: 1.0,
// Set `tracePropagationTargets` to control for which URLs distributed tracing should be enabled
tracePropagationTargets: ["localhost", /^https:\/\/yourserver\.io\/api/],
// Capture Replay for 10% of all sessions,
// plus for 100% of sessions with an error
// Learn more at
// https://docs.sentry.io/platforms/javascript/session-replay/configuration/#general-integration-configuration
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
});
import * as Sentry from "@sentry/remix/cloudflare";
import { useEffect } from "react";
import { useLocation, useMatches } from "@remix-run/react";
Sentry.init({
dsn: "___PUBLIC_DSN___",
integrations: [
Sentry.browserTracingIntegration({
useEffect,
useLocation,
useMatches,
}),
// Replay is only available in the client
Sentry.replayIntegration(),
],
// Set tracesSampleRate to 1.0 to capture 100%
// of transactions for tracing.
// We recommend adjusting this value in production
// Learn more at
// https://docs.sentry.io/platforms/javascript/configuration/options/#traces-sample-rate
tracesSampleRate: 1.0,
// Set `tracePropagationTargets` to control for which URLs distributed tracing should be enabled
tracePropagationTargets: ["localhost", /^https:\/\/yourserver\.io\/api/],
// Capture Replay for 10% of all sessions,
// plus for 100% of sessions with an error
// Learn more at
// https://docs.sentry.io/platforms/javascript/session-replay/configuration/#general-integration-configuration
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
});
Available since: v10.45.0
Because Hydrogen uses a custom entry.server.tsx that bypasses the default Remix document request handler, the SDK cannot automatically inject trace propagation headers. To connect server-side traces with client-side pageload spans, you need to manually add the Server-Timing header using generateSentryServerTimingHeader().
Update your entry.server.tsx to generate and append the header to your response:
app/entry.server.tsximport { generateSentryServerTimingHeader } from "@sentry/remix/cloudflare";
export default async function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
) {
// ...
const response = new Response(body, {
headers: responseHeaders,
status: responseStatusCode,
});
const serverTimingValue = generateSentryServerTimingHeader();
if (serverTimingValue) {
response.headers.append("Server-Timing", serverTimingValue);
}
return response;
}
import { generateSentryServerTimingHeader } from "@sentry/remix/cloudflare";
export default async function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
) {
// ...
const response = new Response(body, {
headers: responseHeaders,
status: responseStatusCode,
});
const serverTimingValue = generateSentryServerTimingHeader();
if (serverTimingValue) {
response.headers.append("Server-Timing", serverTimingValue);
}
return response;
}
The browser SDK automatically reads the Server-Timing header via the Performance API and uses it to connect the client-side pageload span with the server-side transaction. No additional client-side configuration is needed beyond having browserTracingIntegration configured.
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").