diff --git a/frontend/web/components/pages/onboarding/OnboardingFlow/OnboardingFlow.tsx b/frontend/web/components/pages/onboarding/OnboardingFlow/OnboardingFlow.tsx
index 7f8f8acdcfc7..92ca1744f564 100644
--- a/frontend/web/components/pages/onboarding/OnboardingFlow/OnboardingFlow.tsx
+++ b/frontend/web/components/pages/onboarding/OnboardingFlow/OnboardingFlow.tsx
@@ -1,6 +1,7 @@
import React, { FC, useState } from 'react'
import { useHistory } from 'react-router-dom'
import Button from 'components/base/forms/Button'
+import Link from 'components/base/link'
import Icon from 'components/icons/Icon'
import OnboardingHeader from 'components/pages/onboarding/OnboardingHeader'
import ThemeToggle from 'components/ThemeToggle'
@@ -30,6 +31,7 @@ const OnboardingFlow: FC = () => {
environment,
environmentKey,
featureName: bootstrappedFeatureName,
+ hasOnboardingFlag,
organisationId,
organisationName,
projectId,
@@ -197,6 +199,27 @@ const OnboardingFlow: FC = () => {
)
}
+ // ensureFlag seeded nothing, so there is no flag to tour with.
+ if (!hasOnboardingFlag) {
+ return (
+
diff --git a/frontend/web/components/pages/onboarding/bootstrap/__tests__/onboardingFlag.test.ts b/frontend/web/components/pages/onboarding/bootstrap/__tests__/onboardingFlag.test.ts
new file mode 100644
index 000000000000..a2841366fda5
--- /dev/null
+++ b/frontend/web/components/pages/onboarding/bootstrap/__tests__/onboardingFlag.test.ts
@@ -0,0 +1,53 @@
+import { ProjectFlag, Tag } from 'common/types/responses'
+import {
+ ONBOARDING_FLAG_NAME,
+ findOnboardingFlag,
+ shouldSeedOnboardingFlag,
+} from 'components/pages/onboarding/bootstrap/onboardingFlag'
+
+const flag = (name: string, tags: number[] = []): ProjectFlag =>
+ ({ id: name.length, name, tags } as ProjectFlag)
+
+const onboardingTag = { id: 7, label: 'Onboarding' } as Tag
+
+describe('shouldSeedOnboardingFlag', () => {
+ it('seeds into an empty project', () => {
+ expect(shouldSeedOnboardingFlag([])).toBe(true)
+ })
+
+ it('seeds nothing once the project has flags of its own', () => {
+ expect(shouldSeedOnboardingFlag([flag('checkout_v2')])).toBe(false)
+ })
+})
+
+describe('findOnboardingFlag', () => {
+ it('finds a previous run by its tag, whatever it was renamed to', () => {
+ const renamed = flag('my_own_name', [onboardingTag.id])
+ expect(
+ findOnboardingFlag([flag('checkout_v2'), renamed], onboardingTag),
+ ).toBe(renamed)
+ })
+
+ it('prefers the tag over the name when both are present', () => {
+ const tagged = flag('renamed_by_hand', [onboardingTag.id])
+ const named = flag(ONBOARDING_FLAG_NAME)
+ expect(findOnboardingFlag([named, tagged], onboardingTag)).toBe(tagged)
+ })
+
+ it('falls back to the name when the tag is missing', () => {
+ const seeded = flag(ONBOARDING_FLAG_NAME)
+ expect(findOnboardingFlag([flag('checkout_v2'), seeded], undefined)).toBe(
+ seeded,
+ )
+ })
+
+ it('finds nothing in a project that never ran the tour', () => {
+ expect(
+ findOnboardingFlag([flag('checkout_v2')], onboardingTag),
+ ).toBeUndefined()
+ })
+
+ it('finds nothing in an empty project', () => {
+ expect(findOnboardingFlag([], onboardingTag)).toBeUndefined()
+ })
+})
diff --git a/frontend/web/components/pages/onboarding/hooks/bootstrapOnboarding.ts b/frontend/web/components/pages/onboarding/bootstrap/bootstrapOnboarding.ts
similarity index 90%
rename from frontend/web/components/pages/onboarding/hooks/bootstrapOnboarding.ts
rename to frontend/web/components/pages/onboarding/bootstrap/bootstrapOnboarding.ts
index 3192bc96ba5e..aa0ed4ddd107 100644
--- a/frontend/web/components/pages/onboarding/hooks/bootstrapOnboarding.ts
+++ b/frontend/web/components/pages/onboarding/bootstrap/bootstrapOnboarding.ts
@@ -16,24 +16,23 @@ import {
ProjectSummary,
Tag,
} from 'common/types/responses'
-import { SmartDefaults } from './useSmartDefaults'
+import {
+ ONBOARDING_FLAG_NAME,
+ ONBOARDING_TAG,
+ findOnboardingFlag,
+ shouldSeedOnboardingFlag,
+} from './onboardingFlag'
+import { SmartDefaults } from 'components/pages/onboarding/hooks/useSmartDefaults'
import { createOrganisationViaAccountStore } from './createOrganisationViaAccountStore'
import API from 'project/api'
import Constants from 'common/constants'
type Store = ReturnType
-const FLAG_NAME = 'show_demo_button'
const DEFAULT_ORG_NAME = 'My organisation'
const DEFAULT_PROJECT_NAME = 'My first project'
const DEV_ENVIRONMENT_NAME = 'Development'
const PROD_ENVIRONMENT_NAME = 'Production'
-const ONBOARDING_TAG = {
- color: '#3cb371',
- description: 'Created during onboarding',
- label: 'Onboarding',
-}
-
type ExistingOrg = { id: number; name: string }
export type BootstrapInput = {
@@ -47,6 +46,8 @@ export type OnboardingBootstrap = {
project: ProjectSummary
environment: Environment
featureName: string
+ // False when the project already had flags, so nothing was seeded.
+ hasOnboardingFlag: boolean
}
async function ensureOrganisation(
@@ -154,20 +155,20 @@ async function ensureFlag(
}),
)
.unwrap()
+ const results = flags?.results ?? []
const onboardingTag = await findOnboardingTag(store, project.id)
- const existing =
- (onboardingTag &&
- flags?.results?.find((f) => f.tags?.includes(onboardingTag.id))) ||
- flags?.results?.find((f) => f.name === FLAG_NAME)
+ const existing = findOnboardingFlag(results, onboardingTag)
if (existing) {
return existing
}
- const isFirstFeature = !flags?.results?.length
+ if (!shouldSeedOnboardingFlag(results)) {
+ return undefined
+ }
const created = await store
.dispatch(
projectFlagService.endpoints.createProjectFlag.initiate({
body: {
- name: FLAG_NAME,
+ name: ONBOARDING_FLAG_NAME,
project: project.id,
type: 'STANDARD',
} as Req['createProjectFlag']['body'],
@@ -175,9 +176,7 @@ async function ensureFlag(
}),
)
.unwrap()
- if (isFirstFeature) {
- API.trackEvent(Constants.events.CREATE_FIRST_FEATURE)
- }
+ API.trackEvent(Constants.events.CREATE_FIRST_FEATURE)
return created
}
@@ -227,7 +226,8 @@ export async function bootstrapOnboarding(
AppActions.refreshOrganisation()
return {
environment,
- featureName: flag?.name ?? FLAG_NAME,
+ featureName: flag?.name ?? ONBOARDING_FLAG_NAME,
+ hasOnboardingFlag: !!flag,
organisationId: organisation.id,
organisationName: organisation.name,
project,
diff --git a/frontend/web/components/pages/onboarding/hooks/createOrganisationViaAccountStore.ts b/frontend/web/components/pages/onboarding/bootstrap/createOrganisationViaAccountStore.ts
similarity index 100%
rename from frontend/web/components/pages/onboarding/hooks/createOrganisationViaAccountStore.ts
rename to frontend/web/components/pages/onboarding/bootstrap/createOrganisationViaAccountStore.ts
diff --git a/frontend/web/components/pages/onboarding/bootstrap/onboardingFlag.ts b/frontend/web/components/pages/onboarding/bootstrap/onboardingFlag.ts
new file mode 100644
index 000000000000..f664c6729292
--- /dev/null
+++ b/frontend/web/components/pages/onboarding/bootstrap/onboardingFlag.ts
@@ -0,0 +1,23 @@
+import { ProjectFlag, Tag } from 'common/types/responses'
+
+export const ONBOARDING_FLAG_NAME = 'show_demo_button'
+
+export const ONBOARDING_TAG = {
+ color: '#3cb371',
+ description: 'Created during onboarding',
+ label: 'Onboarding',
+}
+
+// A previous run's flag. Tag first: renaming is a delete and recreate, so the
+// name alone is not reliable.
+export const findOnboardingFlag = (
+ flags: ProjectFlag[],
+ onboardingTag?: Tag,
+): ProjectFlag | undefined =>
+ (onboardingTag && flags.find((f) => f.tags?.includes(onboardingTag.id))) ||
+ flags.find((f) => f.name === ONBOARDING_FLAG_NAME)
+
+// Only seed into an empty project: features are project-level, so an unwanted
+// flag shows up in every environment, production included.
+export const shouldSeedOnboardingFlag = (flags: ProjectFlag[]): boolean =>
+ !flags.length
diff --git a/frontend/web/components/pages/onboarding/hooks/useEnsureOnboardingResources.ts b/frontend/web/components/pages/onboarding/hooks/useEnsureOnboardingResources.ts
index 574ae4f0f2bd..86c6495a0003 100644
--- a/frontend/web/components/pages/onboarding/hooks/useEnsureOnboardingResources.ts
+++ b/frontend/web/components/pages/onboarding/hooks/useEnsureOnboardingResources.ts
@@ -5,7 +5,7 @@ import useSelectedOrganisation from 'common/hooks/useSelectedOrganisation'
import { useGetProfileQuery } from 'common/services/useProfile'
import { Environment } from 'common/types/responses'
import { useSmartDefaults } from './useSmartDefaults'
-import { bootstrapOnboarding } from './bootstrapOnboarding'
+import { bootstrapOnboarding } from 'components/pages/onboarding/bootstrap/bootstrapOnboarding'
export type OnboardingResourcesStatus = 'creating' | 'ready' | 'error'
@@ -15,6 +15,7 @@ export type OnboardingResources = {
organisationName: string
projectName: string
featureName: string
+ hasOnboardingFlag: boolean
caseSensitive: boolean
environment: Environment | null
environmentKey: string
@@ -46,6 +47,7 @@ export const useEnsureOnboardingResources = (): OnboardingResources => {
const [organisationName, setOrganisationName] = useState('')
const [projectName, setProjectName] = useState('')
const [featureName, setFeatureName] = useState('')
+ const [hasOnboardingFlag, setHasOnboardingFlag] = useState(true)
// Whether the project enforces lower-case feature names; drives the same name
// normalisation the create-feature modal applies (see the header).
const [caseSensitive, setCaseSensitive] = useState(false)
@@ -77,6 +79,7 @@ export const useEnsureOnboardingResources = (): OnboardingResources => {
setEnvironment(res.environment)
setEnvironmentKey(res.environment.api_key)
setFeatureName(res.featureName)
+ setHasOnboardingFlag(res.hasOnboardingFlag)
setStatus('ready')
})
.catch((e) => {
@@ -91,6 +94,7 @@ export const useEnsureOnboardingResources = (): OnboardingResources => {
environmentKey,
error,
featureName,
+ hasOnboardingFlag,
organisationId,
organisationName,
projectId,