Skip to content
Draft
Show file tree
Hide file tree
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
node_modules

/.cache
/build
.env

/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@sentry:registry=http://127.0.0.1:4873
@sentry-internal:registry=http://127.0.0.1:4873
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# React Router 7 RSC

E2E test app for React Router 7 RSC (React Server Components) and `@sentry/react-router`.

**Note:** Skipped in CI (`sentryTest.skip: true`) - React Router's RSC Framework Mode is experimental.
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
* {
box-sizing: border-box;
}

body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
margin: 0;
padding: 20px;
line-height: 1.6;
}

h1 {
margin-top: 0;
}

nav {
margin-bottom: 20px;
}

nav ul {
list-style: none;
padding: 0;
display: flex;
gap: 20px;
}

nav a {
color: #0066cc;
text-decoration: none;
}

nav a:hover {
text-decoration: underline;
}

button {
padding: 8px 16px;
font-size: 14px;
cursor: pointer;
}

.error {
color: #cc0000;
background: #ffeeee;
padding: 10px;
border-radius: 4px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { createReadableStreamFromReadable } from '@react-router/node';
import * as Sentry from '@sentry/react-router';
import { renderToPipeableStream } from 'react-dom/server';
import { ServerRouter } from 'react-router';
import { type HandleErrorFunction } from 'react-router';

const ABORT_DELAY = 5_000;

const handleRequest = Sentry.createSentryHandleRequest({
streamTimeout: ABORT_DELAY,
ServerRouter,
renderToPipeableStream,
createReadableStreamFromReadable,
});

export default handleRequest;

export const handleError: HandleErrorFunction = Sentry.createSentryHandleError({ logErrors: true });
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import * as Sentry from '@sentry/react-router';
import { Links, Meta, Outlet, ScrollRestoration, isRouteErrorResponse } from 'react-router';
import type { Route } from './+types/root';
import stylesheet from './app.css?url';
import { SentryClient } from './sentry-client';

export const links: Route.LinksFunction = () => [{ rel: 'stylesheet', href: stylesheet }];

export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body>
<SentryClient />
{children}
<ScrollRestoration />
{/* <Scripts /> is not needed in RSC mode - scripts are injected by the RSC framework */}
</body>
</html>
);
}

export default function App() {
return <Outlet />;
}

export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
let message = 'Oops!';
let details = 'An unexpected error occurred.';
let stack: string | undefined;

if (isRouteErrorResponse(error)) {
message = error.status === 404 ? '404' : 'Error';
details = error.status === 404 ? 'The requested page could not be found.' : error.statusText || details;
} else if (error && error instanceof Error) {
Sentry.captureException(error);
if (import.meta.env.DEV) {
details = error.message;
stack = error.stack;
}
}

return (
<main>
<h1>{message}</h1>
<p>{details}</p>
{stack && (
<pre>
<code>{stack}</code>
</pre>
)}
</main>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { type RouteConfig, index, prefix, route } from '@react-router/dev/routes';

export default [
index('routes/home.tsx'),
...prefix('rsc', [
// RSC Server Component tests
route('server-component', 'routes/rsc/server-component.tsx'),
route('server-component-error', 'routes/rsc/server-component-error.tsx'),
route('server-component-async', 'routes/rsc/server-component-async.tsx'),
route('server-component-redirect', 'routes/rsc/server-component-redirect.tsx'),
route('server-component-not-found', 'routes/rsc/server-component-not-found.tsx'),
route('server-component/:param', 'routes/rsc/server-component-param.tsx'),
// RSC Server Function tests
route('server-function', 'routes/rsc/server-function.tsx'),
route('server-function-error', 'routes/rsc/server-function-error.tsx'),
]),
...prefix('performance', [
index('routes/performance/index.tsx'),
route('with/:param', 'routes/performance/dynamic-param.tsx'),
]),
] satisfies RouteConfig;
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Link } from 'react-router';

