From 2f131f5270d54c01bebe665f9085a16dcf3a6d3c Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Mon, 22 Aug 2022 15:28:06 +0100 Subject: [PATCH 1/4] feat(react): Support `useRoutes` hook of React Router 6. --- packages/react/src/index.ts | 2 +- packages/react/src/reactrouterv6.tsx | 70 ++++ packages/react/test/reactrouterv6.test.tsx | 455 +++++++++++++++++---- 3 files changed, 448 insertions(+), 79 deletions(-) diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index d9fb753fc0d9..976929e1ca4b 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -7,4 +7,4 @@ export { ErrorBoundary, withErrorBoundary } from './errorboundary'; export { createReduxEnhancer } from './redux'; export { reactRouterV3Instrumentation } from './reactrouterv3'; export { reactRouterV4Instrumentation, reactRouterV5Instrumentation, withSentryRouting } from './reactrouter'; -export { reactRouterV6Instrumentation, withSentryReactRouterV6Routing } from './reactrouterv6'; +export { reactRouterV6Instrumentation, withSentryReactRouterV6Routing, wrapUseRoutes } from './reactrouterv6'; diff --git a/packages/react/src/reactrouterv6.tsx b/packages/react/src/reactrouterv6.tsx index a7950ce0b12f..f0a6b8504be9 100644 --- a/packages/react/src/reactrouterv6.tsx +++ b/packages/react/src/reactrouterv6.tsx @@ -20,6 +20,8 @@ type Params = { readonly [key in Key]: string | undefined; }; +type UseRoutes = (routes: RouteObject[], locationArg?: Partial | string) => React.ReactElement | null; + // https://github.com/remix-run/react-router/blob/9fa54d643134cd75a0335581a75db8100ed42828/packages/react-router/lib/router.ts#L114-L134 interface RouteMatch { /** @@ -217,3 +219,71 @@ export function withSentryReactRouterV6Routing

