diff --git a/static/app/components/events/featureFlags/eventFeatureFlagList.spec.tsx b/static/app/components/events/featureFlags/eventFeatureFlagList.spec.tsx index 1d8b60d89569c1..bfb0c51acb982c 100644 --- a/static/app/components/events/featureFlags/eventFeatureFlagList.spec.tsx +++ b/static/app/components/events/featureFlags/eventFeatureFlagList.spec.tsx @@ -9,8 +9,10 @@ import { import {EventFeatureFlagList} from 'sentry/components/events/featureFlags/eventFeatureFlagList'; import { + EMPTY_STATE_SECTION_PROPS, MOCK_DATA_SECTION_PROPS, MOCK_FLAGS, + NO_FLAG_CONTEXT_SECTION_PROPS, } from 'sentry/components/events/featureFlags/testUtils'; // Needed to mock useVirtualizer lists. @@ -192,4 +194,30 @@ describe('EventFeatureFlagList', function () { .compareDocumentPosition(screen.getByText(enableReplay.flag)) ).toBe(document.DOCUMENT_POSITION_FOLLOWING); }); + + it('renders empty state', function () { + render(); + + 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.getByRole('button', {name: 'Set Up Integration'})).toBeInTheDocument(); + expect( + screen.queryByText('No feature flags were found for this event') + ).toBeInTheDocument(); + }); + + it('renders nothing if event.contexts.flags is not set', function () { + render(); + + 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(); + expect(screen.queryByText('Feature Flags')).not.toBeInTheDocument(); + }); }); diff --git a/static/app/components/events/featureFlags/eventFeatureFlagList.tsx b/static/app/components/events/featureFlags/eventFeatureFlagList.tsx index ae0c7aba42a188..c553644a127b4a 100644 --- a/static/app/components/events/featureFlags/eventFeatureFlagList.tsx +++ b/static/app/components/events/featureFlags/eventFeatureFlagList.tsx @@ -1,14 +1,20 @@ -import {useCallback, useMemo, useRef, useState} from 'react'; +import {Fragment, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import styled from '@emotion/styled'; +import {openModal} from 'sentry/actionCreators/modal'; import {Button} from 'sentry/components/button'; import ButtonBar from 'sentry/components/buttonBar'; +import EmptyStateWarning from 'sentry/components/emptyStateWarning'; import ErrorBoundary from 'sentry/components/errorBoundary'; import { CardContainer, FeatureFlagDrawer, } from 'sentry/components/events/featureFlags/featureFlagDrawer'; import FeatureFlagSort from 'sentry/components/events/featureFlags/featureFlagSort'; +import { + modalCss, + SetupIntegrationModal, +} from 'sentry/components/events/featureFlags/setupIntegrationModal'; import { FlagControlOptions, OrderBy, @@ -86,6 +92,16 @@ export function EventFeatureFlagList({ event, }); + const hasFlagContext = !!event.contexts.flags; + const hasFlags = Boolean(hasFlagContext && event?.contexts?.flags?.values.length); + + function handleSetupButtonClick() { + trackAnalytics('flags.setup_modal_opened', {organization}); + openModal(modalProps => , { + modalCss, + }); + } + const suspectFlagNames: Set = useMemo(() => { return isSuspectError || isSuspectPending ? new Set() @@ -150,37 +166,63 @@ export function EventFeatureFlagList({ [openDrawer, event, group, project, hydratedFlags, organization, sortBy, orderBy] ); - if (!hydratedFlags.length) { + useEffect(() => { + if (hasFlags) { + trackAnalytics('flags.table_rendered', { + organization, + numFlags: hydratedFlags.length, + }); + } + }, [hasFlags, hydratedFlags.length, organization]); + + // TODO: for LD users, show a CTA in this section instead + // if contexts.flags is not set, hide the section + if (!hasFlagContext) { return null; } const actions = ( {feedbackButton} - - + {hasFlags && ( + + + + + + ); + }, [Footer, handleDone, state]); + + const ModalBody = useCallback( + ({children: bodyChildren}: Parameters['Body']>[0]) => { + return {bodyChildren}; + }, + [Body] + ); + + const onGenerateURL = useCallback(async () => { + const newToken = await createToken(); + const encodedToken = encodeURI(newToken.token); + const provider = state.provider.toLowerCase(); + + setState(prevState => { + return { + ...prevState, + url: `https://sentry.io/api/0/organizations/${organization?.slug}/flags/hooks/provider/${provider}/token/${encodedToken}/`, + }; + }); + + trackAnalytics('flags.webhook_url_generated', {organization}); + }, [createToken, organization, state.provider]); + + const providers = ['LaunchDarkly']; + + return ( + + {t('Set Up Feature Flag Integration')} + + + ({ + value: integration, + label: integration, + }))} + placeholder={t('Select a feature flag service')} + value={state.provider} + onChange={value => setState({...state, provider: value})} + flexibleControlStateSize + stacked + required + /> + + {t('Create Webhook URL')} + + + + {t('Webhook URL')} + + {state.url ?? ''} + + + {t( + 'The final step is to create a Webhook integration within your feature flag service by utilizing the Webhook URL provided in the field above.' + )} + }> + {t('You won’t be able to access this URL once this modal is closed.')} + + + + + + + ); +} + +export const modalCss = css` + width: 100%; + max-width: 680px; +`; + +const StyledButtonBar = styled(ButtonBar)` + display: flex; + width: 100%; + justify-content: space-between; +`; + +const SelectContainer = styled('div')` + display: grid; + grid-template-columns: 1fr max-content; + align-items: center; + gap: ${space(1)}; +`; + +const WebhookButton = styled(Button)` + margin-top: ${space(1)}; +`; + +const WebhookContainer = styled('div')` + display: flex; + flex-direction: column; + gap: ${space(1)}; +`; + +const InfoContainer = styled('div')` + display: flex; + flex-direction: column; + gap: ${space(2)}; + margin-top: ${space(1)}; +`; diff --git a/static/app/components/events/featureFlags/testUtils.tsx b/static/app/components/events/featureFlags/testUtils.tsx index cdc932dab1f628..e4de5c67748a87 100644 --- a/static/app/components/events/featureFlags/testUtils.tsx +++ b/static/app/components/events/featureFlags/testUtils.tsx @@ -31,3 +31,21 @@ export const MOCK_DATA_SECTION_PROPS = { project: ProjectFixture(), group: GroupFixture(), }; + +export const EMPTY_STATE_SECTION_PROPS = { + event: EventFixture({ + id: 'abc123def456ghi789jkl', + contexts: {flags: {values: []}}, + }), + project: ProjectFixture(), + group: GroupFixture(), +}; + +export const NO_FLAG_CONTEXT_SECTION_PROPS = { + event: EventFixture({ + id: 'abc123def456ghi789jkl', + contexts: {other: {}}, + }), + project: ProjectFixture(), + group: GroupFixture(), +}; diff --git a/static/app/utils/analytics/featureFlagAnalyticsEvents.tsx b/static/app/utils/analytics/featureFlagAnalyticsEvents.tsx index 612391a7dea184..ebb162119e9cae 100644 --- a/static/app/utils/analytics/featureFlagAnalyticsEvents.tsx +++ b/static/app/utils/analytics/featureFlagAnalyticsEvents.tsx @@ -4,8 +4,13 @@ export type FeatureFlagEventParameters = { numSuspectFlags: number; numTotalFlags: number; }; + 'flags.setup_modal_opened': {}; 'flags.sort-flags': {sortMethod: string}; + 'flags.table_rendered': { + numFlags: number; + }; 'flags.view-all-clicked': {}; + 'flags.webhook_url_generated': {}; }; export type FeatureFlagEventKey = keyof FeatureFlagEventParameters; @@ -14,4 +19,7 @@ export const featureFlagEventMap: Record = { 'flags.view-all-clicked': 'Clicked View All Flags', 'flags.sort-flags': 'Sorted Flags', 'flags.event_and_suspect_flags_found': 'Number of Event and Suspect Flags', + 'flags.setup_modal_opened': 'Flag Setup Integration Modal Opened', + 'flags.webhook_url_generated': 'Flag Webhook URL Generated in Setup Integration Modal', + 'flags.table_rendered': 'Flag Table Rendered', };