Skip to content

Commit 3dd54fc

Browse files
onurtemizkanchargome
authored andcommitted
fix(react): Use Set as the allRoutes container. (#14878)
1 parent dbd3296 commit 3dd54fc

File tree

6 files changed

+623
-212
lines changed

6 files changed

+623
-212
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

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

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

185185
const SentryRoutes: React.FC<{
186186
children?: React.ReactNode;
@@ -207,18 +207,29 @@ export function createV6CompatibleWrapUseRoutes(origUseRoutes: UseRoutes, versio
207207

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

213-
updatePageloadTransaction(getActiveRootSpan(), normalizedLocation, routes, undefined, undefined, allRoutes);
217+
updatePageloadTransaction(
218+
getActiveRootSpan(),
219+
normalizedLocation,
220+
routes,
221+
undefined,
222+
undefined,
223+
Array.from(allRoutes),
224+
);
214225
isMountRenderPass.current = false;
215226
} else {
216227
handleNavigation({
217228
location: normalizedLocation,
218229
routes,
219230
navigationType,
220231
version,
221-
allRoutes,
232+
allRoutes: Array.from(allRoutes),
222233
});
223234
}
224235
}, [navigationType, stableLocationParam]);
@@ -343,14 +354,18 @@ function locationIsInsideDescendantRoute(location: Location, routes: RouteObject
343354
return false;
344355
}
345356

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

353-
allRoutes.push(route);
361+
if (route.children && !route.index) {
362+
route.children.forEach(child => {
363+
const childRoutes = getChildRoutesRecursively(child, allRoutes);
364+
365+
childRoutes.forEach(r => allRoutes.add(r));
366+
});
367+
}
368+
}
354369

355370
return allRoutes;
356371
}
@@ -513,7 +528,7 @@ export function createV6CompatibleWithSentryReactRouterRouting<P extends Record<
513528
return Routes;
514529
}
515530

516-
const allRoutes: RouteObject[] = [];
531+
const allRoutes: Set<RouteObject> = new Set();
517532

518533
const SentryRoutes: React.FC<P> = (props: P) => {
519534
const isMountRenderPass = React.useRef(true);
@@ -527,18 +542,22 @@ export function createV6CompatibleWithSentryReactRouterRouting<P extends Record<
527542

528543
if (isMountRenderPass.current) {
529544
routes.forEach(route => {
530-
allRoutes.push(...getChildRoutesRecursively(route));
545+
const extractedChildRoutes = getChildRoutesRecursively(route);
546+
547+
extractedChildRoutes.forEach(r => {
548+
allRoutes.add(r);
549+
});
531550
});
532551

533-
updatePageloadTransaction(getActiveRootSpan(), location, routes, undefined, undefined, allRoutes);
552+
updatePageloadTransaction(getActiveRootSpan(), location, routes, undefined, undefined, Array.from(allRoutes));
534553
isMountRenderPass.current = false;
535554
} else {
536555
handleNavigation({
537556
location,
538557
routes,
539558
navigationType,
540559
version,
541-
allRoutes,
560+
allRoutes: Array.from(allRoutes),
542561
});
543562
}
544563
},

0 commit comments

Comments
 (0)