From 34f7904574b4ee3019deecb3e589d67d3bfb0104 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Fri, 5 Dec 2025 10:18:46 -0800 Subject: [PATCH 1/2] ref(ui): Add deprecation warning to useProjects() without slugs Add a development-time console warning when useProjects() is called without the slugs parameter. This prepares for removing the all_projects=1 bootstrap fetch by encouraging explicit project fetching. Updates 18 components to pass specific slugs to useProjects(), ensuring they will continue working when projects are no longer pre-loaded. --- .../profiling/exportProfileButton.tsx | 3 ++- .../profiling/profilingBreadcrumbs.spec.tsx | 7 +++++++ .../profiling/profilingBreadcrumbs.tsx | 9 ++++++++- .../discover/teamKeyTransactionField.tsx | 2 +- static/app/utils/useProjects.tsx | 19 +++++++++++++++++-- .../rules/issue/details/ruleDetails.tsx | 2 +- .../tableCells/starredSegmentCell.tsx | 2 +- .../views/insights/pages/transactionCell.tsx | 4 ++-- .../app/views/issueDetails/groupDetails.tsx | 4 +++- .../issueDetails/groupDistributionsDrawer.tsx | 9 +++++++-- .../groupFeatureFlags/flagDrawerContent.tsx | 4 ++-- .../details/missingInstrumentation.tsx | 15 +++++++++++---- .../traceDrawer/details/span/index.tsx | 3 ++- .../traceDrawer/details/transaction/index.tsx | 2 +- .../traceDrawer/details/uptime/index.tsx | 7 +++---- .../app/views/profiling/profilesProvider.tsx | 2 +- static/app/views/projectDetail/index.tsx | 2 +- .../app/views/projectDetail/projectDetail.tsx | 4 ++-- .../detail/errorList/errorTableCell.tsx | 10 ++++++---- 19 files changed, 78 insertions(+), 32 deletions(-) diff --git a/static/app/components/profiling/exportProfileButton.tsx b/static/app/components/profiling/exportProfileButton.tsx index b239f342b7002f..d2d0c9f0995b2f 100644 --- a/static/app/components/profiling/exportProfileButton.tsx +++ b/static/app/components/profiling/exportProfileButton.tsx @@ -21,7 +21,8 @@ export function ExportProfileButton(props: ExportProfileButtonProps) { const api = useApi(); const organization = useOrganization(); - const project = useProjects().projects.find(p => { + const {projects} = useProjects({slugs: props.projectId ? [props.projectId] : []}); + const project = projects.find(p => { return p.slug === props.projectId; }); diff --git a/static/app/components/profiling/profilingBreadcrumbs.spec.tsx b/static/app/components/profiling/profilingBreadcrumbs.spec.tsx index 5b9c4fb1d58373..4486e60dea5306 100644 --- a/static/app/components/profiling/profilingBreadcrumbs.spec.tsx +++ b/static/app/components/profiling/profilingBreadcrumbs.spec.tsx @@ -1,9 +1,16 @@ +import {ProjectFixture} from 'sentry-fixture/project'; + import {initializeOrg} from 'sentry-test/initializeOrg'; import {render, screen} from 'sentry-test/reactTestingLibrary'; import {ProfilingBreadcrumbs} from 'sentry/components/profiling/profilingBreadcrumbs'; +import ProjectsStore from 'sentry/stores/projectsStore'; describe('Breadcrumb', () => { + beforeEach(() => { + ProjectsStore.loadInitialData([ProjectFixture({slug: 'bar'})]); + }); + it('renders the profiling link', () => { const {organization} = initializeOrg(); render( diff --git a/static/app/components/profiling/profilingBreadcrumbs.tsx b/static/app/components/profiling/profilingBreadcrumbs.tsx index 9a5714ca267cb3..a3a784eb79a781 100644 --- a/static/app/components/profiling/profilingBreadcrumbs.tsx +++ b/static/app/components/profiling/profilingBreadcrumbs.tsx @@ -21,7 +21,14 @@ export interface ProfilingBreadcrumbsProps { } function ProfilingBreadcrumbs({organization, trails}: ProfilingBreadcrumbsProps) { - const {projects} = useProjects(); + // Extract project slugs from trails that have them + const projectSlugs = trails + .filter( + (trail): trail is ProfileSummaryTrail | FlamegraphTrail => + trail.type === 'profile summary' || trail.type === 'flamechart' + ) + .map(trail => trail.payload.projectSlug); + const {projects} = useProjects({slugs: projectSlugs}); const crumbs = useMemo( () => trails.map(trail => trailToCrumb(trail, {organization, projects})), [organization, trails, projects] diff --git a/static/app/utils/discover/teamKeyTransactionField.tsx b/static/app/utils/discover/teamKeyTransactionField.tsx index 5bbdd4b7daeea9..b90d82934036d8 100644 --- a/static/app/utils/discover/teamKeyTransactionField.tsx +++ b/static/app/utils/discover/teamKeyTransactionField.tsx @@ -87,7 +87,7 @@ export default function TeamKeyTransactionFieldWrapper({ transactionName, ...props }: WrapperProps) { - const {projects} = useProjects(); + const {projects} = useProjects({slugs: projectSlug ? [projectSlug] : []}); const project = projects.find(proj => proj.slug === projectSlug); // All these fields need to be defined in order to toggle a team key diff --git a/static/app/utils/useProjects.tsx b/static/app/utils/useProjects.tsx index e12e174ab51e02..72c381bdbb4673 100644 --- a/static/app/utils/useProjects.tsx +++ b/static/app/utils/useProjects.tsx @@ -149,10 +149,25 @@ async function fetchProjects( * This hook also provides a way to select specific project slugs, and search * (type-ahead) for more projects that may not be in the project store. * - * NOTE: Currently ALL projects are always loaded, but this hook is designed - * for future-compat in a world where we do _not_ load all projects. + * DEPRECATED USAGE: Calling useProjects() without the `slugs` parameter is + * deprecated. This pattern assumes all projects are loaded in the store, + * which will not be true once we remove the all_projects=1 bootstrap fetch. + * + * RECOMMENDED: Always pass specific slugs you need: + * const {projects} = useProjects({slugs: [group.project.slug]}); + * + * This ensures projects are fetched on-demand if not already in the store. */ function useProjects({limit, slugs, orgId: propOrgId}: Options = {}) { + if (process.env.NODE_ENV !== 'production' && !slugs) { + // eslint-disable-next-line no-console + console.warn( + 'useProjects() called without slugs parameter. ' + + 'This usage is deprecated and may break when all_projects=1 is removed. ' + + 'Pass specific slugs to ensure projects are fetched on-demand.' + ); + } + const api = useApi(); const organization = useOrganization({allowNull: true}); diff --git a/static/app/views/alerts/rules/issue/details/ruleDetails.tsx b/static/app/views/alerts/rules/issue/details/ruleDetails.tsx index cf71b77736cc03..5b8040a24e73fa 100644 --- a/static/app/views/alerts/rules/issue/details/ruleDetails.tsx +++ b/static/app/views/alerts/rules/issue/details/ruleDetails.tsx @@ -74,7 +74,7 @@ function AlertRuleDetails({params, location, router}: AlertRuleDetailsProps) { const queryClient = useQueryClient(); const organization = useOrganization(); const api = useApi(); - const {projects, fetching: projectIsLoading} = useProjects(); + const {projects, fetching: projectIsLoading} = useProjects({slugs: [params.projectId]}); const project = projects.find(({slug}) => slug === params.projectId); const {projectId: projectSlug, ruleId} = params; const { diff --git a/static/app/views/insights/common/components/tableCells/starredSegmentCell.tsx b/static/app/views/insights/common/components/tableCells/starredSegmentCell.tsx index cbbe80614cf2c2..256c7d32f296b3 100644 --- a/static/app/views/insights/common/components/tableCells/starredSegmentCell.tsx +++ b/static/app/views/insights/common/components/tableCells/starredSegmentCell.tsx @@ -27,7 +27,7 @@ export const STARRED_SEGMENT_TABLE_QUERY_KEY = ['starred-segment-table']; export function StarredSegmentCell({segmentName, isStarred, projectSlug}: Props) { const queryClient = useQueryClient(); - const {projects} = useProjects(); + const {projects} = useProjects({slugs: [projectSlug]}); const project = projects.find(p => p.slug === projectSlug); const {setStarredSegment, isPending} = useStarredSegment({ diff --git a/static/app/views/insights/pages/transactionCell.tsx b/static/app/views/insights/pages/transactionCell.tsx index b3f95e2a6d0d77..2817caf6ef726d 100644 --- a/static/app/views/insights/pages/transactionCell.tsx +++ b/static/app/views/insights/pages/transactionCell.tsx @@ -16,12 +16,12 @@ interface Props { } export function TransactionCell({project, transaction, transactionMethod}: Props) { - const projects = useProjects(); + const {projects} = useProjects({slugs: project ? [project] : []}); const organization = useOrganization(); const location = useLocation(); const {view} = useDomainViewFilters(); - const projectId = projects.projects.find(p => p.slug === project)?.id; + const projectId = projects.find(p => p.slug === project)?.id; const searchQuery = new MutableSearch(''); if (transactionMethod) { diff --git a/static/app/views/issueDetails/groupDetails.tsx b/static/app/views/issueDetails/groupDetails.tsx index 2134df6d2aeda7..2f8721e792ea57 100644 --- a/static/app/views/issueDetails/groupDetails.tsx +++ b/static/app/views/issueDetails/groupDetails.tsx @@ -234,7 +234,6 @@ function useFetchGroupDetails(): FetchGroupDetailsState { const navigate = useNavigate(); const defaultIssueEvent = useDefaultIssueEvent(); const hasStreamlinedUI = useHasStreamlinedUI(); - const {projects} = useProjects(); const [allProjectChanged, setAllProjectChanged] = useState(false); @@ -261,6 +260,9 @@ function useFetchGroupDetails(): FetchGroupDetailsState { refetch: refetchGroupCall, } = useGroup({groupId}); + const groupProjectSlug = groupData?.project?.slug; + const {projects} = useProjects({slugs: groupProjectSlug ? [groupProjectSlug] : []}); + /** * TODO(streamline-ui): Remove this whole hook once the legacy UI is removed. The streamlined UI exposes the * filters on the page so the user is expected to clear it themselves, and the empty state is actually expected. diff --git a/static/app/views/issueDetails/groupDistributionsDrawer.tsx b/static/app/views/issueDetails/groupDistributionsDrawer.tsx index d60fffb0aa057c..371ff4ca1d7963 100644 --- a/static/app/views/issueDetails/groupDistributionsDrawer.tsx +++ b/static/app/views/issueDetails/groupDistributionsDrawer.tsx @@ -22,11 +22,16 @@ type Props = { */ export function GroupDistributionsDrawer({group, includeFeatureFlagsTab}: Props) { const organization = useOrganization(); - const {projects} = useProjects(); - const project = projects.find(p => p.slug === group.project.slug)!; + const {projects, fetching} = useProjects({slugs: [group.project.slug]}); + const project = projects.find(p => p.slug === group.project.slug); const {tab, setTab} = useDrawerTab({enabled: includeFeatureFlagsTab}); + // Wait for project to load before rendering the drawer content + if (fetching || !project) { + return null; + } + return ( diff --git a/static/app/views/issueDetails/groupFeatureFlags/flagDrawerContent.tsx b/static/app/views/issueDetails/groupFeatureFlags/flagDrawerContent.tsx index f7ec05423be2b3..fd545f94e53556 100644 --- a/static/app/views/issueDetails/groupFeatureFlags/flagDrawerContent.tsx +++ b/static/app/views/issueDetails/groupFeatureFlags/flagDrawerContent.tsx @@ -45,8 +45,8 @@ export default function FlagDrawerContent({ }); // CTA logic - const {projects} = useProjects(); - const project = projects.find(p => p.slug === group.project.slug)!; + const {projects} = useProjects({slugs: [group.project.slug]}); + const project = projects.find(p => p.slug === group.project.slug); const showCTA = allGroupFlagCount === 0 && diff --git a/static/app/views/performance/newTraceDetails/traceDrawer/details/missingInstrumentation.tsx b/static/app/views/performance/newTraceDetails/traceDrawer/details/missingInstrumentation.tsx index 10e2d94eb0517b..4ff799188af6d0 100644 --- a/static/app/views/performance/newTraceDetails/traceDrawer/details/missingInstrumentation.tsx +++ b/static/app/views/performance/newTraceDetails/traceDrawer/details/missingInstrumentation.tsx @@ -33,14 +33,21 @@ export function MissingInstrumentationNodeDetails({ ...props }: TraceTreeNodeDetailsProps) { const {node} = props; - const {projects} = useProjects(); + const isEAP = isEAPSpanNode(node.previous); - if (isEAPSpanNode(node.previous)) { + // For EAP spans, the event may not be loaded yet, but we have project_slug on the span itself + const eapProjectSlug = isEAP + ? (node.previous as TraceTreeNode).value.project_slug + : undefined; + const event = node.previous.event ?? node.next.event ?? null; + const projectSlug = eapProjectSlug ?? event?.projectSlug; + const {projects} = useProjects({slugs: projectSlug ? [projectSlug] : []}); + + if (isEAP) { return ; } - const event = node.previous.event ?? node.next.event ?? null; - const project = projects.find(proj => proj.slug === event?.projectSlug); + const project = projects.find(proj => proj.slug === projectSlug); const profileMeta = getProfileMeta(event) || ''; const profileContext = event?.contexts?.profile ?? {}; diff --git a/static/app/views/performance/newTraceDetails/traceDrawer/details/span/index.tsx b/static/app/views/performance/newTraceDetails/traceDrawer/details/span/index.tsx index e03f4d65b6371a..05876560103f3d 100644 --- a/static/app/views/performance/newTraceDetails/traceDrawer/details/span/index.tsx +++ b/static/app/views/performance/newTraceDetails/traceDrawer/details/span/index.tsx @@ -157,7 +157,8 @@ export function SpanNodeDetails( const {node, organization} = props; const location = useLocation(); const theme = useTheme(); - const {projects} = useProjects(); + const projectSlug = node.value.project_slug ?? node.event?.projectSlug; + const {projects} = useProjects({slugs: projectSlug ? [projectSlug] : []}); const issues = TraceTree.UniqueIssues(node); const parentTransaction = isEAPSpanNode(node) diff --git a/static/app/views/performance/newTraceDetails/traceDrawer/details/transaction/index.tsx b/static/app/views/performance/newTraceDetails/traceDrawer/details/transaction/index.tsx index c71bda65db43d6..c443a13444ea73 100644 --- a/static/app/views/performance/newTraceDetails/traceDrawer/details/transaction/index.tsx +++ b/static/app/views/performance/newTraceDetails/traceDrawer/details/transaction/index.tsx @@ -94,7 +94,7 @@ export function TransactionNodeDetails({ replay, hideNodeActions, }: TraceTreeNodeDetailsProps>) { - const {projects} = useProjects(); + const {projects} = useProjects({slugs: [node.value.project_slug]}); const issues = useMemo(() => { return [...node.errors, ...node.occurrences]; }, [node.errors, node.occurrences]); diff --git a/static/app/views/performance/newTraceDetails/traceDrawer/details/uptime/index.tsx b/static/app/views/performance/newTraceDetails/traceDrawer/details/uptime/index.tsx index 872d2da8a5f523..f14f1569dab734 100644 --- a/static/app/views/performance/newTraceDetails/traceDrawer/details/uptime/index.tsx +++ b/static/app/views/performance/newTraceDetails/traceDrawer/details/uptime/index.tsx @@ -61,11 +61,10 @@ export function UptimeNodeDetails( const {node} = props; const location = useLocation(); const theme = useTheme(); - const {projects} = useProjects(); + const projectSlug = node.value.project_slug ?? node.event?.projectSlug; + const {projects} = useProjects({slugs: projectSlug ? [projectSlug] : []}); - const project = projects.find( - proj => proj.slug === (node.value.project_slug ?? node.event?.projectSlug) - ); + const project = projects.find(proj => proj.slug === projectSlug); return ( { if (!profileMeta) { diff --git a/static/app/views/projectDetail/index.tsx b/static/app/views/projectDetail/index.tsx index 0d15e4080c1f52..64f443c6420ef6 100644 --- a/static/app/views/projectDetail/index.tsx +++ b/static/app/views/projectDetail/index.tsx @@ -10,7 +10,7 @@ function ProjectDetailContainer( 'projects' | 'loadingProjects' | 'selection' > ) { - const {projects} = useProjects(); + const {projects} = useProjects({slugs: [props.params.projectId]}); const project = projects.find(p => p.slug === props.params.projectId); useRouteAnalyticsParams( diff --git a/static/app/views/projectDetail/projectDetail.tsx b/static/app/views/projectDetail/projectDetail.tsx index b554e26664fbf7..c6749f09a62779 100644 --- a/static/app/views/projectDetail/projectDetail.tsx +++ b/static/app/views/projectDetail/projectDetail.tsx @@ -54,8 +54,8 @@ type Props = RouteComponentProps & { export default function ProjectDetail({router, location, organization}: Props) { const api = useApi(); - const params = useParams(); - const {projects, fetching: loadingProjects} = useProjects(); + const params = useParams(); + const {projects, fetching: loadingProjects} = useProjects({slugs: [params.projectId]}); const {selection} = usePageFilters(); const project = projects.find(p => p.slug === params.projectId); const {query} = location.query; diff --git a/static/app/views/replays/detail/errorList/errorTableCell.tsx b/static/app/views/replays/detail/errorList/errorTableCell.tsx index ff858c45ebb1ce..aca4b85d5845cc 100644 --- a/static/app/views/replays/detail/errorList/errorTableCell.tsx +++ b/static/app/views/replays/detail/errorList/errorTableCell.tsx @@ -53,7 +53,7 @@ export default function ErrorTableCell({ const {eventId, groupId, groupShortId, level, projectSlug} = frame.data; const title = frame.message; - const {projects} = useProjects(); + const {projects} = useProjects({slugs: projectSlug ? [projectSlug] : []}); const project = useMemo( () => projects.find(p => p.slug === projectSlug), [projects, projectSlug] @@ -161,9 +161,11 @@ export default function ErrorTableCell({ () => ( - - - + {project && ( + + + + )} {eventUrl ? ( Date: Fri, 5 Dec 2025 22:45:28 +0000 Subject: [PATCH 2/2] :hammer_and_wrench: apply pre-commit fixes --- static/app/views/projectDetail/projectDetail.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/static/app/views/projectDetail/projectDetail.tsx b/static/app/views/projectDetail/projectDetail.tsx index c6749f09a62779..f762c94fa0b230 100644 --- a/static/app/views/projectDetail/projectDetail.tsx +++ b/static/app/views/projectDetail/projectDetail.tsx @@ -82,7 +82,7 @@ export default function ProjectDetail({router, location, organization}: Props) { }, [hasTransactions, hasSessions]); const onRetryProjects = useCallback(() => { - fetchOrganizationDetails(api, params.orgId!); + fetchOrganizationDetails(api, params.orgId); }, [api, params.orgId]); const handleSearch = useCallback( @@ -263,14 +263,14 @@ export default function ProjectDetail({router, location, organization}: Props) {