From 9192c5c46433b2c9b40f1dbdfa9e8837080e89cc Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Wed, 8 Jul 2026 09:10:42 -0300 Subject: [PATCH 1/3] feat(onboarding): emit first-milestone events from the bootstrap The single-page flow auto-creates the project and flag via RTK, which emit no analytics, so the onboarding funnel had no project/feature step for the new flow. Emit the milestone events (First Project created, First Feature created) on a real create - the milestone the user reached - but not the generic Project/Feature created action events, which stay reserved for resources a user makes by hand. Contributes to #7738 Co-Authored-By: Claude Opus 4.8 --- .../pages/onboarding/hooks/bootstrapOnboarding.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/frontend/web/components/pages/onboarding/hooks/bootstrapOnboarding.ts b/frontend/web/components/pages/onboarding/hooks/bootstrapOnboarding.ts index e1e3ef766a17..13ae99aaca08 100644 --- a/frontend/web/components/pages/onboarding/hooks/bootstrapOnboarding.ts +++ b/frontend/web/components/pages/onboarding/hooks/bootstrapOnboarding.ts @@ -18,6 +18,8 @@ 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 @@ -93,6 +95,10 @@ async function ensureProject( }), ) .unwrap() + // Auto-created setup is a milestone reached, not a user action: fire First + // Project created, not the generic Project created (reserved for projects a + // user makes by hand). First is safe here - we only get here with no project. + API.trackEvent(Constants.events.CREATE_FIRST_PROJECT) await store .dispatch( environmentService.endpoints.createEnvironment.initiate({ @@ -170,7 +176,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,6 +189,11 @@ async function ensureFlag( }), ) .unwrap() + // Milestone, not action (see the project create above): First only. + 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 From b3eb1174201354871c5d7c987e3085e593df9949 Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Wed, 8 Jul 2026 09:10:46 -0300 Subject: [PATCH 2/3] feat(onboarding): tag users with onboarding_variant Set an onboarding_variant user property (control | single_page, from the onboarding_quickstart_flow flag) once, after Flagsmith resolves it at identify. As a user property it attaches to every event the user emits, so the shared funnel can be split by variant with a single Group by, without per-event tagging or touching the shared stores. Contributes to #7738 Co-Authored-By: Claude Opus 4.8 --- frontend/common/utils/getOnboardingVariant.ts | 10 ++++++++++ frontend/web/project/api.ts | 8 +++++++- 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 frontend/common/utils/getOnboardingVariant.ts diff --git a/frontend/common/utils/getOnboardingVariant.ts b/frontend/common/utils/getOnboardingVariant.ts new file mode 100644 index 000000000000..10b36c33a1d2 --- /dev/null +++ b/frontend/common/utils/getOnboardingVariant.ts @@ -0,0 +1,10 @@ +import Utils from './utils' + +export type OnboardingVariant = 'control' | 'single_page' + +// Which onboarding flow the user is in, from the onboarding_quickstart_flow +// flag the new flow is gated behind (#7738). +export const getOnboardingVariant = (): OnboardingVariant => + Utils.getFlagsmithHasFeature('onboarding_quickstart_flow') + ? 'single_page' + : 'control' diff --git a/frontend/web/project/api.ts b/frontend/web/project/api.ts index 05678da4cb61..4e1dcbbfcf8d 100644 --- a/frontend/web/project/api.ts +++ b/frontend/web/project/api.ts @@ -12,6 +12,7 @@ import { AccountModel, User } from 'common/types/responses' import AccountStore from 'common/stores/account-store' import flagsmith from '@flagsmith/flagsmith' import Utils from 'common/utils/utils' +import { getOnboardingVariant } from 'common/utils/getOnboardingVariant' import loadChat, { identifyChatUser } from 'common/loadChat' const API = { @@ -247,7 +248,12 @@ const API = { role: selectedRole, tasks: user.onboarding?.tasks?.map((t) => t.name) || [], }) - API.flagsmithIdentify() + // Pin the onboarding variant as a user property once the flag has + // resolved, so every event (incl. autocapture pageviews) is attributable + // to control vs single_page in one funnel (#7738). + API.flagsmithIdentify()?.then(() => + API.trackTraits({ onboarding_variant: getOnboardingVariant() }), + ) } catch (err) { console.error('Error identifying', err) } From 0267ae79cdee3def8d817f920e4a5f1c091148da Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Wed, 8 Jul 2026 09:10:48 -0300 Subject: [PATCH 3/3] feat(onboarding): new-flow diagnostics events Emit single_page-only diagnostics for the flow's internal drop-off: onboarding snippet copied (install / wire), AI-connect used, and the in-page flag toggle, each carrying org / project / environment IDs (no PII). Kept separate from the control comparison funnel since the control has no equivalent. Contributes to #7738 Co-Authored-By: Claude Opus 4.8 --- frontend/common/constants.ts | 12 ++++++ .../ConnectWithAiPanel.tsx | 11 ++++- .../OnboardingConnectPanel.tsx | 3 ++ .../OnboardingFlow/OnboardingFlow.tsx | 40 +++++++++++++++++-- 4 files changed, 62 insertions(+), 4 deletions(-) diff --git a/frontend/common/constants.ts b/frontend/common/constants.ts index f2ad5e48daa1..bc5668307da1 100644 --- a/frontend/common/constants.ts +++ b/frontend/common/constants.ts @@ -260,6 +260,18 @@ const Constants = { 'category': 'User', 'event': `User oauth ${type}`, }), + 'ONBOARDING_AI_CONNECT': { + 'category': 'Onboarding', + 'event': 'Onboarding AI connect used', + }, + 'ONBOARDING_FLAG_TOGGLED': { + 'category': 'Onboarding', + 'event': 'Onboarding flag toggled', + }, + 'ONBOARDING_SNIPPET_COPIED': { + 'category': 'Onboarding', + 'event': 'Onboarding snippet copied', + }, 'REFERRER_CONVERSION': (referrer: string) => ({ 'category': 'Referrer', 'event': `${referrer} converted`, diff --git a/frontend/web/components/pages/onboarding/OnboardingConnectPanel/ConnectWithAiPanel.tsx b/frontend/web/components/pages/onboarding/OnboardingConnectPanel/ConnectWithAiPanel.tsx index 626a1000f44e..df2bc0e2c1dd 100644 --- a/frontend/web/components/pages/onboarding/OnboardingConnectPanel/ConnectWithAiPanel.tsx +++ b/frontend/web/components/pages/onboarding/OnboardingConnectPanel/ConnectWithAiPanel.tsx @@ -7,6 +7,7 @@ import { useCopyFeedback } from 'components/pages/onboarding/hooks/useCopyFeedba export type ConnectWithAiPanelProps = { environmentKey: string featureName: string + onCopyPrompt?: () => void } // "Connect with AI" tab: an agent-agnostic prompt the user pastes into their @@ -18,6 +19,7 @@ export type ConnectWithAiPanelProps = { const ConnectWithAiPanel: FC = ({ environmentKey, featureName, + onCopyPrompt, }) => { const { copied, copy } = useCopyFeedback() @@ -39,7 +41,14 @@ Detect my stack, install the SDK, and wire ${featureName} into one place. Then r {aiPrompt} -