Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions packages/react/src/reactrouter-compat-utils/instrumentation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@ export interface ReactRouterOptions {
type V6CompatibleVersion = '6' | '7';

// Keeping as a global variable for cross-usage in multiple functions
const allRoutes = new Set<RouteObject>();
// only exported for testing purposes
export const allRoutes = new Set<RouteObject>();

/**
* Processes resolved routes by adding them to allRoutes and checking for nested async handlers.
Expand Down Expand Up @@ -679,13 +680,12 @@ export function handleNavigation(opts: {
}
}

function addRoutesToAllRoutes(routes: RouteObject[]): void {
/* Only exported for testing purposes */
export function addRoutesToAllRoutes(routes: RouteObject[]): void {
routes.forEach(route => {
const extractedChildRoutes = getChildRoutesRecursively(route);

extractedChildRoutes.forEach(r => {
allRoutes.add(r);
});
if (!allRoutes.has(route)) {
allRoutes.add(route);
}
});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: rebuildRoutePathFromAllRoutes fails to filter matched child routes from allRoutes because it now only contains root routes, causing incorrect path reconstruction or infinite recursion.
(Severity: High 0.80 | Confidence: 0.95)

🔍 Detailed Analysis

The rebuildRoutePathFromAllRoutes function recursively builds a route path. It uses _matchRoutes to find the current route segment and then filters this route out of allRoutes for the next recursive step. However, allRoutes is now populated only with root-level routes, not nested child routes. When _matchRoutes returns a match for a nested route, the filter allRoutes.filter(route =&gt; route !== match.route) fails to remove it because the child route object doesn't exist at the top level of allRoutes. This can lead to infinite recursion or incorrect path reconstruction for applications using static nested routes, resulting in incorrect transaction names.

💡 Suggested Fix

The allRoutes collection should be populated with a flattened list of all routes, including nested children, rather than just the root-level routes. This will ensure that when rebuildRoutePathFromAllRoutes attempts to filter out a matched child route, it can be successfully found and removed from the list for the subsequent recursive call.

🤖 Prompt for AI Agent
Fix this bug. In packages/react/src/reactrouter-compat-utils/instrumentation.tsx at
lines 686-689: The `rebuildRoutePathFromAllRoutes` function recursively builds a route
path. It uses `_matchRoutes` to find the current route segment and then filters this
route out of `allRoutes` for the next recursive step. However, `allRoutes` is now
populated only with root-level routes, not nested child routes. When `_matchRoutes`
returns a match for a nested route, the filter `allRoutes.filter(route =&gt; route !==
match.route)` fails to remove it because the child route object doesn't exist at the top
level of `allRoutes`. This can lead to infinite recursion or incorrect path
reconstruction for applications using static nested routes, resulting in incorrect
transaction names.

Did we get this right? 👍 / 👎 to inform future reviews.

}

Expand Down
195 changes: 195 additions & 0 deletions packages/react/test/reactrouter-compat-utils/instrumentation.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
createReactRouterV6CompatibleTracingIntegration,
updateNavigationSpan,
} from '../../src/reactrouter-compat-utils';
import { addRoutesToAllRoutes, allRoutes } from '../../src/reactrouter-compat-utils/instrumentation';
import type { Location, RouteObject } from '../../src/types';

const mockUpdateName = vi.fn();
Expand Down Expand Up @@ -141,3 +142,197 @@ describe('reactrouter-compat-utils/instrumentation', () => {
});
});
});

describe('addRoutesToAllRoutes', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.resetModules();
allRoutes.clear();
});

it('should add simple routes without nesting', () => {
const routes = [
{ path: '/', element: <div /> },
{ path: '/user/:id', element: <div /> },
{ path: '/group/:group/:user?', element: <div /> },
];

addRoutesToAllRoutes(routes);
const allRoutesArr = Array.from(allRoutes);

expect(allRoutesArr).toHaveLength(3);
expect(allRoutesArr).toEqual(
expect.arrayContaining([
expect.objectContaining({ path: '/' }),
expect.objectContaining({ path: '/user/:id' }),
expect.objectContaining({ path: '/group/:group/:user?' }),
]),
);

// Verify exact structure matches manual testing results
allRoutesArr.forEach(route => {
expect(route).toHaveProperty('element');
expect(route.element).toHaveProperty('props');
});
});

