Skip to content

Commit 28ab460

Browse files
mydeaclaude
andcommitted
test(e2e): Add @sentry/react/router e2e apps for React Router v6 and v8
Add `react-router-6-router-entry` (React 18) and `react-router-8-router-entry` (React 19) alongside the existing v7 app, all configured purely through `@sentry/react/router` (zero-arg integration + `wrapReactRouterRouting`). Each has the full span/error/navigation-trace-propagation suite and runs `tsc --noEmit` in `test:assert` so CI verifies the `@sentry/react/router` types match against each React Router major (tsconfig uses `moduleResolution: bundler` so the subpath's exports-map types resolve). The v6 app depends only on `react-router-dom` (not `react-router` directly) - this is the common real-world v6 setup and exercises that the entry's `react-router` import still resolves via the copy `react-router-dom` pulls in. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MjLdAGt9CHRbbJyBSCnduV
1 parent 127deb5 commit 28ab460

30 files changed

Lines changed: 752 additions & 0 deletions
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
8+
# testing
9+
/coverage
10+
11+
# production
12+
/build
13+
14+
# misc
15+
.DS_Store
16+
.env.local
17+
.env.development.local
18+
.env.test.local
19+
.env.production.local
20+
21+
npm-debug.log*
22+
yarn-debug.log*
23+
yarn-error.log*
24+
25+
/test-results/
26+
/playwright-report/
27+
/playwright/.cache/
28+
29+
!*.d.ts
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title>Vite + React + TS</title>
8+
</head>
9+
<body>
10+
<div id="root"></div>
11+
<script type="module" src="/src/main.tsx"></script>
12+
</body>
13+
</html>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
{
2+
"name": "react-router-6-router-entry",
3+
"version": "0.1.0",
4+
"private": true,
5+
"dependencies": {
6+
"@sentry/react": "file:../../packed/sentry-react-packed.tgz",
7+
"@types/react": "18.3.1",
8+
"@types/react-dom": "18.3.1",
9+
"react": "18.3.1",
10+
"react-dom": "18.3.1",
11+
"react-router-dom": "^6.30.0"
12+
},
13+
"devDependencies": {
14+
"@playwright/test": "~1.56.0",
15+
"@sentry-internal/test-utils": "link:../../../test-utils",
16+
"vite": "^6.4.2",
17+
"@vitejs/plugin-react": "^4.3.4",
18+
"typescript": "~5.0.0"
19+
},
20+
"scripts": {
21+
"build": "vite build",
22+
"dev": "vite",
23+
"preview": "vite preview",
24+
"test": "playwright test",
25+
"clean": "npx rimraf node_modules pnpm-lock.yaml",
26+
"typecheck": "tsc --noEmit",
27+
"test:build": "pnpm install && pnpm build",
28+
"test:build-canary": "pnpm install && pnpm add react@canary react-dom@canary && pnpm build",
29+
"test:assert": "pnpm typecheck && pnpm test"
30+
},
31+
"eslintConfig": {
32+
"extends": [
33+
"react-app",
34+
"react-app/jest"
35+
]
36+
},
37+
"browserslist": {
38+
"production": [
39+
">0.2%",
40+
"not dead",
41+
"not op_mini all"
42+
],
43+
"development": [
44+
"last 1 chrome version",
45+
"last 1 firefox version",
46+
"last 1 safari version"
47+
]
48+
},
49+
"volta": {
50+
"extends": "../../package.json"
51+
}
52+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { getPlaywrightConfig } from '@sentry-internal/test-utils';
2+
3+
const config = getPlaywrightConfig({
4+
startCommand: `pnpm preview --port 3030`,
5+
port: 3030,
6+
});
7+
8+
export default config;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
interface Window {
2+
recordedTransactions?: string[];
3+
capturedExceptionId?: string;
4+
sentryReplayId?: string;
5+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import * as Sentry from '@sentry/react';
2+
// The `@sentry/react/router` entry pulls the required router hooks from `react-router` itself, so
3+
// `reactRouterBrowserTracingIntegration()` needs no arguments. On React Router v6 the DOM bindings
4+
// (`BrowserRouter`, `Link`) come from `react-router-dom`. Note this app depends only on
5+
// `react-router-dom` (not `react-router` directly) - the entry's `react-router` import still resolves
6+
// via the copy `react-router-dom` pulls in, which is the common real-world v6 setup.
7+
import { reactRouterBrowserTracingIntegration, wrapReactRouterRouting } from '@sentry/react/router';
8+
import * as React from 'react';
9+
import ReactDOM from 'react-dom/client';
10+
import { BrowserRouter, Route, Routes } from 'react-router-dom';
11+
import Index from './pages/Index';
12+
import Products from './pages/Products';
13+
import User from './pages/User';
14+
15+
Sentry.init({
16+
environment: 'qa', // dynamic sampling bias to keep transactions
17+
dsn: import.meta.env.PUBLIC_E2E_TEST_DSN,
18+
integrations: [reactRouterBrowserTracingIntegration()],
19+
tracesSampleRate: 1.0,
20+
release: 'e2e-test',
21+
tunnel: 'http://localhost:3031',
22+
});
23+
24+
const SentryRoutes = wrapReactRouterRouting(Routes);
25+
26+
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
27+
root.render(
28+
<BrowserRouter>
29+
<SentryRoutes>
30+
<Route path="/" element={<Index />} />
31+
<Route path="/user/:id" element={<User />} />
32+
<Route path="/products" element={<Products />} />
33+
</SentryRoutes>
34+
</BrowserRouter>,
35+
);
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import * as React from 'react';
2+
import { Link } from 'react-router-dom';
3+
4+
const Index = () => {
5+
return (
6+
<>
7+
<input
8+
type="button"
9+
value="Capture Exception"
10+
id="exception-button"
11+
onClick={() => {
12+
throw new Error('I am an error!');
13+
}}
14+
/>
15+
<Link to="/user/5" id="navigation">
16+
navigate
17+
</Link>
18+
<Link to="/products" id="navigation-products">
19+
products
20+
</Link>
21+
</>
22+
);
23+
};
24+
25+
export default Index;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import * as React from 'react';
2+
3+
const Products = () => {
4+
// Fired on mount, i.e. while navigating to /products. This mirrors a typical
5+
// route component that loads its data in an effect. The request is same-origin,
6+
// so the SDK attaches `sentry-trace`/`baggage` headers by default.
7+
React.useEffect(() => {
8+
fetch('/api/products').catch(() => {
9+
// ignore network errors in the test environment
10+
});
11+
}, []);
12+
13+
return <div id="products">Products</div>;
14+
};
15+
16+
export default Products;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import * as React from 'react';
2+
3+
const User = () => {
4+
return <p>I am a blank page :)</p>;
5+
};
6+
7+
export default User;
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { startEventProxyServer } from '@sentry-internal/test-utils';
2+
3+
startEventProxyServer({
4+
port: 3031,
5+
proxyServerName: 'react-router-6-router-entry',
6+
});

0 commit comments

Comments
 (0)