Hydrogen with Remix (Legacy) for Cloudflare
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:
On Shopify Oxygen, never import anything directly from @sentry/cloudflare. Only use the @sentry/cloudflare/request subpath import shown below. All other imports (for example, import * as Sentry from "@sentry/cloudflare") will break your app on Oxygen — use @sentry/remix/cloudflare for everything else.
No request isolation
wrapRequestHandler doesn't isolate requests from each other. Scope data you set during a request, for example with setTag or setUser, can show up on events from other concurrent or later requests in the same isolate. Use wrapRequestHandler only on runtimes that can't enable nodejs_compat, such as Shopify Oxygen. For Remix on Cloudflare Workers, follow the Remix on Cloudflare guide instead.
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,
});
By default, the SDK sends user identity data (IP address, ID, and similar) and other data like HTTP bodies and URL query parameters. This will give you rich debugging context.
The SDK always filters sensitive values whose keys match a built-in denylist, such as auth or password, and sends [Filtered] instead.
To send less data, turn off the categories you don't need in the dataCollection option. For the full list of categories and their defaults, see the dataCollection options.
Sentry.init({
dsn: "___PUBLIC_DSN___",
dataCollection: {
userInfo: false,
// other categories
},
});
Sentry.init({
dsn: "___PUBLIC_DSN___",
dataCollection: {
userInfo: false,
// other categories
},
});
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").