Skip to content

docs(react-router): Add docs for distributed tracing #13467

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 23, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 127 additions & 57 deletions docs/platforms/javascript/guides/react-router/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -83,42 +83,42 @@ npx react-router reveal

Initialize the Sentry React SDK in your `entry.client.tsx` file:

```tsx {filename: entry.client.tsx}
import * as Sentry from "@sentry/react-router";
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import { HydratedRouter } from "react-router/dom";

Sentry.init({
dsn: "___PUBLIC_DSN___",

// Adds request headers and IP for users, for more info visit:
// https://docs.sentry.io/platforms/javascript/guides/react-router/configuration/options/#sendDefaultPii
sendDefaultPii: true,

integrations: [
// ___PRODUCT_OPTION_START___ performance
Sentry.browserTracingIntegration(),
// ___PRODUCT_OPTION_END___ performance
// ___PRODUCT_OPTION_START___ session-replay
Sentry.replayIntegration(),
// ___PRODUCT_OPTION_END___ session-replay
],
// ___PRODUCT_OPTION_START___ performance

tracesSampleRate: 1.0, // Capture 100% of the transactions

// Set `tracePropagationTargets` to declare which URL(s) should have trace propagation enabled
tracePropagationTargets: [/^\//, /^https:\/\/yourserver\.io\/api/],
// ___PRODUCT_OPTION_END___ performance
// ___PRODUCT_OPTION_START___ session-replay

// Capture Replay for 10% of all sessions,
// plus 100% of sessions with an error
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
// ___PRODUCT_OPTION_END___ session-replay
});
```tsx {diff} {filename: entry.client.tsx}
+import * as Sentry from "@sentry/react-router";
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import { HydratedRouter } from "react-router/dom";

