From b43e3e7cf42b1558fd14f2988a491bd92b90ab22 Mon Sep 17 00:00:00 2001 From: Michelle Zhang <56095982+michellewzhang@users.noreply.github.com> Date: Mon, 17 Mar 2025 16:01:24 -0700 Subject: [PATCH 01/23] feat(flags): add feature flag audit log table to tags dist drawer --- .../flagDetailsDrawerContent.tsx | 232 ++++++++++++++++++ .../groupFeatureFlagsDrawerContent.tsx | 5 +- .../groupTags/groupTagsDrawer.tsx | 142 ++++++++--- .../streamline/featureFlagUtils.tsx | 15 ++ .../organizationFeatureFlagsAuditLogTable.tsx | 22 +- 5 files changed, 365 insertions(+), 51 deletions(-) create mode 100644 static/app/views/issueDetails/groupFeatureFlags/flagDetailsDrawerContent.tsx diff --git a/static/app/views/issueDetails/groupFeatureFlags/flagDetailsDrawerContent.tsx b/static/app/views/issueDetails/groupFeatureFlags/flagDetailsDrawerContent.tsx new file mode 100644 index 00000000000000..34f7ae59f92b7c --- /dev/null +++ b/static/app/views/issueDetails/groupFeatureFlags/flagDetailsDrawerContent.tsx @@ -0,0 +1,232 @@ +import {Fragment, useMemo, useState} from 'react'; +import styled from '@emotion/styled'; + +import {DateTime} from 'sentry/components/dateTime'; +import {DropdownMenu} from 'sentry/components/dropdownMenu'; +import EmptyStateWarning from 'sentry/components/emptyStateWarning'; +import LoadingError from 'sentry/components/loadingError'; +import LoadingIndicator from 'sentry/components/loadingIndicator'; +import Pagination from 'sentry/components/pagination'; +import {IconArrow, IconEllipsis} from 'sentry/icons'; +import {t} from 'sentry/locale'; +import {space} from 'sentry/styles/space'; +import {decodeScalar} from 'sentry/utils/queryString'; +import useLocationQuery from 'sentry/utils/url/useLocationQuery'; +import useCopyToClipboard from 'sentry/utils/useCopyToClipboard'; +import {useNavigate} from 'sentry/utils/useNavigate'; +import useOrganization from 'sentry/utils/useOrganization'; +import {useParams} from 'sentry/utils/useParams'; +import { + getFlagActionLabel, + type RawFlag, +} from 'sentry/views/issueDetails/streamline/featureFlagUtils'; +import {useOrganizationFlagLog} from 'sentry/views/issueDetails/streamline/hooks/useOrganizationFlagLog'; + +export function FlagDetailsDrawerContent() { + const navigate = useNavigate(); + const organization = useOrganization(); + const {tagKey} = useParams<{tagKey: string}>(); + const sortArrow = <IconArrow color="gray300" size="xs" direction="down" />; + + const locationQuery = useLocationQuery({ + fields: { + cursor: decodeScalar, + end: decodeScalar, + flag: decodeScalar, + sort: (value: any) => decodeScalar(value, '-created_at'), + start: decodeScalar, + statsPeriod: decodeScalar, + utc: decodeScalar, + }, + }); + + const flagQuery = useMemo(() => { + const filteredFields = Object.fromEntries( + Object.entries(locationQuery).filter(([_key, val]) => val !== '') + ); + return { + ...filteredFields, + flag: tagKey, + per_page: 15, + queryReferrer: 'featureFlagDetailsDrawer', + }; + }, [locationQuery, tagKey]); + + const { + data: flagLog, + isPending, + isError, + getResponseHeader, + } = useOrganizationFlagLog({ + organization, + query: flagQuery, + }); + const pageLinks = getResponseHeader?.('Link') ?? null; + + if (isPending) { + return <LoadingIndicator />; + } + + if (isError) { + return ( + <LoadingError message={t('There was an error loading feature flag details.')} /> + ); + } + + if (!flagLog.data.length) { + return ( + <EmptyStateWarning withIcon={false} small> + {t('No audit log events were found for this flag.')} + </EmptyStateWarning> + ); + } + + return ( + <Fragment> + <Table> + <Header> + <ColumnTitle>{t('Provider')}</ColumnTitle> + <ColumnTitle>{t('Flag Name')}</ColumnTitle> + <ColumnTitle>{t('Action')}</ColumnTitle> + <ColumnTitle> + {sortArrow} + {t('Date')} + </ColumnTitle> + </Header> + <Body> + {flagLog.data.map((fv, i) => ( + <FlagDetailsRow key={`${fv.id}-${i}`} flagValue={fv} /> + ))} + </Body> + </Table> + <Pagination + pageLinks={pageLinks} + onCursor={(cursor, path, query) => + navigate({ + pathname: path, + query: { + ...query, + flagDrawerCursor: cursor, + }, + }) + } + size="xs" + /> + </Fragment> + ); +} + +function FlagDetailsRow({flagValue}: {flagValue: RawFlag}) { + return ( + <Row> + <LeftAlignedValue>{flagValue.provider}</LeftAlignedValue> + <LeftAlignedValue> + <code>{flagValue.flag}</code> + </LeftAlignedValue> + {getFlagActionLabel(flagValue.action)} + <DateTime date={flagValue.createdAt} year timeZone /> + <FlagValueActionsMenu flagValue={flagValue} /> + </Row> + ); +} + +function FlagValueActionsMenu({flagValue}: {flagValue: RawFlag}) { + const organization = useOrganization(); + const {onClick: handleCopy} = useCopyToClipboard({ + text: flagValue.flag, + }); + const key = flagValue.flag; + const [isVisible, setIsVisible] = useState(false); + + return ( + <DropdownMenu + size="xs" + className={isVisible ? '' : 'invisible'} + onOpenChange={isOpen => setIsVisible(isOpen)} + triggerProps={{ + 'aria-label': t('Tag Value Actions Menu'), + icon: <IconEllipsis />, + showChevron: false, + size: 'xs', + }} + items={[ + { + key: 'view-issues-true', + label: t('Search issues where this flag value is TRUE'), + to: { + pathname: `/organizations/${organization.slug}/issues/`, + query: {query: `flags["${key}"]:"true"`}, + }, + }, + { + key: 'view-issues-false', + label: t('Search issues where this flag value is FALSE'), + to: { + pathname: `/organizations/${organization.slug}/issues/`, + query: {query: `flags["${key}"]:"false"`}, + }, + }, + { + key: 'copy-value', + label: t('Copy tag value to clipboard'), + onAction: handleCopy, + }, + ]} + /> + ); +} + +const Table = styled('div')` + display: grid; + grid-template-columns: 0.4fr 0.7fr 0.3fr 0.5fr min-content; + column-gap: ${space(1)}; + row-gap: ${space(0.5)}; + margin: 0 -${space(1)}; + + @media (min-width: ${p => p.theme.breakpoints.xlarge}) { + column-gap: ${space(2)}; + } +`; + +const ColumnTitle = styled('div')` + white-space: nowrap; + color: ${p => p.theme.subText}; + font-weight: ${p => p.theme.fontWeightBold}; +`; + +const Body = styled('div')` + display: grid; + grid-column: 1 / -1; + grid-template-columns: subgrid; +`; + +const Header = styled(Body)` + display: grid; + grid-column: 1 / -1; + grid-template-columns: subgrid; + border-bottom: 1px solid ${p => p.theme.border}; + margin: 0 ${space(1)}; +`; + +const Row = styled(Body)` + &:nth-child(even) { + background: ${p => p.theme.backgroundSecondary}; + } + align-items: center; + border-radius: 4px; + padding: ${space(0.25)} ${space(1)}; + + .invisible { + visibility: hidden; + } + &:hover, + &:active { + .invisible { + visibility: visible; + } + } +`; + +const LeftAlignedValue = styled('div')` + text-align: left; +`; diff --git a/static/app/views/issueDetails/groupFeatureFlags/groupFeatureFlagsDrawerContent.tsx b/static/app/views/issueDetails/groupFeatureFlags/groupFeatureFlagsDrawerContent.tsx index 12b800d698a0b6..20d457d6b5d141 100644 --- a/static/app/views/issueDetails/groupFeatureFlags/groupFeatureFlagsDrawerContent.tsx +++ b/static/app/views/issueDetails/groupFeatureFlags/groupFeatureFlagsDrawerContent.tsx @@ -7,6 +7,7 @@ import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {Group} from 'sentry/types/group'; import useGroupFeatureFlags from 'sentry/views/issueDetails/groupFeatureFlags/useGroupFeatureFlags'; +import TagDetailsLink from 'sentry/views/issueDetails/groupTags/tagDetailsLink'; import {TagDistribution} from 'sentry/views/issueDetails/groupTags/tagDistribution'; import type {GroupTag} from 'sentry/views/issueDetails/groupTags/useGroupTags'; @@ -69,7 +70,9 @@ export default function GroupFeatureFlagsDrawerContent({ <Wrapper> <Container> {displayTags.map((tag, tagIdx) => ( - <TagDistribution tag={tag} key={tagIdx} /> + <TagDetailsLink tag={tag} groupId={group.id} key={tagIdx}> + <TagDistribution tag={tag} key={tagIdx} /> + </TagDetailsLink> ))} </Container> </Wrapper> diff --git a/static/app/views/issueDetails/groupTags/groupTagsDrawer.tsx b/static/app/views/issueDetails/groupTags/groupTagsDrawer.tsx index 3afa974a26d597..40dd920a07b562 100644 --- a/static/app/views/issueDetails/groupTags/groupTagsDrawer.tsx +++ b/static/app/views/issueDetails/groupTags/groupTagsDrawer.tsx @@ -31,11 +31,15 @@ import useOrganization from 'sentry/utils/useOrganization'; import {useParams} from 'sentry/utils/useParams'; import useProjects from 'sentry/utils/useProjects'; import useUrlParams from 'sentry/utils/useUrlParams'; +import {FlagDetailsDrawerContent} from 'sentry/views/issueDetails/groupFeatureFlags/flagDetailsDrawerContent'; import GroupFeatureFlagsDrawerContent from 'sentry/views/issueDetails/groupFeatureFlags/groupFeatureFlagsDrawerContent'; import {TagDetailsDrawerContent} from 'sentry/views/issueDetails/groupTags/tagDetailsDrawerContent'; import TagDetailsLink from 'sentry/views/issueDetails/groupTags/tagDetailsLink'; import {TagDistribution} from 'sentry/views/issueDetails/groupTags/tagDistribution'; -import {useGroupTags} from 'sentry/views/issueDetails/groupTags/useGroupTags'; +import { + type GroupTag, + useGroupTags, +} from 'sentry/views/issueDetails/groupTags/useGroupTags'; import {Tab, TabPaths} from 'sentry/views/issueDetails/types'; import {useGroupDetailsRoute} from 'sentry/views/issueDetails/useGroupDetailsRoute'; import {useEnvironmentsFromUrl} from 'sentry/views/issueDetails/utils'; @@ -63,6 +67,89 @@ function useDrawerTab({enabled}: {enabled: boolean}) { return {tab, setTab}; } +function getHeaderTitle( + tagKey: string | undefined, + tab: DrawerTab, + includeFeatureFlagsTab: boolean +) { + if (tagKey) { + return tab === TAGS_TAB + ? tct('Tag Details - [tagKey]', {tagKey}) + : tct('Feature Flag Details - [tagKey]', {tagKey}); + } + + return includeFeatureFlagsTab ? t('Tags & Feature Flags') : t('All Tags'); +} + +function DrawerContent({ + tagKey, + tab, + group, + environments, + search, + isPending, + isHighlightsPending, + isError, + refetch, + displayTags, +}: { + displayTags: GroupTag[]; + environments: string[]; + group: Group; + isError: boolean; + isHighlightsPending: boolean; + isPending: boolean; + refetch: () => void; + search: string; + tab: DrawerTab; + tagKey: string | undefined; +}) { + if (tagKey) { + return tab === TAGS_TAB ? ( + <TagDetailsDrawerContent group={group} /> + ) : ( + <FlagDetailsDrawerContent /> + ); + } + + if (tab === FEATURE_FLAGS_TAB) { + return ( + <GroupFeatureFlagsDrawerContent + group={group} + environments={environments} + search={search} + /> + ); + } + + if (isPending || isHighlightsPending) { + return <LoadingIndicator />; + } + + if (isError) { + return ( + <LoadingError + message={t('There was an error loading issue tags.')} + onRetry={refetch} + /> + ); + } + + return ( + <Wrapper> + <Container> + {displayTags.map(tag => ( + <div key={tag.name}> + <TagDetailsLink tag={tag} groupId={group.id}> + <TagDistribution tag={tag} /> + </TagDetailsLink> + </div> + ))} + </Container> + </Wrapper> + ); +} + export function GroupTagsDrawer({ group, includeFeatureFlagsTab, @@ -243,7 +330,14 @@ export function GroupTagsDrawer({ ? [ { label: t('All Feature Flags'), + to: tagKey + ? { + pathname: `${baseUrl}${TabPaths[Tab.TAGS]}`, + query: {tab: FEATURE_FLAGS_TAB, ...location.query}, + } + : undefined, }, + ...(tagKey ? [{label: tagKey}] : []), ] : []), ]} @@ -251,43 +345,23 @@ export function GroupTagsDrawer({ </EventDrawerHeader> <EventNavigator> <Header> - {tagKey - ? tct('Tag Details - [tagKey]', {tagKey}) - : includeFeatureFlagsTab - ? t('Tags & Feature Flags') - : t('All Tags')} + {getHeaderTitle(tagKey, tab as DrawerTab, includeFeatureFlagsTab)} </Header> {headerActions} </EventNavigator> <EventDrawerBody> - {tagKey ? ( - <TagDetailsDrawerContent group={group} /> - ) : tab === FEATURE_FLAGS_TAB ? ( - <GroupFeatureFlagsDrawerContent - group={group} - environments={environments} - search={search} - /> - ) : isPending || isHighlightsPending ? ( - <LoadingIndicator /> - ) : isError ? ( - <LoadingError - message={t('There was an error loading issue tags.')} - onRetry={refetch} - /> - ) : ( - <Wrapper> - <Container> - {displayTags.map(tag => ( - <div key={tag.name}> - <TagDetailsLink tag={tag} groupId={group.id}> - <TagDistribution tag={tag} /> - </TagDetailsLink> - </div> - ))} - </Container> - </Wrapper> - )} + <DrawerContent + tagKey={tagKey} + tab={tab as DrawerTab} + group={group} + environments={environments} + search={search} + isPending={isPending} + isHighlightsPending={isHighlightsPending} + isError={isError} + refetch={refetch} + displayTags={displayTags} + /> </EventDrawerBody> </EventDrawerContainer> ); diff --git a/static/app/views/issueDetails/streamline/featureFlagUtils.tsx b/static/app/views/issueDetails/streamline/featureFlagUtils.tsx index 255855e8dfebe0..b64c06eba0f9fa 100644 --- a/static/app/views/issueDetails/streamline/featureFlagUtils.tsx +++ b/static/app/views/issueDetails/streamline/featureFlagUtils.tsx @@ -1,3 +1,5 @@ +import {Tag} from 'sentry/components/core/badge/tag'; + export type RawFlag = { action: string; createdAt: string; @@ -38,3 +40,16 @@ export function hydrateToFlagSeries( }); return flagData; } + +export function getFlagActionLabel(action: string) { + const labelType = + action === 'created' ? 'info' : action === 'deleted' ? 'error' : undefined; + + const capitalized = action.toUpperCase(); + + return ( + <div style={{alignSelf: 'flex-start'}}> + <Tag type={labelType}>{capitalized}</Tag> + </div> + ); +} diff --git a/static/app/views/settings/featureFlags/changeTracking/organizationFeatureFlagsAuditLogTable.tsx b/static/app/views/settings/featureFlags/changeTracking/organizationFeatureFlagsAuditLogTable.tsx index 997d1a72c2a91d..14c5085e2ee735 100644 --- a/static/app/views/settings/featureFlags/changeTracking/organizationFeatureFlagsAuditLogTable.tsx +++ b/static/app/views/settings/featureFlags/changeTracking/organizationFeatureFlagsAuditLogTable.tsx @@ -1,6 +1,5 @@ import {Fragment, useMemo, useState} from 'react'; -import {Tag} from 'sentry/components/core/badge/tag'; import GridEditable, {type GridColumnOrder} from 'sentry/components/gridEditable'; import Pagination from 'sentry/components/pagination'; import useQueryBasedColumnResize from 'sentry/components/replays/useQueryBasedColumnResize'; @@ -12,7 +11,10 @@ import useLocationQuery from 'sentry/utils/url/useLocationQuery'; import {useLocation} from 'sentry/utils/useLocation'; import {useNavigate} from 'sentry/utils/useNavigate'; import useOrganization from 'sentry/utils/useOrganization'; -import type {RawFlag} from 'sentry/views/issueDetails/streamline/featureFlagUtils'; +import { + getFlagActionLabel, + type RawFlag, +} from 'sentry/views/issueDetails/streamline/featureFlagUtils'; import {useOrganizationFlagLog} from 'sentry/views/issueDetails/streamline/hooks/useOrganizationFlagLog'; import TextBlock from 'sentry/views/settings/components/text/textBlock'; @@ -22,7 +24,7 @@ const BASE_COLUMNS: Array<GridColumnOrder<ColumnKey>> = [ {key: 'provider', name: t('Provider')}, {key: 'flag', name: t('Feature Flag'), width: 600}, {key: 'action', name: t('Action')}, - {key: 'createdAt', name: t('Created')}, + {key: 'createdAt', name: t('Date')}, ]; export function OrganizationFeatureFlagsAuditLogTable({ @@ -83,19 +85,7 @@ export function OrganizationFeatureFlagsAuditLogTable({ case 'createdAt': return FIELD_FORMATTERS.date.renderFunc('createdAt', dataRow); case 'action': { - const type = - dataRow.action === 'created' - ? 'info' - : dataRow.action === 'deleted' - ? 'error' - : undefined; - const capitalized = - dataRow.action.charAt(0).toUpperCase() + dataRow.action.slice(1); - return ( - <div style={{alignSelf: 'flex-start'}}> - <Tag type={type}>{capitalized}</Tag> - </div> - ); + return getFlagActionLabel(dataRow.action); } default: return dataRow[column.key!]; From ec89ac0b6c8816b242460e49a669eb9855340cc3 Mon Sep 17 00:00:00 2001 From: Michelle Zhang <56095982+michellewzhang@users.noreply.github.com> Date: Mon, 17 Mar 2025 16:11:48 -0700 Subject: [PATCH 02/23] :white_check_mark: tests --- .../flagDetailsDrawerContent.spec.tsx | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 static/app/views/issueDetails/groupFeatureFlags/flagDetailsDrawerContent.spec.tsx diff --git a/static/app/views/issueDetails/groupFeatureFlags/flagDetailsDrawerContent.spec.tsx b/static/app/views/issueDetails/groupFeatureFlags/flagDetailsDrawerContent.spec.tsx new file mode 100644 index 00000000000000..f85749c00331cd --- /dev/null +++ b/static/app/views/issueDetails/groupFeatureFlags/flagDetailsDrawerContent.spec.tsx @@ -0,0 +1,116 @@ +import {GroupFixture} from 'sentry-fixture/group'; + +import {initializeOrg} from 'sentry-test/initializeOrg'; +import { + render, + screen, + userEvent, + waitForElementToBeRemoved, +} from 'sentry-test/reactTestingLibrary'; + +import {FlagDetailsDrawerContent} from './flagDetailsDrawerContent'; + +const mockNavigate = jest.fn(); +jest.mock('sentry/utils/useNavigate', () => ({ + useNavigate: () => mockNavigate, +})); + +const group = GroupFixture(); + +function init(tagKey: string) { + return initializeOrg({ + router: { + location: { + pathname: '/organizations/:orgId/issues/:groupId/', + query: {}, + }, + params: {orgId: 'org-slug', groupId: group.id, tagKey}, + }, + }); +} + +describe('FlagDetailsDrawerContent', () => { + beforeEach(() => { + MockApiClient.addMockResponse({ + url: '/organizations/org-slug/flags/logs/', + query: {flag: 'test-flag-key'}, + body: { + data: [ + { + id: '1', + provider: 'test-provider', + flag: 'test-flag-key', + action: 'updated', + createdAt: '2021-01-01T00:00:00Z', + }, + ], + }, + }); + }); + + afterEach(() => { + MockApiClient.clearMockResponses(); + }); + + it('renders a list of tag values', async () => { + const {router} = init('test-flag-key'); + render(<FlagDetailsDrawerContent />, {router}); + + await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator')); + + expect(screen.getByText('Provider')).toBeInTheDocument(); + expect(screen.getByText('Flag Name')).toBeInTheDocument(); + expect(screen.getByText('Date')).toBeInTheDocument(); + expect(screen.getByText('Action')).toBeInTheDocument(); + + // Displays dropdown menu + await userEvent.hover(screen.getByText('test-flag-key')); + expect( + screen.getByRole('button', {name: 'Tag Value Actions Menu'}) + ).toBeInTheDocument(); + await userEvent.click(screen.getByRole('button', {name: 'Tag Value Actions Menu'})); + expect( + screen.getByRole('menuitemradio', { + name: 'Search issues where this flag value is FALSE', + }) + ).toBeInTheDocument(); + expect( + screen.getByRole('menuitemradio', { + name: 'Search issues where this flag value is TRUE', + }) + ).toBeInTheDocument(); + expect( + await screen.findByRole('menuitemradio', {name: 'Copy tag value to clipboard'}) + ).toBeInTheDocument(); + }); + + it('renders an error message if flag values request fails', async () => { + const {router} = init('test-flag-key'); + + MockApiClient.addMockResponse({ + url: '/organizations/org-slug/flags/logs/', + statusCode: 500, + }); + + render(<FlagDetailsDrawerContent />, {router}); + + expect( + await screen.findByText('There was an error loading feature flag details.') + ).toBeInTheDocument(); + }); + + it('renders an empty state message if audit log values are empty', async () => { + const {router} = init('test-flag-key'); + + MockApiClient.addMockResponse({ + url: '/organizations/org-slug/flags/logs/', + body: {data: []}, + }); + + render(<FlagDetailsDrawerContent />, {router}); + + expect( + await screen.findByText('No audit log events were found for this flag.') + ).toBeInTheDocument(); + }); +}); From db8a3ac2673940b3a0f2d73ccfb827a568d41810 Mon Sep 17 00:00:00 2001 From: Michelle Zhang <56095982+michellewzhang@users.noreply.github.com> Date: Mon, 17 Mar 2025 16:21:31 -0700 Subject: [PATCH 03/23] :twisted_rightwards_arrows: merge fix --- .../issueDetails/groupTags/groupTagsDrawer.tsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/static/app/views/issueDetails/groupTags/groupTagsDrawer.tsx b/static/app/views/issueDetails/groupTags/groupTagsDrawer.tsx index 2333864b6e9bd7..44e25504da05c4 100644 --- a/static/app/views/issueDetails/groupTags/groupTagsDrawer.tsx +++ b/static/app/views/issueDetails/groupTags/groupTagsDrawer.tsx @@ -83,6 +83,7 @@ function getHeaderTitle( } function DrawerContent({ + data, tagKey, tab, group, @@ -94,6 +95,7 @@ function DrawerContent({ refetch, displayTags, }: { + data: GroupTag[]; displayTags: GroupTag[]; environments: string[]; group: Group; @@ -136,6 +138,16 @@ function DrawerContent({ ); } + if (displayTags.length === 0) { + return ( + <StyledEmptyStateWarning withIcon> + {data.length === 0 + ? t('No tags were found for this issue') + : t('No tags were found for this search')} + </StyledEmptyStateWarning> + ); + } + return ( <Wrapper> <Container> @@ -352,6 +364,7 @@ export function GroupTagsDrawer({ </EventNavigator> <EventDrawerBody> <DrawerContent + data={data} tagKey={tagKey} tab={tab as DrawerTab} group={group} From f8e8f6a4c980a073c1220591f26553ff1a8b0f3a Mon Sep 17 00:00:00 2001 From: Michelle Zhang <56095982+michellewzhang@users.noreply.github.com> Date: Mon, 17 Mar 2025 16:27:19 -0700 Subject: [PATCH 04/23] :pencil2: wording --- .../groupFeatureFlags/flagDetailsDrawerContent.spec.tsx | 8 +++++--- .../groupFeatureFlags/flagDetailsDrawerContent.tsx | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/static/app/views/issueDetails/groupFeatureFlags/flagDetailsDrawerContent.spec.tsx b/static/app/views/issueDetails/groupFeatureFlags/flagDetailsDrawerContent.spec.tsx index f85749c00331cd..f41b9986ed4b4d 100644 --- a/static/app/views/issueDetails/groupFeatureFlags/flagDetailsDrawerContent.spec.tsx +++ b/static/app/views/issueDetails/groupFeatureFlags/flagDetailsDrawerContent.spec.tsx @@ -66,9 +66,11 @@ describe('FlagDetailsDrawerContent', () => { // Displays dropdown menu await userEvent.hover(screen.getByText('test-flag-key')); expect( - screen.getByRole('button', {name: 'Tag Value Actions Menu'}) + screen.getByRole('button', {name: 'Flag Audit Log Actions Menu'}) ).toBeInTheDocument(); - await userEvent.click(screen.getByRole('button', {name: 'Tag Value Actions Menu'})); + await userEvent.click( + screen.getByRole('button', {name: 'Flag Audit Log Actions Menu'}) + ); expect( screen.getByRole('menuitemradio', { name: 'Search issues where this flag value is FALSE', @@ -80,7 +82,7 @@ describe('FlagDetailsDrawerContent', () => { }) ).toBeInTheDocument(); expect( - await screen.findByRole('menuitemradio', {name: 'Copy tag value to clipboard'}) + await screen.findByRole('menuitemradio', {name: 'Copy flag value to clipboard'}) ).toBeInTheDocument(); }); diff --git a/static/app/views/issueDetails/groupFeatureFlags/flagDetailsDrawerContent.tsx b/static/app/views/issueDetails/groupFeatureFlags/flagDetailsDrawerContent.tsx index 34f7ae59f92b7c..1ed611c711964d 100644 --- a/static/app/views/issueDetails/groupFeatureFlags/flagDetailsDrawerContent.tsx +++ b/static/app/views/issueDetails/groupFeatureFlags/flagDetailsDrawerContent.tsx @@ -144,7 +144,7 @@ function FlagValueActionsMenu({flagValue}: {flagValue: RawFlag}) { className={isVisible ? '' : 'invisible'} onOpenChange={isOpen => setIsVisible(isOpen)} triggerProps={{ - 'aria-label': t('Tag Value Actions Menu'), + 'aria-label': t('Flag Audit Log Actions Menu'), icon: <IconEllipsis />, showChevron: false, size: 'xs', @@ -168,7 +168,7 @@ function FlagValueActionsMenu({flagValue}: {flagValue: RawFlag}) { }, { key: 'copy-value', - label: t('Copy tag value to clipboard'), + label: t('Copy flag value to clipboard'), onAction: handleCopy, }, ]} From 31e352b260e148992173e8409e13205270c9d361 Mon Sep 17 00:00:00 2001 From: Michelle Zhang <56095982+michellewzhang@users.noreply.github.com> Date: Tue, 18 Mar 2025 11:42:33 -0700 Subject: [PATCH 05/23] :recycle: pr comments 1 --- .../flagDetailsDrawerContent.spec.tsx | 2 +- .../groupFeatureFlags/flagDetailsDrawerContent.tsx | 2 +- .../views/issueDetails/groupTags/groupTagsDrawer.tsx | 3 ++- .../issueDetails/streamline/featureFlagUtils.tsx | 12 +++++++++--- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/static/app/views/issueDetails/groupFeatureFlags/flagDetailsDrawerContent.spec.tsx b/static/app/views/issueDetails/groupFeatureFlags/flagDetailsDrawerContent.spec.tsx index f41b9986ed4b4d..4dbee606450f92 100644 --- a/static/app/views/issueDetails/groupFeatureFlags/flagDetailsDrawerContent.spec.tsx +++ b/static/app/views/issueDetails/groupFeatureFlags/flagDetailsDrawerContent.spec.tsx @@ -112,7 +112,7 @@ describe('FlagDetailsDrawerContent', () => { render(<FlagDetailsDrawerContent />, {router}); expect( - await screen.findByText('No audit log events were found for this flag.') + await screen.findByText('No audit logs were found for this feature flag.') ).toBeInTheDocument(); }); }); diff --git a/static/app/views/issueDetails/groupFeatureFlags/flagDetailsDrawerContent.tsx b/static/app/views/issueDetails/groupFeatureFlags/flagDetailsDrawerContent.tsx index 1ed611c711964d..2e9476156af9e4 100644 --- a/static/app/views/issueDetails/groupFeatureFlags/flagDetailsDrawerContent.tsx +++ b/static/app/views/issueDetails/groupFeatureFlags/flagDetailsDrawerContent.tsx @@ -76,7 +76,7 @@ export function FlagDetailsDrawerContent() { if (!flagLog.data.length) { return ( <EmptyStateWarning withIcon={false} small> - {t('No audit log events were found for this flag.')} + {t('No audit logs were found for this feature flag.')} </EmptyStateWarning> ); } diff --git a/static/app/views/issueDetails/groupTags/groupTagsDrawer.tsx b/static/app/views/issueDetails/groupTags/groupTagsDrawer.tsx index 44e25504da05c4..b2f687121ff3a7 100644 --- a/static/app/views/issueDetails/groupTags/groupTagsDrawer.tsx +++ b/static/app/views/issueDetails/groupTags/groupTagsDrawer.tsx @@ -173,6 +173,7 @@ export function GroupTagsDrawer({ const location = useLocation(); const organization = useOrganization(); const environments = useEnvironmentsFromUrl(); + // XXX: tagKey param is re-used for feature flag details drawer const {tagKey} = useParams<{tagKey: string}>(); const drawerRef = useRef<HTMLDivElement>(null); const {projects} = useProjects(); @@ -346,7 +347,7 @@ export function GroupTagsDrawer({ to: tagKey ? { pathname: `${baseUrl}${TabPaths[Tab.TAGS]}`, - query: {tab: FEATURE_FLAGS_TAB, ...location.query}, + query: {...location.query, tab: FEATURE_FLAGS_TAB}, } : undefined, }, diff --git a/static/app/views/issueDetails/streamline/featureFlagUtils.tsx b/static/app/views/issueDetails/streamline/featureFlagUtils.tsx index b64c06eba0f9fa..448bdb07e4ad4b 100644 --- a/static/app/views/issueDetails/streamline/featureFlagUtils.tsx +++ b/static/app/views/issueDetails/streamline/featureFlagUtils.tsx @@ -1,3 +1,5 @@ +import styled from '@emotion/styled'; + import {Tag} from 'sentry/components/core/badge/tag'; export type RawFlag = { @@ -45,11 +47,15 @@ export function getFlagActionLabel(action: string) { const labelType = action === 'created' ? 'info' : action === 'deleted' ? 'error' : undefined; - const capitalized = action.toUpperCase(); + const capitalized = action.charAt(0).toUpperCase() + action.slice(1); return ( - <div style={{alignSelf: 'flex-start'}}> + <ActionLabel> <Tag type={labelType}>{capitalized}</Tag> - </div> + </ActionLabel> ); } + +const ActionLabel = styled('div')` + align-self: flex-start; +`; From e28485f14ebe165a3e9df930cb7bffd5bbe92b9d Mon Sep 17 00:00:00 2001 From: Michelle Zhang <56095982+michellewzhang@users.noreply.github.com> Date: Tue, 18 Mar 2025 12:12:18 -0700 Subject: [PATCH 06/23] :sparkles: add back button for empty state --- .../flagDetailsDrawerContent.tsx | 36 ++++++++++++++++--- .../groupTags/groupTagsDrawer.tsx | 29 ++++++++------- 2 files changed, 48 insertions(+), 17 deletions(-) diff --git a/static/app/views/issueDetails/groupFeatureFlags/flagDetailsDrawerContent.tsx b/static/app/views/issueDetails/groupFeatureFlags/flagDetailsDrawerContent.tsx index 2e9476156af9e4..a364d77c50b2e6 100644 --- a/static/app/views/issueDetails/groupFeatureFlags/flagDetailsDrawerContent.tsx +++ b/static/app/views/issueDetails/groupFeatureFlags/flagDetailsDrawerContent.tsx @@ -1,6 +1,7 @@ import {Fragment, useMemo, useState} from 'react'; import styled from '@emotion/styled'; +import {LinkButton} from 'sentry/components/core/button'; import {DateTime} from 'sentry/components/dateTime'; import {DropdownMenu} from 'sentry/components/dropdownMenu'; import EmptyStateWarning from 'sentry/components/emptyStateWarning'; @@ -13,21 +14,27 @@ import {space} from 'sentry/styles/space'; import {decodeScalar} from 'sentry/utils/queryString'; import useLocationQuery from 'sentry/utils/url/useLocationQuery'; import useCopyToClipboard from 'sentry/utils/useCopyToClipboard'; +import {useLocation} from 'sentry/utils/useLocation'; import {useNavigate} from 'sentry/utils/useNavigate'; import useOrganization from 'sentry/utils/useOrganization'; import {useParams} from 'sentry/utils/useParams'; +import {DrawerTab} from 'sentry/views/issueDetails/groupTags/groupTagsDrawer'; import { getFlagActionLabel, type RawFlag, } from 'sentry/views/issueDetails/streamline/featureFlagUtils'; import {useOrganizationFlagLog} from 'sentry/views/issueDetails/streamline/hooks/useOrganizationFlagLog'; +import {Tab, TabPaths} from 'sentry/views/issueDetails/types'; +import {useGroupDetailsRoute} from 'sentry/views/issueDetails/useGroupDetailsRoute'; export function FlagDetailsDrawerContent() { const navigate = useNavigate(); const organization = useOrganization(); const {tagKey} = useParams<{tagKey: string}>(); - const sortArrow = <IconArrow color="gray300" size="xs" direction="down" />; + const {baseUrl} = useGroupDetailsRoute(); + const location = useLocation(); + const sortArrow = <IconArrow color="gray300" size="xs" direction="down" />; const locationQuery = useLocationQuery({ fields: { cursor: decodeScalar, @@ -75,9 +82,20 @@ export function FlagDetailsDrawerContent() { if (!flagLog.data.length) { return ( - <EmptyStateWarning withIcon={false} small> - {t('No audit logs were found for this feature flag.')} - </EmptyStateWarning> + <EmptyStateContainer> + <StyledEmptyStateWarning withIcon={false} small> + {t('No audit logs were found for this feature flag.')} + </StyledEmptyStateWarning> + <LinkButton + size="sm" + to={{ + pathname: `${baseUrl}${TabPaths[Tab.TAGS]}`, + query: {...location.query, tab: DrawerTab.FEATURE_FLAGS}, + }} + > + {t('See all flags')} + </LinkButton> + </EmptyStateContainer> ); } @@ -230,3 +248,13 @@ const Row = styled(Body)` const LeftAlignedValue = styled('div')` text-align: left; `; + +const EmptyStateContainer = styled('div')` + display: flex; + flex-direction: column; + align-items: center; +`; + +const StyledEmptyStateWarning = styled(EmptyStateWarning)` + padding: ${space(3)}; +`; diff --git a/static/app/views/issueDetails/groupTags/groupTagsDrawer.tsx b/static/app/views/issueDetails/groupTags/groupTagsDrawer.tsx index b2f687121ff3a7..8822bd66625ce4 100644 --- a/static/app/views/issueDetails/groupTags/groupTagsDrawer.tsx +++ b/static/app/views/issueDetails/groupTags/groupTagsDrawer.tsx @@ -46,14 +46,15 @@ import {useGroupDetailsRoute} from 'sentry/views/issueDetails/useGroupDetailsRou import {useEnvironmentsFromUrl} from 'sentry/views/issueDetails/utils'; // Used for `tab` state and URL param. -const TAGS_TAB = 'tags'; -const FEATURE_FLAGS_TAB = 'featureFlags'; -type DrawerTab = 'tags' | 'featureFlags'; +export enum DrawerTab { + TAGS = 'tags', + FEATURE_FLAGS = 'featureFlags', +} function useDrawerTab({enabled}: {enabled: boolean}) { const {getParamValue: getTabParam, setParamValue: setTabParam} = useUrlParams('tab'); const [tab, setTab] = useState<DrawerTab>( - getTabParam() === FEATURE_FLAGS_TAB ? FEATURE_FLAGS_TAB : TAGS_TAB + getTabParam() === DrawerTab.FEATURE_FLAGS ? DrawerTab.FEATURE_FLAGS : DrawerTab.TAGS ); useEffect(() => { @@ -63,7 +64,7 @@ function useDrawerTab({enabled}: {enabled: boolean}) { }, [tab, setTabParam, enabled]); if (!enabled) { - return {tab: TAGS_TAB, setTab: (_tab: string) => {}}; + return {tab: DrawerTab.TAGS, setTab: (_tab: string) => {}}; } return {tab, setTab}; } @@ -74,7 +75,7 @@ function getHeaderTitle( includeFeatureFlagsTab: boolean ) { if (tagKey) { - return tab === TAGS_TAB + return tab === DrawerTab.TAGS ? tct('Tag Details - [tagKey]', {tagKey}) : tct('Feature Flag Details - [tagKey]', {tagKey}); } @@ -108,14 +109,14 @@ function DrawerContent({ tagKey: string | undefined; }) { if (tagKey) { - return tab === TAGS_TAB ? ( + return tab === DrawerTab.TAGS ? ( <TagDetailsDrawerContent group={group} /> ) : ( <FlagDetailsDrawerContent /> ); } - if (tab === FEATURE_FLAGS_TAB) { + if (tab === DrawerTab.FEATURE_FLAGS) { return ( <GroupFeatureFlagsDrawerContent group={group} @@ -305,8 +306,10 @@ export function GroupTagsDrawer({ setSearch(''); }} > - <SegmentedControl.Item key={TAGS_TAB}>{t('All Tags')}</SegmentedControl.Item> - <SegmentedControl.Item key={FEATURE_FLAGS_TAB}> + <SegmentedControl.Item key={DrawerTab.TAGS}> + {t('All Tags')} + </SegmentedControl.Item> + <SegmentedControl.Item key={DrawerTab.FEATURE_FLAGS}> {t('All Feature Flags')} </SegmentedControl.Item> </SegmentedControl> @@ -327,7 +330,7 @@ export function GroupTagsDrawer({ </CrumbContainer> ), }, - ...(tab === TAGS_TAB + ...(tab === DrawerTab.TAGS ? [ { label: t('All Tags'), @@ -340,14 +343,14 @@ export function GroupTagsDrawer({ }, ...(tagKey ? [{label: tagKey}] : []), ] - : tab === FEATURE_FLAGS_TAB + : tab === DrawerTab.FEATURE_FLAGS ? [ { label: t('All Feature Flags'), to: tagKey ? { pathname: `${baseUrl}${TabPaths[Tab.TAGS]}`, - query: {...location.query, tab: FEATURE_FLAGS_TAB}, + query: {...location.query, tab: DrawerTab.FEATURE_FLAGS}, } : undefined, }, From 5a9488f96bc451e5e277373ba0a6358bf53ebb3f Mon Sep 17 00:00:00 2001 From: Michelle Zhang <56095982+michellewzhang@users.noreply.github.com> Date: Tue, 18 Mar 2025 13:24:29 -0700 Subject: [PATCH 07/23] :art: simplify props --- .../groupTags/groupTagsDrawer.tsx | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/static/app/views/issueDetails/groupTags/groupTagsDrawer.tsx b/static/app/views/issueDetails/groupTags/groupTagsDrawer.tsx index 8822bd66625ce4..937d35d927bcdd 100644 --- a/static/app/views/issueDetails/groupTags/groupTagsDrawer.tsx +++ b/static/app/views/issueDetails/groupTags/groupTagsDrawer.tsx @@ -84,25 +84,23 @@ function getHeaderTitle( } function DrawerContent({ - data, - tagKey, - tab, + displayTags, group, environments, search, - isPending, - isHighlightsPending, + isLoading, isError, refetch, - displayTags, + tab, + tagKey, + data, }: { data: GroupTag[]; displayTags: GroupTag[]; environments: string[]; group: Group; isError: boolean; - isHighlightsPending: boolean; - isPending: boolean; + isLoading: boolean; refetch: () => void; search: string; tab: DrawerTab; @@ -126,7 +124,7 @@ function DrawerContent({ ); } - if (isPending || isHighlightsPending) { + if (isLoading) { return <LoadingIndicator />; } @@ -368,17 +366,16 @@ export function GroupTagsDrawer({ </EventNavigator> <EventDrawerBody> <DrawerContent - data={data} - tagKey={tagKey} - tab={tab as DrawerTab} + displayTags={displayTags} group={group} environments={environments} search={search} - isPending={isPending} - isHighlightsPending={isHighlightsPending} + isLoading={isPending || isHighlightsPending} isError={isError} refetch={refetch} - displayTags={displayTags} + tab={tab as DrawerTab} + tagKey={tagKey} + data={data} /> </EventDrawerBody> </EventDrawerContainer> From fe137fd88952e84ab32f8309992229c0626ab825 Mon Sep 17 00:00:00 2001 From: Michelle Zhang <56095982+michellewzhang@users.noreply.github.com> Date: Tue, 18 Mar 2025 13:43:15 -0700 Subject: [PATCH 08/23] :lipstick: style comments --- .../flagDetailsDrawerContent.tsx | 3 --- .../groupFeatureFlagsDrawerContent.tsx | 17 +++++------- .../groupTags/groupTagsDrawer.tsx | 26 +++++++------------ .../groupTags/tagDetailsDrawerContent.tsx | 3 --- 4 files changed, 16 insertions(+), 33 deletions(-) diff --git a/static/app/views/issueDetails/groupFeatureFlags/flagDetailsDrawerContent.tsx b/static/app/views/issueDetails/groupFeatureFlags/flagDetailsDrawerContent.tsx index a364d77c50b2e6..1eedd72d6cfdcf 100644 --- a/static/app/views/issueDetails/groupFeatureFlags/flagDetailsDrawerContent.tsx +++ b/static/app/views/issueDetails/groupFeatureFlags/flagDetailsDrawerContent.tsx @@ -219,9 +219,6 @@ const Body = styled('div')` `; const Header = styled(Body)` - display: grid; - grid-column: 1 / -1; - grid-template-columns: subgrid; border-bottom: 1px solid ${p => p.theme.border}; margin: 0 ${space(1)}; `; diff --git a/static/app/views/issueDetails/groupFeatureFlags/groupFeatureFlagsDrawerContent.tsx b/static/app/views/issueDetails/groupFeatureFlags/groupFeatureFlagsDrawerContent.tsx index 592764733b5dc1..a2a6e02f32bd13 100644 --- a/static/app/views/issueDetails/groupFeatureFlags/groupFeatureFlagsDrawerContent.tsx +++ b/static/app/views/issueDetails/groupFeatureFlags/groupFeatureFlagsDrawerContent.tsx @@ -8,7 +8,6 @@ import useGroupFeatureFlags from 'sentry/views/issueDetails/groupFeatureFlags/us import { Container, StyledEmptyStateWarning, - Wrapper, } from 'sentry/views/issueDetails/groupTags/groupTagsDrawer'; import TagDetailsLink from 'sentry/views/issueDetails/groupTags/tagDetailsLink'; import {TagDistribution} from 'sentry/views/issueDetails/groupTags/tagDistribution'; @@ -76,14 +75,12 @@ export default function GroupFeatureFlagsDrawerContent({ : t('No feature flags were found for this search')} </StyledEmptyStateWarning> ) : ( - <Wrapper> - <Container> - {displayTags.map((tag, tagIdx) => ( - <TagDetailsLink tag={tag} groupId={group.id} key={tagIdx}> - <TagDistribution tag={tag} key={tagIdx} /> - </TagDetailsLink> - ))} - </Container> - </Wrapper> + <Container> + {displayTags.map((tag, tagIdx) => ( + <TagDetailsLink tag={tag} groupId={group.id} key={tagIdx}> + <TagDistribution tag={tag} key={tagIdx} /> + </TagDetailsLink> + ))} + </Container> ); } diff --git a/static/app/views/issueDetails/groupTags/groupTagsDrawer.tsx b/static/app/views/issueDetails/groupTags/groupTagsDrawer.tsx index 937d35d927bcdd..61905106a773f4 100644 --- a/static/app/views/issueDetails/groupTags/groupTagsDrawer.tsx +++ b/static/app/views/issueDetails/groupTags/groupTagsDrawer.tsx @@ -148,17 +148,15 @@ function DrawerContent({ } return ( - <Wrapper> - <Container> - {displayTags.map(tag => ( - <div key={tag.name}> - <TagDetailsLink tag={tag} groupId={group.id}> - <TagDistribution tag={tag} /> - </TagDetailsLink> - </div> - ))} - </Container> - </Wrapper> + <Container> + {displayTags.map(tag => ( + <div key={tag.name}> + <TagDetailsLink tag={tag} groupId={group.id}> + <TagDistribution tag={tag} /> + </TagDetailsLink> + </div> + ))} + </Container> ); } @@ -382,12 +380,6 @@ export function GroupTagsDrawer({ ); } -export const Wrapper = styled('div')` - display: flex; - flex-direction: column; - gap: ${space(2)}; -`; - export const Container = styled('div')` display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); diff --git a/static/app/views/issueDetails/groupTags/tagDetailsDrawerContent.tsx b/static/app/views/issueDetails/groupTags/tagDetailsDrawerContent.tsx index 4018899d7a766a..c7656894fed00c 100644 --- a/static/app/views/issueDetails/groupTags/tagDetailsDrawerContent.tsx +++ b/static/app/views/issueDetails/groupTags/tagDetailsDrawerContent.tsx @@ -345,9 +345,6 @@ const Body = styled('div')` `; const Header = styled(Body)` - display: grid; - grid-column: 1 / -1; - grid-template-columns: subgrid; border-bottom: 1px solid ${p => p.theme.border}; margin: 0 ${space(1)}; `; From 50dc510bd41bf20df8a3d0e56277149e5836903e Mon Sep 17 00:00:00 2001 From: Michelle Zhang <56095982+michellewzhang@users.noreply.github.com> Date: Tue, 18 Mar 2025 13:52:50 -0700 Subject: [PATCH 09/23] :recycle: simplify tags path --- .../groupTags/groupTagsDrawer.tsx | 47 ++++++++----------- 1 file changed, 20 insertions(+), 27 deletions(-) diff --git a/static/app/views/issueDetails/groupTags/groupTagsDrawer.tsx b/static/app/views/issueDetails/groupTags/groupTagsDrawer.tsx index 61905106a773f4..fb1588399f701c 100644 --- a/static/app/views/issueDetails/groupTags/groupTagsDrawer.tsx +++ b/static/app/views/issueDetails/groupTags/groupTagsDrawer.tsx @@ -326,33 +326,26 @@ export function GroupTagsDrawer({ </CrumbContainer> ), }, - ...(tab === DrawerTab.TAGS - ? [ - { - label: t('All Tags'), - to: tagKey - ? { - pathname: `${baseUrl}${TabPaths[Tab.TAGS]}`, - query: location.query, - } - : undefined, - }, - ...(tagKey ? [{label: tagKey}] : []), - ] - : tab === DrawerTab.FEATURE_FLAGS - ? [ - { - label: t('All Feature Flags'), - to: tagKey - ? { - pathname: `${baseUrl}${TabPaths[Tab.TAGS]}`, - query: {...location.query, tab: DrawerTab.FEATURE_FLAGS}, - } - : undefined, - }, - ...(tagKey ? [{label: tagKey}] : []), - ] - : []), + tab === DrawerTab.TAGS + ? { + label: t('All Tags'), + to: tagKey + ? { + pathname: `${baseUrl}${TabPaths[Tab.TAGS]}`, + query: {...location.query, tab: DrawerTab.TAGS}, + } + : undefined, + } + : { + label: t('All Feature Flags'), + to: tagKey + ? { + pathname: `${baseUrl}${TabPaths[Tab.TAGS]}`, + query: {...location.query, tab: DrawerTab.FEATURE_FLAGS}, + } + : undefined, + }, + ...(tagKey ? [{label: tagKey}] : []), ]} /> </EventDrawerHeader> From e73a5c4da6b711fd159d98435a424532b8ec985d Mon Sep 17 00:00:00 2001 From: Michelle Zhang <56095982+michellewzhang@users.noreply.github.com> Date: Tue, 18 Mar 2025 13:59:48 -0700 Subject: [PATCH 10/23] :fire: rm location params --- .../flagDetailsDrawerContent.tsx | 24 ++++--------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/static/app/views/issueDetails/groupFeatureFlags/flagDetailsDrawerContent.tsx b/static/app/views/issueDetails/groupFeatureFlags/flagDetailsDrawerContent.tsx index 1eedd72d6cfdcf..fcb7688cbb8a6c 100644 --- a/static/app/views/issueDetails/groupFeatureFlags/flagDetailsDrawerContent.tsx +++ b/static/app/views/issueDetails/groupFeatureFlags/flagDetailsDrawerContent.tsx @@ -11,8 +11,6 @@ import Pagination from 'sentry/components/pagination'; import {IconArrow, IconEllipsis} from 'sentry/icons'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; -import {decodeScalar} from 'sentry/utils/queryString'; -import useLocationQuery from 'sentry/utils/url/useLocationQuery'; import useCopyToClipboard from 'sentry/utils/useCopyToClipboard'; import {useLocation} from 'sentry/utils/useLocation'; import {useNavigate} from 'sentry/utils/useNavigate'; @@ -35,29 +33,17 @@ export function FlagDetailsDrawerContent() { const location = useLocation(); const sortArrow = <IconArrow color="gray300" size="xs" direction="down" />; - const locationQuery = useLocationQuery({ - fields: { - cursor: decodeScalar, - end: decodeScalar, - flag: decodeScalar, - sort: (value: any) => decodeScalar(value, '-created_at'), - start: decodeScalar, - statsPeriod: decodeScalar, - utc: decodeScalar, - }, - }); const flagQuery = useMemo(() => { - const filteredFields = Object.fromEntries( - Object.entries(locationQuery).filter(([_key, val]) => val !== '') - ); return { - ...filteredFields, flag: tagKey, - per_page: 15, + per_page: 50, queryReferrer: 'featureFlagDetailsDrawer', + statsPeriod: '90d', + sort: '-created_at', + cursor: location.query.flagDrawerCursor, }; - }, [locationQuery, tagKey]); + }, [tagKey, location.query.flagDrawerCursor]); const { data: flagLog, From 2ab9e7a5efe81c5241441fea41c96a526ba3e3b7 Mon Sep 17 00:00:00 2001 From: Michelle Zhang <56095982+michellewzhang@users.noreply.github.com> Date: Tue, 18 Mar 2025 14:17:55 -0700 Subject: [PATCH 11/23] :art: clear cursor on drawer close & new link component --- .../groupFeatureFlags/flagDetailsLink.tsx | 36 +++++++++++++++++++ .../groupFeatureFlagsDrawerContent.tsx | 6 ++-- .../groupTags/useGroupTagsDrawer.tsx | 1 + 3 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 static/app/views/issueDetails/groupFeatureFlags/flagDetailsLink.tsx diff --git a/static/app/views/issueDetails/groupFeatureFlags/flagDetailsLink.tsx b/static/app/views/issueDetails/groupFeatureFlags/flagDetailsLink.tsx new file mode 100644 index 00000000000000..bbfe36e5083555 --- /dev/null +++ b/static/app/views/issueDetails/groupFeatureFlags/flagDetailsLink.tsx @@ -0,0 +1,36 @@ +import styled from '@emotion/styled'; + +import Link from 'sentry/components/links/link'; +import {useLocation} from 'sentry/utils/useLocation'; +import type {GroupTag} from 'sentry/views/issueDetails/groupTags/useGroupTags'; + +export default function FlagDetailsLink({ + tag, + children, +}: { + children: React.ReactNode; + groupId: string; + tag: GroupTag; +}) { + const location = useLocation(); + + return ( + <StyledLink + to={{ + pathname: `${location.pathname}${tag.key}/`, + query: location.query, + }} + > + {children} + </StyledLink> + ); +} + +const StyledLink = styled(Link)` + border-radius: ${p => p.theme.borderRadius}; + display: block; + + &:hover h5 { + text-decoration: underline; + } +`; diff --git a/static/app/views/issueDetails/groupFeatureFlags/groupFeatureFlagsDrawerContent.tsx b/static/app/views/issueDetails/groupFeatureFlags/groupFeatureFlagsDrawerContent.tsx index a2a6e02f32bd13..8b46043f47cf2d 100644 --- a/static/app/views/issueDetails/groupFeatureFlags/groupFeatureFlagsDrawerContent.tsx +++ b/static/app/views/issueDetails/groupFeatureFlags/groupFeatureFlagsDrawerContent.tsx @@ -4,12 +4,12 @@ import LoadingError from 'sentry/components/loadingError'; import LoadingIndicator from 'sentry/components/loadingIndicator'; import {t} from 'sentry/locale'; import type {Group} from 'sentry/types/group'; +import FlagDetailsLink from 'sentry/views/issueDetails/groupFeatureFlags/flagDetailsLink'; import useGroupFeatureFlags from 'sentry/views/issueDetails/groupFeatureFlags/useGroupFeatureFlags'; import { Container, StyledEmptyStateWarning, } from 'sentry/views/issueDetails/groupTags/groupTagsDrawer'; -import TagDetailsLink from 'sentry/views/issueDetails/groupTags/tagDetailsLink'; import {TagDistribution} from 'sentry/views/issueDetails/groupTags/tagDistribution'; import type {GroupTag} from 'sentry/views/issueDetails/groupTags/useGroupTags'; @@ -77,9 +77,9 @@ export default function GroupFeatureFlagsDrawerContent({ ) : ( <Container> {displayTags.map((tag, tagIdx) => ( - <TagDetailsLink tag={tag} groupId={group.id} key={tagIdx}> + <FlagDetailsLink tag={tag} groupId={group.id} key={tagIdx}> <TagDistribution tag={tag} key={tagIdx} /> - </TagDetailsLink> + </FlagDetailsLink> ))} </Container> ); diff --git a/static/app/views/issueDetails/groupTags/useGroupTagsDrawer.tsx b/static/app/views/issueDetails/groupTags/useGroupTagsDrawer.tsx index ec0d31a36fe199..6b1b9a516b308a 100644 --- a/static/app/views/issueDetails/groupTags/useGroupTagsDrawer.tsx +++ b/static/app/views/issueDetails/groupTags/useGroupTagsDrawer.tsx @@ -35,6 +35,7 @@ export function useGroupTagsDrawer({ ...location.query, tagDrawerSort: undefined, tab: undefined, + flagDrawerCursor: undefined, }, }, {replace: true} From 551bd35e809593392e7b03a4947477d903e67cf9 Mon Sep 17 00:00:00 2001 From: Michelle Zhang <56095982+michellewzhang@users.noreply.github.com> Date: Tue, 18 Mar 2025 14:25:35 -0700 Subject: [PATCH 12/23] :bug: ugh copy and paste --- .../views/issueDetails/groupFeatureFlags/flagDetailsLink.tsx | 1 - .../groupFeatureFlags/groupFeatureFlagsDrawerContent.tsx | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/static/app/views/issueDetails/groupFeatureFlags/flagDetailsLink.tsx b/static/app/views/issueDetails/groupFeatureFlags/flagDetailsLink.tsx index bbfe36e5083555..9308b00339d8f2 100644 --- a/static/app/views/issueDetails/groupFeatureFlags/flagDetailsLink.tsx +++ b/static/app/views/issueDetails/groupFeatureFlags/flagDetailsLink.tsx @@ -9,7 +9,6 @@ export default function FlagDetailsLink({ children, }: { children: React.ReactNode; - groupId: string; tag: GroupTag; }) { const location = useLocation(); diff --git a/static/app/views/issueDetails/groupFeatureFlags/groupFeatureFlagsDrawerContent.tsx b/static/app/views/issueDetails/groupFeatureFlags/groupFeatureFlagsDrawerContent.tsx index 8b46043f47cf2d..c728a6e93aa7a8 100644 --- a/static/app/views/issueDetails/groupFeatureFlags/groupFeatureFlagsDrawerContent.tsx +++ b/static/app/views/issueDetails/groupFeatureFlags/groupFeatureFlagsDrawerContent.tsx @@ -77,7 +77,7 @@ export default function GroupFeatureFlagsDrawerContent({ ) : ( <Container> {displayTags.map((tag, tagIdx) => ( - <FlagDetailsLink tag={tag} groupId={group.id} key={tagIdx}> + <FlagDetailsLink tag={tag} key={tagIdx}> <TagDistribution tag={tag} key={tagIdx} /> </FlagDetailsLink> ))} From 24d93432ae3add5935f4b212b06a34b664232006 Mon Sep 17 00:00:00 2001 From: Michelle Zhang <56095982+michellewzhang@users.noreply.github.com> Date: Tue, 18 Mar 2025 14:34:59 -0700 Subject: [PATCH 13/23] :bug: add div --- .../groupFeatureFlagsDrawerContent.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/static/app/views/issueDetails/groupFeatureFlags/groupFeatureFlagsDrawerContent.tsx b/static/app/views/issueDetails/groupFeatureFlags/groupFeatureFlagsDrawerContent.tsx index c728a6e93aa7a8..06095ce9212af4 100644 --- a/static/app/views/issueDetails/groupFeatureFlags/groupFeatureFlagsDrawerContent.tsx +++ b/static/app/views/issueDetails/groupFeatureFlags/groupFeatureFlagsDrawerContent.tsx @@ -76,10 +76,12 @@ export default function GroupFeatureFlagsDrawerContent({ </StyledEmptyStateWarning> ) : ( <Container> - {displayTags.map((tag, tagIdx) => ( - <FlagDetailsLink tag={tag} key={tagIdx}> - <TagDistribution tag={tag} key={tagIdx} /> - </FlagDetailsLink> + {displayTags.map(tag => ( + <div key={tag.name}> + <FlagDetailsLink tag={tag} key={tag.name}> + <TagDistribution tag={tag} key={tag.name} /> + </FlagDetailsLink> + </div> ))} </Container> ); From e3c6f06253bb203967f707a535435df07dba93fa Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Tue, 18 Mar 2025 15:24:07 -0700 Subject: [PATCH 14/23] feat(flags): add CTA to flag drawer --- .../featureFlags/eventFeatureFlagList.tsx | 9 +-- .../components/events/featureFlags/utils.tsx | 11 +++ .../components/globalDrawer/components.tsx | 2 +- .../groupFeatureFlags/flagDrawerCTA.tsx | 78 +++++++++++++++++++ .../groupFeatureFlagsDrawerContent.tsx | 11 +++ 5 files changed, 104 insertions(+), 7 deletions(-) create mode 100644 static/app/views/issueDetails/groupFeatureFlags/flagDrawerCTA.tsx diff --git a/static/app/components/events/featureFlags/eventFeatureFlagList.tsx b/static/app/components/events/featureFlags/eventFeatureFlagList.tsx index 0faebe53f13b7d..7017a251951431 100644 --- a/static/app/components/events/featureFlags/eventFeatureFlagList.tsx +++ b/static/app/components/events/featureFlags/eventFeatureFlagList.tsx @@ -14,12 +14,12 @@ import FeatureFlagSort from 'sentry/components/events/featureFlags/featureFlagSo import { FlagControlOptions, OrderBy, + shouldShowFeatureFlagCTA, SortBy, sortedFlags, } from 'sentry/components/events/featureFlags/utils'; import useDrawer from 'sentry/components/globalDrawer'; import KeyValueData from 'sentry/components/keyValueData'; -import {featureFlagOnboardingPlatforms} from 'sentry/data/platformCategories'; import {IconMegaphone, IconSearch} from 'sentry/icons'; import {t, tn} from 'sentry/locale'; import type {Event, FeatureFlag} from 'sentry/types/event'; @@ -219,11 +219,8 @@ export function EventFeatureFlagList({ } // contexts.flags is not set and project has not ingested flags - if (!hasFlagContext && !project.hasFlags) { - const showCTA = - featureFlagOnboardingPlatforms.includes(project.platform ?? 'other') && - organization.features.includes('feature-flag-cta'); - return showCTA ? <FeatureFlagInlineCTA projectId={event.projectID} /> : null; + if (!hasFlagContext && shouldShowFeatureFlagCTA(organization, project)) { + return <FeatureFlagInlineCTA projectId={event.projectID} />; } const actions = ( diff --git a/static/app/components/events/featureFlags/utils.tsx b/static/app/components/events/featureFlags/utils.tsx index c32510a7ebd609..2e2a2451d61aca 100644 --- a/static/app/components/events/featureFlags/utils.tsx +++ b/static/app/components/events/featureFlags/utils.tsx @@ -1,5 +1,8 @@ import type {KeyValueDataContentProps} from 'sentry/components/keyValueData'; +import {featureFlagOnboardingPlatforms} from 'sentry/data/platformCategories'; import {t} from 'sentry/locale'; +import type {Organization} from 'sentry/types/organization'; +import type {Project} from 'sentry/types/project'; export enum OrderBy { NEWEST = 'newest', @@ -139,3 +142,11 @@ export const PROVIDER_TO_SETUP_WEBHOOK_URL: Record<WebhookProviderEnum, string> [WebhookProviderEnum.UNLEASH]: 'https://docs.sentry.io/organization/integrations/feature-flag/unleash/#set-up-change-tracking', }; + +export function shouldShowFeatureFlagCTA(organization: Organization, project: Project) { + return ( + !project.hasFlags && + featureFlagOnboardingPlatforms.includes(project.platform ?? 'other') && + organization.features.includes('feature-flag-cta') + ); +} diff --git a/static/app/components/globalDrawer/components.tsx b/static/app/components/globalDrawer/components.tsx index b2525ce2c049cc..8890e308570dcd 100644 --- a/static/app/components/globalDrawer/components.tsx +++ b/static/app/components/globalDrawer/components.tsx @@ -19,7 +19,7 @@ const DrawerContentContext = createContext<DrawerContentContextType>({ ariaLabel: 'slide out drawer', }); -function useDrawerContentContext() { +export function useDrawerContentContext() { return useContext(DrawerContentContext); } diff --git a/static/app/views/issueDetails/groupFeatureFlags/flagDrawerCTA.tsx b/static/app/views/issueDetails/groupFeatureFlags/flagDrawerCTA.tsx new file mode 100644 index 00000000000000..c43e5cc0571278 --- /dev/null +++ b/static/app/views/issueDetails/groupFeatureFlags/flagDrawerCTA.tsx @@ -0,0 +1,78 @@ +import styled from '@emotion/styled'; + +import {Button, LinkButton} from 'sentry/components/core/button'; +import {useFeatureFlagOnboarding} from 'sentry/components/events/featureFlags/useFeatureFlagOnboarding'; +import {useDrawerContentContext} from 'sentry/components/globalDrawer/components'; +import {t} from 'sentry/locale'; +import {space} from 'sentry/styles/space'; +import {trackAnalytics} from 'sentry/utils/analytics'; +import useOrganization from 'sentry/utils/useOrganization'; + +export default function FlagDrawerCTA() { + const organization = useOrganization(); + const {activateSidebar} = useFeatureFlagOnboarding(); + const {onClose: closeDrawer} = useDrawerContentContext(); + + function handleSetupButtonClick(e: any) { + trackAnalytics('flags.setup_modal_opened', {organization}); + trackAnalytics('flags.cta_setup_button_clicked', {organization}); + closeDrawer?.(); + setTimeout(() => { + // Wait for global drawer state to update + activateSidebar(e); + }, 100); + } + + return ( + <BannerWrapper> + <BannerTitle>{t('Set Up Feature Flags')}</BannerTitle> + <BannerDescription> + {t( + 'Want to know which feature flags were associated with this error? Set up your feature flag integration.' + )} + </BannerDescription> + <ActionButton> + <Button onClick={handleSetupButtonClick} priority="primary"> + {t('Set Up Now')} + </Button> + <LinkButton + priority="default" + href="https://docs.sentry.io/product/explore/feature-flags/" + external + > + {t('Read More')} + </LinkButton> + </ActionButton> + </BannerWrapper> + ); +} + +const BannerTitle = styled('div')` + font-size: ${p => p.theme.fontSizeExtraLarge}; + margin-bottom: ${space(1)}; + font-weight: ${p => p.theme.fontWeightBold}; +`; + +const BannerDescription = styled('div')` + margin-bottom: ${space(1.5)}; + max-width: 340px; +`; + +const ActionButton = styled('div')` + display: flex; + gap: ${space(1)}; +`; + +const BannerWrapper = styled('div')` + position: relative; + border: 1px solid ${p => p.theme.border}; + border-radius: ${p => p.theme.borderRadius}; + padding: ${space(2)}; + margin: ${space(1)} 0; + background: linear-gradient( + 90deg, + ${p => p.theme.backgroundSecondary}00 0%, + ${p => p.theme.backgroundSecondary}FF 70%, + ${p => p.theme.backgroundSecondary}FF 100% + ); +`; diff --git a/static/app/views/issueDetails/groupFeatureFlags/groupFeatureFlagsDrawerContent.tsx b/static/app/views/issueDetails/groupFeatureFlags/groupFeatureFlagsDrawerContent.tsx index a2a6e02f32bd13..0877ff72b6df82 100644 --- a/static/app/views/issueDetails/groupFeatureFlags/groupFeatureFlagsDrawerContent.tsx +++ b/static/app/views/issueDetails/groupFeatureFlags/groupFeatureFlagsDrawerContent.tsx @@ -1,9 +1,13 @@ import {useMemo} from 'react'; +import {shouldShowFeatureFlagCTA} from 'sentry/components/events/featureFlags/utils'; import LoadingError from 'sentry/components/loadingError'; import LoadingIndicator from 'sentry/components/loadingIndicator'; import {t} from 'sentry/locale'; import type {Group} from 'sentry/types/group'; +import useOrganization from 'sentry/utils/useOrganization'; +import useProjectFromSlug from 'sentry/utils/useProjectFromSlug'; +import FlagDrawerCTA from 'sentry/views/issueDetails/groupFeatureFlags/flagDrawerCTA'; import useGroupFeatureFlags from 'sentry/views/issueDetails/groupFeatureFlags/useGroupFeatureFlags'; import { Container, @@ -61,6 +65,13 @@ export default function GroupFeatureFlagsDrawerContent({ return searchedTags; }, [data, search, tagValues]); + const organization = useOrganization(); + const project = useProjectFromSlug({organization, projectSlug: group.project?.slug}); + + if (data.length === 0 && project && shouldShowFeatureFlagCTA(organization, project)) { + return <FlagDrawerCTA />; + } + return isPending ? ( <LoadingIndicator /> ) : isError ? ( From 0d452754e1a283d2a7062e39be947703145dcd14 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Tue, 18 Mar 2025 16:02:32 -0700 Subject: [PATCH 15/23] Fix setup sidebar analytics event --- .../events/featureFlags/featureFlagInlineCTA.tsx | 5 ++++- static/app/utils/analytics/featureFlagAnalyticsEvents.tsx | 4 ++++ .../views/issueDetails/groupFeatureFlags/flagDrawerCTA.tsx | 7 +++++-- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/static/app/components/events/featureFlags/featureFlagInlineCTA.tsx b/static/app/components/events/featureFlags/featureFlagInlineCTA.tsx index 1b61b3cef6b1ed..c859adc982654b 100644 --- a/static/app/components/events/featureFlags/featureFlagInlineCTA.tsx +++ b/static/app/components/events/featureFlags/featureFlagInlineCTA.tsx @@ -20,7 +20,10 @@ export default function FeatureFlagInlineCTA({projectId}: {projectId: string}) { const {activateSidebar} = useFeatureFlagOnboarding(); function handleSetupButtonClick(e: any) { - trackAnalytics('flags.setup_modal_opened', {organization}); + trackAnalytics('flags.setup_sidebar_opened', { + organization, + surface: 'issue_details.flags_section', + }); trackAnalytics('flags.cta_setup_button_clicked', {organization}); activateSidebar(e); } diff --git a/static/app/utils/analytics/featureFlagAnalyticsEvents.tsx b/static/app/utils/analytics/featureFlagAnalyticsEvents.tsx index a61509db4a7e91..b222d06aa17f9e 100644 --- a/static/app/utils/analytics/featureFlagAnalyticsEvents.tsx +++ b/static/app/utils/analytics/featureFlagAnalyticsEvents.tsx @@ -10,6 +10,9 @@ export type FeatureFlagEventParameters = { direction: 'next' | 'prev'; surface: 'settings'; }; + 'flags.setup_sidebar_opened': { + surface: 'issue_details.flags_section' | 'issue_details.flags_drawer'; + }; 'flags.sort_flags': {sortMethod: string}; 'flags.table_rendered': { numFlags: number; @@ -31,4 +34,5 @@ export const featureFlagEventMap: Record<FeatureFlagEventKey, string | null> = { 'flags.cta_dismissed': 'Flag CTA Dismissed', 'flags.logs-paginated': 'Feature Flag Logs Paginated', 'flags.view-setup-sidebar': 'Viewed Feature Flag Onboarding Sidebar', + 'flags.setup_sidebar_opened': 'Feature Flag Setup Sidebar Opened', }; diff --git a/static/app/views/issueDetails/groupFeatureFlags/flagDrawerCTA.tsx b/static/app/views/issueDetails/groupFeatureFlags/flagDrawerCTA.tsx index c43e5cc0571278..2b944f74412947 100644 --- a/static/app/views/issueDetails/groupFeatureFlags/flagDrawerCTA.tsx +++ b/static/app/views/issueDetails/groupFeatureFlags/flagDrawerCTA.tsx @@ -14,7 +14,10 @@ export default function FlagDrawerCTA() { const {onClose: closeDrawer} = useDrawerContentContext(); function handleSetupButtonClick(e: any) { - trackAnalytics('flags.setup_modal_opened', {organization}); + trackAnalytics('flags.setup_sidebar_opened', { + organization, + surface: 'issue_details.flags_drawer', + }); trackAnalytics('flags.cta_setup_button_clicked', {organization}); closeDrawer?.(); setTimeout(() => { @@ -55,7 +58,6 @@ const BannerTitle = styled('div')` const BannerDescription = styled('div')` margin-bottom: ${space(1.5)}; - max-width: 340px; `; const ActionButton = styled('div')` @@ -65,6 +67,7 @@ const ActionButton = styled('div')` const BannerWrapper = styled('div')` position: relative; + max-width: 600px; border: 1px solid ${p => p.theme.border}; border-radius: ${p => p.theme.borderRadius}; padding: ${space(2)}; From ddd625608fd29d0c7272a1b0441c30ba49cc837d Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Tue, 18 Mar 2025 16:15:08 -0700 Subject: [PATCH 16/23] Don't check cta ff --- .../events/featureFlags/eventFeatureFlagList.tsx | 9 +++++++-- static/app/components/events/featureFlags/utils.tsx | 11 ----------- .../groupFeatureFlagsDrawerContent.tsx | 9 +++++++-- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/static/app/components/events/featureFlags/eventFeatureFlagList.tsx b/static/app/components/events/featureFlags/eventFeatureFlagList.tsx index 7017a251951431..8f139de6b486fe 100644 --- a/static/app/components/events/featureFlags/eventFeatureFlagList.tsx +++ b/static/app/components/events/featureFlags/eventFeatureFlagList.tsx @@ -14,12 +14,12 @@ import FeatureFlagSort from 'sentry/components/events/featureFlags/featureFlagSo import { FlagControlOptions, OrderBy, - shouldShowFeatureFlagCTA, SortBy, sortedFlags, } from 'sentry/components/events/featureFlags/utils'; import useDrawer from 'sentry/components/globalDrawer'; import KeyValueData from 'sentry/components/keyValueData'; +import {featureFlagOnboardingPlatforms} from 'sentry/data/platformCategories'; import {IconMegaphone, IconSearch} from 'sentry/icons'; import {t, tn} from 'sentry/locale'; import type {Event, FeatureFlag} from 'sentry/types/event'; @@ -219,7 +219,12 @@ export function EventFeatureFlagList({ } // contexts.flags is not set and project has not ingested flags - if (!hasFlagContext && shouldShowFeatureFlagCTA(organization, project)) { + if ( + !hasFlagContext && + !project.hasFlags && + featureFlagOnboardingPlatforms.includes(project.platform ?? 'other') && + organization.features.includes('feature-flag-cta') + ) { return <FeatureFlagInlineCTA projectId={event.projectID} />; } diff --git a/static/app/components/events/featureFlags/utils.tsx b/static/app/components/events/featureFlags/utils.tsx index 2e2a2451d61aca..c32510a7ebd609 100644 --- a/static/app/components/events/featureFlags/utils.tsx +++ b/static/app/components/events/featureFlags/utils.tsx @@ -1,8 +1,5 @@ import type {KeyValueDataContentProps} from 'sentry/components/keyValueData'; -import {featureFlagOnboardingPlatforms} from 'sentry/data/platformCategories'; import {t} from 'sentry/locale'; -import type {Organization} from 'sentry/types/organization'; -import type {Project} from 'sentry/types/project'; export enum OrderBy { NEWEST = 'newest', @@ -142,11 +139,3 @@ export const PROVIDER_TO_SETUP_WEBHOOK_URL: Record<WebhookProviderEnum, string> [WebhookProviderEnum.UNLEASH]: 'https://docs.sentry.io/organization/integrations/feature-flag/unleash/#set-up-change-tracking', }; - -export function shouldShowFeatureFlagCTA(organization: Organization, project: Project) { - return ( - !project.hasFlags && - featureFlagOnboardingPlatforms.includes(project.platform ?? 'other') && - organization.features.includes('feature-flag-cta') - ); -} diff --git a/static/app/views/issueDetails/groupFeatureFlags/groupFeatureFlagsDrawerContent.tsx b/static/app/views/issueDetails/groupFeatureFlags/groupFeatureFlagsDrawerContent.tsx index b4252998b0e737..312a487ad7db8f 100644 --- a/static/app/views/issueDetails/groupFeatureFlags/groupFeatureFlagsDrawerContent.tsx +++ b/static/app/views/issueDetails/groupFeatureFlags/groupFeatureFlagsDrawerContent.tsx @@ -1,8 +1,8 @@ import {useMemo} from 'react'; -import {shouldShowFeatureFlagCTA} from 'sentry/components/events/featureFlags/utils'; import LoadingError from 'sentry/components/loadingError'; import LoadingIndicator from 'sentry/components/loadingIndicator'; +import {featureFlagOnboardingPlatforms} from 'sentry/data/platformCategories'; import {t} from 'sentry/locale'; import type {Group} from 'sentry/types/group'; import useOrganization from 'sentry/utils/useOrganization'; @@ -68,7 +68,12 @@ export default function GroupFeatureFlagsDrawerContent({ const organization = useOrganization(); const project = useProjectFromSlug({organization, projectSlug: group.project?.slug}); - if (data.length === 0 && project && shouldShowFeatureFlagCTA(organization, project)) { + if ( + data.length === 0 && + project && + !project.hasFlags && + featureFlagOnboardingPlatforms.includes(project.platform ?? 'other') + ) { return <FlagDrawerCTA />; } From d02d4e36b00167282f40dc77d9f0b9e6471f024d Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Tue, 18 Mar 2025 16:34:02 -0700 Subject: [PATCH 17/23] Style --- .../groupFeatureFlags/flagDrawerCTA.tsx | 66 +++++++++++++------ 1 file changed, 46 insertions(+), 20 deletions(-) diff --git a/static/app/views/issueDetails/groupFeatureFlags/flagDrawerCTA.tsx b/static/app/views/issueDetails/groupFeatureFlags/flagDrawerCTA.tsx index 2b944f74412947..0822c99bfe733e 100644 --- a/static/app/views/issueDetails/groupFeatureFlags/flagDrawerCTA.tsx +++ b/static/app/views/issueDetails/groupFeatureFlags/flagDrawerCTA.tsx @@ -1,5 +1,7 @@ import styled from '@emotion/styled'; +import onboardingInstall from 'sentry-images/spot/onboarding-install.svg'; + import {Button, LinkButton} from 'sentry/components/core/button'; import {useFeatureFlagOnboarding} from 'sentry/components/events/featureFlags/useFeatureFlagOnboarding'; import {useDrawerContentContext} from 'sentry/components/globalDrawer/components'; @@ -28,24 +30,27 @@ export default function FlagDrawerCTA() { return ( <BannerWrapper> - <BannerTitle>{t('Set Up Feature Flags')}</BannerTitle> - <BannerDescription> - {t( - 'Want to know which feature flags were associated with this error? Set up your feature flag integration.' - )} - </BannerDescription> - <ActionButton> - <Button onClick={handleSetupButtonClick} priority="primary"> - {t('Set Up Now')} - </Button> - <LinkButton - priority="default" - href="https://docs.sentry.io/product/explore/feature-flags/" - external - > - {t('Read More')} - </LinkButton> - </ActionButton> + <CardContent> + <BannerTitle>{t('Set Up Feature Flags')}</BannerTitle> + <BannerDescription> + {t( + 'Want to know which feature flags were associated with this error? Set up your feature flag integration.' + )} + </BannerDescription> + <ActionButton> + <Button onClick={handleSetupButtonClick} priority="primary"> + {t('Set Up Now')} + </Button> + <LinkButton + priority="default" + href="https://docs.sentry.io/product/explore/feature-flags/" + external + > + {t('Read More')} + </LinkButton> + </ActionButton> + </CardContent> + <BannerIllustration src={onboardingInstall} alt="Install" /> </BannerWrapper> ); } @@ -58,6 +63,7 @@ const BannerTitle = styled('div')` const BannerDescription = styled('div')` margin-bottom: ${space(1.5)}; + max-width: 340px; `; const ActionButton = styled('div')` @@ -67,10 +73,8 @@ const ActionButton = styled('div')` const BannerWrapper = styled('div')` position: relative; - max-width: 600px; border: 1px solid ${p => p.theme.border}; border-radius: ${p => p.theme.borderRadius}; - padding: ${space(2)}; margin: ${space(1)} 0; background: linear-gradient( 90deg, @@ -78,4 +82,26 @@ const BannerWrapper = styled('div')` ${p => p.theme.backgroundSecondary}FF 70%, ${p => p.theme.backgroundSecondary}FF 100% ); + display: flex; + flex-direction: row; + align-items: flex-end; + justify-content: space-between; + gap: ${space(1)}; +`; + +const BannerIllustration = styled('img')` + height: 100%; + object-fit: contain; + max-width: 30%; + margin-right: 10px; + margin-bottom: -${space(2)}; + padding: ${space(2)}; +`; + +const CardContent = styled('div')` + padding: ${space(2)}; + + display: flex; + flex-direction: column; + justify-content: center; `; From 62e7f9ff0a93f52b4f9c38080d32e795c545c842 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Tue, 18 Mar 2025 16:40:52 -0700 Subject: [PATCH 18/23] Rename CardContent --- .../groupFeatureFlags/flagDrawerCTA.tsx | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/static/app/views/issueDetails/groupFeatureFlags/flagDrawerCTA.tsx b/static/app/views/issueDetails/groupFeatureFlags/flagDrawerCTA.tsx index 0822c99bfe733e..c3967af6257352 100644 --- a/static/app/views/issueDetails/groupFeatureFlags/flagDrawerCTA.tsx +++ b/static/app/views/issueDetails/groupFeatureFlags/flagDrawerCTA.tsx @@ -30,7 +30,7 @@ export default function FlagDrawerCTA() { return ( <BannerWrapper> - <CardContent> + <BannerContent> <BannerTitle>{t('Set Up Feature Flags')}</BannerTitle> <BannerDescription> {t( @@ -49,12 +49,17 @@ export default function FlagDrawerCTA() { {t('Read More')} </LinkButton> </ActionButton> - </CardContent> + </BannerContent> <BannerIllustration src={onboardingInstall} alt="Install" /> </BannerWrapper> ); } +const ActionButton = styled('div')` + display: flex; + gap: ${space(1)}; +`; + const BannerTitle = styled('div')` font-size: ${p => p.theme.fontSizeExtraLarge}; margin-bottom: ${space(1)}; @@ -66,9 +71,21 @@ const BannerDescription = styled('div')` max-width: 340px; `; -const ActionButton = styled('div')` +const BannerContent = styled('div')` + padding: ${space(2)}; + display: flex; - gap: ${space(1)}; + flex-direction: column; + justify-content: center; +`; + +const BannerIllustration = styled('img')` + height: 100%; + object-fit: contain; + max-width: 30%; + margin-right: 10px; + margin-bottom: -${space(2)}; + padding: ${space(2)}; `; const BannerWrapper = styled('div')` @@ -88,20 +105,3 @@ const BannerWrapper = styled('div')` justify-content: space-between; gap: ${space(1)}; `; - -const BannerIllustration = styled('img')` - height: 100%; - object-fit: contain; - max-width: 30%; - margin-right: 10px; - margin-bottom: -${space(2)}; - padding: ${space(2)}; -`; - -const CardContent = styled('div')` - padding: ${space(2)}; - - display: flex; - flex-direction: column; - justify-content: center; -`; From 0c0ddfa61c2c97d7ef10d13db79a005d1a701053 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Wed, 19 Mar 2025 10:47:42 -0700 Subject: [PATCH 19/23] Update inline cta style and reword drawer cta --- .../featureFlags/featureFlagInlineCTA.tsx | 28 +++++++++++++++++-- .../groupFeatureFlags/flagDrawerCTA.tsx | 2 +- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/static/app/components/events/featureFlags/featureFlagInlineCTA.tsx b/static/app/components/events/featureFlags/featureFlagInlineCTA.tsx index c859adc982654b..55e9dbddb6232b 100644 --- a/static/app/components/events/featureFlags/featureFlagInlineCTA.tsx +++ b/static/app/components/events/featureFlags/featureFlagInlineCTA.tsx @@ -1,5 +1,7 @@ import styled from '@emotion/styled'; +import onboardingInstall from 'sentry-images/spot/onboarding-install.svg'; + import {usePrompt} from 'sentry/actionCreators/prompts'; import ButtonBar from 'sentry/components/buttonBar'; import {Button, LinkButton} from 'sentry/components/core/button'; @@ -77,7 +79,7 @@ export default function FeatureFlagInlineCTA({projectId}: {projectId: string}) { actions={actions} > <BannerWrapper> - <div> + <BannerContent> <BannerTitle>{t('Set Up Feature Flags')}</BannerTitle> <BannerDescription> {t( @@ -96,7 +98,7 @@ export default function FeatureFlagInlineCTA({projectId}: {projectId: string}) { {t('Read More')} </LinkButton> </ActionButton> - </div> + </BannerContent> <CloseDropdownMenu position="bottom-end" triggerProps={{ @@ -130,6 +132,7 @@ export default function FeatureFlagInlineCTA({projectId}: {projectId: string}) { }, ]} /> + <BannerIllustration src={onboardingInstall} alt="Install" /> </BannerWrapper> </InterimSection> ); @@ -146,6 +149,22 @@ const BannerDescription = styled('div')` max-width: 340px; `; +const BannerContent = styled('div')` + padding: ${space(2)}; + display: flex; + flex-direction: column; + justify-content: center; +`; + +const BannerIllustration = styled('img')` + height: 100%; + object-fit: contain; + max-width: 30%; + margin-right: 10px; + margin-bottom: -${space(2)}; + padding: ${space(2)}; +`; + const CloseDropdownMenu = styled(DropdownMenu)` position: absolute; display: block; @@ -173,4 +192,9 @@ const BannerWrapper = styled('div')` ${p => p.theme.backgroundSecondary}FF 70%, ${p => p.theme.backgroundSecondary}FF 100% ); + display: flex; + flex-direction: row; + align-items: flex-end; + justify-content: space-between; + gap: ${space(1)}; `; diff --git a/static/app/views/issueDetails/groupFeatureFlags/flagDrawerCTA.tsx b/static/app/views/issueDetails/groupFeatureFlags/flagDrawerCTA.tsx index c3967af6257352..47faf991831476 100644 --- a/static/app/views/issueDetails/groupFeatureFlags/flagDrawerCTA.tsx +++ b/static/app/views/issueDetails/groupFeatureFlags/flagDrawerCTA.tsx @@ -34,7 +34,7 @@ export default function FlagDrawerCTA() { <BannerTitle>{t('Set Up Feature Flags')}</BannerTitle> <BannerDescription> {t( - 'Want to know which feature flags were associated with this error? Set up your feature flag integration.' + 'Want to know which feature flags were associated with this issue? Set up your feature flag integration.' )} </BannerDescription> <ActionButton> From 103e9830dff7140aa9506b9ee1f3c2d19b0aa849 Mon Sep 17 00:00:00 2001 From: "getsantry[bot]" <66042841+getsantry[bot]@users.noreply.github.com> Date: Wed, 19 Mar 2025 17:52:02 +0000 Subject: [PATCH 20/23] :hammer_and_wrench: apply pre-commit fixes --- .../groupFeatureFlags/groupFeatureFlagsDrawerContent.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/static/app/views/issueDetails/groupFeatureFlags/groupFeatureFlagsDrawerContent.tsx b/static/app/views/issueDetails/groupFeatureFlags/groupFeatureFlagsDrawerContent.tsx index 8f59d243269638..312a487ad7db8f 100644 --- a/static/app/views/issueDetails/groupFeatureFlags/groupFeatureFlagsDrawerContent.tsx +++ b/static/app/views/issueDetails/groupFeatureFlags/groupFeatureFlagsDrawerContent.tsx @@ -9,7 +9,6 @@ import useOrganization from 'sentry/utils/useOrganization'; import useProjectFromSlug from 'sentry/utils/useProjectFromSlug'; import FlagDetailsLink from 'sentry/views/issueDetails/groupFeatureFlags/flagDetailsLink'; import FlagDrawerCTA from 'sentry/views/issueDetails/groupFeatureFlags/flagDrawerCTA'; -import FlagDetailsLink from 'sentry/views/issueDetails/groupFeatureFlags/flagDetailsLink'; import useGroupFeatureFlags from 'sentry/views/issueDetails/groupFeatureFlags/useGroupFeatureFlags'; import { Container, From 716e33986c282f5ffe3f8be39d34467a685892ae Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Thu, 20 Mar 2025 14:48:17 -0700 Subject: [PATCH 21/23] Share content component --- .../featureFlags/featureFlagInlineCTA.tsx | 88 +++++++++++-------- .../groupFeatureFlags/flagDrawerCTA.tsx | 84 ++---------------- 2 files changed, 55 insertions(+), 117 deletions(-) diff --git a/static/app/components/events/featureFlags/featureFlagInlineCTA.tsx b/static/app/components/events/featureFlags/featureFlagInlineCTA.tsx index 55e9dbddb6232b..b79436de8eefb3 100644 --- a/static/app/components/events/featureFlags/featureFlagInlineCTA.tsx +++ b/static/app/components/events/featureFlags/featureFlagInlineCTA.tsx @@ -1,3 +1,4 @@ +import {Fragment} from 'react'; import styled from '@emotion/styled'; import onboardingInstall from 'sentry-images/spot/onboarding-install.svg'; @@ -17,6 +18,38 @@ import useOrganization from 'sentry/utils/useOrganization'; import {SectionKey} from 'sentry/views/issueDetails/streamline/context'; import {InterimSection} from 'sentry/views/issueDetails/streamline/interimSection'; +export function FeatureFlagCTAContent({ + handleSetupButtonClick, +}: { + handleSetupButtonClick: (e: any) => void; +}) { + return ( + <Fragment> + <BannerContent> + <BannerTitle>{t('Set Up Feature Flags')}</BannerTitle> + <BannerDescription> + {t( + 'Want to know which feature flags were associated with this issue? Set up your feature flag integration.' + )} + </BannerDescription> + <ActionButton> + <Button onClick={handleSetupButtonClick} priority="primary"> + {t('Set Up Now')} + </Button> + <LinkButton + priority="default" + href="https://docs.sentry.io/product/explore/feature-flags/" + external + > + {t('Read More')} + </LinkButton> + </ActionButton> + </BannerContent> + <BannerIllustration src={onboardingInstall} alt="Install" /> + </Fragment> + ); +} + export default function FeatureFlagInlineCTA({projectId}: {projectId: string}) { const organization = useOrganization(); const {activateSidebar} = useFeatureFlagOnboarding(); @@ -79,26 +112,7 @@ export default function FeatureFlagInlineCTA({projectId}: {projectId: string}) { actions={actions} > <BannerWrapper> - <BannerContent> - <BannerTitle>{t('Set Up Feature Flags')}</BannerTitle> - <BannerDescription> - {t( - 'Want to know which feature flags were associated with this error? Set up your feature flag integration.' - )} - </BannerDescription> - <ActionButton> - <Button onClick={handleSetupButtonClick} priority="primary"> - {t('Set Up Now')} - </Button> - <LinkButton - priority="default" - href="https://docs.sentry.io/product/explore/feature-flags/" - external - > - {t('Read More')} - </LinkButton> - </ActionButton> - </BannerContent> + <FeatureFlagCTAContent handleSetupButtonClick={handleSetupButtonClick} /> <CloseDropdownMenu position="bottom-end" triggerProps={{ @@ -132,12 +146,16 @@ export default function FeatureFlagInlineCTA({projectId}: {projectId: string}) { }, ]} /> - <BannerIllustration src={onboardingInstall} alt="Install" /> </BannerWrapper> </InterimSection> ); } +const ActionButton = styled('div')` + display: flex; + gap: ${space(1)}; +`; + const BannerTitle = styled('div')` font-size: ${p => p.theme.fontSizeExtraLarge}; margin-bottom: ${space(1)}; @@ -165,26 +183,10 @@ const BannerIllustration = styled('img')` padding: ${space(2)}; `; -const CloseDropdownMenu = styled(DropdownMenu)` - position: absolute; - display: block; - top: ${space(1)}; - right: ${space(1)}; - color: ${p => p.theme.white}; - cursor: pointer; - z-index: 1; -`; - -const ActionButton = styled('div')` - display: flex; - gap: ${space(1)}; -`; - -const BannerWrapper = styled('div')` +export const BannerWrapper = styled('div')` position: relative; border: 1px solid ${p => p.theme.border}; border-radius: ${p => p.theme.borderRadius}; - padding: ${space(2)}; margin: ${space(1)} 0; background: linear-gradient( 90deg, @@ -198,3 +200,13 @@ const BannerWrapper = styled('div')` justify-content: space-between; gap: ${space(1)}; `; + +const CloseDropdownMenu = styled(DropdownMenu)` + position: absolute; + display: block; + top: ${space(1)}; + right: ${space(1)}; + color: ${p => p.theme.white}; + cursor: pointer; + z-index: 1; +`; diff --git a/static/app/views/issueDetails/groupFeatureFlags/flagDrawerCTA.tsx b/static/app/views/issueDetails/groupFeatureFlags/flagDrawerCTA.tsx index 47faf991831476..9a6c97cd3d3f75 100644 --- a/static/app/views/issueDetails/groupFeatureFlags/flagDrawerCTA.tsx +++ b/static/app/views/issueDetails/groupFeatureFlags/flagDrawerCTA.tsx @@ -1,12 +1,9 @@ -import styled from '@emotion/styled'; - -import onboardingInstall from 'sentry-images/spot/onboarding-install.svg'; - -import {Button, LinkButton} from 'sentry/components/core/button'; +import { + BannerWrapper, + FeatureFlagCTAContent, +} from 'sentry/components/events/featureFlags/featureFlagInlineCTA'; import {useFeatureFlagOnboarding} from 'sentry/components/events/featureFlags/useFeatureFlagOnboarding'; import {useDrawerContentContext} from 'sentry/components/globalDrawer/components'; -import {t} from 'sentry/locale'; -import {space} from 'sentry/styles/space'; import {trackAnalytics} from 'sentry/utils/analytics'; import useOrganization from 'sentry/utils/useOrganization'; @@ -30,78 +27,7 @@ export default function FlagDrawerCTA() { return ( <BannerWrapper> - <BannerContent> - <BannerTitle>{t('Set Up Feature Flags')}</BannerTitle> - <BannerDescription> - {t( - 'Want to know which feature flags were associated with this issue? Set up your feature flag integration.' - )} - </BannerDescription> - <ActionButton> - <Button onClick={handleSetupButtonClick} priority="primary"> - {t('Set Up Now')} - </Button> - <LinkButton - priority="default" - href="https://docs.sentry.io/product/explore/feature-flags/" - external - > - {t('Read More')} - </LinkButton> - </ActionButton> - </BannerContent> - <BannerIllustration src={onboardingInstall} alt="Install" /> + <FeatureFlagCTAContent handleSetupButtonClick={handleSetupButtonClick} /> </BannerWrapper> ); } - -const ActionButton = styled('div')` - display: flex; - gap: ${space(1)}; -`; - -const BannerTitle = styled('div')` - font-size: ${p => p.theme.fontSizeExtraLarge}; - margin-bottom: ${space(1)}; - font-weight: ${p => p.theme.fontWeightBold}; -`; - -const BannerDescription = styled('div')` - margin-bottom: ${space(1.5)}; - max-width: 340px; -`; - -const BannerContent = styled('div')` - padding: ${space(2)}; - - display: flex; - flex-direction: column; - justify-content: center; -`; - -const BannerIllustration = styled('img')` - height: 100%; - object-fit: contain; - max-width: 30%; - margin-right: 10px; - margin-bottom: -${space(2)}; - padding: ${space(2)}; -`; - -const BannerWrapper = styled('div')` - position: relative; - border: 1px solid ${p => p.theme.border}; - border-radius: ${p => p.theme.borderRadius}; - margin: ${space(1)} 0; - background: linear-gradient( - 90deg, - ${p => p.theme.backgroundSecondary}00 0%, - ${p => p.theme.backgroundSecondary}FF 70%, - ${p => p.theme.backgroundSecondary}FF 100% - ); - display: flex; - flex-direction: row; - align-items: flex-end; - justify-content: space-between; - gap: ${space(1)}; -`; From e316a654484d4f67d4a96dfbda1bcbadc6f27da1 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Thu, 20 Mar 2025 15:19:31 -0700 Subject: [PATCH 22/23] Fix event flags cta vs hide conditions --- .../featureFlags/eventFeatureFlagList.spec.tsx | 2 +- .../events/featureFlags/eventFeatureFlagList.tsx | 16 ++++++---------- .../components/events/featureFlags/testUtils.tsx | 2 +- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/static/app/components/events/featureFlags/eventFeatureFlagList.spec.tsx b/static/app/components/events/featureFlags/eventFeatureFlagList.spec.tsx index a66baa739709c0..57e9cf38d1eb16 100644 --- a/static/app/components/events/featureFlags/eventFeatureFlagList.spec.tsx +++ b/static/app/components/events/featureFlags/eventFeatureFlagList.spec.tsx @@ -217,7 +217,7 @@ describe('EventFeatureFlagList', function () { ).toBe(document.DOCUMENT_POSITION_FOLLOWING); }); - it('renders empty state', function () { + it('renders empty state if project has flags', function () { render(<EventFeatureFlagList {...EMPTY_STATE_SECTION_PROPS} />); const control = screen.queryByRole('button', {name: 'Sort Flags'}); diff --git a/static/app/components/events/featureFlags/eventFeatureFlagList.tsx b/static/app/components/events/featureFlags/eventFeatureFlagList.tsx index 8f139de6b486fe..ab21a4832f361a 100644 --- a/static/app/components/events/featureFlags/eventFeatureFlagList.tsx +++ b/static/app/components/events/featureFlags/eventFeatureFlagList.tsx @@ -127,8 +127,6 @@ export function EventFeatureFlagList({ : new Set(suspectFlags.map(f => f.flag)); }, [isSuspectError, isSuspectPending, suspectFlags]); - const hasFlagContext = Boolean(event.contexts?.flags?.values); - const eventFlags: Array<Required<FeatureFlag>> = useMemo(() => { // At runtime there's no type guarantees on the event flags. So we have to // explicitly validate against SDK developer error or user-provided contexts. @@ -218,14 +216,12 @@ export function EventFeatureFlagList({ return null; } - // contexts.flags is not set and project has not ingested flags - if ( - !hasFlagContext && - !project.hasFlags && - featureFlagOnboardingPlatforms.includes(project.platform ?? 'other') && - organization.features.includes('feature-flag-cta') - ) { - return <FeatureFlagInlineCTA projectId={event.projectID} />; + // If the project has never ingested flags, either show a CTA or hide the section entirely. + if (!hasFlags && !project.hasFlags) { + const showCTA = + featureFlagOnboardingPlatforms.includes(project.platform ?? 'other') && + organization.features.includes('feature-flag-cta'); + return showCTA ? <FeatureFlagInlineCTA projectId={event.projectID} /> : null; } const actions = ( diff --git a/static/app/components/events/featureFlags/testUtils.tsx b/static/app/components/events/featureFlags/testUtils.tsx index 5e52a8329ce92d..94f286dab0127c 100644 --- a/static/app/components/events/featureFlags/testUtils.tsx +++ b/static/app/components/events/featureFlags/testUtils.tsx @@ -150,7 +150,7 @@ export const EMPTY_STATE_SECTION_PROPS = { id: 'abc123def456ghi789jkl', contexts: {flags: {values: []}}, }), - project: ProjectFixture(), + project: ProjectFixture({hasFlags: true}), group: GroupFixture(), }; From 990a44f9e70adc734998da2b7685f0856d9764c2 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Fri, 21 Mar 2025 16:44:43 -0700 Subject: [PATCH 23/23] Update drawer showCTA condition and jest coverage --- .../groupFeatureFlagsDrawerContent.spec.tsx | 179 ++++++++++++++++++ .../groupFeatureFlagsDrawerContent.tsx | 16 +- tests/js/fixtures/featureFlags.ts | 42 ++++ 3 files changed, 228 insertions(+), 9 deletions(-) create mode 100644 static/app/views/issueDetails/groupFeatureFlags/groupFeatureFlagsDrawerContent.spec.tsx create mode 100644 tests/js/fixtures/featureFlags.ts diff --git a/static/app/views/issueDetails/groupFeatureFlags/groupFeatureFlagsDrawerContent.spec.tsx b/static/app/views/issueDetails/groupFeatureFlags/groupFeatureFlagsDrawerContent.spec.tsx new file mode 100644 index 00000000000000..c1662aa7f5e6d8 --- /dev/null +++ b/static/app/views/issueDetails/groupFeatureFlags/groupFeatureFlagsDrawerContent.spec.tsx @@ -0,0 +1,179 @@ +import {FeatureFlagsFixture} from 'sentry-fixture/featureFlags'; +import {GroupFixture} from 'sentry-fixture/group'; +import {ProjectFixture} from 'sentry-fixture/project'; + +import {render, screen, waitFor} from 'sentry-test/reactTestingLibrary'; + +import ProjectsStore from 'sentry/stores/projectsStore'; +import GroupFeatureFlagsDrawerContent from 'sentry/views/issueDetails/groupFeatureFlags/groupFeatureFlagsDrawerContent'; + +describe('GroupFeatureFlagsDrawerContent', function () { + function getEmptyState() { + return screen.queryByTestId('empty-state') ?? screen.getByTestId('empty-message'); + } + + beforeEach(function () { + jest.resetAllMocks(); + MockApiClient.addMockResponse({ + url: `/organizations/org-slug/issues/1/tags/`, + body: [], + }); + + ProjectsStore.init(); + ProjectsStore.loadInitialData([ + ProjectFixture({platform: 'javascript', hasFlags: false}), + ]); + }); + + it('calls flags backend and renders distribution cards', async function () { + const mockTagsEndpoint = MockApiClient.addMockResponse({ + url: `/organizations/org-slug/issues/1/tags/`, + body: FeatureFlagsFixture(), + }); + + render( + <GroupFeatureFlagsDrawerContent + environments={[]} + group={GroupFixture()} + search="" + /> + ); + + await waitFor(() => { + expect(screen.queryByTestId('loading-indicator')).not.toBeInTheDocument(); + }); + + expect(mockTagsEndpoint).toHaveBeenCalledWith( + '/organizations/org-slug/issues/1/tags/', + expect.objectContaining({ + query: expect.objectContaining({useFlagsBackend: '1'}), + }) + ); + + expect(screen.getByText('feature.organizations:my-feature')).toBeInTheDocument(); + expect(screen.getByText('my-rolled-out-feature')).toBeInTheDocument(); + }); + + it('renders error state', async function () { + MockApiClient.addMockResponse({ + url: `/organizations/org-slug/issues/1/tags/`, + statusCode: 400, + body: { + detail: 'Bad request', + }, + }); + + render( + <GroupFeatureFlagsDrawerContent + environments={[]} + group={GroupFixture()} + search="" + /> + ); + + await waitFor(() => { + expect(screen.getByTestId('loading-error')).toBeInTheDocument(); + }); + }); + + it('renders empty state when no flags match the search', async function () { + MockApiClient.addMockResponse({ + url: `/organizations/org-slug/issues/1/tags/`, + body: FeatureFlagsFixture(), + }); + + render( + <GroupFeatureFlagsDrawerContent + environments={[]} + group={GroupFixture()} + search="zxf" + /> + ); + + await waitFor(() => { + expect(screen.queryByTestId('loading-indicator')).not.toBeInTheDocument(); + }); + + const emptyState = getEmptyState(); + expect(emptyState).toBeInTheDocument(); + expect(emptyState).toHaveTextContent('No feature flags were found for this search'); + }); + + it('renders empty state when no flags returned and hasFlags', async function () { + ProjectsStore.reset(); + ProjectsStore.loadInitialData([ + ProjectFixture({platform: 'javascript', hasFlags: true}), + ]); + + render( + <GroupFeatureFlagsDrawerContent + environments={[]} + group={GroupFixture()} + search="" + /> + ); + + await waitFor(() => { + expect(screen.queryByTestId('loading-indicator')).not.toBeInTheDocument(); + }); + + const emptyState = getEmptyState(); + expect(emptyState).toBeInTheDocument(); + expect(emptyState).toHaveTextContent('No feature flags were found for this issue'); + }); + + it('renders CTA when no flags returned and hasFlags is false', async function () { + render( + <GroupFeatureFlagsDrawerContent + environments={[]} + group={GroupFixture()} + search="" + /> + ); + + await waitFor(() => { + expect(screen.queryByTestId('loading-indicator')).not.toBeInTheDocument(); + }); + + expect(screen.getByText('Set Up Feature Flags')).toBeInTheDocument(); + }); + + it('does not render CTA when no flags returned and platform unsupported', async function () { + ProjectsStore.reset(); + ProjectsStore.loadInitialData([ + ProjectFixture({platform: 'dotnet-awslambda', hasFlags: false}), + ]); + + render( + <GroupFeatureFlagsDrawerContent + environments={[]} + group={GroupFixture()} + search="" + /> + ); + + await waitFor(() => { + expect(screen.queryByTestId('loading-indicator')).not.toBeInTheDocument(); + }); + + expect(getEmptyState()).toBeInTheDocument(); + }); + + it('does not render CTA when project not found', async function () { + ProjectsStore.reset(); + + render( + <GroupFeatureFlagsDrawerContent + environments={[]} + group={GroupFixture()} + search="" + /> + ); + + await waitFor(() => { + expect(screen.queryByTestId('loading-indicator')).not.toBeInTheDocument(); + }); + + expect(getEmptyState()).toBeInTheDocument(); + }); +}); diff --git a/static/app/views/issueDetails/groupFeatureFlags/groupFeatureFlagsDrawerContent.tsx b/static/app/views/issueDetails/groupFeatureFlags/groupFeatureFlagsDrawerContent.tsx index 312a487ad7db8f..ff77c05edeb05c 100644 --- a/static/app/views/issueDetails/groupFeatureFlags/groupFeatureFlagsDrawerContent.tsx +++ b/static/app/views/issueDetails/groupFeatureFlags/groupFeatureFlagsDrawerContent.tsx @@ -5,8 +5,7 @@ import LoadingIndicator from 'sentry/components/loadingIndicator'; import {featureFlagOnboardingPlatforms} from 'sentry/data/platformCategories'; import {t} from 'sentry/locale'; import type {Group} from 'sentry/types/group'; -import useOrganization from 'sentry/utils/useOrganization'; -import useProjectFromSlug from 'sentry/utils/useProjectFromSlug'; +import useProjects from 'sentry/utils/useProjects'; import FlagDetailsLink from 'sentry/views/issueDetails/groupFeatureFlags/flagDetailsLink'; import FlagDrawerCTA from 'sentry/views/issueDetails/groupFeatureFlags/flagDrawerCTA'; import useGroupFeatureFlags from 'sentry/views/issueDetails/groupFeatureFlags/useGroupFeatureFlags'; @@ -65,17 +64,14 @@ export default function GroupFeatureFlagsDrawerContent({ return searchedTags; }, [data, search, tagValues]); - const organization = useOrganization(); - const project = useProjectFromSlug({organization, projectSlug: group.project?.slug}); + const {projects} = useProjects(); + const project = projects.find(p => p.slug === group.project.slug)!; - if ( + const showCTA = data.length === 0 && project && !project.hasFlags && - featureFlagOnboardingPlatforms.includes(project.platform ?? 'other') - ) { - return <FlagDrawerCTA />; - } + featureFlagOnboardingPlatforms.includes(project.platform ?? 'other'); return isPending ? ( <LoadingIndicator /> @@ -84,6 +80,8 @@ export default function GroupFeatureFlagsDrawerContent({ message={t('There was an error loading feature flags.')} onRetry={refetch} /> + ) : showCTA ? ( + <FlagDrawerCTA /> ) : displayTags.length === 0 ? ( <StyledEmptyStateWarning withIcon> {data.length === 0 diff --git a/tests/js/fixtures/featureFlags.ts b/tests/js/fixtures/featureFlags.ts new file mode 100644 index 00000000000000..6712d506de8eb5 --- /dev/null +++ b/tests/js/fixtures/featureFlags.ts @@ -0,0 +1,42 @@ +import { GroupTag } from 'sentry/views/issueDetails/groupTags/useGroupTags'; + +export function FeatureFlagsFixture(params: GroupTag[] = []): GroupTag[] { + return [ + { + key: 'feature.organizations:my-feature', + name: 'Feature.Organizations:My-Feature', + totalValues: 11, + topValues: [ + { + name: 'true', + value: 'true', + count: 7, + lastSeen: '2025-03-21T18:17:44Z', + firstSeen: '2025-03-20T16:05:25Z', + }, + { + name: 'false', + value: 'false', + count: 4, + lastSeen: '2025-03-21T19:17:44Z', + firstSeen: '2025-03-15T16:00:00Z', + }, + ], + }, + { + key: 'my-rolled-out-feature', + name: 'My-Rolled-Out-Feature', + totalValues: 23, + topValues: [ + { + name: 'true', + value: 'true', + count: 23, + lastSeen: '2025-03-21T18:17:44Z', + firstSeen: '2025-03-21T16:05:25Z', + }, + ], + }, + ...params, + ]; +}