Skip to content

Commit 5808986

Browse files
authored
fix(react): Add support for cross-usage of React Router instrumentations (#15283)
This started to be an issue after we added support for descendant React Router routes. React Router does not limit users to only one way to declare routes. Currently, we provide instrumentation for 3 ways of declaring RR routes: | React Router v6/v7 | Sentry | |-----------------------|----------------------------------| | `useRoutes` | `Sentry.wrapUseRoutes` | | `<Routes />` | `Sentry.withReactRouterRouting` | | `createBrowserRouter` | `Sentry.wrapCreateBrowserRouter` | | `createHashRouter` | `Sentry.wrapCreateBrowserRouter` | | `createMemoryRouter` | `Sentry.wrapCreateMemoryRouter` | When users decide to declare their routes using more than one of these options together, we risk losing context while rebuilding parameterised span names. This PR adds: - Proper support and context sharing for cross-usage of route declarations - E2E tests for pairs of 2 and all three possible methods used together to declare descendant routes. - Adds another recursion break for the path rebuild method for cases when the rebuilt path has not changed in a deeper recursive call. (This fixes #15279, together with the updates mentioned above)
1 parent 977c035 commit 5808986

File tree

14 files changed

+1254
-29
lines changed

14 files changed

+1254
-29
lines changed
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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@sentry:registry=http://127.0.0.1:4873
2+
@sentry-internal:registry=http://127.0.0.1:4873
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
{
2+
"name": "react-router-7-cross-usage",
3+
"version": "0.1.0",
4+
"private": true,
5+
"dependencies": {
6+
"@sentry/react": "latest || *",
7+
"@types/react": "18.0.0",
8+
"@types/react-dom": "18.0.0",
9+
"express": "4.20.0",
10+
"react": "18.2.0",
11+
"react-dom": "18.2.0",
12+
"react-router-dom": "^7.2.0",
13+
"react-scripts": "5.0.1",
14+
"typescript": "~5.0.0"
15+
},
16+
"scripts": {
17+
"build": "react-scripts build",
18+
"start": "serve -s build",
19+
"test": "playwright test",
20+
"clean": "npx rimraf node_modules pnpm-lock.yaml",
21+
"test:build": "pnpm install && npx playwright install && pnpm build",
22+
"test:build-ts3.8": "pnpm install && pnpm add typescript@3.8 && npx playwright install && pnpm build",
23+
"test:build-canary": "pnpm install && pnpm add react@canary react-dom@canary && npx playwright install && pnpm build",
24+
"test:assert": "pnpm test"
25+
},
26+
"eslintConfig": {
27+
"extends": [
28+
"react-app",
29+
"react-app/jest"
30+
]
31+
},
32+
"browserslist": {
33+
"production": [
34+
">0.2%",
35+
"not dead",
36+
"not op_mini all"
37+
],
38+
"development": [
39+
"last 1 chrome version",
40+
"last 1 firefox version",
41+
"last 1 safari version"
42+
]
43+
},
44+
"devDependencies": {
45+
"@playwright/test": "~1.50.0",
46+
"@sentry-internal/test-utils": "link:../../../test-utils",
47+
"serve": "14.0.1",
48+
"npm-run-all2": "^6.2.0"
49+
},
50+
"volta": {
51+
"extends": "../../package.json"
52+
}
53+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { getPlaywrightConfig } from '@sentry-internal/test-utils';
2+
3+
const config = getPlaywrightConfig({
4+
startCommand: `pnpm start`,
5+
});
6+
7+
export default config;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1" />
6+
<meta name="theme-color" content="#000000" />
7+
<meta name="description" content="Web site created using create-react-app" />
8+
<title>React App</title>
9+
</head>
10+
<body>
11+
<noscript>You need to enable JavaScript to run this app.</noscript>
12+
<div id="root"></div>
13+
<!--
14+
This HTML file is a template.
15+
If you open it directly in the browser, you will see an empty page.
16+
17+
You can add webfonts, meta tags, or analytics to this file.
18+
The build step will place the bundled scripts into the <body> tag.
19+
20+
To begin the development, run `npm start` or `yarn start`.
21+
To create a production bundle, use `npm run build` or `yarn build`.
22+
-->
23+
</body>
24+
</html>
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: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import * as Sentry from '@sentry/react';
2+
import React from 'react';
3+
import ReactDOM from 'react-dom/client';
4+
import {
5+
Outlet,
6+
Route,
7+
RouterProvider,
8+
Routes,
9+
createBrowserRouter,
10+
createRoutesFromChildren,
11+
matchRoutes,
12+
useLocation,
13+
useNavigationType,
14+
useRoutes,
15+
} from 'react-router-dom';
16+
import Index from './pages/Index';
17+
18+
const replay = Sentry.replayIntegration();
19+
20+
Sentry.init({
21+
environment: 'qa', // dynamic sampling bias to keep transactions
22+
dsn: process.env.REACT_APP_E2E_TEST_DSN,
23+
integrations: [
24+
Sentry.reactRouterV7BrowserTracingIntegration({
25+
useEffect: React.useEffect,
26+
useLocation,
27+
useNavigationType,
28+
createRoutesFromChildren,
29+
matchRoutes,
30+
trackFetchStreamPerformance: true,
31+
}),
32+
replay,
33+
],
34+
// We recommend adjusting this value in production, or using tracesSampler
35+
// for finer control
36+
tracesSampleRate: 1.0,
37+
release: 'e2e-test',
38+
39+
// Always capture replays, so we can test this properly
40+
replaysSessionSampleRate: 1.0,
41+
replaysOnErrorSampleRate: 0.0,
42+
43+
tunnel: 'http://localhost:3031',
44+
});
45+
46+
const SentryRoutes = Sentry.withSentryReactRouterV7Routing(Routes);
47+
const sentryUseRoutes = Sentry.wrapUseRoutesV7(useRoutes);
48+
const sentryCreateBrowserRouter = Sentry.wrapCreateBrowserRouterV7(createBrowserRouter);
49+
50+
const DetailsRoutes = () =>
51+
sentryUseRoutes([
52+
{
53+
path: ':detailId',
54+
element: <div id="details">Details</div>,
55+
},
56+
]);
57+
58+
const DetailsRoutesAlternative = () => (
59+
<SentryRoutes>
60+
<Route path=":detailId" element={<div id="details">Details</div>} />
61+
</SentryRoutes>
62+
);
63+
64+
const ViewsRoutes = () =>
65+
sentryUseRoutes([
66+
{
67+
index: true,
68+
element: <div id="views">Views</div>,
69+
},
70+
{
71+
path: 'views/:viewId/*',
72+
element: <DetailsRoutes />,
73+
},
74+
{
75+
path: 'old-views/:viewId/*',
76+
element: <DetailsRoutesAlternative />,
77+
},
78+
]);
79+
80+
const ProjectsRoutes = () => (
81+
<SentryRoutes>
82+
<Route path="projects" element={<Outlet />}>
83+
<Route index element={<div>Project Page Root</div>} />
84+
<Route path="*" element={<Outlet />}>
85+
<Route path=":projectId/*" element={<ViewsRoutes />} />
86+
</Route>
87+
</Route>
88+
</SentryRoutes>
89+
);
90+
91+
const router = sentryCreateBrowserRouter([
92+
{
93+
children: [
94+
{
95+
path: '/',
96+
element: <Index />,
97+
},
98+
{
99+
path: '/*',
100+
element: <ProjectsRoutes />,
101+
},
102+
],
103+
},
104+
]);
105+
106+
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
107+
root.render(<RouterProvider router={router} />);
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// biome-ignore lint/nursery/noUnusedImports: Need React import for JSX
2+
import * as React from 'react';
3+
import { Link } from 'react-router-dom';
4+
5+
const Index = () => {
6+
return (
7+
<>
8+
<Link to="/projects/123/views/456/789" id="navigation">
9+
navigate
10+
</Link>
11+
<Link to="/projects/123/old-views/345/654" id="old-navigation">
12+
navigate old
13+
</Link>
14+
</>
15+
);
16+
};
17+
18+
export default Index;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/// <reference types="react-scripts" />
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-7-cross-usage',
6+
});

0 commit comments

Comments
 (0)