Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
1f1b759
feat(react-router): Add Experimental React Server Components (RSC) in…
onurtemizkan Dec 24, 2025
b20ff40
Fix RSC wrapper error handling and enable optional E2E tests
onurtemizkan Jan 20, 2026
6b48970
Add experimental flags
onurtemizkan Jan 20, 2026
882e529
Add client passthrough stubs for RSC wrappers and update E2E test app…
onurtemizkan Jan 22, 2026
6e70dca
Update react-router to 7.12.0 in RSC test app
onurtemizkan Jan 22, 2026
ee30971
Handle 404 responses in RSC server function wrapper
onurtemizkan Feb 4, 2026
79f9fb4
Add Vite plugin for automatic RSC server component instrumentation
onurtemizkan Feb 5, 2026
6e73ff6
Replace custom WeakSet error dedup with SDK's `__sentry_captured__` f…
onurtemizkan Feb 5, 2026
73c1000
Remove unused RSC exports and dead types from public API
onurtemizkan Feb 5, 2026
786a623
Switch to AST parsing, keep server component manually instrumented
onurtemizkan Feb 6, 2026
edfb9c0
Use `react_router.rsc` mechanism type for RSC error capture
onurtemizkan Feb 6, 2026
a7ce5d8
Fix TypeScript build errors
onurtemizkan Feb 6, 2026
8e39c93
Fix E2E type error in waitForError callbacks after rebase
onurtemizkan Feb 6, 2026
a074b4f
Remove isAlreadyCaptured, clean up RSC tests and E2E app
onurtemizkan Feb 19, 2026
7446fa7
Filter invalid identifier exports and add typeof guard in RSC utils
onurtemizkan Feb 19, 2026
c5d70f5
Clean up tests and docs
onurtemizkan Feb 20, 2026
641051d
Add manifest-based route matching fallback for RSC and make RSC auto-…
onurtemizkan Feb 23, 2026
6ca735a
Parameterize HTTP server span from wrapServerComponent in RSC mode
onurtemizkan Feb 23, 2026
06ced60
Lint
onurtemizkan Feb 23, 2026
e2be218
Guard against non-function exports in wrapServerFunction
onurtemizkan Feb 23, 2026
ecf2e43
Strip Vite query strings from module IDs in RSC plugin
onurtemizkan Feb 23, 2026
adf886c
Inherit debug option from parent config in RSC auto-instrumentation p…
onurtemizkan Feb 27, 2026
e83ff1a
Use startSpanManual in wrapServerFunction to preserve redirect span s…
onurtemizkan Feb 27, 2026
98cd9d4
Guard against circular parentId references in resolveFullRoutePath
onurtemizkan Feb 27, 2026
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,11 @@
node_modules

/.cache
/build
.env
.react-router

