Skip to content

Commit

Permalink
feat(flags): add javascript onboarding (#81078)
Browse files Browse the repository at this point in the history
adds javascript onboarding to the feature flag sidebar. also, if we
detect that `event.contexts.flags` is not undefined, we hide the first
half of the onboarding as specified in the designs

### updated copy 
<img width="394" alt="SCR-20241121-koun"
src="https://github.com/user-attachments/assets/0c690716-dfea-4b5b-9df6-b63e347de0eb">


### no integration yet 

<img width="395" alt="SCR-20241120-mhqa"
src="https://github.com/user-attachments/assets/5a832b85-9eb1-4496-ac29-a61693791f44">
<img width="394" alt="SCR-20241120-mhrb"
src="https://github.com/user-attachments/assets/fe9cd906-00b2-4276-81ba-8262689ab9a7">


https://github.com/user-attachments/assets/3fd3eade-aac4-4635-9fc9-ab5a71886651

### integration detected


https://github.com/user-attachments/assets/7c42a145-2a03-40ab-b066-279edeeed140

<img width="391" alt="SCR-20241120-mrni"
src="https://github.com/user-attachments/assets/09077c47-248b-4fd8-ad06-df293e9b5cdf">



- relates to
getsentry/team-replay#502

### todo: 
- [x] analytics charts
- [ ] figure out CTA details (another way the sidebar can be triggered)
  • Loading branch information
michellewzhang authored Nov 21, 2024
1 parent cef6f14 commit 2e5e065
Show file tree
Hide file tree
Showing 9 changed files with 203 additions and 61 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import type {Project} from 'sentry/types/project';
import {trackAnalytics} from 'sentry/utils/analytics';
import {useFeedbackForm} from 'sentry/utils/useFeedbackForm';
import useOrganization from 'sentry/utils/useOrganization';
import useUrlParams from 'sentry/utils/useUrlParams';
import {SectionKey} from 'sentry/views/issueDetails/streamline/context';
import {InterimSection} from 'sentry/views/issueDetails/streamline/interimSection';
import {useIssueDetailsEventView} from 'sentry/views/issueDetails/streamline/useIssueDetailsDiscoverQuery';
Expand Down Expand Up @@ -76,7 +77,12 @@ export function EventFeatureFlagList({
statsPeriod: eventView.statsPeriod,
},
});
const {activateSidebar} = useFeatureFlagOnboarding();
const {activateSidebarSkipConfigure} = useFeatureFlagOnboarding();
const {setParamValue: setProjectId} = useUrlParams('project');

useEffect(() => {
setProjectId(event.projectID);
}, [setProjectId, event.projectID]);

