Remix Issues
useMatches
only exposes the filepath, but we want a parameterized route:
8:53:24 AM web.1 | {
8:53:24 AM web.1 | id: 'routes/bottles.$bottleId._index',
8:53:24 AM web.1 | pathname: '/bottles/4/',
8:53:24 AM web.1 | params: { bottleId: '4' },
8:53:24 AM web.1 | data: { dehydratedState: { mutations: [], queries: [Array] } },
8:53:24 AM web.1 | handle: undefined
8:53:24 AM web.1 | }
We want to effectively create the transaction description of /bottles/:$bottleId
in this scenario. In Django when we first did this we simply implemented a regexp parser. We could do something similarly dumb simple here if Remix does not (and cannot) expose its route matcher.
Note: This is critical to Remix working, and is glossed over in our onboarding.
There are two methods for injection in Remix via the meta tag (v1 and v2):
// V1
export function meta({ data }) {
return {
"trace-parent": data?.sentryTrace
baggage: data?.sentryBaggage
};
}
// V2
export default withSentry(function App() {
return (
<html lang="en" className="h-full">
<head>
<Meta />
{data?.sentryTrace && (
<meta name="sentry-trace" content={data.sentryTrace} />
)}
{data?.sentryBaggage && (
<meta name="baggage" content={data.sentryBaggage} />
)}
<Links />
</head>
</html>
)
});
Its possible V2 could be the mechanism for V1 as well, but realistically we'd prefer Remix provide an injection point that lets us insert into <Meta/>
.
Note: You cannot use the V2 meta function to inject tracing headers, as meta gets overwritten by child routes in V2.
There are too many variations and caveats to SDK initialization. For example, in the default Remix scenario we suggest you use entry.server.ts
, which happens after the runtime has initialized. While this might be ok, as the SDK gets initialized on import, there are other mechanisms where you might need to initialize the SDK earlier (and reference it).
For example, Remix's best method for managing sessions and other shared context within an Express-based runtime is via loader context (getLoaderContext
). That happens outside of the scope, and is also an ideal place to bind shared context. While a middleware-based approach might solve for this down the road (ore a preLoader
global hook), this is key for things like session information as loaders are executed in parallel (thus the root loader cant be a control point):
// An express middleware to bind various request state, wihch can then be referenced in `getLoaderContext`
app.all("*", async (req, res, next) => {
const session = await getSession(req);
const user = await getUser(session);
const accessToken = await getAccessToken(session);
Sentry.setUser({
id: `${user?.id}`,
username: user?.username,
email: user?.email,
});
req.user = user || null;
req.accessToken = accessToken || null;
req.api = new ApiClient({
server: config.API_SERVER,
accessToken,
});
if (accessToken && !user) {
return logout(req);
}
next();
});
We then bind that context:
app.all(
"*",
MODE === "production"
? createSentryRequestHandler({ build: require(BUILD_DIR), getLoadContext })
: (...args) => {
purgeRequireCache();
const requestHandler = createSentryRequestHandler({
build: require(BUILD_DIR),
mode: MODE,
getLoadContext,
});
return requestHandler(...args);
},
);
Lastly, we would need to re-bind this context in our root component:
export async function loader({ context }) {
return json({
accessToken: context.accessToken,
user: context.user,
});
}
export default withSentry(function App() {
const { accessToken, user } = useLoaderData<LoaderData>();
Sentry.setUser(user ? {
id: `${user?.id}`,
username: user?.username,
email: user?.email,
} : null);
});
This is a basic instrumentation of a fully hydrated page load. You'll find there's a sort-of hidden child here (with the (+)
button):
Upon inspection, you'll see we effectively have an orphaned child. If this data were corerct, you'd expect a span generated on the server, wihch then expands into the client hydration. That span would also reflect the total duration of the client hydration. You would also expect the transaction here (the parent transaction - the server hydration) to reflect that total duration in some fashion.
Loaders are not capturing all relevant spans, and its currently unclear why. In this case we're calling out to an API endpoint in the loader, and that endpoint is using fetch
(in Remix) to query another API service:
export async function loader({ params, context }: LoaderArgs) {
invariant(params.bottleId);
const bottle: BottleWithStats = await context.api.get(
`/bottles/${params.bottleId}`,
);
return json({ bottle });
}
In the same example above (the missing spans), traceparent is not present on any of the outgoing fetch
calls, meaning the server never connects a trace.