feat(FN-1998): instrument onboarding funnel analytics events

- Add onboarding event tracking utility with localStorage persistence, session IDs, and ring-buffer pruning
- Emit funnel events from ModelOnboardingModal for step progress, dismissal/completion, and first-task/CTA actions
- Track resume-card and auto-trigger onboarding entry points in OnboardingResumeCard and useAuthOnboarding
- Expand dashboard tests and mocks to validate onboarding event storage behavior and new instrumentation calls
This commit is contained in:
Fusion
2026-04-17 08:01:38 -07:00
committed by gsxdsm
parent bdfb4842d2
commit 1433ca98ce
8 changed files with 474 additions and 3 deletions

View File

@@ -411,6 +411,7 @@ import {
getStepData,
type OnboardingStep,
} from "./model-onboarding-state";
import { trackOnboardingEvent } from "./onboarding-events";
import type { SectionId } from "./SettingsModal";
export interface ModelOnboardingModalProps {
@@ -492,6 +493,9 @@ export function ModelOnboardingModal({
});
const pollCountRef = useRef<number>(0);
const previousCreatedTaskRef = useRef<Task | null | undefined>(firstCreatedTask);
const hasTrackedWizardOpenRef = useRef(false);
const resumedFromStep = persistedState?.currentStep;
const isResumedFlow = !!persistedState && persistedState.currentStep !== "complete";
// Initialize skippedProviders from persisted state
const [skippedProviders, setSkippedProviders] = useState<Record<string, boolean>>(
@@ -519,6 +523,18 @@ export function ModelOnboardingModal({
}
}, [step, completedSteps, skippedSteps]);
useEffect(() => {
if (hasTrackedWizardOpenRef.current) {
return;
}
hasTrackedWizardOpenRef.current = true;
trackOnboardingEvent("onboarding:wizard-opened", {
source: isResumedFlow ? "resume" : "initial",
resumedFromStep,
});
}, [isResumedFlow, resumedFromStep]);
useEffect(() => {
const hadCreatedTask = previousCreatedTaskRef.current != null;
const hasCreatedTask = firstCreatedTask != null;
@@ -775,6 +791,7 @@ export function ModelOnboardingModal({
setCompletedSteps((prev) => [...new Set([...prev, step])]);
// Completing a step clears any prior skipped status
setSkippedSteps((prev) => prev.filter((s) => s !== step));
trackOnboardingEvent("onboarding:step-completed", { step });
if (step === "github" && !isGithubAuthenticated) {
setGitHubSkippedState(false);
@@ -791,6 +808,7 @@ export function ModelOnboardingModal({
const handleSkip = useCallback(() => {
setSkippedSteps((prev) => [...new Set([...prev, step])]);
markStepSkipped(step);
trackOnboardingEvent("onboarding:step-skipped", { step });
if (step === "github" && !isGithubAuthenticated) {
setGitHubSkippedState(true);
@@ -1126,11 +1144,12 @@ export function ModelOnboardingModal({
setSaving(true);
try {
await completeOnboarding();
trackOnboardingEvent("onboarding:completed", { completedSteps, skippedSteps });
setStep("complete");
} finally {
setSaving(false);
}
}, [completeOnboarding]);
}, [completeOnboarding, completedSteps, skippedSteps]);
const handleCreateFirstTask = useCallback(async () => {
const trimmedDescription = firstTaskDescription.trim();
@@ -1148,6 +1167,7 @@ export function ModelOnboardingModal({
const createdTask = await createTask({ description: trimmedDescription });
setInlineCreatedTask(createdTask);
setShowTaskCreated(true);
trackOnboardingEvent("onboarding:first-task-created", { taskId: createdTask?.id });
addToast("Task created", "success");
success = true;
} catch (err: unknown) {
@@ -1177,6 +1197,7 @@ export function ModelOnboardingModal({
}
// Keep onboarding open so task creation can hand back to a success state
trackOnboardingEvent("onboarding:open-new-task", {});
onOpenNewTask?.();
}, [completeOnboarding, onOpenNewTask]);
@@ -1193,11 +1214,18 @@ export function ModelOnboardingModal({
// Close modal and trigger callback
setIsOpen(false);
onComplete();
trackOnboardingEvent("onboarding:open-github-import", {});
onOpenGitHubImport?.();
}, [completeOnboarding, onComplete, onOpenGitHubImport]);
// Dismiss without completing (still marks onboarding complete)
const handleDismiss = useCallback(async () => {
trackOnboardingEvent("onboarding:dismissed", {
currentStep: step,
completedSteps,
skippedSteps,
});
setSaving(true);
try {
await updateGlobalSettings({ modelOnboardingComplete: true });
@@ -1206,10 +1234,11 @@ export function ModelOnboardingModal({
}
setIsOpen(false);
onComplete();
}, [onComplete]);
}, [step, completedSteps, skippedSteps, onComplete]);
// Close from the completion step
const handleFinish = useCallback(() => {
trackOnboardingEvent("onboarding:finished", {});
setIsOpen(false);
onComplete();
}, [onComplete]);