From dcfd3ab202b9a6eec7595d67dc4215315a0178ee Mon Sep 17 00:00:00 2001 From: Michelle Zhang <56095982+michellewzhang@users.noreply.github.com> Date: Fri, 15 Nov 2024 14:59:09 -0800 Subject: [PATCH 1/2] feat(flags): add python onboarding sidebar --- .../featureFlags/eventFeatureFlagList.tsx | 16 +- .../featureFlagOnboardingLayout.tsx | 138 ++++++ .../featureFlagOnboardingSidebar.tsx | 399 ++++++++++++++++++ .../onboardingIntegrationSection.tsx | 132 ++++++ .../featureFlags/setupIntegrationModal.tsx | 215 ---------- .../featureFlags/useFeatureFlagOnboarding.tsx | 29 ++ .../components/events/featureFlags/utils.tsx | 21 + .../gettingStartedDoc/onboardingLayout.tsx | 2 + .../onboarding/gettingStartedDoc/types.ts | 5 +- .../utils/useLoadGettingStarted.tsx | 8 +- static/app/components/sidebar/index.tsx | 7 + static/app/components/sidebar/types.tsx | 1 + static/app/data/platformCategories.tsx | 6 + .../app/gettingStartedDocs/python/python.tsx | 33 ++ .../analytics/featureFlagAnalyticsEvents.tsx | 4 +- 15 files changed, 783 insertions(+), 233 deletions(-) create mode 100644 static/app/components/events/featureFlags/featureFlagOnboardingLayout.tsx create mode 100644 static/app/components/events/featureFlags/featureFlagOnboardingSidebar.tsx create mode 100644 static/app/components/events/featureFlags/onboardingIntegrationSection.tsx delete mode 100644 static/app/components/events/featureFlags/setupIntegrationModal.tsx create mode 100644 static/app/components/events/featureFlags/useFeatureFlagOnboarding.tsx diff --git a/static/app/components/events/featureFlags/eventFeatureFlagList.tsx b/static/app/components/events/featureFlags/eventFeatureFlagList.tsx index 3b8b5ef957577..a3de99c68903f 100644 --- a/static/app/components/events/featureFlags/eventFeatureFlagList.tsx +++ b/static/app/components/events/featureFlags/eventFeatureFlagList.tsx @@ -1,7 +1,6 @@ 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'; @@ -11,10 +10,7 @@ import { 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 {useFeatureFlagOnboarding} from 'sentry/components/events/featureFlags/useFeatureFlagOnboarding'; import { FlagControlOptions, OrderBy, @@ -80,6 +76,7 @@ export function EventFeatureFlagList({ statsPeriod: eventView.statsPeriod, }, }); + const {activateSidebar} = useFeatureFlagOnboarding(); const { suspectFlags, @@ -95,13 +92,6 @@ export function EventFeatureFlagList({ 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() @@ -195,7 +185,7 @@ export function EventFeatureFlagList({ diff --git a/static/app/components/events/featureFlags/featureFlagOnboardingLayout.tsx b/static/app/components/events/featureFlags/featureFlagOnboardingLayout.tsx new file mode 100644 index 0000000000000..43923245adb4e --- /dev/null +++ b/static/app/components/events/featureFlags/featureFlagOnboardingLayout.tsx @@ -0,0 +1,138 @@ +import {useMemo} from 'react'; +import styled from '@emotion/styled'; + +import OnboardingIntegrationSection from 'sentry/components/events/featureFlags/onboardingIntegrationSection'; +import {AuthTokenGeneratorProvider} from 'sentry/components/onboarding/gettingStartedDoc/authTokenGenerator'; +import type {OnboardingLayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/onboardingLayout'; +import {Step, StepType} from 'sentry/components/onboarding/gettingStartedDoc/step'; +import type {DocsParams} from 'sentry/components/onboarding/gettingStartedDoc/types'; +import {useSourcePackageRegistries} from 'sentry/components/onboarding/gettingStartedDoc/useSourcePackageRegistries'; +import {useUrlPlatformOptions} from 'sentry/components/onboarding/platformOptionsControl'; +import ConfigStore from 'sentry/stores/configStore'; +import {useLegacyStore} from 'sentry/stores/useLegacyStore'; +import {space} from 'sentry/styles/space'; +import useApi from 'sentry/utils/useApi'; +import useOrganization from 'sentry/utils/useOrganization'; + +export function FeatureFlagOnboardingLayout({ + docsConfig, + dsn, + platformKey, + projectId, + projectSlug, + newOrg, + projectKeyId, + configType = 'onboarding', + integration = '', + provider = '', +}: OnboardingLayoutProps) { + const api = useApi(); + const organization = useOrganization(); + const {isPending: isLoadingRegistry, data: registryData} = + useSourcePackageRegistries(organization); + const selectedOptions = useUrlPlatformOptions(docsConfig.platformOptions); + const {isSelfHosted, urlPrefix} = useLegacyStore(ConfigStore); + + const {introduction, steps} = useMemo(() => { + const doc = docsConfig[configType] ?? docsConfig.onboarding; + + const docParams: DocsParams = { + api, + projectKeyId, + dsn, + organization, + platformKey, + projectId, + projectSlug, + isFeedbackSelected: false, + isPerformanceSelected: false, + isProfilingSelected: false, + isReplaySelected: false, + sourcePackageRegistries: { + isLoading: isLoadingRegistry, + data: registryData, + }, + platformOptions: selectedOptions, + newOrg, + isSelfHosted, + urlPrefix, + integration, + }; + + return { + introduction: doc.introduction?.(docParams), + steps: [ + ...doc.install(docParams), + ...doc.configure(docParams), + ...doc.verify(docParams), + ], + nextSteps: doc.nextSteps?.(docParams) || [], + }; + }, [ + docsConfig, + dsn, + isLoadingRegistry, + newOrg, + organization, + platformKey, + projectId, + projectSlug, + registryData, + selectedOptions, + configType, + urlPrefix, + isSelfHosted, + api, + projectKeyId, + integration, + ]); + + return ( + + + {introduction && {introduction}} + + {steps.map(step => + step.type === StepType.CONFIGURE ? ( + + ) : ( + + ) + )} + + + + + ); +} + +const Steps = styled('div')` + display: flex; + flex-direction: column; + gap: 1.5rem; +`; + +const Wrapper = styled('div')` + h4 { + margin-bottom: 0.5em; + } + && { + p { + margin-bottom: 0; + } + h5 { + margin-bottom: 0; + } + } +`; + +const Introduction = styled('div')` + display: flex; + flex-direction: column; + margin: 0 0 ${space(2)} 0; +`; diff --git a/static/app/components/events/featureFlags/featureFlagOnboardingSidebar.tsx b/static/app/components/events/featureFlags/featureFlagOnboardingSidebar.tsx new file mode 100644 index 0000000000000..36bf19cd65f44 --- /dev/null +++ b/static/app/components/events/featureFlags/featureFlagOnboardingSidebar.tsx @@ -0,0 +1,399 @@ +import type {ReactNode} from 'react'; +import {Fragment, useMemo, useState} from 'react'; +import styled from '@emotion/styled'; + +import HighlightTopRightPattern from 'sentry-images/pattern/highlight-top-right.svg'; + +import {LinkButton} from 'sentry/components/button'; +import {CompactSelect} from 'sentry/components/compactSelect'; +import {FeatureFlagOnboardingLayout} from 'sentry/components/events/featureFlags/featureFlagOnboardingLayout'; +import {ProviderOptions} from 'sentry/components/events/featureFlags/utils'; +import RadioGroup from 'sentry/components/forms/controls/radioGroup'; +import IdBadge from 'sentry/components/idBadge'; +import ExternalLink from 'sentry/components/links/externalLink'; +import LoadingIndicator from 'sentry/components/loadingIndicator'; +import useCurrentProjectState from 'sentry/components/onboarding/gettingStartedDoc/utils/useCurrentProjectState'; +import {useLoadGettingStarted} from 'sentry/components/onboarding/gettingStartedDoc/utils/useLoadGettingStarted'; +import SidebarPanel from 'sentry/components/sidebar/sidebarPanel'; +import type {CommonSidebarProps} from 'sentry/components/sidebar/types'; +import {SidebarPanelKey} from 'sentry/components/sidebar/types'; +import TextOverflow from 'sentry/components/textOverflow'; +import {featureFlagOnboardingPlatforms} from 'sentry/data/platformCategories'; +import platforms, {otherPlatform} from 'sentry/data/platforms'; +import {t, tct} from 'sentry/locale'; +import {space} from 'sentry/styles/space'; +import type {SelectValue} from 'sentry/types/core'; +import type {Project} from 'sentry/types/project'; +import useOrganization from 'sentry/utils/useOrganization'; +import useUrlParams from 'sentry/utils/useUrlParams'; + +function FeatureFlagOnboardingSidebar(props: CommonSidebarProps) { + const {currentPanel, collapsed, hidePanel, orientation} = props; + const organization = useOrganization(); + + const isActive = currentPanel === SidebarPanelKey.FEATURE_FLAG_ONBOARDING; + const hasProjectAccess = organization.access.includes('project:read'); + + const { + hasDocs, + projects, + allProjects, + currentProject, + setCurrentProject, + supportedProjects, + unsupportedProjects, + } = useCurrentProjectState({ + currentPanel, + targetPanel: SidebarPanelKey.FEATURE_FLAG_ONBOARDING, + onboardingPlatforms: featureFlagOnboardingPlatforms, + allPlatforms: featureFlagOnboardingPlatforms, + }); + + const projectSelectOptions = useMemo(() => { + const supportedProjectItems: SelectValue[] = supportedProjects.map( + project => { + return { + value: project.id, + textValue: project.id, + label: ( + + ), + }; + } + ); + + const unsupportedProjectItems: SelectValue[] = unsupportedProjects.map( + project => { + return { + value: project.id, + textValue: project.id, + label: ( + + ), + disabled: true, + }; + } + ); + return [ + { + label: t('Supported'), + options: supportedProjectItems, + }, + { + label: t('Unsupported'), + options: unsupportedProjectItems, + }, + ]; + }, [supportedProjects, unsupportedProjects]); + + const selectedProject = currentProject ?? projects[0] ?? allProjects[0]; + if (!isActive || !hasProjectAccess || !selectedProject) { + return null; + } + + return ( + + + + {t('Debug Issues with Feature Flag Context')} + +
{ + // we need to stop bubbling the CompactSelect click event + // failing to do so will cause the sidebar panel to close + // the event.target will be unmounted by the time the panel listener + // receives the event and assume the click was outside the panel + e.stopPropagation(); + }} + > + + ) : ( + t('Select a project') + ) + } + value={currentProject?.id} + onChange={opt => + setCurrentProject(allProjects.find(p => p.id === opt.value)) + } + triggerProps={{'aria-label': currentProject?.slug}} + options={projectSelectOptions} + position="bottom-end" + /> +
+
+ +
+
+ ); +} + +function OnboardingContent({ + currentProject, + hasDocs, +}: { + currentProject: Project; + hasDocs: boolean; +}) { + const organization = useOrganization(); + const openFeatureProviders = [ProviderOptions.LAUNCHDARKLY]; + const sdkProviders = [ProviderOptions.LAUNCHDARKLY]; + + // First dropdown: OpenFeature providers + const openFeatureProviderOptions = openFeatureProviders.map(provider => { + return { + value: provider, + textValue: provider, + label: {provider}, + }; + }); + + const [openFeatureProvider, setOpenFeatureProvider] = useState<{ + value: string; + label?: ReactNode; + textValue?: string; + }>(openFeatureProviderOptions[0]); + + // Second dropdown: other SDK providers + const sdkProviderOptions = sdkProviders.map(provider => { + return { + value: provider, + textValue: provider, + label: {provider}, + }; + }); + + const [sdkProvider, setsdkProvider] = useState<{ + value: string; + label?: ReactNode; + textValue?: string; + }>(sdkProviderOptions[0]); + + const defaultTab: string = 'openFeature'; + const {getParamValue: setupMode, setParamValue: setSetupMode} = useUrlParams( + 'mode', + defaultTab + ); + + const currentPlatform = currentProject.platform + ? platforms.find(p => p.id === currentProject.platform) ?? otherPlatform + : otherPlatform; + + const { + docs, + dsn, + isLoading: isProjKeysLoading, + projectKeyId, + } = useLoadGettingStarted({ + platform: currentPlatform, + projSlug: currentProject.slug, + orgSlug: organization.slug, + productType: 'featureFlags', + }); + + const radioButtons = ( +
+ + {tct( + 'I use the [link:OpenFeature] SDK using a provider from [providerSelect]', + { + providerSelect: ( + + ), + link: , // TODO: link + } + )} + , + ], + [ + 'other', + + {tct('I use an SDK from [providerSelect]', { + providerSelect: ( + + ), + })} + , + ], + ]} + value={setupMode()} + onChange={setSetupMode} + /> +
+ ); + + if (isProjKeysLoading) { + return ( + + {radioButtons} + + + ); + } + + const doesNotSupportFeatureFlags = currentProject.platform + ? !featureFlagOnboardingPlatforms.includes(currentProject.platform) + : true; + + if (doesNotSupportFeatureFlags) { + return ( + +
+ {tct( + 'Feature Flags isn’t available for your [platform] project. It is currently only available for Python and JavaScript projects.', + {platform: currentPlatform?.name || currentProject.slug} + )} +
+
+ + {t('Go to Sentry Documentation')} + +
+
+ ); + } + + // No platform, docs import failed, no DSN, or the platform doesn't have onboarding yet + if (!currentPlatform || !docs || !dsn || !hasDocs || !projectKeyId) { + return ( + +
+ {tct( + 'Fiddlesticks. This checklist isn’t available for your [project] project yet, but for now, go to Sentry docs for installation details.', + {project: currentProject.slug} + )} +
+
+ + {t('Read Docs')} + +
+
+ ); + } + + return ( + + {radioButtons} + + + ); +} + +const TaskSidebarPanel = styled(SidebarPanel)` + width: 600px; + max-width: 100%; +`; + +const TopRightBackgroundImage = styled('img')` + position: absolute; + top: 0; + right: 0; + width: 60%; + user-select: none; +`; + +const TaskList = styled('div')` + display: grid; + grid-auto-flow: row; + grid-template-columns: 100%; + gap: ${space(1)}; + margin: 50px ${space(4)} ${space(4)} ${space(4)}; +`; + +const Heading = styled('div')` + display: flex; + color: ${p => p.theme.activeText}; + font-size: ${p => p.theme.fontSizeExtraSmall}; + text-transform: uppercase; + font-weight: ${p => p.theme.fontWeightBold}; + line-height: 1; + margin-top: ${space(3)}; +`; + +const StyledIdBadge = styled(IdBadge)` + overflow: hidden; + white-space: nowrap; + flex-shrink: 1; +`; + +const HeaderActions = styled('div')` + display: flex; + flex-direction: row; + justify-content: space-between; + gap: ${space(3)}; +`; + +const PlatformSelect = styled('div')` + display: flex; + gap: ${space(1)}; + align-items: center; + flex-wrap: wrap; +`; + +const StyledRadioGroup = styled(RadioGroup)` + padding: ${space(1)} 0; +`; + +const Header = styled('div')` + padding: ${space(1)} 0; +`; + +export default FeatureFlagOnboardingSidebar; diff --git a/static/app/components/events/featureFlags/onboardingIntegrationSection.tsx b/static/app/components/events/featureFlags/onboardingIntegrationSection.tsx new file mode 100644 index 0000000000000..0755205ec2081 --- /dev/null +++ b/static/app/components/events/featureFlags/onboardingIntegrationSection.tsx @@ -0,0 +1,132 @@ +import {Fragment, useState} from 'react'; +import styled from '@emotion/styled'; + +import Alert from 'sentry/components/alert'; +import {Button} from 'sentry/components/button'; +import Input from 'sentry/components/input'; +import TextCopyInput from 'sentry/components/textCopyInput'; +import {IconCheckmark} from 'sentry/icons'; +import {t} from 'sentry/locale'; +import {space} from 'sentry/styles/space'; +import useApi from 'sentry/utils/useApi'; +import useOrganization from 'sentry/utils/useOrganization'; + +function _usePostSecret({ + orgSlug, + provider, +}: { + orgSlug: string | undefined; + provider: string; +}) { + const api = useApi(); + + const postSecret = async (secret: string) => + await api.requestPromise( + `/organizations/${orgSlug}/flags/hooks/provider/${provider.toLowerCase()}/signing-secret/`, + { + method: 'POST', + data: { + secret: secret, + }, + } + ); + + return {postSecret}; +} + +export default function OnboardingIntegrationSection({ + provider, + integration, +}: { + integration: string; + provider: string; +}) { + const organization = useOrganization(); + const [tokenSaved, setTokenSaved] = useState(false); + // const {postSecret} = usePostSecret({provider, orgSlug: organization?.slug}); + const [secret, setSecret] = useState(''); + const [storedProvider, setStoredProvider] = useState(provider); + const [storedIntegration, setStoredIntegration] = useState(integration); + + if (provider !== storedProvider || integration !== storedIntegration) { + setStoredProvider(provider); + setStoredIntegration(integration); + setSecret(''); + setTokenSaved(false); + } + + return ( + +

{t('Integrate Feature Flag Service')}

+ {} + + + {t('Signing Secret')} + + setSecret(e.target.value)} + /> + + + {tokenSaved ? ( + }> + {t('Secret token verified.')} + + ) : null} + + + {t( + 'Once the token is saved, go back to your feature flag service and create a webhook integration using the URL provided below.' + )} + {t('Webhook URL')} + + {`https://sentry.io/api/0/organizations/${organization.slug}/flags/hooks/provider/${provider.toLowerCase()}/`} + + + +
+ ); +} + +const InputTitle = styled('div')` + font-weight: bold; +`; + +const InputArea = styled('div')` + display: flex; + flex-direction: row; + gap: ${space(1)}; + align-items: center; +`; + +const IntegrationSection = styled('div')` + display: flex; + flex-direction: column; + gap: ${space(3)}; + margin: ${space(3)} 0; +`; + +const SubSection = styled('div')` + display: flex; + gap: ${space(1)}; + flex-direction: column; +`; + +const StyledAlert = styled(Alert)` + margin: ${space(1.5)} 0 0 0; +`; diff --git a/static/app/components/events/featureFlags/setupIntegrationModal.tsx b/static/app/components/events/featureFlags/setupIntegrationModal.tsx deleted file mode 100644 index aa66ca627d75e..0000000000000 --- a/static/app/components/events/featureFlags/setupIntegrationModal.tsx +++ /dev/null @@ -1,215 +0,0 @@ -import {Fragment, useCallback, useState} from 'react'; -import {css} from '@emotion/react'; -import styled from '@emotion/styled'; - -import type {ModalRenderProps} from 'sentry/actionCreators/modal'; -import Alert from 'sentry/components/alert'; -import {Button, LinkButton} from 'sentry/components/button'; -import ButtonBar from 'sentry/components/buttonBar'; -import SelectField from 'sentry/components/forms/fields/selectField'; -import type {Data} from 'sentry/components/forms/types'; -import TextCopyInput from 'sentry/components/textCopyInput'; -import {IconWarning} from 'sentry/icons'; -import {t} from 'sentry/locale'; -import OrganizationStore from 'sentry/stores/organizationStore'; -import {useLegacyStore} from 'sentry/stores/useLegacyStore'; -import {space} from 'sentry/styles/space'; -import {defined} from 'sentry/utils'; -import useApi from 'sentry/utils/useApi'; - -export type ChildrenProps = { - Body: (props: {children: React.ReactNode}) => ReturnType; - Footer: () => ReturnType; - Header: (props: {children: React.ReactNode}) => ReturnType; - state: T; -}; - -interface State { - provider: string; - url: string | undefined; -} - -function useGenerateAuthToken({ - state, - orgSlug, -}: { - orgSlug: string | undefined; - state: State; -}) { - const api = useApi(); - const date = new Date().toISOString(); - - const createToken = async () => - await api.requestPromise(`/organizations/${orgSlug}/org-auth-tokens/`, { - method: 'POST', - data: { - name: `${state.provider} Token ${date}`, - }, - }); - - return {createToken}; -} - -export function SetupIntegrationModal({ - Header, - Body, - Footer, - closeModal, -}: ModalRenderProps) { - const [state, setState] = useState({ - provider: 'LaunchDarkly', - url: undefined, - }); - const {organization} = useLegacyStore(OrganizationStore); - const {createToken} = useGenerateAuthToken({state, orgSlug: organization?.slug}); - - const handleDone = useCallback(() => { - closeModal(); - }, [closeModal]); - - const ModalHeader = useCallback( - ({children: headerChildren}: {children: React.ReactNode}) => { - return ( -
-

{headerChildren}

-
- ); - }, - [Header] - ); - - const ModalFooter = useCallback(() => { - return ( -
- - - {t('Read Docs')} - - - -
- ); - }, [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}/`, - }; - }); - }, [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/useFeatureFlagOnboarding.tsx b/static/app/components/events/featureFlags/useFeatureFlagOnboarding.tsx new file mode 100644 index 0000000000000..5140f4bec3cd7 --- /dev/null +++ b/static/app/components/events/featureFlags/useFeatureFlagOnboarding.tsx @@ -0,0 +1,29 @@ +import {useCallback, useEffect} from 'react'; + +import {SidebarPanelKey} from 'sentry/components/sidebar/types'; +import SidebarPanelStore from 'sentry/stores/sidebarPanelStore'; +import {trackAnalytics} from 'sentry/utils/analytics'; +import {useLocation} from 'sentry/utils/useLocation'; +import useOrganization from 'sentry/utils/useOrganization'; + +export function useFeatureFlagOnboarding() { + const location = useLocation(); + const organization = useOrganization(); + + useEffect(() => { + if (location.hash === '#flag-sidequest') { + SidebarPanelStore.activatePanel(SidebarPanelKey.FEATURE_FLAG_ONBOARDING); + trackAnalytics('flags.view-setup-sidebar', { + organization, + }); + } + }, [location.hash, organization]); + + const activateSidebar = useCallback((event: {preventDefault: () => void}) => { + event.preventDefault(); + window.location.hash = 'flag-sidequest'; + SidebarPanelStore.activatePanel(SidebarPanelKey.FEATURE_FLAG_ONBOARDING); + }, []); + + return {activateSidebar}; +} diff --git a/static/app/components/events/featureFlags/utils.tsx b/static/app/components/events/featureFlags/utils.tsx index a560b54d3d360..03857cfab98ad 100644 --- a/static/app/components/events/featureFlags/utils.tsx +++ b/static/app/components/events/featureFlags/utils.tsx @@ -112,3 +112,24 @@ export const sortedFlags = ({ return flags; } }; + +export enum ProviderOptions { + LAUNCHDARKLY = 'LaunchDarkly', + OPENFEATURE = 'OpenFeature', +} + +type Labels = { + pythonIntegration: string; // what's in the integrations array + pythonModule: string; // what's imported from sentry_sdk.integrations +}; + +export const PROVIDER_OPTION_TO_LABELS: Record = { + [ProviderOptions.LAUNCHDARKLY]: { + pythonModule: 'launchdarkly', + pythonIntegration: 'LaunchDarklyIntegration', + }, + [ProviderOptions.OPENFEATURE]: { + pythonModule: 'OpenFeature', + pythonIntegration: 'OpenFeatureIntegration', + }, +}; diff --git a/static/app/components/onboarding/gettingStartedDoc/onboardingLayout.tsx b/static/app/components/onboarding/gettingStartedDoc/onboardingLayout.tsx index a5f467c3c23f9..6af245364eca7 100644 --- a/static/app/components/onboarding/gettingStartedDoc/onboardingLayout.tsx +++ b/static/app/components/onboarding/gettingStartedDoc/onboardingLayout.tsx @@ -44,7 +44,9 @@ export type OnboardingLayoutProps = { projectSlug: Project['slug']; activeProductSelection?: ProductSolution[]; configType?: ConfigType; + integration?: string; newOrg?: boolean; + provider?: string; }; const EMPTY_ARRAY: never[] = []; diff --git a/static/app/components/onboarding/gettingStartedDoc/types.ts b/static/app/components/onboarding/gettingStartedDoc/types.ts index 81f16159610ca..182fdfb6986b1 100644 --- a/static/app/components/onboarding/gettingStartedDoc/types.ts +++ b/static/app/components/onboarding/gettingStartedDoc/types.ts @@ -67,6 +67,7 @@ export interface DocsParams< name?: boolean; screenshot?: boolean; }; + integration?: string; newOrg?: boolean; profilingOptions?: { defaultProfilingMode?: 'transaction' | 'continuous'; @@ -106,6 +107,7 @@ export interface Docs; crashReportOnboarding?: OnboardingConfig; customMetricsOnboarding?: OnboardingConfig; + featureFlagOnboarding?: OnboardingConfig; feedbackOnboardingCrashApi?: OnboardingConfig; feedbackOnboardingNpm?: OnboardingConfig; performanceOnboarding?: OnboardingConfig; @@ -122,4 +124,5 @@ export type ConfigType = | 'crashReportOnboarding' | 'replayOnboarding' | 'replayOnboardingJsLoader' - | 'customMetricsOnboarding'; + | 'customMetricsOnboarding' + | 'featureFlagOnboarding'; diff --git a/static/app/components/onboarding/gettingStartedDoc/utils/useLoadGettingStarted.tsx b/static/app/components/onboarding/gettingStartedDoc/utils/useLoadGettingStarted.tsx index 74d2998883592..9c66df925109c 100644 --- a/static/app/components/onboarding/gettingStartedDoc/utils/useLoadGettingStarted.tsx +++ b/static/app/components/onboarding/gettingStartedDoc/utils/useLoadGettingStarted.tsx @@ -3,6 +3,7 @@ import * as Sentry from '@sentry/react'; import type {Docs} from 'sentry/components/onboarding/gettingStartedDoc/types'; import { + featureFlagOnboardingPlatforms, feedbackOnboardingPlatforms, replayPlatforms, withPerformanceOnboarding, @@ -15,7 +16,7 @@ import {useProjectKeys} from 'sentry/utils/useProjectKeys'; type Props = { orgSlug: Organization['slug']; platform: PlatformIntegration; - productType?: 'feedback' | 'replay' | 'performance'; + productType?: 'feedback' | 'replay' | 'performance' | 'featureFlags'; projSlug?: Project['slug']; }; @@ -45,7 +46,10 @@ export function useLoadGettingStarted({ !platformPath || (productType === 'replay' && !replayPlatforms.includes(platform.id)) || (productType === 'performance' && !withPerformanceOnboarding.has(platform.id)) || - (productType === 'feedback' && !feedbackOnboardingPlatforms.includes(platform.id)) + (productType === 'feedback' && + !feedbackOnboardingPlatforms.includes(platform.id)) || + (productType === 'featureFlags' && + !featureFlagOnboardingPlatforms.includes(platform.id)) ) { setModule('none'); return; diff --git a/static/app/components/sidebar/index.tsx b/static/app/components/sidebar/index.tsx index 06a3368b62f1c..f3804c4028f18 100644 --- a/static/app/components/sidebar/index.tsx +++ b/static/app/components/sidebar/index.tsx @@ -6,6 +6,7 @@ import {hideSidebar, showSidebar} from 'sentry/actionCreators/preferences'; import Feature from 'sentry/components/acl/feature'; import GuideAnchor from 'sentry/components/assistant/guideAnchor'; import {Chevron} from 'sentry/components/chevron'; +import FeatureFlagOnboardingSidebar from 'sentry/components/events/featureFlags/featureFlagOnboardingSidebar'; import FeedbackOnboardingSidebar from 'sentry/components/feedback/feedbackOnboarding/sidebar'; import Hook from 'sentry/components/hook'; import {OnboardingContext} from 'sentry/components/onboarding/onboardingContext'; @@ -789,6 +790,12 @@ function Sidebar() { hidePanel={hidePanel} {...sidebarItemProps} /> + togglePanel(SidebarPanelKey.FEATURE_FLAG_ONBOARDING)} + hidePanel={hidePanel} + {...sidebarItemProps} + /> togglePanel(SidebarPanelKey.PROFILING_ONBOARDING)} diff --git a/static/app/components/sidebar/types.tsx b/static/app/components/sidebar/types.tsx index 5fe8c9728688c..91f826cda24e9 100644 --- a/static/app/components/sidebar/types.tsx +++ b/static/app/components/sidebar/types.tsx @@ -9,6 +9,7 @@ export enum SidebarPanelKey { PROFILING_ONBOARDING = 'profiling_onboarding', METRICS_ONBOARDING = 'metrics_onboarding', FEEDBACK_ONBOARDING = 'feedback_onboarding', + FEATURE_FLAG_ONBOARDING = 'flag_onboarding', } export type CommonSidebarProps = { diff --git a/static/app/data/platformCategories.tsx b/static/app/data/platformCategories.tsx index 22e25ad2ccdd8..f8558bad0174d 100644 --- a/static/app/data/platformCategories.tsx +++ b/static/app/data/platformCategories.tsx @@ -562,6 +562,12 @@ export const feedbackOnboardingPlatforms: readonly PlatformKey[] = [ ...feedbackCrashApiPlatforms, ]; +// Feature flag onboarding platforms +export const featureFlagOnboardingPlatforms: readonly PlatformKey[] = [ + // 'javascript', + 'python', +]; + const customMetricBackendPlatforms: readonly PlatformKey[] = [ 'bun', 'dart', diff --git a/static/app/gettingStartedDocs/python/python.tsx b/static/app/gettingStartedDocs/python/python.tsx index df866857112ac..633a41930d67e 100644 --- a/static/app/gettingStartedDocs/python/python.tsx +++ b/static/app/gettingStartedDocs/python/python.tsx @@ -1,3 +1,4 @@ +import {PROVIDER_OPTION_TO_LABELS} from 'sentry/components/events/featureFlags/utils'; import ExternalLink from 'sentry/components/links/externalLink'; import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step'; import { @@ -214,6 +215,37 @@ export function AlternativeConfiguration() { ); } +export const featureFlagOnboarding: OnboardingConfig = { + install: onboarding.install, + configure: ({integration = '', dsn}) => [ + { + type: StepType.CONFIGURE, + description: tct('Add [name] to your integrations list.', { + name: ( + {`${PROVIDER_OPTION_TO_LABELS[integration].pythonIntegration}()`} + ), + }), + configurations: [ + { + language: 'python', + code: ` +import sentry-sdk +from sentry_sdk.integrations.${PROVIDER_OPTION_TO_LABELS[integration].pythonModule} import ${PROVIDER_OPTION_TO_LABELS[integration].pythonIntegration} + +sentry_sdk.init( + dsn="${dsn.public}", + integrations=[ + ${PROVIDER_OPTION_TO_LABELS[integration].pythonIntegration}(), + ] +)`, + }, + ], + }, + ], + verify: () => [], + nextSteps: () => [], +}; + const docs: Docs = { onboarding, performanceOnboarding, @@ -221,6 +253,7 @@ const docs: Docs = { installSnippet: getInstallSnippet(), }), crashReportOnboarding: crashReportOnboardingPython, + featureFlagOnboarding, }; export default docs; diff --git a/static/app/utils/analytics/featureFlagAnalyticsEvents.tsx b/static/app/utils/analytics/featureFlagAnalyticsEvents.tsx index 813662657d872..07a811d7a237c 100644 --- a/static/app/utils/analytics/featureFlagAnalyticsEvents.tsx +++ b/static/app/utils/analytics/featureFlagAnalyticsEvents.tsx @@ -4,12 +4,12 @@ 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.view-setup-sidebar': {}; }; export type FeatureFlagEventKey = keyof FeatureFlagEventParameters; @@ -18,6 +18,6 @@ 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.table_rendered': 'Flag Table Rendered', + 'flags.view-setup-sidebar': 'Viewed Feature Flag Onboarding Sidebar', }; From 9dfbc5a6629127614e04b8e95b9b280ccd8e1ea1 Mon Sep 17 00:00:00 2001 From: Michelle Zhang <56095982+michellewzhang@users.noreply.github.com> Date: Fri, 15 Nov 2024 15:11:08 -0800 Subject: [PATCH 2/2] :recycle: disable button if no secret --- .../events/featureFlags/onboardingIntegrationSection.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/static/app/components/events/featureFlags/onboardingIntegrationSection.tsx b/static/app/components/events/featureFlags/onboardingIntegrationSection.tsx index 0755205ec2081..b4133aff25ca7 100644 --- a/static/app/components/events/featureFlags/onboardingIntegrationSection.tsx +++ b/static/app/components/events/featureFlags/onboardingIntegrationSection.tsx @@ -75,6 +75,7 @@ export default function OnboardingIntegrationSection({ // postSecret(secret); setTokenSaved(true); }} + disabled={secret === ''} > {t('Save')}