/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,6 @@
html,
body {
@media (prefers-color-scheme: dark) {
color-scheme: dark;
}
}
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,58 @@
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 />
</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,24 @@
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'),
route('server-component-comment-directive', 'routes/rsc/server-component-comment-directive.tsx'),
// RSC Server Function tests
route('server-function', 'routes/rsc/server-function.tsx'),
route('server-function-error', 'routes/rsc/server-function-error.tsx'),
route('server-function-arrow', 'routes/rsc/server-function-arrow.tsx'),
route('server-function-default', 'routes/rsc/server-function-default.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,16 @@
import { wrapServerComponent } from '@sentry/react-router';
import type { Route } from './+types/dynamic-param';

function DynamicParamPage({ params }: Route.ComponentProps) {
return (
<main>
<h1>Dynamic Param Page</h1>
<p data-testid="param">Param: {params.param}</p>
</main>
);
}

export default wrapServerComponent(DynamicParamPage, {
componentRoute: '/performance/with/:param',
componentType: 'Page',
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { wrapServerComponent } from '@sentry/react-router';
import { Link } from 'react-router';

function PerformancePage() {
return (
<main>
<h1>Performance Test</h1>
<nav>
<ul>
<li>
<Link to="/performance/with/test-param">Dynamic Param</Link>
</li>
</ul>
</nav>
</main>
);
}

export default wrapServerComponent(PerformancePage, {
componentRoute: '/performance',
componentType: 'Page',
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
'use server';

// This file only has a default export — the Sentry plugin should wrap it as
// a default server function, NOT extract "defaultAction" as a named export.
export default async function defaultAction(formData: FormData): Promise<{ success: boolean; message: string }> {
const name = formData.get('name') as string;
await new Promise(resolve => setTimeout(resolve, 50));
return {
success: true,
message: `Default: Hello, ${name}!`,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
'use server';

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

await new Promise(resolve => setTimeout(resolve, 50));

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

export async function submitFormWithError(_formData: FormData): Promise<{ success: boolean; message: string }> {
throw new Error('RSC Server Function Error: Something went wrong!');
}

export const submitFormArrow = async (formData: FormData): Promise<{ success: boolean; message: string }> => {
const name = formData.get('name') as string;
await new Promise(resolve => setTimeout(resolve, 50));
return {
success: true,
message: `Arrow: Hello, ${name}!`,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { wrapServerComponent } from '@sentry/react-router';

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

async function AsyncServerComponent() {
const data = await fetchData();

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

export default wrapServerComponent(AsyncServerComponent, {
componentRoute: '/rsc/server-component-async',
componentType: 'Page',
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// This is a server component, NOT a client component.
// "use client" — this comment should be ignored by the Sentry plugin.

import { wrapServerComponent } from '@sentry/react-router';
import type { Route } from './+types/server-component-comment-directive';

async function ServerComponentWithCommentDirective({ loaderData }: Route.ComponentProps) {
await new Promise(resolve => setTimeout(resolve, 10));

return (
<main>
<h1 data-testid="title">Server Component With Comment Directive</h1>
<p data-testid="loader-message">Message: {loaderData?.message ?? 'No loader data'}</p>
</main>
);
}

export default wrapServerComponent(ServerComponentWithCommentDirective, {
componentRoute: '/rsc/server-component-comment-directive',
componentType: 'Page',
});

export async function loader() {
return { message: 'Hello from comment-directive server component!' };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { wrapServerComponent } from '@sentry/react-router';

async function ServerComponentWithError() {
throw new Error('RSC Server Component Error: Mamma mia!');
}

export default wrapServerComponent(ServerComponentWithError, {
componentRoute: '/rsc/server-component-error',
componentType: 'Page',
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { wrapServerComponent } from '@sentry/react-router';

async function NotFoundServerComponent() {
throw new Response('Not Found', { status: 404 });
}

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

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,11 @@
import { redirect } from 'react-router';
import { wrapServerComponent } from '@sentry/react-router';

async function RedirectServerComponent() {
throw redirect('/');
}

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

async function ServerComponent({ loaderData }: Route.ComponentProps) {
await new Promise(resolve => setTimeout(resolve, 10));

return (
<main>
<h1>Server Component</h1>
<p data-testid="loader-message">Message: {loaderData?.message ?? 'No loader data'}</p>
</main>
);
}

export default wrapServerComponent(ServerComponent, {
componentRoute: '/rsc/server-component',
componentType: 'Page',
});

export async function loader() {
return { message: 'Hello from server loader!' };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Form, useActionData } from 'react-router';
import { submitFormArrow } from './actions';
import type { Route } from './+types/server-function-arrow';

export async function action({ request }: Route.ActionArgs) {
const formData = await request.formData();
return submitFormArrow(formData);
}

export default function ServerFunctionArrowPage() {
const actionData = useActionData<typeof action>();

return (
<main>
<h1>Server Function Arrow Test</h1>
<Form method="post">
<label htmlFor="name">Name:</label>
<input type="text" id="name" name="name" defaultValue="Arrow User" />
<button type="submit" id="submit">
Submit
</button>
</Form>

{actionData && (
<div data-testid="result">
<p data-testid="message">Message: {actionData.message}</p>
</div>
)}
</main>
);
}
Loading
Loading