Skip to content

Commit 31fd072

Browse files
fix(remix): Provide sentry-trace and baggage via root loader. (#5509)
We are currently wrapping [`meta` functions](https://remix.run/docs/en/v1/api/conventions#meta) to create `sentry-trace` and `baggage` `<meta>` tags in the server-side rendered HTML. It all seems convenient to use that facility (not bothering users to configure anything through the whole process), but turns out it's breaking the hydration. While on React 17, it's just a warning (and `<meta>` tags are still available at the end). On React 18, hydration logic fails and falls back to client-side rendering. The problem is that the HTML template for hydration is generated on build time, and uses the `meta` functions before we wrap them. And when we finally wrap it on initial runtime (we are wrapping `createRequestHandler`), the updated template doesn't match. But we also have `loader` functions available for us that can pass data from server to client-side, and their return values are also available in `meta` functions. Furthermore, using a `loader` data in `meta` seems to spare it from hydration and let it add `<meta>` tags whenever the data is available (which is `handleDocumentRequest` in this case, so just before the start of our `pageload` transaction). Also, it turns out we don't need to add `<meta>` tags to every sub-route. If we add them to the `root` route, they will be available on the sub-routes. So, this PR removes the monkey patching logic for `meta` functions. Instead, introduces a special `loader` wrapper for `root`. This will require the users to set `sentry-trace` and `baggage` in `meta` functions in `root.tsx`. It will look like: ```typescript // root.tsx export const meta: MetaFunction = ({data}) => { return { charset: "utf-8", title: "New Remix App", viewport: "width=device-width,initial-scale=1", 'sentry-trace': data.sentryTrace, baggage: data.sentryBaggage, }; }; ``` Co-authored-by: Abhijeet Prasad <[email protected]>
1 parent 9b7131f commit 31fd072

File tree

3 files changed

+68
-24
lines changed

3 files changed

+68
-24
lines changed

packages/remix/src/utils/instrumentServer.ts

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable max-lines */
22
import { captureException, getCurrentHub } from '@sentry/node';
3-
import { getActiveTransaction } from '@sentry/tracing';
4-
import { addExceptionMechanism, fill, loadModule, logger, serializeBaggage } from '@sentry/utils';
3+
import { getActiveTransaction, hasTracingEnabled } from '@sentry/tracing';
4+
import { addExceptionMechanism, fill, isNodeEnv, loadModule, logger, serializeBaggage } from '@sentry/utils';
55

66
// Types vendored from @remix-run/[email protected]:
77
// https://github.com/remix-run/remix/blob/f3691d51027b93caa3fd2cdfe146d7b62a6eb8f2/packages/remix-server-runtime/server.ts
@@ -72,6 +72,8 @@ interface HandleDataRequestFunction {
7272

7373
interface ServerEntryModule {
7474
default: HandleDocumentRequestFunction;
75+
meta: MetaFunction;
76+
loader: DataFunction;
7577
handleDataRequest?: HandleDataRequestFunction;
7678
}
7779

@@ -237,33 +239,31 @@ function makeWrappedLoader(origAction: DataFunction): DataFunction {
237239
return makeWrappedDataFunction(origAction, 'loader');
238240
}
239241

240-
function makeWrappedMeta(origMeta: MetaFunction | HtmlMetaDescriptor = {}): MetaFunction {
241-
return function (
242-
this: unknown,
243-
args: { data: AppData; parentsData: RouteData; params: Params; location: Location },
244-
): HtmlMetaDescriptor {
245-
let origMetaResult;
246-
if (origMeta instanceof Function) {
247-
origMetaResult = origMeta.call(this, args);
248-
} else {
249-
origMetaResult = origMeta;
250-
}
242+
function getTraceAndBaggage(): { sentryTrace?: string; sentryBaggage?: string } {
243+
const transaction = getActiveTransaction();
244+
const currentScope = getCurrentHub().getScope();
251245

252-
const scope = getCurrentHub().getScope();
253-
if (scope) {
254-
const span = scope.getSpan();
255-
const transaction = getActiveTransaction();
246+
if (isNodeEnv() && hasTracingEnabled()) {
247+
if (currentScope) {
248+
const span = currentScope.getSpan();
256249

257250
if (span && transaction) {
258251
return {
259-
...origMetaResult,
260-
'sentry-trace': span.toTraceparent(),
261-
baggage: serializeBaggage(transaction.getBaggage()),
252+
sentryTrace: span.toTraceparent(),
253+
sentryBaggage: serializeBaggage(transaction.getBaggage()),
262254
};
263255
}
264256
}
257+
}
265258

266-
return origMetaResult;
259+
return {};
260+
}
261+
262+
function makeWrappedRootLoader(origLoader: DataFunction): DataFunction {
263+
return async function (this: unknown, args: DataFunctionArgs): Promise<Response | AppData> {
264+
const res = await origLoader.call(this, args);
265+
266+
return { ...res, ...getTraceAndBaggage() };
267267
};
268268
}
269269

@@ -378,8 +378,6 @@ function makeWrappedCreateRequestHandler(
378378
for (const [id, route] of Object.entries(build.routes)) {
379379
const wrappedRoute = { ...route, module: { ...route.module } };
380380

381-
fill(wrappedRoute.module, 'meta', makeWrappedMeta);
382-
383381
if (wrappedRoute.module.action) {
384382
fill(wrappedRoute.module, 'action', makeWrappedAction);
385383
}
@@ -388,6 +386,16 @@ function makeWrappedCreateRequestHandler(
388386
fill(wrappedRoute.module, 'loader', makeWrappedLoader);
389387
}
390388

389+
// Entry module should have a loader function to provide `sentry-trace` and `baggage`
390+
// They will be available for the root `meta` function as `data.sentryTrace` and `data.sentryBaggage`
391+
if (!wrappedRoute.parentId) {
392+
if (!wrappedRoute.module.loader) {
393+
wrappedRoute.module.loader = () => ({});
394+
}
395+
396+
fill(wrappedRoute.module, 'loader', makeWrappedRootLoader);
397+
}
398+
391399
routes[id] = wrappedRoute;
392400
}
393401

packages/remix/test/integration/app/root.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ import type { MetaFunction } from '@remix-run/node';
22
import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration } from '@remix-run/react';
33
import { withSentry } from '@sentry/remix';
44

5-
export const meta: MetaFunction = () => ({
5+
export const meta: MetaFunction = ({ data }) => ({
66
charset: 'utf-8',
77
title: 'New Remix App',
88
viewport: 'width=device-width,initial-scale=1',
9+
'sentry-trace': data.sentryTrace,
10+
baggage: data.sentryBaggage,
911
});
1012

1113
function App() {

packages/remix/test/integration/test/client/meta-tags.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { test, expect } from '@playwright/test';
2+
import { getFirstSentryEnvelopeRequest } from './utils/helpers';
3+
import { Event } from '@sentry/types';
24

35
test('should inject `sentry-trace` and `baggage` meta tags inside the root page.', async ({ page }) => {
46
await page.goto('/');
@@ -27,3 +29,35 @@ test('should inject `sentry-trace` and `baggage` meta tags inside a parameterize
2729

2830
expect(sentryBaggageContent).toEqual(expect.any(String));
2931
});
32+
33+
test('should send transactions with corresponding `sentry-trace` and `baggage` inside root page', async ({ page }) => {
34+
const envelope = await getFirstSentryEnvelopeRequest<Event>(page, '/');
35+
36+
const sentryTraceTag = await page.$('meta[name="sentry-trace"]');
37+
const sentryTraceContent = await sentryTraceTag?.getAttribute('content');
38+
const sentryBaggageTag = await page.$('meta[name="baggage"]');
39+
const sentryBaggageContent = await sentryBaggageTag?.getAttribute('content');
40+
41+
expect(sentryTraceContent).toContain(
42+
`${envelope.contexts?.trace.trace_id}-${envelope.contexts?.trace.parent_span_id}-`,
43+
);
44+
45+
expect(sentryBaggageContent).toContain(envelope.contexts?.trace.trace_id);
46+
});
47+
48+
test('should send transactions with corresponding `sentry-trace` and `baggage` inside a parameterized route', async ({
49+
page,
50+
}) => {
51+
const envelope = await getFirstSentryEnvelopeRequest<Event>(page, '/loader-json-response/0');
52+
53+
const sentryTraceTag = await page.$('meta[name="sentry-trace"]');
54+
const sentryTraceContent = await sentryTraceTag?.getAttribute('content');
55+
const sentryBaggageTag = await page.$('meta[name="baggage"]');
56+
const sentryBaggageContent = await sentryBaggageTag?.getAttribute('content');
57+
58+
expect(sentryTraceContent).toContain(
59+
`${envelope.contexts?.trace.trace_id}-${envelope.contexts?.trace.parent_span_id}-`,
60+
);
61+
62+
expect(sentryBaggageContent).toContain(envelope.contexts?.trace.trace_id);
63+
});

0 commit comments

Comments
 (0)