From e87673583d4da03fb6f7553601df594274d4c3c2 Mon Sep 17 00:00:00 2001 From: Scott Cooper Date: Wed, 8 Apr 2026 15:00:51 -0700 Subject: [PATCH 1/2] feat(issue-details): Add supergroup section to issue details sidebar Show a compact supergroup card in the issue details sidebar when the current issue belongs to a supergroup. Clicking opens the existing supergroup drawer. --- .../streamline/sidebar/sidebar.tsx | 4 + .../sidebar/supergroupSection.spec.tsx | 101 ++++++++++++++++ .../streamline/sidebar/supergroupSection.tsx | 110 ++++++++++++++++++ 3 files changed, 215 insertions(+) create mode 100644 static/app/views/issueDetails/streamline/sidebar/supergroupSection.spec.tsx create mode 100644 static/app/views/issueDetails/streamline/sidebar/supergroupSection.tsx diff --git a/static/app/views/issueDetails/streamline/sidebar/sidebar.tsx b/static/app/views/issueDetails/streamline/sidebar/sidebar.tsx index 9da52c748257fb..c35151a3bdc092 100644 --- a/static/app/views/issueDetails/streamline/sidebar/sidebar.tsx +++ b/static/app/views/issueDetails/streamline/sidebar/sidebar.tsx @@ -30,6 +30,7 @@ import {MergedIssuesSidebarSection} from 'sentry/views/issueDetails/streamline/s import {PeopleSection} from 'sentry/views/issueDetails/streamline/sidebar/peopleSection'; import {SeerSection} from 'sentry/views/issueDetails/streamline/sidebar/seerSection'; import {SimilarIssuesSidebarSection} from 'sentry/views/issueDetails/streamline/sidebar/similarIssuesSidebarSection'; +import {SupergroupSection} from 'sentry/views/issueDetails/streamline/sidebar/supergroupSection'; type Props = {group: Group; project: Project; event?: Event}; @@ -130,6 +131,9 @@ export function StreamlinedSidebar({group, event, project}: Props) { {issueTypeConfig.detector.enabled && ( )} + + + )} diff --git a/static/app/views/issueDetails/streamline/sidebar/supergroupSection.spec.tsx b/static/app/views/issueDetails/streamline/sidebar/supergroupSection.spec.tsx new file mode 100644 index 00000000000000..558a34c6b65814 --- /dev/null +++ b/static/app/views/issueDetails/streamline/sidebar/supergroupSection.spec.tsx @@ -0,0 +1,101 @@ +import {GroupFixture} from 'sentry-fixture/group'; +import {OrganizationFixture} from 'sentry-fixture/organization'; + +import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; + +import {SupergroupSection} from 'sentry/views/issueDetails/streamline/sidebar/supergroupSection'; + +const organization = OrganizationFixture({features: ['top-issues-ui']}); + +describe('SupergroupSection', () => { + beforeEach(() => { + MockApiClient.clearMockResponses(); + }); + + it('renders supergroup card when issue belongs to a supergroup', async () => { + 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: 'Root cause 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('Supergroup')).toBeInTheDocument(); + expect(screen.getByText('TypeError')).toBeInTheDocument(); + expect(screen.getByText('Null pointer in auth flow')).toBeInTheDocument(); + expect(screen.getByText('auth/login')).toBeInTheDocument(); + expect(screen.getByText('3 issues')).toBeInTheDocument(); + }); + + it('does not render when issue is not in a supergroup', async () => { + const group = GroupFixture({id: '1'}); + MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/seer/supergroups/by-group/`, + body: {data: []}, + }); + + const {container} = render(, {organization}); + + // Wait for the request to resolve, then check it's still empty + await screen.findByText(() => false).catch(() => {}); + expect(container).toBeEmptyDOMElement(); + }); + + it('does not render without the feature flag', () => { + const group = GroupFixture({id: '1'}); + const orgWithoutFlag = OrganizationFixture({features: []}); + + const {container} = render(, { + organization: orgWithoutFlag, + }); + + expect(container).toBeEmptyDOMElement(); + }); + + it('opens the supergroup drawer on click', async () => { + 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], + created_at: '2024-01-01T00:00:00Z', + updated_at: '2024-01-01T00:00:00Z', + }, + ], + }, + }); + // The drawer's issue list fetches group details + MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/issues/`, + body: [], + }); + + render(, {organization}); + + const card = await screen.findByRole('button', {name: 'Supergroup details'}); + await userEvent.click(card); + + expect(await screen.findByText('Supergroups')).toBeInTheDocument(); + }); +}); diff --git a/static/app/views/issueDetails/streamline/sidebar/supergroupSection.tsx b/static/app/views/issueDetails/streamline/sidebar/supergroupSection.tsx new file mode 100644 index 00000000000000..1701ae8fb67348 --- /dev/null +++ b/static/app/views/issueDetails/streamline/sidebar/supergroupSection.tsx @@ -0,0 +1,110 @@ +import {Fragment} from 'react'; +import styled from '@emotion/styled'; + +import InteractionStateLayer from '@sentry/scraps/interactionStateLayer'; +import {Flex, Stack} from '@sentry/scraps/layout'; +import {Text} from '@sentry/scraps/text'; + +import {useDrawer} from 'sentry/components/globalDrawer'; +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/streamline/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('Supergroup details'), + drawerKey: 'supergroup-drawer', + } + ); + }; + + return ( +
+ {t('Supergroup')} + + + + + + {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; +`; From e1e3798c44f4ab6d14a4e1513837c726741ab9a4 Mon Sep 17 00:00:00 2001 From: Scott Cooper Date: Wed, 8 Apr 2026 15:05:46 -0700 Subject: [PATCH 2/2] simplify test to single render check --- .../sidebar/supergroupSection.spec.tsx | 75 ++----------------- 1 file changed, 5 insertions(+), 70 deletions(-) diff --git a/static/app/views/issueDetails/streamline/sidebar/supergroupSection.spec.tsx b/static/app/views/issueDetails/streamline/sidebar/supergroupSection.spec.tsx index 558a34c6b65814..a5a2838f9e11a2 100644 --- a/static/app/views/issueDetails/streamline/sidebar/supergroupSection.spec.tsx +++ b/static/app/views/issueDetails/streamline/sidebar/supergroupSection.spec.tsx @@ -1,18 +1,13 @@ import {GroupFixture} from 'sentry-fixture/group'; import {OrganizationFixture} from 'sentry-fixture/organization'; -import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; +import {render, screen} from 'sentry-test/reactTestingLibrary'; import {SupergroupSection} from 'sentry/views/issueDetails/streamline/sidebar/supergroupSection'; -const organization = OrganizationFixture({features: ['top-issues-ui']}); - describe('SupergroupSection', () => { - beforeEach(() => { - MockApiClient.clearMockResponses(); - }); - - it('renders supergroup card when issue belongs to a supergroup', async () => { + 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/`, @@ -23,7 +18,7 @@ describe('SupergroupSection', () => { title: 'Null pointer in auth flow', error_type: 'TypeError', code_area: 'auth/login', - summary: 'Root cause summary', + summary: '', group_ids: [1, 2, 3], created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', @@ -34,68 +29,8 @@ describe('SupergroupSection', () => { render(, {organization}); - expect(await screen.findByText('Supergroup')).toBeInTheDocument(); - expect(screen.getByText('TypeError')).toBeInTheDocument(); + expect(await screen.findByText('TypeError')).toBeInTheDocument(); expect(screen.getByText('Null pointer in auth flow')).toBeInTheDocument(); - expect(screen.getByText('auth/login')).toBeInTheDocument(); expect(screen.getByText('3 issues')).toBeInTheDocument(); }); - - it('does not render when issue is not in a supergroup', async () => { - const group = GroupFixture({id: '1'}); - MockApiClient.addMockResponse({ - url: `/organizations/${organization.slug}/seer/supergroups/by-group/`, - body: {data: []}, - }); - - const {container} = render(, {organization}); - - // Wait for the request to resolve, then check it's still empty - await screen.findByText(() => false).catch(() => {}); - expect(container).toBeEmptyDOMElement(); - }); - - it('does not render without the feature flag', () => { - const group = GroupFixture({id: '1'}); - const orgWithoutFlag = OrganizationFixture({features: []}); - - const {container} = render(, { - organization: orgWithoutFlag, - }); - - expect(container).toBeEmptyDOMElement(); - }); - - it('opens the supergroup drawer on click', async () => { - 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], - created_at: '2024-01-01T00:00:00Z', - updated_at: '2024-01-01T00:00:00Z', - }, - ], - }, - }); - // The drawer's issue list fetches group details - MockApiClient.addMockResponse({ - url: `/organizations/${organization.slug}/issues/`, - body: [], - }); - - render(, {organization}); - - const card = await screen.findByRole('button', {name: 'Supergroup details'}); - await userEvent.click(card); - - expect(await screen.findByText('Supergroups')).toBeInTheDocument(); - }); });