diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-cross-usage/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-cross-usage/src/index.tsx
index bfcc527ded1b..089b27ab974a 100644
--- a/dev-packages/e2e-tests/test-applications/react-router-7-cross-usage/src/index.tsx
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-cross-usage/src/index.tsx
@@ -89,6 +89,14 @@ const ProjectsRoutes = () => (
);
const router = sentryCreateBrowserRouter([
+ {
+ path: '/post/:post',
+ element:
Post
,
+ children: [
+ { index: true, element: Post Index
},
+ { path: '/post/:post/related', element: Related Posts
},
+ ],
+ },
{
children: [
{
diff --git a/packages/react/src/reactrouter-compat-utils/instrumentation.tsx b/packages/react/src/reactrouter-compat-utils/instrumentation.tsx
index bf57fdbd74dc..a72c4fd05378 100644
--- a/packages/react/src/reactrouter-compat-utils/instrumentation.tsx
+++ b/packages/react/src/reactrouter-compat-utils/instrumentation.tsx
@@ -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();
+// only exported for testing purposes
+export const allRoutes = new Set();
/**
* Processes resolved routes by adding them to allRoutes and checking for nested async handlers.
@@ -679,7 +680,8 @@ 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);
diff --git a/packages/react/src/reactrouter-compat-utils/utils.ts b/packages/react/src/reactrouter-compat-utils/utils.ts
index c0750c17c57c..d6501d0e4dbf 100644
--- a/packages/react/src/reactrouter-compat-utils/utils.ts
+++ b/packages/react/src/reactrouter-compat-utils/utils.ts
@@ -45,21 +45,27 @@ export function pathIsWildcardAndHasChildren(path: string, branch: RouteMatch within ) */
+export function routeIsDescendant(route: RouteObject): boolean {
return !!(!route.children && route.element && route.path?.endsWith('/*'));
}
function sendIndexPath(pathBuilder: string, pathname: string, basename: string): [string, TransactionSource] {
- const reconstructedPath = pathBuilder || _stripBasename ? stripBasenameFromPathname(pathname, basename) : pathname;
-
- const formattedPath =
- // If the path ends with a slash, remove it
- reconstructedPath[reconstructedPath.length - 1] === '/'
- ? reconstructedPath.slice(0, -1)
- : // If the path ends with a wildcard, remove it
- reconstructedPath.slice(-2) === '/*'
- ? reconstructedPath.slice(0, -1)
- : reconstructedPath;
+ const reconstructedPath =
+ pathBuilder && pathBuilder.length > 0
+ ? pathBuilder
+ : _stripBasename
+ ? stripBasenameFromPathname(pathname, basename)
+ : pathname;
+
+ let formattedPath =
+ // If the path ends with a wildcard suffix, remove both the slash and the asterisk
+ reconstructedPath.slice(-2) === '/*' ? reconstructedPath.slice(0, -2) : reconstructedPath;
+
+ // If the path ends with a slash, remove it (but keep single '/')
+ if (formattedPath.length > 1 && formattedPath[formattedPath.length - 1] === '/') {
+ formattedPath = formattedPath.slice(0, -1);
+ }
return [formattedPath, 'route'];
}
diff --git a/packages/react/test/reactrouter-compat-utils/instrumentation.test.tsx b/packages/react/test/reactrouter-compat-utils/instrumentation.test.tsx
index 0eeeeb342287..4785849f1192 100644
--- a/packages/react/test/reactrouter-compat-utils/instrumentation.test.tsx
+++ b/packages/react/test/reactrouter-compat-utils/instrumentation.test.tsx
@@ -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();
@@ -47,6 +48,7 @@ vi.mock('../../src/reactrouter-compat-utils/utils', () => ({
initializeRouterUtils: vi.fn(),
getGlobalLocation: vi.fn(() => ({ pathname: '/test', search: '', hash: '' })),
getGlobalPathname: vi.fn(() => '/test'),
+ routeIsDescendant: vi.fn(() => false),
}));
vi.mock('../../src/reactrouter-compat-utils/lazy-routes', () => ({
@@ -141,3 +143,193 @@ 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: },
+ { path: '/user/:id', element: },
+ { path: '/group/:group/:user?', element: },
+ ];
+
+ 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: },
+ { path: '/user/:id', element: },
+ { path: '/group/:group/:user?', element: },
+ {
+ path: '/v1/post/:post',
+ element: ,
+ children: [
+ { path: 'featured', element: },
+ { path: '/v1/post/:post/related', element: },
+ {
+ element: More Nested Children
,
+ children: [{ path: 'edit', element: Edit Post
}],
+ },
+ ],
+ },
+ {
+ path: '/v2/post/:post',
+ element: ,
+ children: [
+ { index: true, element: },
+ { path: 'featured', element: },
+ { path: '/v2/post/:post/related', element: },
+ ],
+ },
+ ];
+
+ addRoutesToAllRoutes(routes);
+ const allRoutesArr = Array.from(allRoutes);
+
+ expect(allRoutesArr).toEqual([
+ { path: '/', element: },
+ { path: '/user/:id', element: },
+ { path: '/group/:group/:user?', element: },
+ // v1 routes ----
+ {
+ path: '/v1/post/:post',
+ element: ,
+ children: [
+ { element: , path: 'featured' },
+ { element: , path: '/v1/post/:post/related' },
+ { children: [{ element: Edit Post
, path: 'edit' }], element: More Nested Children
},
+ ],
+ },
+ { element: , path: 'featured' },
+ { element: , path: '/v1/post/:post/related' },
+ { children: [{ element: Edit Post
, path: 'edit' }], element: More Nested Children
},
+ { element: Edit Post
, path: 'edit' },
+ // v2 routes ---
+ {
+ path: '/v2/post/:post',
+ element: expect.objectContaining({ type: 'div', props: {} }),
+ children: [
+ { element: , index: true },
+ { element: , path: 'featured' },
+ { element: , path: '/v2/post/:post/related' },
+ ],
+ },
+ { element: , index: true },
+ { element: , path: 'featured' },
+ { element: , path: '/v2/post/:post/related' },
+ ]);
+ });
+
+ it('should handle routes with nested index routes', () => {
+ const routes = [
+ {
+ path: '/dashboard',
+ element: ,
+ children: [
+ { index: true, element: Dashboard Index
},
+ { path: 'settings', element: Settings
},
+ ],
+ },
+ ];
+
+ addRoutesToAllRoutes(routes);
+ const allRoutesArr = Array.from(allRoutes);
+
+ expect(allRoutesArr).toEqual([
+ {
+ path: '/dashboard',
+ element: expect.objectContaining({ type: 'div' }),
+ children: [
+ { element: Dashboard Index
, index: true },
+ { element: Settings
, path: 'settings' },
+ ],
+ },
+ { element: Dashboard Index
, index: true },
+ { element: Settings
, path: 'settings' },
+ ]);
+ });
+
+ it('should handle deeply nested routes with layout wrappers', () => {
+ const routes = [
+ {
+ path: '/',
+ element: Root
,
+ children: [
+ { path: 'dashboard', element: Dashboard
},
+ {
+ element: AuthLayout
,
+ children: [{ path: 'login', element: Login
}],
+ },
+ ],
+ },
+ ];
+
+ addRoutesToAllRoutes(routes);
+ const allRoutesArr = Array.from(allRoutes);
+
+ expect(allRoutesArr).toEqual([
+ {
+ path: '/',
+ element: expect.objectContaining({ type: 'div', props: { children: 'Root' } }),
+ children: [
+ {
+ 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' } }),
+ },
+ ],
+ },
+ ],
+ },
+ { element: Dashboard
, path: 'dashboard' },
+ {
+ children: [{ element: Login
, path: 'login' }],
+ element: AuthLayout
,
+ },
+ { element: Login
, path: 'login' },
+ ]);
+ });
+
+ it('should not duplicate routes when called multiple times', () => {
+ const routes = [
+ { path: '/', element: },
+ { path: '/about', element: },
+ ];
+
+ addRoutesToAllRoutes(routes);
+ const firstCount = allRoutes.size;
+
+ addRoutesToAllRoutes(routes);
+ const secondCount = allRoutes.size;
+
+ expect(firstCount).toBe(secondCount);
+ });
+});
diff --git a/packages/react/test/reactrouter-compat-utils/utils.test.ts b/packages/react/test/reactrouter-compat-utils/utils.test.ts
index 91885940db31..9ff48e7450bc 100644
--- a/packages/react/test/reactrouter-compat-utils/utils.test.ts
+++ b/packages/react/test/reactrouter-compat-utils/utils.test.ts
@@ -436,7 +436,7 @@ describe('reactrouter-compat-utils/utils', () => {
];
const result = getNormalizedName(routes, location, branches, '');
- expect(result).toEqual(['', 'route']);
+ expect(result).toEqual(['/', 'route']);
});
it('should handle simple route path', () => {