diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/tests/transactions.test.ts index 3901b0938ca5..b6a97054312a 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/tests/transactions.test.ts @@ -498,3 +498,87 @@ test('Updates navigation transaction name correctly when span is cancelled early expect(['externalFinish', 'cancelled']).toContain(idleSpanFinishReason); } }); + +test('Creates separate transactions for rapid consecutive navigations', async ({ page }) => { + await page.goto('/'); + + // First navigation: / -> /lazy/inner/:id/:anotherId/:someAnotherId + const firstTransactionPromise = waitForTransaction('react-router-7-lazy-routes', async transactionEvent => { + return ( + !!transactionEvent?.transaction && + transactionEvent.contexts?.trace?.op === 'navigation' && + transactionEvent.transaction === '/lazy/inner/:id/:anotherId/:someAnotherId' + ); + }); + + const navigationToInner = page.locator('id=navigation'); + await expect(navigationToInner).toBeVisible(); + await navigationToInner.click(); + + const firstEvent = await firstTransactionPromise; + + // Verify first transaction + expect(firstEvent.transaction).toBe('/lazy/inner/:id/:anotherId/:someAnotherId'); + expect(firstEvent.contexts?.trace?.op).toBe('navigation'); + const firstTraceId = firstEvent.contexts?.trace?.trace_id; + const firstSpanId = firstEvent.contexts?.trace?.span_id; + + // Second navigation: /lazy/inner -> /another-lazy/sub/:id/:subId + const secondTransactionPromise = waitForTransaction('react-router-7-lazy-routes', async transactionEvent => { + return ( + !!transactionEvent?.transaction && + transactionEvent.contexts?.trace?.op === 'navigation' && + transactionEvent.transaction === '/another-lazy/sub/:id/:subId' + ); + }); + + const navigationToAnother = page.locator('id=navigate-to-another-from-inner'); + await expect(navigationToAnother).toBeVisible(); + await navigationToAnother.click(); + + const secondEvent = await secondTransactionPromise; + + // Verify second transaction + expect(secondEvent.transaction).toBe('/another-lazy/sub/:id/:subId'); + expect(secondEvent.contexts?.trace?.op).toBe('navigation'); + const secondTraceId = secondEvent.contexts?.trace?.trace_id; + const secondSpanId = secondEvent.contexts?.trace?.span_id; + + // Third navigation: /another-lazy -> /lazy/inner/:id/:anotherId/:someAnotherId (back to same route as first) + const thirdTransactionPromise = waitForTransaction('react-router-7-lazy-routes', async transactionEvent => { + return ( + !!transactionEvent?.transaction && + transactionEvent.contexts?.trace?.op === 'navigation' && + transactionEvent.transaction === '/lazy/inner/:id/:anotherId/:someAnotherId' && + // Ensure we're not matching the first transaction again + transactionEvent.contexts?.trace?.trace_id !== firstTraceId + ); + }); + + const navigationBackToInner = page.locator('id=navigate-to-inner-from-deep'); + await expect(navigationBackToInner).toBeVisible(); + await navigationBackToInner.click(); + + const thirdEvent = await thirdTransactionPromise; + + // Verify third transaction + expect(thirdEvent.transaction).toBe('/lazy/inner/:id/:anotherId/:someAnotherId'); + expect(thirdEvent.contexts?.trace?.op).toBe('navigation'); + const thirdTraceId = thirdEvent.contexts?.trace?.trace_id; + const thirdSpanId = thirdEvent.contexts?.trace?.span_id; + + // Verify each navigation created a separate transaction with unique trace and span IDs + expect(firstTraceId).toBeDefined(); + expect(secondTraceId).toBeDefined(); + expect(thirdTraceId).toBeDefined(); + + // All trace IDs should be unique + expect(firstTraceId).not.toBe(secondTraceId); + expect(secondTraceId).not.toBe(thirdTraceId); + expect(firstTraceId).not.toBe(thirdTraceId); + + // All span IDs should be unique + expect(firstSpanId).not.toBe(secondSpanId); + expect(secondSpanId).not.toBe(thirdSpanId); + expect(firstSpanId).not.toBe(thirdSpanId); +}); diff --git a/packages/react/src/reactrouter-compat-utils/instrumentation.tsx b/packages/react/src/reactrouter-compat-utils/instrumentation.tsx index a6e55f1a967c..2df54c7f1230 100644 --- a/packages/react/src/reactrouter-compat-utils/instrumentation.tsx +++ b/packages/react/src/reactrouter-compat-utils/instrumentation.tsx @@ -279,7 +279,10 @@ export function createV6CompatibleWrapCreateBrowserRouter< state.historyAction === 'PUSH' || (state.historyAction === 'POP' && isInitialPageloadComplete); if (shouldHandleNavigation) { - const navigationHandler = (): void => { + // Only handle navigation when it's complete (state is idle). + // During 'loading' or 'submitting', state.location may still have the old pathname, + // which would cause us to create a span for the wrong route. + if (state.navigation.state === 'idle') { handleNavigation({ location: state.location, routes, @@ -288,13 +291,6 @@ export function createV6CompatibleWrapCreateBrowserRouter< basename, allRoutes: Array.from(allRoutes), }); - }; - - // Wait for the next render if loading an unsettled route - if (state.navigation.state !== 'idle') { - requestAnimationFrame(navigationHandler); - } else { - navigationHandler(); } } }); @@ -632,7 +628,8 @@ export function handleNavigation(opts: { allRoutes?: RouteObject[]; }): void { const { location, routes, navigationType, version, matches, basename, allRoutes } = opts; - const branches = Array.isArray(matches) ? matches : _matchRoutes(routes, location, basename); + // Use allRoutes for matching to include lazy-loaded routes + const branches = Array.isArray(matches) ? matches : _matchRoutes(allRoutes || routes, location, basename); const client = getClient(); if (!client || !CLIENTS_WITH_INSTRUMENT_NAVIGATION.has(client)) { @@ -649,7 +646,7 @@ export function handleNavigation(opts: { if ((navigationType === 'PUSH' || navigationType === 'POP') && branches) { const [name, source] = resolveRouteNameAndSource( location, - routes, + allRoutes || routes, allRoutes || routes, branches as RouteMatch[], basename, @@ -659,8 +656,11 @@ export function handleNavigation(opts: { const spanJson = activeSpan && spanToJSON(activeSpan); const isAlreadyInNavigationSpan = spanJson?.op === 'navigation'; - // Cross usage can result in multiple navigation spans being created without this check - if (!isAlreadyInNavigationSpan) { + // Only skip creating a new span if we're already in a navigation span AND the route name matches. + // This handles cross-usage (multiple wrappers for same navigation) while allowing consecutive navigations. + const isSpanForSameRoute = isAlreadyInNavigationSpan && spanJson?.description === name; + + if (!isSpanForSameRoute) { const navigationSpan = startBrowserTracingNavigationSpan(client, { name, attributes: { @@ -727,7 +727,13 @@ function updatePageloadTransaction({ : (_matchRoutes(allRoutes || routes, location, basename) as unknown as RouteMatch[]); if (branches) { - const [name, source] = resolveRouteNameAndSource(location, routes, allRoutes || routes, branches, basename); + const [name, source] = resolveRouteNameAndSource( + location, + allRoutes || routes, + allRoutes || routes, + branches, + basename, + ); getCurrentScope().setTransactionName(name || '/'); @@ -780,7 +786,7 @@ function patchSpanEnd( if (branches) { const [name, source] = resolveRouteNameAndSource( location, - routes, + currentAllRoutes.length > 0 ? currentAllRoutes : routes, currentAllRoutes.length > 0 ? currentAllRoutes : routes, branches, basename, diff --git a/packages/react/src/reactrouter-compat-utils/utils.ts b/packages/react/src/reactrouter-compat-utils/utils.ts index d6501d0e4dbf..4cec7bd98dcd 100644 --- a/packages/react/src/reactrouter-compat-utils/utils.ts +++ b/packages/react/src/reactrouter-compat-utils/utils.ts @@ -171,6 +171,13 @@ export function locationIsInsideDescendantRoute(location: Location, routes: Rout return false; } +/** + * Returns a fallback transaction name from location pathname. + */ +function getFallbackTransactionName(location: Location, basename: string): string { + return _stripBasename ? stripBasenameFromPathname(location.pathname, basename) : location.pathname || ''; +} + /** * Gets a normalized route name and transaction source from the current routes and location. */ @@ -184,53 +191,55 @@ export function getNormalizedName( return [_stripBasename ? stripBasenameFromPathname(location.pathname, basename) : location.pathname, 'url']; } + if (!branches) { + return [getFallbackTransactionName(location, basename), 'url']; + } + let pathBuilder = ''; - if (branches) { - for (const branch of branches) { - const route = branch.route; - if (route) { - // Early return if index route - if (route.index) { - return sendIndexPath(pathBuilder, branch.pathname, basename); - } - const path = route.path; - - // If path is not a wildcard and has no child routes, append the path - if (path && !pathIsWildcardAndHasChildren(path, branch)) { - const newPath = path[0] === '/' || pathBuilder[pathBuilder.length - 1] === '/' ? path : `/${path}`; - pathBuilder = trimSlash(pathBuilder) + prefixWithSlash(newPath); - - // If the path matches the current location, return the path - if (trimSlash(location.pathname) === trimSlash(basename + branch.pathname)) { - if ( - // If the route defined on the element is something like - // Product} /> - // We should check against the branch.pathname for the number of / separators - getNumberOfUrlSegments(pathBuilder) !== getNumberOfUrlSegments(branch.pathname) && - // We should not count wildcard operators in the url segments calculation - !pathEndsWithWildcard(pathBuilder) - ) { - return [(_stripBasename ? '' : basename) + newPath, 'route']; - } - - // if the last character of the pathbuilder is a wildcard and there are children, remove the wildcard - if (pathIsWildcardAndHasChildren(pathBuilder, branch)) { - pathBuilder = pathBuilder.slice(0, -1); - } - - return [(_stripBasename ? '' : basename) + pathBuilder, 'route']; - } - } - } + for (const branch of branches) { + const route = branch.route; + if (!route) { + continue; + } + + // Early return for index routes + if (route.index) { + return sendIndexPath(pathBuilder, branch.pathname, basename); } - } - const fallbackTransactionName = _stripBasename - ? stripBasenameFromPathname(location.pathname, basename) - : location.pathname || ''; + const path = route.path; + if (!path || pathIsWildcardAndHasChildren(path, branch)) { + continue; + } + + // Build the route path + const newPath = path[0] === '/' || pathBuilder[pathBuilder.length - 1] === '/' ? path : `/${path}`; + pathBuilder = trimSlash(pathBuilder) + prefixWithSlash(newPath); + + // Check if this path matches the current location + if (trimSlash(location.pathname) !== trimSlash(basename + branch.pathname)) { + continue; + } + + // Check if this is a parameterized route like /stores/:storeId/products/:productId + if ( + getNumberOfUrlSegments(pathBuilder) !== getNumberOfUrlSegments(branch.pathname) && + !pathEndsWithWildcard(pathBuilder) + ) { + return [(_stripBasename ? '' : basename) + newPath, 'route']; + } + + // Handle wildcard routes with children - strip trailing wildcard + if (pathIsWildcardAndHasChildren(pathBuilder, branch)) { + pathBuilder = pathBuilder.slice(0, -1); + } + + return [(_stripBasename ? '' : basename) + pathBuilder, 'route']; + } - return [fallbackTransactionName, 'url']; + // Fallback when no matching route found + return [getFallbackTransactionName(location, basename), 'url']; } /** diff --git a/packages/react/test/reactrouter-cross-usage.test.tsx b/packages/react/test/reactrouter-cross-usage.test.tsx index 77d8e3d95b2e..bdc5fc02c889 100644 --- a/packages/react/test/reactrouter-cross-usage.test.tsx +++ b/packages/react/test/reactrouter-cross-usage.test.tsx @@ -9,7 +9,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, setCurrentClient, } from '@sentry/core'; -import { render } from '@testing-library/react'; +import { act, render, waitFor } from '@testing-library/react'; import * as React from 'react'; import { createMemoryRouter, @@ -26,6 +26,7 @@ import { } from 'react-router-6'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { BrowserClient } from '../src'; +import { allRoutes } from '../src/reactrouter-compat-utils/instrumentation'; import { reactRouterV6BrowserTracingIntegration, withSentryReactRouterV6Routing, @@ -101,6 +102,7 @@ describe('React Router cross usage of wrappers', () => { beforeEach(() => { vi.clearAllMocks(); getCurrentScope().setClient(undefined); + allRoutes.clear(); }); describe('wrapCreateBrowserRouter and wrapUseRoutes', () => { @@ -218,8 +220,7 @@ describe('React Router cross usage of wrappers', () => { expect(container.innerHTML).toContain('Details'); - // It's called 1 time from the wrapped `MemoryRouter` - expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(2); expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { name: '/second-level/:id/third-level/:id', attributes: { @@ -339,7 +340,6 @@ describe('React Router cross usage of wrappers', () => { expect(container.innerHTML).toContain('Details'); - // It's called 1 time from the wrapped `MemoryRouter` expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); }); }); @@ -465,8 +465,7 @@ describe('React Router cross usage of wrappers', () => { expect(container.innerHTML).toContain('Details'); - // It's called 1 time from the wrapped `createMemoryRouter` - expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(2); expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { name: '/second-level/:id/third-level/:id', @@ -596,7 +595,7 @@ describe('React Router cross usage of wrappers', () => { ); expect(container.innerHTML).toContain('Details'); - expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(2); expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { name: '/second-level/:id/third-level/:id', attributes: { @@ -607,4 +606,276 @@ describe('React Router cross usage of wrappers', () => { }); }); }); + + describe('consecutive navigations to different routes', () => { + it('should create separate transactions for consecutive navigations to different routes', async () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration( + reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + ); + + const createSentryMemoryRouter = wrapCreateMemoryRouterV6(createMemoryRouter); + + const router = createSentryMemoryRouter( + [ + { + children: [ + { path: '/users', element:
Users
}, + { path: '/settings', element:
Settings
}, + { path: '/profile', element:
Profile
}, + ], + }, + ], + { initialEntries: ['/users'] }, + ); + + render( + + + , + ); + + expect(mockStartBrowserTracingNavigationSpan).not.toHaveBeenCalled(); + + await act(async () => { + router.navigate('/settings'); + await waitFor(() => expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1)); + }); + + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/settings', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + }, + }); + + await act(async () => { + router.navigate('/profile'); + await waitFor(() => expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(2)); + }); + + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(2); + + const calls = mockStartBrowserTracingNavigationSpan.mock.calls; + expect(calls[0]![1].name).toBe('/settings'); + expect(calls[1]![1].name).toBe('/profile'); + expect(calls[0]![1].attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('navigation'); + expect(calls[1]![1].attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('navigation'); + }); + + it('should create separate transactions for rapid consecutive navigations', async () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration( + reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + ); + + const createSentryMemoryRouter = wrapCreateMemoryRouterV6(createMemoryRouter); + + const router = createSentryMemoryRouter( + [ + { + children: [ + { path: '/a', element:
A
}, + { path: '/b', element:
B
}, + { path: '/c', element:
C
}, + ], + }, + ], + { initialEntries: ['/a'] }, + ); + + render( + + + , + ); + + await act(async () => { + router.navigate('/b'); + router.navigate('/c'); + await waitFor(() => expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(2)); + }); + + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(2); + + const calls = mockStartBrowserTracingNavigationSpan.mock.calls; + expect(calls[0]![1].name).toBe('/b'); + expect(calls[1]![1].name).toBe('/c'); + }); + + it('should NOT create duplicate spans for same route name (even with different params)', async () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration( + reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + ); + + const createSentryMemoryRouter = wrapCreateMemoryRouterV6(createMemoryRouter); + + const router = createSentryMemoryRouter( + [ + { + children: [{ path: '/user/:id', element:
User
}], + }, + ], + { initialEntries: ['/user/1'] }, + ); + + render( + + + , + ); + + await act(async () => { + router.navigate('/user/2'); + await waitFor(() => expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1)); + }); + + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledWith(expect.any(BrowserClient), { + name: '/user/:id', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + }, + }); + + await act(async () => { + router.navigate('/user/3'); + await new Promise(resolve => setTimeout(resolve, 100)); + }); + + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); + }); + + it('should handle mixed cross-usage and consecutive navigations correctly', async () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration( + reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + ); + + const createSentryMemoryRouter = wrapCreateMemoryRouterV6(createMemoryRouter); + const sentryUseRoutes = wrapUseRoutesV6(useRoutes); + + const UsersRoute: React.FC = () => sentryUseRoutes([{ path: '/', element:
Users
}]); + + const SettingsRoute: React.FC = () => sentryUseRoutes([{ path: '/', element:
Settings
}]); + + const router = createSentryMemoryRouter( + [ + { + children: [ + { path: '/users/*', element: }, + { path: '/settings/*', element: }, + ], + }, + ], + { initialEntries: ['/users'] }, + ); + + render( + + + , + ); + + expect(mockStartBrowserTracingNavigationSpan).not.toHaveBeenCalled(); + + await act(async () => { + router.navigate('/settings'); + await waitFor(() => expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1)); + }); + + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/settings/*', + attributes: expect.objectContaining({ + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + }), + }); + }); + + it('should not create duplicate spans for cross-usage on same route', async () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration( + reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + ); + + const createSentryMemoryRouter = wrapCreateMemoryRouterV6(createMemoryRouter); + const sentryUseRoutes = wrapUseRoutesV6(useRoutes); + + const NestedRoute: React.FC = () => sentryUseRoutes([{ path: '/', element:
Details
}]); + + const router = createSentryMemoryRouter( + [ + { + children: [{ path: '/details/*', element: }], + }, + ], + { initialEntries: ['/home'] }, + ); + + render( + + + , + ); + + await act(async () => { + router.navigate('/details'); + await waitFor(() => expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalled()); + }); + + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledWith(expect.any(BrowserClient), { + name: '/details/*', + attributes: expect.objectContaining({ + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + }), + }); + }); + }); }); diff --git a/packages/react/test/reactrouter-descendant-routes.test.tsx b/packages/react/test/reactrouter-descendant-routes.test.tsx index fe75bc81e858..a08893694a30 100644 --- a/packages/react/test/reactrouter-descendant-routes.test.tsx +++ b/packages/react/test/reactrouter-descendant-routes.test.tsx @@ -25,6 +25,7 @@ import { } from 'react-router-6'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { BrowserClient } from '../src'; +import { allRoutes } from '../src/reactrouter-compat-utils/instrumentation'; import { reactRouterV6BrowserTracingIntegration, withSentryReactRouterV6Routing, @@ -79,6 +80,7 @@ describe('React Router Descendant Routes', () => { beforeEach(() => { vi.clearAllMocks(); getCurrentScope().setClient(undefined); + allRoutes.clear(); }); describe('withSentryReactRouterV6Routing', () => { diff --git a/packages/react/test/reactrouterv6.test.tsx b/packages/react/test/reactrouterv6.test.tsx index 61fefdff9b63..fda5043d2e6a 100644 --- a/packages/react/test/reactrouterv6.test.tsx +++ b/packages/react/test/reactrouterv6.test.tsx @@ -28,6 +28,7 @@ import { } from 'react-router-6'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { BrowserClient } from '../src'; +import { allRoutes } from '../src/reactrouter-compat-utils/instrumentation'; import { reactRouterV6BrowserTracingIntegration, withSentryReactRouterV6Routing, @@ -83,6 +84,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { beforeEach(() => { vi.clearAllMocks(); getCurrentScope().setClient(undefined); + allRoutes.clear(); }); it('wrapCreateMemoryRouterV6 starts and updates a pageload transaction - single initialEntry', () => {