Skip to content

Commit effb8bd

Browse files
committed
feat(react-router): Add support for React Router instrumentation API
1 parent 63daea2 commit effb8bd

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+3168
-41
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
8+
# testing
9+
/coverage
10+
11+
# production
12+
/build
13+
14+
# misc
15+
.DS_Store
16+
.env.local
17+
.env.development.local
18+
.env.test.local
19+
.env.production.local
20+
21+
npm-debug.log*
22+
yarn-debug.log*
23+
yarn-error.log*
24+
25+
/test-results/
26+
/playwright-report/
27+
/playwright/.cache/
28+
29+
!*.d.ts
30+
31+
# react router
32+
.react-router
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@sentry:registry=http://127.0.0.1:4873
2+
@sentry-internal:registry=http://127.0.0.1:4873
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
body {
2+
font-family: system-ui, sans-serif;
3+
margin: 0;
4+
padding: 20px;
5+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import * as Sentry from '@sentry/react-router';
2+
import { StrictMode, startTransition } from 'react';
3+
import { hydrateRoot } from 'react-dom/client';
4+
import { HydratedRouter } from 'react-router/dom';
5+
6+
// Create the tracing integration with useInstrumentationAPI enabled
7+
// This must be set BEFORE Sentry.init() to prepare the instrumentation
8+
const tracing = Sentry.reactRouterTracingIntegration({ useInstrumentationAPI: true });
9+
10+
Sentry.init({
11+
environment: 'qa', // dynamic sampling bias to keep transactions
12+
dsn: 'https://username@domain/123',
13+
tunnel: `http://localhost:3031/`, // proxy server
14+
integrations: [tracing],
15+
tracesSampleRate: 1.0,
16+
tracePropagationTargets: [/^\//],
17+
});
18+
19+
// Get the client instrumentation from the Sentry integration
20+
// NOTE: As of React Router 7.x, HydratedRouter does NOT invoke these hooks in Framework Mode.
21+
// The client-side instrumentation is prepared for when React Router adds support.
22+
// Client-side navigation is currently handled by the legacy instrumentHydratedRouter() approach.
23+
const sentryClientInstrumentation = [tracing.clientInstrumentation];
24+
25+
startTransition(() => {
26+
hydrateRoot(
27+
document,
28+
<StrictMode>
29+
{/* unstable_instrumentations is React Router 7.x's prop name (will become `instrumentations` in v8) */}
30+
<HydratedRouter unstable_instrumentations={sentryClientInstrumentation} />
31+
</StrictMode>,
32+
);
33+
});
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { createReadableStreamFromReadable } from '@react-router/node';
2+
import * as Sentry from '@sentry/react-router';
3+
import { renderToPipeableStream } from 'react-dom/server';
4+
import { ServerRouter } from 'react-router';
5+
import { type HandleErrorFunction } from 'react-router';
6+
7+
const ABORT_DELAY = 5_000;
8+
9+
const handleRequest = Sentry.createSentryHandleRequest({
10+
streamTimeout: ABORT_DELAY,
11+
ServerRouter,
12+
renderToPipeableStream,
13+
createReadableStreamFromReadable,
14+
});
15+
16+
export default handleRequest;
17+
18+
export const handleError: HandleErrorFunction = Sentry.createSentryHandleError({ logErrors: true });
19+
20+
// Use Sentry's instrumentation API for server-side tracing
21+
// `unstable_instrumentations` is React Router 7.x's export name (will become `instrumentations` in v8)
22+
export const unstable_instrumentations = [Sentry.createSentryServerInstrumentation()];
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import * as Sentry from '@sentry/react-router';
2+
import { Links, Meta, Outlet, Scripts, ScrollRestoration, isRouteErrorResponse } from 'react-router';
3+
import type { Route } from './+types/root';
4+
import stylesheet from './app.css?url';
5+
6+
export const links: Route.LinksFunction = () => [
7+
{ rel: 'preconnect', href: 'https://fonts.googleapis.com' },
8+
{
9+
rel: 'preconnect',
10+
href: 'https://fonts.gstatic.com',
11+
crossOrigin: 'anonymous',
12+
},
13+
{
14+
rel: 'stylesheet',
15+
href: 'https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap',
16+
},
17+
{ rel: 'stylesheet', href: stylesheet },
18+
];
19+
20+
export function Layout({ children }: { children: React.ReactNode }) {
21+
return (
22+
<html lang="en">
23+
<head>
24+
<meta charSet="utf-8" />
25+
<meta name="viewport" content="width=device-width, initial-scale=1" />
26+
<Meta />
27+
<Links />
28+
</head>
29+
<body>
30+
{children}
31+
<ScrollRestoration />
32+
<Scripts />
33+
</body>
34+
</html>
35+
);
36+
}
37+
38+
export default function App() {
39+
return <Outlet />;
40+
}
41+
42+
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
43+
let message = 'Oops!';
44+
let details = 'An unexpected error occurred.';
45+
let stack: string | undefined;
46+
47+
if (isRouteErrorResponse(error)) {
48+
message = error.status === 404 ? '404' : 'Error';
49+
details = error.status === 404 ? 'The requested page could not be found.' : error.statusText || details;
50+
} else if (error && error instanceof Error) {
51+
Sentry.captureException(error);
52+
if (import.meta.env.DEV) {
53+
details = error.message;
54+
stack = error.stack;
55+
}
56+
}
57+
58+
return (
59+
<main className="pt-16 p-4 container mx-auto">
60+
<h1>{message}</h1>
61+
<p>{details}</p>
62+
{stack && (
63+
<pre className="w-full p-4 overflow-x-auto">
64+
<code>{stack}</code>
65+
</pre>
66+
)}
67+
</main>
68+
);
69+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { type RouteConfig, index, prefix, route } from '@react-router/dev/routes';
2+
3+
export default [
4+
index('routes/home.tsx'),
5+
...prefix('performance', [
6+
index('routes/performance/index.tsx'),
7+
route('ssr', 'routes/performance/ssr.tsx'),
8+
route('with/:param', 'routes/performance/dynamic-param.tsx'),
9+
route('static', 'routes/performance/static.tsx'),
10+
route('server-loader', 'routes/performance/server-loader.tsx'),
11+
route('server-action', 'routes/performance/server-action.tsx'),
12+
route('with-middleware', 'routes/performance/with-middleware.tsx'),
13+
route('error-loader', 'routes/performance/error-loader.tsx'),
14+
route('lazy-route', 'routes/performance/lazy-route.tsx'),
15+
]),
16+
] satisfies RouteConfig;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type { Route } from './+types/home';
2+
3+
export function meta({}: Route.MetaArgs) {
4+
return [
5+
{ title: 'React Router Instrumentation API Test' },
6+
{ name: 'description', content: 'Testing React Router instrumentation API' },
7+
];
8+
}
9+
10+
export default function Home() {
11+
return <div>home</div>;
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { Route } from './+types/dynamic-param';
2+
3+
// Minimal loader to trigger Sentry's route instrumentation
4+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
5+
export function loader() {
6+
return null;
7+
}
8+
9+
export default function DynamicParamPage({ params }: Route.ComponentProps) {
10+
return (
11+
<div>
12+
<h1>Dynamic Param Page</h1>
13+
<div>Param: {params.param}</div>
14+
</div>
15+
);
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export function loader(): never {
2+
throw new Error('Loader error for testing');
3+
}
4+
5+
export default function ErrorLoaderPage() {
6+
return (
7+
<div>
8+
<h1>Error Loader Page</h1>
9+
<p>This should not render</p>
10+
</div>
11+
);
12+
}

0 commit comments

Comments
 (0)