Skip to content

Commit 80b9537

Browse files
committed
fix(react): Use Set as the allRoutes container.
1 parent 4415881 commit 80b9537

File tree

6 files changed

+515
-265
lines changed

6 files changed

+515
-265
lines changed

dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/src/index.tsx

+15-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import React from 'react';
33
import ReactDOM from 'react-dom/client';
44
import {
55
BrowserRouter,
6+
Outlet,
67
Route,
78
Routes,
89
createRoutesFromChildren,
@@ -48,17 +49,28 @@ const DetailsRoutes = () => (
4849
</SentryRoutes>
4950
);
5051

52+
const DetailsRoutesAlternative = () => (
53+
<SentryRoutes>
54+
<Route path=":detailId" element={<div id="details">Details</div>} />
55+
</SentryRoutes>
56+
);
57+
5158
const ViewsRoutes = () => (
5259
<SentryRoutes>
5360
<Route index element={<div id="views">Views</div>} />
5461
<Route path="views/:viewId/*" element={<DetailsRoutes />} />
62+
<Route path="old-views/:viewId/*" element={<DetailsRoutesAlternative />} />
5563
</SentryRoutes>
5664
);
5765

5866
const ProjectsRoutes = () => (
5967
<SentryRoutes>
60-
<Route path="projects/:projectId/*" element={<ViewsRoutes />}></Route>
61-
<Route path="*" element={<div>No Match Page</div>} />
68+
<Route path="projects" element={<Outlet />}>
69+
<Route index element={<div>Project Page Root</div>} />
70+
<Route path="*" element={<Outlet />}>
71+
<Route path=":projectId/*" element={<ViewsRoutes />} />
72+
</Route>
73+
</Route>
6274
</SentryRoutes>
6375
);
6476

@@ -67,7 +79,7 @@ root.render(
6779
<BrowserRouter>
6880
<SentryRoutes>
6981
<Route path="/" element={<Index />} />
70-
<Route path="/*" element={<ProjectsRoutes />}></Route>
82+
<Route path="/*" element={<ProjectsRoutes />} />
7183
</SentryRoutes>
7284
</BrowserRouter>,
7385
);

dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/src/pages/Index.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ const Index = () => {
88
<Link to="/projects/123/views/456/789" id="navigation">
99
navigate
1010
</Link>
11+
<Link to="/projects/123/old-views/345/654" id="old-navigation">
12+
navigate old
13+
</Link>
1114
</>
1215
);
1316
};

dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/tests/transactions.test.ts