, R // will break advanced type inference done by react router params return SentryRoutes; } + +export function wrapUseRoutes(origUseRoutes: UseRoutes): UseRoutes { + if (!_useEffect || !_useLocation || !_useNavigationType || !_matchRoutes) { + logger.warn('react-router-v6 is not initialized'); + return origUseRoutes; + } + let isBaseLocation: boolean = false; + + return (routes: RouteObject[], location?: Partial | string): React.ReactElement | null => { + const SentryRoutes: React.FC = props => { + const Routes = origUseRoutes(routes, location); + + // TODO: Simplify + const locationObject = + (typeof location === 'string' + ? { pathname: location } + : location?.pathname + ? (location as Location) + : _useLocation()) || _useLocation(); + + const navigationType = _useNavigationType(); + + _useEffect(() => { + isBaseLocation = true; + + if (activeTransaction) { + const [name, source] = getNormalizedName(routes, locationObject, _matchRoutes); + activeTransaction.setName(name); + activeTransaction.setMetadata({ source }); + } + // eslint-disable-next-line react/prop-types + }, [props.children]); + + _useEffect(() => { + if (isBaseLocation) { + if (activeTransaction) { + activeTransaction.finish(); + } + + return; + } + + if (_startTransactionOnLocationChange && (navigationType === 'PUSH' || navigationType === 'POP')) { + if (activeTransaction) { + activeTransaction.finish(); + } + + const [name, source] = getNormalizedName(routes, locationObject, _matchRoutes); + activeTransaction = _customStartTransaction({ + name, + op: 'navigation', + tags: SENTRY_TAGS, + metadata: { + source, + }, + }); + } + // eslint-disable-next-line react/prop-types + }, [props.children, locationObject, navigationType, isBaseLocation]); + + isBaseLocation = false; + + return Routes; + }; + + return ; + }; +} diff --git a/packages/react/test/reactrouterv6.test.tsx b/packages/react/test/reactrouterv6.test.tsx index 0058f257aee5..b9d9b71e8f21 100644 --- a/packages/react/test/reactrouterv6.test.tsx +++ b/packages/react/test/reactrouterv6.test.tsx @@ -10,10 +10,11 @@ import { Routes, useLocation, useNavigationType, + useRoutes, } from 'react-router-6'; import { reactRouterV6Instrumentation } from '../src'; -import { withSentryReactRouterV6Routing } from '../src/reactrouterv6'; +import { withSentryReactRouterV6Routing, wrapUseRoutes } from '../src/reactrouterv6'; describe('React Router v6', () => { function createInstrumentation(_opts?: { @@ -43,52 +44,265 @@ describe('React Router v6', () => { return [mockStartTransaction, { mockSetName, mockFinish, mockSetMetadata }]; } - it('starts a pageload transaction', () => { - const [mockStartTransaction] = createInstrumentation(); - const SentryRoutes = withSentryReactRouterV6Routing(Routes); + describe('withSentryReactRouterV6Routing', () => { + it('starts a pageload transaction', () => { + const [mockStartTransaction] = createInstrumentation(); + const SentryRoutes = withSentryReactRouterV6Routing(Routes); - render( - - - Home} /> - - , - ); + render( + + + Home} /> + + , + ); - expect(mockStartTransaction).toHaveBeenCalledTimes(1); - expect(mockStartTransaction).toHaveBeenLastCalledWith({ - name: '/', - op: 'pageload', - tags: { 'routing.instrumentation': 'react-router-v6' }, - metadata: { source: 'url' }, + expect(mockStartTransaction).toHaveBeenCalledTimes(1); + expect(mockStartTransaction).toHaveBeenLastCalledWith({ + name: '/', + op: 'pageload', + tags: { 'routing.instrumentation': 'react-router-v6' }, + metadata: { source: 'url' }, + }); + }); + + it('skips pageload transaction with `startTransactionOnPageLoad: false`', () => { + const [mockStartTransaction] = createInstrumentation({ startTransactionOnPageLoad: false }); + const SentryRoutes = withSentryReactRouterV6Routing(Routes); + + render( + + + Home} /> + + , + ); + + expect(mockStartTransaction).toHaveBeenCalledTimes(0); + }); + + it('skips navigation transaction, with `startTransactionOnLocationChange: false`', () => { + const [mockStartTransaction] = createInstrumentation({ startTransactionOnLocationChange: false }); + const SentryRoutes = withSentryReactRouterV6Routing(Routes); + + render( + + + About} /> + } /> + + , + ); + + expect(mockStartTransaction).toHaveBeenCalledTimes(1); + expect(mockStartTransaction).toHaveBeenLastCalledWith({ + name: '/', + op: 'pageload', + tags: { 'routing.instrumentation': 'react-router-v6' }, + metadata: { source: 'url' }, + }); + }); + + it('starts a navigation transaction', () => { + const [mockStartTransaction] = createInstrumentation(); + const SentryRoutes = withSentryReactRouterV6Routing(Routes); + + render( + + + About} /> + } /> + + , + ); + + expect(mockStartTransaction).toHaveBeenCalledTimes(2); + expect(mockStartTransaction).toHaveBeenLastCalledWith({ + name: '/about', + op: 'navigation', + tags: { 'routing.instrumentation': 'react-router-v6' }, + metadata: { source: 'route' }, + }); + }); + + it('works with nested routes', () => { + const [mockStartTransaction] = createInstrumentation(); + const SentryRoutes = withSentryReactRouterV6Routing(Routes); + + render( + + + About}> + us} /> + + } /> + + , + ); + + expect(mockStartTransaction).toHaveBeenCalledTimes(2); + expect(mockStartTransaction).toHaveBeenLastCalledWith({ + name: '/about/us', + op: 'navigation', + tags: { 'routing.instrumentation': 'react-router-v6' }, + metadata: { source: 'route' }, + }); + }); + + it('works with paramaterized paths', () => { + const [mockStartTransaction] = createInstrumentation(); + const SentryRoutes = withSentryReactRouterV6Routing(Routes); + + render( + + + About}> + page} /> + + } /> + + , + ); + + expect(mockStartTransaction).toHaveBeenCalledTimes(2); + expect(mockStartTransaction).toHaveBeenLastCalledWith({ + name: '/about/:page', + op: 'navigation', + tags: { 'routing.instrumentation': 'react-router-v6' }, + metadata: { source: 'route' }, + }); + }); + + it('works with paths with multiple parameters', () => { + const [mockStartTransaction] = createInstrumentation(); + const SentryRoutes = withSentryReactRouterV6Routing(Routes); + + render( + + + Stores}> + Store}> + Product} /> + + + } /> + + , + ); + + expect(mockStartTransaction).toHaveBeenCalledTimes(2); + expect(mockStartTransaction).toHaveBeenLastCalledWith({ + name: '/stores/:storeId/products/:productId', + op: 'navigation', + tags: { 'routing.instrumentation': 'react-router-v6' }, + metadata: { source: 'route' }, + }); + }); + + it('works with nested paths with parameters', () => { + const [mockStartTransaction] = createInstrumentation(); + const SentryRoutes = withSentryReactRouterV6Routing(Routes); + + render( + + + } /> + Account Page} /> + + Project Index} /> + Project Page}> + Project Page Root} /> + Editor}> + View Canvas} /> + Space Canvas} /> + + + + + No Match Page} /> + + , + ); + + expect(mockStartTransaction).toHaveBeenCalledTimes(2); + expect(mockStartTransaction).toHaveBeenLastCalledWith({ + name: '/projects/:projectId/views/:viewId', + op: 'navigation', + tags: { 'routing.instrumentation': 'react-router-v6' }, + metadata: { source: 'route' }, + }); }); }); - it('skips pageload transaction with `startTransactionOnPageLoad: false`', () => { - const [mockStartTransaction] = createInstrumentation({ startTransactionOnPageLoad: false }); - const SentryRoutes = withSentryReactRouterV6Routing(Routes); + describe('wrapUseRoutes', () => { + it('starts a pageload transaction', () => { + const [mockStartTransaction] = createInstrumentation(); + const wrappedUseRoutes = wrapUseRoutes(useRoutes); - render( - - - Home} /> - - , - ); + const Routes = () => + wrappedUseRoutes([ + { + path: '/', + element:

