From e136ad04185f7ca7fa1527cf6bfd71610b09942c Mon Sep 17 00:00:00 2001 From: TkDodo Date: Tue, 4 Nov 2025 11:13:01 +0100 Subject: [PATCH 1/9] 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/9] 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/9] 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/9] 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)); From 0f0615a648382a72ac0d763332e6cd7183856720 Mon Sep 17 00:00:00 2001 From: TkDodo Date: Wed, 5 Nov 2025 08:44:29 +0100 Subject: [PATCH 5/9] fix(ui): add ScrapsProvider in _admin this ensures links are rendered with RouterLinks --- static/gsAdmin/views/layout.tsx | 113 ++++++++++++++++---------------- 1 file changed, 58 insertions(+), 55 deletions(-) diff --git a/static/gsAdmin/views/layout.tsx b/static/gsAdmin/views/layout.tsx index 30ada421450e11..1a995047d3d639 100644 --- a/static/gsAdmin/views/layout.tsx +++ b/static/gsAdmin/views/layout.tsx @@ -8,6 +8,7 @@ import GlobalModal from 'sentry/components/globalModal'; import Indicators from 'sentry/components/indicators'; import ListLink from 'sentry/components/links/listLink'; import {IconSentry, IconSliders} from 'sentry/icons'; +import {ScrapsProviders} from 'sentry/scrapsProviders'; import {space} from 'sentry/styles/space'; import localStorage from 'sentry/utils/localStorage'; // eslint-disable-next-line no-restricted-imports -- @TODO(jonasbadalic): Remove theme import @@ -46,61 +47,63 @@ function Layout({children}: Props) { return ( - - - - - - - - - Admin - - - - Home - - Customers - Users - Sentry Apps - Doc Integrations - Broadcasts - Promos - Beacons - Policies - Options - Debugging Tools - - Instance level OAuth Clients - - Private APIs - Relocations - Sentry Employees - Billing Plans - Invoices - - Spike Projection Generation - - Launchpad (Emerge) Related - -
- - } - > - {isDark ? 'Light mode' : 'Dark mode'} - -
-
- {children} -
+ + + + + + + + + + Admin + + + + Home + + Customers + Users + Sentry Apps + Doc Integrations + Broadcasts + Promos + Beacons + Policies + Options + Debugging Tools + + Instance level OAuth Clients + + Private APIs + Relocations + Sentry Employees + Billing Plans + Invoices + + Spike Projection Generation + + Launchpad (Emerge) Related + +
+ + } + > + {isDark ? 'Light mode' : 'Dark mode'} + +
+
+ {children} +
+
); } From 803054e15fecb3eb19588b7cecd7522770f5c6ee Mon Sep 17 00:00:00 2001 From: TkDodo Date: Wed, 5 Nov 2025 08:46:34 +0100 Subject: [PATCH 6/9] ref: disallow 'a' as LinkBehavior component React Router falls back to rendering an 'a' tag anyway --- static/app/components/core/link/link.tsx | 2 +- static/app/components/core/link/linkBehaviorContext.tsx | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/static/app/components/core/link/link.tsx b/static/app/components/core/link/link.tsx index 5184dd7ce78d0d..820e8d67fc07aa 100644 --- a/static/app/components/core/link/link.tsx +++ b/static/app/components/core/link/link.tsx @@ -64,7 +64,7 @@ const Anchor = styled('a', { export const Link = styled((props: LinkProps) => { const {Component, behavior} = useLinkBehavior(props); - if (props.disabled || Component === 'a') { + if (props.disabled) { return ; } diff --git a/static/app/components/core/link/linkBehaviorContext.tsx b/static/app/components/core/link/linkBehaviorContext.tsx index d11f8d9d2fd56a..b2bb90944e9683 100644 --- a/static/app/components/core/link/linkBehaviorContext.tsx +++ b/static/app/components/core/link/linkBehaviorContext.tsx @@ -1,12 +1,13 @@ 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: FunctionComponent; }>({ - component: 'a', + component: RouterLink, behavior: props => props, }); From 281b4043d7f8350ec50ae7ca5a549e9cd6e17b1e Mon Sep 17 00:00:00 2001 From: TkDodo Date: Wed, 5 Nov 2025 16:39:41 +0100 Subject: [PATCH 7/9] fix: wrap additional routers into ScrapsProviders at the top level --- static/app/bootstrap/processInitQueue.tsx | 9 ++++++++- static/app/views/integrationPipeline/pipelineView.tsx | 7 ++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/static/app/bootstrap/processInitQueue.tsx b/static/app/bootstrap/processInitQueue.tsx index 2276b9584068d8..c27ad6ff5197b8 100644 --- a/static/app/bootstrap/processInitQueue.tsx +++ b/static/app/bootstrap/processInitQueue.tsx @@ -6,6 +6,7 @@ import throttle from 'lodash/throttle'; import {exportedGlobals} from 'sentry/bootstrap/exportGlobals'; import {CommandPaletteProvider} from 'sentry/components/commandPalette/context'; import {ThemeAndStyleProvider} from 'sentry/components/themeAndStyleProvider'; +import {ScrapsProviders} from 'sentry/scrapsProviders'; import type {OnSentryInitConfiguration} from 'sentry/types/system'; import {SentryInitRenderReactComponent} from 'sentry/types/system'; import { @@ -112,7 +113,13 @@ async function processItem(initConfig: OnSentryInitConfiguration) { - } /> + + + + } + /> diff --git a/static/app/views/integrationPipeline/pipelineView.tsx b/static/app/views/integrationPipeline/pipelineView.tsx index 07ca9c9366ec64..db3a228abaae22 100644 --- a/static/app/views/integrationPipeline/pipelineView.tsx +++ b/static/app/views/integrationPipeline/pipelineView.tsx @@ -5,6 +5,7 @@ import {wrapCreateBrowserRouterV6} from '@sentry/react'; import {fetchOrganizations} from 'sentry/actionCreators/organizations'; import Indicators from 'sentry/components/indicators'; import {ThemeAndStyleProvider} from 'sentry/components/themeAndStyleProvider'; +import {ScrapsProviders} from 'sentry/scrapsProviders'; import ConfigStore from 'sentry/stores/configStore'; import OrganizationsStore from 'sentry/stores/organizationsStore'; import {useLegacyStore} from 'sentry/stores/useLegacyStore'; @@ -43,7 +44,11 @@ function buildRouter(Component: React.ComponentType, props: any) { const router = sentryCreateBrowserRouter([ { path: '*', - element: , + element: ( + + + + ), }, ]); DANGEROUS_SET_REACT_ROUTER_6_HISTORY(router); From 9e28155f37df374c36e86d0539aaf40ded3a55e9 Mon Sep 17 00:00:00 2001 From: TkDodo Date: Wed, 5 Nov 2025 18:29:27 +0100 Subject: [PATCH 8/9] ref: log if there is no linkBehaviorContext --- .../core/link/linkBehaviorContext.tsx | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/static/app/components/core/link/linkBehaviorContext.tsx b/static/app/components/core/link/linkBehaviorContext.tsx index b2bb90944e9683..ba0237a1cb884f 100644 --- a/static/app/components/core/link/linkBehaviorContext.tsx +++ b/static/app/components/core/link/linkBehaviorContext.tsx @@ -1,20 +1,30 @@ import {createContext, useContext, type FunctionComponent} from 'react'; import {Link as RouterLink} from 'react-router-dom'; +import * as Sentry from '@sentry/react'; import type {LinkProps} from './link'; -const LinkBehaviorContext = createContext<{ +type LinkBehavior = { behavior: (props: LinkProps) => LinkProps; component: FunctionComponent; -}>({ +}; + +const LinkBehaviorContext = createContext(null); + +const defaultLinkBeahvior = { component: RouterLink, behavior: props => props, -}); +} satisfies LinkBehavior; export const LinkBehaviorContextProvider = LinkBehaviorContext.Provider; export const useLinkBehavior = (props: LinkProps) => { - const {component, behavior} = useContext(LinkBehaviorContext); + const linkBehavior = useContext(LinkBehaviorContext); + + if (!linkBehavior) { + Sentry.logger.warn('LinkBehaviorContext not found'); + } + const {component, behavior} = linkBehavior ?? defaultLinkBeahvior; return {Component: component, behavior: () => behavior(props)}; }; From 9181eb72080b7a61820a0da4a30f26257f770f13 Mon Sep 17 00:00:00 2001 From: TkDodo Date: Wed, 5 Nov 2025 18:50:44 +0100 Subject: [PATCH 9/9] ref: constrain logs to production --- static/app/components/core/link/linkBehaviorContext.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/static/app/components/core/link/linkBehaviorContext.tsx b/static/app/components/core/link/linkBehaviorContext.tsx index ba0237a1cb884f..6f00b65de0e4e8 100644 --- a/static/app/components/core/link/linkBehaviorContext.tsx +++ b/static/app/components/core/link/linkBehaviorContext.tsx @@ -11,7 +11,7 @@ type LinkBehavior = { const LinkBehaviorContext = createContext(null); -const defaultLinkBeahvior = { +const defaultLinkBehavior = { component: RouterLink, behavior: props => props, } satisfies LinkBehavior; @@ -21,10 +21,10 @@ export const LinkBehaviorContextProvider = LinkBehaviorContext.Provider; export const useLinkBehavior = (props: LinkProps) => { const linkBehavior = useContext(LinkBehaviorContext); - if (!linkBehavior) { + if (process.env.NODE_ENV === 'production' && !linkBehavior) { Sentry.logger.warn('LinkBehaviorContext not found'); } - const {component, behavior} = linkBehavior ?? defaultLinkBeahvior; + const {component, behavior} = linkBehavior ?? defaultLinkBehavior; return {Component: component, behavior: () => behavior(props)}; };