From e409e5ecd580f73ea02e06dff252ac49a6389f4e Mon Sep 17 00:00:00 2001 From: Onur Temizkan <onur@narval.co.uk> Date: Wed, 21 Dec 2022 13:54:40 +0000 Subject: [PATCH 1/6] fix(tracing): Don't finish React Router 6 `pageload` transactions early --- packages/react/src/reactrouterv6.tsx | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/packages/react/src/reactrouterv6.tsx b/packages/react/src/reactrouterv6.tsx index 4635296a6bb8..2c1ddef69478 100644 --- a/packages/react/src/reactrouterv6.tsx +++ b/packages/react/src/reactrouterv6.tsx @@ -131,17 +131,8 @@ function handleNavigation( location: Location, routes: RouteObject[], navigationType: Action, - isBaseLocation: boolean, matches?: AgnosticDataRouteMatch, ): void { - if (isBaseLocation) { - if (activeTransaction) { - activeTransaction.finish(); - } - - return; - } - const branches = Array.isArray(matches) ? matches : _matchRoutes(routes, location); if (_startTransactionOnLocationChange && (navigationType === 'PUSH' || navigationType === 'POP') && branches) { @@ -179,12 +170,12 @@ export function withSentryReactRouterV6Routing<P extends Record<string, any>, R return Routes; } - let isBaseLocation: boolean = false; let routes: RouteObject[]; const SentryRoutes: React.FC<P> = (props: P) => { const location = _useLocation(); const navigationType = _useNavigationType(); + let isBaseLocation: boolean = false; _useEffect(() => { // Performance concern: @@ -197,7 +188,9 @@ export function withSentryReactRouterV6Routing<P extends Record<string, any>, R }, [props.children]); _useEffect(() => { - handleNavigation(location, routes, navigationType, isBaseLocation); + if (!isBaseLocation) { + handleNavigation(location, routes, navigationType); + } }, [props.children, location, navigationType, isBaseLocation]); isBaseLocation = false; @@ -224,8 +217,6 @@ export function wrapUseRoutes(origUseRoutes: UseRoutes): UseRoutes { return origUseRoutes; } - let isBaseLocation: boolean = false; - // eslint-disable-next-line react/display-name return (routes: RouteObject[], location?: Partial<Location> | string): React.ReactElement | null => { const SentryRoutes: React.FC<unknown> = (props: unknown) => { @@ -234,6 +225,7 @@ export function wrapUseRoutes(origUseRoutes: UseRoutes): UseRoutes { const locationArgObject = typeof location === 'string' ? { pathname: location } : location; const locationObject = (locationArgObject as Location) || _useLocation(); const navigationType = _useNavigationType(); + let isBaseLocation: boolean = false; _useEffect(() => { isBaseLocation = true; @@ -242,7 +234,9 @@ export function wrapUseRoutes(origUseRoutes: UseRoutes): UseRoutes { }, [props]); _useEffect(() => { - handleNavigation(locationObject, routes, navigationType, isBaseLocation); + if (!isBaseLocation) { + handleNavigation(locationObject, routes, navigationType); + } }, [props, locationObject, navigationType, isBaseLocation]); isBaseLocation = false; @@ -275,7 +269,7 @@ export function wrapCreateBrowserRouter(createRouterFunction: CreateRouterFuncti (state.historyAction === 'PUSH' || state.historyAction === 'POP') && activeTransaction ) { - handleNavigation(location, routes, state.historyAction, false); + handleNavigation(location, routes, state.historyAction); } }); From 8c7b2e75fd3e4b2bdcc0e6ff62b78dee3566d2d6 Mon Sep 17 00:00:00 2001 From: Luca Forstner <luca.forstner@sentry.io> Date: Thu, 22 Dec 2022 14:19:56 +0000 Subject: [PATCH 2/6] Follow react patterns --- packages/react/src/reactrouterv6.tsx | 47 ++++++++++++++++++---------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/packages/react/src/reactrouterv6.tsx b/packages/react/src/reactrouterv6.tsx index 2c1ddef69478..8bd573a9bdb9 100644 --- a/packages/react/src/reactrouterv6.tsx +++ b/packages/react/src/reactrouterv6.tsx @@ -171,29 +171,35 @@ export function withSentryReactRouterV6Routing<P extends Record<string, any>, R } let routes: RouteObject[]; + let isFirstPageloadUpdateUseEffectCall = true; + let isFirstNavigationUseEffectCall = true; const SentryRoutes: React.FC<P> = (props: P) => { const location = _useLocation(); const navigationType = _useNavigationType(); - let isBaseLocation: boolean = false; _useEffect(() => { // Performance concern: // This is repeated when <Routes /> is rendered. routes = _createRoutesFromChildren(props.children) as RouteObject[]; - isBaseLocation = true; - - updatePageloadTransaction(location, routes); - // eslint-disable-next-line react-hooks/exhaustive-deps }, [props.children]); _useEffect(() => { - if (!isBaseLocation) { + if (isFirstNavigationUseEffectCall) { + isFirstNavigationUseEffectCall = false; + } else { handleNavigation(location, routes, navigationType); } - }, [props.children, location, navigationType, isBaseLocation]); + }, [location, navigationType]); - isBaseLocation = false; + _useEffect(() => { + if (isFirstPageloadUpdateUseEffectCall) { + routes = _createRoutesFromChildren(props.children) as RouteObject[]; + updatePageloadTransaction(location, routes); + } else { + isFirstPageloadUpdateUseEffectCall = false; + } + }, [props.children, location]); // @ts-ignore Setting more specific React Component typing for `R` generic above // will break advanced type inference done by react router params @@ -217,29 +223,36 @@ export function wrapUseRoutes(origUseRoutes: UseRoutes): UseRoutes { return origUseRoutes; } + let isFirstPageloadUpdateUseEffectCall = true; + let isFirstNavigationUseEffectCall = true; + // eslint-disable-next-line react/display-name return (routes: RouteObject[], location?: Partial<Location> | string): React.ReactElement | null => { const SentryRoutes: React.FC<unknown> = (props: unknown) => { const Routes = origUseRoutes(routes, location); + // the case where location is a string might be killing performance because we're always creating a new + // object(with different identity) that will trigger useEffects below to run again const locationArgObject = typeof location === 'string' ? { pathname: location } : location; + const locationObject = (locationArgObject as Location) || _useLocation(); const navigationType = _useNavigationType(); - let isBaseLocation: boolean = false; _useEffect(() => { - isBaseLocation = true; - - updatePageloadTransaction(locationObject, routes); - }, [props]); + if (isFirstPageloadUpdateUseEffectCall) { + updatePageloadTransaction(locationObject, routes); + } else { + isFirstPageloadUpdateUseEffectCall = false; + } + }, [locationObject, routes]); _useEffect(() => { - if (!isBaseLocation) { + if (isFirstNavigationUseEffectCall) { handleNavigation(locationObject, routes, navigationType); + } else { + isFirstNavigationUseEffectCall = false; } - }, [props, locationObject, navigationType, isBaseLocation]); - - isBaseLocation = false; + }, [locationObject, routes, navigationType]); return Routes; }; From 9af81a018f0a325625c7a1149395e135acc5f244 Mon Sep 17 00:00:00 2001 From: Luca Forstner <luca.forstner@sentry.io> Date: Thu, 22 Dec 2022 14:22:28 +0000 Subject: [PATCH 3/6] fix logic --- packages/react/src/reactrouterv6.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/react/src/reactrouterv6.tsx b/packages/react/src/reactrouterv6.tsx index 8bd573a9bdb9..f833888625bb 100644 --- a/packages/react/src/reactrouterv6.tsx +++ b/packages/react/src/reactrouterv6.tsx @@ -194,10 +194,9 @@ export function withSentryReactRouterV6Routing<P extends Record<string, any>, R _useEffect(() => { if (isFirstPageloadUpdateUseEffectCall) { + isFirstPageloadUpdateUseEffectCall = false; routes = _createRoutesFromChildren(props.children) as RouteObject[]; updatePageloadTransaction(location, routes); - } else { - isFirstPageloadUpdateUseEffectCall = false; } }, [props.children, location]); @@ -240,17 +239,16 @@ export function wrapUseRoutes(origUseRoutes: UseRoutes): UseRoutes { _useEffect(() => { if (isFirstPageloadUpdateUseEffectCall) { - updatePageloadTransaction(locationObject, routes); - } else { isFirstPageloadUpdateUseEffectCall = false; + updatePageloadTransaction(locationObject, routes); } }, [locationObject, routes]); _useEffect(() => { if (isFirstNavigationUseEffectCall) { - handleNavigation(locationObject, routes, navigationType); - } else { isFirstNavigationUseEffectCall = false; + } else { + handleNavigation(locationObject, routes, navigationType); } }, [locationObject, routes, navigationType]); From 7aee49e7faccb238339bdc8a687761418cca55cf Mon Sep 17 00:00:00 2001 From: Luca Forstner <luca.forstner@sentry.io> Date: Thu, 22 Dec 2022 14:22:39 +0000 Subject: [PATCH 4/6] clean --- packages/react/src/reactrouterv6.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/react/src/reactrouterv6.tsx b/packages/react/src/reactrouterv6.tsx index f833888625bb..cb5298cf7a92 100644 --- a/packages/react/src/reactrouterv6.tsx +++ b/packages/react/src/reactrouterv6.tsx @@ -184,14 +184,6 @@ export function withSentryReactRouterV6Routing<P extends Record<string, any>, R routes = _createRoutesFromChildren(props.children) as RouteObject[]; }, [props.children]); - _useEffect(() => { - if (isFirstNavigationUseEffectCall) { - isFirstNavigationUseEffectCall = false; - } else { - handleNavigation(location, routes, navigationType); - } - }, [location, navigationType]); - _useEffect(() => { if (isFirstPageloadUpdateUseEffectCall) { isFirstPageloadUpdateUseEffectCall = false; @@ -200,6 +192,14 @@ export function withSentryReactRouterV6Routing<P extends Record<string, any>, R } }, [props.children, location]); + _useEffect(() => { + if (isFirstNavigationUseEffectCall) { + isFirstNavigationUseEffectCall = false; + } else { + handleNavigation(location, routes, navigationType); + } + }, [location, navigationType]); + // @ts-ignore Setting more specific React Component typing for `R` generic above // will break advanced type inference done by react router params return <Routes {...props} />; From 81b57cf9ee7318a2886488a9f8cdbac242337ed8 Mon Sep 17 00:00:00 2001 From: Luca Forstner <luca.forstner@sentry.io> Date: Tue, 3 Jan 2023 14:39:36 +0000 Subject: [PATCH 5/6] Clean again --- packages/react/src/reactrouterv6.tsx | 78 +++++++++++++--------------- 1 file changed, 35 insertions(+), 43 deletions(-) diff --git a/packages/react/src/reactrouterv6.tsx b/packages/react/src/reactrouterv6.tsx index cb5298cf7a92..cd0825a34a84 100644 --- a/packages/react/src/reactrouterv6.tsx +++ b/packages/react/src/reactrouterv6.tsx @@ -170,35 +170,28 @@ export function withSentryReactRouterV6Routing<P extends Record<string, any>, R return Routes; } - let routes: RouteObject[]; - let isFirstPageloadUpdateUseEffectCall = true; - let isFirstNavigationUseEffectCall = true; + let previousLocation: Location | null = null; const SentryRoutes: React.FC<P> = (props: P) => { const location = _useLocation(); const navigationType = _useNavigationType(); - _useEffect(() => { - // Performance concern: - // This is repeated when <Routes /> is rendered. - routes = _createRoutesFromChildren(props.children) as RouteObject[]; - }, [props.children]); - - _useEffect(() => { - if (isFirstPageloadUpdateUseEffectCall) { - isFirstPageloadUpdateUseEffectCall = false; - routes = _createRoutesFromChildren(props.children) as RouteObject[]; - updatePageloadTransaction(location, routes); - } - }, [props.children, location]); + _useEffect( + () => { + const routes = _createRoutesFromChildren(props.children) as RouteObject[]; - _useEffect(() => { - if (isFirstNavigationUseEffectCall) { - isFirstNavigationUseEffectCall = false; - } else { - handleNavigation(location, routes, navigationType); - } - }, [location, navigationType]); + if (previousLocation === null) { + updatePageloadTransaction(location, routes); + } else { + handleNavigation(location, routes, navigationType); + } + + previousLocation = location; + }, + // `props.children` is purpusely not included in the dependency array, because we do not want to re-run this effect + // when the children change. We only want to start transactions when the location or navigation type change. + [location, navigationType], + ); // @ts-ignore Setting more specific React Component typing for `R` generic above // will break advanced type inference done by react router params @@ -222,35 +215,34 @@ export function wrapUseRoutes(origUseRoutes: UseRoutes): UseRoutes { return origUseRoutes; } - let isFirstPageloadUpdateUseEffectCall = true; - let isFirstNavigationUseEffectCall = true; + let previousLocation: Location | null = null; // eslint-disable-next-line react/display-name - return (routes: RouteObject[], location?: Partial<Location> | string): React.ReactElement | null => { - const SentryRoutes: React.FC<unknown> = (props: unknown) => { - const Routes = origUseRoutes(routes, location); + return (routes: RouteObject[], locationArg?: Partial<Location> | string): React.ReactElement | null => { + const SentryRoutes: React.FC<unknown> = () => { + const Routes = origUseRoutes(routes, locationArg); - // the case where location is a string might be killing performance because we're always creating a new - // object(with different identity) that will trigger useEffects below to run again - const locationArgObject = typeof location === 'string' ? { pathname: location } : location; - - const locationObject = (locationArgObject as Location) || _useLocation(); + const location = _useLocation(); const navigationType = _useNavigationType(); - _useEffect(() => { - if (isFirstPageloadUpdateUseEffectCall) { - isFirstPageloadUpdateUseEffectCall = false; - updatePageloadTransaction(locationObject, routes); - } - }, [locationObject, routes]); + // A value with stable identity to either pick `locationArg` if available or `location` if not + const stableLocationParam = + typeof locationArg === 'string' || locationArg?.pathname !== undefined + ? (locationArg as { pathname: string }) + : location; _useEffect(() => { - if (isFirstNavigationUseEffectCall) { - isFirstNavigationUseEffectCall = false; + const normalizedLocation = + typeof stableLocationParam === 'string' ? { pathname: stableLocationParam } : stableLocationParam; + + if (previousLocation === null) { + updatePageloadTransaction(normalizedLocation, routes); } else { - handleNavigation(locationObject, routes, navigationType); + handleNavigation(normalizedLocation, routes, navigationType); } - }, [locationObject, routes, navigationType]); + + previousLocation = normalizedLocation; + }, [navigationType, stableLocationParam]); return Routes; }; From af715ba0f8b1fada65af3961a56e4216cb0e1e42 Mon Sep 17 00:00:00 2001 From: Luca Forstner <luca.forstner@sentry.io> Date: Tue, 3 Jan 2023 16:49:16 +0000 Subject: [PATCH 6/6] Use boolean --- packages/react/src/reactrouterv6.tsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/react/src/reactrouterv6.tsx b/packages/react/src/reactrouterv6.tsx index cd0825a34a84..47db243cd8ab 100644 --- a/packages/react/src/reactrouterv6.tsx +++ b/packages/react/src/reactrouterv6.tsx @@ -170,7 +170,7 @@ export function withSentryReactRouterV6Routing<P extends Record<string, any>, R return Routes; } - let previousLocation: Location | null = null; + let isMountRenderPass: boolean = true; const SentryRoutes: React.FC<P> = (props: P) => { const location = _useLocation(); @@ -180,13 +180,12 @@ export function withSentryReactRouterV6Routing<P extends Record<string, any>, R () => { const routes = _createRoutesFromChildren(props.children) as RouteObject[]; - if (previousLocation === null) { + if (isMountRenderPass) { updatePageloadTransaction(location, routes); + isMountRenderPass = false; } else { handleNavigation(location, routes, navigationType); } - - previousLocation = location; }, // `props.children` is purpusely not included in the dependency array, because we do not want to re-run this effect // when the children change. We only want to start transactions when the location or navigation type change. @@ -215,7 +214,7 @@ export function wrapUseRoutes(origUseRoutes: UseRoutes): UseRoutes { return origUseRoutes; } - let previousLocation: Location | null = null; + let isMountRenderPass: boolean = true; // eslint-disable-next-line react/display-name return (routes: RouteObject[], locationArg?: Partial<Location> | string): React.ReactElement | null => { @@ -235,13 +234,12 @@ export function wrapUseRoutes(origUseRoutes: UseRoutes): UseRoutes { const normalizedLocation = typeof stableLocationParam === 'string' ? { pathname: stableLocationParam } : stableLocationParam; - if (previousLocation === null) { + if (isMountRenderPass) { updatePageloadTransaction(normalizedLocation, routes); + isMountRenderPass = false; } else { handleNavigation(normalizedLocation, routes, navigationType); } - - previousLocation = normalizedLocation; }, [navigationType, stableLocationParam]); return Routes;