Home
, + }, + ]); + + render( + + + , + ); - expect(mockStartTransaction).toHaveBeenCalledTimes(0); + expect(mockStartTransaction).toHaveBeenCalledTimes(1); + expect(mockStartTransaction).toHaveBeenLastCalledWith({ + name: '/', + op: 'pageload', + tags: { 'routing.instrumentation': 'react-router-v6' }, + metadata: { source: 'url' }, + }); + }); + + it('skips pageload transaction with `startTransactionOnPageLoad: false`', () => { + const [mockStartTransaction] = createInstrumentation({ startTransactionOnPageLoad: false }); + const wrappedUseRoutes = wrapUseRoutes(useRoutes); + + const Routes = () => + wrappedUseRoutes([ + { + path: '/', + element:
Home
, + }, + ]); + + render( + + + , + ); + + expect(mockStartTransaction).toHaveBeenCalledTimes(0); + }); }); it('skips navigation transaction, with `startTransactionOnLocationChange: false`', () => { const [mockStartTransaction] = createInstrumentation({ startTransactionOnLocationChange: false }); - const SentryRoutes = withSentryReactRouterV6Routing(Routes); + const wrappedUseRoutes = wrapUseRoutes(useRoutes); + + const Routes = () => + wrappedUseRoutes([ + { + path: '/', + element: , + }, + { + path: '/about', + element:
About
, + }, + ]); render( - - About} /> - } /> - + , ); @@ -103,14 +317,23 @@ describe('React Router v6', () => { it('starts a navigation transaction', () => { const [mockStartTransaction] = createInstrumentation(); - const SentryRoutes = withSentryReactRouterV6Routing(Routes); + const wrappedUseRoutes = wrapUseRoutes(useRoutes); + + const Routes = () => + wrappedUseRoutes([ + { + path: '/', + element: , + }, + { + path: '/about', + element:
About
, + }, + ]); render( - - About} /> - } /> - + , ); @@ -125,16 +348,29 @@ describe('React Router v6', () => { it('works with nested routes', () => { const [mockStartTransaction] = createInstrumentation(); - const SentryRoutes = withSentryReactRouterV6Routing(Routes); + const wrappedUseRoutes = wrapUseRoutes(useRoutes); + + const Routes = () => + wrappedUseRoutes([ + { + path: '/', + element: , + }, + { + path: '/about', + element:
About
, + children: [ + { + path: '/about/us', + element:
us
, + }, + ], + }, + ]); render( - - About}> - us} /> - - } /> - + , ); @@ -149,16 +385,29 @@ describe('React Router v6', () => { it('works with paramaterized paths', () => { const [mockStartTransaction] = createInstrumentation(); - const SentryRoutes = withSentryReactRouterV6Routing(Routes); + const wrappedUseRoutes = wrapUseRoutes(useRoutes); + + const Routes = () => + wrappedUseRoutes([ + { + path: '/', + element: , + }, + { + path: '/about', + element:
About
, + children: [ + { + path: '/about/:page', + element:
page
, + }, + ], + }, + ]); render( - - About}> - page} /> - - } /> - + , ); @@ -173,18 +422,35 @@ describe('React Router v6', () => { it('works with paths with multiple parameters', () => { const [mockStartTransaction] = createInstrumentation(); - const SentryRoutes = withSentryReactRouterV6Routing(Routes); + const wrappedUseRoutes = wrapUseRoutes(useRoutes); + + const Routes = () => + wrappedUseRoutes([ + { + path: '/', + element: , + }, + { + path: '/stores', + element:
Stores
, + children: [ + { + path: '/stores/:storeId', + element:
Store
, + children: [ + { + path: '/stores/:storeId/products/:productId', + element:
Product
, + }, + ], + }, + ], + }, + ]); render( - - Stores}> - Store}> - Product} /> - - - } /> - + , ); @@ -199,26 +465,59 @@ describe('React Router v6', () => { it('works with nested paths with parameters', () => { const [mockStartTransaction] = createInstrumentation(); - const SentryRoutes = withSentryReactRouterV6Routing(Routes); + const wrappedUseRoutes = wrapUseRoutes(useRoutes); + + const Routes = () => + wrappedUseRoutes([ + { + index: true, + element: , + }, + { + path: 'account', + element:
Account Page
, + }, + { + path: 'projects', + children: [ + { + index: true, + element:
Project Index
, + }, + { + path: ':projectId', + element:
Project Page
, + children: [ + { + index: true, + element:
Project Page Root
, + }, + { + element:
Editor
, + children: [ + { + path: 'views/:viewId', + element:
View Canvas
, + }, + { + path: 'spaces/:spaceId', + element:
Space Canvas
, + }, + ], + }, + ], + }, + ], + }, + { + path: '*', + element:
No Match Page
, + }, + ]); render( - - } /> - Account Page} /> - - Project Index} /> - Project Page}> - Project Page Root} /> - Editor}> - View Canvas} /> - Space Canvas} /> - - - - - No Match Page} /> - + , ); From 35c8ef7ec1d87627fca38463a33c38a4b986ab57 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Tue, 23 Aug 2022 09:34:07 +0100 Subject: [PATCH 2/4] Fix test grouping. --- packages/react/test/reactrouterv6.test.tsx | 468 ++++++++++----------- 1 file changed, 234 insertions(+), 234 deletions(-) diff --git a/packages/react/test/reactrouterv6.test.tsx b/packages/react/test/reactrouterv6.test.tsx index b9d9b71e8f21..60031f7eed29 100644 --- a/packages/react/test/reactrouterv6.test.tsx +++ b/packages/react/test/reactrouterv6.test.tsx @@ -282,251 +282,251 @@ describe('React Router v6', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(0); }); - }); - it('skips navigation transaction, with `startTransactionOnLocationChange: false`', () => { - const [mockStartTransaction] = createInstrumentation({ startTransactionOnLocationChange: false }); - const wrappedUseRoutes = wrapUseRoutes(useRoutes); - - const Routes = () => - wrappedUseRoutes([ - { - path: '/', - element: , - }, - { - path: '/about', - element:
About
, - }, - ]); - - render( - - - , - ); - - expect(mockStartTransaction).toHaveBeenCalledTimes(1); - expect(mockStartTransaction).toHaveBeenLastCalledWith({ - name: '/', - op: 'pageload', - tags: { 'routing.instrumentation': 'react-router-v6' }, - metadata: { source: 'url' }, + it('skips navigation transaction, with `startTransactionOnLocationChange: false`', () => { + const [mockStartTransaction] = createInstrumentation({ startTransactionOnLocationChange: false }); + const wrappedUseRoutes = wrapUseRoutes(useRoutes); + + const Routes = () => + wrappedUseRoutes([ + { + path: '/', + element: , + }, + { + path: '/about', + element:
About
, + }, + ]); + + render( + + + , + ); + + expect(mockStartTransaction).toHaveBeenCalledTimes(1); + expect(mockStartTransaction).toHaveBeenLastCalledWith({ + name: '/', + op: 'pageload', + tags: { 'routing.instrumentation': 'react-router-v6' }, + metadata: { source: 'url' }, + }); }); - }); - it('starts a navigation transaction', () => { - const [mockStartTransaction] = createInstrumentation(); - const wrappedUseRoutes = wrapUseRoutes(useRoutes); - - const Routes = () => - wrappedUseRoutes([ - { - path: '/', - element: , - }, - { - path: '/about', - element:
About
, - }, - ]); - - render( - - - , - ); - - expect(mockStartTransaction).toHaveBeenCalledTimes(2); - expect(mockStartTransaction).toHaveBeenLastCalledWith({ - name: '/about', - op: 'navigation', - tags: { 'routing.instrumentation': 'react-router-v6' }, - metadata: { source: 'route' }, + it('starts a navigation transaction', () => { + const [mockStartTransaction] = createInstrumentation(); + const wrappedUseRoutes = wrapUseRoutes(useRoutes); + + const Routes = () => + wrappedUseRoutes([ + { + path: '/', + element: , + }, + { + path: '/about', + element:
About
, + }, + ]); + + render( + + + , + ); + + expect(mockStartTransaction).toHaveBeenCalledTimes(2); + expect(mockStartTransaction).toHaveBeenLastCalledWith({ + name: '/about', + op: 'navigation', + tags: { 'routing.instrumentation': 'react-router-v6' }, + metadata: { source: 'route' }, + }); }); - }); - it('works with nested routes', () => { - const [mockStartTransaction] = createInstrumentation(); - const wrappedUseRoutes = wrapUseRoutes(useRoutes); - - const Routes = () => - wrappedUseRoutes([ - { - path: '/', - element: , - }, - { - path: '/about', - element:
About
, - children: [ - { - path: '/about/us', - element:
us
, - }, - ], - }, - ]); - - render( - - - , - ); - - expect(mockStartTransaction).toHaveBeenCalledTimes(2); - expect(mockStartTransaction).toHaveBeenLastCalledWith({ - name: '/about/us', - op: 'navigation', - tags: { 'routing.instrumentation': 'react-router-v6' }, - metadata: { source: 'route' }, + it('works with nested routes', () => { + const [mockStartTransaction] = createInstrumentation(); + const wrappedUseRoutes = wrapUseRoutes(useRoutes); + + const Routes = () => + wrappedUseRoutes([ + { + path: '/', + element: , + }, + { + path: '/about', + element:
About
, + children: [ + { + path: '/about/us', + element:
us
, + }, + ], + }, + ]); + + render( + + + , + ); + + expect(mockStartTransaction).toHaveBeenCalledTimes(2); + expect(mockStartTransaction).toHaveBeenLastCalledWith({ + name: '/about/us', + op: 'navigation', + tags: { 'routing.instrumentation': 'react-router-v6' }, + metadata: { source: 'route' }, + }); }); - }); - it('works with paramaterized paths', () => { - const [mockStartTransaction] = createInstrumentation(); - const wrappedUseRoutes = wrapUseRoutes(useRoutes); - - const Routes = () => - wrappedUseRoutes([ - { - path: '/', - element: , - }, - { - path: '/about', - element:
About
, - children: [ - { - path: '/about/:page', - element:
page
, - }, - ], - }, - ]); - - render( - - - , - ); - - expect(mockStartTransaction).toHaveBeenCalledTimes(2); - expect(mockStartTransaction).toHaveBeenLastCalledWith({ - name: '/about/:page', - op: 'navigation', - tags: { 'routing.instrumentation': 'react-router-v6' }, - metadata: { source: 'route' }, + it('works with paramaterized paths', () => { + const [mockStartTransaction] = createInstrumentation(); + const wrappedUseRoutes = wrapUseRoutes(useRoutes); + + const Routes = () => + wrappedUseRoutes([ + { + path: '/', + element: , + }, + { + path: '/about', + element:
About
, + children: [ + { + path: '/about/:page', + element:
page
, + }, + ], + }, + ]); + + render( + + + , + ); + + expect(mockStartTransaction).toHaveBeenCalledTimes(2); + expect(mockStartTransaction).toHaveBeenLastCalledWith({ + name: '/about/:page', + op: 'navigation', + tags: { 'routing.instrumentation': 'react-router-v6' }, + metadata: { source: 'route' }, + }); }); - }); - it('works with paths with multiple parameters', () => { - const [mockStartTransaction] = createInstrumentation(); - const wrappedUseRoutes = wrapUseRoutes(useRoutes); - - const Routes = () => - wrappedUseRoutes([ - { - path: '/', - element: , - }, - { - path: '/stores', - element:
Stores
, - children: [ - { - path: '/stores/:storeId', - element:
Store
, - children: [ - { - path: '/stores/:storeId/products/:productId', - element:
Product
, - }, - ], - }, - ], - }, - ]); - - render( - - - , - ); - - expect(mockStartTransaction).toHaveBeenCalledTimes(2); - expect(mockStartTransaction).toHaveBeenLastCalledWith({ - name: '/stores/:storeId/products/:productId', - op: 'navigation', - tags: { 'routing.instrumentation': 'react-router-v6' }, - metadata: { source: 'route' }, + it('works with paths with multiple parameters', () => { + const [mockStartTransaction] = createInstrumentation(); + const wrappedUseRoutes = wrapUseRoutes(useRoutes); + + const Routes = () => + wrappedUseRoutes([ + { + path: '/', + element: , + }, + { + path: '/stores', + element:
Stores
, + children: [ + { + path: '/stores/:storeId', + element:
Store
, + children: [ + { + path: '/stores/:storeId/products/:productId', + element:
Product
, + }, + ], + }, + ], + }, + ]); + + render( + + + , + ); + + expect(mockStartTransaction).toHaveBeenCalledTimes(2); + expect(mockStartTransaction).toHaveBeenLastCalledWith({ + name: '/stores/:storeId/products/:productId', + op: 'navigation', + tags: { 'routing.instrumentation': 'react-router-v6' }, + metadata: { source: 'route' }, + }); }); - }); - it('works with nested paths with parameters', () => { - const [mockStartTransaction] = createInstrumentation(); - const wrappedUseRoutes = wrapUseRoutes(useRoutes); - - const Routes = () => - wrappedUseRoutes([ - { - index: true, - element: , - }, - { - path: 'account', - element:
Account Page
, - }, - { - path: 'projects', - children: [ - { - index: true, - element:
Project Index
, - }, - { - path: ':projectId', - element:
Project Page
, - children: [ - { - index: true, - element:
Project Page Root
, - }, - { - element:
Editor
, - children: [ - { - path: 'views/:viewId', - element:
View Canvas
, - }, - { - path: 'spaces/:spaceId', - element:
Space Canvas
, - }, - ], - }, - ], - }, - ], - }, - { - path: '*', - element:
No Match Page
, - }, - ]); - - render( - - - , - ); - - expect(mockStartTransaction).toHaveBeenCalledTimes(2); - expect(mockStartTransaction).toHaveBeenLastCalledWith({ - name: '/projects/:projectId/views/:viewId', - op: 'navigation', - tags: { 'routing.instrumentation': 'react-router-v6' }, - metadata: { source: 'route' }, + it('works with nested paths with parameters', () => { + const [mockStartTransaction] = createInstrumentation(); + const wrappedUseRoutes = wrapUseRoutes(useRoutes); + + const Routes = () => + wrappedUseRoutes([ + { + index: true, + element: , + }, + { + path: 'account', + element:
Account Page
, + }, + { + path: 'projects', + children: [ + { + index: true, + element:
Project Index
, + }, + { + path: ':projectId', + element:
Project Page
, + children: [ + { + index: true, + element:
Project Page Root
, + }, + { + element:
Editor
, + children: [ + { + path: 'views/:viewId', + element:
View Canvas
, + }, + { + path: 'spaces/:spaceId', + element:
Space Canvas
, + }, + ], + }, + ], + }, + ], + }, + { + path: '*', + element:
No Match Page
, + }, + ]); + + render( + + + , + ); + + expect(mockStartTransaction).toHaveBeenCalledTimes(2); + expect(mockStartTransaction).toHaveBeenLastCalledWith({ + name: '/projects/:projectId/views/:viewId', + op: 'navigation', + tags: { 'routing.instrumentation': 'react-router-v6' }, + metadata: { source: 'route' }, + }); }); }); }); From 2bebfa4b54920e158cc3f1faffad8b74f9612915 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Tue, 23 Aug 2022 10:24:58 +0100 Subject: [PATCH 3/4] Clean up implementation. --- packages/react/src/reactrouterv6.tsx | 125 ++++++++++++--------------- 1 file changed, 54 insertions(+), 71 deletions(-) diff --git a/packages/react/src/reactrouterv6.tsx b/packages/react/src/reactrouterv6.tsx index f0a6b8504be9..bffadfae891a 100644 --- a/packages/react/src/reactrouterv6.tsx +++ b/packages/react/src/reactrouterv6.tsx @@ -143,6 +143,45 @@ function getNormalizedName( return [location.pathname, 'url']; } +function updatePageloadTransaction(location: Location, routes: RouteObject[]): void { + if (activeTransaction) { + const [name, source] = getNormalizedName(routes, location, _matchRoutes); + activeTransaction.setName(name); + activeTransaction.setMetadata({ source }); + } +} + +function handleNavigation( + location: Location, + routes: RouteObject[], + navigationType: Action, + isBaseLocation: boolean, +): void { + if (isBaseLocation) { + if (activeTransaction) { + activeTransaction.finish(); + } + + return; + } + + if (_startTransactionOnLocationChange && (navigationType === 'PUSH' || navigationType === 'POP')) { + if (activeTransaction) { + activeTransaction.finish(); + } + + const [name, source] = getNormalizedName(routes, location, _matchRoutes); + activeTransaction = _customStartTransaction({ + name, + op: 'navigation', + tags: SENTRY_TAGS, + metadata: { + source, + }, + }); + } +} + export function withSentryReactRouterV6Routing

, R extends React.FC

>(Routes: R): R { if ( !_useEffect || @@ -171,39 +210,12 @@ export function withSentryReactRouterV6Routing

, R routes = _createRoutesFromChildren(props.children); isBaseLocation = true; - if (activeTransaction) { - const [name, source] = getNormalizedName(routes, location, _matchRoutes); - activeTransaction.setName(name); - activeTransaction.setMetadata({ source }); - } - + updatePageloadTransaction(location, routes); // eslint-disable-next-line react-hooks/exhaustive-deps }, [props.children]); _useEffect(() => { - if (isBaseLocation) { - if (activeTransaction) { - activeTransaction.finish(); - } - - return; - } - - if (_startTransactionOnLocationChange && (navigationType === 'PUSH' || navigationType === 'POP')) { - if (activeTransaction) { - activeTransaction.finish(); - } - - const [name, source] = getNormalizedName(routes, location, _matchRoutes); - activeTransaction = _customStartTransaction({ - name, - op: 'navigation', - tags: SENTRY_TAGS, - metadata: { - source, - }, - }); - } + handleNavigation(location, routes, navigationType, isBaseLocation); }, [props.children, location, navigationType, isBaseLocation]); isBaseLocation = false; @@ -222,62 +234,33 @@ export function withSentryReactRouterV6Routing

, R export function wrapUseRoutes(origUseRoutes: UseRoutes): UseRoutes { if (!_useEffect || !_useLocation || !_useNavigationType || !_matchRoutes) { - logger.warn('react-router-v6 is not initialized'); + __DEBUG_BUILD__ && + logger.warn( + 'reactRouterV6Instrumentation was unable to wrap `useRoutes` because of one or more missing parameters.', + ); + return origUseRoutes; } + let isBaseLocation: boolean = false; return (routes: RouteObject[], location?: Partial | string): React.ReactElement | null => { - const SentryRoutes: React.FC = props => { + const SentryRoutes: React.FC = (props: unknown) => { const Routes = origUseRoutes(routes, location); - // TODO: Simplify - const locationObject = - (typeof location === 'string' - ? { pathname: location } - : location?.pathname - ? (location as Location) - : _useLocation()) || _useLocation(); - + const locationArgObject = typeof location === 'string' ? { pathname: location } : location; + const locationObject = (locationArgObject as Location) || _useLocation(); const navigationType = _useNavigationType(); _useEffect(() => { isBaseLocation = true; - if (activeTransaction) { - const [name, source] = getNormalizedName(routes, locationObject, _matchRoutes); - activeTransaction.setName(name); - activeTransaction.setMetadata({ source }); - } - // eslint-disable-next-line react/prop-types - }, [props.children]); + updatePageloadTransaction(locationObject, routes); + }, [props]); _useEffect(() => { - if (isBaseLocation) { - if (activeTransaction) { - activeTransaction.finish(); - } - - return; - } - - if (_startTransactionOnLocationChange && (navigationType === 'PUSH' || navigationType === 'POP')) { - if (activeTransaction) { - activeTransaction.finish(); - } - - const [name, source] = getNormalizedName(routes, locationObject, _matchRoutes); - activeTransaction = _customStartTransaction({ - name, - op: 'navigation', - tags: SENTRY_TAGS, - metadata: { - source, - }, - }); - } - // eslint-disable-next-line react/prop-types - }, [props.children, locationObject, navigationType, isBaseLocation]); + handleNavigation(locationObject, routes, navigationType, isBaseLocation); + }, [props, locationObject, navigationType, isBaseLocation]); isBaseLocation = false; From da7b499efcbe28e5bdf91eba0d3afabb3810b90a Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Mon, 29 Aug 2022 14:03:47 +0100 Subject: [PATCH 4/4] Early exit if `_customStartTransaction` is not defined. --- packages/react/src/reactrouterv6.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/src/reactrouterv6.tsx b/packages/react/src/reactrouterv6.tsx index bffadfae891a..9706eb57e9fe 100644 --- a/packages/react/src/reactrouterv6.tsx +++ b/packages/react/src/reactrouterv6.tsx @@ -233,7 +233,7 @@ export function withSentryReactRouterV6Routing

, R } export function wrapUseRoutes(origUseRoutes: UseRoutes): UseRoutes { - if (!_useEffect || !_useLocation || !_useNavigationType || !_matchRoutes) { + if (!_useEffect || !_useLocation || !_useNavigationType || !_matchRoutes || !_customStartTransaction) { __DEBUG_BUILD__ && logger.warn( 'reactRouterV6Instrumentation was unable to wrap `useRoutes` because of one or more missing parameters.',