+71
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ test('sends a pageload transaction with a parameterized URL', async ({ page }) =
1010

1111
const rootSpan = await transactionPromise;
1212

13+
expect((await page.innerHTML('#root')).includes('Details')).toBe(true);
1314
expect(rootSpan).toMatchObject({
1415
contexts: {
1516
trace: {
@@ -24,6 +25,30 @@ test('sends a pageload transaction with a parameterized URL', async ({ page }) =
2425
});
2526
});
2627

28+
test('sends a pageload transaction with a parameterized URL - alternative route', async ({ page }) => {
29+
const transactionPromise = waitForTransaction('react-router-6-descendant-routes', async transactionEvent => {
30+
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
31+
});
32+
33+
await page.goto(`/projects/234/old-views/234/567`);
34+
35+
const rootSpan = await transactionPromise;
36+
37+
expect((await page.innerHTML('#root')).includes('Details')).toBe(true);
38+
expect(rootSpan).toMatchObject({
39+
contexts: {
40+
trace: {
41+
op: 'pageload',
42+
origin: 'auto.pageload.react.reactrouter_v6',
43+
},
44+
},
45+
transaction: '/projects/:projectId/old-views/:viewId/:detailId',
46+
transaction_info: {
47+
source: 'route',
48+
},
49+
});
50+
});
51+
2752
test('sends a navigation transaction with a parameterized URL', async ({ page }) => {
2853
const pageloadTxnPromise = waitForTransaction('react-router-6-descendant-routes', async transactionEvent => {
2954
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
@@ -52,6 +77,8 @@ test('sends a navigation transaction with a parameterized URL', async ({ page })
5277
const linkElement = page.locator('id=navigation');
5378

5479
const [_, navigationTxn] = await Promise.all([linkElement.click(), navigationTxnPromise]);
80+
81+
expect((await page.innerHTML('#root')).includes('Details')).toBe(true);
5582
expect(navigationTxn).toMatchObject({
5683
contexts: {
5784
trace: {
@@ -65,3 +92,47 @@ test('sends a navigation transaction with a parameterized URL', async ({ page })
6592
},
6693
});
6794
});
95+
96+
test('sends a navigation transaction with a parameterized URL - alternative route', async ({ page }) => {
97+
const pageloadTxnPromise = waitForTransaction('react-router-6-descendant-routes', async transactionEvent => {
98+
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
99+
});
100+
101+
const navigationTxnPromise = waitForTransaction('react-router-6-descendant-routes', async transactionEvent => {
102+
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation';
103+
});
104+
105+
await page.goto(`/`);
106+
const pageloadTxn = await pageloadTxnPromise;
107+
108+
expect(pageloadTxn).toMatchObject({
109+
contexts: {
110+
trace: {
111+
op: 'pageload',
112+
origin: 'auto.pageload.react.reactrouter_v6',
113+
},
114+
},
115+
transaction: '/',
116+
transaction_info: {
117+
source: 'route',
118+
},
119+
});
120+
121+
const linkElement = page.locator('id=old-navigation');
122+
123+
const [_, navigationTxn] = await Promise.all([linkElement.click(), navigationTxnPromise]);
124+
125+
expect((await page.innerHTML('#root')).includes('Details')).toBe(true);
126+
expect(navigationTxn).toMatchObject({
127+
contexts: {
128+
trace: {
129+
op: 'navigation',
130+
origin: 'auto.navigation.react.reactrouter_v6',
131+
},
132+
},
133+
transaction: '/projects/:projectId/old-views/:viewId/:detailId',
134+
transaction_info: {
135+
source: 'route',
136+
},
137+
});
138+
});

packages/react/src/reactrouterv6-compat-utils.tsx

+29-15
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ export function createV6CompatibleWrapUseRoutes(origUseRoutes: UseRoutes, versio
179179
return origUseRoutes;
180180
}
181181

182-
const allRoutes: RouteObject[] = [];
182+
const allRoutes: Set<RouteObject> = new Set();
183183

184184
const SentryRoutes: React.FC<{
185185
children?: React.ReactNode;
@@ -206,18 +206,24 @@ export function createV6CompatibleWrapUseRoutes(origUseRoutes: UseRoutes, versio
206206

207207
if (isMountRenderPass.current) {
208208
routes.forEach(route => {
209-
allRoutes.push(...getChildRoutesRecursively(route));
209+
const extractedChildRoutes = getChildRoutesRecursively(route);
210+
211+
extractedChildRoutes.forEach(r => {
212+
allRoutes.add(r);
213+
});
210214
});
211215

212-
updatePageloadTransaction(getActiveRootSpan(), normalizedLocation, routes, undefined, undefined, allRoutes);
216+
updatePageloadTransaction(getActiveRootSpan(), normalizedLocation, routes, undefined, undefined, [
217+
...allRoutes,
218+
]);
213219
isMountRenderPass.current = false;
214220
} else {
215221
handleNavigation({
216222
location: normalizedLocation,
217223
routes,
218224
navigationType,
219225
version,
220-
allRoutes,
226+
allRoutes: [...allRoutes],
221227
});
222228
}
223229
}, [navigationType, stableLocationParam]);
@@ -342,14 +348,18 @@ function locationIsInsideDescendantRoute(location: Location, routes: RouteObject
342348
return false;
343349
}
344350

345-
function getChildRoutesRecursively(route: RouteObject, allRoutes: RouteObject[] = []): RouteObject[] {
346-
if (route.children && !route.index) {
347-
route.children.forEach(child => {
348-
allRoutes.push(...getChildRoutesRecursively(child, allRoutes));
349-
});
350-
}
351+
function getChildRoutesRecursively(route: RouteObject, allRoutes: Set<RouteObject> = new Set()): Set<RouteObject> {
352+
if (!allRoutes.has(route)) {
353+
allRoutes.add(route);
351354

352-
allRoutes.push(route);
355+
if (route.children && !route.index) {
356+
route.children.forEach(child => {
357+
const childRoutes = getChildRoutesRecursively(child, allRoutes);
358+
359+
childRoutes.forEach(r => allRoutes.add(r));
360+
});
361+
}
362+
}
353363

354364
return allRoutes;
355365
}
@@ -510,7 +520,7 @@ export function createV6CompatibleWithSentryReactRouterRouting<P extends Record<
510520
return Routes;
511521
}
512522

513-
const allRoutes: RouteObject[] = [];
523+
const allRoutes: Set<RouteObject> = new Set();
514524

515525
const SentryRoutes: React.FC<P> = (props: P) => {
516526
const isMountRenderPass = React.useRef(true);
@@ -524,18 +534,22 @@ export function createV6CompatibleWithSentryReactRouterRouting<P extends Record<
524534

525535
if (isMountRenderPass.current) {
526536
routes.forEach(route => {
527-
allRoutes.push(...getChildRoutesRecursively(route));
537+
const extractedChildRoutes = getChildRoutesRecursively(route);
538+
539+
extractedChildRoutes.forEach(r => {
540+
allRoutes.add(r);
541+
});
528542
});
529543

530-
updatePageloadTransaction(getActiveRootSpan(), location, routes, undefined, undefined, allRoutes);
544+
updatePageloadTransaction(getActiveRootSpan(), location, routes, undefined, undefined, [...allRoutes]);
531545
isMountRenderPass.current = false;
532546
} else {
533547
handleNavigation({
534548
location,
535549
routes,
536550
navigationType,
537551
version,
538-
allRoutes,
552+
allRoutes: [...allRoutes],
539553
});
540554
}
541555
},

0 commit comments

Comments
 (0)