Skip to content

Instantly share code, notes, and snippets.

@huw
Last active March 12, 2024 21:32
Show Gist options
  • Star 12 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save huw/e6749ea2e205e0179d0c87c3a9859f9e to your computer and use it in GitHub Desktop.
Save huw/e6749ea2e205e0179d0c87c3a9859f9e to your computer and use it in GitHub Desktop.
Remix, Sentry & Cloudflare Workers

How to integrate Remix, Sentry, and Cloudflare Workers (incl. tracing)

The code above is just a sample—I have adapted it from a production codebase and I can't guarantee it will work as-is. It's just here to illustrate what a solution should look like.

The main things you need to do to get everything hooked up are:

  1. Rewrite instrumentBuild to accept a passed-through Hub instance, rather than using getCurrentHub() (including, perniciously, in helper functions such as hasTracingEnabled())
  2. In server/index.ts, create a root transaction from the current route’s name and wrap the loaders and actions in spans via instrumentBuild.
  3. In root.tsx, pass through your tracing & baggage into the meta function.
  4. In root.tsx, include the current domain in tracePropagationTargets (because Remix fetchers will fetch from the entire URL rather than / root, which will confuse Sentry)
  5. (Remix v2) In root.tsx, create an ErrorBoundary component (with the v2_errorBoundary flag set if you're on a version below v2). You'll need to edit captureRemixErrorBoundaryError from @sentry/remix to detect the Workers environment, and call it.
  6. (Remix v2) In entry.server.tsx, wrap <RemixServer> in a custom RemixSSRContext that will store your current Toucan Hub instance. We'll use this in the error boundary if we render it in SSR, otherwise we'll use the client-side Hub. For this, you'll also need to make sure that you add sentry (your Toucan Hub) to loadContext in server/index.ts, so we can access it in entry.server.tsx.
  7. (Remix v2) In entry.server.tsx, export handleError which will capture any server-side errors. This relies on a few more rewritten functions.

The long and the short of it is—it's a real pain to configure. Expect this to take multiple days on a modest codebase; most of it will be spent wrapping your head around how Sentry & Remix capture errors so you can make sure you're not missing anything. Reply to this Gist with questions and I'll be happy to help!

import { isRouteErrorResponse } from "@remix-run/react";
import * as Sentry from "@sentry/remix";
import { addExceptionMechanism, isString } from "@sentry/utils";
import { useContext } from "react";
import { SentrySSRContext } from "./SentrySSRContext";
declare const document: unknown;
// Adapted from [Sentry](https://github.com/getsentry/sentry-javascript/blob/540adac9ec81803f86a3a7f5b34ebbc1ad2a8d23/packages/remix/src/client/errors.tsx#L18-L65)
export const captureRemixErrorBoundaryError = (error: unknown) => {
const sentrySSR = useContext(SentrySSRContext);
const sentry = typeof document === "undefined" ? sentrySSR : Sentry.getCurrentHub();
if (sentry === undefined) {
throw new Error("Sentry wasn't correctly initialized. Error reports will not be delivered.");
}
const isClientSideRuntimeError = typeof document !== "undefined" && error instanceof Error;
const isRemixErrorResponse = isRouteErrorResponse(error);
// Server-side errors apart from `ErrorResponse`s also appear here without their stacktraces.
// So, we only capture:
// 1. `ErrorResponse`s
// 2. Client-side runtime errors here,
// And other server - side errors in `handleError` function where stacktraces are available.
if (isRemixErrorResponse || isClientSideRuntimeError) {
const eventData = isRemixErrorResponse
? {
function: "ErrorResponse",
...(error.data as { [key: string]: unknown }),
}
: {
function: "ReactError",
};
sentry.withScope((scope) => {
scope.addEventProcessor((event) => {
addExceptionMechanism(event, {
type: "instrument",
handled: true,
data: eventData,
});
return event;
});
if (isRemixErrorResponse) {
if (isString(error.data)) {
sentry.captureException(error.data);
} else if (error.statusText) {
sentry.captureException(error.statusText);
} else {
sentry.captureException(error);
}
} else {
sentry.captureException(error);
}
});
}
};
import { isResponse } from "@remix-run/server-runtime/dist/responses";
import { type Hub } from "@sentry/types";
import { addExceptionMechanism } from "@sentry/utils";
const extractData = async (response: Response) => {
const contentType = response.headers.get("Content-Type");
// Cloning the response to avoid consuming the original body stream
const responseClone = response.clone();
if (contentType !== null && /\bapplication\/json\b/u.test(contentType)) {
return await responseClone.json();
}
return await responseClone.text();
};
const extractResponseError = async (response: Response) => {
const responseData = await extractData(response);
if (typeof responseData === "string") {
return responseData;
}
if (response.statusText) {
return response.statusText;
}
return responseData;
};
// Adapted from [Sentry](https://github.com/getsentry/sentry-javascript/blob/540adac9ec81803f86a3a7f5b34ebbc1ad2a8d23/packages/remix/src/utils/instrumentServer.ts#L63C1-L118)
export const captureRemixServerException = async (
hub: Hub,
error: unknown,
name: string,
request: Request
): Promise<void> => {
// Skip capturing if the thrown error is not a 5xx response
// https://remix.run/docs/en/v1/api/conventions#throwing-responses-in-loaders
if (isResponse(error) && error.status < 500) {
return;
}
const scope = hub.getScope();
const activeTransactionName = scope.getTransaction()?.name;
scope.setSDKProcessingMetadata({
request: {
...request,
// When `route` is not defined, `RequestData` integration uses the full URL
route:
activeTransactionName !== undefined
? {
path: activeTransactionName,
}
: undefined,
},
});
scope.addEventProcessor((event) => {
addExceptionMechanism(event, {
type: "instrument",
handled: true,
data: {
function: name,
},
});
return event;
});
const normalizedError = isResponse(error) ? await extractResponseError(error) : error;
// eslint-disable-next-line no-console -- This is fine, errors will end up in the Cloudflare logs which should be helpful at the end of the day.
console.error(normalizedError);
hub.captureException(normalizedError);
};
import { type AppLoadContext, type EntryContext } from "@remix-run/cloudflare";
import { RemixServer } from "@remix-run/react";
import isbot from "isbot";
import { renderToReadableStream } from "react-dom/server";
import { SentrySSRContext } from "./SentrySSRContext";
export { handleError } from "./handleError";
const entry = async (
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
loadContext: AppLoadContext
) => {
let updatedResponseStatusCode = responseStatusCode;
const body = await renderToReadableStream(
<SentrySSRContext.Provider value={loadContext.sentry}>
<RemixServer context={remixContext} url={request.url} />
</SentrySSRContext.Provider>,
{
signal: request.signal,
onError: (error) => {
// Don't capture errors with Sentry here; they'll be handled by Remix.
updatedResponseStatusCode = 500;
},
}
);
if (isbot(request.headers.get("user-agent"))) {
await body.allReady;
}
responseHeaders.set("Content-Type", "text/html");
return new Response(body, {
headers: responseHeaders,
status: updatedResponseStatusCode,
});
};
export default entry;
import { matchServerRoutes, type RouteMatch } from "@remix-run/server-runtime/dist/routeMatching";
import { type ServerRoute } from "@remix-run/server-runtime/dist/routes";
import { type TransactionSource } from "@sentry/types";
// https://github.com/remix-run/remix/blob/97999d02493e8114c39d48b76944069d58526e8d/packages/remix-server-runtime/server.ts#L573-L586
const isIndexRequestUrl = (url: URL): boolean => {
for (const parameter of url.searchParams.getAll("index")) {
// only use bare `?index` params without a value
// ✅ /foo?index
// ✅ /foo?index&index=123
// ✅ /foo?index=123&index
// ❌ /foo?index=123
if (parameter === "") {
return true;
}
}
return false;
};
// https://github.com/remix-run/remix/blob/97999d02493e8114c39d48b76944069d58526e8d/packages/remix-server-runtime/server.ts#L588-L596
const getRequestMatch = (url: URL, matches: RouteMatch<ServerRoute>[]): RouteMatch<ServerRoute> => {
const match = matches.slice(-1)[0];
if (match === undefined) {
throw new Error("No match found in the array. This should never occur.");
}
if (!isIndexRequestUrl(url) && match.route.id.endsWith("/index")) {
const nextMatch = matches.slice(-2)[0];
if (nextMatch === undefined) {
throw new Error("No match found in the array. This should never occur.");
}
return nextMatch;
}
return match;
};
/**
* Get transaction name from routes and url
*/
export const getTransactionDetails = (
routes: ServerRoute[],
url: URL
): { name: string; source: TransactionSource; params?: { [key: string]: string | undefined } } => {
const matches = matchServerRoutes(routes, url.pathname);
const match = matches && getRequestMatch(url, matches);
return match === null
? { name: url.pathname, source: "url" }
: { name: match.route.id, source: "route", params: match.params };
};
import { type DataFunctionArgs } from "@remix-run/server-runtime";
import { type Hub } from "@sentry/types";
import { captureRemixServerException } from "./captureRemixServerException";
export const handleError = async (
error: unknown,
{ request, context: { sentry } }: DataFunctionArgs & { context: { sentry: Hub } }
) => {
if (error instanceof Error) {
await captureRemixServerException(sentry, error, "remix.server", request);
} else {
sentry.captureException(error);
}
};
import { getAssetFromKV } from "@cloudflare/kv-asset-handler";
import { type AssetManifestType } from "@cloudflare/kv-asset-handler/dist/types";
import { type AppLoadContext, type ServerBuild } from "@remix-run/cloudflare";
import { createRequestHandler } from "@remix-run/cloudflare";
import { createRoutes } from "@remix-run/server-runtime/dist/routes";
import { addTracingExtensions, hasTracingEnabled } from "@sentry/core";
import { type Hub } from "@sentry/types";
import { getTransactionDetails } from "./getTransactionDetails";
import { instrumentBuild } from "./instrumentBuild";
import { startRequestHandlerTransaction } from "./startRequestHandlerTransaction";
declare const process: {
env: {
NODE_ENV?: string;
};
};
export const handleFetch = async <
Environment extends { __STATIC_CONTENT: KVNamespace } = {
__STATIC_CONTENT: KVNamespace;
}
>(
hub: Hub,
getLoadContext: (environment: Environment) => AppLoadContext | Promise<AppLoadContext>,
MANIFEST: AssetManifestType,
build: ServerBuild,
request: Request,
environment: Environment,
context: ExecutionContext
) => {
// This mutates the global object to save tracing methods (ex. `startTransaction`). However, the methods are pure functions (they take the current Hub instance as a parameter) and so having them overwrite if global state is accidentally shared is possibly okay.
addTracingExtensions();
try {
const url = new URL(request.url);
const ttl = url.pathname.startsWith("/build/")
? 60 * 60 * 24 * 365 // 1 year
: 60 * 5; // 5 minutes
return await getAssetFromKV(
{
request,
waitUntil: context.waitUntil.bind(context),
} as FetchEvent,
{
ASSET_NAMESPACE: environment.__STATIC_CONTENT,
ASSET_MANIFEST: MANIFEST as AssetManifestType,
cacheControl: {
browserTTL: ttl,
edgeTTL: ttl,
},
}
);
} catch {
// If the asset doesn't exist, fall through to the Remix handler.
}
try {
// Wrap each Remix loader and action in a Sentry span
const instrumentedBuild = instrumentBuild(hub, build);
const handleRemixRequest = createRequestHandler(instrumentedBuild, process.env.NODE_ENV);
const loadContext = { ...(await getLoadContext(environment)), sentry: hub };
// Generate the root transaction for this request
// Adapted from [the Remix/Express adapter](https://github.com/getsentry/sentry-javascript/blob/7f4c4ec10b97be945dab0dca1d47adb9a9954af3/packages/remix/src/utils/serverAdapters/express.ts)
const routes = createRoutes(instrumentedBuild.routes);
const options = hub.getClient()?.getOptions();
const url = new URL(request.url);
const { name, source, params } = getTransactionDetails(routes, url);
// Even if tracing is disabled, we still want to set the route name
hub.getScope().setSDKProcessingMetadata({
request,
route: {
path: name,
},
});
if (!options || !hasTracingEnabled(options)) {
return await handleRemixRequest(request, loadContext);
}
// Start the transaction, linking to an ongoing trace if necessary
const transaction = startRequestHandlerTransaction(hub, request, name, source, params);
const response = await handleRemixRequest(request, loadContext);
transaction.setHttpStatus(response.status);
transaction.finish();
return response;
} catch (error: unknown) {
hub.captureException(error);
return new Response("Internal Server Error", { status: 500 });
}
};
import __STATIC_CONTENT_MANIFEST from "__STATIC_CONTENT_MANIFEST";
import { type AssetManifestType } from "@cloudflare/kv-asset-handler/dist/types";
import { logDevReady } from "@remix-run/cloudflare";
import * as build from "@remix-run/dev/server-build";
import { Dedupe, ExtraErrorData, Toucan } from "toucan-js";
import { handleFetch } from "./handleFetch";
const MANIFEST = JSON.parse(__STATIC_CONTENT_MANIFEST) as AssetManifestType;
interface Environment {
__STATIC_CONTENT: KVNamespace<string>;
SENTRY_ENABLED: boolean;
SENTRY_DSN: string;
SENTRY_ENVIRONMENT: string;
SENTRY_VERSION: string | undefined;
}
// Let the Remix dev server know we've finished a new build and loaded the fetch handler.
if (process.env.NODE_ENV === "development") {
logDevReady(build);
}
const index: ExportedHandler<Environment> = {
fetch: async (request, environment, context) => {
const sentry = new Toucan({
enabled: environment.SENTRY_ENABLED,
dsn: environment.SENTRY_DSN,
environment: environment.SENTRY_ENVIRONMENT,
release: environment.SENTRY_VERSION,
dist: "server",
tracesSampleRate: environment.SENTRY_ENVIRONMENT === "development" ? 1 : 0.01,
integrations: [new Dedupe(), new ExtraErrorData()],
requestDataOptions: {
allowedIps: true,
allowedSearchParams: true,
},
request,
context,
});
sentry.setTags({
runtime: "Cloudflare Workers",
});
return await handleFetch(
sentry,
(loadEnvironment) => ({
SENTRY_ENABLED: loadEnvironment.SENTRY_ENABLED,
SENTRY_DSN: loadEnvironment.SENTRY_DSN,
SENTRY_ENVIRONMENT: loadEnvironment.SENTRY_ENVIRONMENT,
SENTRY_VERSION: loadEnvironment.SENTRY_VERSION,
}),
MANIFEST,
build,
request,
environment,
context
);
},
};
export default index;
/**
* @file Instrument the Remix build with Sentry. Adapted from [`instrumentServer`](https://github.com/getsentry/sentry-javascript/blob/b290fcae0466ecd8026c40b14d87473c130e9207/packages/remix/src/utils/instrumentServer.ts).
*/
import {
type AppLoadContext,
type EntryContext,
type HandleDocumentRequestFunction,
type ServerBuild,
} from "@remix-run/cloudflare";
import { type AppData, isCatchResponse } from "@remix-run/react/dist/data";
import { isRedirectResponse, isResponse, json } from "@remix-run/server-runtime/dist/responses";
import { type ActionFunction, type LoaderArgs, type LoaderFunction } from "@remix-run/server-runtime/dist/routeModules";
import { type Hub, type WrappedFunction } from "@sentry/types";
import { dynamicSamplingContextToSentryBaggageHeader, fill } from "@sentry/utils";
// Adapted from [Sentry](https://github.com/getsentry/sentry-javascript/blob/540adac9ec81803f86a3a7f5b34ebbc1ad2a8d23/packages/remix/src/utils/instrumentServer.ts#L120-L170)
const makeWrappedDocumentRequestFunction =
(hub: Hub) =>
(origDocumentRequestFunction: HandleDocumentRequestFunction): HandleDocumentRequestFunction => {
return async function (
this: unknown,
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
context: EntryContext,
loadContext: AppLoadContext
): Promise<Response> {
const activeTransaction = hub.getScope().getTransaction();
const span = activeTransaction?.startChild({
op: "function.remix.document_request",
description: activeTransaction.name,
tags: {
method: request.method,
url: request.url,
},
});
const response = await origDocumentRequestFunction.call(
// eslint-disable-next-line @babel/no-invalid-this -- We need to be able to pass `this` to the original function to wrap it correctly.
this,
request,
responseStatusCode,
responseHeaders,
context,
loadContext
);
span?.finish();
return response;
};
};
// Allow slightly differently typed data functions for loaders & actions, without having to rewrite the code.
interface MakeWrappedDataFunction {
(hub: Hub, id: string, dataFunctionType: "action", originalFunction: ActionFunction): ActionFunction;
(hub: Hub, id: string, dataFunctionType: "loader", originalFunction: LoaderFunction): LoaderFunction;
}
// Adapted from [Sentry](https://github.com/getsentry/sentry-javascript/blob/540adac9ec81803f86a3a7f5b34ebbc1ad2a8d23/packages/remix/src/utils/instrumentServer.ts#L172-L210)
const makeWrappedDataFunction: MakeWrappedDataFunction = (hub: Hub, id: string, dataFunctionType, originalFunction) => {
return async function (this: unknown, args: Parameters<typeof originalFunction>[0]): Promise<Response | AppData> {
const activeTransaction = hub.getScope().getTransaction();
const span = activeTransaction?.startChild({
op: `function.remix.${dataFunctionType}`,
description: id,
tags: {
name: dataFunctionType,
},
});
if (span) {
// Assign data function to hub to be able to see `db` transactions (if any) as children.
hub.getScope().setSpan(span);
}
// eslint-disable-next-line @babel/no-invalid-this -- We need to be able to pass `this` to the original function to wrap it correctly.
const response = (await originalFunction.call(this, args)) as unknown;
hub.getScope().setSpan(activeTransaction);
span?.finish();
return response;
};
};
const makeWrappedAction =
(hub: Hub, id: string) =>
(origAction: ActionFunction): ActionFunction => {
return makeWrappedDataFunction(hub, id, "action", origAction);
};
const makeWrappedLoader =
(hub: Hub, id: string) =>
(origLoader: LoaderFunction): LoaderFunction => {
return makeWrappedDataFunction(hub, id, "loader", origLoader);
};
// Adapted from [Sentry](https://github.com/getsentry/sentry-javascript/blob/540adac9ec81803f86a3a7f5b34ebbc1ad2a8d23/packages/remix/src/utils/instrumentServer.ts#L246-L283)
const makeWrappedRootLoader =
(hub: Hub) =>
(origLoader: LoaderFunction): LoaderFunction => {
return async function (this: unknown, args: LoaderArgs): Promise<Response | AppData> {
// eslint-disable-next-line @babel/no-invalid-this -- We need to be able to pass `this` to the original function to wrap it correctly.
const response: object = (await origLoader.call(this, args)) as unknown as object;
const scope = hub.getScope();
const sentryHeaders = {
"sentry-trace": scope.getSpan()?.toTraceparent(),
baggage: dynamicSamplingContextToSentryBaggageHeader(scope.getTransaction()?.getDynamicSamplingContext()),
};
// Note: `redirect` and `catch` responses do not have bodies to extract
if (isResponse(response) && !isRedirectResponse(response) && !isCatchResponse(response)) {
// The original Sentry implementation `.clone()`s the response body in order to check if it's an object, which is really wasteful. Since I know my root loaders are going to return objects, I can just assert it and save time.
return json(
{
...(await response.json<{ [key: string]: unknown }>()),
sentryHeaders,
},
{ headers: response.headers, statusText: response.statusText, status: response.status }
);
}
return { ...response, sentryHeaders };
};
};
/**
* Instruments `remix` ServerBuild for performance tracing.
*
* Adapted from [Sentry](https://github.com/getsentry/sentry-javascript/blob/540adac9ec81803f86a3a7f5b34ebbc1ad2a8d23/packages/remix/src/utils/instrumentServer.ts#L409-L451).
*/
export const instrumentBuild = (hub: Hub, build: ServerBuild): ServerBuild => {
const routes: ServerBuild["routes"] = {};
const wrappedEntry = { ...build.entry, module: { ...build.entry.module } };
// Not keeping boolean flags like it's done for `requestHandler` functions,
// Because the build can change between build and runtime.
// So if there is a new `loader` or`action` or `documentRequest` after build.
// We should be able to wrap them, as they may not be wrapped before.
if (!(wrappedEntry.module.default as WrappedFunction).__sentry_original__) {
fill(wrappedEntry.module, "default", makeWrappedDocumentRequestFunction(hub));
}
for (const [id, route] of Object.entries(build.routes)) {
const wrappedRoute = { ...route, module: { ...route.module } };
if (wrappedRoute.module.action && !(wrappedRoute.module.action as WrappedFunction).__sentry_original__) {
fill(wrappedRoute.module, "action", makeWrappedAction(hub, id));
}
if (wrappedRoute.module.loader && !(wrappedRoute.module.loader as WrappedFunction).__sentry_original__) {
fill(wrappedRoute.module, "loader", makeWrappedLoader(hub, id));
}
// Entry module should have a loader function to provide `sentry-trace` and `baggage`
// They will be available for the root `meta` function as `data.sentry.trace` and `data.sentry.baggage`
if (wrappedRoute.parentId === undefined) {
if (!wrappedRoute.module.loader) {
wrappedRoute.module.loader = () => ({});
}
// We want to wrap the root loader regardless of whether it's already wrapped before.
fill(wrappedRoute.module, "loader", makeWrappedRootLoader(hub));
}
routes[id] = wrappedRoute;
}
return { ...build, routes, entry: wrappedEntry };
};
import { json, type LoaderFunction, type V2_MetaFunction } from "@remix-run/cloudflare";
import {
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
isRouteErrorResponse,
useCatch,
useLoaderData,
useLocation,
useMatches,
useRouteError,
} from "@remix-run/react";
import { type V2_ErrorBoundaryComponent } from "@remix-run/react/dist/routeModules";
import { ExtraErrorData } from "@sentry/integrations";
import { setUser, withProfiler } from "@sentry/react";
import { BrowserTracing, init as initSentry, remixRouterInstrumentation, withSentry } from "@sentry/remix";
import { type ReactNode, useEffect } from "react";
import { captureRemixErrorBoundaryError } from "./captureRemixErrorBoundaryError";
/**
* Remix meta function for the HTML `<meta>` tags.
*
* Note that `sentryHeaders` should be added in {@link instrumentBuild}. They're then read by `BrowserTracing`.
*
* @returns The meta tags for the app.
*/
export const meta = (({ data }: { data?: { sentryHeaders?: { "sentry-trace"?: string; baggage?: string } } }) => [
{
"sentry-trace": data?.sentryHeaders?.["sentry-trace"],
},
{
baggage: data?.sentryHeaders?.baggage,
},
]) satisfies V2_MetaFunction;
/**
* Get top-level configuration from the server to pass to the client.
*
* Be extremely careful not to expose any secrets here. These won't be included in the browser bundle but clients will be able to access them via the JavaScript context.
*
* @returns Configuration object.
*/
export const loader = (async ({
request,
context: { SENTRY_ENABLED, SENTRY_DSN, SENTRY_ENVIRONMENT, SENTRY_VERSION },
}) => {
return json({
SENTRY_ENABLED,
SENTRY_DSN,
SENTRY_ENVIRONMENT,
SENTRY_VERSION,
});
}) satisfies LoaderFunction;
const Page = ({ children }: { children: ReactNode }) => {
return (
<html lang="en">
<head>
<Meta />
<Links />
</head>
<body style={{ margin: 0 }}>
{children}
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);
};
export const ErrorBoundary = (() => {
const error = useRouteError();
captureRemixErrorBoundaryError(error);
const errorMessage = isRouteErrorResponse(error)
? `${error.status} ${error.statusText}`
: "Sorry, something went wrong.";
return (
<Page>
<div>
<h3>
{errorMessage}
</h3>
</Page>
);
}) satisfies V2_ErrorBoundaryComponent;
/**
* The root component for the app.
*
* This should only handle high-level wrappers & other tools, not business logic. Put that into a route.
*
* @returns JSX to render the root component.
*/
const Root = () => {
const { SENTRY_ENABLED, SENTRY_DSN, SENTRY_ENVIRONMENT, SENTRY_VERSION } =
useLoaderData<typeof loader>();
const location = useLocation();
useEffect(() => {
if (SENTRY_ENABLED) {
initSentry({
dsn: SENTRY_DSN,
environment: SENTRY_ENVIRONMENT,
release: SENTRY_VERSION,
tunnel: "/errors",
tracesSampleRate: SENTRY_ENVIRONMENT === "development" ? 1 : 0.01,
integrations: [
new BrowserTracing({
routingInstrumentation: remixRouterInstrumentation(useEffect, useLocation, useMatches),
// Remix makes fetch requests with the full domain rather than `/`, which means we have to allowlist using the actual domain.
tracePropagationTargets: [new RegExp(`^https?://${new URL(window.location.href).host}`, "u")],
}),
// Log non-native parts of the error message
new ExtraErrorData(),
],
});
}
}, [SENTRY_ENABLED, SENTRY_DSN, SENTRY_ENVIRONMENT, SENTRY_VERSION]);
return (
<Page>
<Outlet />
</Page>
);
};
export default withSentry(withProfiler(Root), { wrapWithErrorBoundary: false });
import { type Hub } from "@sentry/types";
import { createContext } from "react";
export const SentrySSRContext = createContext<Hub | undefined>(undefined);
import { type Hub, type TransactionSource } from "@sentry/types";
import { tracingContextFromHeaders } from "@sentry/utils";
/**
* Starts a new transaction for the given request to be used by different `RequestHandler` wrappers.
*
* Adapted from [Sentry](https://github.com/getsentry/sentry-javascript/blob/540adac9ec81803f86a3a7f5b34ebbc1ad2a8d23/packages/remix/src/utils/instrumentServer.ts#L300-L340)
*/
export const startRequestHandlerTransaction = (
hub: Hub,
request: Request,
name: string,
source: TransactionSource,
data?: { [key: string]: string | undefined }
) => {
const { traceparentData, dynamicSamplingContext, propagationContext } = tracingContextFromHeaders(
request.headers.get("sentry-trace") ?? undefined,
request.headers.get("baggage")
);
hub.getScope().setPropagationContext(propagationContext);
const transaction = hub.startTransaction({
name,
op: "http.server",
tags: {
method: request.method,
},
data,
...traceparentData,
metadata: {
source,
dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext,
},
});
hub.getScope().setSpan(transaction);
return transaction;
};
@aaronadamsCA
Copy link

Do you actually have a custom createEventHandler or is it safe to assume you're still just importing it from the official Remix adapter?

I'm just trying to figure out the complexity to implement the same thing with Cloudflare Pages and that's the one part here I don't understand so far.

@huw
Copy link
Author

huw commented Feb 27, 2023

I have a custom createEventHandler for use with ES Modules. If you’re on CJS or (I think) Pages you should be fine.

See more on that here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment