= ({
diff --git a/frontend/web/components/pages/onboarding/OnboardingFlow/OnboardingFlow.tsx b/frontend/web/components/pages/onboarding/OnboardingFlow/OnboardingFlow.tsx
index ad32dc2ede47..0c99c4eeaf3b 100644
--- a/frontend/web/components/pages/onboarding/OnboardingFlow/OnboardingFlow.tsx
+++ b/frontend/web/components/pages/onboarding/OnboardingFlow/OnboardingFlow.tsx
@@ -16,8 +16,12 @@ import { useOnboardingFlag } from 'components/pages/onboarding/hooks/useOnboardi
import { useOnboardingConnection } from 'components/pages/onboarding/hooks/useOnboardingConnection'
import { useUpdateOrganisationMutation } from 'common/services/useOrganisation'
import { useUpdateProjectMutation } from 'common/services/useProject'
+import API from 'project/api'
+import Constants from 'common/constants'
import './OnboardingFlow.scss'
+type OnboardingSnippet = 'install' | 'wire'
+
// The single-page onboarding flow, rendered at /getting-started when
// onboarding_quickstart_flow is on (see GettingStartedGate).
const OnboardingFlow: FC = () => {
@@ -139,6 +143,38 @@ const OnboardingFlow: FC = () => {
history.push(`${base}/features?feature=${flagId}&tab=${tab}`)
}
+ const diagnosticIds = {
+ environment_id: environment?.id,
+ organisation_id: organisationId,
+ project_id: projectId,
+ }
+ const trackSnippetCopied = (snippet: OnboardingSnippet) =>
+ API.trackEvent({
+ ...Constants.events.ONBOARDING_SNIPPET_COPIED,
+ extra: { ...diagnosticIds, snippet },
+ })
+ const handleCopyInstall = () => {
+ trackSnippetCopied('install')
+ setInstallCopied(true)
+ }
+ const handleCopyWire = () => {
+ trackSnippetCopied('wire')
+ setSnippetCopied(true)
+ }
+ const handleCopyPrompt = () =>
+ API.trackEvent({
+ ...Constants.events.ONBOARDING_AI_CONNECT,
+ extra: diagnosticIds,
+ })
+ const handleToggleFlag = async (enabled: boolean) => {
+ if (await toggleFlag(enabled)) {
+ API.trackEvent({
+ ...Constants.events.ONBOARDING_FLAG_TOGGLED,
+ extra: { ...diagnosticIds, enabled },
+ })
+ }
+ }
+
if (status === 'creating') {
return (
@@ -177,8 +213,9 @@ const OnboardingFlow: FC = () => {
setInstallCopied(true)}
- onCopyWire={() => setSnippetCopied(true)}
+ onCopyInstall={handleCopyInstall}
+ onCopyWire={handleCopyWire}
+ onCopyPrompt={handleCopyPrompt}
/>
{
tags: flagTags,
},
]}
- onToggle={(_flag, next) => toggleFlag(next)}
+ onToggle={(_flag, next) => handleToggleFlag(next)}
togglingFlag={isToggling ? featureName : null}
togglesReady={flagStateReady}
/>
diff --git a/frontend/web/components/pages/onboarding/hooks/bootstrapOnboarding.ts b/frontend/web/components/pages/onboarding/hooks/bootstrapOnboarding.ts
index e1e3ef766a17..3192bc96ba5e 100644
--- a/frontend/web/components/pages/onboarding/hooks/bootstrapOnboarding.ts
+++ b/frontend/web/components/pages/onboarding/hooks/bootstrapOnboarding.ts
@@ -18,17 +18,16 @@ import {
} from 'common/types/responses'
import { SmartDefaults } from './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'
-// Written on create and matched by name on reuse, so the two must stay in step.
const DEV_ENVIRONMENT_NAME = 'Development'
const PROD_ENVIRONMENT_NAME = 'Production'
-// Attached to the demo flag so the flags table shows an "Onboarding" badge.
-// Green from the product tag palette (Constants.tagColors), not an arbitrary hex.
const ONBOARDING_TAG = {
color: '#3cb371',
description: 'Created during onboarding',
@@ -50,13 +49,12 @@ export type OnboardingBootstrap = {
featureName: string
}
-// Reuse the loaded org, or create one only when the user has none (the plan
-// org cap rejects extra creates with a 403). Create goes through the legacy
-// AccountStore so the shell's current-org selection is populated.
async function ensureOrganisation(
store: Store,
{ defaults, existingOrg }: BootstrapInput,
): Promise {
+ // Create only when the user has none: the plan org cap rejects extra creates
+ // with a 403. Goes through AccountStore so the shell adopts the new org.
if (existingOrg) {
return existingOrg
}
@@ -71,8 +69,6 @@ async function ensureOrganisation(
return { id, name }
}
-// Reuse the first project, else create one with Development + Production
-// environments. Avoids stacking up duplicate projects on revisits.
async function ensureProject(
store: Store,
organisationId: number,
@@ -93,6 +89,7 @@ async function ensureProject(
}),
)
.unwrap()
+ API.trackEvent(Constants.events.CREATE_FIRST_PROJECT)
await store
.dispatch(
environmentService.endpoints.createEnvironment.initiate({
@@ -112,8 +109,6 @@ async function ensureProject(
return project
}
-// Surface an environment key: reuse one (preferring Development), or create it
-// only if the reused project somehow has none.
async function ensureEnvironments(
store: Store,
project: ProjectSummary,
@@ -138,7 +133,6 @@ async function ensureEnvironments(
.unwrap()
}
-// Find the project's Onboarding tag, if it's been created yet.
async function findOnboardingTag(
store: Store,
projectId: number,
@@ -149,8 +143,6 @@ async function findOnboardingTag(
return tags?.find((t) => t.label === ONBOARDING_TAG.label)
}
-// Reuse the onboarding flag, matched by its Onboarding tag (or its name) so we
-// never grab one of the user's other flags; else create the demo flag.
async function ensureFlag(
store: Store,
project: ProjectSummary,
@@ -170,7 +162,8 @@ async function ensureFlag(
if (existing) {
return existing
}
- return store
+ const isFirstFeature = !flags?.results?.length
+ const created = await store
.dispatch(
projectFlagService.endpoints.createProjectFlag.initiate({
body: {
@@ -182,11 +175,12 @@ async function ensureFlag(
}),
)
.unwrap()
+ if (isFirstFeature) {
+ API.trackEvent(Constants.events.CREATE_FIRST_FEATURE)
+ }
+ return created
}
-// Attach the "Onboarding" tag to the demo flag (find-or-create), so the flags
-// table shows the badge from the design. Best-effort: tagging is cosmetic and
-// must never block the bootstrap.
async function ensureOnboardingTag(
store: Store,
project: ProjectSummary,
@@ -215,14 +209,10 @@ async function ensureOnboardingTag(
.unwrap()
}
} catch {
- // Cosmetic only - never block onboarding on tagging.
+ // Cosmetic: tagging must never block onboarding.
}
}
-// Idempotently bring up everything the single-page flow needs: org, project,
-// Development + Production environments, and a first flag, reusing whatever
-// already exists. Split into ensure* steps so each concern is testable and the
-// reuse-vs-create decisions are obvious.
export async function bootstrapOnboarding(
store: Store,
input: BootstrapInput,
@@ -234,7 +224,6 @@ export async function bootstrapOnboarding(
if (flag) {
await ensureOnboardingTag(store, project, flag)
}
- // Refresh the legacy org store so the shell sees the project.
AppActions.refreshOrganisation()
return {
environment,
diff --git a/frontend/web/components/pages/onboarding/hooks/useOnboardingFlag.ts b/frontend/web/components/pages/onboarding/hooks/useOnboardingFlag.ts
index a931208c0574..3e7c3cfa0a99 100644
--- a/frontend/web/components/pages/onboarding/hooks/useOnboardingFlag.ts
+++ b/frontend/web/components/pages/onboarding/hooks/useOnboardingFlag.ts
@@ -41,9 +41,9 @@ export const useOnboardingFlag = (
const { displayEnabled, isLoading, revertToggle, startToggle } =
useFeatureRowState(state?.enabled)
- const toggle = async (enabled: boolean) => {
+ const toggle = async (enabled: boolean): Promise => {
if (!environment || !state || !startToggle(enabled)) {
- return
+ return false
}
try {
await updateFeatureState({
@@ -51,9 +51,11 @@ export const useOnboardingFlag = (
environmentFlagId: state.id,
environmentId: environment.api_key,
}).unwrap()
+ return true
} catch {
revertToggle()
toast('Couldn’t update your flag. Please try again.', 'danger')
+ return false
}
}