From 31573b88840f63a9f500b21f94bd5471163124a9 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Jul 2026 20:50:35 +0000 Subject: [PATCH] ref(issues): Remove organizations:top-issues-ui flag (frontend) The organizations:top-issues-ui flag was never rolled out (absent from sentry-options-automator, flagpole-disabled) and has been stale for months. Remove the flag checks and the "supergroups" UI they gated, which is now dead code: the issue-stream supergroup rows/drawer, the issue-details supergroup section, their hooks, fixtures, and analytics events. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_015kWdpv3H5FqLqtdyrxX25e --- .../stream/supergroups/supergroupRow.tsx | 301 --------- .../analytics/workflowAnalyticsEvents.tsx | 10 - .../views/issueDetails/sidebar/sidebar.tsx | 4 - .../sidebar/supergroupSection.spec.tsx | 36 - .../sidebar/supergroupSection.tsx | 105 --- static/app/views/issueList/groupListBody.tsx | 81 +-- static/app/views/issueList/issueListTable.tsx | 4 - static/app/views/issueList/overview.tsx | 15 +- .../aggregateSupergroupStats.spec.ts | 141 ---- .../supergroups/aggregateSupergroupStats.ts | 95 --- .../supergroups/supergroupDrawer.spec.tsx | 78 --- .../supergroups/supergroupDrawer.tsx | 614 ------------------ .../supergroups/supergroupFeedback.tsx | 67 -- .../app/views/issueList/supergroups/types.ts | 10 - .../supergroups/useSuperGroups.spec.tsx | 64 -- .../issueList/supergroups/useSuperGroups.tsx | 86 --- .../supergroups/useSupergroupDrawer.tsx | 121 ---- tests/js/fixtures/supergroupDetail.ts | 17 - 18 files changed, 2 insertions(+), 1847 deletions(-) delete mode 100644 static/app/components/stream/supergroups/supergroupRow.tsx delete mode 100644 static/app/views/issueDetails/sidebar/supergroupSection.spec.tsx delete mode 100644 static/app/views/issueDetails/sidebar/supergroupSection.tsx delete mode 100644 static/app/views/issueList/supergroups/aggregateSupergroupStats.spec.ts delete mode 100644 static/app/views/issueList/supergroups/aggregateSupergroupStats.ts delete mode 100644 static/app/views/issueList/supergroups/supergroupDrawer.spec.tsx delete mode 100644 static/app/views/issueList/supergroups/supergroupDrawer.tsx delete mode 100644 static/app/views/issueList/supergroups/supergroupFeedback.tsx delete mode 100644 static/app/views/issueList/supergroups/types.ts delete mode 100644 static/app/views/issueList/supergroups/useSuperGroups.spec.tsx delete mode 100644 static/app/views/issueList/supergroups/useSuperGroups.tsx delete mode 100644 static/app/views/issueList/supergroups/useSupergroupDrawer.tsx delete mode 100644 tests/js/fixtures/supergroupDetail.ts diff --git a/static/app/components/stream/supergroups/supergroupRow.tsx b/static/app/components/stream/supergroups/supergroupRow.tsx deleted file mode 100644 index 56256cbeaeee..000000000000 --- a/static/app/components/stream/supergroups/supergroupRow.tsx +++ /dev/null @@ -1,301 +0,0 @@ -import styled from '@emotion/styled'; - -import InteractionStateLayer from '@sentry/scraps/interactionStateLayer'; -import {Stack} from '@sentry/scraps/layout'; -import {Text} from '@sentry/scraps/text'; - -import {GroupStatusChart} from 'sentry/components/charts/groupStatusChart'; -import {Count} from 'sentry/components/count'; -import {PanelItem} from 'sentry/components/panels/panelItem'; -import {Placeholder} from 'sentry/components/placeholder'; -import {TimeSince} from 'sentry/components/timeSince'; -import {IconStack} from 'sentry/icons'; -import {t} from 'sentry/locale'; -import {useLocation} from 'sentry/utils/useLocation'; -import {useNavigate} from 'sentry/utils/useNavigate'; -import {COLUMN_BREAKPOINTS} from 'sentry/views/issueList/actions/utils'; -import type {AggregatedSupergroupStats} from 'sentry/views/issueList/supergroups/aggregateSupergroupStats'; -import type {SupergroupDetail} from 'sentry/views/issueList/supergroups/types'; -import {SUPERGROUP_DRAWER_QUERY_PARAM} from 'sentry/views/issueList/supergroups/useSupergroupDrawer'; - -interface SupergroupRowProps { - supergroup: SupergroupDetail; - aggregatedStats?: AggregatedSupergroupStats | null; -} - -export function SupergroupRow({supergroup, aggregatedStats}: SupergroupRowProps) { - const location = useLocation(); - const navigate = useNavigate(); - const supergroupId = String(supergroup.id); - const handleClick = () => { - navigate( - { - pathname: location.pathname, - query: { - ...location.query, - [SUPERGROUP_DRAWER_QUERY_PARAM]: supergroupId, - }, - }, - {replace: true, preventScrollReset: true} - ); - }; - - const highlighted = location.query[SUPERGROUP_DRAWER_QUERY_PARAM] === supergroupId; - - return ( - - - - - - - - {supergroup.title} - - - {supergroup.error_type} - - - {supergroup.code_area ? ( - - {supergroup.code_area} - - ) : null} - {supergroup.code_area ? : null} - - {`${supergroup.group_ids.length} ${t('issues')}`} - - - - - - {aggregatedStats?.lastSeen ? ( - - ) : ( - - )} - - - - {aggregatedStats?.firstSeen ? ( - - ) : ( - - )} - - - - {aggregatedStats?.mergedStats && aggregatedStats.mergedStats.length > 0 ? ( - - ) : ( - - )} - - - - {aggregatedStats ? ( - - - {aggregatedStats.filteredEventCount !== null && ( - - )} - - ) : ( - - )} - - - - {aggregatedStats ? ( - - - {aggregatedStats.filteredUserCount !== null && ( - - )} - - ) : ( - - )} - - - - - - ); -} - -const Wrapper = styled(PanelItem)<{highlighted: boolean}>` - position: relative; - line-height: 1.1; - padding: ${p => p.theme.space.md} 0; - cursor: pointer; - min-height: 82px; - background: ${p => - p.highlighted ? p.theme.tokens.background.secondary : 'transparent'}; -`; - -const Summary = styled('div')` - overflow: hidden; - margin-left: ${p => p.theme.space.md}; - margin-right: ${p => p.theme.space['3xl']}; - flex: 1; - display: flex; - flex-direction: column; - justify-content: center; - gap: ${p => p.theme.space.xs}; - font-size: ${p => p.theme.font.size.md}; -`; - -const IconArea = styled('div')` - align-self: flex-start; - width: 32px; - display: flex; - flex-direction: column; - align-items: flex-end; - flex-shrink: 0; - padding-top: ${p => p.theme.space.sm}; - gap: ${p => p.theme.space.xs}; -`; - -const AccentIcon = styled(IconStack)` - color: ${p => p.theme.tokens.graphics.accent.vibrant}; -`; - -const MetaRow = styled('div')` - display: inline-grid; - grid-auto-flow: column dense; - gap: ${p => p.theme.space.sm}; - justify-content: start; - align-items: center; - color: ${p => p.theme.tokens.content.secondary}; - font-size: ${p => p.theme.font.size.sm}; - white-space: nowrap; - line-height: 1.2; - min-height: ${p => p.theme.space.xl}; -`; - -const Dot = styled('div')` - width: 3px; - height: 3px; - border-radius: 50%; - background: currentcolor; - flex-shrink: 0; -`; - -const LastSeenColumn = styled('div')` - display: flex; - align-items: center; - justify-content: flex-end; - width: 86px; - padding-right: ${p => p.theme.space.xl}; - margin-right: ${p => p.theme.space.xl}; - - @container (width < ${COLUMN_BREAKPOINTS.LAST_SEEN}) { - display: none; - } -`; - -const FirstSeenColumn = styled('div')` - display: flex; - align-items: center; - justify-content: flex-end; - width: 50px; - padding-right: ${p => p.theme.space.xl}; - margin-right: ${p => p.theme.space.xl}; - - @container (width < ${COLUMN_BREAKPOINTS.FIRST_SEEN}) { - display: none; - } -`; - -const ChartColumn = styled('div')` - width: 175px; - align-self: center; - margin-right: ${p => p.theme.space.xl}; - - @container (width < ${COLUMN_BREAKPOINTS.TREND}) { - display: none; - } -`; - -const DataColumn = styled('div')` - display: flex; - justify-content: flex-end; - text-align: right; - align-items: center; - align-self: center; - padding-right: ${p => p.theme.space.xl}; - margin-right: ${p => p.theme.space.xl}; - width: 60px; -`; - -const EventsColumn = styled(DataColumn)` - @container (width < ${COLUMN_BREAKPOINTS.EVENTS}) { - display: none; - } -`; - -const UsersColumn = styled(DataColumn)` - @container (width < ${COLUMN_BREAKPOINTS.USERS}) { - display: none; - } -`; - -const PrimaryCount = styled(Count)` - font-size: ${p => p.theme.font.size.md}; - display: flex; - justify-content: right; - margin-bottom: ${p => p.theme.space['2xs']}; - font-variant-numeric: tabular-nums; -`; - -const SecondaryCount = styled(Count)` - font-size: ${p => p.theme.font.size.sm}; - display: flex; - justify-content: flex-end; - color: ${p => p.theme.tokens.content.secondary}; - font-variant-numeric: tabular-nums; -`; - -// Empty spacers to match StreamGroup column widths and keep alignment -const PrioritySpacer = styled('div')` - width: 64px; - padding-right: ${p => p.theme.space.xl}; - margin-right: ${p => p.theme.space.xl}; - - @container (width < ${COLUMN_BREAKPOINTS.PRIORITY}) { - display: none; - } -`; - -const AssigneeSpacer = styled('div')` - width: 66px; - padding-right: ${p => p.theme.space.xl}; - margin-right: ${p => p.theme.space.xl}; - - @container (width < ${COLUMN_BREAKPOINTS.ASSIGNEE}) { - display: none; - } -`; diff --git a/static/app/utils/analytics/workflowAnalyticsEvents.tsx b/static/app/utils/analytics/workflowAnalyticsEvents.tsx index 295f5cc014cf..6ae8631903d0 100644 --- a/static/app/utils/analytics/workflowAnalyticsEvents.tsx +++ b/static/app/utils/analytics/workflowAnalyticsEvents.tsx @@ -181,14 +181,6 @@ export type TeamInsightsEventParameters = { 'releases_list.click_add_release_health': { project_id: number; }; - 'supergroup.drawer_opened': { - supergroup_id: number; - }; - 'supergroup.feedback_submitted': { - choice_selected: boolean; - supergroup_id: number; - user_id: string; - }; 'suspect_commit.feedback_submitted': { choice_selected: boolean; group_owner_id: number; @@ -267,7 +259,5 @@ export const workflowEventMap: Record = { 'releases_list.click_add_release_health': 'Releases List: Click Add Release Health', trace_timeline_clicked: 'Trace Timeline Clicked', trace_timeline_more_events_clicked: 'Trace Timeline More Events Clicked', - 'supergroup.drawer_opened': 'Supergroup Drawer Opened', - 'supergroup.feedback_submitted': 'Supergroup Feedback Submitted', 'suspect_commit.feedback_submitted': 'Suspect Commit Feedback Submitted', }; diff --git a/static/app/views/issueDetails/sidebar/sidebar.tsx b/static/app/views/issueDetails/sidebar/sidebar.tsx index 66d1b20cd1f8..df5cbb871a13 100644 --- a/static/app/views/issueDetails/sidebar/sidebar.tsx +++ b/static/app/views/issueDetails/sidebar/sidebar.tsx @@ -29,7 +29,6 @@ import {FirstLastSeenSection} from 'sentry/views/issueDetails/sidebar/firstLastS import {MergedIssuesSidebarSection} from 'sentry/views/issueDetails/sidebar/mergedSidebarSection'; import {PeopleSection} from 'sentry/views/issueDetails/sidebar/peopleSection'; import {SimilarIssuesSidebarSection} from 'sentry/views/issueDetails/sidebar/similarIssuesSidebarSection'; -import {SupergroupSection} from 'sentry/views/issueDetails/sidebar/supergroupSection'; type Props = {group: Group; project: Project; event?: Event}; @@ -128,9 +127,6 @@ export function IssueDetailsSidebar({group, event, project}: Props) { {issueTypeConfig.detector.enabled && ( )} - - - )} diff --git a/static/app/views/issueDetails/sidebar/supergroupSection.spec.tsx b/static/app/views/issueDetails/sidebar/supergroupSection.spec.tsx deleted file mode 100644 index 2ec108d52669..000000000000 --- a/static/app/views/issueDetails/sidebar/supergroupSection.spec.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import {GroupFixture} from 'sentry-fixture/group'; -import {OrganizationFixture} from 'sentry-fixture/organization'; - -import {render, screen} from 'sentry-test/reactTestingLibrary'; - -import {SupergroupSection} from 'sentry/views/issueDetails/sidebar/supergroupSection'; - -describe('SupergroupSection', () => { - it('renders supergroup info when issue belongs to one', async () => { - const organization = OrganizationFixture({features: ['top-issues-ui']}); - const group = GroupFixture({id: '1'}); - MockApiClient.addMockResponse({ - url: `/organizations/${organization.slug}/seer/supergroups/by-group/`, - body: { - data: [ - { - id: 10, - title: 'Null pointer in auth flow', - error_type: 'TypeError', - code_area: 'auth/login', - summary: '', - group_ids: [1, 2, 3], - created_at: '2024-01-01T00:00:00Z', - updated_at: '2024-01-01T00:00:00Z', - }, - ], - }, - }); - - render(, {organization}); - - expect(await screen.findByText('TypeError')).toBeInTheDocument(); - expect(screen.getByText('Null pointer in auth flow')).toBeInTheDocument(); - expect(screen.getByText('3 issues')).toBeInTheDocument(); - }); -}); diff --git a/static/app/views/issueDetails/sidebar/supergroupSection.tsx b/static/app/views/issueDetails/sidebar/supergroupSection.tsx deleted file mode 100644 index 0a3f2b54f496..000000000000 --- a/static/app/views/issueDetails/sidebar/supergroupSection.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import {Fragment} from 'react'; -import styled from '@emotion/styled'; - -import {useDrawer} from '@sentry/scraps/drawer'; -import InteractionStateLayer from '@sentry/scraps/interactionStateLayer'; -import {Flex, Stack} from '@sentry/scraps/layout'; -import {Text} from '@sentry/scraps/text'; - -import {IconStack} from 'sentry/icons'; -import {t, tn} from 'sentry/locale'; -import type {Group} from 'sentry/types/group'; -import {useOrganization} from 'sentry/utils/useOrganization'; -import {SidebarSectionTitle} from 'sentry/views/issueDetails/sidebar/sidebar'; -import {SupergroupDetailDrawer} from 'sentry/views/issueList/supergroups/supergroupDrawer'; -import {useSuperGroups} from 'sentry/views/issueList/supergroups/useSuperGroups'; - -interface SupergroupSectionProps { - group: Group; -} - -export function SupergroupSection({group}: SupergroupSectionProps) { - const organization = useOrganization(); - const {openDrawer} = useDrawer(); - const {data: lookup, isLoading} = useSuperGroups([group.id]); - const supergroup = lookup[group.id]; - - if (!organization.features.includes('top-issues-ui')) { - return null; - } - - if (isLoading || !supergroup) { - return null; - } - - const issueCount = supergroup.group_ids.length; - - const handleClick = () => { - openDrawer(() => , { - ariaLabel: t('Issue group details'), - drawerKey: 'supergroup-drawer', - }); - }; - - return ( -
- {t('Issue Group')} - - - - - - {supergroup.error_type ? ( - - {supergroup.error_type} - - ) : null} - - {supergroup.title} - - - {supergroup.code_area ? ( - - - {supergroup.code_area} - - - - ) : null} - - {tn('%s issue', '%s issues', issueCount)} - - - - - -
- ); -} - -const SupergroupCard = styled('button')` - position: relative; - width: 100%; - padding: ${p => p.theme.space.md}; - border: 1px solid ${p => p.theme.tokens.border.primary}; - border-radius: ${p => p.theme.radius.md}; - cursor: pointer; - background: transparent; - text-align: left; - font: inherit; - color: inherit; -`; - -const AccentIcon = styled(IconStack)` - color: ${p => p.theme.tokens.graphics.accent.vibrant}; - flex-shrink: 0; - margin-top: 2px; -`; - -const Dot = styled('div')` - width: 3px; - height: 3px; - border-radius: 50%; - background: currentcolor; - flex-shrink: 0; -`; diff --git a/static/app/views/issueList/groupListBody.tsx b/static/app/views/issueList/groupListBody.tsx index 539e1c1e6af8..c2530c6ffd35 100644 --- a/static/app/views/issueList/groupListBody.tsx +++ b/static/app/views/issueList/groupListBody.tsx @@ -1,20 +1,15 @@ -import {useMemo} from 'react'; import {useTheme} from '@emotion/react'; import type {GroupListColumn} from 'sentry/components/issues/groupList'; import {LoadingError} from 'sentry/components/loadingError'; import {PanelBody} from 'sentry/components/panels/panelBody'; import {LoadingStreamGroup, StreamGroup} from 'sentry/components/stream/group'; -import {SupergroupRow} from 'sentry/components/stream/supergroups/supergroupRow'; import {GroupStore} from 'sentry/stores/groupStore'; import type {Group} from 'sentry/types/group'; import type {IndexedMembersByProject} from 'sentry/utils/members/shared'; import {useMedia} from 'sentry/utils/useMedia'; import {useOrganization} from 'sentry/utils/useOrganization'; import {useSyncedLocalStorageState} from 'sentry/utils/useSyncedLocalStorageState'; -import {aggregateSupergroupStats} from 'sentry/views/issueList/supergroups/aggregateSupergroupStats'; -import type {SupergroupDetail} from 'sentry/views/issueList/supergroups/types'; -import type {SupergroupLookup} from 'sentry/views/issueList/supergroups/useSuperGroups'; import type {IssueUpdateData} from 'sentry/views/issueList/types'; import {useIssueProgress} from 'sentry/views/issueList/useIssueProgress'; @@ -34,7 +29,6 @@ type GroupListBodyProps = { refetchGroups: () => void; selectedProjectIds: number[]; onGroupClick?: (group: Group) => void; - supergroupLookup?: SupergroupLookup; withColumns?: GroupListColumn[]; }; @@ -46,7 +40,6 @@ type GroupListProps = { onActionTaken: (itemIds: string[], data: IssueUpdateData) => void; query: string; onGroupClick?: (group: Group) => void; - supergroupLookup?: SupergroupLookup; withColumns?: GroupListColumn[]; }; @@ -61,10 +54,6 @@ const DEFAULT_COLUMNS: GroupListColumn[] = [ 'lastTriggered', ]; -type RenderItem = - | {id: string; type: 'issue'} - | {matchingIds: string[]; supergroup: SupergroupDetail; type: 'supergroup'}; - function LoadingSkeleton({ pageSize, displayReprocessingLayout, @@ -100,7 +89,6 @@ export function GroupListBody({ pageSize, onActionTaken, onGroupClick, - supergroupLookup, withColumns, }: GroupListBodyProps) { const organization = useOrganization(); @@ -140,43 +128,11 @@ export function GroupListBody({ groupStatsPeriod={groupStatsPeriod} onActionTaken={onActionTaken} onGroupClick={onGroupClick} - supergroupLookup={supergroupLookup} withColumns={columns} /> ); } -function buildRenderItems( - groupIds: string[], - getSuperGroupForIssue: (id: string) => SupergroupDetail | null | undefined, - enabled: boolean -): RenderItem[] { - if (!enabled) { - return groupIds.map(id => ({type: 'issue' as const, id})); - } - - const seen = new Map(); - const items: RenderItem[] = []; - - for (const id of groupIds) { - const sg = getSuperGroupForIssue(id); - if (sg && sg.group_ids.length > 1) { - const existing = seen.get(sg.id); - if (existing) { - existing.push(id); - } else { - const matchingIds = [id]; - seen.set(sg.id, matchingIds); - items.push({type: 'supergroup', supergroup: sg, matchingIds}); - } - } else { - items.push({type: 'issue', id}); - } - } - - return items; -} - function GroupList({ groupIds, memberList, @@ -185,11 +141,9 @@ function GroupList({ groupStatsPeriod, onActionTaken, onGroupClick, - supergroupLookup, withColumns = DEFAULT_COLUMNS, }: GroupListProps) { const theme = useTheme(); - const organization = useOrganization(); const [isSavedSearchesOpen] = useSyncedLocalStorageState( SAVED_SEARCHES_SIDEBAR_OPEN_LOCALSTORAGE_KEY, false @@ -202,17 +156,6 @@ function GroupList({ const showProgress = withColumns.includes('progress'); const {data: progressData} = useIssueProgress(showProgress ? groupIds : []); - const hasTopIssuesUI = organization.features.includes('top-issues-ui'); - const renderItems = useMemo( - () => - buildRenderItems( - groupIds, - (id: string) => supergroupLookup?.[id] ?? null, - hasTopIssuesUI - ), - [groupIds, supergroupLookup, hasTopIssuesUI] - ); - const renderStreamGroup = (id: string, columns: GroupListColumn[]) => { const group = GroupStore.get(id) as Group | undefined; if (!group) { @@ -239,27 +182,5 @@ function GroupList({ ); }; - return ( - - {renderItems.map(item => { - if (item.type === 'issue') { - return renderStreamGroup(item.id, withColumns); - } - - const {supergroup, matchingIds} = item; - const memberGroups = matchingIds - .map(id => GroupStore.get(id) as Group | undefined) - .filter((g): g is Group => g !== undefined); - const stats = aggregateSupergroupStats(memberGroups, groupStatsPeriod); - - return ( - - ); - })} - - ); + return {groupIds.map(id => renderStreamGroup(id, withColumns))}; } diff --git a/static/app/views/issueList/issueListTable.tsx b/static/app/views/issueList/issueListTable.tsx index adce1655745b..2a0eb731e924 100644 --- a/static/app/views/issueList/issueListTable.tsx +++ b/static/app/views/issueList/issueListTable.tsx @@ -19,7 +19,6 @@ import {IssueListActions} from 'sentry/views/issueList/actions'; import {GroupListBody} from 'sentry/views/issueList/groupListBody'; import {IssueListBulkCommandPaletteActions} from 'sentry/views/issueList/issueListBulkCommandPaletteActions'; import {NewViewEmptyState} from 'sentry/views/issueList/newViewEmptyState'; -import type {SupergroupLookup} from 'sentry/views/issueList/supergroups/useSuperGroups'; import type {IssueUpdateData} from 'sentry/views/issueList/types'; interface IssueListTableProps { @@ -46,7 +45,6 @@ interface IssueListTableProps { statsLoading: boolean; statsPeriod: string; onGroupClick?: (group: Group) => void; - supergroupLookup?: SupergroupLookup; withColumns?: GroupListColumn[]; } @@ -73,7 +71,6 @@ export function IssueListTable({ issuesSuccessfullyLoaded, pageSize, onGroupClick, - supergroupLookup, withColumns, }: IssueListTableProps) { const location = useLocation(); @@ -146,7 +143,6 @@ export function IssueListTable({ refetchGroups={refetchGroups} onActionTaken={onActionTaken} onGroupClick={onGroupClick} - supergroupLookup={supergroupLookup} withColumns={withColumns} /> diff --git a/static/app/views/issueList/overview.tsx b/static/app/views/issueList/overview.tsx index 0a17422cde38..1f0d1714fb8b 100644 --- a/static/app/views/issueList/overview.tsx +++ b/static/app/views/issueList/overview.tsx @@ -54,8 +54,6 @@ import {IssueListTable} from 'sentry/views/issueList/issueListTable'; import {IssuesDataConsentBanner} from 'sentry/views/issueList/issuesDataConsentBanner'; import {IssueSelectionProvider} from 'sentry/views/issueList/issueSelectionContext'; import {IssueViewsHeader} from 'sentry/views/issueList/issueViewsHeader'; -import {useSupergroupDrawer} from 'sentry/views/issueList/supergroups/useSupergroupDrawer'; -import {useSuperGroups} from 'sentry/views/issueList/supergroups/useSuperGroups'; import type {IssueUpdateData} from 'sentry/views/issueList/types'; import {parseIssuePrioritySearch} from 'sentry/views/issueList/utils/parseIssuePrioritySearch'; import {useLLMContext} from 'sentry/views/seerExplorer/contexts/llmContext'; @@ -195,11 +193,6 @@ function IssueListOverviewInner({ useIssuesINPObserver(); - const {data: supergroupLookup, isLoading: supergroupsLoading} = - useSuperGroups(groupIds); - - useSupergroupDrawer({lookup: supergroupLookup, memberList}); - const onRealtimePoll = useCallback( (data: any, {queryCount: newQueryCount}: {queryCount: number}) => { // Note: We do not update state with cursors from polling, @@ -563,11 +556,6 @@ function IssueListOverviewInner({ num_issues: groups.length, group_ids: groups.map(group => group.id), total_issues_count: queryCount, - total_issue_group_count: new Set( - Object.values(supergroupLookup) - .filter(sg => sg !== null) - .map(sg => sg.id) - ).size, sort, realtime_active: realtimeActive, is_view: urlParams.viewId ? true : false, @@ -996,9 +984,8 @@ function IssueListOverviewInner({ displayReprocessingActions={displayReprocessingActions} memberList={memberList} selectedProjectIds={selection.projects} - issuesLoading={issuesLoading || supergroupsLoading} + issuesLoading={issuesLoading} statsLoading={statsLoading} - supergroupLookup={supergroupLookup} error={error} refetchGroups={fetchData} onGroupClick={isPreviewMode ? openIssuePreview : undefined} diff --git a/static/app/views/issueList/supergroups/aggregateSupergroupStats.spec.ts b/static/app/views/issueList/supergroups/aggregateSupergroupStats.spec.ts deleted file mode 100644 index abe0f467ac4e..000000000000 --- a/static/app/views/issueList/supergroups/aggregateSupergroupStats.spec.ts +++ /dev/null @@ -1,141 +0,0 @@ -import {GroupFixture} from 'sentry-fixture/group'; - -import type {Group} from 'sentry/types/group'; - -import {aggregateSupergroupStats} from './aggregateSupergroupStats'; - -describe('aggregateSupergroupStats', () => { - it('returns null for empty groups', () => { - expect(aggregateSupergroupStats([], '24h')).toBeNull(); - }); - - it('sums event and user counts', () => { - const groups = [ - GroupFixture({count: '10', userCount: 3}), - GroupFixture({count: '20', userCount: 7}), - ]; - const result = aggregateSupergroupStats(groups, '24h'); - expect(result?.eventCount).toBe(30); - expect(result?.userCount).toBe(10); - }); - - it('takes min firstSeen and max lastSeen', () => { - const groups = [ - GroupFixture({firstSeen: '2024-01-05T00:00:00Z', lastSeen: '2024-01-10T00:00:00Z'}), - GroupFixture({firstSeen: '2024-01-01T00:00:00Z', lastSeen: '2024-01-15T00:00:00Z'}), - ]; - const result = aggregateSupergroupStats(groups, '24h'); - expect(result?.firstSeen).toBe('2024-01-01T00:00:00Z'); - expect(result?.lastSeen).toBe('2024-01-15T00:00:00Z'); - }); - - it('point-wise sums stats timeseries', () => { - const groups = [ - GroupFixture({ - stats: { - '24h': [ - [1000, 1], - [2000, 2], - ], - }, - }), - GroupFixture({ - stats: { - '24h': [ - [1000, 3], - [2000, 4], - ], - }, - }), - ]; - const result = aggregateSupergroupStats(groups, '24h'); - expect(result?.mergedStats).toEqual([ - [1000, 4], - [2000, 6], - ]); - }); - - it('returns null filtered fields when no groups have filters', () => { - const groups = [GroupFixture({filtered: null})]; - const result = aggregateSupergroupStats(groups, '24h'); - expect(result?.filteredEventCount).toBeNull(); - expect(result?.filteredUserCount).toBeNull(); - expect(result?.mergedFilteredStats).toBeNull(); - }); - - it('treats missing counts as zero when stats have not loaded', () => { - const result = aggregateSupergroupStats( - [GroupFixture({count: '10', userCount: 3}), {} as Group], - '24h' - ); - expect(result?.eventCount).toBe(10); - expect(result?.userCount).toBe(3); - }); - - it('aggregates filtered stats separately', () => { - const groups = [ - GroupFixture({ - count: '100', - userCount: 50, - stats: { - '24h': [ - [1000, 10], - [2000, 20], - ], - }, - filtered: { - count: '30', - userCount: 15, - firstSeen: '2024-01-01T00:00:00Z', - lastSeen: '2024-01-10T00:00:00Z', - stats: { - '24h': [ - [1000, 3], - [2000, 5], - ], - }, - }, - }), - GroupFixture({ - count: '200', - userCount: 80, - stats: { - '24h': [ - [1000, 40], - [2000, 60], - ], - }, - filtered: { - count: '70', - userCount: 25, - firstSeen: '2024-01-02T00:00:00Z', - lastSeen: '2024-01-12T00:00:00Z', - stats: { - '24h': [ - [1000, 7], - [2000, 15], - ], - }, - }, - }), - ]; - - const result = aggregateSupergroupStats(groups, '24h'); - - // Total stats - expect(result?.eventCount).toBe(300); - expect(result?.userCount).toBe(130); - expect(result?.mergedStats).toEqual([ - [1000, 50], - [2000, 80], - ]); - - // Filtered stats - expect(result?.filteredEventCount).toBe(100); - expect(result?.filteredUserCount).toBe(40); - expect(result?.mergedFilteredStats).toEqual([ - [1000, 10], - [2000, 20], - ]); - }); -}); diff --git a/static/app/views/issueList/supergroups/aggregateSupergroupStats.ts b/static/app/views/issueList/supergroups/aggregateSupergroupStats.ts deleted file mode 100644 index 5a4d8943673f..000000000000 --- a/static/app/views/issueList/supergroups/aggregateSupergroupStats.ts +++ /dev/null @@ -1,95 +0,0 @@ -import type {TimeseriesValue} from 'sentry/types/core'; -import type {Group} from 'sentry/types/group'; - -export interface AggregatedSupergroupStats { - eventCount: number; - filteredEventCount: number | null; - filteredUserCount: number | null; - firstSeen: string | null; - lastSeen: string | null; - mergedFilteredStats: TimeseriesValue[] | null; - mergedStats: TimeseriesValue[]; - userCount: number; -} - -function addTimeseries( - acc: TimeseriesValue[] | null, - series: TimeseriesValue[] -): TimeseriesValue[] { - if (acc === null) { - return series.map(([ts, val]) => [ts, val]); - } - for (let i = 0; i < Math.min(acc.length, series.length); i++) { - acc[i] = [acc[i]![0], acc[i]![1] + series[i]![1]]; - } - return acc; -} - -/** - * Aggregate stats from member groups for display in a supergroup row. - * Sums event/user counts, takes min firstSeen and max lastSeen, - * and point-wise sums the trend data. - * - * When groups have filtered stats (from search filters), those are - * aggregated separately so the supergroup row can show total vs matching. - */ -export function aggregateSupergroupStats( - groups: Group[], - statsPeriod: string -): AggregatedSupergroupStats | null { - if (groups.length === 0) { - return null; - } - - let eventCount = 0; - let userCount = 0; - let filteredEventCount: number | null = null; - let filteredUserCount: number | null = null; - let firstSeen: string | null = null; - let lastSeen: string | null = null; - let mergedStats: TimeseriesValue[] | null = null; - let mergedFilteredStats: TimeseriesValue[] | null = null; - - for (const group of groups) { - eventCount += parseInt(group.count, 10) || 0; - userCount += group.userCount ?? 0; - - if (group.filtered) { - filteredEventCount ??= 0; - filteredUserCount ??= 0; - filteredEventCount += parseInt(group.filtered.count, 10) || 0; - filteredUserCount += group.filtered.userCount ?? 0; - - const filteredStats = group.filtered.stats?.[statsPeriod]; - if (filteredStats) { - mergedFilteredStats = addTimeseries(mergedFilteredStats, filteredStats); - } - } - - const gFirstSeen = group.lifetime?.firstSeen ?? group.firstSeen; - if (!firstSeen || gFirstSeen < firstSeen) { - firstSeen = gFirstSeen; - } - - const gLastSeen = group.lifetime?.lastSeen ?? group.lastSeen; - if (!lastSeen || gLastSeen > lastSeen) { - lastSeen = gLastSeen; - } - - const stats = group.stats?.[statsPeriod]; - if (stats) { - mergedStats = addTimeseries(mergedStats, stats); - } - } - - return { - eventCount, - userCount, - filteredEventCount, - filteredUserCount, - firstSeen, - lastSeen, - mergedStats: mergedStats ?? [], - mergedFilteredStats, - }; -} diff --git a/static/app/views/issueList/supergroups/supergroupDrawer.spec.tsx b/static/app/views/issueList/supergroups/supergroupDrawer.spec.tsx deleted file mode 100644 index 02ac237855d6..000000000000 --- a/static/app/views/issueList/supergroups/supergroupDrawer.spec.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import {GroupFixture} from 'sentry-fixture/group'; -import {OrganizationFixture} from 'sentry-fixture/organization'; -import {SupergroupDetailFixture} from 'sentry-fixture/supergroupDetail'; - -import {render, screen} from 'sentry-test/reactTestingLibrary'; - -import {SupergroupDetailDrawer} from 'sentry/views/issueList/supergroups/supergroupDrawer'; - -describe('SupergroupDetailDrawer', () => { - const organization = OrganizationFixture(); - const issuesUrl = `/organizations/${organization.slug}/issues/`; - - beforeEach(() => { - MockApiClient.clearMockResponses(); - MockApiClient.addMockResponse({ - url: '/organizations/org-slug/users/', - body: [], - }); - MockApiClient.addMockResponse({ - url: `/organizations/${organization.slug}/projects/`, - body: [], - }); - }); - - it('hoists stream matches to the first page for supergroups too large to inline', async () => { - // Over the inline-id limit, so hoisting takes the stream-scan path. - const memberIds = Array.from({length: 250}, (_, i) => i + 1); - const matchedMemberId = '100'; - const nonMemberId = '9999'; - - // Page fetch: only this request has `group=`. - const otherMembers = memberIds - .filter(id => String(id) !== matchedMemberId) - .slice(0, 24) - .map(id => GroupFixture({id: String(id), title: `OTHER_${id}`})); - MockApiClient.addMockResponse({ - url: issuesUrl, - method: 'GET', - body: [ - GroupFixture({id: matchedMemberId, metadata: {title: 'MATCHED_MEMBER'}}), - ...otherMembers, - ], - match: [(_url, options) => Array.isArray(options.query?.group)], - }); - - // Stream scan: one member + one non-member. Only the member should hoist. - MockApiClient.addMockResponse({ - url: issuesUrl, - method: 'GET', - body: [ - GroupFixture({id: matchedMemberId, metadata: {title: 'MATCHED_MEMBER'}}), - GroupFixture({id: nonMemberId, title: 'NON_MEMBER'}), - ], - match: [(_url, options) => !options.query?.group], - }); - - render( - , - { - organization, - initialRouterConfig: { - route: '/organizations/:orgId/issues/', - location: { - pathname: '/organizations/:orgId/issues/', - query: {query: 'is:unresolved'}, - }, - }, - } - ); - - expect(await screen.findByText('MATCHED_MEMBER')).toBeInTheDocument(); - const rows = screen.getAllByTestId('group'); - expect(rows[0]).toHaveTextContent('MATCHED_MEMBER'); - }); -}); diff --git a/static/app/views/issueList/supergroups/supergroupDrawer.tsx b/static/app/views/issueList/supergroups/supergroupDrawer.tsx deleted file mode 100644 index 4ca5618ad89a..000000000000 --- a/static/app/views/issueList/supergroups/supergroupDrawer.tsx +++ /dev/null @@ -1,614 +0,0 @@ -import {Fragment, useCallback, useEffect, useMemo, useState} from 'react'; -import styled from '@emotion/styled'; -import {useQuery, useQueryClient} from '@tanstack/react-query'; - -import {Badge} from '@sentry/scraps/badge'; -import {Button} from '@sentry/scraps/button'; -import {Checkbox} from '@sentry/scraps/checkbox'; -import {inlineCodeStyles} from '@sentry/scraps/code'; -import {DrawerBody, DrawerHeader} from '@sentry/scraps/drawer'; -import {Container, Flex, Stack} from '@sentry/scraps/layout'; -import {Heading, Text} from '@sentry/scraps/text'; - -import {bulkDelete, bulkUpdate, mergeGroups} from 'sentry/actionCreators/group'; -import { - addErrorMessage, - addLoadingMessage, - clearIndicators, -} from 'sentry/actionCreators/indicator'; -import {AnalyticsArea, useAnalyticsArea} from 'sentry/components/analyticsArea'; -import {NavigationCrumbs} from 'sentry/components/events/eventDrawer'; -import {FeedbackButton} from 'sentry/components/feedbackButton/feedbackButton'; -import type {GroupListColumn} from 'sentry/components/issues/groupList'; -import {IssueStreamHeaderLabel} from 'sentry/components/IssueStreamHeaderLabel'; -import {ALL_ACCESS_PROJECTS} from 'sentry/components/pageFilters/constants'; -import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters'; -import {Panel} from 'sentry/components/panels/panel'; -import {PanelBody} from 'sentry/components/panels/panelBody'; -import { - DEFAULT_STREAM_GROUP_STATS_PERIOD, - LoadingStreamGroup, - StreamGroup, -} from 'sentry/components/stream/group'; -import {IconChevron, IconFilter} from 'sentry/icons'; -import {t} from 'sentry/locale'; -import {GroupStore} from 'sentry/stores/groupStore'; -import type {Group} from 'sentry/types/group'; -import {trackAnalytics} from 'sentry/utils/analytics'; -import {apiOptions} from 'sentry/utils/api/apiOptions'; -import {uniq} from 'sentry/utils/array/uniq'; -import {MarkedText} from 'sentry/utils/marked/markedText'; -import type {IndexedMembersByProject} from 'sentry/utils/members/shared'; -import {useApi} from 'sentry/utils/useApi'; -import {useLocation} from 'sentry/utils/useLocation'; -import {useOrganization} from 'sentry/utils/useOrganization'; -import {ActionSet} from 'sentry/views/issueList/actions/actionSet'; -import {COLUMN_BREAKPOINTS, ConfirmAction} from 'sentry/views/issueList/actions/utils'; -import { - IssueSelectionProvider, - useIssueSelectionActions, - useIssueSelectionSummary, -} from 'sentry/views/issueList/issueSelectionContext'; -import {SupergroupFeedback} from 'sentry/views/issueList/supergroups/supergroupFeedback'; -import type {SupergroupDetail} from 'sentry/views/issueList/supergroups/types'; -import type {IssueUpdateData} from 'sentry/views/issueList/types'; -const DRAWER_COLUMNS: GroupListColumn[] = [ - 'graph', - 'event', - 'users', - 'assignee', - 'firstSeen', - 'lastSeen', -]; - -interface SupergroupDetailDrawerProps { - supergroup: SupergroupDetail; - filterWithCurrentSearch?: boolean; - memberList?: IndexedMembersByProject; -} - -export function SupergroupDetailDrawer({ - supergroup, - memberList, - filterWithCurrentSearch, -}: SupergroupDetailDrawerProps) { - const organization = useOrganization(); - - useEffect(() => { - trackAnalytics('supergroup.drawer_opened', { - supergroup_id: supergroup.id, - organization, - }); - }, [supergroup.id, organization]); - - return ( - - - - - - {t('Experimental')} - - - - - - - - - - - - - {supergroup.summary && ( - - - - )} - - - {supergroup.error_type && ( - - - {t('Error')} - - {supergroup.error_type} - - )} - {supergroup.code_area && ( - - - {t('Location')} - - {supergroup.code_area} - - )} - - - - - {supergroup.group_ids.length > 0 && ( - - - - )} - - - ); -} - -const PAGE_SIZE = 25; - -/** - * How many items we're willing to hoist. Doubles as the inline-id cap (members - * we'll embed in `issue.id:[…]` before the URL gets too big) and the stream- - * scan window (top stream results we inspect when the id list is too big to - * inline). - */ -const HOIST_LIMIT = 50; - -function SupergroupIssueList({ - groupIds, - memberList, - filterWithCurrentSearch, -}: { - groupIds: number[]; - memberList: IndexedMembersByProject | undefined; - filterWithCurrentSearch?: boolean; -}) { - const organization = useOrganization(); - const location = useLocation(); - const [page, setPage] = useState(0); - - const { - query: searchQuery, - project, - environment, - statsPeriod, - start, - end, - } = location.query; - const query = typeof searchQuery === 'string' ? searchQuery : ''; - - const inlineIdListFits = groupIds.length <= HOIST_LIMIT; - - const {data: matchedGroups, isPending: matchedPending} = useQuery({ - ...apiOptions.as()('/organizations/$organizationIdOrSlug/issues/', { - path: {organizationIdOrSlug: organization.slug}, - query: { - project, - environment, - statsPeriod, - start, - end, - query: inlineIdListFits ? `${query} issue.id:[${groupIds.join(',')}]` : query, - per_page: HOIST_LIMIT, - }, - staleTime: 30_000, - }), - enabled: !!filterWithCurrentSearch && groupIds.length > 0, - }); - - const matchedIds = useMemo(() => { - if (!matchedGroups) { - return new Set(); - } - if (inlineIdListFits) { - return new Set(matchedGroups.map(g => g.id)); - } - const memberSet = new Set(groupIds.map(String)); - return new Set(matchedGroups.map(g => g.id).filter(id => memberSet.has(id))); - }, [matchedGroups, inlineIdListFits, groupIds]); - - // When filtering with the stream query, wait for the match so the IDs we - // fetch reflect the hoisted ordering. - const pageFetchReady = !filterWithCurrentSearch || !matchedPending; - const sortedGroupIds = useMemo( - () => - filterWithCurrentSearch - ? [...groupIds].sort( - (a, b) => - Number(matchedIds.has(String(b))) - Number(matchedIds.has(String(a))) - ) - : groupIds, - [filterWithCurrentSearch, groupIds, matchedIds] - ); - - const totalPages = Math.max(1, Math.ceil(sortedGroupIds.length / PAGE_SIZE)); - const safePage = Math.min(page, totalPages - 1); - const visibleIds = sortedGroupIds.slice( - safePage * PAGE_SIZE, - (safePage + 1) * PAGE_SIZE - ); - - const {data: allGroups, isPending: allPending} = useQuery({ - ...apiOptions.as()('/organizations/$organizationIdOrSlug/issues/', { - path: {organizationIdOrSlug: organization.slug}, - query: { - group: visibleIds.map(String), - project: ALL_ACCESS_PROJECTS, - }, - staleTime: 30_000, - }), - enabled: pageFetchReady, - }); - - if (allPending || (filterWithCurrentSearch && matchedPending)) { - return ( - - {filterWithCurrentSearch && ( - - - - {t('Matches current filters')} - - - )} - - - {t('Issue')} - - - - {visibleIds.map(id => ( - - ))} - - - - ); - } - - const groupMap = new Map(allGroups?.map(g => [g.id, g])); - const sortedGroups = visibleIds - .map(id => groupMap.get(String(id))) - .filter((g): g is Group => g !== undefined) - .sort((a, b) => Number(matchedIds.has(b.id)) - Number(matchedIds.has(a.id))); - const visibleGroupIds = sortedGroups.map(g => g.id); - - return ( - - {filterWithCurrentSearch && ( - - - - {t('Matches current filters')} - - - )} - - - - - {sortedGroups.map(group => { - const members = group.project - ? memberList?.get(group.project.slug) - : undefined; - return ( - - {matchedIds.has(group.id) && ( - - - - )} - - - ); - })} - - - - {totalPages > 1 && ( - - - {`${safePage * PAGE_SIZE + 1}-${Math.min((safePage + 1) * PAGE_SIZE, sortedGroupIds.length)} of ${sortedGroupIds.length}`} - - -