From e136ad04185f7ca7fa1527cf6bfd71610b09942c Mon Sep 17 00:00:00 2001 From: TkDodo Date: Tue, 4 Nov 2025 11:13:01 +0100 Subject: [PATCH 1/4] ref(ui): decouple scraps/link from sentry specific code --- static/app/components/core/link/link.tsx | 26 ++++---------- .../core/link/linkBehaviorContext.tsx | 20 +++++++++++ static/app/main.tsx | 6 ++-- static/app/scrapsProviders/index.tsx | 10 ++++++ static/app/scrapsProviders/link.tsx | 35 +++++++++++++++++++ static/app/{ => scrapsProviders}/tracking.tsx | 0 6 files changed, 75 insertions(+), 22 deletions(-) create mode 100644 static/app/components/core/link/linkBehaviorContext.tsx create mode 100644 static/app/scrapsProviders/index.tsx create mode 100644 static/app/scrapsProviders/link.tsx rename static/app/{ => scrapsProviders}/tracking.tsx (100%) diff --git a/static/app/components/core/link/link.tsx b/static/app/components/core/link/link.tsx index 96aac7c230967a..5184dd7ce78d0d 100644 --- a/static/app/components/core/link/link.tsx +++ b/static/app/components/core/link/link.tsx @@ -1,15 +1,10 @@ -import { - Link as RouterLink, - type LinkProps as ReactRouterLinkProps, -} from 'react-router-dom'; +import {type LinkProps as ReactRouterLinkProps} from 'react-router-dom'; import isPropValid from '@emotion/is-prop-valid'; import {css, type Theme} from '@emotion/react'; import styled from '@emotion/styled'; import type {LocationDescriptor} from 'history'; -import {locationDescriptorToTo} from 'sentry/utils/reactRouter6Compat/location'; -import normalizeUrl from 'sentry/utils/url/normalizeUrl'; -import {useLocation} from 'sentry/utils/useLocation'; +import {useLinkBehavior} from './linkBehaviorContext'; export interface LinkProps extends React.RefAttributes, @@ -61,26 +56,19 @@ const getLinkStyles = ({ `; const Anchor = styled('a', { - shouldForwardProp: prop => - typeof prop === 'string' && isPropValid(prop) && prop !== 'disabled', + shouldForwardProp: prop => isPropValid(prop) && prop !== 'disabled', })<{disabled?: LinkProps['disabled']}>` ${getLinkStyles} `; -/** - * A context-aware version of Link (from react-router) that falls - * back to if there is no router present - */ -export const Link = styled(({disabled, to, ...props}: LinkProps) => { - const location = useLocation(); +export const Link = styled((props: LinkProps) => { + const {Component, behavior} = useLinkBehavior(props); - if (disabled || !location) { + if (props.disabled || Component === 'a') { return ; } - return ( - - ); + return ; })` ${getLinkStyles} `; diff --git a/static/app/components/core/link/linkBehaviorContext.tsx b/static/app/components/core/link/linkBehaviorContext.tsx new file mode 100644 index 00000000000000..0527cde3d1cc23 --- /dev/null +++ b/static/app/components/core/link/linkBehaviorContext.tsx @@ -0,0 +1,20 @@ +import {createContext, useContext, type FunctionComponent} from 'react'; +import {Link as RouterLink} from 'react-router-dom'; + +import type {LinkProps} from './link'; + +const LinkBehaviorContext = createContext<{ + behavior: (props: LinkProps) => LinkProps; + component: FunctionComponent | 'a'; +}>({ + component: RouterLink, + behavior: props => props, +}); + +export const LinkBehaviorContextProvider = LinkBehaviorContext.Provider; + +export const useLinkBehavior = (props: LinkProps) => { + const {component, behavior} = useContext(LinkBehaviorContext); + + return {Component: component, behavior: () => behavior(props)}; +}; diff --git a/static/app/main.tsx b/static/app/main.tsx index 6b20c680bc9e70..7d94551ca4eb6b 100644 --- a/static/app/main.tsx +++ b/static/app/main.tsx @@ -10,7 +10,7 @@ import {FrontendVersionProvider} from 'sentry/components/frontendVersionContext' import {ThemeAndStyleProvider} from 'sentry/components/themeAndStyleProvider'; import {SENTRY_RELEASE_VERSION, USE_REACT_QUERY_DEVTOOL} from 'sentry/constants'; import {routes} from 'sentry/routes'; -import {SentryTrackingProvider} from 'sentry/tracking'; +import {ScrapsProviders} from 'sentry/scrapsProviders'; import {DANGEROUS_SET_REACT_ROUTER_6_HISTORY} from 'sentry/utils/browserHistory'; function buildRouter() { @@ -28,13 +28,13 @@ function Main() { - + - + {USE_REACT_QUERY_DEVTOOL && ( )} diff --git a/static/app/scrapsProviders/index.tsx b/static/app/scrapsProviders/index.tsx new file mode 100644 index 00000000000000..1e4013d968e02e --- /dev/null +++ b/static/app/scrapsProviders/index.tsx @@ -0,0 +1,10 @@ +import {SentryLinkBehaviorProvider} from './link'; +import {SentryTrackingProvider} from './tracking'; + +export function ScrapsProviders({children}: {children: React.ReactNode}) { + return ( + + {children} + + ); +} diff --git a/static/app/scrapsProviders/link.tsx b/static/app/scrapsProviders/link.tsx new file mode 100644 index 00000000000000..326101b5ccacab --- /dev/null +++ b/static/app/scrapsProviders/link.tsx @@ -0,0 +1,35 @@ +import {useMemo} from 'react'; +import {Link as RouterLink} from 'react-router-dom'; + +import type {LinkProps} from '@sentry/scraps/link'; +import {LinkBehaviorContextProvider} from '@sentry/scraps/link/linkBehaviorContext'; + +import {locationDescriptorToTo} from 'sentry/utils/reactRouter6Compat/location'; +import normalizeUrl from 'sentry/utils/url/normalizeUrl'; +import {useLocation} from 'sentry/utils/useLocation'; + +export function SentryLinkBehaviorProvider({children}: {children: React.ReactNode}) { + const location = useLocation(); + + return ( + ({ + // fall back to if there is no router present + component: location ? RouterLink : 'a', + behavior: ({to, ...props}: LinkProps) => { + const normalizedTo = locationDescriptorToTo(normalizeUrl(to, location)); + + return { + to: normalizedTo, + ...props, + }; + }, + }), + [location] + )} + > + {children} + + ); +} diff --git a/static/app/tracking.tsx b/static/app/scrapsProviders/tracking.tsx similarity index 100% rename from static/app/tracking.tsx rename to static/app/scrapsProviders/tracking.tsx From 40dd7ce1d6da429bcf5f06300657fba538fa2541 Mon Sep 17 00:00:00 2001 From: TkDodo Date: Tue, 4 Nov 2025 11:44:01 +0100 Subject: [PATCH 2/4] fix: scrapsTestingProviders --- tests/js/sentry-test/reactTestingLibrary.tsx | 12 +++++++----- tests/js/sentry-test/scrapsTestingProviders.tsx | 5 +++++ 2 files changed, 12 insertions(+), 5 deletions(-) create mode 100644 tests/js/sentry-test/scrapsTestingProviders.tsx diff --git a/tests/js/sentry-test/reactTestingLibrary.tsx b/tests/js/sentry-test/reactTestingLibrary.tsx index 9f2766b7965a2f..ff6258db8bcf4a 100644 --- a/tests/js/sentry-test/reactTestingLibrary.tsx +++ b/tests/js/sentry-test/reactTestingLibrary.tsx @@ -23,8 +23,6 @@ import * as qs from 'query-string'; import {LocationFixture} from 'sentry-fixture/locationFixture'; import {ThemeFixture} from 'sentry-fixture/theme'; -import {makeTestQueryClient} from 'sentry-test/queryClient'; - import {CommandPaletteProvider} from 'sentry/components/commandPalette/context'; import {GlobalDrawer} from 'sentry/components/globalDrawer'; import GlobalModal from 'sentry/components/globalModal'; @@ -43,6 +41,8 @@ import {instrumentUserEvent} from '../instrumentedEnv/userEventIntegration'; import {initializeOrg} from './initializeOrg'; import {SentryNuqsTestingAdapter} from './nuqsTestingAdapter'; +import {makeTestQueryClient} from './queryClient'; +import {ScrapsTestingProviders} from './scrapsTestingProviders'; interface ProviderOptions { /** @@ -218,9 +218,11 @@ function makeAllTheProviders(options: ProviderOptions) { - - {wrappedContent} - + + + {wrappedContent} + + diff --git a/tests/js/sentry-test/scrapsTestingProviders.tsx b/tests/js/sentry-test/scrapsTestingProviders.tsx new file mode 100644 index 00000000000000..f1116a22dde428 --- /dev/null +++ b/tests/js/sentry-test/scrapsTestingProviders.tsx @@ -0,0 +1,5 @@ +import {SentryLinkBehaviorProvider} from 'sentry/scrapsProviders/link'; + +export function ScrapsTestingProviders({children}: {children: React.ReactNode}) { + return {children}; +} From 7c0cb6bf1b77647a00d580dcd30b6511722d2eed Mon Sep 17 00:00:00 2001 From: TkDodo Date: Tue, 4 Nov 2025 14:46:25 +0100 Subject: [PATCH 3/4] fix: move ScrapsProvider down the tree into RouterProvider so that we can call useLocation inside --- static/app/main.tsx | 13 +++++-------- static/app/routes.tsx | 9 ++++++++- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/static/app/main.tsx b/static/app/main.tsx index 7d94551ca4eb6b..48c11ec7a6c044 100644 --- a/static/app/main.tsx +++ b/static/app/main.tsx @@ -10,7 +10,6 @@ import {FrontendVersionProvider} from 'sentry/components/frontendVersionContext' import {ThemeAndStyleProvider} from 'sentry/components/themeAndStyleProvider'; import {SENTRY_RELEASE_VERSION, USE_REACT_QUERY_DEVTOOL} from 'sentry/constants'; import {routes} from 'sentry/routes'; -import {ScrapsProviders} from 'sentry/scrapsProviders'; import {DANGEROUS_SET_REACT_ROUTER_6_HISTORY} from 'sentry/utils/browserHistory'; function buildRouter() { @@ -28,13 +27,11 @@ function Main() { - - - - - - - + + + + + {USE_REACT_QUERY_DEVTOOL && ( )} diff --git a/static/app/routes.tsx b/static/app/routes.tsx index 9354b9bed5a6e9..1ce1aa33d0748b 100644 --- a/static/app/routes.tsx +++ b/static/app/routes.tsx @@ -3,6 +3,7 @@ import memoize from 'lodash/memoize'; import {EXPERIMENTAL_SPA} from 'sentry/constants'; import {t} from 'sentry/locale'; +import {ScrapsProviders} from 'sentry/scrapsProviders'; import HookStore from 'sentry/stores/hookStore'; import type {HookName} from 'sentry/types/hooks'; import errorHandler from 'sentry/utils/errorHandler'; @@ -3066,7 +3067,13 @@ function buildRoutes(): RouteObject[] { }; const appRoutes: SentryRouteObject = { - component: ProvideAriaRouter, + component: ({children}: {children: React.ReactNode}) => { + return ( + + {children} + + ); + }, deprecatedRouteProps: true, children: [ experimentalSpaRoutes, From f1625c5a823be917692ecd2df4eac9b4c275e944 Mon Sep 17 00:00:00 2001 From: TkDodo Date: Tue, 4 Nov 2025 18:51:17 +0100 Subject: [PATCH 4/4] fix: we only need to fall back to 'a' tags for when we don't have a LinkBehaviorContext set both the sentry app and tests set that, so this is a defensive fallback --- static/app/components/core/link/linkBehaviorContext.tsx | 3 +-- static/app/scrapsProviders/link.tsx | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/static/app/components/core/link/linkBehaviorContext.tsx b/static/app/components/core/link/linkBehaviorContext.tsx index 0527cde3d1cc23..d11f8d9d2fd56a 100644 --- a/static/app/components/core/link/linkBehaviorContext.tsx +++ b/static/app/components/core/link/linkBehaviorContext.tsx @@ -1,5 +1,4 @@ import {createContext, useContext, type FunctionComponent} from 'react'; -import {Link as RouterLink} from 'react-router-dom'; import type {LinkProps} from './link'; @@ -7,7 +6,7 @@ const LinkBehaviorContext = createContext<{ behavior: (props: LinkProps) => LinkProps; component: FunctionComponent | 'a'; }>({ - component: RouterLink, + component: 'a', behavior: props => props, }); diff --git a/static/app/scrapsProviders/link.tsx b/static/app/scrapsProviders/link.tsx index 326101b5ccacab..36bb61fc0165ee 100644 --- a/static/app/scrapsProviders/link.tsx +++ b/static/app/scrapsProviders/link.tsx @@ -15,8 +15,7 @@ export function SentryLinkBehaviorProvider({children}: {children: React.ReactNod ({ - // fall back to if there is no router present - component: location ? RouterLink : 'a', + component: RouterLink, behavior: ({to, ...props}: LinkProps) => { const normalizedTo = locationDescriptorToTo(normalizeUrl(to, location));