it('should handle complex nested routes with multiple levels', () => {
const routes = [
{ path: '/', element: <div /> },
{ path: '/user/:id', element: <div /> },
{ path: '/group/:group/:user?', element: <div /> },
{
path: '/v1/post/:post',
element: <div />,
children: [
{ path: 'featured', element: <div /> },
{ path: '/v1/post/:post/related', element: <div /> },
{
element: <div>More Nested Children</div>,
children: [{ path: 'edit', element: <div>Edit Post</div> }],
},
],
},
{
path: '/v2/post/:post',
element: <div />,
children: [
{ index: true, element: <div /> },
{ path: 'featured', element: <div /> },
{ path: '/v2/post/:post/related', element: <div /> },
],
},
];

addRoutesToAllRoutes(routes);
const allRoutesArr = Array.from(allRoutes);

expect(allRoutesArr).toEqual([
{
path: '/',
element: expect.objectContaining({ type: 'div', props: {} }),
},
{
path: '/user/:id',
element: expect.objectContaining({ type: 'div', props: {} }),
},
{
path: '/group/:group/:user?',
element: expect.objectContaining({ type: 'div', props: {} }),
},
{
path: '/v1/post/:post',
element: expect.objectContaining({ type: 'div', props: {} }),
children: [
{ element: <div />, path: 'featured' },
{ element: <div />, path: '/v1/post/:post/related' },
{ children: [{ element: <div>Edit Post</div>, path: 'edit' }], element: <div>More Nested Children</div> },
],
},
{
path: '/v2/post/:post',
element: expect.objectContaining({ type: 'div', props: {} }),
children: [
{ element: <div />, index: true },
{ element: <div />, path: 'featured' },
{ element: <div />, path: '/v2/post/:post/related' },
],
},
]);
});

it('should handle routes with nested index routes', () => {
const routes = [
{
path: '/dashboard',
element: <div />,
children: [
{ index: true, element: <div>Dashboard Index</div> },
{ path: 'settings', element: <div>Settings</div> },
],
},
];

addRoutesToAllRoutes(routes);
const allRoutesArr = Array.from(allRoutes);

expect(allRoutesArr).toEqual([
{
path: '/dashboard',
element: expect.objectContaining({ type: 'div' }),
children: [
{ element: <div>Dashboard Index</div>, index: true },
{ element: <div>Settings</div>, path: 'settings' },
],
},
]);
});

it('should handle deeply nested routes with layout wrappers', () => {
const routes = [
{
path: '/',
element: <div>Root</div>,
children: [
{ path: 'contact', element: <div>Contact</div> },
{ path: 'dashboard', element: <div>Dashboard</div> },
{
element: <div>AuthLayout</div>,
children: [
{ path: 'login', element: <div>Login</div> },
{ path: 'logout', element: <div>Logout</div> },
],
},
],
},
];

addRoutesToAllRoutes(routes);
const allRoutesArr = Array.from(allRoutes);

expect(allRoutesArr).toEqual([
{
path: '/',
element: expect.objectContaining({ type: 'div', props: { children: 'Root' } }),
children: [
{
path: 'contact',
element: expect.objectContaining({ type: 'div', props: { children: 'Contact' } }),
},
{
path: 'dashboard',
element: expect.objectContaining({ type: 'div', props: { children: 'Dashboard' } }),
},
{
element: expect.objectContaining({ type: 'div', props: { children: 'AuthLayout' } }),
children: [
{
path: 'login',
element: expect.objectContaining({ type: 'div', props: { children: 'Login' } }),
},
{
path: 'logout',
element: expect.objectContaining({ type: 'div', props: { children: 'Logout' } }),
},
],
},
],
},
]);
});

it('should not duplicate routes when called multiple times', () => {
const routes = [
{ path: '/', element: <div /> },
{ path: '/about', element: <div /> },
];

addRoutesToAllRoutes(routes);
const firstCount = allRoutes.size;

addRoutesToAllRoutes(routes);
const secondCount = allRoutes.size;

expect(firstCount).toBe(secondCount);
});
});
Loading