Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion static/app/bootstrap/processInitQueue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -112,7 +113,13 @@ async function processItem(initConfig: OnSentryInitConfiguration) {
<QueryClientProvider client={queryClient}>
<ThemeAndStyleProvider>
<CommandPaletteProvider>
<SimpleRouter element={<Component {...props} />} />
<SimpleRouter
element={
<ScrapsProviders>
<Component {...props} />
</ScrapsProviders>
}
/>
</CommandPaletteProvider>
</ThemeAndStyleProvider>
</QueryClientProvider>
Expand Down
26 changes: 7 additions & 19 deletions static/app/components/core/link/link.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLAnchorElement>,
Expand Down Expand Up @@ -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 <a> 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) {
return <Anchor {...props} />;
}

return (
<RouterLink to={locationDescriptorToTo(normalizeUrl(to, location))} {...props} />
);
return <Component {...behavior()} />;
})`
${getLinkStyles}
`;
Expand Down
30 changes: 30 additions & 0 deletions static/app/components/core/link/linkBehaviorContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +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';

type LinkBehavior = {
behavior: (props: LinkProps) => LinkProps;
component: FunctionComponent<LinkProps>;
};

const LinkBehaviorContext = createContext<LinkBehavior | null>(null);

const defaultLinkBehavior = {
component: RouterLink,
behavior: props => props,
} satisfies LinkBehavior;

export const LinkBehaviorContextProvider = LinkBehaviorContext.Provider;

export const useLinkBehavior = (props: LinkProps) => {
const linkBehavior = useContext(LinkBehaviorContext);

if (process.env.NODE_ENV === 'production' && !linkBehavior) {
Sentry.logger.warn('LinkBehaviorContext not found');
}
const {component, behavior} = linkBehavior ?? defaultLinkBehavior;

return {Component: component, behavior: () => behavior(props)};
};
13 changes: 5 additions & 8 deletions static/app/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {SentryTrackingProvider} from 'sentry/tracking';
import {DANGEROUS_SET_REACT_ROUTER_6_HISTORY} from 'sentry/utils/browserHistory';