export default function Home() {
return (
<main>
<h1>React Router 7 RSC Test App</h1>
<nav>
<ul>
<li>
<Link to="/rsc/server-component">Server Component</Link>
</li>
<li>
<Link to="/rsc/server-component-error">Server Component Error</Link>
</li>
<li>
<Link to="/rsc/server-component-async">Server Component Async</Link>
</li>
<li>
<Link to="/rsc/server-component/test-param">Server Component with Param</Link>
</li>
<li>
<Link to="/rsc/server-function">Server Function</Link>
</li>
<li>
<Link to="/rsc/server-function-error">Server Function Error</Link>
</li>
<li>
<Link to="/performance">Performance</Link>
</li>
</ul>
</nav>
</main>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { Route } from './+types/dynamic-param';

export default function DynamicParamPage({ params }: Route.ComponentProps) {
return (
<main>
<h1>Dynamic Param Page</h1>
<p data-testid="param">Param: {params.param}</p>
</main>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Link } from 'react-router';

export default function PerformancePage() {
return (
<main>
<h1>Performance Test</h1>
<nav>
<ul>
<li>
<Link to="/performance/with/test-param">Dynamic Param</Link>
</li>
</ul>
</nav>
</main>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'use server';

import { wrapServerFunction } from '@sentry/react-router';

async function _submitForm(formData: FormData): Promise<{ success: boolean; message: string }> {
const name = formData.get('name') as string;

// Simulate some async work
await new Promise(resolve => setTimeout(resolve, 50));

return {
success: true,
message: `Hello, ${name}! Form submitted successfully.`,
};
}

export const submitForm = wrapServerFunction('submitForm', _submitForm);

async function _submitFormWithError(_formData: FormData): Promise<{ success: boolean; message: string }> {
// Simulate an error in server function
throw new Error('RSC Server Function Error: Something went wrong!');
}

export const submitFormWithError = wrapServerFunction('submitFormWithError', _submitFormWithError);

async function _getData(): Promise<{ timestamp: number; data: string }> {
await new Promise(resolve => setTimeout(resolve, 20));

return {
timestamp: Date.now(),
data: 'Fetched from server function',
};
}

export const getData = wrapServerFunction('getData', _getData);
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { wrapServerComponent } from '@sentry/react-router';
import type { Route } from './+types/server-component-async';

async function fetchData(): Promise<{ title: string; content: string }> {
// Simulate async data fetch
await new Promise(resolve => setTimeout(resolve, 50));
return {
title: 'Async Server Component',
content: 'This content was fetched asynchronously on the server.',
};
}

// Wrapped async server component for RSC mode
async function _AsyncServerComponent(_props: Route.ComponentProps) {
const data = await fetchData();

return (
<main>
<h1 data-testid="title">{data.title}</h1>
<p data-testid="content">{data.content}</p>
</main>
);
}

// Loader fetches data in standard mode
export async function loader() {
const data = await fetchData();
return data;
}

export default wrapServerComponent(_AsyncServerComponent, {
componentRoute: '/rsc/server-component-async',
componentType: 'Page',
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { wrapServerComponent } from '@sentry/react-router';
import type { Route } from './+types/server-component-error';

// Demonstrate error capture in wrapServerComponent
async function _ServerComponentWithError(_props: Route.ComponentProps) {
throw new Error('RSC Server Component Error: Mamma mia!');
}

const ServerComponent = wrapServerComponent(_ServerComponentWithError, {
componentRoute: '/rsc/server-component-error',
componentType: 'Page',
});

// For testing, we can trigger the wrapped component via a loader
export async function loader() {
// Call the wrapped ServerComponent to test error capture
try {
await ServerComponent({} as Route.ComponentProps);
} catch (e) {
// Error is captured by Sentry, rethrow for error boundary
throw e;
}
return {};
}

export default ServerComponent;
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { Route } from './+types/server-component-not-found';

// This route demonstrates that 404 responses are NOT captured as errors
export async function loader() {
// Throw a 404 response
throw new Response('Not Found', { status: 404 });
}

export default function NotFoundServerComponentPage() {
return (
<main>
<h1>Not Found Server Component</h1>
<p>This triggers a 404 response.</p>
</main>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { wrapServerComponent } from '@sentry/react-router';
import type { Route } from './+types/server-component-param';

// Wrapped parameterized server component for RSC mode
async function _ParamServerComponent({ params }: Route.ComponentProps) {
await new Promise(resolve => setTimeout(resolve, 10));

return (
<main>
<h1>Server Component with Parameter</h1>
<p data-testid="param">Parameter: {params.param}</p>
</main>
);
}

export default wrapServerComponent(_ParamServerComponent, {
componentRoute: '/rsc/server-component/:param',
componentType: 'Page',
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { redirect } from 'react-router';
import type { Route } from './+types/server-component-redirect';

// This route demonstrates that redirects are NOT captured as errors
export async function loader() {
// Redirect to home page
throw redirect('/');
}

export default function RedirectServerComponentPage() {
return (
<main>
<h1>Redirect Server Component</h1>
<p>You should be redirected and not see this.</p>
</main>
);
}
Loading
Loading