Product} />
@@ -330,13 +440,13 @@ function getNormalizedName(
// eslint-disable-next-line deprecation/deprecation
getNumberOfUrlSegments(pathBuilder) !== getNumberOfUrlSegments(branch.pathname) &&
// We should not count wildcard operators in the url segments calculation
- pathBuilder.slice(-2) !== '/*'
+ !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 (pathEndsWithWildcard(pathBuilder, branch)) {
+ if (pathIsWildcardAndHasChildren(pathBuilder, branch)) {
pathBuilder = pathBuilder.slice(0, -1);
}
@@ -347,7 +457,11 @@ function getNormalizedName(
}
}
- return [_stripBasename ? stripBasenameFromPathname(location.pathname, basename) : location.pathname, 'url'];
+ const fallbackTransactionName = _stripBasename
+ ? stripBasenameFromPathname(location.pathname, basename)
+ : location.pathname || '/';
+
+ return [fallbackTransactionName, 'url'];
}
function updatePageloadTransaction(
@@ -356,13 +470,25 @@ function updatePageloadTransaction(
routes: RouteObject[],
matches?: AgnosticDataRouteMatch,
basename?: string,
+ allRoutes?: RouteObject[],
): void {
const branches = Array.isArray(matches)
? matches
: (_matchRoutes(routes, location, basename) as unknown as RouteMatch[]);
if (branches) {
- const [name, source] = getNormalizedName(routes, location, branches, basename);
+ let name,
+ source: TransactionSource = 'url';
+ const isInDescendantRoute = locationIsInsideDescendantRoute(location, allRoutes || routes);
+
+ if (isInDescendantRoute) {
+ name = prefixWithSlash(rebuildRoutePathFromAllRoutes(allRoutes || routes, location));
+ source = 'route';
+ }
+
+ if (!isInDescendantRoute || !name) {
+ [name, source] = getNormalizedName(routes, location, branches, basename);
+ }
getCurrentScope().setTransactionName(name);
@@ -387,9 +513,11 @@ export function createV6CompatibleWithSentryReactRouterRouting = (props: P) => {
+ const isMountRenderPass = React.useRef(true);
+
const location = _useLocation();
const navigationType = _useNavigationType();
@@ -397,11 +525,21 @@ export function createV6CompatibleWithSentryReactRouterRouting
{
const routes = _createRoutesFromChildren(props.children) as RouteObject[];
- if (isMountRenderPass) {
- updatePageloadTransaction(getActiveRootSpan(), location, routes);
- isMountRenderPass = false;
+ if (isMountRenderPass.current) {
+ routes.forEach(route => {
+ allRoutes.push(...getChildRoutesRecursively(route));
+ });
+
+ updatePageloadTransaction(getActiveRootSpan(), location, routes, undefined, undefined, allRoutes);
+ isMountRenderPass.current = false;
} else {
- handleNavigation(location, routes, navigationType, version);
+ handleNavigation({
+ location,
+ routes,
+ navigationType,
+ version,
+ allRoutes,
+ });
}
},
// `props.children` is purposely not included in the dependency array, because we do not want to re-run this effect
diff --git a/packages/react/test/reactrouterv6.4.test.tsx b/packages/react/test/reactrouterv6.4.test.tsx
deleted file mode 100644
index 3ae6a69bdf56..000000000000
--- a/packages/react/test/reactrouterv6.4.test.tsx
+++ /dev/null
@@ -1,676 +0,0 @@
-import {
- SEMANTIC_ATTRIBUTE_SENTRY_OP,
- SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
- SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
- createTransport,
- getCurrentScope,
- setCurrentClient,
-} from '@sentry/core';
-import { render } from '@testing-library/react';
-import { Request } from 'node-fetch';
-import * as React from 'react';
-import {
- Navigate,
- RouterProvider,
- createMemoryRouter,
- createRoutesFromChildren,
- matchRoutes,
- useLocation,
- useNavigationType,
-} from 'react-router-6.4';
-
-import { BrowserClient, wrapCreateBrowserRouter } from '../src';
-import { reactRouterV6BrowserTracingIntegration } from '../src/reactrouterv6';
-import type { CreateRouterFunction } from '../src/types';
-
-beforeAll(() => {
- // @ts-expect-error need to override global Request because it's not in the jest environment (even with an
- // `@jest-environment jsdom` directive, for some reason)
- global.Request = Request;
-});
-
-const mockStartBrowserTracingPageLoadSpan = jest.fn();
-const mockStartBrowserTracingNavigationSpan = jest.fn();
-
-const mockRootSpan = {
- updateName: jest.fn(),
- setAttribute: jest.fn(),
- getSpanJSON() {
- return { op: 'pageload' };
- },
-};
-
-jest.mock('@sentry/browser', () => {
- const actual = jest.requireActual('@sentry/browser');
- return {
- ...actual,
- startBrowserTracingNavigationSpan: (...args: unknown[]) => {
- mockStartBrowserTracingNavigationSpan(...args);
- return actual.startBrowserTracingNavigationSpan(...args);
- },
- startBrowserTracingPageLoadSpan: (...args: unknown[]) => {
- mockStartBrowserTracingPageLoadSpan(...args);
- return actual.startBrowserTracingPageLoadSpan(...args);
- },
- };
-});
-
-jest.mock('@sentry/core', () => {
- const actual = jest.requireActual('@sentry/core');
- return {
- ...actual,
- getRootSpan: () => {
- return mockRootSpan;
- },
- };
-});
-
-describe('reactRouterV6BrowserTracingIntegration (v6.4)', () => {
- function createMockBrowserClient(): BrowserClient {
- return new BrowserClient({
- integrations: [],
- tracesSampleRate: 1,
- transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => Promise.resolve({})),
- stackParser: () => [],
- });
- }
-
- beforeEach(() => {
- jest.clearAllMocks();
- getCurrentScope().setClient(undefined);
- });
-
- describe('wrapCreateBrowserRouter', () => {
- it('starts a pageload transaction', () => {
- const client = createMockBrowserClient();
- setCurrentClient(client);
-
- client.addIntegration(
- reactRouterV6BrowserTracingIntegration({
- useEffect: React.useEffect,
- useLocation,
- useNavigationType,
- createRoutesFromChildren,
- matchRoutes,
- }),
- );
- // eslint-disable-next-line deprecation/deprecation
- const sentryCreateBrowserRouter = wrapCreateBrowserRouter(createMemoryRouter as CreateRouterFunction);
-
- const router = sentryCreateBrowserRouter(
- [
- {
- path: '/',
- element:
TEST
,
- },
- ],
- {
- initialEntries: ['/'],
- },
- );
-
- // @ts-expect-error router is fine
- render();
-
- expect(mockStartBrowserTracingPageLoadSpan).toHaveBeenCalledTimes(1);
- expect(mockStartBrowserTracingPageLoadSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), {
- name: '/',
- attributes: {
- [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
- [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload',
- [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v6',
- },
- });
- });
-
- it("updates the scope's `transactionName` on a pageload", () => {
- const client = createMockBrowserClient();
- setCurrentClient(client);
-
- client.addIntegration(
- reactRouterV6BrowserTracingIntegration({
- useEffect: React.useEffect,
- useLocation,
- useNavigationType,
- createRoutesFromChildren,
- matchRoutes,
- }),
- );
- // eslint-disable-next-line deprecation/deprecation
- const sentryCreateBrowserRouter = wrapCreateBrowserRouter(createMemoryRouter as CreateRouterFunction);
-
- const router = sentryCreateBrowserRouter(
- [
- {
- path: '/',
- element: TEST
,
- },
- ],
- {
- initialEntries: ['/'],
- },
- );
-
- // @ts-expect-error router is fine
- render();
-
- expect(getCurrentScope().getScopeData().transactionName).toEqual('/');
- });
-
- it('starts a navigation transaction', () => {
- const client = createMockBrowserClient();
- setCurrentClient(client);
-
- client.addIntegration(
- reactRouterV6BrowserTracingIntegration({
- useEffect: React.useEffect,
- useLocation,
- useNavigationType,
- createRoutesFromChildren,
- matchRoutes,
- }),
- );
- // eslint-disable-next-line deprecation/deprecation
- const sentryCreateBrowserRouter = wrapCreateBrowserRouter(createMemoryRouter as CreateRouterFunction);
-
- const router = sentryCreateBrowserRouter(
- [
- {
- path: '/',
- element: ,
- },
- {
- path: 'about',
- element: About
,
- },
- ],
- {
- initialEntries: ['/'],
- },
- );
-
- // @ts-expect-error router is fine
- render();
-
- expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1);
- expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), {
- name: '/about',
- attributes: {
- [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
- [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
- [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6',
- },
- });
- });
-
- it('works with nested routes', () => {
- const client = createMockBrowserClient();
- setCurrentClient(client);
-
- client.addIntegration(
- reactRouterV6BrowserTracingIntegration({
- useEffect: React.useEffect,
- useLocation,
- useNavigationType,
- createRoutesFromChildren,
- matchRoutes,
- }),
- );
- // eslint-disable-next-line deprecation/deprecation
- const sentryCreateBrowserRouter = wrapCreateBrowserRouter(createMemoryRouter as CreateRouterFunction);
-
- const router = sentryCreateBrowserRouter(
- [
- {
- path: '/',
- element: ,
- },
- {
- path: 'about',
- element: About
,
- children: [
- {
- path: 'us',
- element: Us
,
- },
- ],
- },
- ],
- {
- initialEntries: ['/'],
- },
- );
-
- // @ts-expect-error router is fine
- render();
-
- expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1);
- expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), {
- name: '/about/us',
- attributes: {
- [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
- [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
- [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6',
- },
- });
- });
-
- it('works with parameterized paths', () => {
- const client = createMockBrowserClient();
- setCurrentClient(client);
-
- client.addIntegration(
- reactRouterV6BrowserTracingIntegration({
- useEffect: React.useEffect,
- useLocation,
- useNavigationType,
- createRoutesFromChildren,
- matchRoutes,
- }),
- );
- // eslint-disable-next-line deprecation/deprecation
- const sentryCreateBrowserRouter = wrapCreateBrowserRouter(createMemoryRouter as CreateRouterFunction);
-
- const router = sentryCreateBrowserRouter(
- [
- {
- path: '/',
- element: ,
- },
- {
- path: 'about',
- element: About
,
- children: [
- {
- path: ':page',
- element: Page
,
- },
- ],
- },
- ],
- {
- initialEntries: ['/'],
- },
- );
-
- // @ts-expect-error router is fine
- render();
-
- expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1);
- expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), {
- name: '/about/:page',
- attributes: {
- [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
- [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
- [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6',
- },
- });
- });
-
- it('works with paths with multiple parameters', () => {
- const client = createMockBrowserClient();
- setCurrentClient(client);
-
- client.addIntegration(
- reactRouterV6BrowserTracingIntegration({
- useEffect: React.useEffect,
- useLocation,
- useNavigationType,
- createRoutesFromChildren,
- matchRoutes,
- }),
- );
- // eslint-disable-next-line deprecation/deprecation
- const sentryCreateBrowserRouter = wrapCreateBrowserRouter(createMemoryRouter as CreateRouterFunction);
-
- const router = sentryCreateBrowserRouter(
- [
- {
- path: '/',
- element: ,
- },
- {
- path: 'stores',
- element: Stores
,
- children: [
- {
- path: ':storeId',
- element: Store
,
- children: [
- {
- path: 'products',
- element: Products
,
- children: [
- {
- path: ':productId',
- element: Product
,
- },
- ],
- },
- ],
- },
- ],
- },
- ],
- {
- initialEntries: ['/'],
- },
- );
-
- // @ts-expect-error router is fine
- render();
-
- expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1);
- expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), {
- name: '/stores/:storeId/products/:productId',
- attributes: {
- [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
- [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
- [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6',
- },
- });
- });
-
- it('updates pageload transaction to a parameterized route', () => {
- const client = createMockBrowserClient();
- setCurrentClient(client);
-
- client.addIntegration(
- reactRouterV6BrowserTracingIntegration({
- useEffect: React.useEffect,
- useLocation,
- useNavigationType,
- createRoutesFromChildren,
- matchRoutes,
- }),
- );
- // eslint-disable-next-line deprecation/deprecation
- const sentryCreateBrowserRouter = wrapCreateBrowserRouter(createMemoryRouter as CreateRouterFunction);
-
- const router = sentryCreateBrowserRouter(
- [
- {
- path: 'about',
- element: About
,
- children: [
- {
- path: ':page',
- element: page
,
- },
- ],
- },
- ],
- {
- initialEntries: ['/about/us'],
- },
- );
-
- // @ts-expect-error router is fine
- render();
-
- expect(mockStartBrowserTracingPageLoadSpan).toHaveBeenCalledTimes(1);
- expect(mockRootSpan.updateName).toHaveBeenLastCalledWith('/about/:page');
- expect(mockRootSpan.setAttribute).toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route');
- });
-
- it('works with `basename` option', () => {
- const client = createMockBrowserClient();
- setCurrentClient(client);
-
- client.addIntegration(
- reactRouterV6BrowserTracingIntegration({
- useEffect: React.useEffect,
- useLocation,
- useNavigationType,
- createRoutesFromChildren,
- matchRoutes,
- }),
- );
- // eslint-disable-next-line deprecation/deprecation
- const sentryCreateBrowserRouter = wrapCreateBrowserRouter(createMemoryRouter as CreateRouterFunction);
-
- const router = sentryCreateBrowserRouter(
- [
- {
- path: '/',
- element: ,
- },
- {
- path: 'about',
- element: About
,
- children: [
- {
- path: 'us',
- element: Us
,
- },
- ],
- },
- ],
- {
- initialEntries: ['/app'],
- basename: '/app',
- },
- );
-
- // @ts-expect-error router is fine
- render();
-
- expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1);
- expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), {
- name: '/app/about/us',
- attributes: {
- [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
- [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
- [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6',
- },
- });
- });
-
- it('works with parameterized paths and `basename`', () => {
- const client = createMockBrowserClient();
- setCurrentClient(client);
-
- client.addIntegration(
- reactRouterV6BrowserTracingIntegration({
- useEffect: React.useEffect,
- useLocation,
- useNavigationType,
- createRoutesFromChildren,
- matchRoutes,
- }),
- );
- // eslint-disable-next-line deprecation/deprecation
- const sentryCreateBrowserRouter = wrapCreateBrowserRouter(createMemoryRouter as CreateRouterFunction);
-
- const router = sentryCreateBrowserRouter(
- [
- {
- path: '/',
- element: ,
- },
- {
- path: ':orgId',
- children: [
- {
- path: 'users',
- children: [
- {
- path: ':userId',
- element: User
,
- },
- ],
- },
- ],
- },
- ],
- {
- initialEntries: ['/admin'],
- basename: '/admin',
- },
- );
-
- // @ts-expect-error router is fine
- render();
-
- expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1);
- expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), {
- name: '/admin/:orgId/users/:userId',
- attributes: {
- [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
- [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
- [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6',
- },
- });
- });
-
- it('strips `basename` from transaction names of parameterized paths', () => {
- const client = createMockBrowserClient();
- setCurrentClient(client);
-
- client.addIntegration(
- reactRouterV6BrowserTracingIntegration({
- useEffect: React.useEffect,
- useLocation,
- useNavigationType,
- createRoutesFromChildren,
- matchRoutes,
- stripBasename: true,
- }),
- );
- // eslint-disable-next-line deprecation/deprecation
- const sentryCreateBrowserRouter = wrapCreateBrowserRouter(createMemoryRouter as CreateRouterFunction);
-
- const router = sentryCreateBrowserRouter(
- [
- {
- path: '/',
- element: ,
- },
- {
- path: ':orgId',
- children: [
- {
- path: 'users',
- children: [
- {
- path: ':userId',
- element: User
,
- },
- ],
- },
- ],
- },
- ],
- {
- initialEntries: ['/admin'],
- basename: '/admin',
- },
- );
-
- // @ts-expect-error router is fine
- render();
-
- expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1);
- expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), {
- name: '/:orgId/users/:userId',
- attributes: {
- [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
- [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
- [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6',
- },
- });
- });
-
- it('strips `basename` from transaction names of non-parameterized paths', () => {
- const client = createMockBrowserClient();
- setCurrentClient(client);
-
- client.addIntegration(
- reactRouterV6BrowserTracingIntegration({
- useEffect: React.useEffect,
- useLocation,
- useNavigationType,
- createRoutesFromChildren,
- matchRoutes,
- stripBasename: true,
- }),
- );
- // eslint-disable-next-line deprecation/deprecation
- const sentryCreateBrowserRouter = wrapCreateBrowserRouter(createMemoryRouter as CreateRouterFunction);
-
- const router = sentryCreateBrowserRouter(
- [
- {
- path: '/',
- element: ,
- },
- {
- path: 'about',
- element: About
,
- children: [
- {
- path: 'us',
- element: Us
,
- },
- ],
- },
- ],
- {
- initialEntries: ['/app'],
- basename: '/app',
- },
- );
-
- // @ts-expect-error router is fine
- render();
-
- expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1);
- expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), {
- name: '/about/us',
- attributes: {
- [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
- [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
- [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6',
- },
- });
- });
-
- it("updates the scope's `transactionName` on a navigation", () => {
- const client = createMockBrowserClient();
- setCurrentClient(client);
-
- client.addIntegration(
- reactRouterV6BrowserTracingIntegration({
- useEffect: React.useEffect,
- useLocation,
- useNavigationType,
- createRoutesFromChildren,
- matchRoutes,
- }),
- );
- // eslint-disable-next-line deprecation/deprecation
- const sentryCreateBrowserRouter = wrapCreateBrowserRouter(createMemoryRouter as CreateRouterFunction);
-
- const router = sentryCreateBrowserRouter(
- [
- {
- path: '/',
- element: ,
- },
- {
- path: 'about',
- element: About
,
- },
- ],
- {
- initialEntries: ['/'],
- },
- );
-
- // @ts-expect-error router is fine
- render();
-
- expect(getCurrentScope().getScopeData().transactionName).toEqual('/about');
- });
- });
-});
diff --git a/packages/react/test/reactrouterv6.test.tsx b/packages/react/test/reactrouterv6.test.tsx
index b9cf4003c330..815b562f08f7 100644
--- a/packages/react/test/reactrouterv6.test.tsx
+++ b/packages/react/test/reactrouterv6.test.tsx
@@ -25,7 +25,7 @@ import { BrowserClient } from '../src';
import {
reactRouterV6BrowserTracingIntegration,
withSentryReactRouterV6Routing,
- wrapUseRoutes,
+ wrapUseRoutesV6,
} from '../src/reactrouterv6';
const mockStartBrowserTracingPageLoadSpan = jest.fn();
@@ -491,6 +491,109 @@ describe('reactRouterV6BrowserTracingIntegration', () => {
});
});
+ it('works with descendant wildcard routes - pageload', () => {
+ const client = createMockBrowserClient();
+ setCurrentClient(client);
+
+ client.addIntegration(
+ reactRouterV6BrowserTracingIntegration({
+ useEffect: React.useEffect,
+ useLocation,
+ useNavigationType,
+ createRoutesFromChildren,
+ matchRoutes,
+ }),
+ );
+ const SentryRoutes = withSentryReactRouterV6Routing(Routes);
+
+ const DetailsRoutes = () => (
+
+ Details} />
+
+ );
+
+ const ViewsRoutes = () => (
+
+ Views} />
+ } />
+
+ );
+
+ const ProjectsRoutes = () => (
+
+ }>
+