+Sentry.init({
+ dsn: "___PUBLIC_DSN___",
+
+ // Adds request headers and IP for users, for more info visit:
+ // https://docs.sentry.io/platforms/javascript/guides/react-router/configuration/options/#sendDefaultPii
+ sendDefaultPii: true,
+
+ integrations: [
+ // ___PRODUCT_OPTION_START___ performance
+ Sentry.browserTracingIntegration(),
+ // ___PRODUCT_OPTION_END___ performance
+ // ___PRODUCT_OPTION_START___ session-replay
+ Sentry.replayIntegration(),
+ // ___PRODUCT_OPTION_END___ session-replay
+ ],
+ // ___PRODUCT_OPTION_START___ performance
+
+ tracesSampleRate: 1.0, // Capture 100% of the transactions
+
+ // Set `tracePropagationTargets` to declare which URL(s) should have trace propagation enabled
+ tracePropagationTargets: [/^\//, /^https:\/\/yourserver\.io\/api/],
+ // ___PRODUCT_OPTION_END___ performance
+ // ___PRODUCT_OPTION_START___ session-replay
+
+ // Capture Replay for 10% of all sessions,
+ // plus 100% of sessions with an error
+ replaysSessionSampleRate: 0.1,
+ replaysOnErrorSampleRate: 1.0,
+ // ___PRODUCT_OPTION_END___ session-replay
+});

startTransition(() => {
hydrateRoot(
Expand All @@ -133,7 +133,7 @@ startTransition(() => {
Now, update your `app/root.tsx` file to report any unhandled errors from your error boundary:

```tsx {diff} {filename: app/root.tsx}
import * as Sentry from "@sentry/react-router";
+import * as Sentry from "@sentry/react-router";

export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
let message = "Oops!";
Expand Down Expand Up @@ -199,40 +199,110 @@ Sentry.init({
});
```

In your `entry.server.tsx` file, export the `handleError` function:
Update your `entry.server.tsx` file:

```tsx {diff} {filename: entry.server.tsx}
import * as Sentry from "@sentry/react-router";
import { type HandleErrorFunction } from "react-router";
+import * as Sentry from '@sentry/react-router';
import { createReadableStreamFromReadable } from '@react-router/node';
import { renderToPipeableStream } from 'react-dom/server';
import { ServerRouter } from 'react-router';
import { type HandleErrorFunction } from 'react-router';

+const handleRequest = Sentry.createSentryHandleRequest({
+ ServerRouter,
+ renderToPipeableStream,
+ createReadableStreamFromReadable,
+});

export default handleRequest;

export const handleError: HandleErrorFunction = (error, { request }) => {
// React Router may abort some interrupted requests, report those
// React Router may abort some interrupted requests, don't log those
if (!request.signal.aborted) {
+ Sentry.captureException(error);

// make sure to still log the error so you can see it
// optionally log the error so you can see it
console.error(error);
}
};

- export default function handleRequest(
+ function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
routerContext: EntryContext,
loadContext: AppLoadContext,
) {
return new Promise((resolve, reject) => {
// ...
}
}

+ export default Sentry.sentryHandleRequest(handleRequest);

// ... rest of your server entry
```

<Expandable title="Do you need to customize your handleRequest function?">
If you need to update the logic of your `handleRequest` function you'll need to include the provided Sentry helper functions (`getMetaTagTransformer` and `wrapSentryHandleRequest`) manually:
Comment on lines +232 to +233
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do I understand this correctly that this more manual approach is necessary when users have their own handleRequest function already? If so, I think we should point this out more clearly because I didn't fully understand the "If you need to update the logic of your handleRequest function" part. (Maybe that's just me though, so feel free to disregard)

Suggested change
<Expandable title="Do you need to customize your handleRequest function?">
If you need to update the logic of your `handleRequest` function you'll need to include the provided Sentry helper functions (`getMetaTagTransformer` and `wrapSentryHandleRequest`) manually:
<Expandable title="Do you have your own handleRequest function?">
If already defined your own `handleRequest` function you'll need to include the provided Sentry helper functions (`getMetaTagTransformer` and `wrapSentryHandleRequest`) manually:

(feel free to go with different wording as you see fit

Copy link
Member Author

@chargome chargome Apr 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah that is correct, BUT whenever a user creates this hook, the handleRequest function is already defined – so I only want to encourage this path when they need some special logic in their function .. not sure how to name this though

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I don't fully understand this yet but I guess what you mean is that when you run react-router reveal there already is a predefined handleRequest and this is the one users don't need to add Sentry stuff to?
So it's just for the case that users have a custom behaviour in the function already prior to adding Sentry?

I don't think this is a blocker btw, so if you wanna merge this today, feel free. Maybe we have a minute tomorrow to chat about it this quickly in person

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah let's talk talk about this tomorrow 👍


```tsx {1-4, 44-45, 69-70}
import { getMetaTagTransformer, wrapSentryHandleRequest } from '@sentry/react-router';
// ... other imports

const handleRequest = function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
routerContext: EntryContext,
_loadContext: AppLoadContext,
): Promise<Response> {
return new Promise((resolve, reject) => {
let shellRendered = false;
const userAgent = request.headers.get('user-agent');

// Determine if we should use onAllReady or onShellReady
const isBot = typeof userAgent === 'string' && botRegex.test(userAgent);
const isSpaMode = !!(routerContext as { isSpaMode?: boolean }).isSpaMode;

const readyOption = isBot || isSpaMode ? 'onAllReady' : 'onShellReady';

const { pipe, abort } = renderToPipeableStream(
<ServerRouter context={routerContext} url={request.url} />,
{
[readyOption]() {
shellRendered = true;
const body = new PassThrough();

const stream = createReadableStreamFromReadable(body);

responseHeaders.set('Content-Type', 'text/html');

resolve(
new Response(stream, {
headers: responseHeaders,
status: responseStatusCode,
}),
);

// this enables distributed tracing between client and server
pipe(getMetaTagTransformer(body));
},
onShellError(error: unknown) {
reject(error);
},
onError(error: unknown) {
// eslint-disable-next-line no-param-reassign
responseStatusCode = 500;
// Log streaming rendering errors from inside the shell. Don't log
// errors encountered during initial shell rendering since they'll
// reject and get logged in handleDocumentRequest.
if (shellRendered) {
// eslint-disable-next-line no-console
console.error(error);
}
},
},
);

// Abort the rendering stream after the `streamTimeout`
setTimeout(abort, streamTimeout);
});
};

// wrap the default export
export default wrapSentryHandleRequest(handleRequest);

// ... rest of your entry.server.ts file
```
</Expandable>

### Update Scripts

Since React Router is running in ESM mode, you need to use the `--import` command line options to load our server-side instrumentation module before the application starts.
Expand Down