Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions frontend/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,23 @@ const Constants = {
'category': 'Onboarding',
'event': 'Onboarding flag toggled',
},
'ONBOARDING_ROLLOUT_CONTINUED': {
'category': 'Onboarding',
'event': 'Onboarding rollout quest continued',
},
'ONBOARDING_ROLLOUT_FEEDBACK': {
'category': 'Onboarding',
'event': 'Onboarding rollout feedback opened',
},
// The fake door's only real signal: demand for one-click rollouts.
'ONBOARDING_ROLLOUT_NOTIFY_ME': {
'category': 'Onboarding',
'event': 'Onboarding rollout notify me',
},
'ONBOARDING_ROLLOUT_VIEWED': {
'category': 'Onboarding',
'event': 'Onboarding rollout quest viewed',
},
'ONBOARDING_SNIPPET_COPIED': {
'category': 'Onboarding',
'event': 'Onboarding snippet copied',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ import OnboardingFlagsTable from 'components/pages/onboarding/OnboardingFlagsTab
import OnboardingNextSteps, {
OnboardingNextStep,
} from 'components/pages/onboarding/OnboardingNextSteps'
import { useRolloutQuest } from 'components/pages/onboarding/OnboardingRolloutQuest'
import { useEnsureOnboardingResources } from 'components/pages/onboarding/hooks/useEnsureOnboardingResources'
import { useOnboardingFlagRename } from 'components/pages/onboarding/hooks/useOnboardingFlagRename'
import { useOnboardingFlag } from 'components/pages/onboarding/hooks/useOnboardingFlag'
import { useOnboardingConnection } from 'components/pages/onboarding/hooks/useOnboardingConnection'
import { useUpdateOrganisationMutation } from 'common/services/useOrganisation'
import { useUpdateProjectMutation } from 'common/services/useProject'
import { useGetProfileQuery } from 'common/services/useProfile'
import API from 'project/api'
import Constants from 'common/constants'
import { isPendingAuthorisation } from 'common/utils/pendingAuthorisation'
Expand All @@ -41,6 +43,8 @@ const OnboardingFlow: FC = () => {
const history = useHistory()
const [updateOrganisation] = useUpdateOrganisationMutation()
const [updateProject] = useUpdateProjectMutation()
// Already fetched by useEnsureOnboardingResources, so this is a cache read.
const { data: profile } = useGetProfileQuery({})

// A client waiting on the consent screen sent this user here to sign up. It
// is answered once the workspace exists, never on load: bootstrapping is a
Expand Down Expand Up @@ -144,8 +148,8 @@ const OnboardingFlow: FC = () => {
}
}

// Each next-step card deep-links to the flag's real config; nothing faked.
const goToNextStep = (step: OnboardingNextStep) => {
// Where a quest ends up: the flag's real config, nothing faked.
const goToFlagConfig = (step: OnboardingNextStep) => {
if (projectId === null) {
return
}
Expand All @@ -167,6 +171,21 @@ const OnboardingFlow: FC = () => {
organisation_id: organisationId,
project_id: projectId,
}
const openRolloutQuest = useRolloutQuest({
diagnosticIds,
featureName,
onContinue: () => goToFlagConfig('rollout'),
who: { email: profile?.email, organisation: organisationDisplayName },
})

// Off, rollout deep-links to the overrides tab like every other next step.
const rolloutQuestEnabled = Utils.getFlagsmithHasFeature(
'onboarding_rollout_quest',
)
Comment on lines +182 to +184

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Import Utils before reading the rollout feature flag.

Lines 164-166 reference an unbound Utils identifier. TypeScript compilation fails before OnboardingFlow can render.

Proposed fix
 import { useGetProfileQuery } from 'common/services/useProfile'
+import Utils from 'common/utils/utils'
 import API from 'project/api'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const rolloutQuestEnabled = Utils.getFlagsmithHasFeature(
'onboarding_rollout_quest',
)
import { useGetProfileQuery } from 'common/services/useProfile'
import Utils from 'common/utils/utils'
import API from 'project/api'

const goToNextStep = (step: OnboardingNextStep) =>
step === 'rollout' && rolloutQuestEnabled
? openRolloutQuest()
: goToFlagConfig(step)
const trackSnippetCopied = (snippet: OnboardingSnippet) =>
API.trackEvent({
...Constants.events.ONBOARDING_SNIPPET_COPIED,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.onboarding-rollout-quest {
// The fill alone is near-invisible in dark mode, where the card and the modal
// are a shade apart. .border-default sets only a colour, so the shorthand
// lives here, as it does in MetricsTable and VariationTable.
&__card {
border: 1px solid var(--color-border-default);
}

&__step-number {
width: 24px;
height: 24px;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import React, { FC } from 'react'
import Button from 'components/base/forms/Button'
import Icon from 'components/icons/Icon'
import RolloutComingSoonCard from './RolloutComingSoonCard'
import {
ROLLOUT_GUIDE_URL,
RolloutPrerequisite,
getRolloutSteps,
} from './rolloutSteps'
import './OnboardingRolloutQuest.scss'

export type OnboardingRolloutQuestProps = {
featureName: string
onContinue: () => void
onDismiss: () => void
onNotifyMe: () => void
onFeedback: () => void
}

const OnboardingRolloutQuest: FC<OnboardingRolloutQuestProps> = ({
featureName,
onContinue,
onDismiss,
onFeedback,
onNotifyMe,
}) => (
<div className='onboarding-rollout-quest d-flex flex-column gap-4'>
<p className='fs-caption lh-sm text-secondary m-0'>
Release {featureName} to a growing percentage of your users.
</p>

<section className='onboarding-rollout-quest__card bg-surface-muted rounded-xl p-4 d-flex flex-column gap-3'>
<h6 className='m-0'>How to roll out gradually today</h6>
<ol className='list-unstyled d-flex flex-column gap-3 m-0'>
{getRolloutSteps(featureName).map((step, index) => (
<li key={step.title} className='d-flex gap-3'>
<span className='onboarding-rollout-quest__step-number bg-surface-action-subtle text-action rounded-full fs-captionSmall fw-bold d-flex align-items-center justify-content-center flex-shrink-0'>
{index + 1}
</span>
<span className='d-flex flex-column gap-1'>
<span className='fw-bold text-default'>{step.title}</span>
<span className='fs-caption lh-sm text-secondary'>
{step.body}
</span>
</span>
</li>
))}
</ol>
<RolloutPrerequisite />
<Button
theme='text'
href={ROLLOUT_GUIDE_URL}
target='_blank'
className='align-self-start'
>
<Icon name='file-text' width={14} />
Read the gradual rollout guide
</Button>
</section>

<RolloutComingSoonCard onNotifyMe={onNotifyMe} onFeedback={onFeedback} />

<div className='d-flex align-items-center gap-3'>
<Button onClick={onContinue}>
<Icon name='layers' width={14} />
Create a rollout segment
</Button>
<Button theme='text' onClick={onDismiss}>
Maybe later
</Button>
</div>
</div>
)

export default OnboardingRolloutQuest
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React, { FC, useState } from 'react'
import flagsmith from '@flagsmith/flagsmith'
import Button from 'components/base/forms/Button'
import Chip from 'components/base/Chip'
import Icon from 'components/icons/Icon'
import { ROLLOUT_BETA_REQUESTED } from './trackRolloutInterest'

export const ROLLOUT_FEEDBACK_URL =
'mailto:support@flagsmith.com?subject=Gradual%20rollout'

export type RolloutComingSoonCardProps = {
onNotifyMe: () => void
onFeedback: () => void
}

// The one thing we don't ship yet: the three steps above in a single action. It
// promises a simpler flow, never the capability.
const RolloutComingSoonCard: FC<RolloutComingSoonCardProps> = ({
onFeedback,
onNotifyMe,
}) => {
// A trait rather than local state, so asking survives a reload and the beta
// can later be handed out by targeting it. Read at render, as Announcement
// does, so it still resolves if traits land after mount.
const [notified, setNotified] = useState(false)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't done it with the sources fake door but I believe setting a trait and using the trait as a persisted state is actually better, especially if we want to use it to access the beta in the future

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call, done in 963519c. Reused the event name as the trait key so there's one name for the thing:

    const alreadyAsked = notified || !!flagsmith.getTrait(ROLLOUT_BETA_REQUESTED) 

const alreadyAsked = notified || !!flagsmith.getTrait(ROLLOUT_BETA_REQUESTED)
const notifyMe = () => {
setNotified(true)
flagsmith.setTrait(ROLLOUT_BETA_REQUESTED, true)
onNotifyMe()
}
return (
<section className='bg-surface-action-subtle rounded-xl p-4 d-flex flex-column gap-2'>
{/* Accent rather than the design's solid purple: there is no inverse
text token, and white on the dark-mode action surface is ~3.2:1. */}
<Chip variant='accent' size='xs' className='align-self-start'>
<Icon name='flash' width={12} />
Coming soon
</Chip>
<h6 className='m-0'>We’re making gradual rollouts one-click</h6>
<p className='fs-caption lh-sm text-secondary m-0'>
Automatically release according to a schedule, without manual editing of
segments. Want early access?
</p>
<div className='d-flex align-items-center gap-3'>
{alreadyAsked ? (
<span className='fs-caption d-inline-flex align-items-center gap-2 text-action'>
<Icon name='checkmark-circle' width={16} />
Thanks, we’ll be in touch.
</span>
) : (
<Button onClick={notifyMe}>
<Icon name='bell' width={14} />
Notify me
</Button>
)}
{/* No target: a mailto in a new tab leaves a blank tab behind. */}
<Button theme='text' href={ROLLOUT_FEEDBACK_URL} onClick={onFeedback}>
Tell us what you need
</Button>
</div>
</section>
)
}

export default RolloutComingSoonCard
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { default } from './OnboardingRolloutQuest'
export type { OnboardingRolloutQuestProps } from './OnboardingRolloutQuest'
export { default as useRolloutQuest } from './useRolloutQuest'
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React, { FC } from 'react'
import Icon from 'components/icons/Icon'

export const ROLLOUT_GUIDE_URL =
'https://docs.flagsmith.com/managing-flags/rollout/rollout-by-percentage'

export const getRolloutSteps = (
featureName: string,
): { title: string; body: string }[] => [
{
body: 'Add a “Percentage split” rule and set your starting share, e.g. 10% of users.',
title: 'Create a segment',
},
{
body: `Turn ${featureName} on for that segment, so only those users get it.`,
title: 'Override the flag',
},
{
body: 'Raise the percentage as your confidence grows: 10% → 25% → 50% → 100%.',
title: 'Increase over time',
},
]

// Sits with the steps rather than in docs: without it the override reaches
// nobody and the user has no way to tell.
export const RolloutPrerequisite: FC = () => (
<p className='fs-captionSmall lh-sm text-secondary d-flex gap-2 m-0'>
{/* info hardcodes a blue fill, so it needs telling to inherit. */}
<Icon name='info' width={14} fill='currentColor' />
<span>
Percentage splits only apply to users you identify. If your app doesn’t
call <code className='fs-captionSmall'>flagsmith.identify(...)</code> yet,
add it first, or everyone keeps getting the same value.
</span>
</p>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import flagsmith from '@flagsmith/flagsmith'

export const ROLLOUT_BETA_REQUESTED = 'rollout_beta_requested'
export const ROLLOUT_FEEDBACK_CLICKED = 'rollout_feedback_clicked'

type RolloutInterest = {
email?: string
organisation?: string
}

// The onboarding funnel events say an organisation wants this; this says who to
// reply to. Sent through flagsmith.trackEvent, same as the segment sources door.
const trackRolloutInterest = (
event: string,
{ email, organisation }: RolloutInterest,
): void => {
flagsmith.trackEvent(event, {
metadata: {
email,
organisation,
origin: 'onboarding-rollout-quest',
},
})
}

export default trackRolloutInterest
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from 'react'
import API from 'project/api'
import Constants from 'common/constants'
import trackRolloutInterest, {
ROLLOUT_BETA_REQUESTED,
ROLLOUT_FEEDBACK_CLICKED,
} from './trackRolloutInterest'
import OnboardingRolloutQuest from './OnboardingRolloutQuest'

type UseRolloutQuest = {
featureName: string
/** Where "Create a rollout segment" ends up. */
onContinue: () => void
diagnosticIds: Record<string, unknown>
/** Who to reply to when someone asks for access. */
who: { email?: string; organisation?: string }
}

// The segment overrides tab alone shows none of the steps a rollout takes, so
// this opens first.
const useRolloutQuest = ({
diagnosticIds,
featureName,
onContinue,
who,
}: UseRolloutQuest): (() => void) => {
const track = (event: { category: string; event: string }) =>
API.trackEvent({ ...event, extra: diagnosticIds })

return () => {
track(Constants.events.ONBOARDING_ROLLOUT_VIEWED)
openModal(
// Matches the next-step card that opens it.
'Gradual rollout',
Comment on lines +33 to +34

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep the modal title aligned with the specified copy.

The PR objective and supplied designs require the title “Roll out gradually”. Line 34 uses “Gradual rollout” instead. Restore the specified title, or update the product copy and designs in the same change if the new wording is intentional.

Proposed fix
       // Matches the next-step card that opens it.
-      'Gradual rollout',
+      'Roll out gradually',
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Matches the next-step card that opens it.
'Gradual rollout',
// Matches the next-step card that opens it.
'Roll out gradually',

React.createElement(OnboardingRolloutQuest, {
featureName,
onContinue: () => {
track(Constants.events.ONBOARDING_ROLLOUT_CONTINUED)
closeModal()
onContinue()
},
onDismiss: () => closeModal(),
onFeedback: () => {
track(Constants.events.ONBOARDING_ROLLOUT_FEEDBACK)
trackRolloutInterest(ROLLOUT_FEEDBACK_CLICKED, who)
},
onNotifyMe: () => {
track(Constants.events.ONBOARDING_ROLLOUT_NOTIFY_ME)
trackRolloutInterest(ROLLOUT_BETA_REQUESTED, who)
},
}),
'modal--wide',
)
}
}

export default useRolloutQuest
Loading