mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-28 05:46:58 +02:00
* feat(analytics): capture onboarding funnel events + person-property $set Closes the visibility gap introduced by the Onboarding relaunch: the five new steps between signup and workspace_created were invisible to PostHog, and we couldn't see Step 3 web-fork drop-off, cloud waitlist intent, or starter-content acceptance at all. Server-side events (see docs/analytics.md for full contracts): - onboarding_questionnaire_submitted — fires once when all three answers first land; also $set's role/use_case/team_size on the person so every subsequent event is cohortable - agent_created — not onboarding-specific; is_first_agent_in_workspace isolates the Step 4 signal - onboarding_completed — fires on the actual NULL → timestamp flip with completion_path (full / runtime_skipped / cloud_waitlist / skip_existing / unknown) + joined_cloud_waitlist - cloud_waitlist_joined — sizes hosted-runtime interest - starter_content_decided — imported vs dismissed, split by agent_guided / self_serve branch on both sides Also adds Event.Set (→ PostHog $set) alongside the existing SetOnce so the same events can carry mutable cohort signals without a separate identify round-trip. * feat(analytics): wire frontend onboarding events + completion_path - captureEvent / setPersonProperties helpers in @multica/core/analytics, with the same pre-init buffering as identify/pageview so config races don't drop step transitions - onboarding_runtime_path_selected fires from step-platform-fork for the three web-fork choices (download desktop / CLI / cloud waitlist), plus platform_preference on person properties for downstream splits - completeOnboarding now takes an OnboardingCompletionPath; the onboarding shell derives full / runtime_skipped / cloud_waitlist from runtime + waitlist state (lifted to the shell so StepFirstIssue can see both), and handleWelcomeSkip passes skip_existing - saveQuestionnaire mirrors team_size/role/use_case into person properties via $set so every event on this user becomes cohortable - StepAgent sends the template slug, StarterContentPrompt passes workspace_id on dismiss so the server can mirror the branch label * docs(analytics): document onboarding funnel events + $set person properties
67 lines
2.5 KiB
TypeScript
67 lines
2.5 KiB
TypeScript
import { api } from "../api";
|
|
import { useAuthStore } from "../auth";
|
|
import { setPersonProperties } from "../analytics";
|
|
import type { OnboardingCompletionPath, QuestionnaireAnswers } from "./types";
|
|
|
|
/**
|
|
* Persist Q1/Q2/Q3 answers and sync the refreshed user into the auth
|
|
* store. Source of truth is `user.onboarding_questionnaire` (JSONB on
|
|
* the server). No client-side cache here.
|
|
*
|
|
* Resume-by-step is intentionally not persisted: every onboarding
|
|
* entry starts at Welcome. The questionnaire is the only piece of
|
|
* progress that survives a re-entry — it pre-fills Step 1 so the
|
|
* user doesn't re-answer.
|
|
*/
|
|
export async function saveQuestionnaire(
|
|
answers: Partial<QuestionnaireAnswers>,
|
|
): Promise<void> {
|
|
const user = await api.patchOnboarding({ questionnaire: answers });
|
|
useAuthStore.getState().setUser(user);
|
|
// Mirror the three cohort signals into person properties so every
|
|
// PostHog event on this user can be broken down by role / use_case /
|
|
// team_size without re-joining the DB. Matches the $set block the
|
|
// server writes alongside `onboarding_questionnaire_submitted`.
|
|
if (answers.team_size || answers.role || answers.use_case) {
|
|
setPersonProperties({
|
|
...(answers.team_size ? { team_size: answers.team_size } : {}),
|
|
...(answers.role ? { role: answers.role } : {}),
|
|
...(answers.use_case ? { use_case: answers.use_case } : {}),
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Finalize onboarding. POST /complete marks `onboarded_at` atomically
|
|
* (COALESCE-guarded for idempotency). We then refresh the auth store
|
|
* so every gate sees the updated user.
|
|
*
|
|
* `completionPath` is the client's view of which Step-3 exit the user
|
|
* took; the server funnel-splits `onboarding_completed` on this value.
|
|
* Legacy callers that don't pass a path get recorded as `unknown`.
|
|
*/
|
|
export async function completeOnboarding(
|
|
completionPath?: OnboardingCompletionPath,
|
|
): Promise<void> {
|
|
await api.markOnboardingComplete(
|
|
completionPath ? { completion_path: completionPath } : undefined,
|
|
);
|
|
await useAuthStore.getState().refreshMe();
|
|
}
|
|
|
|
/**
|
|
* Records interest in cloud runtimes. Pure side effect — does NOT
|
|
* complete onboarding; the user still has to pick a real Step 3
|
|
* path (CLI with a detected runtime) or Skip to move on.
|
|
*
|
|
* Returned user object is not synced into the auth store because no
|
|
* user-visible field (`onboarded_at`, anything in `UserResponse`)
|
|
* actually changes here.
|
|
*/
|
|
export async function joinCloudWaitlist(
|
|
email: string,
|
|
reason: string,
|
|
): Promise<void> {
|
|
await api.joinCloudWaitlist({ email, reason });
|
|
}
|