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}`}
-
-
- }
- aria-label={t('Previous')}
- disabled={safePage === 0}
- onClick={() => setPage(safePage - 1)}
- />
- }
- aria-label={t('Next')}
- disabled={safePage >= totalPages - 1}
- onClick={() => setPage(safePage + 1)}
- />
-
-
- )}
-
- );
-}
-
-function DrawerActionsBar({groupIds}: {groupIds: string[]}) {
- const api = useApi();
- const organization = useOrganization();
- const queryClient = useQueryClient();
- const area = useAnalyticsArea();
- const {selection} = usePageFilters();
- const {toggleSelectAllVisible, deselectAll} = useIssueSelectionActions();
- const {pageSelected, anySelected, multiSelected, selectedIdsSet} =
- useIssueSelectionSummary();
-
- const selectedProjectSlug = useMemo(() => {
- const projects = [...selectedIdsSet]
- .map(id => GroupStore.get(id))
- .filter((group): group is Group => !!group?.project)
- .map(group => group.project.slug);
- const uniqProjects = uniq(projects);
- return uniqProjects.length === 1 ? uniqProjects[0] : undefined;
- }, [selectedIdsSet]);
-
- const handleUpdate = useCallback(
- (data: IssueUpdateData) => {
- const itemIds = [...selectedIdsSet];
- if (itemIds.length) {
- addLoadingMessage(t('Saving changes\u2026'));
- }
- bulkUpdate(
- api,
- {
- orgId: organization.slug,
- itemIds,
- data,
- project: selection.projects,
- environment: selection.environments,
- ...selection.datetime,
- },
- {
- success: () => {
- clearIndicators();
- for (const itemId of itemIds) {
- queryClient.invalidateQueries({
- queryKey: [`/organizations/${organization.slug}/issues/${itemId}/`],
- exact: false,
- });
- }
- },
- error: () => {
- clearIndicators();
- addErrorMessage(t('Unable to update issues'));
- },
- }
- );
- deselectAll();
- },
- [api, organization.slug, selectedIdsSet, selection, queryClient, deselectAll]
- );
-
- const handleDelete = useCallback(() => {
- const itemIds = [...selectedIdsSet];
- bulkDelete(
- api,
- {
- orgId: organization.slug,
- itemIds,
- project: selection.projects,
- environment: selection.environments,
- ...selection.datetime,
- },
- {}
- );
- deselectAll();
- }, [api, organization.slug, selectedIdsSet, selection, deselectAll]);
-
- const handleMerge = useCallback(() => {
- const itemIds = [...selectedIdsSet];
- mergeGroups(
- api,
- {
- orgId: organization.slug,
- itemIds,
- project: selection.projects,
- environment: selection.environments,
- ...selection.datetime,
- },
- {}
- );
- trackAnalytics('issues_stream.merged', {
- organization,
- project_id: undefined,
- platform: undefined,
- items_merged: itemIds.length,
- area,
- });
- deselectAll();
- }, [api, area, organization, selectedIdsSet, selection, deselectAll]);
-
- const onShouldConfirm = useCallback(
- (action: ConfirmAction) => {
- switch (action) {
- case ConfirmAction.RESOLVE:
- case ConfirmAction.UNRESOLVE:
- case ConfirmAction.ARCHIVE:
- case ConfirmAction.SET_PRIORITY:
- case ConfirmAction.UNBOOKMARK:
- return pageSelected && selectedIdsSet.size > 1;
- case ConfirmAction.BOOKMARK:
- return selectedIdsSet.size > 1;
- case ConfirmAction.MERGE:
- case ConfirmAction.DELETE:
- default:
- return true;
- }
- },
- [pageSelected, selectedIdsSet.size]
- );
-
- return (
-
-
- {anySelected ? (
-
-
-
- ) : (
-
- {t('Issue')}
-
-
- )}
-
- );
-}
-
-function DrawerColumnHeaders() {
- return (
-
- {DRAWER_COLUMNS.includes('lastSeen') && (
-
- {t('Last Seen')}
-
- )}
- {DRAWER_COLUMNS.includes('firstSeen') && (
-
- {t('Age')}
-
- )}
- {DRAWER_COLUMNS.includes('graph') && (
-
- {t('Graph')}
-
- )}
- {DRAWER_COLUMNS.includes('event') && (
-
- {t('Events')}
-
- )}
- {DRAWER_COLUMNS.includes('users') && (
-
- {t('Users')}
-
- )}
- {DRAWER_COLUMNS.includes('assignee') && (
-
- {t('Assignee')}
-
- )}
-
- );
-}
-
-const ActionsBarContainer = styled('div')`
- display: flex;
- gap: ${p => p.theme.space.md};
- height: 36px;
- padding: 0;
- padding-left: ${p => p.theme.space.xl};
- align-items: center;
- background: ${p => p.theme.tokens.background.secondary};
- border-radius: ${p => p.theme.radius.md} ${p => p.theme.radius.md} 0 0;
- border-bottom: 1px solid ${p => p.theme.tokens.border.primary};
-`;
-
-const HeaderButtonsWrapper = styled('div')`
- flex: 1;
- display: flex;
- gap: ${p => p.theme.space.xs};
- white-space: nowrap;
-`;
-
-const IssueLabel = styled(IssueStreamHeaderLabel)`
- flex: 1;
-`;
-
-const ColumnLabel = styled(IssueStreamHeaderLabel)`
- width: 60px;
-`;
-
-const GraphColumnLabel = styled(IssueStreamHeaderLabel)`
- width: 175px;
-`;
-
-const AssigneeColumnLabel = styled(IssueStreamHeaderLabel)`
- width: 66px;
-`;
-
-const LoadingHeader = styled('div')`
- display: flex;
- min-height: 36px;
- padding: ${p => p.theme.space.xs} 0;
- padding-left: ${p => p.theme.space.xl};
- align-items: center;
- background: ${p => p.theme.tokens.background.secondary};
- border-radius: ${p => p.theme.radius.md} ${p => p.theme.radius.md} 0 0;
- border-bottom: 1px solid ${p => p.theme.tokens.border.primary};
-`;
-
-const DrawerContentBody = styled(DrawerBody)`
- padding: 0;
-`;
-
-const PanelContainer = styled(Panel)`
- container-type: inline-size;
-`;
-
-const MatchedIndicator = styled('div')`
- position: absolute;
- top: 32px;
- left: 18px;
- z-index: 2;
- color: ${p => p.theme.tokens.graphics.accent.vibrant};
- pointer-events: none;
-`;
-
-const IssueRow = styled('div')`
- position: relative;
-
- /* On hover or when checkbox is checked, hide the filter icon so the checkbox shows through */
- &:hover,
- &:has(input:checked) {
- ${MatchedIndicator} {
- display: none;
- }
- }
-`;
-
-const StyledMarkedText = styled(MarkedText)`
- code:not(pre code) {
- ${p => inlineCodeStyles(p.theme)};
- }
-`;
diff --git a/static/app/views/issueList/supergroups/supergroupFeedback.tsx b/static/app/views/issueList/supergroups/supergroupFeedback.tsx
deleted file mode 100644
index 16e24cc51261..000000000000
--- a/static/app/views/issueList/supergroups/supergroupFeedback.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import {useState} from 'react';
-import styled from '@emotion/styled';
-
-import {Button} from '@sentry/scraps/button';
-import {Flex} from '@sentry/scraps/layout';
-
-import {IconThumb} from 'sentry/icons';
-import {t} from 'sentry/locale';
-import {trackAnalytics} from 'sentry/utils/analytics';
-import {useOrganization} from 'sentry/utils/useOrganization';
-import {useUser} from 'sentry/utils/useUser';
-
-interface SupergroupFeedbackProps {
- supergroupId: number;
-}
-
-export function SupergroupFeedback({supergroupId}: SupergroupFeedbackProps) {
- const [feedbackSubmitted, setFeedbackSubmitted] = useState(false);
- const organization = useOrganization();
- const user = useUser();
-
- const handleFeedback = (isAccurate: boolean) => {
- trackAnalytics('supergroup.feedback_submitted', {
- choice_selected: isAccurate,
- supergroup_id: supergroupId,
- user_id: user.id,
- organization,
- });
-
- setFeedbackSubmitted(true);
- };
-
- return (
-
- {feedbackSubmitted ? (
- t('Thanks!')
- ) : (
-
- {t('Help us improve this feature. Is this grouping accurate?')}
-
- }
- onClick={() => handleFeedback(true)}
- aria-label={t('Yes, this grouping is accurate')}
- />
- }
- onClick={() => handleFeedback(false)}
- aria-label={t('No, this grouping is not accurate')}
- />
-
-
- )}
-
- );
-}
-
-const FeedbackContainer = styled('div')`
- display: flex;
- align-items: center;
- gap: ${p => p.theme.space.md};
- padding: ${p => p.theme.space.md} ${p => p.theme.space['2xl']};
- border-bottom: 1px solid ${p => p.theme.tokens.border.primary};
- background: ${p => p.theme.tokens.background.transparent.promotion.muted};
-`;
diff --git a/static/app/views/issueList/supergroups/types.ts b/static/app/views/issueList/supergroups/types.ts
deleted file mode 100644
index 21bd5b487195..000000000000
--- a/static/app/views/issueList/supergroups/types.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-export interface SupergroupDetail {
- code_area: string;
- created_at: string;
- error_type: string;
- group_ids: number[];
- id: number;
- summary: string;
- title: string;
- updated_at: string;
-}
diff --git a/static/app/views/issueList/supergroups/useSuperGroups.spec.tsx b/static/app/views/issueList/supergroups/useSuperGroups.spec.tsx
deleted file mode 100644
index 177e3bb6869c..000000000000
--- a/static/app/views/issueList/supergroups/useSuperGroups.spec.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-import {OrganizationFixture} from 'sentry-fixture/organization';
-import {SupergroupDetailFixture} from 'sentry-fixture/supergroupDetail';
-
-import {renderHookWithProviders, waitFor} from 'sentry-test/reactTestingLibrary';
-
-import {useSuperGroups} from 'sentry/views/issueList/supergroups/useSuperGroups';
-
-const organization = OrganizationFixture({features: ['top-issues-ui']});
-const API_URL = `/organizations/${organization.slug}/seer/supergroups/by-group/`;
-
-describe('useSuperGroups', () => {
- it('does not show loading state when archiving a group backfills a new one', async () => {
- const supergroup = SupergroupDetailFixture({group_ids: [1, 2, 3]});
- const mockRequest = MockApiClient.addMockResponse({
- url: API_URL,
- body: {data: [supergroup]},
- });
-
- const {result, rerender} = renderHookWithProviders(
- (props: {groupIds: string[]}) => useSuperGroups(props.groupIds),
- {
- organization,
- initialProps: {groupIds: ['1', '2', '3']},
- }
- );
-
- await waitFor(() => expect(result.current.isLoading).toBe(false));
- expect(mockRequest).toHaveBeenCalledTimes(1);
-
- // Group '3' is archived and removed from the list
- rerender({groupIds: ['1', '2']});
- expect(result.current.isLoading).toBe(false);
- expect(mockRequest).toHaveBeenCalledTimes(1);
-
- // A new group backfills into the list — should refetch in the background
- rerender({groupIds: ['1', '2', '4']});
- expect(result.current.isLoading).toBe(false);
-
- await waitFor(() => expect(mockRequest).toHaveBeenCalledTimes(2));
- });
-
- it('shows loading state when navigating to an entirely new page', async () => {
- const supergroup = SupergroupDetailFixture({group_ids: [1, 2, 3]});
- const mockRequest = MockApiClient.addMockResponse({
- url: API_URL,
- body: {data: [supergroup]},
- });
-
- const {result, rerender} = renderHookWithProviders(
- (props: {groupIds: string[]}) => useSuperGroups(props.groupIds),
- {
- organization,
- initialProps: {groupIds: ['1', '2', '3']},
- }
- );
-
- await waitFor(() => expect(result.current.isLoading).toBe(false));
- expect(mockRequest).toHaveBeenCalledTimes(1);
-
- // Navigate to a completely different page of results
- rerender({groupIds: ['4', '5', '6']});
- expect(result.current.isLoading).toBe(true);
- });
-});
diff --git a/static/app/views/issueList/supergroups/useSuperGroups.tsx b/static/app/views/issueList/supergroups/useSuperGroups.tsx
deleted file mode 100644
index 1174abfcdec0..000000000000
--- a/static/app/views/issueList/supergroups/useSuperGroups.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-import {useMemo, useRef} from 'react';
-
-import {getApiUrl} from 'sentry/utils/api/getApiUrl';
-import {useApiQuery} from 'sentry/utils/queryClient';
-import {useOrganization} from 'sentry/utils/useOrganization';
-import type {SupergroupDetail} from 'sentry/views/issueList/supergroups/types';
-
-export type SupergroupLookup = Record;
-
-/**
- * Fetch supergroup assignments for a batch of group IDs.
- * Returns a lookup map and loading state so callers can block rendering
- * until the data is available, preventing pop-in when issues are regrouped.
- */
-export function useSuperGroups(groupIds: string[]): {
- data: SupergroupLookup;
- isLoading: boolean;
-} {
- const organization = useOrganization();
- const requestedGroupIdsRef = useRef(groupIds);
- const hasTopIssuesUI = organization.features.includes('top-issues-ui');
-
- const previousRequestedGroupIds = requestedGroupIdsRef.current;
-
- // Stabilize the query key: if the new groupIds are a subset of what we
- // already requested (groups were removed), reuse the previous set to
- // avoid a redundant refetch.
- const requestedGroupIds = useMemo(() => {
- const prev = requestedGroupIdsRef.current;
- if (groupIds.length === 0 || prev.length < groupIds.length) {
- return groupIds;
- }
- const prevSet = new Set(prev);
- return groupIds.every(id => prevSet.has(id)) ? prev : groupIds;
- }, [groupIds]);
-
- requestedGroupIdsRef.current = requestedGroupIds;
- const enabled = hasTopIssuesUI && requestedGroupIds.length > 0;
-
- const {data: response, isLoading} = useApiQuery<{data: SupergroupDetail[]}>(
- [
- getApiUrl('/organizations/$organizationIdOrSlug/seer/supergroups/by-group/', {
- path: {organizationIdOrSlug: organization.slug},
- }),
- {
- query: {
- group_id: requestedGroupIds,
- status: 'unresolved',
- },
- },
- ],
- {
- staleTime: 30_000,
- enabled,
- retry: false,
- placeholderData: previousData => {
- if (!previousData) {
- return;
- }
- const prevSet = new Set(previousRequestedGroupIds);
- return groupIds.some(id => prevSet.has(id)) ? previousData : undefined;
- },
- }
- );
-
- const lookup = useMemo(() => {
- if (!response?.data) {
- return {};
- }
- const result: SupergroupLookup = Object.fromEntries(groupIds.map(id => [id, null]));
- for (const sg of response.data) {
- if (sg.group_ids.length <= 1) {
- continue;
- }
- for (const groupId of sg.group_ids) {
- result[String(groupId)] = sg;
- }
- }
- return result;
- }, [response, groupIds]);
-
- return {
- data: lookup,
- isLoading: enabled && isLoading,
- };
-}
diff --git a/static/app/views/issueList/supergroups/useSupergroupDrawer.tsx b/static/app/views/issueList/supergroups/useSupergroupDrawer.tsx
deleted file mode 100644
index f6d6ac63f783..000000000000
--- a/static/app/views/issueList/supergroups/useSupergroupDrawer.tsx
+++ /dev/null
@@ -1,121 +0,0 @@
-import {useEffect, useEffectEvent, useMemo, useRef} from 'react';
-import * as Sentry from '@sentry/react';
-import {skipToken, useQuery} from '@tanstack/react-query';
-
-import {useDrawer} from '@sentry/scraps/drawer';
-
-import {t} from 'sentry/locale';
-import {apiOptions} from 'sentry/utils/api/apiOptions';
-import type {IndexedMembersByProject} from 'sentry/utils/members/shared';
-import {useLocation} from 'sentry/utils/useLocation';
-import {useNavigate} from 'sentry/utils/useNavigate';
-import {useOrganization} from 'sentry/utils/useOrganization';
-import {SupergroupDetailDrawer} from 'sentry/views/issueList/supergroups/supergroupDrawer';
-import type {SupergroupDetail} from 'sentry/views/issueList/supergroups/types';
-import type {SupergroupLookup} from 'sentry/views/issueList/supergroups/useSuperGroups';
-
-export const SUPERGROUP_DRAWER_QUERY_PARAM = 'supergroupDrawer';
-
-interface UseSupergroupDrawerOptions {
- lookup: SupergroupLookup;
- memberList: IndexedMembersByProject | undefined;
-}
-
-export function useSupergroupDrawer({lookup, memberList}: UseSupergroupDrawerOptions) {
- const organization = useOrganization();
- const location = useLocation();
- const navigate = useNavigate();
- const {openDrawer} = useDrawer();
-
- const hasTopIssuesUI = organization.features.includes('top-issues-ui');
-
- const queryParam = location.query[SUPERGROUP_DRAWER_QUERY_PARAM];
- const drawerSupergroupId =
- hasTopIssuesUI && typeof queryParam === 'string' ? queryParam : undefined;
-
- const lookupSupergroup = useMemo(() => {
- if (!drawerSupergroupId) {
- return;
- }
- for (const sg of Object.values(lookup)) {
- if (sg && String(sg.id) === drawerSupergroupId) {
- return sg;
- }
- }
- return;
- }, [drawerSupergroupId, lookup]);
-
- const {data: fetchedSupergroupResponse, isError} = useQuery(
- apiOptions.as<{data: SupergroupDetail}>()(
- '/organizations/$organizationIdOrSlug/seer/supergroups/$supergroupId/',
- {
- path:
- drawerSupergroupId && !lookupSupergroup
- ? {
- organizationIdOrSlug: organization.slug,
- supergroupId: drawerSupergroupId,
- }
- : skipToken,
- staleTime: 30_000,
- }
- )
- );
-
- const fetchedSupergroup = fetchedSupergroupResponse?.data;
- const supergroup = lookupSupergroup ?? fetchedSupergroup;
-
- const stripDrawerParam = useEffectEvent(() => {
- navigate(
- {
- pathname: location.pathname,
- query: {
- ...location.query,
- [SUPERGROUP_DRAWER_QUERY_PARAM]: undefined,
- },
- },
- {replace: true, preventScrollReset: true}
- );
- });
-
- const lastOpenedIdRef = useRef(undefined);
-
- useEffect(() => {
- if (!hasTopIssuesUI) {
- return;
- }
- if (!drawerSupergroupId) {
- lastOpenedIdRef.current = undefined;
- return;
- }
-
- if (lastOpenedIdRef.current === drawerSupergroupId) {
- return;
- }
-
- if (!supergroup) {
- if (isError) {
- stripDrawerParam();
- }
- return;
- }
-
- lastOpenedIdRef.current = drawerSupergroupId;
- Sentry.getReplay()?.start();
- openDrawer(
- () => (
-
- ),
- {
- ariaLabel: t('Issue group details'),
- drawerKey: 'supergroup-drawer',
- shouldCloseOnLocationChange: nextLocation =>
- !nextLocation.query[SUPERGROUP_DRAWER_QUERY_PARAM],
- onClose: () => stripDrawerParam(),
- }
- );
- }, [hasTopIssuesUI, drawerSupergroupId, supergroup, isError, memberList, openDrawer]);
-}
diff --git a/tests/js/fixtures/supergroupDetail.ts b/tests/js/fixtures/supergroupDetail.ts
deleted file mode 100644
index 60613a67993f..000000000000
--- a/tests/js/fixtures/supergroupDetail.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import type {SupergroupDetail} from 'sentry/views/issueList/supergroups/types';
-
-export function SupergroupDetailFixture(
- params: Partial = {}
-): SupergroupDetail {
- return {
- id: 1,
- title: 'Test Supergroup',
- summary: 'A test supergroup',
- error_type: 'TypeError',
- code_area: 'frontend',
- group_ids: [1, 2, 3],
- created_at: '2024-01-01T00:00:00Z',
- updated_at: '2024-01-01T00:00:00Z',
- ...params,
- };
-}