diff --git a/static/app/components/events/featureFlags/cta/featureFlagCTAContent.tsx b/static/app/components/events/featureFlags/cta/featureFlagCTAContent.tsx new file mode 100644 index 00000000000000..fab85c2fc527a1 --- /dev/null +++ b/static/app/components/events/featureFlags/cta/featureFlagCTAContent.tsx @@ -0,0 +1,118 @@ +import {Fragment, useEffect} from 'react'; +import styled from '@emotion/styled'; + +import onboardingInstall from 'sentry-images/spot/onboarding-install.svg'; + +import {useAnalyticsArea} from 'sentry/components/analyticsArea'; +import {Button} from 'sentry/components/core/button'; +import {LinkButton} from 'sentry/components/core/button/linkButton'; +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 FeatureFlagCTAContent({ + handleSetupButtonClick, +}: { + handleSetupButtonClick: (e: any) => void; +}) { + const organization = useOrganization(); + const analyticsArea = useAnalyticsArea(); + + useEffect(() => { + trackAnalytics('flags.cta_rendered', { + organization, + surface: analyticsArea, + }); + }, [organization, analyticsArea]); + + return ( + + + {t('Set Up Feature Flags')} + + {t( + 'Want to know which feature flags were associated with this issue? Set up your feature flag integration.' + )} + + + + { + trackAnalytics('flags.cta_read_more_clicked', { + organization, + surface: analyticsArea, + }); + }} + > + {t('Read More')} + + + + + + ); +} + +const ActionButton = styled('div')` + display: flex; + gap: ${space(1)}; +`; + +const BannerTitle = styled('div')` + font-size: ${p => p.theme.fontSize.xl}; + margin-bottom: ${space(1)}; + font-weight: ${p => p.theme.fontWeight.bold}; +`; + +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')` + object-fit: contain; + max-width: 30%; + min-width: 150px; + padding-inline: ${space(2)}; + padding-top: ${space(2)}; + align-self: flex-end; +`; + +export const BannerWrapper = styled('div')` + position: relative; + border: 1px solid ${p => p.theme.border}; + border-radius: ${p => p.theme.borderRadius}; + 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)}; + + container-name: bannerWrapper; + container-type: inline-size; + + @container bannerWrapper (max-width: 400px) { + img { + display: none; + } + } +`; diff --git a/static/app/components/events/featureFlags/cta/featureFlagInlineCTA.spec.tsx b/static/app/components/events/featureFlags/cta/featureFlagInlineCTA.spec.tsx deleted file mode 100644 index 89aa9ac7f05d98..00000000000000 --- a/static/app/components/events/featureFlags/cta/featureFlagInlineCTA.spec.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; - -import FeatureFlagInlineCTA from 'sentry/components/events/featureFlags/cta/featureFlagInlineCTA'; - -describe('featureFlagInlineCTA', () => { - beforeEach(() => { - MockApiClient.clearMockResponses(); - MockApiClient.addMockResponse({ - url: '/organizations/org-slug/prompts-activity/', - body: {data: {dismissed_ts: null}}, - }); - }); - - it('shows an onboarding banner that may be dismissed', async () => { - MockApiClient.addMockResponse({ - url: '/organizations/org-slug/prompts-activity/', - body: {data: {}}, - }); - const dismissMock = MockApiClient.addMockResponse({ - url: '/organizations/org-slug/prompts-activity/', - method: 'PUT', - }); - - render(); - expect(await screen.findByText('Set Up Feature Flags')).toBeInTheDocument(); - - // Open the snooze or dismiss dropdown - await userEvent.click(screen.getByTestId('icon-close')); - expect(screen.getByText('Dismiss')).toBeInTheDocument(); - expect(screen.getByText('Snooze')).toBeInTheDocument(); - - // Click dismiss - await userEvent.click(screen.getByRole('menuitemradio', {name: 'Dismiss'})); - expect(dismissMock).toHaveBeenCalledWith( - '/organizations/org-slug/prompts-activity/', - expect.objectContaining({ - data: expect.objectContaining({ - feature: 'issue_feature_flags_inline_onboarding', - status: 'dismissed', - }), - }) - ); - expect(screen.queryByText('Set Up Feature Flags')).not.toBeInTheDocument(); - }); - - it('shows an onboarding banner that may be snoozed', async () => { - MockApiClient.addMockResponse({ - url: '/organizations/org-slug/prompts-activity/', - body: {data: {}}, - }); - const snoozeMock = MockApiClient.addMockResponse({ - url: '/organizations/org-slug/prompts-activity/', - method: 'PUT', - }); - - render(); - expect(await screen.findByText('Set Up Feature Flags')).toBeInTheDocument(); - - // Open the snooze or dismiss dropdown - await userEvent.click(screen.getByTestId('icon-close')); - expect(screen.getByText('Dismiss')).toBeInTheDocument(); - expect(screen.getByText('Snooze')).toBeInTheDocument(); - - // Click snooze - await userEvent.click(screen.getByRole('menuitemradio', {name: 'Snooze'})); - expect(snoozeMock).toHaveBeenCalledWith( - '/organizations/org-slug/prompts-activity/', - expect.objectContaining({ - data: expect.objectContaining({ - feature: 'issue_feature_flags_inline_onboarding', - status: 'snoozed', - }), - }) - ); - expect(screen.queryByText('Set Up Feature Flags')).not.toBeInTheDocument(); - }); - - it('does not render if already dismissed', () => { - MockApiClient.addMockResponse({ - url: '/organizations/org-slug/prompts-activity/', - body: { - data: { - feature: 'issue_feature_flags_inline_onboarding', - status: 'dismissed', - dismissed_ts: 3, - }, - }, - }); - - render(); - expect(screen.queryByText('Set Up Feature Flags')).not.toBeInTheDocument(); - }); -}); diff --git a/static/app/components/events/featureFlags/cta/featureFlagInlineCTA.tsx b/static/app/components/events/featureFlags/cta/featureFlagInlineCTA.tsx deleted file mode 100644 index 5e41aa7b1074dd..00000000000000 --- a/static/app/components/events/featureFlags/cta/featureFlagInlineCTA.tsx +++ /dev/null @@ -1,240 +0,0 @@ -import {Fragment, useEffect} from 'react'; -import styled from '@emotion/styled'; - -import onboardingInstall from 'sentry-images/spot/onboarding-install.svg'; - -import {usePrompt} from 'sentry/actionCreators/prompts'; -import {useAnalyticsArea} from 'sentry/components/analyticsArea'; -import {Button} from 'sentry/components/core/button'; -import {ButtonBar} from 'sentry/components/core/button/buttonBar'; -import {LinkButton} from 'sentry/components/core/button/linkButton'; -import {DropdownMenu} from 'sentry/components/dropdownMenu'; -import FeatureFlagSettingsButton from 'sentry/components/events/featureFlags/featureFlagSettingsButton'; -import {useFeatureFlagOnboarding} from 'sentry/components/events/featureFlags/onboarding/useFeatureFlagOnboarding'; -import {IconClose, IconMegaphone} from 'sentry/icons'; -import {t} from 'sentry/locale'; -import {space} from 'sentry/styles/space'; -import type {PlatformKey} from 'sentry/types/project'; -import {trackAnalytics} from 'sentry/utils/analytics'; -import {useFeedbackForm} from 'sentry/utils/useFeedbackForm'; -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; -}) { - const organization = useOrganization(); - const analyticsArea = useAnalyticsArea(); - - useEffect(() => { - trackAnalytics('flags.cta_rendered', { - organization, - surface: analyticsArea, - }); - }, [organization, analyticsArea]); - - return ( - - - {t('Set Up Feature Flags')} - - {t( - 'Want to know which feature flags were associated with this issue? Set up your feature flag integration.' - )} - - - - { - trackAnalytics('flags.cta_read_more_clicked', { - organization, - surface: analyticsArea, - }); - }} - > - {t('Read More')} - - - - - - ); -} - -export default function FeatureFlagInlineCTA({ - projectId, - projectPlatform, -}: { - projectId: string; - projectPlatform?: PlatformKey; -}) { - const organization = useOrganization(); - const analyticsArea = useAnalyticsArea(); - - const {activateSidebar} = useFeatureFlagOnboarding({projectPlatform}); - - const {isLoading, isError, isPromptDismissed, dismissPrompt, snoozePrompt} = usePrompt({ - feature: 'issue_feature_flags_inline_onboarding', - organization, - projectId, - daysToSnooze: 7, - }); - - const openForm = useFeedbackForm(); - const feedbackButton = openForm ? ( - - ) : null; - - if (isLoading || isError || isPromptDismissed) { - return null; - } - - const actions = ( - - {feedbackButton} - - - ); - - return ( - - - - , - }} - size="xs" - items={[ - { - key: 'dismiss', - label: t('Dismiss'), - onAction: () => { - dismissPrompt(); - trackAnalytics('flags.cta_dismissed', { - organization, - type: 'dismiss', - surface: analyticsArea, - }); - }, - }, - { - key: 'snooze', - label: t('Snooze'), - onAction: () => { - snoozePrompt(); - trackAnalytics('flags.cta_dismissed', { - organization, - type: 'snooze', - surface: analyticsArea, - }); - }, - }, - ]} - /> - - - ); -} - -const ActionButton = styled('div')` - display: flex; - gap: ${space(1)}; -`; - -const BannerTitle = styled('div')` - font-size: ${p => p.theme.fontSize.xl}; - margin-bottom: ${space(1)}; - font-weight: ${p => p.theme.fontWeight.bold}; -`; - -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')` - object-fit: contain; - max-width: 30%; - min-width: 150px; - padding-inline: ${space(2)}; - padding-top: ${space(2)}; - align-self: flex-end; -`; - -export const BannerWrapper = styled('div')` - position: relative; - border: 1px solid ${p => p.theme.border}; - border-radius: ${p => p.theme.borderRadius}; - 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)}; - - container-name: bannerWrapper; - container-type: inline-size; - - @container bannerWrapper (max-width: 400px) { - img { - display: none; - } - } -`; - -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/components/events/featureFlags/eventFeatureFlagSection.spec.tsx b/static/app/components/events/featureFlags/eventFeatureFlagSection.spec.tsx index e2bb16d0f80a56..af38e6e4f15364 100644 --- a/static/app/components/events/featureFlags/eventFeatureFlagSection.spec.tsx +++ b/static/app/components/events/featureFlags/eventFeatureFlagSection.spec.tsx @@ -5,7 +5,6 @@ import { render, screen, userEvent, - waitFor, waitForDrawerToHide, } from 'sentry-test/reactTestingLibrary'; @@ -16,9 +15,8 @@ import { MOCK_DATA_SECTION_PROPS_MANY_FLAGS, MOCK_DATA_SECTION_PROPS_ONE_EXTRA_FLAG, MOCK_FLAGS, - NO_FLAG_CONTEXT_SECTION_PROPS_CTA, - NO_FLAG_CONTEXT_SECTION_PROPS_NO_CTA, - NO_FLAG_CONTEXT_WITH_FLAGS_SECTION_PROPS_NO_CTA, + NO_FLAG_CONTEXT_SECTION_PROPS, + NO_FLAG_CONTEXT_WITH_FLAGS_SECTION_PROPS, } from 'sentry/components/events/featureFlags/testUtils'; // Needed to mock useVirtualizer lists. @@ -57,6 +55,7 @@ describe('EventFeatureFlagList', () => { body: TagsFixture(), }); }); + it('renders a list of feature flags with a button to view more flags', async () => { render(); @@ -220,36 +219,13 @@ describe('EventFeatureFlagList', () => { ).toBeInTheDocument(); }); - it('renders cta if event.contexts.flags is not set and should show cta', async () => { - const org = OrganizationFixture({features: ['feature-flag-cta']}); + it('renders empty state if event.contexts.flags is not set - flags already sent', () => { + const org = OrganizationFixture({features: []}); - render(, { + render(, { organization: org, }); - const control = screen.queryByRole('button', {name: 'Sort Flags'}); - expect(control).not.toBeInTheDocument(); - const search = screen.queryByRole('button', {name: 'Open Feature Flag Search'}); - expect(search).not.toBeInTheDocument(); - expect( - screen.queryByRole('button', {name: 'Set Up Integration'}) - ).not.toBeInTheDocument(); - - // wait for the CTA to be rendered - expect(await screen.findByText('Set Up Feature Flags')).toBeInTheDocument(); - expect(screen.getByText('Feature Flags')).toBeInTheDocument(); - }); - - it('renders empty state if event.contexts.flags is not set but should not show cta - flags already sent', () => { - const org = OrganizationFixture({features: ['feature-flag-cta']}); - - render( - , - { - organization: org, - } - ); - const control = screen.queryByRole('button', {name: 'Sort Flags'}); expect(control).not.toBeInTheDocument(); const search = screen.queryByRole('button', {name: 'Open Feature Flag Search'}); @@ -262,32 +238,10 @@ describe('EventFeatureFlagList', () => { ).toBeInTheDocument(); }); - it('renders nothing if event.contexts.flags is not set and should not show cta - wrong platform', async () => { - const org = OrganizationFixture({features: ['feature-flag-cta']}); - - render(, { - organization: org, - }); - - const control = screen.queryByRole('button', {name: 'Sort Flags'}); - expect(control).not.toBeInTheDocument(); - const search = screen.queryByRole('button', {name: 'Open Feature Flag Search'}); - expect(search).not.toBeInTheDocument(); - expect( - screen.queryByRole('button', {name: 'Set Up Integration'}) - ).not.toBeInTheDocument(); - - // CTA should not appear - await waitFor(() => { - expect(screen.queryByText('Set Up Feature Flags')).not.toBeInTheDocument(); - }); - expect(screen.queryByText('Feature Flags')).not.toBeInTheDocument(); - }); - - it('renders nothing if event.contexts.flags is not set and should not show cta - no feature flag', async () => { - const org = OrganizationFixture({features: ['fake-feature-flag']}); + it('renders nothing if event.contexts.flags is not set - wrong platform', () => { + const org = OrganizationFixture({features: []}); - render(, { + render(, { organization: org, }); @@ -295,14 +249,6 @@ describe('EventFeatureFlagList', () => { expect(control).not.toBeInTheDocument(); const search = screen.queryByRole('button', {name: 'Open Feature Flag Search'}); expect(search).not.toBeInTheDocument(); - expect( - screen.queryByRole('button', {name: 'Set Up Integration'}) - ).not.toBeInTheDocument(); - - // CTA should not appear - await waitFor(() => { - expect(screen.queryByText('Set Up Feature Flags')).not.toBeInTheDocument(); - }); expect(screen.queryByText('Feature Flags')).not.toBeInTheDocument(); }); }); diff --git a/static/app/components/events/featureFlags/eventFeatureFlagSection.tsx b/static/app/components/events/featureFlags/eventFeatureFlagSection.tsx index 8bf35839500b0f..bce4239d2ba4b0 100644 --- a/static/app/components/events/featureFlags/eventFeatureFlagSection.tsx +++ b/static/app/components/events/featureFlags/eventFeatureFlagSection.tsx @@ -6,7 +6,6 @@ import AnalyticsArea from 'sentry/components/analyticsArea'; import {Button} from 'sentry/components/core/button'; import {ButtonBar} from 'sentry/components/core/button/buttonBar'; import EmptyStateWarning from 'sentry/components/emptyStateWarning'; -import FeatureFlagInlineCTA from 'sentry/components/events/featureFlags/cta/featureFlagInlineCTA'; import { CardContainer, EventFeatureFlagDrawer, @@ -28,7 +27,6 @@ import {useGroupSuspectFlagScores} from 'sentry/components/issues/suspect/useGro import useLegacyEventSuspectFlags from 'sentry/components/issues/suspect/useLegacyEventSuspectFlags'; import useSuspectFlagScoreThreshold from 'sentry/components/issues/suspect/useSuspectFlagScoreThreshold'; 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'; @@ -252,17 +250,9 @@ function BaseEventFeatureFlagList({event, group, project}: EventFeatureFlagSecti return null; } - // If the project has never ingested flags, either show a CTA or hide the section entirely. + // If the project has never ingested flags, hide the section entirely. if (!hasFlags && !project.hasFlags) { - const showCTA = - featureFlagOnboardingPlatforms.includes(project.platform ?? 'other') && - organization.features.includes('feature-flag-cta'); - return showCTA ? ( - - ) : null; + return null; } const actions = ( diff --git a/static/app/components/events/featureFlags/testUtils.tsx b/static/app/components/events/featureFlags/testUtils.tsx index ef32b93ec64b96..d392b842e1d5db 100644 --- a/static/app/components/events/featureFlags/testUtils.tsx +++ b/static/app/components/events/featureFlags/testUtils.tsx @@ -154,7 +154,7 @@ export const EMPTY_STATE_SECTION_PROPS = { group: GroupFixture(), }; -export const NO_FLAG_CONTEXT_SECTION_PROPS_NO_CTA = { +export const NO_FLAG_CONTEXT_SECTION_PROPS = { event: EventFixture({ id: 'abc123def456ghi789jkl', contexts: {other: {}}, @@ -164,17 +164,7 @@ export const NO_FLAG_CONTEXT_SECTION_PROPS_NO_CTA = { group: GroupFixture({platform: 'unity'}), }; -export const NO_FLAG_CONTEXT_SECTION_PROPS_CTA = { - event: EventFixture({ - id: 'abc123def456ghi789jkl', - contexts: {other: {}}, - platform: 'javascript', - }), - project: ProjectFixture({platform: 'javascript', hasFlags: false}), - group: GroupFixture({platform: 'javascript'}), -}; - -export const NO_FLAG_CONTEXT_WITH_FLAGS_SECTION_PROPS_NO_CTA = { +export const NO_FLAG_CONTEXT_WITH_FLAGS_SECTION_PROPS = { event: EventFixture({ id: 'abc123def456ghi789jkl', contexts: {other: {}}, diff --git a/static/app/views/issueDetails/groupFeatureFlags/flagDrawerCTA.tsx b/static/app/views/issueDetails/groupFeatureFlags/flagDrawerCTA.tsx index 0f395c7e93f0a7..beb8399bf9904e 100644 --- a/static/app/views/issueDetails/groupFeatureFlags/flagDrawerCTA.tsx +++ b/static/app/views/issueDetails/groupFeatureFlags/flagDrawerCTA.tsx @@ -1,7 +1,6 @@ -import { +import FeatureFlagCTAContent, { BannerWrapper, - FeatureFlagCTAContent, -} from 'sentry/components/events/featureFlags/cta/featureFlagInlineCTA'; +} from 'sentry/components/events/featureFlags/cta/featureFlagCTAContent'; import {useFeatureFlagOnboarding} from 'sentry/components/events/featureFlags/onboarding/useFeatureFlagOnboarding'; import {useDrawerContentContext} from 'sentry/components/globalDrawer/components'; import type {PlatformKey} from 'sentry/types/project';