const {
suspectFlags,
Expand Down Expand Up @@ -185,7 +191,7 @@ export function EventFeatureFlagList({
<Button
aria-label={t('Set Up Integration')}
size="xs"
onClick={activateSidebar}
onClick={activateSidebarSkipConfigure}
>
{t('Set Up Integration')}
</Button>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
import {useMemo} from 'react';
import {useMemo, useState} from 'react';
import styled from '@emotion/styled';

import Alert from 'sentry/components/alert';
import {Button} from 'sentry/components/button';
import {Flex} from 'sentry/components/container/flex';
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} 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 {t} from 'sentry/locale';
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';

interface FeatureFlagOnboardingLayoutProps extends OnboardingLayoutProps {
integration?: string;
provider?: string;
skipConfig?: boolean;
}

export function FeatureFlagOnboardingLayout({
Expand All @@ -28,13 +34,15 @@ export function FeatureFlagOnboardingLayout({
configType = 'onboarding',
integration = '',
provider = '',
skipConfig,
}: FeatureFlagOnboardingLayoutProps) {
const api = useApi();
const organization = useOrganization();
const {isPending: isLoadingRegistry, data: registryData} =
useSourcePackageRegistries(organization);
const selectedOptions = useUrlPlatformOptions(docsConfig.platformOptions);
const {isSelfHosted, urlPrefix} = useLegacyStore(ConfigStore);
const [skipSteps, setSkipSteps] = useState(skipConfig);

const {steps} = useMemo(() => {
const doc = docsConfig[configType] ?? docsConfig.onboarding;
Expand Down Expand Up @@ -87,11 +95,23 @@ export function FeatureFlagOnboardingLayout({
return (
<AuthTokenGeneratorProvider projectSlug={projectSlug}>
<Wrapper>
<Steps>
{steps.map(step => (
<Step key={step.title ?? step.type} {...step} />
))}
</Steps>
{!skipConfig ? null : (
<Alert type="info" showIcon>
<Flex gap={space(3)}>
{t('Feature flag integration detected. Please follow the remaining steps.')}
<Button onClick={() => setSkipSteps(!skipSteps)}>
{skipSteps ? t('Show Full Guide') : t('Hide Full Guide')}
</Button>
</Flex>
</Alert>
)}
{!skipSteps && (
<Steps>
{steps.map(step => (
<Step key={step.title ?? step.type} {...step} />
))}
</Steps>
)}
<OnboardingIntegrationSection provider={provider} integration={integration} />
</Wrapper>
</AuthTokenGeneratorProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import HighlightTopRightPattern from 'sentry-images/pattern/highlight-top-right.
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 {FLAG_HASH_SKIP_CONFIG} from 'sentry/components/events/featureFlags/useFeatureFlagOnboarding';
import {
IntegrationOptions,
ProviderOptions,
} from 'sentry/components/events/featureFlags/utils';
import RadioGroup from 'sentry/components/forms/controls/radioGroup';
import IdBadge from 'sentry/components/idBadge';
import LoadingIndicator from 'sentry/components/loadingIndicator';
Expand Down Expand Up @@ -146,6 +150,13 @@ function OnboardingContent({
hasDocs: boolean;
}) {
const organization = useOrganization();

// useMemo is needed to remember the original hash
// in case window.location.hash disappears
const ORIGINAL_HASH = useMemo(() => {
return window.location.hash;
}, []);
const skipConfig = ORIGINAL_HASH === FLAG_HASH_SKIP_CONFIG;
const openFeatureProviders = [ProviderOptions.LAUNCHDARKLY];
const sdkProviders = [ProviderOptions.LAUNCHDARKLY];

Expand Down Expand Up @@ -244,7 +255,10 @@ function OnboardingContent({
],
]}
value={setupMode()}
onChange={setSetupMode}
onChange={value => {
setSetupMode(value);
window.location.hash = ORIGINAL_HASH;
}}
/>
</Header>
);
Expand Down Expand Up @@ -311,6 +325,7 @@ function OnboardingContent({
<Fragment>
{radioButtons}
<FeatureFlagOnboardingLayout
skipConfig={skipConfig}
docsConfig={docs}
dsn={dsn}
projectKeyId={projectKeyId}
Expand All @@ -320,7 +335,9 @@ function OnboardingContent({
projectSlug={currentProject.slug}
integration={
// either OpenFeature or the SDK selected from the second dropdown
setupMode() === 'openFeature' ? ProviderOptions.OPENFEATURE : sdkProvider.value
setupMode() === 'openFeature'
? IntegrationOptions.OPENFEATURE
: sdkProvider.value
}
provider={
// dropdown value (from either dropdown)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import styled from '@emotion/styled';

import Alert from 'sentry/components/alert';
import {Button} from 'sentry/components/button';
import {PROVIDER_OPTION_TO_URLS} from 'sentry/components/events/featureFlags/utils';
import Input from 'sentry/components/input';
import ExternalLink from 'sentry/components/links/externalLink';
import TextCopyInput from 'sentry/components/textCopyInput';
import {IconCheckmark} from 'sentry/icons';
import {t} from 'sentry/locale';
import {t, tct} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import useApi from 'sentry/utils/useApi';
import useOrganization from 'sentry/utils/useOrganization';
Expand Down Expand Up @@ -60,12 +62,33 @@ export default function OnboardingIntegrationSection({
<h4 style={{marginTop: space(4)}}>{t('Integrate Feature Flag Service')}</h4>
<IntegrationSection>
<SubSection>
<InputTitle>{t('Signing Secret')}</InputTitle>
<div>
{tct(
"Create a webhook integration with your [link:feature flag service]. When you do so, you'll need to enter a URL, which you can find below.",
{link: <ExternalLink href={PROVIDER_OPTION_TO_URLS[provider]} />}
)}
</div>
<InputTitle>{t('Webhook URL')}</InputTitle>
<TextCopyInput
style={{padding: '20px'}}
aria-label={t('Webhook URL')}
size="sm"
>
{`https://sentry.io/api/0/organizations/${organization.slug}/flags/hooks/provider/${provider.toLowerCase()}/`}
</TextCopyInput>
</SubSection>
<SubSection>
<div>
{t(
"During the process of creating a webhook integration, you'll be given the option to sign the webhook. This is an auto-generated secret code that Sentry requires to verify requests from your feature flag service. Paste the secret below."
)}
</div>
<InputTitle>{t('Secret')}</InputTitle>
<InputArea>
<Input
value={secret}
type="text"
placeholder={t('Signing Secret')}
placeholder={t('Secret')}
onChange={e => setSecret(e.target.value)}
/>
<Button
Expand All @@ -76,7 +99,7 @@ export default function OnboardingIntegrationSection({
}}
disabled={secret === ''}
>
{t('Save')}
{t('Save Secret')}
</Button>
</InputArea>
{tokenSaved ? (
Expand All @@ -85,19 +108,6 @@ export default function OnboardingIntegrationSection({
</StyledAlert>
) : null}
</SubSection>
<SubSection>
{t(
'Once the token is saved, go back to your feature flag service and create a webhook integration using the URL provided below.'
)}
<InputTitle>{t('Webhook URL')}</InputTitle>
<TextCopyInput
style={{padding: '20px'}}
aria-label={t('Webhook URL')}
size="sm"
>
{`https://sentry.io/api/0/organizations/${organization.slug}/flags/hooks/provider/${provider.toLowerCase()}/`}
</TextCopyInput>
</SubSection>
</IntegrationSection>
</Fragment>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,35 @@ import {trackAnalytics} from 'sentry/utils/analytics';
import {useLocation} from 'sentry/utils/useLocation';
import useOrganization from 'sentry/utils/useOrganization';

const FLAG_HASH = '#flag-sidequest';
export const FLAG_HASH_SKIP_CONFIG = '#flag-sidequest-skip';

export function useFeatureFlagOnboarding() {
const location = useLocation();
const organization = useOrganization();

useEffect(() => {
if (location.hash === '#flag-sidequest') {
if (location.hash === FLAG_HASH || location.hash === FLAG_HASH_SKIP_CONFIG) {
SidebarPanelStore.activatePanel(SidebarPanelKey.FEATURE_FLAG_ONBOARDING);
trackAnalytics('flags.view-setup-sidebar', {
organization,
});
}
}, [location.hash, organization]);

const activateSidebar = useCallback((event: {preventDefault: () => void}) => {
const activateSidebar = useCallback((event: React.MouseEvent) => {
event.preventDefault();
window.location.hash = FLAG_HASH;
SidebarPanelStore.activatePanel(SidebarPanelKey.FEATURE_FLAG_ONBOARDING);
}, []);

// if we detect that event.contexts.flags is set, use this hook instead
// to skip the configure step
const activateSidebarSkipConfigure = useCallback((event: React.MouseEvent) => {
event.preventDefault();
window.location.hash = 'flag-sidequest';
window.location.hash = FLAG_HASH_SKIP_CONFIG;
SidebarPanelStore.activatePanel(SidebarPanelKey.FEATURE_FLAG_ONBOARDING);
}, []);

return {activateSidebar};
return {activateSidebar, activateSidebarSkipConfigure};
}
31 changes: 7 additions & 24 deletions static/app/components/events/featureFlags/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,31 +115,14 @@ export const sortedFlags = ({

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 enum IntegrationOptions {
LAUNCHDARKLY = 'LaunchDarkly',
OPENFEATURE = 'OpenFeature',
}

// to organize this better, we could do something like
// [ProviderOptions.LAUNCHDARKLY]: {
// python: {
// module: 'launchdarkly',
// integration 'LaunchDarklyIntegration',
// },
// javascript: {
// ...
// }
// }
export const PROVIDER_OPTION_TO_LABELS: Record<ProviderOptions, Labels> = {
[ProviderOptions.LAUNCHDARKLY]: {
pythonModule: 'launchdarkly',
pythonIntegration: 'LaunchDarklyIntegration',
},
[ProviderOptions.OPENFEATURE]: {
pythonModule: 'OpenFeature',
pythonIntegration: 'OpenFeatureIntegration',
},
export const PROVIDER_OPTION_TO_URLS: Record<ProviderOptions, string> = {
[ProviderOptions.LAUNCHDARKLY]:
'https://app.launchdarkly.com/settings/integrations/webhooks/new?q=Webhooks',
};
2 changes: 1 addition & 1 deletion static/app/data/platformCategories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -564,7 +564,7 @@ export const feedbackOnboardingPlatforms: readonly PlatformKey[] = [

// Feature flag onboarding platforms
export const featureFlagOnboardingPlatforms: readonly PlatformKey[] = [
// 'javascript',
'javascript',
'python',
];

Expand Down
Loading

0 comments on commit 2e5e065

Please sign in to comment.