diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 7bb4781513d95f..a2f08ede5ed774 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -678,6 +678,7 @@ tests/sentry/api/endpoints/test_organization_attribute_mappings.py @get
/static/app/components/events/highlights/ @getsentry/issue-workflow
/static/app/components/issues/ @getsentry/issue-workflow
/static/app/components/stackTrace/ @getsentry/issue-workflow
+/static/app/components/stream/supergroupRow.tsx @getsentry/issue-detection-frontend
/static/app/views/issueList/ @getsentry/issue-workflow
/static/app/views/issueList/issueListSeerComboBox.tsx @getsentry/issue-workflow @getsentry/machine-learning-ai
/static/app/views/issueList/pages/supergroups.tsx @getsentry/issue-detection-frontend
diff --git a/static/app/components/group/issueSuperGroup.tsx b/static/app/components/group/issueSuperGroup.tsx
deleted file mode 100644
index 62a717a650a618..00000000000000
--- a/static/app/components/group/issueSuperGroup.tsx
+++ /dev/null
@@ -1,109 +0,0 @@
-import styled from '@emotion/styled';
-
-import {Container, Flex, Stack} from '@sentry/scraps/layout';
-import {Text} from '@sentry/scraps/text';
-import {Tooltip} from '@sentry/scraps/tooltip';
-
-import {useDrawer} from 'sentry/components/globalDrawer';
-import {IconFocus, IconStack} from 'sentry/icons';
-import {t, tn} from 'sentry/locale';
-import {SupergroupDetailDrawer} from 'sentry/views/issueList/supergroups/supergroupDrawer';
-import type {SupergroupDetail} from 'sentry/views/issueList/supergroups/types';
-
-interface Props {
- supergroup: SupergroupDetail;
-}
-
-/**
- * Show a badge indicating an issue belongs to a supergroup.
- */
-export function IssueSuperGroup({supergroup}: Props) {
- const {openDrawer} = useDrawer();
-
- const tooltipTitle = (
-
-
-
- {supergroup.title}
-
-
- {tn('%s issue', '%s issues', supergroup.group_ids.length)}
-
-
-
-
- {supergroup.error_type ? (
-
-
- {t('Error:')}
-
-
- {supergroup.error_type}
-
-
- ) : null}
- {supergroup.code_area ? (
-
-
- {t('Location:')}
-
-
- {supergroup.code_area}
-
-
- ) : null}
-
-
- {supergroup.summary ? (
-
-
-
-
-
- {t('Root Cause')}
-
-
-
- {supergroup.summary}
-
-
-
- ) : null}
-
- );
-
- const handleClick = (e: React.MouseEvent) => {
- e.preventDefault();
- e.stopPropagation();
- openDrawer(() => , {
- ariaLabel: t('Supergroup details'),
- drawerKey: 'supergroup-drawer',
- });
- };
-
- return (
-
-
-
- {supergroup.group_ids.length > 50 ? '50+' : supergroup.group_ids.length}
-
-
- );
-}
-
-const SuperGroupButton = styled('button')`
- display: inline-flex;
- align-items: center;
- background: none;
- border: none;
- padding: 0;
- cursor: pointer;
- color: ${p => p.theme.colors.gray500};
- font-size: ${p => p.theme.font.size.sm};
- gap: 0 ${p => p.theme.space.xs};
- position: relative;
-
- &:hover {
- color: ${p => p.theme.tokens.interactive.link.accent.hover};
- }
-`;
diff --git a/static/app/components/groupMetaRow.tsx b/static/app/components/groupMetaRow.tsx
index 1a4eac3eac4821..125c66974d7f49 100644
--- a/static/app/components/groupMetaRow.tsx
+++ b/static/app/components/groupMetaRow.tsx
@@ -13,7 +13,6 @@ import {TimesTag} from 'sentry/components/group/inboxBadges/timesTag';
import {UnhandledTag} from 'sentry/components/group/inboxBadges/unhandledTag';
import {IssueReplayCount} from 'sentry/components/group/issueReplayCount';
import {IssueSeerBadge} from 'sentry/components/group/issueSeerBadge';
-import {IssueSuperGroup} from 'sentry/components/group/issueSuperGroup';
import ProjectBadge from 'sentry/components/idBadge/projectBadge';
import {extractSelectionParameters} from 'sentry/components/pageFilters/parse';
import {Placeholder} from 'sentry/components/placeholder';
@@ -24,7 +23,6 @@ import {defined} from 'sentry/utils';
import {getTitle} from 'sentry/utils/events';
import {useReplayCountForIssues} from 'sentry/utils/replayCount/useReplayCountForIssues';
import {projectCanLinkToReplay} from 'sentry/utils/replays/projectSupportsReplay';
-import {useSuperGroupForIssues} from 'sentry/utils/supergroup/useSuperGroupForIssues';
import {useLocation} from 'sentry/utils/useLocation';
import {useOrganization} from 'sentry/utils/useOrganization';
@@ -75,7 +73,6 @@ export function GroupMetaRow({data, showAssignee, showLifetime = true}: Props) {
const issuesPath = `/organizations/${organization.slug}/issues/`;
const {getReplayCountForIssue} = useReplayCountForIssues();
- const {getSuperGroupForIssue} = useSuperGroupForIssues();
const showReplayCount =
organization.features.includes('session-replay') &&
@@ -83,10 +80,6 @@ export function GroupMetaRow({data, showAssignee, showLifetime = true}: Props) {
data.issueCategory &&
!!getReplayCountForIssue(data.id, data.issueCategory);
- const supergroup = organization.features.includes('top-issues-ui')
- ? getSuperGroupForIssue(data.id)
- : undefined;
-
const autofixRunExists = getAutofixRunExists(data);
const seerFixable = isIssueQuickFixable(data);
const showSeer =
@@ -126,7 +119,6 @@ export function GroupMetaRow({data, showAssignee, showLifetime = true}: Props) {
) : null,
showReplayCount ? : null,
- supergroup ? : null,
showSeer ? : null,
logger ? (
diff --git a/static/app/components/stream/supergroupRow.tsx b/static/app/components/stream/supergroupRow.tsx
new file mode 100644
index 00000000000000..6f07d00c579e51
--- /dev/null
+++ b/static/app/components/stream/supergroupRow.tsx
@@ -0,0 +1,267 @@
+import {useState} from 'react';
+import styled from '@emotion/styled';
+
+import InteractionStateLayer from '@sentry/scraps/interactionStateLayer';
+import {Text} from '@sentry/scraps/text';
+
+import {GroupStatusChart} from 'sentry/components/charts/groupStatusChart';
+import {Count} from 'sentry/components/count';
+import {useDrawer} from 'sentry/components/globalDrawer';
+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 type {AggregatedSupergroupStats} from 'sentry/utils/supergroup/aggregateSupergroupStats';
+import {COLUMN_BREAKPOINTS} from 'sentry/views/issueList/actions/utils';
+import {SupergroupDetailDrawer} from 'sentry/views/issueList/supergroups/supergroupDrawer';
+import type {SupergroupDetail} from 'sentry/views/issueList/supergroups/types';
+
+interface SupergroupRowProps {
+ matchedCount: number;
+ supergroup: SupergroupDetail;
+ aggregatedStats?: AggregatedSupergroupStats | null;
+}
+
+export function SupergroupRow({
+ supergroup,
+ matchedCount,
+ aggregatedStats,
+}: SupergroupRowProps) {
+ const {openDrawer, isDrawerOpen} = useDrawer();
+ const [isActive, setIsActive] = useState(false);
+ const handleClick = () => {
+ setIsActive(true);
+ openDrawer(() => , {
+ ariaLabel: t('Supergroup details'),
+ drawerKey: 'supergroup-drawer',
+ onClose: () => setIsActive(false),
+ });
+ };
+
+ const highlighted = isActive && isDrawerOpen;
+
+ return (
+
+
+
+
+
+
+ {supergroup.error_type ? (
+
+ {supergroup.error_type}
+
+ ) : null}
+
+ {supergroup.title}
+
+
+ {supergroup.code_area ? (
+
+ {supergroup.code_area}
+
+ ) : null}
+ {supergroup.code_area && matchedCount > 0 ? : null}
+ {matchedCount > 0 ? (
+
+ {matchedCount} / {supergroup.group_ids.length} {t('issues matched')}
+
+ ) : null}
+
+
+
+
+ {aggregatedStats?.lastSeen ? (
+
+ ) : (
+
+ )}
+
+
+
+ {aggregatedStats?.firstSeen ? (
+
+ ) : (
+
+ )}
+
+
+
+ {aggregatedStats?.mergedStats && aggregatedStats.mergedStats.length > 0 ? (
+
+ ) : (
+
+ )}
+
+
+
+ {aggregatedStats ? (
+
+ ) : (
+
+ )}
+
+
+
+ {aggregatedStats ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ );
+}
+
+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;
+ align-items: center;
+ justify-content: flex-end;
+ flex-shrink: 0;
+ padding-top: ${p => p.theme.space.sm};
+`;
+
+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;
+`;
+
+// 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/supergroup/aggregateSupergroupStats.ts b/static/app/utils/supergroup/aggregateSupergroupStats.ts
new file mode 100644
index 00000000000000..09e7013875168f
--- /dev/null
+++ b/static/app/utils/supergroup/aggregateSupergroupStats.ts
@@ -0,0 +1,58 @@
+import type {TimeseriesValue} from 'sentry/types/core';
+import type {Group} from 'sentry/types/group';
+
+export interface AggregatedSupergroupStats {
+ eventCount: number;
+ firstSeen: string | null;
+ lastSeen: string | null;
+ mergedStats: TimeseriesValue[];
+ userCount: number;
+}
+
+/**
+ * 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.
+ */
+export function aggregateSupergroupStats(
+ groups: Group[],
+ statsPeriod: string
+): AggregatedSupergroupStats | null {
+ if (groups.length === 0) {
+ return null;
+ }
+
+ let eventCount = 0;
+ let userCount = 0;
+ let firstSeen: string | null = null;
+ let lastSeen: string | null = null;
+ let mergedStats: TimeseriesValue[] = [];
+
+ for (const group of groups) {
+ eventCount += parseInt(group.count, 10) || 0;
+ userCount += group.userCount || 0;
+
+ const gFirstSeen = group.lifetime?.firstSeen ?? group.firstSeen;
+ if (gFirstSeen && (!firstSeen || gFirstSeen < firstSeen)) {
+ firstSeen = gFirstSeen;
+ }
+
+ const gLastSeen = group.lifetime?.lastSeen ?? group.lastSeen;
+ if (gLastSeen && (!lastSeen || gLastSeen > lastSeen)) {
+ lastSeen = gLastSeen;
+ }
+
+ const stats = group.stats?.[statsPeriod];
+ if (stats) {
+ if (mergedStats.length === 0) {
+ mergedStats = stats.map(([ts, val]) => [ts, val] as TimeseriesValue);
+ } else {
+ for (let i = 0; i < Math.min(mergedStats.length, stats.length); i++) {
+ mergedStats[i] = [mergedStats[i]![0], mergedStats[i]![1] + stats[i]![1]];
+ }
+ }
+ }
+ }
+
+ return {eventCount, userCount, firstSeen, lastSeen, mergedStats};
+}
diff --git a/static/app/utils/supergroup/useSuperGroupForIssues.tsx b/static/app/utils/supergroup/useSuperGroupForIssues.tsx
deleted file mode 100644
index bab747aae8994b..00000000000000
--- a/static/app/utils/supergroup/useSuperGroupForIssues.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-import {useCallback} from 'react';
-
-import type {ApiResult} from 'sentry/api';
-import {getApiUrl} from 'sentry/utils/api/getApiUrl';
-import {useAggregatedQueryKeys} from 'sentry/utils/api/useAggregatedQueryKeys';
-import type {ApiQueryKey} from 'sentry/utils/queryClient';
-import {useOrganization} from 'sentry/utils/useOrganization';
-import type {SupergroupDetail} from 'sentry/views/issueList/supergroups/types';
-
-type SupergroupState = Record;
-
-function supergroupReducer(
- prevState: undefined | SupergroupState,
- response: ApiResult<{data: SupergroupDetail[]}>,
- aggregates: readonly string[]
-): undefined | SupergroupState {
- const defaults = Object.fromEntries(
- aggregates.map(id => [id, null])
- ) as SupergroupState;
- const supergroups = response[0].data;
- const byGroupId: SupergroupState = Object.fromEntries(
- supergroups.flatMap(sg => sg.group_ids.map(groupId => [String(groupId), sg]))
- );
- return {...defaults, ...prevState, ...byGroupId};
-}
-
-/**
- * Query results for whether an Issue/Group belongs to a supergroup.
- */
-export function useSuperGroupForIssues() {
- const organization = useOrganization();
-
- const cache = useAggregatedQueryKeys({
- cacheKey: `/organizations/${organization.slug}/seer/supergroups/by-group/`,
- bufferLimit: 25,
- getQueryKey: useCallback(
- (ids: readonly string[]): ApiQueryKey => [
- getApiUrl('/organizations/$organizationIdOrSlug/seer/supergroups/by-group/', {
- path: {organizationIdOrSlug: organization.slug},
- }),
- {
- query: {
- group_id: ids,
- },
- },
- ],
- [organization.slug]
- ),
- responseReducer: supergroupReducer,
- });
-
- const getSuperGroupForIssue = useCallback(
- (id: string): SupergroupDetail | null | undefined => {
- cache.buffer([id]);
- return cache.data?.[id];
- },
- [cache]
- );
-
- return {getSuperGroupForIssue};
-}
diff --git a/static/app/utils/supergroup/useSuperGroups.tsx b/static/app/utils/supergroup/useSuperGroups.tsx
new file mode 100644
index 00000000000000..664ea33df0a662
--- /dev/null
+++ b/static/app/utils/supergroup/useSuperGroups.tsx
@@ -0,0 +1,58 @@
+import {useMemo} 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 hasTopIssuesUI = organization.features.includes('top-issues-ui');
+ const enabled = hasTopIssuesUI && groupIds.length > 0;
+
+ const {data: response, isLoading} = useApiQuery<{data: SupergroupDetail[]}>(
+ [
+ getApiUrl('/organizations/$organizationIdOrSlug/seer/supergroups/by-group/', {
+ path: {organizationIdOrSlug: organization.slug},
+ }),
+ {
+ query: {
+ group_id: groupIds,
+ },
+ },
+ ],
+ {
+ staleTime: 30_000,
+ enabled,
+ }
+ );
+
+ const lookup = useMemo(() => {
+ if (!response?.data) {
+ return {};
+ }
+
+ const result: SupergroupLookup = Object.fromEntries(groupIds.map(id => [id, null]));
+ for (const sg of response.data) {
+ 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/groupListBody.tsx b/static/app/views/issueList/groupListBody.tsx
index b7fe8cd7f099ad..8690b712f67d45 100644
--- a/static/app/views/issueList/groupListBody.tsx
+++ b/static/app/views/issueList/groupListBody.tsx
@@ -1,3 +1,4 @@
+import {useMemo} from 'react';
import {useTheme} from '@emotion/react';
import type {IndexedMembersByProject} from 'sentry/actionCreators/members';
@@ -5,12 +6,16 @@ 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/supergroupRow';
import {GroupStore} from 'sentry/stores/groupStore';
import type {Group} from 'sentry/types/group';
+import {aggregateSupergroupStats} from 'sentry/utils/supergroup/aggregateSupergroupStats';
+import type {SupergroupLookup} from 'sentry/utils/supergroup/useSuperGroups';
import {useApi} from 'sentry/utils/useApi';
import {useMedia} from 'sentry/utils/useMedia';
import {useOrganization} from 'sentry/utils/useOrganization';
import {useSyncedLocalStorageState} from 'sentry/utils/useSyncedLocalStorageState';
+import type {SupergroupDetail} from 'sentry/views/issueList/supergroups/types';
import type {IssueUpdateData} from 'sentry/views/issueList/types';
import {NoGroupsHandler} from './noGroupsHandler';
@@ -28,6 +33,7 @@ type GroupListBodyProps = {
query: string;
refetchGroups: () => void;
selectedProjectIds: number[];
+ supergroupLookup?: SupergroupLookup;
};
type GroupListProps = {
@@ -37,6 +43,7 @@ type GroupListProps = {
memberList: IndexedMembersByProject;
onActionTaken: (itemIds: string[], data: IssueUpdateData) => void;
query: string;
+ supergroupLookup?: SupergroupLookup;
};
const COLUMNS: GroupListColumn[] = [
@@ -50,6 +57,10 @@ const COLUMNS: GroupListColumn[] = [
'lastTriggered',
];
+type RenderItem =
+ | {id: string; type: 'issue'}
+ | {matchingIds: string[]; supergroup: SupergroupDetail; type: 'supergroup'};
+
function LoadingSkeleton({
pageSize,
displayReprocessingLayout,
@@ -82,6 +93,7 @@ export function GroupListBody({
selectedProjectIds,
pageSize,
onActionTaken,
+ supergroupLookup,
}: GroupListBodyProps) {
const api = useApi();
const organization = useOrganization();
@@ -119,10 +131,42 @@ export function GroupListBody({
displayReprocessingLayout={displayReprocessingLayout}
groupStatsPeriod={groupStatsPeriod}
onActionTaken={onActionTaken}
+ supergroupLookup={supergroupLookup}
/>
);
}
+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,
@@ -130,8 +174,10 @@ function GroupList({
displayReprocessingLayout,
groupStatsPeriod,
onActionTaken,
+ supergroupLookup,
}: GroupListProps) {
const theme = useTheme();
+ const organization = useOrganization();
const [isSavedSearchesOpen] = useSyncedLocalStorageState(
SAVED_SEARCHES_SIDEBAR_OPEN_LOCALSTORAGE_KEY,
false
@@ -141,29 +187,58 @@ function GroupList({
`(width < ${isSavedSearchesOpen ? theme.breakpoints.xl : theme.breakpoints.md})`
);
+ 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) {
+ return null;
+ }
+ return (
+ onActionTaken([id], {priority})}
+ withColumns={columns}
+ />
+ );
+ };
+
return (
- {groupIds.map(id => {
- const hasGuideAnchor = id === topIssue;
- const group = GroupStore.get(id) as Group | undefined;
-
- if (!group) {
- return null;
+ {renderItems.map(item => {
+ if (item.type === 'issue') {
+ return renderStreamGroup(item.id, COLUMNS);
}
+ 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 (
- onActionTaken([id], {priority})}
- withColumns={COLUMNS}
+
);
})}
diff --git a/static/app/views/issueList/issueListTable.tsx b/static/app/views/issueList/issueListTable.tsx
index c3419f993bbe0b..b81579c6ed50ac 100644
--- a/static/app/views/issueList/issueListTable.tsx
+++ b/static/app/views/issueList/issueListTable.tsx
@@ -10,6 +10,7 @@ import {t} from 'sentry/locale';
import type {PageFilters} from 'sentry/types/core';
import {DemoTourElement, DemoTourStep} from 'sentry/utils/demoMode/demoTours';
import {VisuallyCompleteWithData} from 'sentry/utils/performanceForSentry';
+import type {SupergroupLookup} from 'sentry/utils/supergroup/useSuperGroups';
import {useLocation} from 'sentry/utils/useLocation';
import {IssueListActions} from 'sentry/views/issueList/actions';
import {GroupListBody} from 'sentry/views/issueList/groupListBody';
@@ -40,6 +41,7 @@ interface IssueListTableProps {
selection: PageFilters;
statsLoading: boolean;
statsPeriod: string;
+ supergroupLookup?: SupergroupLookup;
}
export function IssueListTable({
@@ -64,6 +66,7 @@ export function IssueListTable({
paginationAnalyticsEvent,
issuesSuccessfullyLoaded,
pageSize,
+ supergroupLookup,
}: IssueListTableProps) {
const location = useLocation();
@@ -124,6 +127,7 @@ export function IssueListTable({
pageSize={pageSize}
refetchGroups={refetchGroups}
onActionTaken={onActionTaken}
+ supergroupLookup={supergroupLookup}
/>
diff --git a/static/app/views/issueList/overview.tsx b/static/app/views/issueList/overview.tsx
index fc9dd16ae81b3d..7701534b39a2be 100644
--- a/static/app/views/issueList/overview.tsx
+++ b/static/app/views/issueList/overview.tsx
@@ -37,6 +37,7 @@ import type {RequestError} from 'sentry/utils/requestError/requestError';
import {useDisableRouteAnalytics} from 'sentry/utils/routeAnalytics/useDisableRouteAnalytics';
import {useRouteAnalyticsEventNames} from 'sentry/utils/routeAnalytics/useRouteAnalyticsEventNames';
import {useRouteAnalyticsParams} from 'sentry/utils/routeAnalytics/useRouteAnalyticsParams';
+import {useSuperGroups} from 'sentry/utils/supergroup/useSuperGroups';
import {normalizeUrl} from 'sentry/utils/url/normalizeUrl';
import {useApi} from 'sentry/utils/useApi';
import {useLocation} from 'sentry/utils/useLocation';
@@ -163,6 +164,9 @@ function IssueListOverview({
useIssuesINPObserver();
+ const {data: supergroupLookup, isLoading: supergroupsLoading} =
+ useSuperGroups(groupIds);
+
const onRealtimePoll = useCallback(
(data: any, {queryCount: newQueryCount}: {queryCount: number}) => {
// Note: We do not update state with cursors from polling,
@@ -900,8 +904,9 @@ function IssueListOverview({
displayReprocessingActions={displayReprocessingActions}
memberList={memberList}
selectedProjectIds={selection.projects}
- issuesLoading={issuesLoading}
+ issuesLoading={issuesLoading || supergroupsLoading}
statsLoading={statsLoading}
+ supergroupLookup={supergroupLookup}
error={error}
refetchGroups={fetchData}
paginationCaption={
diff --git a/static/app/views/issueList/supergroups/supergroupDrawer.tsx b/static/app/views/issueList/supergroups/supergroupDrawer.tsx
index a238000fad6326..885bdd9524464a 100644
--- a/static/app/views/issueList/supergroups/supergroupDrawer.tsx
+++ b/static/app/views/issueList/supergroups/supergroupDrawer.tsx
@@ -22,10 +22,7 @@ import type {SupergroupDetail} from 'sentry/views/issueList/supergroups/types';
export function SupergroupDetailDrawer({supergroup}: {supergroup: SupergroupDetail}) {
const organization = useOrganization();
const placeholderRows = Math.min(supergroup.group_ids.length, 10);
- const issueIdQuery =
- supergroup.group_ids.length === 1
- ? `issue.id:${supergroup.group_ids[0]}`
- : `issue.id:[${supergroup.group_ids.join(',')}]`;
+ const issueIdQuery = `issue.id:[${supergroup.group_ids.join(',')}]`;
return (