Skip to content

Commit ee4df2b

Browse files
andreiborzaclaude
andcommitted
test(e2e): Add a static trace lifecycle React Router E2E app
Copies `react-router-7-framework` into `react-router-7-framework-static`, which keeps `traceLifecycle: 'static'` and its transaction-based specs. The rest of the React Router group moves to span streaming in the PRs above, so this copy is what keeps the static lifecycle covered. Also groups `collectStreamedSpans` by trace. It accumulated every envelope into a single array, so a test asserting on one request's children could mix in spans from an earlier page load, or have its wait satisfied by them. The predicate now sees one trace at a time. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 3251e33 commit ee4df2b

45 files changed

Lines changed: 1531 additions & 15 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
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: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
html,
2+
body {
3+
@media (prefers-color-scheme: dark) {
4+
color-scheme: dark;
5+
}
6+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { createContext } from 'react-router';
2+
3+
export type User = {
4+
id: string;
5+
name: string;
6+
};
7+
8+
export const userContext = createContext<User | null>(null);
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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+
Sentry.init({
7+
traceLifecycle: 'static',
8+
environment: 'qa', // dynamic sampling bias to keep transactions
9+
// todo: get this from env
10+
dsn: 'https://username@domain/123',
11+
tunnel: `http://localhost:3031/`, // proxy server
12+
integrations: [Sentry.reactRouterTracingIntegration()],
13+
tracesSampleRate: 1.0,
14+
tracePropagationTargets: [/^\//],
15+
});
16+
17+
startTransition(() => {
18+
hydrateRoot(
19+
document,
20+
<StrictMode>
21+
<HydratedRouter onError={Sentry.sentryOnError} />
22+
</StrictMode>,
23+
);
24+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
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 });
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { Links, Meta, Outlet, Scripts, ScrollRestoration, isRouteErrorResponse } from 'react-router';
2+
import type { Route } from './+types/root';
3+
import stylesheet from './app.css?url';
4+
5+
export const links: Route.LinksFunction = () => [
6+
{ rel: 'preconnect', href: 'https://fonts.googleapis.com' },
7+
{
8+
rel: 'preconnect',
9+
href: 'https://fonts.gstatic.com',
10+
crossOrigin: 'anonymous',
11+
},
12+
{
13+
rel: 'stylesheet',
14+
href: 'https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap',
15+
},
16+
{ rel: 'stylesheet', href: stylesheet },
17+
];
18+
19+
export function Layout({ children }: { children: React.ReactNode }) {
20+
return (
21+
<html lang="en">
22+
<head>
23+
<meta charSet="utf-8" />
24+
<meta name="viewport" content="width=device-width, initial-scale=1" />
25+
<Meta />
26+
<Links />
27+
</head>
28+
<body>
29+
{children}
30+
<ScrollRestoration />
31+
<Scripts />
32+
</body>
33+
</html>
34+
);
35+
}
36+
37+
export default function App() {
38+
return <Outlet />;
39+
}
40+
41+
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
42+
let message = 'Oops!';
43+
let details = 'An unexpected error occurred.';
44+
let stack: string | undefined;
45+
46+
if (isRouteErrorResponse(error)) {
47+
message = error.status === 404 ? '404' : 'Error';
48+
details = error.status === 404 ? 'The requested page could not be found.' : error.statusText || details;
49+
} else if (error && error instanceof Error) {
50+
if (import.meta.env.DEV) {
51+
details = error.message;
52+
stack = error.stack;
53+
}
54+
}
55+
56+
return (
57+
<main className="pt-16 p-4 container mx-auto">
58+
<h1>{message}</h1>
59+
<p>{details}</p>
60+
{stack && (
61+
<pre className="w-full p-4 overflow-x-auto">
62+
<code>{stack}</code>
63+
</pre>
64+
)}
65+
</main>
66+
);
67+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { type RouteConfig, index, prefix, route } from '@react-router/dev/routes';
2+
3+
export default [
4+
index('routes/home.tsx'),
5+
route('__sentry-flush', 'routes/sentry-flush.tsx'),
6+
...prefix('errors', [
7+
route('client', 'routes/errors/client.tsx'),
8+
route('client/:client-param', 'routes/errors/client-param.tsx'),
9+
route('client-loader', 'routes/errors/client-loader.tsx'),
10+
route('server-loader', 'routes/errors/server-loader.tsx'),
11+
route('client-action', 'routes/errors/client-action.tsx'),
12+
route('server-action', 'routes/errors/server-action.tsx'),
13+
]),
14+
...prefix('performance', [
15+
index('routes/performance/index.tsx'),
16+
route('ssr', 'routes/performance/ssr.tsx'),
17+
route('with/:param', 'routes/performance/dynamic-param.tsx'),
18+
route('static', 'routes/performance/static.tsx'),
19+
route('server-loader', 'routes/performance/server-loader.tsx'),
20+
route('server-action', 'routes/performance/server-action.tsx'),
21+
route('with-middleware', 'routes/performance/with-middleware.tsx'),
22+
route('redis', 'routes/performance/redis.tsx'),
23+
]),
24+
] satisfies RouteConfig;
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Form } from 'react-router';
2+
3+
export function clientAction() {
4+
throw new Error('Madonna mia! Che casino nella Client Action!');
5+
}
6+
7+
export default function ClientActionErrorPage() {
8+
return (
9+
<div>
10+
<h1>Client Error Action Page</h1>
11+
<Form method="post">
12+
<button id="submit" type="submit">
13+
Submit
14+
</button>
15+
</Form>
16+
</div>
17+
);
18+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { Route } from './+types/server-loader';
2+
3+
export function clientLoader() {
4+
throw new Error('¡Madre mía del client loader!');
5+
return { data: 'sad' };
6+
}
7+
8+
export default function ClientLoaderErrorPage({ loaderData }: Route.ComponentProps) {
9+
const { data } = loaderData;
10+
return (
11+
<div>
12+
<h1>Client Loader Error Page</h1>
13+
<div>{data}</div>
14+
</div>
15+
);
16+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { Route } from './+types/client-param';
2+
3+
export default function ClientErrorParamPage({ params }: Route.ComponentProps) {
4+
return (
5+
<div>
6+
<h1>Client Error Param Page</h1>
7+
<button
8+
id="throw-on-click"
9+
onClick={() => {
10+
throw new Error(`¡Madre mía de ${params['client-param']}!`);
11+
}}
12+
>
13+
Throw Error
14+
</button>
15+
</div>
16+
);
17+
}

0 commit comments

Comments
 (0)