diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-rsc/.gitignore b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/.gitignore
new file mode 100644
index 000000000000..012e938ef384
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/.gitignore
@@ -0,0 +1,10 @@
+node_modules
+
+/.cache
+/build
+.env
+
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-rsc/.npmrc b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/.npmrc
new file mode 100644
index 000000000000..070f80f05092
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/.npmrc
@@ -0,0 +1,2 @@
+@sentry:registry=http://127.0.0.1:4873
+@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-rsc/README.md b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/README.md
new file mode 100644
index 000000000000..9163c5ff69c4
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/README.md
@@ -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.
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/app.css b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/app.css
new file mode 100644
index 000000000000..36331bc72654
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/app.css
@@ -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;
+}
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/entry.server.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/entry.server.tsx
new file mode 100644
index 000000000000..738cd1515a4d
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/entry.server.tsx
@@ -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 });
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/root.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/root.tsx
new file mode 100644
index 000000000000..468cb79fc6f5
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/root.tsx
@@ -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 (
+
+
+
+
+
+
+
+
+
+ {children}
+
+ {/* is not needed in RSC mode - scripts are injected by the RSC framework */}
+
+
+ );
+}
+
+export default function App() {
+ return ;
+}
+
+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 (
+
+ {message}
+ {details}
+ {stack && (
+
+ {stack}
+
+ )}
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/routes.ts b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/routes.ts
new file mode 100644
index 000000000000..dff6af8aba5f
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/routes.ts
@@ -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;
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/routes/home.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/routes/home.tsx
new file mode 100644
index 000000000000..4b44ffca47d3
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/routes/home.tsx
@@ -0,0 +1,34 @@
+import { Link } from 'react-router';
+
+export default function Home() {
+ return (
+
+ React Router 7 RSC Test App
+
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/routes/performance/dynamic-param.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/routes/performance/dynamic-param.tsx
new file mode 100644
index 000000000000..51948e4d322f
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/routes/performance/dynamic-param.tsx
@@ -0,0 +1,10 @@
+import type { Route } from './+types/dynamic-param';
+
+export default function DynamicParamPage({ params }: Route.ComponentProps) {
+ return (
+
+ Dynamic Param Page
+ Param: {params.param}
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/routes/performance/index.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/routes/performance/index.tsx
new file mode 100644
index 000000000000..459806f56e17
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/routes/performance/index.tsx
@@ -0,0 +1,16 @@
+import { Link } from 'react-router';
+
+export default function PerformancePage() {
+ return (
+
+ Performance Test
+
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/routes/rsc/actions.ts b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/routes/rsc/actions.ts
new file mode 100644
index 000000000000..0ae0caec75c7
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/routes/rsc/actions.ts
@@ -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);
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/routes/rsc/server-component-async.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/routes/rsc/server-component-async.tsx
new file mode 100644
index 000000000000..bc96a16c4a66
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/routes/rsc/server-component-async.tsx
@@ -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 (
+
+ {data.title}
+ {data.content}
+
+ );
+}
+
+// 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',
+});
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/routes/rsc/server-component-error.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/routes/rsc/server-component-error.tsx
new file mode 100644
index 000000000000..1581ddadd8cd
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/routes/rsc/server-component-error.tsx
@@ -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;
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/routes/rsc/server-component-not-found.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/routes/rsc/server-component-not-found.tsx
new file mode 100644
index 000000000000..0fad23e20fe1
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/routes/rsc/server-component-not-found.tsx
@@ -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 (
+
+ Not Found Server Component
+ This triggers a 404 response.
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/routes/rsc/server-component-param.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/routes/rsc/server-component-param.tsx
new file mode 100644
index 000000000000..3311718415da
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/routes/rsc/server-component-param.tsx
@@ -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 (
+
+ Server Component with Parameter
+ Parameter: {params.param}
+
+ );
+}
+
+export default wrapServerComponent(_ParamServerComponent, {
+ componentRoute: '/rsc/server-component/:param',
+ componentType: 'Page',
+});
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/routes/rsc/server-component-redirect.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/routes/rsc/server-component-redirect.tsx
new file mode 100644
index 000000000000..a85dadcfe961
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/routes/rsc/server-component-redirect.tsx
@@ -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 (
+
+ Redirect Server Component
+ You should be redirected and not see this.
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/routes/rsc/server-component.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/routes/rsc/server-component.tsx
new file mode 100644
index 000000000000..0be52c9ca6d9
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/routes/rsc/server-component.tsx
@@ -0,0 +1,25 @@
+import { wrapServerComponent } from '@sentry/react-router';
+import type { Route } from './+types/server-component';
+
+// Demonstrate wrapServerComponent - this wrapper can be used to instrument
+// server components when RSC Framework Mode is enabled
+async function _ServerComponent({ loaderData }: Route.ComponentProps) {
+ await new Promise(resolve => setTimeout(resolve, 10));
+
+ return (
+
+ Server Component
+ This demonstrates a wrapped server component.
+ Message: {loaderData?.message ?? 'No loader data'}
+
+ );
+}
+
+export async function loader() {
+ return { message: 'Hello from server loader!' };
+}
+
+export default wrapServerComponent(_ServerComponent, {
+ componentRoute: '/rsc/server-component',
+ componentType: 'Page',
+});
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/routes/rsc/server-function-error.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/routes/rsc/server-function-error.tsx
new file mode 100644
index 000000000000..3d72bab7ccf0
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/routes/rsc/server-function-error.tsx
@@ -0,0 +1,32 @@
+import { Form, useActionData } from 'react-router';
+import { submitFormWithError } from './actions';
+import type { Route } from './+types/server-function-error';
+
+export async function action({ request }: Route.ActionArgs) {
+ const formData = await request.formData();
+ return submitFormWithError(formData);
+}
+
+export default function ServerFunctionErrorPage() {
+ const actionData = useActionData();
+
+ return (
+
+ Server Function Error Test
+ This page tests error capture in wrapServerFunction.
+
+
+
+ {actionData && (
+
+
This should not appear - error should be thrown
+
+ )}
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/routes/rsc/server-function.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/routes/rsc/server-function.tsx
new file mode 100644
index 000000000000..af147366f4c1
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/routes/rsc/server-function.tsx
@@ -0,0 +1,34 @@
+import { Form, useActionData } from 'react-router';
+import { submitForm } from './actions';
+import type { Route } from './+types/server-function';
+
+export async function action({ request }: Route.ActionArgs) {
+ const formData = await request.formData();
+ return submitForm(formData);
+}
+
+export default function ServerFunctionPage() {
+ const actionData = useActionData();
+
+ return (
+
+ Server Function Test
+ This page tests wrapServerFunction instrumentation.
+
+
+
+ {actionData && (
+
+
Success: {String(actionData.success)}
+
Message: {actionData.message}
+
+ )}
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/sentry-client.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/sentry-client.tsx
new file mode 100644
index 000000000000..2349c00ce937
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/app/sentry-client.tsx
@@ -0,0 +1,31 @@
+'use client';
+
+import { useEffect } from 'react';
+
+// RSC mode doesn't use entry.client.tsx, so we initialize Sentry via a client component.
+export function SentryClient() {
+ useEffect(() => {
+ import('@sentry/react-router')
+ .then(Sentry => {
+ if (!Sentry.isInitialized()) {
+ Sentry.init({
+ environment: 'qa',
+ dsn: 'https://username@domain/123',
+ tunnel: `http://localhost:3031/`,
+ integrations: [Sentry.reactRouterTracingIntegration()],
+ tracesSampleRate: 1.0,
+ tracePropagationTargets: [/^\//],
+ });
+ }
+ })
+ .catch(e => {
+ // Silent fail in production, but log in dev for debugging
+ if (import.meta.env.DEV) {
+ // eslint-disable-next-line no-console
+ console.warn('[Sentry] Failed to initialize:', e);
+ }
+ });
+ }, []);
+
+ return null;
+}
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-rsc/instrument.mjs b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/instrument.mjs
new file mode 100644
index 000000000000..d9d1ea7f386e
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/instrument.mjs
@@ -0,0 +1,9 @@
+import * as Sentry from '@sentry/react-router';
+
+Sentry.init({
+ dsn: 'https://username@domain/123',
+ environment: 'qa', // dynamic sampling bias to keep transactions
+ tracesSampleRate: 1.0,
+ tunnel: `http://localhost:3031/`, // proxy server
+ debug: true,
+});
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-rsc/package.json b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/package.json
new file mode 100644
index 000000000000..5a8f65710f15
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/package.json
@@ -0,0 +1,64 @@
+{
+ "name": "react-router-7-rsc",
+ "version": "0.1.0",
+ "type": "module",
+ "private": true,
+ "dependencies": {
+ "react": "19.1.0",
+ "react-dom": "19.1.0",
+ "react-router": "7.12.0",
+ "@react-router/node": "7.12.0",
+ "@react-router/serve": "7.12.0",
+ "@sentry/react-router": "latest || *",
+ "isbot": "^5.1.17"
+ },
+ "devDependencies": {
+ "@types/react": "19.1.0",
+ "@types/react-dom": "19.1.0",
+ "@types/node": "^22",
+ "@react-router/dev": "7.12.0",
+ "@vitejs/plugin-react": "4.5.1",
+ "@vitejs/plugin-rsc": "0.5.14",
+ "@playwright/test": "~1.56.0",
+ "@sentry-internal/test-utils": "link:../../../test-utils",
+ "typescript": "5.6.3",
+ "vite": "6.3.5"
+ },
+ "scripts": {
+ "build": "react-router build",
+ "test:build-latest": "pnpm install && pnpm add react-router@latest && pnpm add @react-router/node@latest && pnpm add @react-router/serve@latest && pnpm add @react-router/dev@latest && pnpm build",
+ "dev": "NODE_OPTIONS='--import ./instrument.mjs' react-router dev",
+ "start": "NODE_OPTIONS='--import ./instrument.mjs' react-router-serve ./build/server/index.js",
+ "proxy": "node start-event-proxy.mjs",
+ "typecheck": "react-router typegen && tsc",
+ "clean": "npx rimraf node_modules pnpm-lock.yaml",
+ "test:build": "pnpm install && pnpm build",
+ "test:assert": "pnpm test:ts && pnpm test:playwright",
+ "test:ts": "pnpm typecheck",
+ "test:playwright": "playwright test"
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ },
+ "volta": {
+ "extends": "../../package.json"
+ },
+ "sentryTest": {
+ "optional": true,
+ "optionalVariants": [
+ {
+ "build-command": "pnpm test:build-latest",
+ "label": "react-router-7-rsc (latest)"
+ }
+ ]
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-rsc/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/playwright.config.mjs
new file mode 100644
index 000000000000..3ed5721107a7
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/playwright.config.mjs
@@ -0,0 +1,8 @@
+import { getPlaywrightConfig } from '@sentry-internal/test-utils';
+
+const config = getPlaywrightConfig({
+ startCommand: `PORT=3030 pnpm start`,
+ port: 3030,
+});
+
+export default config;
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-rsc/public/.gitkeep b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/public/.gitkeep
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-rsc/react-router.config.ts b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/react-router.config.ts
new file mode 100644
index 000000000000..51e8967770b3
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/react-router.config.ts
@@ -0,0 +1,5 @@
+import type { Config } from '@react-router/dev/config';
+
+export default {
+ ssr: true,
+} satisfies Config;
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-rsc/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/start-event-proxy.mjs
new file mode 100644
index 000000000000..c39b3e59484b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/start-event-proxy.mjs
@@ -0,0 +1,6 @@
+import { startEventProxyServer } from '@sentry-internal/test-utils';
+
+startEventProxyServer({
+ port: 3031,
+ proxyServerName: 'react-router-7-rsc',
+});
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-rsc/tests/constants.ts b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/tests/constants.ts
new file mode 100644
index 000000000000..e0ecda948342
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/tests/constants.ts
@@ -0,0 +1 @@
+export const APP_NAME = 'react-router-7-rsc';
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-rsc/tests/performance/performance.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/tests/performance/performance.server.test.ts
new file mode 100644
index 000000000000..3de973d1a5ef
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/tests/performance/performance.server.test.ts
@@ -0,0 +1,115 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+import { APP_NAME } from '../constants';
+
+test.describe('RSC - Performance', () => {
+ test('should send server transaction on pageload', async ({ page }) => {
+ const txPromise = waitForTransaction(APP_NAME, transactionEvent => {
+ const isServerTransaction = transactionEvent.contexts?.runtime?.name === 'node';
+ const matchesRoute =
+ transactionEvent.transaction?.includes('/performance') ||
+ transactionEvent.request?.url?.includes('/performance');
+ return Boolean(isServerTransaction && matchesRoute && !transactionEvent.request?.url?.includes('/with/'));
+ });
+
+ await page.goto(`/performance`);
+
+ const transaction = await txPromise;
+
+ expect(transaction).toMatchObject({
+ type: 'transaction',
+ transaction: expect.stringMatching(/GET \/performance|GET \*/),
+ platform: 'node',
+ contexts: {
+ trace: {
+ span_id: expect.any(String),
+ trace_id: expect.any(String),
+ data: {
+ 'sentry.op': 'http.server',
+ 'sentry.origin': expect.stringMatching(/auto\.http\.(otel\.http|react_router\.request_handler)/),
+ 'sentry.source': expect.stringMatching(/route|url/),
+ },
+ op: 'http.server',
+ origin: expect.stringMatching(/auto\.http\.(otel\.http|react_router\.request_handler)/),
+ },
+ },
+ spans: expect.any(Array),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ transaction_info: { source: expect.stringMatching(/route|url/) },
+ request: {
+ url: expect.stringContaining('/performance'),
+ headers: expect.any(Object),
+ },
+ event_id: expect.any(String),
+ environment: 'qa',
+ sdk: {
+ integrations: expect.arrayContaining([expect.any(String)]),
+ name: 'sentry.javascript.react-router',
+ version: expect.any(String),
+ packages: expect.arrayContaining([
+ expect.objectContaining({ name: 'npm:@sentry/react-router', version: expect.any(String) }),
+ expect.objectContaining({ name: 'npm:@sentry/node', version: expect.any(String) }),
+ ]),
+ },
+ tags: {
+ runtime: 'node',
+ },
+ });
+ });
+
+ test('should send server transaction on parameterized route', async ({ page }) => {
+ const txPromise = waitForTransaction(APP_NAME, transactionEvent => {
+ const isServerTransaction = transactionEvent.contexts?.runtime?.name === 'node';
+ const matchesRoute =
+ transactionEvent.transaction?.includes('/performance/with') ||
+ transactionEvent.request?.url?.includes('/performance/with/some-param');
+ return Boolean(isServerTransaction && matchesRoute);
+ });
+
+ await page.goto(`/performance/with/some-param`);
+
+ const transaction = await txPromise;
+
+ expect(transaction).toMatchObject({
+ type: 'transaction',
+ transaction: expect.stringMatching(/GET \/performance\/with|GET \*/),
+ platform: 'node',
+ contexts: {
+ trace: {
+ span_id: expect.any(String),
+ trace_id: expect.any(String),
+ data: {
+ 'sentry.op': 'http.server',
+ 'sentry.origin': expect.stringMatching(/auto\.http\.(otel\.http|react_router\.request_handler)/),
+ 'sentry.source': expect.stringMatching(/route|url/),
+ },
+ op: 'http.server',
+ origin: expect.stringMatching(/auto\.http\.(otel\.http|react_router\.request_handler)/),
+ },
+ },
+ spans: expect.any(Array),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ transaction_info: { source: expect.stringMatching(/route|url/) },
+ request: {
+ url: expect.stringContaining('/performance/with/some-param'),
+ headers: expect.any(Object),
+ },
+ event_id: expect.any(String),
+ environment: 'qa',
+ sdk: {
+ integrations: expect.arrayContaining([expect.any(String)]),
+ name: 'sentry.javascript.react-router',
+ version: expect.any(String),
+ packages: expect.arrayContaining([
+ expect.objectContaining({ name: 'npm:@sentry/react-router', version: expect.any(String) }),
+ expect.objectContaining({ name: 'npm:@sentry/node', version: expect.any(String) }),
+ ]),
+ },
+ tags: {
+ runtime: 'node',
+ },
+ });
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-rsc/tests/rsc/server-component.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/tests/rsc/server-component.test.ts
new file mode 100644
index 000000000000..d6456bad11f8
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/tests/rsc/server-component.test.ts
@@ -0,0 +1,175 @@
+import { expect, test } from '@playwright/test';
+import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
+import { APP_NAME } from '../constants';
+
+test.describe('RSC - Server Component Wrapper', () => {
+ test('captures error from wrapped server component called in loader', async ({ page }) => {
+ const errorMessage = 'RSC Server Component Error: Mamma mia!';
+ const errorPromise = waitForError(APP_NAME, errorEvent => {
+ return errorEvent?.exception?.values?.[0]?.value === errorMessage;
+ });
+
+ await page.goto(`/rsc/server-component-error`);
+
+ const error = await errorPromise;
+
+ expect(error).toMatchObject({
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: errorMessage,
+ mechanism: {
+ handled: false,
+ type: 'instrument',
+ data: {
+ function: 'ServerComponent',
+ component_route: '/rsc/server-component-error',
+ component_type: 'Page',
+ },
+ },
+ },
+ ],
+ },
+ level: 'error',
+ platform: 'node',
+ environment: 'qa',
+ sdk: {
+ integrations: expect.any(Array),
+ name: 'sentry.javascript.react-router',
+ version: expect.any(String),
+ },
+ tags: { runtime: 'node' },
+ contexts: {
+ trace: {
+ span_id: expect.any(String),
+ trace_id: expect.any(String),
+ },
+ },
+ });
+ });
+
+ test('server component page loads with loader data', async ({ page }) => {
+ const txPromise = waitForTransaction(APP_NAME, transactionEvent => {
+ const isServerTransaction = transactionEvent.contexts?.runtime?.name === 'node';
+ const matchesRoute =
+ transactionEvent.transaction?.includes('/rsc/server-component') ||
+ transactionEvent.request?.url?.includes('/rsc/server-component');
+ return Boolean(isServerTransaction && matchesRoute && !transactionEvent.transaction?.includes('-async'));
+ });
+
+ await page.goto(`/rsc/server-component`);
+
+ // Verify the page renders with loader data
+ await expect(page.getByTestId('loader-message')).toContainText('Hello from server loader!');
+
+ const transaction = await txPromise;
+
+ expect(transaction).toMatchObject({
+ type: 'transaction',
+ transaction: expect.stringMatching(/\/rsc\/server-component|GET \*/),
+ platform: 'node',
+ environment: 'qa',
+ contexts: {
+ trace: {
+ span_id: expect.any(String),
+ trace_id: expect.any(String),
+ },
+ },
+ spans: expect.any(Array),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ sdk: {
+ name: 'sentry.javascript.react-router',
+ version: expect.any(String),
+ packages: expect.arrayContaining([
+ expect.objectContaining({ name: 'npm:@sentry/react-router', version: expect.any(String) }),
+ expect.objectContaining({ name: 'npm:@sentry/node', version: expect.any(String) }),
+ ]),
+ },
+ });
+ });
+
+ test('async server component page loads', async ({ page }) => {
+ const txPromise = waitForTransaction(APP_NAME, transactionEvent => {
+ const isServerTransaction = transactionEvent.contexts?.runtime?.name === 'node';
+ const matchesRoute =
+ transactionEvent.transaction?.includes('/rsc/server-component-async') ||
+ transactionEvent.request?.url?.includes('/rsc/server-component-async');
+ return Boolean(isServerTransaction && matchesRoute);
+ });
+
+ await page.goto(`/rsc/server-component-async`);
+
+ // Verify the page renders async content
+ await expect(page.getByTestId('title')).toHaveText('Async Server Component');
+ await expect(page.getByTestId('content')).toHaveText('This content was fetched asynchronously on the server.');
+
+ const transaction = await txPromise;
+
+ expect(transaction).toMatchObject({
+ type: 'transaction',
+ transaction: expect.stringMatching(/\/rsc\/server-component-async|GET \*/),
+ platform: 'node',
+ environment: 'qa',
+ contexts: {
+ trace: {
+ span_id: expect.any(String),
+ trace_id: expect.any(String),
+ },
+ },
+ spans: expect.any(Array),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ sdk: {
+ name: 'sentry.javascript.react-router',
+ version: expect.any(String),
+ packages: expect.arrayContaining([
+ expect.objectContaining({ name: 'npm:@sentry/react-router', version: expect.any(String) }),
+ expect.objectContaining({ name: 'npm:@sentry/node', version: expect.any(String) }),
+ ]),
+ },
+ });
+ });
+
+ test('parameterized server component route works', async ({ page }) => {
+ const txPromise = waitForTransaction(APP_NAME, transactionEvent => {
+ const isServerTransaction = transactionEvent.contexts?.runtime?.name === 'node';
+ const matchesRoute =
+ transactionEvent.transaction?.includes('/rsc/server-component') ||
+ transactionEvent.request?.url?.includes('/rsc/server-component/my-test-param');
+ return Boolean(isServerTransaction && matchesRoute && !transactionEvent.transaction?.includes('-async'));
+ });
+
+ await page.goto(`/rsc/server-component/my-test-param`);
+
+ // Verify the param was passed correctly
+ await expect(page.getByTestId('param')).toContainText('my-test-param');
+
+ const transaction = await txPromise;
+
+ expect(transaction).toMatchObject({
+ type: 'transaction',
+ transaction: expect.stringMatching(/\/rsc\/server-component|GET \*/),
+ platform: 'node',
+ environment: 'qa',
+ contexts: {
+ trace: {
+ span_id: expect.any(String),
+ trace_id: expect.any(String),
+ },
+ },
+ spans: expect.any(Array),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ sdk: {
+ name: 'sentry.javascript.react-router',
+ version: expect.any(String),
+ packages: expect.arrayContaining([
+ expect.objectContaining({ name: 'npm:@sentry/react-router', version: expect.any(String) }),
+ expect.objectContaining({ name: 'npm:@sentry/node', version: expect.any(String) }),
+ ]),
+ },
+ });
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-rsc/tests/rsc/server-function.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/tests/rsc/server-function.test.ts
new file mode 100644
index 000000000000..35ed74c34f25
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/tests/rsc/server-function.test.ts
@@ -0,0 +1,126 @@
+import { expect, test } from '@playwright/test';
+import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
+import { APP_NAME } from '../constants';
+
+test.describe('RSC - Server Function Wrapper', () => {
+ test('creates transaction for wrapped server function via action', async ({ page }) => {
+ const txPromise = waitForTransaction(APP_NAME, transactionEvent => {
+ const isServerTransaction = transactionEvent.contexts?.runtime?.name === 'node';
+ const matchesRoute =
+ transactionEvent.transaction?.includes('/rsc/server-function') ||
+ transactionEvent.request?.url?.includes('/rsc/server-function');
+ return Boolean(isServerTransaction && matchesRoute && !transactionEvent.transaction?.includes('-error'));
+ });
+
+ await page.goto(`/rsc/server-function`);
+ await page.locator('#submit').click();
+
+ // Verify the form submission was successful
+ await expect(page.getByTestId('message')).toContainText('Hello, Sentry User!');
+
+ const transaction = await txPromise;
+
+ expect(transaction).toMatchObject({
+ type: 'transaction',
+ transaction: expect.stringMatching(/\/rsc\/server-function|GET \*/),
+ platform: 'node',
+ environment: 'qa',
+ contexts: {
+ trace: {
+ span_id: expect.any(String),
+ trace_id: expect.any(String),
+ },
+ },
+ spans: expect.any(Array),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ sdk: {
+ name: 'sentry.javascript.react-router',
+ version: expect.any(String),
+ packages: expect.arrayContaining([
+ expect.objectContaining({ name: 'npm:@sentry/react-router', version: expect.any(String) }),
+ expect.objectContaining({ name: 'npm:@sentry/node', version: expect.any(String) }),
+ ]),
+ },
+ });
+
+ // Check for server function span in the transaction
+ const serverFunctionSpan = transaction.spans?.find(
+ span => span.data?.['rsc.server_function.name'] === 'submitForm',
+ );
+
+ if (serverFunctionSpan) {
+ expect(serverFunctionSpan).toMatchObject({
+ data: expect.objectContaining({
+ 'sentry.op': 'function.rsc.server_function',
+ 'sentry.origin': 'auto.function.react_router.rsc.server_function',
+ 'rsc.server_function.name': 'submitForm',
+ }),
+ });
+ }
+ });
+
+ test('captures error from wrapped server function', async ({ page }) => {
+ const errorMessage = 'RSC Server Function Error: Something went wrong!';
+ const errorPromise = waitForError(APP_NAME, errorEvent => {
+ return errorEvent?.exception?.values?.[0]?.value === errorMessage;
+ });
+
+ await page.goto(`/rsc/server-function-error`);
+ await page.locator('#submit').click();
+
+ const error = await errorPromise;
+
+ expect(error).toMatchObject({
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: errorMessage,
+ mechanism: {
+ handled: false,
+ type: 'instrument',
+ data: {
+ function: 'serverFunction',
+ server_function_name: 'submitFormWithError',
+ },
+ },
+ },
+ ],
+ },
+ level: 'error',
+ platform: 'node',
+ environment: 'qa',
+ sdk: {
+ integrations: expect.any(Array),
+ name: 'sentry.javascript.react-router',
+ version: expect.any(String),
+ },
+ tags: { runtime: 'node' },
+ contexts: {
+ trace: {
+ span_id: expect.any(String),
+ trace_id: expect.any(String),
+ },
+ },
+ });
+ });
+
+ test('server function page loads correctly', async ({ page }) => {
+ await page.goto(`/rsc/server-function`);
+
+ // Verify the page structure
+ await expect(page.locator('h1')).toHaveText('Server Function Test');
+ await expect(page.locator('#name')).toHaveValue('Sentry User');
+ await expect(page.locator('#submit')).toBeVisible();
+ });
+
+ test('server function form submission with custom input', async ({ page }) => {
+ await page.goto(`/rsc/server-function`);
+ await page.fill('#name', 'Test User');
+ await page.locator('#submit').click();
+
+ // Verify the form submission result
+ await expect(page.getByTestId('message')).toContainText('Hello, Test User!');
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-rsc/tsconfig.json b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/tsconfig.json
new file mode 100644
index 000000000000..6b11840e7262
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/tsconfig.json
@@ -0,0 +1,24 @@
+{
+ "include": ["**/*", "**/.server/**/*", "**/.client/**/*", ".react-router/types/**/*"],
+ "compilerOptions": {
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
+ "types": ["@react-router/node", "vite/client"],
+ "isolatedModules": true,
+ "esModuleInterop": true,
+ "jsx": "react-jsx",
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "resolveJsonModule": true,
+ "target": "ES2022",
+ "strict": true,
+ "allowJs": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "baseUrl": ".",
+ "paths": {
+ "~/*": ["./app/*"]
+ },
+ "noEmit": true,
+ "rootDirs": [".", ".react-router/types"]
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-rsc/vite.config.ts b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/vite.config.ts
new file mode 100644
index 000000000000..45b45b97d368
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-rsc/vite.config.ts
@@ -0,0 +1,22 @@
+import { unstable_reactRouterRSC } from '@react-router/dev/vite';
+import rsc from '@vitejs/plugin-rsc/plugin';
+import { defineConfig } from 'vite';
+
+// RSC Framework Mode (Preview - React Router 7.9.2+)
+// This enables React Server Components support in React Router
+export default defineConfig({
+ plugins: [unstable_reactRouterRSC(), rsc()],
+ // Exclude chokidar from RSC bundling - it's a CommonJS file watcher
+ // that causes parse errors when the RSC plugin tries to process it
+ optimizeDeps: {
+ exclude: ['chokidar'],
+ },
+ ssr: {
+ external: ['chokidar'],
+ },
+ build: {
+ rollupOptions: {
+ external: ['chokidar'],
+ },
+ },
+});
diff --git a/packages/react-router/src/client/index.ts b/packages/react-router/src/client/index.ts
index ba5c1c1264cb..5463d94d7ec6 100644
--- a/packages/react-router/src/client/index.ts
+++ b/packages/react-router/src/client/index.ts
@@ -8,6 +8,40 @@ export { reactRouterTracingIntegration } from './tracingIntegration';
export { captureReactException, reactErrorHandler, Profiler, withProfiler, useProfiler } from '@sentry/react';
+/**
+ * Just a passthrough in case this is imported from the client.
+ */
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function wrapServerComponent any>(
+ serverComponent: T,
+ _context: { componentRoute: string; componentType: string },
+): T {
+ return serverComponent;
+}
+
+/**
+ * Just a passthrough in case this is imported from the client.
+ */
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function wrapServerFunction Promise>(
+ _functionName: string,
+ serverFunction: T,
+ _options?: { name?: string; attributes?: Record },
+): T {
+ return serverFunction;
+}
+
+/**
+ * Just a passthrough in case this is imported from the client.
+ */
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function wrapServerFunctions Promise>>(
+ _moduleName: string,
+ serverFunctions: T,
+): T {
+ return serverFunctions;
+}
+
/**
* @deprecated ErrorBoundary is deprecated, use React Router's error boundary instead.
* See https://docs.sentry.io/platforms/javascript/guides/react-router/#report-errors-from-error-boundaries
diff --git a/packages/react-router/src/index.types.ts b/packages/react-router/src/index.types.ts
index c9c5cb371763..83274b58e9a9 100644
--- a/packages/react-router/src/index.types.ts
+++ b/packages/react-router/src/index.types.ts
@@ -28,3 +28,7 @@ export declare const openFeatureIntegration: typeof clientSdk.openFeatureIntegra
export declare const OpenFeatureIntegrationHook: typeof clientSdk.OpenFeatureIntegrationHook;
export declare const statsigIntegration: typeof clientSdk.statsigIntegration;
export declare const unleashIntegration: typeof clientSdk.unleashIntegration;
+
+export declare const wrapServerComponent: typeof serverSdk.wrapServerComponent;
+export declare const wrapServerFunction: typeof serverSdk.wrapServerFunction;
+export declare const wrapServerFunctions: typeof serverSdk.wrapServerFunctions;
diff --git a/packages/react-router/src/server/index.ts b/packages/react-router/src/server/index.ts
index acca80a94d81..1ea12e27c37c 100644
--- a/packages/react-router/src/server/index.ts
+++ b/packages/react-router/src/server/index.ts
@@ -11,3 +11,28 @@ export { wrapServerAction } from './wrapServerAction';
export { wrapServerLoader } from './wrapServerLoader';
export { createSentryHandleError, type SentryHandleErrorOptions } from './createSentryHandleError';
export { getMetaTagTransformer } from './getMetaTagTransformer';
+
+// React Server Components (RSC) - React Router v7.9.0+
+export {
+ wrapMatchRSCServerRequest,
+ wrapRouteRSCServerRequest,
+ wrapServerFunction,
+ wrapServerFunctions,
+ wrapServerComponent,
+ isServerComponentContext,
+} from './rsc';
+
+export type {
+ RSCRouteConfigEntry,
+ RSCPayload,
+ RSCMatch,
+ DecodedPayload,
+ RouterContextProvider,
+ MatchRSCServerRequestArgs,
+ MatchRSCServerRequestFn,
+ RouteRSCServerRequestArgs,
+ RouteRSCServerRequestFn,
+ RSCHydratedRouterProps,
+ ServerComponentContext,
+ WrapServerFunctionOptions,
+} from './rsc';
diff --git a/packages/react-router/src/server/rsc/index.ts b/packages/react-router/src/server/rsc/index.ts
new file mode 100644
index 000000000000..e1c33d51b51d
--- /dev/null
+++ b/packages/react-router/src/server/rsc/index.ts
@@ -0,0 +1,25 @@
+export { wrapMatchRSCServerRequest } from './wrapMatchRSCServerRequest';
+export { wrapRouteRSCServerRequest } from './wrapRouteRSCServerRequest';
+export { wrapServerFunction, wrapServerFunctions } from './wrapServerFunction';
+export { wrapServerComponent, isServerComponentContext } from './wrapServerComponent';
+
+export type {
+ RSCRouteConfigEntry,
+ RSCPayload,
+ RSCMatch,
+ DecodedPayload,
+ RouterContextProvider,
+ DecodeReplyFunction,
+ DecodeActionFunction,
+ DecodeFormStateFunction,
+ LoadServerActionFunction,
+ SSRCreateFromReadableStreamFunction,
+ BrowserCreateFromReadableStreamFunction,
+ MatchRSCServerRequestArgs,
+ MatchRSCServerRequestFn,
+ RouteRSCServerRequestArgs,
+ RouteRSCServerRequestFn,
+ RSCHydratedRouterProps,
+ ServerComponentContext,
+ WrapServerFunctionOptions,
+} from './types';
diff --git a/packages/react-router/src/server/rsc/responseUtils.ts b/packages/react-router/src/server/rsc/responseUtils.ts
new file mode 100644
index 000000000000..fd5782ec9a4c
--- /dev/null
+++ b/packages/react-router/src/server/rsc/responseUtils.ts
@@ -0,0 +1,93 @@
+import { debug } from '@sentry/core';
+import { DEBUG_BUILD } from '../../common/debug-build';
+
+/**
+ * WeakSet to track errors that have been captured to avoid double-capture.
+ * Uses WeakSet so errors are automatically removed when garbage collected.
+ */
+const CAPTURED_ERRORS = new WeakSet