From 30ffd6ca44afc5fdc6489a990d45e87980edfacc Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Tue, 21 Jul 2026 15:16:36 -0300 Subject: [PATCH 1/3] feat(onboarding): fire First Project/Feature milestone events from bootstrap The single-page flow auto-creates the project and flag, which emitted nothing, so the funnel had no project/feature step. Fire First Project created / First Feature created on a real create. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../onboarding/hooks/bootstrapOnboarding.ts | 35 +++++++------------ 1 file changed, 12 insertions(+), 23 deletions(-) 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, From 2d1be409973d1e079c736f3415d90a3ff48ff16f Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Tue, 21 Jul 2026 15:16:50 -0300 Subject: [PATCH 2/3] feat(onboarding): add new-flow diagnostics events Onboarding snippet copied (install/wire), Onboarding AI connect used, and Onboarding flag toggled (fired only after the toggle saves; toggle now returns success). For measuring internal drop-off in the new flow. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/common/constants.ts | 12 ++++++ .../ConnectWithAiPanel.tsx | 11 ++++- .../OnboardingConnectPanel.tsx | 3 ++ .../OnboardingFlow/OnboardingFlow.tsx | 43 +++++++++++++++++-- .../onboarding/hooks/useOnboardingFlag.ts | 6 ++- 5 files changed, 69 insertions(+), 6 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} -