function buildRouter() {
Expand All @@ -28,13 +27,11 @@ function Main() {
<AppQueryClientProvider>
<FrontendVersionProvider releaseVersion={SENTRY_RELEASE_VERSION ?? null}>
<ThemeAndStyleProvider>
<SentryTrackingProvider>
<NuqsAdapter defaultOptions={{shallow: false}}>
<CommandPaletteProvider>
<RouterProvider router={router} />
</CommandPaletteProvider>
</NuqsAdapter>
</SentryTrackingProvider>
<NuqsAdapter defaultOptions={{shallow: false}}>
<CommandPaletteProvider>
<RouterProvider router={router} />
</CommandPaletteProvider>
</NuqsAdapter>
{USE_REACT_QUERY_DEVTOOL && (
<ReactQueryDevtools initialIsOpen={false} buttonPosition="bottom-left" />
)}
Expand Down
9 changes: 8 additions & 1 deletion static/app/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -3066,7 +3067,13 @@ function buildRoutes(): RouteObject[] {
};

const appRoutes: SentryRouteObject = {
component: ProvideAriaRouter,
component: ({children}: {children: React.ReactNode}) => {
return (
<ProvideAriaRouter>
<ScrapsProviders>{children}</ScrapsProviders>
</ProvideAriaRouter>
);
},
deprecatedRouteProps: true,
children: [
experimentalSpaRoutes,
Expand Down
10 changes: 10 additions & 0 deletions static/app/scrapsProviders/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {SentryLinkBehaviorProvider} from './link';
import {SentryTrackingProvider} from './tracking';

export function ScrapsProviders({children}: {children: React.ReactNode}) {
return (
<SentryTrackingProvider>
<SentryLinkBehaviorProvider>{children}</SentryLinkBehaviorProvider>
</SentryTrackingProvider>
);
}
34 changes: 34 additions & 0 deletions static/app/scrapsProviders/link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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 (
<LinkBehaviorContextProvider
value={useMemo(
() => ({
component: RouterLink,
behavior: ({to, ...props}: LinkProps) => {
const normalizedTo = locationDescriptorToTo(normalizeUrl(to, location));

return {
to: normalizedTo,
...props,
};
},
Comment on lines +19 to +26
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: SentryLinkBehaviorProvider uses an external context, but useLinkBehavior() reads from a local context, bypassing custom link behavior.
Severity: CRITICAL | Confidence: 1.00

🔍 Detailed Analysis

The SentryLinkBehaviorProvider in static/app/scrapsProviders/link.tsx attempts to provide custom link behavior using an external LinkBehaviorContextProvider from @sentry/scraps. However, the useLinkBehavior() hook in static/app/components/core/link/link.tsx reads from a different, local LinkBehaviorContext. This mismatch prevents the custom behavior function, which includes URL normalization via normalizeUrl() and locationDescriptorToTo(), from ever being applied. Consequently, links will not have their URLs normalized as intended.

💡 Suggested Fix

Either modify useLinkBehavior() to read from the external @sentry/scraps context, or update SentryLinkBehaviorProvider to set values on the local context.

🤖 Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: static/app/scrapsProviders/link.tsx#L19-L26

Potential issue: The `SentryLinkBehaviorProvider` in
`static/app/scrapsProviders/link.tsx` attempts to provide custom link behavior using an
external `LinkBehaviorContextProvider` from `@sentry/scraps`. However, the
`useLinkBehavior()` hook in `static/app/components/core/link/link.tsx` reads from a
*different*, local `LinkBehaviorContext`. This mismatch prevents the custom `behavior`
function, which includes URL normalization via `normalizeUrl()` and
`locationDescriptorToTo()`, from ever being applied. Consequently, links will not have
their URLs normalized as intended.

Did we get this right? 👍 / 👎 to inform future reviews.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sentry/scraps is an alias for static/app/components/core/

}),
[location]
)}
>
{children}
</LinkBehaviorContextProvider>
);
}
File renamed without changes.
7 changes: 6 additions & 1 deletion static/app/views/integrationPipeline/pipelineView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -43,7 +44,11 @@ function buildRouter(Component: React.ComponentType, props: any) {
const router = sentryCreateBrowserRouter([
{
path: '*',
element: <Component {...props} props={props} />,
element: (
<ScrapsProviders>
<Component {...props} props={props} />
</ScrapsProviders>
),
},
]);
DANGEROUS_SET_REACT_ROUTER_6_HISTORY(router);
Expand Down
113 changes: 58 additions & 55 deletions static/gsAdmin/views/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -46,61 +47,63 @@ function Layout({children}: Props) {

return (
<ThemeProvider theme={theme}>
<GlobalStyles theme={theme} />
<GlobalModal />
<SystemAlerts className="messages-container" />
<Indicators className="indicators-container" />
<AppContainer>
<Sidebar>
<Logo to="/_admin/">
<IconSentry size="xl" />
Admin
</Logo>
<Navigation>
<NavLink to="/_admin/" index>
Home
</NavLink>
<NavLink to="/_admin/customers/">Customers</NavLink>
<NavLink to="/_admin/users/">Users</NavLink>
<NavLink to="/_admin/sentry-apps/">Sentry Apps</NavLink>
<NavLink to="/_admin/doc-integrations/">Doc Integrations</NavLink>
<NavLink to="/_admin/broadcasts/">Broadcasts</NavLink>
<NavLink to="/_admin/promocodes/">Promos</NavLink>
<NavLink to="/_admin/beacons/">Beacons</NavLink>
<NavLink to="/_admin/policies/">Policies</NavLink>
<NavLink to="/_admin/options/">Options</NavLink>
<NavLink to="/_admin/debugging-tools/">Debugging Tools</NavLink>
<NavLink to="/_admin/instance-level-oauth">
Instance level OAuth Clients
</NavLink>
<NavLink to="/_admin/private-apis/">Private APIs</NavLink>
<NavLink to="/_admin/relocations/">Relocations</NavLink>
<NavLink to="/_admin/employees/">Sentry Employees</NavLink>
<NavLink to="/_admin/billing-plans/">Billing Plans</NavLink>
<NavLink to="/_admin/invoices/">Invoices</NavLink>
<NavLink to="/_admin/spike-projection-generation/">
Spike Projection Generation
</NavLink>
<NavLink to="/_admin/launchpad/">Launchpad (Emerge) Related</NavLink>
</Navigation>
<div>
<ThemeToggle
borderless
size="zero"
onClick={toggleTheme}
icon={
<IconSliders
size="sm"
style={{transform: isDark ? 'scaleX(-1)' : 'none'}}
/>
}
>
{isDark ? 'Light mode' : 'Dark mode'}
</ThemeToggle>
</div>
</Sidebar>
<Content>{children}</Content>
</AppContainer>
<ScrapsProviders>
<GlobalStyles theme={theme} />
<GlobalModal />
<SystemAlerts className="messages-container" />
<Indicators className="indicators-container" />
<AppContainer>
<Sidebar>
<Logo to="/_admin/">
<IconSentry size="xl" />
Admin
</Logo>
<Navigation>
<NavLink to="/_admin/" index>
Home
</NavLink>
<NavLink to="/_admin/customers/">Customers</NavLink>
<NavLink to="/_admin/users/">Users</NavLink>
<NavLink to="/_admin/sentry-apps/">Sentry Apps</NavLink>
<NavLink to="/_admin/doc-integrations/">Doc Integrations</NavLink>
<NavLink to="/_admin/broadcasts/">Broadcasts</NavLink>
<NavLink to="/_admin/promocodes/">Promos</NavLink>
<NavLink to="/_admin/beacons/">Beacons</NavLink>
<NavLink to="/_admin/policies/">Policies</NavLink>
<NavLink to="/_admin/options/">Options</NavLink>
<NavLink to="/_admin/debugging-tools/">Debugging Tools</NavLink>
<NavLink to="/_admin/instance-level-oauth">
Instance level OAuth Clients
</NavLink>
<NavLink to="/_admin/private-apis/">Private APIs</NavLink>
<NavLink to="/_admin/relocations/">Relocations</NavLink>
<NavLink to="/_admin/employees/">Sentry Employees</NavLink>
<NavLink to="/_admin/billing-plans/">Billing Plans</NavLink>
<NavLink to="/_admin/invoices/">Invoices</NavLink>
<NavLink to="/_admin/spike-projection-generation/">
Spike Projection Generation
</NavLink>
<NavLink to="/_admin/launchpad/">Launchpad (Emerge) Related</NavLink>
</Navigation>
<div>
<ThemeToggle
borderless
size="zero"
onClick={toggleTheme}
icon={
<IconSliders
size="sm"
style={{transform: isDark ? 'scaleX(-1)' : 'none'}}
/>
}
>
{isDark ? 'Light mode' : 'Dark mode'}
</ThemeToggle>
</div>
</Sidebar>
<Content>{children}</Content>
</AppContainer>
</ScrapsProviders>
</ThemeProvider>
);
}
Expand Down
12 changes: 7 additions & 5 deletions tests/js/sentry-test/reactTestingLibrary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 {
/**
Expand Down Expand Up @@ -218,9 +218,11 @@ function makeAllTheProviders(options: ProviderOptions) {
<CacheProvider value={{...cache, compat: true}}>
<QueryClientProvider client={makeTestQueryClient()}>
<SentryNuqsTestingAdapter defaultOptions={{shallow: false}}>
<CommandPaletteProvider>
<ThemeProvider theme={ThemeFixture()}>{wrappedContent}</ThemeProvider>
</CommandPaletteProvider>
<ScrapsTestingProviders>
<CommandPaletteProvider>
<ThemeProvider theme={ThemeFixture()}>{wrappedContent}</ThemeProvider>
</CommandPaletteProvider>
</ScrapsTestingProviders>
</SentryNuqsTestingAdapter>
</QueryClientProvider>
</CacheProvider>
Expand Down
5 changes: 5 additions & 0 deletions tests/js/sentry-test/scrapsTestingProviders.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import {SentryLinkBehaviorProvider} from 'sentry/scrapsProviders/link';

export function ScrapsTestingProviders({children}: {children: React.ReactNode}) {
return <SentryLinkBehaviorProvider>{children}</SentryLinkBehaviorProvider>;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Inconsistent Sentry Wrapping Between Test and Production

The test version of ScrapsTestingProviders is missing the SentryTrackingProvider wrapper, which is present in the production ScrapsProviders component. This inconsistency means tests won't include tracking context that production code has, potentially missing bugs related to tracking functionality. The production version wraps children with both SentryTrackingProvider and SentryLinkBehaviorProvider, but the test version only includes SentryLinkBehaviorProvider.

Fix in Cursor Fix in Web

Loading