diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/src/pages/InnerLazyRoutes.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/src/pages/InnerLazyRoutes.tsx index 42324bb09391..dee76ac790aa 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/src/pages/InnerLazyRoutes.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/src/pages/InnerLazyRoutes.tsx @@ -32,6 +32,9 @@ export const someMoreNestedRoutes = [ Navigate to Another Lazy Route + + Navigate to Upper Lazy Route + ), }, 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 8263af9e6a28..2cfa2935b5bf 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 @@ -107,7 +107,15 @@ test('Creates navigation transactions between two different lazy routes', async expect(secondEvent.contexts?.trace?.op).toBe('navigation'); }); -test('Creates navigation transactions from inner lazy route to another lazy route', async ({ page }) => { +test('Creates navigation transactions from inner lazy route to another lazy route with history navigation', async ({ + page, +}) => { + await page.goto('/'); + + // Navigate to inner lazy route first + const navigationToInner = page.locator('id=navigation'); + await expect(navigationToInner).toBeVisible(); + // First, navigate to the inner lazy route const firstTransactionPromise = waitForTransaction('react-router-7-lazy-routes', async transactionEvent => { return ( @@ -117,11 +125,6 @@ test('Creates navigation transactions from inner lazy route to another lazy rout ); }); - await page.goto('/'); - - // Navigate to inner lazy route first - const navigationToInner = page.locator('id=navigation'); - await expect(navigationToInner).toBeVisible(); await navigationToInner.click(); const firstEvent = await firstTransactionPromise; @@ -135,6 +138,10 @@ test('Creates navigation transactions from inner lazy route to another lazy rout expect(firstEvent.type).toBe('transaction'); expect(firstEvent.contexts?.trace?.op).toBe('navigation'); + // Click the navigation link from within the inner lazy route to another lazy route + const navigationToAnotherFromInner = page.locator('id=navigate-to-another-from-inner'); + await expect(navigationToAnotherFromInner).toBeVisible(); + // Now navigate from the inner lazy route to another lazy route const secondTransactionPromise = waitForTransaction('react-router-7-lazy-routes', async transactionEvent => { return ( @@ -144,9 +151,6 @@ test('Creates navigation transactions from inner lazy route to another lazy rout ); }); - // Click the navigation link from within the inner lazy route to another lazy route - const navigationToAnotherFromInner = page.locator('id=navigate-to-another-from-inner'); - await expect(navigationToAnotherFromInner).toBeVisible(); await navigationToAnotherFromInner.click(); const secondEvent = await secondTransactionPromise; @@ -159,4 +163,103 @@ test('Creates navigation transactions from inner lazy route to another lazy rout expect(secondEvent.transaction).toBe('/another-lazy/sub/:id/:subId'); expect(secondEvent.type).toBe('transaction'); expect(secondEvent.contexts?.trace?.op).toBe('navigation'); + + // Go back to the previous page to ensure history navigation works as expected + const goBackTransactionPromise = waitForTransaction('react-router-7-lazy-routes', async transactionEvent => { + return ( + !!transactionEvent?.transaction && + transactionEvent.contexts?.trace?.op === 'navigation' && + transactionEvent.transaction === '/lazy/inner/:id/:anotherId/:someAnotherId' + ); + }); + + await page.goBack(); + + const goBackEvent = await goBackTransactionPromise; + + // Validate the second go back transaction event + expect(goBackEvent.transaction).toBe('/lazy/inner/:id/:anotherId/:someAnotherId'); + expect(goBackEvent.type).toBe('transaction'); + expect(goBackEvent.contexts?.trace?.op).toBe('navigation'); + + // Navigate to the upper route + const goUpperRouteTransactionPromise = waitForTransaction('react-router-7-lazy-routes', async transactionEvent => { + return ( + !!transactionEvent?.transaction && + transactionEvent.contexts?.trace?.op === 'navigation' && + transactionEvent.transaction === '/lazy/inner/:id/:anotherId' + ); + }); + + const navigationToUpper = page.locator('id=navigate-to-upper'); + + await navigationToUpper.click(); + + const goUpperRouteEvent = await goUpperRouteTransactionPromise; + + // Validate the go upper route transaction event + expect(goUpperRouteEvent.transaction).toBe('/lazy/inner/:id/:anotherId'); + expect(goUpperRouteEvent.type).toBe('transaction'); + expect(goUpperRouteEvent.contexts?.trace?.op).toBe('navigation'); +}); + +test('Does not send any duplicate navigation transaction names browsing between different routes', async ({ page }) => { + const transactionNamesList: string[] = []; + + // Monitor and add all transaction names sent to Sentry for the navigations + const allTransactionsPromise = waitForTransaction('react-router-7-lazy-routes', async transactionEvent => { + if (transactionEvent?.transaction) { + transactionNamesList.push(transactionEvent.transaction); + } + + if (transactionNamesList.length>= 5) { + // Stop monitoring once we have enough transaction names + return true; + } + + return false; + }); + + // Go to root page + await page.goto('/'); + page.waitForTimeout(1000); + + // Navigate to inner lazy route + const navigationToInner = page.locator('id=navigation'); + await expect(navigationToInner).toBeVisible(); + await navigationToInner.click(); + + // Navigate to another lazy route + const navigationToAnother = page.locator('id=navigate-to-another-from-inner'); + await expect(navigationToAnother).toBeVisible(); + await page.waitForTimeout(1000); + + // Click to navigate to another lazy route + await navigationToAnother.click(); + const anotherLazyRouteContent = page.locator('id=another-lazy-route-deep'); + await expect(anotherLazyRouteContent).toBeVisible(); + await page.waitForTimeout(1000); + + // Navigate back to inner lazy route + await page.goBack(); + await expect(page.locator('id=innermost-lazy-route')).toBeVisible(); + await page.waitForTimeout(1000); + + // Navigate to upper inner lazy route + const navigationToUpper = page.locator('id=navigate-to-upper'); + await expect(navigationToUpper).toBeVisible(); + await navigationToUpper.click(); + + await page.waitForTimeout(1000); + + await allTransactionsPromise; + + expect(transactionNamesList.length).toBe(5); + expect(transactionNamesList).toEqual([ + '/', + '/lazy/inner/:id/:anotherId/:someAnotherId', + '/another-lazy/sub/:id/:subId', + '/lazy/inner/:id/:anotherId/:someAnotherId', + '/lazy/inner/:id/:anotherId', + ]); }); diff --git a/packages/react/src/reactrouter-compat-utils/index.ts b/packages/react/src/reactrouter-compat-utils/index.ts index 94f879017c13..c2b56ec446fb 100644 --- a/packages/react/src/reactrouter-compat-utils/index.ts +++ b/packages/react/src/reactrouter-compat-utils/index.ts @@ -9,8 +9,6 @@ export { createV6CompatibleWrapCreateMemoryRouter, createV6CompatibleWrapUseRoutes, handleNavigation, - handleExistingNavigationSpan, - createNewNavigationSpan, addResolvedRoutesToParent, processResolvedRoutes, updateNavigationSpan, @@ -21,7 +19,6 @@ export { resolveRouteNameAndSource, getNormalizedName, initializeRouterUtils, - isLikelyLazyRouteContext, locationIsInsideDescendantRoute, prefixWithSlash, rebuildRoutePathFromAllRoutes, diff --git a/packages/react/src/reactrouter-compat-utils/instrumentation.tsx b/packages/react/src/reactrouter-compat-utils/instrumentation.tsx index 5012ed1ccc9c..a464875a8575 100644 --- a/packages/react/src/reactrouter-compat-utils/instrumentation.tsx +++ b/packages/react/src/reactrouter-compat-utils/instrumentation.tsx @@ -44,7 +44,6 @@ import { checkRouteForAsyncHandler } from './lazy-routes'; import { getNormalizedName, initializeRouterUtils, - isLikelyLazyRouteContext, locationIsInsideDescendantRoute, prefixWithSlash, rebuildRoutePathFromAllRoutes, @@ -176,12 +175,7 @@ export function updateNavigationSpan( // Check if this span has already been named to avoid multiple updates // But allow updates if this is a forced update (e.g., when lazy routes are loaded) const hasBeenNamed = - !forceUpdate && - ( - activeRootSpan as { - __sentry_navigation_name_set__?: boolean; - } - )?.__sentry_navigation_name_set__; + !forceUpdate && (activeRootSpan as { __sentry_navigation_name_set__?: boolean })?.__sentry_navigation_name_set__; if (!hasBeenNamed) { // Get fresh branches for the current location with all loaded routes @@ -355,13 +349,7 @@ export function createV6CompatibleWrapCreateMemoryRouter< : router.state.location; if (router.state.historyAction === 'POP' && activeRootSpan) { - updatePageloadTransaction({ - activeRootSpan, - location, - routes, - basename, - allRoutes: Array.from(allRoutes), - }); + updatePageloadTransaction({ activeRootSpan, location, routes, basename, allRoutes: Array.from(allRoutes) }); } router.subscribe((state: RouterState) => { @@ -389,11 +377,7 @@ export function createReactRouterV6CompatibleTracingIntegration( options: Parameters[0] & ReactRouterOptions, version: V6CompatibleVersion, ): Integration { - const integration = browserTracingIntegration({ - ...options, - instrumentPageLoad: false, - instrumentNavigation: false, - }); + const integration = browserTracingIntegration({ ...options, instrumentPageLoad: false, instrumentNavigation: false }); const { useEffect, @@ -532,13 +516,7 @@ function wrapPatchRoutesOnNavigation( if (activeRootSpan && (spanToJSON(activeRootSpan) as { op?: string }).op === 'navigation') { updateNavigationSpan( activeRootSpan, - { - pathname: targetPath, - search: '', - hash: '', - state: null, - key: 'default', - }, + { pathname: targetPath, search: '', hash: '', state: null, key: 'default' }, Array.from(allRoutes), true, // forceUpdate = true since we're loading lazy routes _matchRoutes, @@ -559,13 +537,7 @@ function wrapPatchRoutesOnNavigation( if (pathname) { updateNavigationSpan( activeRootSpan, - { - pathname, - search: '', - hash: '', - state: null, - key: 'default', - }, + { pathname, search: '', hash: '', state: null, key: 'default' }, Array.from(allRoutes), false, // forceUpdate = false since this is after lazy routes are loaded _matchRoutes, @@ -604,18 +576,20 @@ export function handleNavigation(opts: { basename, ); - // Check if this might be a lazy route context - const isLazyRouteContext = isLikelyLazyRouteContext(allRoutes || routes, location); - const activeSpan = getActiveSpan(); 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 && activeSpan && spanJson) { - handleExistingNavigationSpan(activeSpan, spanJson, name, source, isLazyRouteContext); - } else { - createNewNavigationSpan(client, name, source, version, isLazyRouteContext); + if (!isAlreadyInNavigationSpan) { + startBrowserTracingNavigationSpan(client, { + name, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source, + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: `auto.navigation.react.reactrouter_v${version}`, + }, + }); } } } @@ -726,13 +700,7 @@ export function createV6CompatibleWithSentryReactRouterRouting

, - name: string, - source: TransactionSource, - isLikelyLazyRoute: boolean, -): void { - // Check if we've already set the name for this span using a custom property - const hasBeenNamed = ( - activeSpan as { - __sentry_navigation_name_set__?: boolean; - } - )?.__sentry_navigation_name_set__; - - if (!hasBeenNamed) { - // This is the first time we're setting the name for this span - if (!spanJson.timestamp) { - activeSpan?.updateName(name); - } - - // For lazy routes, don't mark as named yet so it can be updated later - if (!isLikelyLazyRoute) { - addNonEnumerableProperty( - activeSpan as { __sentry_navigation_name_set__?: boolean }, - '__sentry_navigation_name_set__', - true, - ); - } - } - - // Always set the source attribute to keep it consistent with the current route - activeSpan?.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source); -} - -/** - * Creates a new navigation span - */ -export function createNewNavigationSpan( - client: Client, - name: string, - source: TransactionSource, - version: string, - isLikelyLazyRoute: boolean, -): void { - const newSpan = startBrowserTracingNavigationSpan(client, { - name, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: `auto.navigation.react.reactrouter_v${version}`, - }, - }); - - // For lazy routes, don't mark as named yet so it can be updated later when the route loads - if (!isLikelyLazyRoute && newSpan) { - addNonEnumerableProperty( - newSpan as { __sentry_navigation_name_set__?: boolean }, - '__sentry_navigation_name_set__', - true, - ); - } -} diff --git a/packages/react/src/reactrouter-compat-utils/utils.ts b/packages/react/src/reactrouter-compat-utils/utils.ts index 8f7abb7d548e..c0750c17c57c 100644 --- a/packages/react/src/reactrouter-compat-utils/utils.ts +++ b/packages/react/src/reactrouter-compat-utils/utils.ts @@ -14,37 +14,6 @@ export function initializeRouterUtils(matchRoutes: MatchRoutes, stripBasename: b _stripBasename = stripBasename; } -/** - * Checks if the given routes or location context suggests this might be a lazy route scenario. - * This helps determine if we should delay marking navigation spans as "named" to allow for updates - * when lazy routes are loaded. - */ -export function isLikelyLazyRouteContext(routes: RouteObject[], location: Location): boolean { - // Check if any route in the current match has lazy properties - const hasLazyRoute = routes.some(route => { - return ( - // React Router lazy() route - route.lazy || - // Route with async handlers that might load child routes - (route.handle && - typeof route.handle === 'object' && - Object.values(route.handle).some(handler => typeof handler === 'function')) - ); - }); - - if (hasLazyRoute) { - return true; - } - - // Check if current route is unmatched, which might indicate a lazy route that hasn't loaded yet - const currentMatches = _matchRoutes(routes, location); - if (!currentMatches || currentMatches.length === 0) { - return true; - } - - return false; -} - // Helper functions function pickPath(match: RouteMatch): string { return trimWildcard(match.route.path || ''); diff --git a/packages/react/test/reactrouter-compat-utils/instrumentation.test.tsx b/packages/react/test/reactrouter-compat-utils/instrumentation.test.tsx index 840adbf0a816..0eeeeb342287 100644 --- a/packages/react/test/reactrouter-compat-utils/instrumentation.test.tsx +++ b/packages/react/test/reactrouter-compat-utils/instrumentation.test.tsx @@ -1,16 +1,13 @@ /** * @vitest-environment jsdom */ -import { startBrowserTracingNavigationSpan } from '@sentry/browser'; import type { Client, Span } from '@sentry/core'; import { addNonEnumerableProperty } from '@sentry/core'; import * as React from 'react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { addResolvedRoutesToParent, - createNewNavigationSpan, createReactRouterV6CompatibleTracingIntegration, - handleExistingNavigationSpan, updateNavigationSpan, } from '../../src/reactrouter-compat-utils'; import type { Location, RouteObject } from '../../src/types'; @@ -94,43 +91,6 @@ describe('reactrouter-compat-utils/instrumentation', () => { }); }); - describe('handleExistingNavigationSpan', () => { - it('should update span name when not already named', () => { - const spanJson = { op: 'navigation', data: {}, span_id: 'test', start_timestamp: 0, trace_id: 'test' }; - - handleExistingNavigationSpan(mockSpan, spanJson, 'Test Route', 'route', false); - - expect(mockUpdateName).toHaveBeenCalledWith('Test Route'); - expect(mockSetAttribute).toHaveBeenCalledWith('sentry.source', 'route'); - expect(addNonEnumerableProperty).toHaveBeenCalledWith(mockSpan, '__sentry_navigation_name_set__', true); - }); - - it('should not mark as named for lazy routes', () => { - const spanJson = { op: 'navigation', data: {}, span_id: 'test', start_timestamp: 0, trace_id: 'test' }; - - handleExistingNavigationSpan(mockSpan, spanJson, 'Test Route', 'route', true); - - expect(mockUpdateName).toHaveBeenCalledWith('Test Route'); - expect(mockSetAttribute).toHaveBeenCalledWith('sentry.source', 'route'); - expect(addNonEnumerableProperty).not.toHaveBeenCalled(); - }); - }); - - describe('createNewNavigationSpan', () => { - it('should create new navigation span with correct attributes', () => { - createNewNavigationSpan(mockClient, 'Test Route', 'route', '6', false); - - expect(startBrowserTracingNavigationSpan).toHaveBeenCalledWith(mockClient, { - name: 'Test Route', - attributes: { - 'sentry.source': 'route', - 'sentry.op': 'navigation', - 'sentry.origin': 'auto.navigation.react.reactrouter_v6', - }, - }); - }); - }); - describe('addResolvedRoutesToParent', () => { it('should add new routes to parent with no existing children', () => { const parentRoute: RouteObject = { path: '/parent', element:

Parent
}; diff --git a/packages/react/test/reactrouter-cross-usage.test.tsx b/packages/react/test/reactrouter-cross-usage.test.tsx index 76cfe59f3df5..77d8e3d95b2e 100644 --- a/packages/react/test/reactrouter-cross-usage.test.tsx +++ b/packages/react/test/reactrouter-cross-usage.test.tsx @@ -36,10 +36,7 @@ import { const mockStartBrowserTracingPageLoadSpan = vi.fn(); const mockStartBrowserTracingNavigationSpan = vi.fn(); -const mockNavigationSpan = { - updateName: vi.fn(), - setAttribute: vi.fn(), -}; +const mockNavigationSpan = { updateName: vi.fn(), setAttribute: vi.fn() }; const mockRootSpan = { updateName: vi.fn(), @@ -126,65 +123,29 @@ describe('React Router cross usage of wrappers', () => { const ThirdLevelRoutes: React.FC = () => sentryUseRoutes([ - { - path: '/', - element:
, - }, - { - path: ':id', - element: , - }, + { path: '/', element:
}, + { path: ':id', element: }, ]); const SecondLevelRoutes: React.FC = () => sentryUseRoutes([ - { - path: 'third-level/*', - element: , - }, - { - path: '/', - element:
, - }, - { - path: '*', - element:
, - }, + { path: 'third-level/*', element: }, + { path: '/', element:
}, + { path: '*', element:
}, ]); const TopLevelRoutes: React.FC = () => sentryUseRoutes([ - { - path: 'second-level/:id/*', - element: , - }, - { - path: '/', - element:
, - }, - { - path: '*', - element:
, - }, + { path: 'second-level/:id/*', element: }, + { path: '/', element:
}, + { path: '*', element:
}, ]); const createSentryMemoryRouter = wrapCreateMemoryRouterV6(createMemoryRouter); - const router = createSentryMemoryRouter( - [ - { - children: [ - { - path: '/*', - element: , - }, - ], - }, - ], - { - initialEntries: ['/second-level/321/third-level/123'], - }, - ); + const router = createSentryMemoryRouter([{ children: [{ path: '/*', element: }] }], { + initialEntries: ['/second-level/321/third-level/123'], + }); const { container } = render( @@ -218,42 +179,21 @@ describe('React Router cross usage of wrappers', () => { const ThirdLevelRoutes: React.FC = () => sentryUseRoutes([ - { - path: '/', - element:
, - }, - { - path: ':id', - element: , - }, + { path: '/', element:
}, + { path: ':id', element: }, ]); const SecondLevelRoutes: React.FC = () => sentryUseRoutes([ - { - path: 'third-level/*', - element: , - }, - { - path: '/', - element:
, - }, - { - path: '*', - element:
, - }, + { path: 'third-level/*', element: }, + { path: '/', element:
}, + { path: '*', element:
}, ]); const TopLevelRoutes: React.FC = () => sentryUseRoutes([ - { - path: 'second-level/:id/*', - element: , - }, - { - path: '*', - element:
, - }, + { path: 'second-level/:id/*', element: }, + { path: '*', element:
}, ]); const createSentryMemoryRouter = wrapCreateMemoryRouterV6(createMemoryRouter); @@ -262,20 +202,12 @@ describe('React Router cross usage of wrappers', () => { [ { children: [ - { - path: '/*', - element: , - }, - { - path: '/navigate', - element: , - }, + { path: '/*', element: }, + { path: '/navigate', element: }, ], }, ], - { - initialEntries: ['/navigate'], - }, + { initialEntries: ['/navigate'] }, ); const { container } = render( @@ -319,46 +251,22 @@ describe('React Router cross usage of wrappers', () => { const ThirdLevelRoutes: React.FC = () => sentryUseRoutes([ - { - path: '/', - element:
, - }, - { - path: ':id', - element: , - }, + { path: '/', element:
}, + { path: ':id', element: }, ]); const SecondLevelRoutes: React.FC = () => sentryUseRoutes([ - { - path: 'third-level/*', - element: , - }, - { - path: '/', - element:
, - }, - { - path: '*', - element:
, - }, + { path: 'third-level/*', element: }, + { path: '/', element:
}, + { path: '*', element:
}, ]); const TopLevelRoutes: React.FC = () => sentryUseRoutes([ - { - path: 'second-level/:id/*', - element: , - }, - { - path: '/', - element:
, - }, - { - path: '*', - element:
, - }, + { path: 'second-level/:id/*', element: }, + { path: '/', element:
}, + { path: '*', element:
}, ]); const SentryRoutes = withSentryReactRouterV6Routing(Routes); @@ -399,42 +307,21 @@ describe('React Router cross usage of wrappers', () => { const ThirdLevelRoutes: React.FC = () => sentryUseRoutes([ - { - path: '/', - element:
, - }, - { - path: ':id', - element: , - }, + { path: '/', element:
}, + { path: ':id', element: }, ]); const SecondLevelRoutes: React.FC = () => sentryUseRoutes([ - { - path: 'third-level/*', - element: , - }, - { - path: '/', - element:
, - }, - { - path: '*', - element:
, - }, + { path: 'third-level/*', element: }, + { path: '/', element:
}, + { path: '*', element:
}, ]); const TopLevelRoutes: React.FC = () => sentryUseRoutes([ - { - path: 'second-level/:id/*', - element: , - }, - { - path: '*', - element:
, - }, + { path: 'second-level/:id/*', element: }, + { path: '*', element:
}, ]); const SentryRoutes = withSentryReactRouterV6Routing(Routes); @@ -500,21 +387,9 @@ describe('React Router cross usage of wrappers', () => { ); - const router = createSentryMemoryRouter( - [ - { - children: [ - { - path: '/*', - element: , - }, - ], - }, - ], - { - initialEntries: ['/second-level/321/third-level/123'], - }, - ); + const router = createSentryMemoryRouter([{ children: [{ path: '/*', element: }] }], { + initialEntries: ['/second-level/321/third-level/123'], + }); const { container } = render( @@ -574,20 +449,12 @@ describe('React Router cross usage of wrappers', () => { [ { children: [ - { - path: '/*', - element: , - }, - { - path: '/navigate', - element: , - }, + { path: '/*', element: }, + { path: '/navigate', element: }, ], }, ], - { - initialEntries: ['/navigate'], - }, + { initialEntries: ['/navigate'] }, ); const { container } = render( @@ -633,14 +500,8 @@ describe('React Router cross usage of wrappers', () => { const ThirdLevelRoutes: React.FC = () => sentryUseRoutes([ - { - path: '/', - element:
, - }, - { - path: ':id', - element: , - }, + { path: '/', element:
}, + { path: ':id', element: }, ]); const SecondLevelRoutes: React.FC = () => ( @@ -653,33 +514,15 @@ describe('React Router cross usage of wrappers', () => { const TopLevelRoutes: React.FC = () => sentryUseRoutes([ - { - path: 'second-level/:id/*', - element: , - }, - { - path: '*', - element:
, - }, + { path: 'second-level/:id/*', element: }, + { path: '*', element:
}, ]); const createSentryMemoryRouter = wrapCreateMemoryRouterV6(createMemoryRouter); - const router = createSentryMemoryRouter( - [ - { - children: [ - { - path: '/*', - element: , - }, - ], - }, - ], - { - initialEntries: ['/second-level/321/third-level/123'], - }, - ); + const router = createSentryMemoryRouter([{ children: [{ path: '/*', element: }] }], { + initialEntries: ['/second-level/321/third-level/123'], + }); const { container } = render( @@ -714,14 +557,8 @@ describe('React Router cross usage of wrappers', () => { const ThirdLevelRoutes: React.FC = () => sentryUseRoutes([ - { - path: '/', - element:
, - }, - { - path: ':id', - element: , - }, + { path: '/', element:
}, + { path: ':id', element: }, ]); const SecondLevelRoutes: React.FC = () => ( @@ -734,14 +571,8 @@ describe('React Router cross usage of wrappers', () => { const TopLevelRoutes: React.FC = () => sentryUseRoutes([ - { - path: 'second-level/:id/*', - element: , - }, - { - path: '*', - element:
, - }, + { path: 'second-level/:id/*', element: }, + { path: '*', element:
}, ]); const createSentryMemoryRouter = wrapCreateMemoryRouterV6(createMemoryRouter); @@ -750,20 +581,12 @@ describe('React Router cross usage of wrappers', () => { [ { children: [ - { - path: '/*', - element: , - }, - { - path: '/navigate', - element: , - }, + { path: '/*', element: }, + { path: '/navigate', element: }, ], }, ], - { - initialEntries: ['/navigate'], - }, + { initialEntries: ['/navigate'] }, ); const { container } = render( @@ -773,10 +596,15 @@ 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(mockNavigationSpan.setAttribute).toHaveBeenLastCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/second-level/:id/third-level/:id', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + }, + }); }); }); });

AltStyle によって変換されたページ (->オリジナル) /