diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/tests/performance/performance.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/tests/performance/performance.server.test.ts
index 582dd4771cd9..3c0b36d6d663 100644
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/tests/performance/performance.server.test.ts
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/tests/performance/performance.server.test.ts
@@ -167,4 +167,19 @@ test.describe('server - instrumentation API performance', () => {
expect(httpServerTransactions).toEqual(['GET /performance']);
});
+
+ test('resolves a real http.route on routes without a loader/action', async ({ page }) => {
+ // Regression guard for the server OTel removal: routes without a loader/action must still get a
+ // proper `http.route` (not the catch-all `*` placeholder) from the underlying HTTP instrumentation.
+ const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
+ return transactionEvent.transaction === 'GET /performance/ssr';
+ });
+
+ await page.goto(`/performance/ssr`);
+
+ const transaction = await txPromise;
+
+ expect(transaction.contexts?.trace?.op).toBe('http.server');
+ expect(transaction.contexts?.trace?.data?.['http.route']).toBe('/performance/ssr');
+ });
});
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/.gitignore b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/.gitignore
deleted file mode 100644
index ebb991370034..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/.gitignore
+++ /dev/null
@@ -1,32 +0,0 @@
-# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
-
-# dependencies
-/node_modules
-/.pnp
-.pnp.js
-
-# testing
-/coverage
-
-# production
-/build
-
-# misc
-.DS_Store
-.env.local
-.env.development.local
-.env.test.local
-.env.production.local
-
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-
-/test-results/
-/playwright-report/
-/playwright/.cache/
-
-!*.d.ts
-
-# react router
-.react-router
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/app.css b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/app.css
deleted file mode 100644
index b31c3a9d0ddf..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/app.css
+++ /dev/null
@@ -1,6 +0,0 @@
-html,
-body {
- @media (prefers-color-scheme: dark) {
- color-scheme: dark;
- }
-}
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/entry.client.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/entry.client.tsx
deleted file mode 100644
index 005268b40ad0..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/entry.client.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import * as Sentry from '@sentry/react-router';
-import { StrictMode, startTransition } from 'react';
-import { hydrateRoot } from 'react-dom/client';
-import { HydratedRouter } from 'react-router/dom';
-
-Sentry.init({
- environment: 'qa', // dynamic sampling bias to keep transactions
- // todo: get this from env
- dsn: 'https://username@domain/123',
- tunnel: `http://localhost:3031/`, // proxy server
- integrations: [Sentry.reactRouterTracingIntegration()],
- tracesSampleRate: 1.0,
- tracePropagationTargets: [/^\//],
-});
-
-startTransition(() => {
- hydrateRoot(
- document,
-
-
- ,
- );
-});
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/entry.server.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/entry.server.tsx
deleted file mode 100644
index 738cd1515a4d..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/entry.server.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-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-framework-node-20-18/app/root.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/root.tsx
deleted file mode 100644
index bc1b8f1236c0..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/root.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import { Links, Meta, Outlet, Scripts, ScrollRestoration, isRouteErrorResponse } from 'react-router';
-import type { Route } from './+types/root';
-import stylesheet from './app.css?url';
-
-export const links: Route.LinksFunction = () => [
- { rel: 'preconnect', href: 'https://fonts.googleapis.com' },
- {
- rel: 'preconnect',
- href: 'https://fonts.gstatic.com',
- crossOrigin: 'anonymous',
- },
- {
- rel: 'stylesheet',
- href: 'https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap',
- },
- { rel: 'stylesheet', href: stylesheet },
-];
-
-export function Layout({ children }: { children: React.ReactNode }) {
- return (
-
-
-
-
-
-
-
-
- {children}
-
-
-
-
- );
-}
-
-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) {
- 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-framework-node-20-18/app/routes.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes.ts
deleted file mode 100644
index b412893def52..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { type RouteConfig, index, prefix, route } from '@react-router/dev/routes';
-
-export default [
- index('routes/home.tsx'),
- ...prefix('errors', [
- route('client', 'routes/errors/client.tsx'),
- route('client/:client-param', 'routes/errors/client-param.tsx'),
- route('client-loader', 'routes/errors/client-loader.tsx'),
- route('server-loader', 'routes/errors/server-loader.tsx'),
- route('client-action', 'routes/errors/client-action.tsx'),
- route('server-action', 'routes/errors/server-action.tsx'),
- ]),
- ...prefix('performance', [
- index('routes/performance/index.tsx'),
- route('ssr', 'routes/performance/ssr.tsx'),
- route('with/:param', 'routes/performance/dynamic-param.tsx'),
- route('static', 'routes/performance/static.tsx'),
- route('server-loader', 'routes/performance/server-loader.tsx'),
- route('server-action', 'routes/performance/server-action.tsx'),
- ]),
-] satisfies RouteConfig;
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/errors/client-action.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/errors/client-action.tsx
deleted file mode 100644
index d3b2d08eef2e..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/errors/client-action.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import { Form } from 'react-router';
-
-export function clientAction() {
- throw new Error('Madonna mia! Che casino nella Client Action!');
-}
-
-export default function ClientActionErrorPage() {
- return (
-
-
Client Error Action Page
-
-
- );
-}
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/errors/client-loader.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/errors/client-loader.tsx
deleted file mode 100644
index 72d9e62a99dc..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/errors/client-loader.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import type { Route } from './+types/server-loader';
-
-export function clientLoader() {
- throw new Error('¡Madre mía del client loader!');
- return { data: 'sad' };
-}
-
-export default function ClientLoaderErrorPage({ loaderData }: Route.ComponentProps) {
- const { data } = loaderData;
- return (
-
-
Client Loader Error Page
-
{data}
-
- );
-}
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/errors/client-param.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/errors/client-param.tsx
deleted file mode 100644
index a2e423391f03..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/errors/client-param.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import type { Route } from './+types/client-param';
-
-export default function ClientErrorParamPage({ params }: Route.ComponentProps) {
- return (
-
-
Client Error Param Page
-
-
- );
-}
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/errors/client.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/errors/client.tsx
deleted file mode 100644
index 190074a5ef09..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/errors/client.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-export default function ClientErrorPage() {
- return (
-
-
Client Error Page
-
-
- );
-}
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/errors/server-action.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/errors/server-action.tsx
deleted file mode 100644
index 863c320f3557..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/errors/server-action.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import { Form } from 'react-router';
-
-export function action() {
- throw new Error('Madonna mia! Che casino nella Server Action!');
-}
-
-export default function ServerActionErrorPage() {
- return (
-
-
Server Error Action Page
-
-
- );
-}
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/errors/server-loader.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/errors/server-loader.tsx
deleted file mode 100644
index cb777686d540..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/errors/server-loader.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import type { Route } from './+types/server-loader';
-
-export function loader() {
- throw new Error('¡Madre mía del server!');
- return { data: 'sad' };
-}
-
-export default function ServerLoaderErrorPage({ loaderData }: Route.ComponentProps) {
- const { data } = loaderData;
- return (
-
-
Server Error Page
-
{data}
-
- );
-}
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/home.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/home.tsx
deleted file mode 100644
index 4498e7a0d017..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/home.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import type { Route } from './+types/home';
-
-export function meta({}: Route.MetaArgs) {
- return [{ title: 'New React Router App' }, { name: 'description', content: 'Welcome to React Router!' }];
-}
-
-export default function Home() {
- return home
;
-}
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/performance/dynamic-param.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/performance/dynamic-param.tsx
deleted file mode 100644
index 1ac02775f2ff..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/performance/dynamic-param.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import type { Route } from './+types/dynamic-param';
-
-export async function loader() {
- await new Promise(resolve => setTimeout(resolve, 500));
- return { data: 'burritos' };
-}
-
-export default function DynamicParamPage({ params }: Route.ComponentProps) {
- const { param } = params;
-
- return (
-
-
Dynamic Parameter Page
-
The parameter value is: {param}
-
- );
-}
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/performance/index.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/performance/index.tsx
deleted file mode 100644
index e5383306625a..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/performance/index.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import { Link } from 'react-router';
-
-export default function PerformancePage() {
- return (
-
-
Performance Page
-
-
- );
-}
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/performance/server-action.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/performance/server-action.tsx
deleted file mode 100644
index 462fc6fbf54c..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/performance/server-action.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import { Form } from 'react-router';
-import type { Route } from './+types/server-action';
-
-export async function action({ request }: Route.ActionArgs) {
- let formData = await request.formData();
- let name = formData.get('name');
- await new Promise(resolve => setTimeout(resolve, 1000));
- return {
- greeting: `Hola ${name}`,
- };
-}
-
-export default function Project({ actionData }: Route.ComponentProps) {
- return (
-
-
Server action page
-
- {actionData ?
{actionData.greeting}
: null}
-
- );
-}
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/performance/server-loader.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/performance/server-loader.tsx
deleted file mode 100644
index e5c222ff4c05..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/performance/server-loader.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import type { Route } from './+types/server-loader';
-
-export async function loader() {
- await new Promise(resolve => setTimeout(resolve, 500));
- return { data: 'burritos' };
-}
-
-export default function ServerLoaderPage({ loaderData }: Route.ComponentProps) {
- const { data } = loaderData;
- return (
-
-
Server Loader Page
-
{data}
-
- );
-}
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/performance/ssr.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/performance/ssr.tsx
deleted file mode 100644
index 253e964ff15d..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/performance/ssr.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-export default function SsrPage() {
- return (
-
-
SSR Page
-
- );
-}
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/performance/static.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/performance/static.tsx
deleted file mode 100644
index 3dea24381fdc..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/app/routes/performance/static.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-export default function StaticPage() {
- return Static Page
;
-}
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/instrument.mjs b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/instrument.mjs
deleted file mode 100644
index 48e4b7b61ff3..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/instrument.mjs
+++ /dev/null
@@ -1,8 +0,0 @@
-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,
-});
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/package.json b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/package.json
deleted file mode 100644
index 65f4a96b0165..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/package.json
+++ /dev/null
@@ -1,64 +0,0 @@
-{
- "name": "react-router-7-framework-node-20-18",
- "version": "0.1.0",
- "type": "module",
- "private": true,
- "dependencies": {
- "react": "^18.3.1",
- "react-dom": "^18.3.1",
- "react-router": "7.13.0",
- "@react-router/node": "7.13.0",
- "@react-router/serve": "7.13.0",
- "@sentry/react-router": "file:../../packed/sentry-react-router-packed.tgz",
- "isbot": "^5.1.17"
- },
- "devDependencies": {
- "@types/react": "18.3.1",
- "@types/react-dom": "18.3.1",
- "@types/node": "^20",
- "@react-router/dev": "7.13.0",
- "@playwright/test": "~1.56.0",
- "@sentry-internal/test-utils": "link:../../../test-utils",
- "typescript": "^5.6.3",
- "vite": "^5.4.11"
- },
- "scripts": {
- "build": "react-router 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"
- },
- "eslintConfig": {
- "extends": [
- "react-app",
- "react-app/jest"
- ]
- },
- "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",
- "node": "20.18.2"
- },
- "pnpm": {
- "overrides": {
- "p-map": "^4.0.0"
- }
- }
-}
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/playwright.config.mjs
deleted file mode 100644
index 3ed5721107a7..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/playwright.config.mjs
+++ /dev/null
@@ -1,8 +0,0 @@
-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-framework-node-20-18/public/favicon.ico b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/public/favicon.ico
deleted file mode 100644
index 5dbdfcddcb14..000000000000
Binary files a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/public/favicon.ico and /dev/null differ
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/react-router.config.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/react-router.config.ts
deleted file mode 100644
index bb1f96469dd2..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/react-router.config.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import type { Config } from '@react-router/dev/config';
-
-export default {
- ssr: true,
- prerender: ['/performance/static'],
-} satisfies Config;
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/start-event-proxy.mjs
deleted file mode 100644
index c430b9c3e710..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/start-event-proxy.mjs
+++ /dev/null
@@ -1,6 +0,0 @@
-import { startEventProxyServer } from '@sentry-internal/test-utils';
-
-startEventProxyServer({
- port: 3031,
- proxyServerName: 'react-router-7-framework-node-20-18',
-});
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/constants.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/constants.ts
deleted file mode 100644
index 2c61859bb6ef..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/constants.ts
+++ /dev/null
@@ -1 +0,0 @@
-export const APP_NAME = 'react-router-7-framework-node-20-18';
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/errors/errors.client.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/errors/errors.client.test.ts
deleted file mode 100644
index c1a7de46f1b6..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/errors/errors.client.test.ts
+++ /dev/null
@@ -1,140 +0,0 @@
-import { expect, test } from '@playwright/test';
-import { waitForError } from '@sentry-internal/test-utils';
-import { APP_NAME } from '../constants';
-
-test.describe('client-side errors', () => {
- const errorMessage = '¡Madre mía!';
- test('captures error thrown on click', async ({ page }) => {
- const errorPromise = waitForError(APP_NAME, async errorEvent => {
- return errorEvent?.exception?.values?.[0]?.value === errorMessage;
- });
-
- await page.goto(`/errors/client`);
- await page.locator('#throw-on-click').click();
-
- const error = await errorPromise;
-
- expect(error).toMatchObject({
- exception: {
- values: [
- {
- type: 'Error',
- value: errorMessage,
- mechanism: {
- handled: false,
- },
- },
- ],
- },
- transaction: '/errors/client',
- request: {
- url: expect.stringContaining('errors/client'),
- headers: expect.any(Object),
- },
- level: 'error',
- platform: 'javascript',
- environment: 'qa',
- sdk: {
- integrations: expect.any(Array),
- name: 'sentry.javascript.react-router',
- version: expect.any(String),
- },
- tags: { runtime: 'browser' },
- contexts: {
- trace: {
- span_id: expect.any(String),
- trace_id: expect.any(String),
- },
- },
- breadcrumbs: [
- {
- category: 'ui.click',
- message: 'body > div > button#throw-on-click',
- },
- ],
- });
- });
-
- test('captures error thrown on click from a parameterized route', async ({ page }) => {
- const errorMessage = '¡Madre mía de churros!';
- const errorPromise = waitForError(APP_NAME, async errorEvent => {
- return errorEvent?.exception?.values?.[0]?.value === errorMessage;
- });
-
- await page.goto('/errors/client/churros');
- await page.locator('#throw-on-click').click();
-
- const error = await errorPromise;
-
- expect(error).toMatchObject({
- exception: {
- values: [
- {
- type: 'Error',
- value: '¡Madre mía de churros!',
- mechanism: {
- handled: false,
- },
- },
- ],
- },
- // todo: should be '/errors/client/:client-param'
- transaction: '/errors/client/churros',
- });
- });
-
- test('captures error thrown in a clientLoader', async ({ page }) => {
- const errorMessage = '¡Madre mía del client loader!';
- const errorPromise = waitForError(APP_NAME, async errorEvent => {
- return errorEvent?.exception?.values?.[0]?.value === errorMessage;
- });
-
- await page.goto('/errors/client-loader');
-
- const error = await errorPromise;
-
- expect(error).toMatchObject({
- exception: {
- values: [
- {
- type: 'Error',
- value: errorMessage,
- mechanism: {
- handled: false,
- type: 'auto.function.react_router.on_error',
- },
- },
- ],
- },
- transaction: '/errors/client-loader',
- });
- });
-
- test('captures error thrown in a clientAction', async ({ page }) => {
- const errorMessage = 'Madonna mia! Che casino nella Client Action!';
- const errorPromise = waitForError(APP_NAME, async errorEvent => {
- return errorEvent?.exception?.values?.[0]?.value === errorMessage;
- });
-
- await page.goto('/errors/client-action');
- await page.locator('#submit').click();
-
- const error = await errorPromise;
-
- expect(error).toMatchObject({
- exception: {
- values: [
- {
- type: 'Error',
- value: errorMessage,
- mechanism: {
- handled: false,
- type: 'auto.function.react_router.on_error',
- },
- },
- ],
- },
- transaction: '/errors/client-action',
- });
- });
-});
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/errors/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/errors/errors.server.test.ts
deleted file mode 100644
index 2759bfecb67e..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/errors/errors.server.test.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-import { expect, test } from '@playwright/test';
-import { waitForError } from '@sentry-internal/test-utils';
-import { APP_NAME } from '../constants';
-
-test.describe('server-side errors', () => {
- test('captures error thrown in server loader', async ({ page }) => {
- const errorMessage = '¡Madre mía del server!';
- const errorPromise = waitForError(APP_NAME, async errorEvent => {
- return errorEvent?.exception?.values?.[0]?.value === errorMessage;
- });
-
- await page.goto(`/errors/server-loader`);
-
- const error = await errorPromise;
-
- expect(error).toMatchObject({
- exception: {
- values: [
- {
- type: 'Error',
- value: errorMessage,
- mechanism: {
- handled: false,
- type: 'react-router',
- },
- },
- ],
- },
- // todo: should be 'GET /errors/server-loader'
- transaction: 'GET *',
- request: {
- url: expect.stringContaining('errors/server-loader'),
- headers: expect.any(Object),
- },
- 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('captures error thrown in server action', async ({ page }) => {
- const errorMessage = 'Madonna mia! Che casino nella Server Action!';
- const errorPromise = waitForError(APP_NAME, async errorEvent => {
- return errorEvent?.exception?.values?.[0]?.value === errorMessage;
- });
-
- await page.goto(`/errors/server-action`);
- await page.locator('#submit').click();
-
- const error = await errorPromise;
-
- expect(error).toMatchObject({
- exception: {
- values: [
- {
- type: 'Error',
- value: errorMessage,
- mechanism: {
- handled: false,
- type: 'react-router',
- },
- },
- ],
- },
- // todo: should be 'POST /errors/server-action'
- transaction: 'POST *',
- request: {
- url: expect.stringContaining('errors/server-action'),
- headers: expect.any(Object),
- },
- 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),
- },
- },
- });
- });
-});
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/performance/navigation.client.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/performance/navigation.client.test.ts
deleted file mode 100644
index 3432b95ddae3..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/performance/navigation.client.test.ts
+++ /dev/null
@@ -1,126 +0,0 @@
-import { expect, test } from '@playwright/test';
-import { waitForTransaction } from '@sentry-internal/test-utils';
-import { APP_NAME } from '../constants';
-
-test.describe('client - navigation performance', () => {
- test('should create navigation transaction', async ({ page }) => {
- const navigationPromise = waitForTransaction(APP_NAME, async transactionEvent => {
- return (
- transactionEvent.transaction === '/performance/ssr' && transactionEvent.contexts?.trace?.op === 'navigation'
- );
- });
-
- const pageloadTxPromise = waitForTransaction(APP_NAME, async transactionEvent => {
- return transactionEvent.transaction === '/performance' && transactionEvent.contexts?.trace?.op === 'pageload';
- });
-
- await page.goto(`/performance`); // pageload
- await pageloadTxPromise;
- await page.getByRole('link', { name: 'SSR Page' }).click(); // navigation
-
- const transaction = await navigationPromise;
-
- expect(transaction).toMatchObject({
- contexts: {
- trace: {
- span_id: expect.any(String),
- trace_id: expect.any(String),
- data: {
- 'sentry.origin': 'auto.navigation.react_router',
- 'sentry.op': 'navigation',
- 'sentry.source': 'route',
- 'url.template': '/performance/ssr',
- 'url.path': '/performance/ssr',
- 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/ssr$/),
- },
- op: 'navigation',
- origin: 'auto.navigation.react_router',
- },
- },
- spans: expect.any(Array),
- start_timestamp: expect.any(Number),
- timestamp: expect.any(Number),
- transaction: '/performance/ssr',
- type: 'transaction',
- transaction_info: { source: 'route' },
- platform: 'javascript',
- request: {
- url: expect.stringContaining('/performance/ssr'),
- 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: [
- { name: 'npm:@sentry/react-router', version: expect.any(String) },
- { name: 'npm:@sentry/browser', version: expect.any(String) },
- ],
- },
- tags: { runtime: 'browser' },
- });
- });
-
- test('should update navigation transaction for dynamic routes', async ({ page }) => {
- const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
- return (
- transactionEvent.transaction === '/performance/with/:param' &&
- transactionEvent.contexts?.trace?.op === 'navigation'
- );
- });
-
- const pageloadTxPromise = waitForTransaction(APP_NAME, async transactionEvent => {
- return transactionEvent.transaction === '/performance' && transactionEvent.contexts?.trace?.op === 'pageload';
- });
-
- await page.goto(`/performance`); // pageload
- await pageloadTxPromise;
- await page.getByRole('link', { name: 'With Param Page' }).click(); // navigation
-
- const transaction = await txPromise;
-
- expect(transaction).toMatchObject({
- contexts: {
- trace: {
- span_id: expect.any(String),
- trace_id: expect.any(String),
- data: {
- 'sentry.origin': 'auto.navigation.react_router',
- 'sentry.op': 'navigation',
- 'sentry.source': 'route',
- 'url.template': '/performance/with/:param',
- 'url.path': '/performance/with/sentry',
- 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/with\/sentry$/),
- },
- op: 'navigation',
- origin: 'auto.navigation.react_router',
- },
- },
- spans: expect.any(Array),
- start_timestamp: expect.any(Number),
- timestamp: expect.any(Number),
- transaction: '/performance/with/:param',
- type: 'transaction',
- transaction_info: { source: 'route' },
- platform: 'javascript',
- request: {
- url: expect.stringContaining('/performance/with/sentry'),
- 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: [
- { name: 'npm:@sentry/react-router', version: expect.any(String) },
- { name: 'npm:@sentry/browser', version: expect.any(String) },
- ],
- },
- tags: { runtime: 'browser' },
- });
- });
-});
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/performance/pageload.client.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/performance/pageload.client.test.ts
deleted file mode 100644
index f996989ccbf5..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/performance/pageload.client.test.ts
+++ /dev/null
@@ -1,148 +0,0 @@
-import { expect, test } from '@playwright/test';
-import { waitForTransaction } from '@sentry-internal/test-utils';
-import { APP_NAME } from '../constants';
-
-test.describe('client - pageload performance', () => {
- test('should send pageload transaction', async ({ page }) => {
- const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
- return transactionEvent.transaction === '/performance' && transactionEvent.contexts?.trace?.op === 'pageload';
- });
-
- await page.goto(`/performance`);
-
- const transaction = await txPromise;
-
- expect(transaction).toMatchObject({
- contexts: {
- trace: {
- span_id: expect.any(String),
- trace_id: expect.any(String),
- data: {
- 'sentry.origin': 'auto.pageload.react_router',
- 'sentry.op': 'pageload',
- 'sentry.source': 'route',
- 'url.template': '/performance',
- // react-router-serve 301-redirects the bare index route to a trailing slash
- 'url.path': '/performance/',
- 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/$/),
- },
- op: 'pageload',
- origin: 'auto.pageload.react_router',
- },
- },
- spans: expect.any(Array),
- start_timestamp: expect.any(Number),
- timestamp: expect.any(Number),
- transaction: '/performance',
- type: 'transaction',
- transaction_info: { source: 'route' },
- measurements: expect.any(Object),
- platform: 'javascript',
- 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: [
- { name: 'npm:@sentry/react-router', version: expect.any(String) },
- { name: 'npm:@sentry/browser', version: expect.any(String) },
- ],
- },
- tags: { runtime: 'browser' },
- });
- });
-
- test('should update pageload transaction for dynamic routes', async ({ page }) => {
- const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
- return (
- transactionEvent.transaction === '/performance/with/:param' &&
- transactionEvent.contexts?.trace?.op === 'pageload'
- );
- });
-
- await page.goto(`/performance/with/sentry`);
-
- const transaction = await txPromise;
-
- expect(transaction).toMatchObject({
- contexts: {
- trace: {
- span_id: expect.any(String),
- trace_id: expect.any(String),
- data: {
- 'sentry.origin': 'auto.pageload.react_router',
- 'sentry.op': 'pageload',
- 'sentry.source': 'route',
- 'url.template': '/performance/with/:param',
- 'url.path': '/performance/with/sentry',
- 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/with\/sentry$/),
- },
- op: 'pageload',
- origin: 'auto.pageload.react_router',
- },
- },
- spans: expect.any(Array),
- start_timestamp: expect.any(Number),
- timestamp: expect.any(Number),
- transaction: '/performance/with/:param',
- type: 'transaction',
- transaction_info: { source: 'route' },
- measurements: expect.any(Object),
- platform: 'javascript',
- request: {
- url: expect.stringContaining('/performance/with/sentry'),
- 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: [
- { name: 'npm:@sentry/react-router', version: expect.any(String) },
- { name: 'npm:@sentry/browser', version: expect.any(String) },
- ],
- },
- tags: { runtime: 'browser' },
- });
- });
-
- test('should send pageload transaction for prerendered pages', async ({ page }) => {
- const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
- return (
- transactionEvent.transaction === '/performance/static' && transactionEvent.contexts?.trace?.op === 'pageload'
- );
- });
-
- await page.goto(`/performance/static`);
-
- const transaction = await txPromise;
-
- expect(transaction).toMatchObject({
- transaction: '/performance/static',
- contexts: {
- trace: {
- span_id: expect.any(String),
- trace_id: expect.any(String),
- data: {
- 'sentry.origin': 'auto.pageload.react_router',
- 'sentry.op': 'pageload',
- 'sentry.source': 'route',
- 'url.template': '/performance/static',
- // react-router-serve 301-redirects prerendered routes to a trailing slash
- 'url.path': '/performance/static/',
- 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/static\/$/),
- },
- op: 'pageload',
- origin: 'auto.pageload.react_router',
- },
- },
- });
- });
-});
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/performance/performance.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/performance/performance.server.test.ts
deleted file mode 100644
index e0ca27a19e10..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/performance/performance.server.test.ts
+++ /dev/null
@@ -1,228 +0,0 @@
-import { expect, test } from '@playwright/test';
-import { waitForTransaction } from '@sentry-internal/test-utils';
-import { APP_NAME } from '../constants';
-
-test.describe('server - performance', () => {
- test('should send server transaction on pageload', async ({ page }) => {
- const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
- return transactionEvent.transaction === 'GET /performance';
- });
-
- await page.goto(`/performance`);
-
- const transaction = await txPromise;
-
- expect(transaction).toMatchObject({
- contexts: {
- trace: {
- span_id: expect.any(String),
- trace_id: expect.any(String),
- data: {
- 'sentry.op': 'http.server',
- 'sentry.origin': 'auto.http.react_router.request_handler',
- 'sentry.source': 'route',
- },
- op: 'http.server',
- origin: 'auto.http.react_router.request_handler',
- },
- },
- spans: expect.any(Array),
- start_timestamp: expect.any(Number),
- timestamp: expect.any(Number),
- transaction: 'GET /performance',
- type: 'transaction',
- transaction_info: { source: 'route' },
- platform: 'node',
- 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: [
- { name: 'npm:@sentry/react-router', version: expect.any(String) },
- { 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, async transactionEvent => {
- return transactionEvent.transaction === 'GET /performance/with/:param';
- });
-
- await page.goto(`/performance/with/some-param`);
-
- const transaction = await txPromise;
-
- expect(transaction).toMatchObject({
- contexts: {
- trace: {
- span_id: expect.any(String),
- trace_id: expect.any(String),
- data: {
- 'sentry.op': 'http.server',
- 'sentry.origin': 'auto.http.react_router.request_handler',
- 'sentry.source': 'route',
- },
- op: 'http.server',
- origin: 'auto.http.react_router.request_handler',
- },
- },
- spans: expect.any(Array),
- start_timestamp: expect.any(Number),
- timestamp: expect.any(Number),
- transaction: 'GET /performance/with/:param',
- type: 'transaction',
- transaction_info: { source: 'route' },
- platform: 'node',
- 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: [
- { name: 'npm:@sentry/react-router', version: expect.any(String) },
- { name: 'npm:@sentry/node', version: expect.any(String) },
- ],
- },
- tags: {
- runtime: 'node',
- },
- });
- });
-
- test('should automatically instrument server loader', async ({ page }) => {
- const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
- return transactionEvent.transaction === 'GET /performance/server-loader.data';
- });
-
- await page.goto('/performance'); // initial ssr pageloads do not contain .data requests
- await page.getByRole('link', { name: 'Server Loader' }).click(); // this will actually trigger a .data request
-
- const transaction = await txPromise;
-
- expect(transaction).toEqual(
- expect.objectContaining({
- contexts: expect.objectContaining({
- trace: {
- span_id: expect.any(String),
- trace_id: expect.any(String),
- op: 'http.server',
- origin: 'auto.http.react_router.server',
- parent_span_id: expect.any(String),
- status: 'ok',
- data: expect.objectContaining({
- 'http.method': 'GET',
- 'http.response.status_code': 200,
- 'http.status_code': 200,
- 'http.status_text': 'OK',
- 'http.target': '/performance/server-loader.data',
- 'http.url': 'http://localhost:3030/performance/server-loader.data',
- 'sentry.op': 'http.server',
- 'sentry.origin': 'auto.http.react_router.server',
- 'sentry.source': 'url',
- url: 'http://localhost:3030/performance/server-loader.data',
- }),
- },
- }),
- transaction: 'GET /performance/server-loader.data',
- type: 'transaction',
- transaction_info: { source: 'url' },
- platform: 'node',
- }),
- );
-
- // ensure we do not have a stray, bogus route attribute
- expect(transaction.contexts?.trace?.data?.['http.route']).not.toBeDefined();
-
- expect(transaction.spans).toContainEqual({
- span_id: expect.any(String),
- trace_id: expect.any(String),
- data: {
- 'sentry.op': 'function.react_router.loader',
- 'sentry.origin': 'auto.http.react_router.server',
- },
- description: 'Executing Server Loader',
- op: 'function.react_router.loader',
- origin: 'auto.http.react_router.server',
- parent_span_id: expect.any(String),
- start_timestamp: expect.any(Number),
- status: 'ok',
- timestamp: expect.any(Number),
- });
- });
-
- test('should automatically instrument server action', async ({ page }) => {
- const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
- return transactionEvent.transaction === 'POST /performance/server-action.data';
- });
-
- await page.goto(`/performance/server-action`);
- await page.getByRole('button', { name: 'Submit' }).click(); // this will trigger a .data request
-
- const transaction = await txPromise;
-
- expect(transaction).toEqual(
- expect.objectContaining({
- contexts: expect.objectContaining({
- trace: {
- span_id: expect.any(String),
- trace_id: expect.any(String),
- op: 'http.server',
- origin: 'auto.http.react_router.server',
- parent_span_id: expect.any(String),
- status: 'ok',
- data: expect.objectContaining({
- 'http.method': 'POST',
- 'http.response.status_code': 200,
- 'http.status_code': 200,
- 'http.status_text': 'OK',
- 'http.target': '/performance/server-action.data',
- 'http.url': 'http://localhost:3030/performance/server-action.data',
- 'sentry.op': 'http.server',
- 'sentry.origin': 'auto.http.react_router.server',
- 'sentry.source': 'url',
- url: 'http://localhost:3030/performance/server-action.data',
- }),
- },
- }),
- transaction: 'POST /performance/server-action.data',
- type: 'transaction',
- transaction_info: { source: 'url' },
- platform: 'node',
- }),
- );
- // ensure we do not have a stray, bogus route attribute
- expect(transaction.contexts?.trace?.data?.['http.route']).not.toBeDefined();
-
- expect(transaction.spans).toContainEqual({
- span_id: expect.any(String),
- trace_id: expect.any(String),
- data: {
- 'sentry.op': 'function.react_router.action',
- 'sentry.origin': 'auto.http.react_router.server',
- },
- description: 'Executing Server Action',
- op: 'function.react_router.action',
- origin: 'auto.http.react_router.server',
- parent_span_id: expect.any(String),
- start_timestamp: expect.any(Number),
- status: 'ok',
- timestamp: expect.any(Number),
- });
- });
-});
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/performance/trace-propagation.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/performance/trace-propagation.test.ts
deleted file mode 100644
index e9b2c9409154..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tests/performance/trace-propagation.test.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import { expect, test } from '@playwright/test';
-import { waitForTransaction } from '@sentry-internal/test-utils';
-import { APP_NAME } from '../constants';
-
-test.describe('Trace propagation', () => {
- test('should inject metatags in ssr pageload', async ({ page }) => {
- await page.goto(`/`);
- const sentryTraceContent = await page.getAttribute('meta[name="sentry-trace"]', 'content');
- expect(sentryTraceContent).toBeDefined();
- expect(sentryTraceContent).toMatch(/^[a-f0-9]{32}-[a-f0-9]{16}-[01]$/);
- const baggageContent = await page.getAttribute('meta[name="baggage"]', 'content');
- expect(baggageContent).toBeDefined();
- expect(baggageContent).toContain('sentry-environment=qa');
- expect(baggageContent).toContain('sentry-public_key=');
- expect(baggageContent).toContain('sentry-trace_id=');
- expect(baggageContent).toContain('sentry-transaction=');
- expect(baggageContent).toContain('sentry-sampled=');
- });
-
- test('should have trace connection', async ({ page }) => {
- const serverTxPromise = waitForTransaction(APP_NAME, async transactionEvent => {
- return transactionEvent.transaction === 'GET *';
- });
-
- const clientTxPromise = waitForTransaction(APP_NAME, async transactionEvent => {
- return transactionEvent.transaction === '/';
- });
-
- await page.goto(`/`);
- const serverTx = await serverTxPromise;
- const clientTx = await clientTxPromise;
-
- expect(clientTx.contexts?.trace?.trace_id).toEqual(serverTx.contexts?.trace?.trace_id);
-
- const requestHandlerSpan = serverTx.spans?.find(span => span.op === 'request_handler.express');
-
- expect(requestHandlerSpan).toBeDefined();
- expect(clientTx.contexts?.trace?.parent_span_id).toBe(requestHandlerSpan?.span_id);
- });
-
- test('should not have trace connection for prerendered pages', async ({ page }) => {
- await page.goto('/performance/static');
-
- const sentryTraceElement = await page.$('meta[name="sentry-trace"]');
- expect(sentryTraceElement).toBeNull();
- });
-});
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tsconfig.json b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tsconfig.json
deleted file mode 100644
index a16df276e8bc..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/tsconfig.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "compilerOptions": {
- "lib": ["DOM", "DOM.Iterable", "ES2022"],
- "types": ["node", "vite/client"],
- "target": "ES2022",
- "module": "ES2022",
- "moduleResolution": "bundler",
- "jsx": "react-jsx",
- "rootDirs": [".", "./.react-router/types"],
- "baseUrl": ".",
-
- "esModuleInterop": true,
- "verbatimModuleSyntax": true,
- "noEmit": true,
- "resolveJsonModule": true,
- "skipLibCheck": true,
- "strict": true
- },
- "include": ["**/*", "**/.server/**/*", "**/.client/**/*", ".react-router/types/**/*"]
-}
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/vite.config.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/vite.config.ts
deleted file mode 100644
index 68ba30d69397..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/vite.config.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { reactRouter } from '@react-router/dev/vite';
-import { defineConfig } from 'vite';
-
-export default defineConfig({
- plugins: [reactRouter()],
-});
diff --git a/packages/react-router/src/server/instrumentation/reactRouter.ts b/packages/react-router/src/server/instrumentation/reactRouter.ts
deleted file mode 100644
index 41a60a9740bc..000000000000
--- a/packages/react-router/src/server/instrumentation/reactRouter.ts
+++ /dev/null
@@ -1,142 +0,0 @@
-import type { InstrumentationConfig } from '@opentelemetry/instrumentation';
-import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation';
-import { HTTP_TARGET } from '@sentry/conventions/attributes';
-import {
- debug,
- getActiveSpan,
- getRootSpan,
- SDK_VERSION,
- SEMANTIC_ATTRIBUTE_SENTRY_OP,
- SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
- SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
- spanToJSON,
- startSpan,
- updateSpanName,
-} from '@sentry/core';
-import type * as reactRouter from 'react-router';
-import { DEBUG_BUILD } from '../../common/debug-build';
-import { isServerBuildLike, setServerBuild } from '../serverBuild';
-import { isInstrumentationApiUsed, isOtelDataLoaderSpanCreationEnabled } from '../serverGlobals';
-import { getOpName, getSpanName, isDataRequest } from './util';
-
-type ReactRouterModuleExports = typeof reactRouter;
-
-const supportedVersions = ['>=7.0.0'];
-const COMPONENT = 'react-router';
-
-/**
- * Instrumentation for React Router's server request handler.
- * This patches the requestHandler function to add Sentry performance monitoring for data loaders.
- */
-export class ReactRouterInstrumentation extends InstrumentationBase {
- public constructor(config: InstrumentationConfig = {}) {
- super('ReactRouterInstrumentation', SDK_VERSION, config);
- }
-
- /**
- * Initializes the instrumentation by defining the React Router server modules to be patched.
- */
- // eslint-disable-next-line @typescript-eslint/naming-convention
- protected init(): InstrumentationNodeModuleDefinition {
- const reactRouterServerModule = new InstrumentationNodeModuleDefinition(
- COMPONENT,
- supportedVersions,
- (moduleExports: ReactRouterModuleExports) => {
- return this._createPatchedModuleProxy(moduleExports);
- },
- (_moduleExports: unknown) => {
- // nothing to unwrap here
- return _moduleExports;
- },
- );
-
- return reactRouterServerModule;
- }
-
- /**
- * Creates a proxy around the React Router module exports that patches the createRequestHandler function.
- * This allows us to wrap the request handler to add performance monitoring for data loaders and actions.
- */
- private _createPatchedModuleProxy(moduleExports: ReactRouterModuleExports): ReactRouterModuleExports {
- return new Proxy(moduleExports, {
- get(target, prop, receiver) {
- if (prop === 'createRequestHandler') {
- const original = target[prop];
- return function sentryWrappedCreateRequestHandler(this: unknown, ...args: unknown[]) {
- // Capture the ServerBuild reference for middleware name lookup
- const build = args[0];
- if (isServerBuildLike(build)) {
- setServerBuild(build);
- } else if (typeof build === 'function') {
- // Build arg can be a factory function (dev mode HMR). Wrap to capture resolved build.
- const originalBuildFn = build as () => unknown;
- args[0] = async function sentryWrappedBuildFn() {
- const resolvedBuild = await originalBuildFn();
- if (isServerBuildLike(resolvedBuild)) {
- setServerBuild(resolvedBuild);
- }
- return resolvedBuild;
- };
- }
-
- const originalRequestHandler = original.apply(this, args);
-
- return async function sentryWrappedRequestHandler(request: Request, initialContext?: unknown) {
- // Skip OTEL span creation when instrumentation API is active or when span creation is not enabled.
- // Checked per-request (not at handler-creation time) because in dev, createRequestHandler runs before entry.server.tsx.
- if (isInstrumentationApiUsed() || !isOtelDataLoaderSpanCreationEnabled()) {
- return originalRequestHandler(request, initialContext);
- }
-
- let url: URL;
- try {
- url = new URL(request.url);
- } catch {
- return originalRequestHandler(request, initialContext);
- }
-
- // We currently just want to trace loaders and actions
- if (!isDataRequest(url.pathname)) {
- return originalRequestHandler(request, initialContext);
- }
-
- const activeSpan = getActiveSpan();
- const rootSpan = activeSpan && getRootSpan(activeSpan);
-
- if (!rootSpan) {
- DEBUG_BUILD && debug.log('No active root span found, skipping tracing for data request');
- return originalRequestHandler(request, initialContext);
- }
-
- // We cannot rely on the regular span name inferral here, as the express instrumentation sets `*` as the route
- // So we force this to be a more sensible name here
- // TODO: try to set derived parameterized route from build here (args[0])
- const spanData = spanToJSON(rootSpan);
- // eslint-disable-next-line typescript/no-deprecated
- const target = spanData.data[HTTP_TARGET] || url.pathname;
- updateSpanName(rootSpan, `${request.method} ${target}`);
- rootSpan.setAttributes({
- [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
- [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.react_router.server',
- });
-
- return startSpan(
- {
- name: getSpanName(url.pathname, request.method),
- attributes: {
- [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.react_router.server',
- [SEMANTIC_ATTRIBUTE_SENTRY_OP]: getOpName(url.pathname, request.method),
- },
- },
- () => {
- return originalRequestHandler(request, initialContext);
- },
- );
- };
- };
- }
- return Reflect.get(target, prop, receiver);
- },
- });
- }
-}
diff --git a/packages/react-router/src/server/instrumentation/util.ts b/packages/react-router/src/server/instrumentation/util.ts
deleted file mode 100644
index 3cad321dcfcc..000000000000
--- a/packages/react-router/src/server/instrumentation/util.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-/**
- * Gets the op name for a request based on whether it's a loader or action request.
- * @param pathName The URL pathname to check
- * @param requestMethod The HTTP request method
- */
-export function getOpName(pathName: string, requestMethod: string): string {
- return isLoaderRequest(pathName, requestMethod)
- ? 'function.react_router.loader'
- : isActionRequest(pathName, requestMethod)
- ? 'function.react_router.action'
- : 'function.react_router';
-}
-
-/**
- * Gets the span name for a request based on whether it's a loader or action request.
- * @param pathName The URL pathname to check
- * @param requestMethod The HTTP request method
- */
-export function getSpanName(pathName: string, requestMethod: string): string {
- return isLoaderRequest(pathName, requestMethod)
- ? 'Executing Server Loader'
- : isActionRequest(pathName, requestMethod)
- ? 'Executing Server Action'
- : 'Unknown Data Request';
-}
-
-/**
- * Checks if the request is a server loader request
- * @param pathname The URL pathname to check
- * @param requestMethod The HTTP request method
- */
-export function isLoaderRequest(pathname: string, requestMethod: string): boolean {
- return isDataRequest(pathname) && requestMethod === 'GET';
-}
-
-/**
- * Checks if the request is a server action request
- * @param pathname The URL pathname to check
- * @param requestMethod The HTTP request method
- */
-export function isActionRequest(pathname: string, requestMethod: string): boolean {
- return isDataRequest(pathname) && requestMethod === 'POST';
-}
-
-/**
- * Checks if the request is a react-router data request
- * @param pathname The URL pathname to check
- */
-export function isDataRequest(pathname: string): boolean {
- return pathname.endsWith('.data');
-}
-
-export const SEMANTIC_ATTRIBUTE_SENTRY_OVERWRITE = 'sentry.overwrite-route';
diff --git a/packages/react-router/src/server/integration/reactRouterServer.ts b/packages/react-router/src/server/integration/reactRouterServer.ts
index 2558f8229a58..8f43009ca3d9 100644
--- a/packages/react-router/src/server/integration/reactRouterServer.ts
+++ b/packages/react-router/src/server/integration/reactRouterServer.ts
@@ -1,23 +1,8 @@
-import { HTTP_ROUTE } from '@sentry/conventions/attributes';
-import { defineIntegration, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
-import { generateInstrumentOnce, NODE_VERSION } from '@sentry/node';
-import { ReactRouterInstrumentation } from '../instrumentation/reactRouter';
+import { defineIntegration } from '@sentry/core';
import { registerServerBuildGlobal } from '../serverBuild';
-import { enableOtelDataLoaderSpanCreation } from '../serverGlobals';
const INTEGRATION_NAME = 'ReactRouterServer' as const;
-const instrumentReactRouter = generateInstrumentOnce(INTEGRATION_NAME, () => {
- return new ReactRouterInstrumentation();
-});
-
-export const instrumentReactRouterServer = Object.assign(
- (): void => {
- instrumentReactRouter();
- },
- { id: INTEGRATION_NAME },
-);
-
/**
* Integration capturing tracing data for React Router server functions.
*/
@@ -25,60 +10,8 @@ export const reactRouterServerIntegration = defineIntegration(() => {
return {
name: INTEGRATION_NAME,
setupOnce() {
- // Register global for Vite plugin ServerBuild capture. Registered independently of the OTEL
- // patch so this capture path keeps working once the OTEL instrumentation is removed.
+ // Register global for Vite plugin ServerBuild capture (used for middleware name resolution).
registerServerBuildGlobal();
-
- // Enable OTEL data-loader spans only on Node versions without the diagnostics_channel-based instrumentation API.
- if (
- (NODE_VERSION.major === 20 && NODE_VERSION.minor < 19) ||
- (NODE_VERSION.major === 22 && NODE_VERSION.minor < 12)
- ) {
- enableOtelDataLoaderSpanCreation();
- }
-
- // Always install to capture ServerBuild for middleware names.
- // Skips per-request wrapping when instrumentation API is active or OTEL span creation is disabled.
- instrumentReactRouterServer();
- },
- processEvent(event) {
- // Express generates bogus `*` routes for data loaders, which we want to remove here
- // we cannot do this earlier because some OTEL instrumentation adds this at some unexpected point
- if (
- event.type === 'transaction' &&
- event.contexts?.trace?.data &&
- event.contexts.trace.data[HTTP_ROUTE] === '*'
- ) {
- const origin = event.contexts.trace.origin;
- const isInstrumentationApiOrigin = origin?.includes('instrumentation_api');
-
- // For instrumentation_api, always clean up bogus `*` route since we set better names
- // For legacy, only clean up if the name has been adjusted (not METHOD *)
- if (isInstrumentationApiOrigin || !event.transaction?.endsWith(' *')) {
- // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
- delete event.contexts.trace.data[HTTP_ROUTE];
- }
- }
-
- return event;
- },
- processSegmentSpan(span) {
- // Express generates bogus `*` routes for data loaders, which we want to remove here
- // we cannot do this earlier because some OTEL instrumentation adds this at some unexpected point
- const attributes = span.attributes;
- if (attributes?.[HTTP_ROUTE] !== '*') {
- return;
- }
-
- const origin = attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN];
- const isInstrumentationApiOrigin = typeof origin === 'string' && origin.includes('instrumentation_api');
-
- // For instrumentation_api, always clean up bogus `*` route since we set better names
- // For legacy, only clean up if the name has been adjusted (not METHOD *)
- if (isInstrumentationApiOrigin || !span.name?.endsWith(' *')) {
- // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
- delete attributes[HTTP_ROUTE];
- }
},
};
});
diff --git a/packages/react-router/src/server/serverGlobals.ts b/packages/react-router/src/server/serverGlobals.ts
index e7a7ce019442..f177c6a9ab6a 100644
--- a/packages/react-router/src/server/serverGlobals.ts
+++ b/packages/react-router/src/server/serverGlobals.ts
@@ -1,11 +1,9 @@
import { GLOBAL_OBJ } from '@sentry/core';
const SENTRY_SERVER_INSTRUMENTATION_FLAG = '__sentryReactRouterServerInstrumentationUsed';
-const SENTRY_OTEL_SPAN_CREATION_FLAG = '__sentryReactRouterOtelSpanCreationEnabled';
type GlobalObjWithFlag = typeof GLOBAL_OBJ & {
[SENTRY_SERVER_INSTRUMENTATION_FLAG]?: boolean;
- [SENTRY_OTEL_SPAN_CREATION_FLAG]?: boolean;
};
/**
@@ -22,19 +20,3 @@ export function markInstrumentationApiUsed(): void {
export function isInstrumentationApiUsed(): boolean {
return !!(GLOBAL_OBJ as GlobalObjWithFlag)[SENTRY_SERVER_INSTRUMENTATION_FLAG];
}
-
-/**
- * Enable OTEL data-loader span creation for React Router server.
- * @internal
- */
-export function enableOtelDataLoaderSpanCreation(): void {
- (GLOBAL_OBJ as GlobalObjWithFlag)[SENTRY_OTEL_SPAN_CREATION_FLAG] = true;
-}
-
-/**
- * Check if OTEL data-loader span creation is enabled for React Router server.
- * @internal
- */
-export function isOtelDataLoaderSpanCreationEnabled(): boolean {
- return !!(GLOBAL_OBJ as GlobalObjWithFlag)[SENTRY_OTEL_SPAN_CREATION_FLAG];
-}
diff --git a/packages/react-router/src/vite/makeServerBuildCapturePlugin.ts b/packages/react-router/src/vite/makeServerBuildCapturePlugin.ts
index e7d081306bfb..d95129a491ff 100644
--- a/packages/react-router/src/vite/makeServerBuildCapturePlugin.ts
+++ b/packages/react-router/src/vite/makeServerBuildCapturePlugin.ts
@@ -18,6 +18,9 @@ export function makeServerBuildCapturePlugin(): Plugin {
},
transform(code, id) {
+ // TODO: This only captures the server build for production SSR builds. Dev mode
+ // (`react-router dev`) is not covered yet, so middleware names may be missing there - this
+ // should be handled for dev too.
if (!isSsrBuild) {
return null;
}
diff --git a/packages/react-router/test/server/createServerInstrumentation.test.ts b/packages/react-router/test/server/createServerInstrumentation.test.ts
index 6762be8e4e83..37143cb66a26 100644
--- a/packages/react-router/test/server/createServerInstrumentation.test.ts
+++ b/packages/react-router/test/server/createServerInstrumentation.test.ts
@@ -68,9 +68,8 @@ describe('createSentryServerInstrumentation', () => {
createSentryServerInstrumentation();
- // Creating the instrumentation must not mark the API active. On React Router versions that
- // don't support the instrumentations API, the registration callbacks are never invoked, so
- // the legacy OTel data-loader path must stay active.
+ // Creating the instrumentation must not mark the API active - the flag should only flip once
+ // React Router actually invokes the registration callbacks.
expect((globalThis as any).__sentryReactRouterServerInstrumentationUsed).toBeUndefined();
});
diff --git a/packages/react-router/test/server/instrumentation/reactRouterServer.test.ts b/packages/react-router/test/server/instrumentation/reactRouterServer.test.ts
deleted file mode 100644
index f9b9d88c1a2b..000000000000
--- a/packages/react-router/test/server/instrumentation/reactRouterServer.test.ts
+++ /dev/null
@@ -1,190 +0,0 @@
-import type { Span, SpanJSON } from '@sentry/core';
-import * as SentryCore from '@sentry/core';
-import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
-import { ReactRouterInstrumentation } from '../../../src/server/instrumentation/reactRouter';
-import * as Util from '../../../src/server/instrumentation/util';
-import * as ServerBuild from '../../../src/server/serverBuild';
-import * as ServerGlobals from '../../../src/server/serverGlobals';
-
-vi.mock('@sentry/core', async () => {
- return {
- getActiveSpan: vi.fn(),
- getRootSpan: vi.fn(),
- spanToJSON: vi.fn(),
- updateSpanName: vi.fn(),
- debug: {
- log: vi.fn(),
- },
- SDK_VERSION: '1.0.0',
- SEMANTIC_ATTRIBUTE_SENTRY_OP: 'sentry.op',
- SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN: 'sentry.origin',
- SEMANTIC_ATTRIBUTE_SENTRY_SOURCE: 'sentry.source',
- startSpan: vi.fn((opts, fn) => fn({})),
- GLOBAL_OBJ: {},
- };
-});
-
-vi.mock('./util', async () => {
- return {
- getSpanName: vi.fn((pathname: string, method: string) => `span:${pathname}:${method}`),
- isDataRequest: vi.fn(),
- };
-});
-
-const mockSpan = {
- spanContext: () => ({ traceId: '1', spanId: '2', traceFlags: 1 }),
- setAttributes: vi.fn(),
-};
-
-function createRequest(url: string, method = 'GET') {
- return { url, method } as unknown as Request;
-}
-
-describe('ReactRouterInstrumentation', () => {
- let instrumentation: ReactRouterInstrumentation;
- let mockModule: any;
- let originalHandler: any;
-
- beforeEach(() => {
- instrumentation = new ReactRouterInstrumentation();
- originalHandler = vi.fn();
- mockModule = {
- createRequestHandler: vi.fn(() => originalHandler),
- };
- vi.clearAllMocks();
- });
-
- afterEach(() => {
- vi.restoreAllMocks();
- });
-
- it('should patch createRequestHandler', () => {
- const proxy = (instrumentation as any)._createPatchedModuleProxy(mockModule);
- expect(typeof proxy.createRequestHandler).toBe('function');
- expect(proxy.createRequestHandler).not.toBe(mockModule.createRequestHandler);
- });
-
- it('should call original handler for non-data requests', async () => {
- vi.spyOn(ServerGlobals, 'isOtelDataLoaderSpanCreationEnabled').mockReturnValue(true);
- vi.spyOn(Util, 'isDataRequest').mockReturnValue(false);
-
- const proxy = (instrumentation as any)._createPatchedModuleProxy(mockModule);
- const wrappedHandler = proxy.createRequestHandler();
- const req = createRequest('https://test.com/page');
- await wrappedHandler(req);
-
- expect(Util.isDataRequest).toHaveBeenCalledWith('/page');
- expect(originalHandler).toHaveBeenCalledWith(req, undefined);
- });
-
- it('should call original handler if no active root span', async () => {
- vi.spyOn(ServerGlobals, 'isOtelDataLoaderSpanCreationEnabled').mockReturnValue(true);
- vi.spyOn(Util, 'isDataRequest').mockReturnValue(true);
- vi.spyOn(SentryCore, 'getActiveSpan').mockReturnValue(undefined);
-
- const proxy = (instrumentation as any)._createPatchedModuleProxy(mockModule);
- const wrappedHandler = proxy.createRequestHandler();
- const req = createRequest('https://test.com/data');
- await wrappedHandler(req);
-
- expect(SentryCore.debug.log).toHaveBeenCalledWith('No active root span found, skipping tracing for data request');
- expect(originalHandler).toHaveBeenCalledWith(req, undefined);
- });
-
- it('should start a span for data requests with active root span', async () => {
- vi.spyOn(ServerGlobals, 'isOtelDataLoaderSpanCreationEnabled').mockReturnValue(true);
- vi.spyOn(Util, 'isDataRequest').mockReturnValue(true);
- // @ts-expect-error MockSpan just for testing
- vi.spyOn(SentryCore, 'getActiveSpan').mockReturnValue(mockSpan as Span);
- // @ts-expect-error MockSpan just for testing
- vi.spyOn(SentryCore, 'getRootSpan').mockReturnValue(mockSpan as Span);
- vi.spyOn(SentryCore, 'spanToJSON').mockReturnValue({ data: {} } as SpanJSON);
- vi.spyOn(Util, 'getSpanName').mockImplementation((pathname, method) => `span:${pathname}:${method}`);
- // @ts-expect-error MockSpan just for testing
- vi.spyOn(SentryCore, 'startSpan').mockImplementation((_opts, fn) => fn(mockSpan as Span));
-
- const proxy = (instrumentation as any)._createPatchedModuleProxy(mockModule);
- const wrappedHandler = proxy.createRequestHandler();
- const req = createRequest('https://test.com/data', 'POST');
- await wrappedHandler(req);
-
- expect(Util.isDataRequest).toHaveBeenCalledWith('/data');
- expect(Util.getSpanName).toHaveBeenCalledWith('/data', 'POST');
- expect(SentryCore.startSpan).toHaveBeenCalled();
- expect(originalHandler).toHaveBeenCalledWith(req, undefined);
- });
-
- it('should handle invalid URLs gracefully', async () => {
- vi.spyOn(ServerGlobals, 'isOtelDataLoaderSpanCreationEnabled').mockReturnValue(true);
- const proxy = (instrumentation as any)._createPatchedModuleProxy(mockModule);
- const wrappedHandler = proxy.createRequestHandler();
- const req = { url: 'not a url', method: 'GET' } as any;
- await wrappedHandler(req);
-
- expect(originalHandler).toHaveBeenCalledWith(req, undefined);
- });
-
- it('should call setServerBuild when static ServerBuild is passed', () => {
- const spy = vi.spyOn(ServerBuild, 'setServerBuild');
- vi.spyOn(ServerBuild, 'isServerBuildLike').mockReturnValue(true);
-
- const staticBuild = { routes: { root: { id: 'root' } } };
- const proxy = (instrumentation as any)._createPatchedModuleProxy(mockModule);
- proxy.createRequestHandler(staticBuild);
-
- expect(spy).toHaveBeenCalledWith(staticBuild);
- });
-
- it('should capture ServerBuild from factory function', async () => {
- const resolvedBuild = { routes: { root: { id: 'root' } } };
- const buildFactory = vi.fn().mockResolvedValue(resolvedBuild);
- vi.spyOn(ServerBuild, 'isServerBuildLike').mockImplementation(val => val === resolvedBuild);
- const spy = vi.spyOn(ServerBuild, 'setServerBuild');
-
- const proxy = (instrumentation as any)._createPatchedModuleProxy(mockModule);
- proxy.createRequestHandler(buildFactory);
-
- // Factory gets wrapped — invoke it via the arg passed to the original createRequestHandler
- const wrappedFactory = mockModule.createRequestHandler.mock.calls[0][0];
- await wrappedFactory();
-
- expect(spy).toHaveBeenCalledWith(resolvedBuild);
- });
-
- it('should bypass instrumentation when instrumentation API is active', async () => {
- vi.spyOn(ServerGlobals, 'isInstrumentationApiUsed').mockReturnValue(true);
- vi.spyOn(ServerGlobals, 'isOtelDataLoaderSpanCreationEnabled').mockReturnValue(true);
- vi.spyOn(Util, 'isDataRequest').mockReturnValue(true);
- const startSpanSpy = vi.spyOn(SentryCore, 'startSpan');
-
- const proxy = (instrumentation as any)._createPatchedModuleProxy(mockModule);
- const handler = proxy.createRequestHandler();
-
- // Handler is always wrapped; the instrumentation API check happens per-request
- expect(handler).not.toBe(originalHandler);
-
- const req = createRequest('https://test.com/data', 'GET');
- await handler(req);
-
- // Should delegate to original handler without creating spans
- expect(originalHandler).toHaveBeenCalledWith(req, undefined);
- expect(startSpanSpy).not.toHaveBeenCalled();
- });
-
- it('should skip span creation when OTEL data-loader span creation is disabled', async () => {
- vi.spyOn(ServerGlobals, 'isInstrumentationApiUsed').mockReturnValue(false);
- vi.spyOn(ServerGlobals, 'isOtelDataLoaderSpanCreationEnabled').mockReturnValue(false);
- vi.spyOn(Util, 'isDataRequest').mockReturnValue(true);
- const startSpanSpy = vi.spyOn(SentryCore, 'startSpan');
-
- const proxy = (instrumentation as any)._createPatchedModuleProxy(mockModule);
- const handler = proxy.createRequestHandler();
-
- const req = createRequest('https://test.com/data', 'GET');
- await handler(req);
-
- // Should delegate to original handler without creating spans
- expect(originalHandler).toHaveBeenCalledWith(req, undefined);
- expect(startSpanSpy).not.toHaveBeenCalled();
- });
-});
diff --git a/packages/react-router/test/server/integration/reactRouterServer.test.ts b/packages/react-router/test/server/integration/reactRouterServer.test.ts
index fd9dc2995119..932ee8452f23 100644
--- a/packages/react-router/test/server/integration/reactRouterServer.test.ts
+++ b/packages/react-router/test/server/integration/reactRouterServer.test.ts
@@ -1,243 +1,23 @@
-import { HTTP_ROUTE } from '@sentry/conventions/attributes';
-import type { Client, Event, EventType, StreamedSpanJSON } from '@sentry/core';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
-import { ReactRouterInstrumentation } from '../../../src/server/instrumentation/reactRouter';
-import {
- instrumentReactRouterServer,
- reactRouterServerIntegration,
-} from '../../../src/server/integration/reactRouterServer';
+import { reactRouterServerIntegration } from '../../../src/server/integration/reactRouterServer';
import * as serverBuild from '../../../src/server/serverBuild';
-import * as serverGlobals from '../../../src/server/serverGlobals';
-
-vi.mock('../../../src/server/instrumentation/reactRouter', () => {
- return {
- ReactRouterInstrumentation: vi.fn(),
- };
-});
-
-const mockNodeVersion = { major: 20, minor: 18, patch: 0 };
-
-vi.mock('@sentry/node', () => {
- return {
- generateInstrumentOnce: vi.fn((_name: string, callback: () => any) => {
- return Object.assign(callback, { id: 'test' });
- }),
- get NODE_VERSION() {
- return mockNodeVersion;
- },
- };
-});
describe('reactRouterServerIntegration', () => {
let registerServerBuildGlobalSpy: ReturnType;
- let enableOtelDataLoaderSpanCreationSpy: ReturnType;
beforeEach(() => {
vi.clearAllMocks();
registerServerBuildGlobalSpy = vi.spyOn(serverBuild, 'registerServerBuildGlobal');
- enableOtelDataLoaderSpanCreationSpy = vi.spyOn(serverGlobals, 'enableOtelDataLoaderSpanCreation');
});
afterEach(() => {
vi.restoreAllMocks();
});
- it('sets up ReactRouterInstrumentation on setupOnce', () => {
- const integration = reactRouterServerIntegration();
- integration.setupOnce!();
-
- expect(ReactRouterInstrumentation).toHaveBeenCalledTimes(1);
- });
-
it('registers the server build global callback on setupOnce', () => {
const integration = reactRouterServerIntegration();
integration.setupOnce!();
expect(registerServerBuildGlobalSpy).toHaveBeenCalledTimes(1);
});
-
- it('does not register the server build global from the OTEL instrumentation setup', () => {
- // Guards against re-coupling: the Vite-plugin capture registration must not depend on the
- // OTEL patch being installed, so it survives once the OTEL instrumentation is removed.
- instrumentReactRouterServer();
-
- expect(registerServerBuildGlobalSpy).not.toHaveBeenCalled();
- });
-
- it('enables OTEL data-loader span creation on Node 20.18', () => {
- mockNodeVersion.major = 20;
- mockNodeVersion.minor = 18;
-
- const integration = reactRouterServerIntegration();
- integration.setupOnce!();
-
- expect(enableOtelDataLoaderSpanCreationSpy).toHaveBeenCalledTimes(1);
- expect(ReactRouterInstrumentation).toHaveBeenCalledTimes(1);
- expect(registerServerBuildGlobalSpy).toHaveBeenCalledTimes(1);
- });
-
- it('enables OTEL data-loader span creation on Node 22.11', () => {
- mockNodeVersion.major = 22;
- mockNodeVersion.minor = 11;
-
- const integration = reactRouterServerIntegration();
- integration.setupOnce!();
-
- expect(enableOtelDataLoaderSpanCreationSpy).toHaveBeenCalledTimes(1);
- expect(ReactRouterInstrumentation).toHaveBeenCalledTimes(1);
- expect(registerServerBuildGlobalSpy).toHaveBeenCalledTimes(1);
- });
-
- it('does not enable OTEL data-loader span creation on Node 20.19', () => {
- mockNodeVersion.major = 20;
- mockNodeVersion.minor = 19;
-
- const integration = reactRouterServerIntegration();
- integration.setupOnce!();
-
- expect(enableOtelDataLoaderSpanCreationSpy).not.toHaveBeenCalled();
- expect(ReactRouterInstrumentation).toHaveBeenCalledTimes(1);
- expect(registerServerBuildGlobalSpy).toHaveBeenCalledTimes(1);
- });
-
- it('does not enable OTEL data-loader span creation on Node 22.12', () => {
- mockNodeVersion.major = 22;
- mockNodeVersion.minor = 12;
-
- const integration = reactRouterServerIntegration();
- integration.setupOnce!();
-
- expect(enableOtelDataLoaderSpanCreationSpy).not.toHaveBeenCalled();
- expect(ReactRouterInstrumentation).toHaveBeenCalledTimes(1);
- expect(registerServerBuildGlobalSpy).toHaveBeenCalledTimes(1);
- });
-
- describe('processEvent', () => {
- const client = {} as Client;
- const hint = {};
-
- it('preserves http.route when it is not "*"', () => {
- const integration = reactRouterServerIntegration();
- const event = {
- type: 'transaction' as EventType,
- transaction: 'GET /users/:id',
- contexts: {
- trace: {
- data: { [HTTP_ROUTE]: '/users/:id' },
- origin: 'auto.http.otel.http',
- },
- },
- } as unknown as Event;
-
- integration.processEvent!(event, hint, client);
-
- expect(event.contexts?.trace?.data?.[HTTP_ROUTE]).toBe('/users/:id');
- });
-
- it('deletes bogus "*" route when origin is instrumentation_api', () => {
- const integration = reactRouterServerIntegration();
- const event = {
- type: 'transaction' as EventType,
- transaction: 'GET *',
- contexts: {
- trace: {
- data: { [HTTP_ROUTE]: '*' },
- origin: 'auto.http.otel.instrumentation_api',
- },
- },
- } as unknown as Event;
-
- integration.processEvent!(event, hint, client);
-
- expect(event.contexts?.trace?.data?.[HTTP_ROUTE]).toBeUndefined();
- });
-
- it('deletes bogus "*" route when legacy origin and transaction name was renamed', () => {
- const integration = reactRouterServerIntegration();
- const event = {
- type: 'transaction' as EventType,
- transaction: 'GET /api/users',
- contexts: {
- trace: {
- data: { [HTTP_ROUTE]: '*' },
- origin: 'auto.http.otel.http',
- },
- },
- } as unknown as Event;
-
- integration.processEvent!(event, hint, client);
-
- expect(event.contexts?.trace?.data?.[HTTP_ROUTE]).toBeUndefined();
- });
-
- it('keeps "*" when legacy origin and transaction name still ends with " *"', () => {
- const integration = reactRouterServerIntegration();
- const event = {
- type: 'transaction' as EventType,
- transaction: 'GET *',
- contexts: {
- trace: {
- data: { [HTTP_ROUTE]: '*' },
- origin: 'auto.http.otel.http',
- },
- },
- } as unknown as Event;
-
- integration.processEvent!(event, hint, client);
-
- expect(event.contexts?.trace?.data?.[HTTP_ROUTE]).toBe('*');
- });
- });
-
- describe('processSegmentSpan', () => {
- const client = {} as Client;
-
- it('preserves http.route when it is not "*"', () => {
- const integration = reactRouterServerIntegration();
- const span = {
- name: 'GET /users/:id',
- attributes: { [HTTP_ROUTE]: '/users/:id', 'sentry.origin': 'auto.http.otel.http' },
- } as unknown as StreamedSpanJSON;
-
- integration.processSegmentSpan!(span, client);
-
- expect(span.attributes?.[HTTP_ROUTE]).toBe('/users/:id');
- });
-
- it('deletes bogus "*" route when origin is instrumentation_api', () => {
- const integration = reactRouterServerIntegration();
- const span = {
- name: 'GET *',
- attributes: { [HTTP_ROUTE]: '*', 'sentry.origin': 'auto.http.otel.instrumentation_api' },
- } as unknown as StreamedSpanJSON;
-
- integration.processSegmentSpan!(span, client);
-
- expect(span.attributes?.[HTTP_ROUTE]).toBeUndefined();
- });
-
- it('deletes bogus "*" route when legacy origin and span name was renamed', () => {
- const integration = reactRouterServerIntegration();
- const span = {
- name: 'GET /api/users',
- attributes: { [HTTP_ROUTE]: '*', 'sentry.origin': 'auto.http.otel.http' },
- } as unknown as StreamedSpanJSON;
-
- integration.processSegmentSpan!(span, client);
-
- expect(span.attributes?.[HTTP_ROUTE]).toBeUndefined();
- });
-
- it('keeps "*" when legacy origin and span name still ends with " *"', () => {
- const integration = reactRouterServerIntegration();
- const span = {
- name: 'GET *',
- attributes: { [HTTP_ROUTE]: '*', 'sentry.origin': 'auto.http.otel.http' },
- } as unknown as StreamedSpanJSON;
-
- integration.processSegmentSpan!(span, client);
-
- expect(span.attributes?.[HTTP_ROUTE]).toBe('*');
- });
- });
});
diff --git a/scripts/report-ci-failures.mjs b/scripts/report-ci-failures.mjs
index d464ffa11985..e87a38b7a824 100644
--- a/scripts/report-ci-failures.mjs
+++ b/scripts/report-ci-failures.mjs
@@ -29,7 +29,7 @@ import { readFileSync } from 'node:fs';
* "aws-serverless-layer (Node 22) Test" -> "aws-serverless-layer Test"
* "Playwright bundle_tracing_replay Tests" -> "Playwright Tests"
* "Playwright esm (1/4) Tests" -> "Playwright Tests"
- * "E2E react-router-7-framework-node-20-18 Test" -> "E2E react-router-7-framework Test"
+ * "E2E react-router-7-framework-spa-node-20-18 Test" -> "E2E react-router-7-framework-spa Test"
*/
function normalizeJobName(name) {
return name