Files
fusion/packages/dashboard/app/components/__tests__/onboarding-events.test.ts
Fusion 8a8f6bf938 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
2026-04-17 08:01:38 -07:00

173 lines
5.8 KiB
TypeScript

import { beforeEach, afterEach, describe, expect, it, vi } from "vitest";
import {
__test_clearSessionId,
clearOnboardingEvents,
getOnboardingEvents,
getOnboardingSessionId,
trackOnboardingEvent,
} from "../onboarding-events";
const EVENTS_STORAGE_KEY = "fusion_onboarding_events";
const SESSION_STORAGE_KEY = "fusion_onboarding_session_id";
describe("onboarding-events", () => {
let consoleInfoSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
localStorage.removeItem(EVENTS_STORAGE_KEY);
sessionStorage.removeItem(SESSION_STORAGE_KEY);
__test_clearSessionId();
consoleInfoSpy = vi.spyOn(console, "info").mockImplementation(() => {});
});
afterEach(() => {
consoleInfoSpy.mockRestore();
vi.restoreAllMocks();
vi.unstubAllGlobals();
localStorage.removeItem(EVENTS_STORAGE_KEY);
sessionStorage.removeItem(SESSION_STORAGE_KEY);
__test_clearSessionId();
});
it("stores tracked events in localStorage", () => {
const setItemSpy = vi.spyOn(window.localStorage, "setItem");
trackOnboardingEvent("onboarding:wizard-opened", { source: "initial" });
expect(setItemSpy).toHaveBeenCalledWith(EVENTS_STORAGE_KEY, expect.any(String));
const raw = localStorage.getItem(EVENTS_STORAGE_KEY);
expect(raw).toBeTruthy();
const events = JSON.parse(raw ?? "[]") as Array<Record<string, unknown>>;
expect(events).toHaveLength(1);
expect(events[0]).toMatchObject({
type: "onboarding:wizard-opened",
metadata: { source: "initial" },
});
expect(typeof events[0]?.timestamp).toBe("string");
});
it("accumulates events in insertion order", () => {
trackOnboardingEvent("onboarding:step-completed", { step: "ai-setup" });
trackOnboardingEvent("onboarding:step-completed", { step: "github" });
trackOnboardingEvent("onboarding:step-completed", { step: "first-task" });
const events = getOnboardingEvents();
expect(events).toHaveLength(3);
expect(events[0]?.metadata.step).toBe("ai-setup");
expect(events[1]?.metadata.step).toBe("github");
expect(events[2]?.metadata.step).toBe("first-task");
});
it("prunes ring buffer by 50 when event count exceeds 200", () => {
for (let index = 1; index <= 201; index += 1) {
trackOnboardingEvent("onboarding:step-completed", { step: "ai-setup", sequence: index });
}
const events = getOnboardingEvents();
expect(events).toHaveLength(151);
expect(events[0]?.metadata.sequence).toBe(51);
expect(events.at(-1)?.metadata.sequence).toBe(201);
});
it("clears stored events", () => {
const removeItemSpy = vi.spyOn(window.localStorage, "removeItem");
trackOnboardingEvent("onboarding:finished");
expect(getOnboardingEvents()).toHaveLength(1);
clearOnboardingEvents();
expect(getOnboardingEvents()).toEqual([]);
expect(removeItemSpy).toHaveBeenCalledWith(EVENTS_STORAGE_KEY);
});
it("returns a stable session id within one browser session", () => {
const randomUuidSpy = vi.spyOn(globalThis.crypto, "randomUUID").mockReturnValue("123e4567-e89b-12d3-a456-426614174000");
const first = getOnboardingSessionId();
const second = getOnboardingSessionId();
expect(first).toBe(second);
expect(first).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);
expect(randomUuidSpy).toHaveBeenCalledTimes(1);
});
it("regenerates session id across storage resets", () => {
const randomUuidSpy = vi
.spyOn(globalThis.crypto, "randomUUID")
.mockReturnValueOnce("123e4567-e89b-12d3-a456-426614174000")
.mockReturnValueOnce("123e4567-e89b-12d3-a456-426614174001");
const first = getOnboardingSessionId();
sessionStorage.removeItem(SESSION_STORAGE_KEY);
__test_clearSessionId();
const second = getOnboardingSessionId();
expect(first).not.toBe(second);
expect(randomUuidSpy).toHaveBeenCalledTimes(2);
});
it("is SSR-safe when window is unavailable", () => {
vi.stubGlobal("window", undefined);
expect(getOnboardingEvents()).toEqual([]);
expect(() => trackOnboardingEvent("onboarding:finished")).not.toThrow();
});
it("stores events with the expected structure", () => {
trackOnboardingEvent("onboarding:step-completed", { step: "github", source: "test" });
const [event] = getOnboardingEvents();
expect(event).toBeDefined();
expect(event).toMatchObject({
type: "onboarding:step-completed",
metadata: { step: "github", source: "test" },
});
expect(typeof event?.sessionId).toBe("string");
expect(event?.sessionId.length).toBeGreaterThan(0);
expect(Date.parse(event?.timestamp ?? "")).not.toBeNaN();
});
it("defaults metadata to an empty object", () => {
trackOnboardingEvent("onboarding:finished");
const [event] = getOnboardingEvents();
expect(event?.metadata).toEqual({});
});
it("handles storage quota errors without throwing", () => {
const setItemSpy = vi.spyOn(window.localStorage, "setItem").mockImplementation(() => {
throw new Error("QuotaExceededError");
});
expect(() => trackOnboardingEvent("onboarding:finished")).not.toThrow();
expect(setItemSpy).toHaveBeenCalled();
});
it("logs events in DEV mode", () => {
trackOnboardingEvent("onboarding:finished");
expect(consoleInfoSpy).toHaveBeenCalledWith("[fusion:onboarding]", expect.any(String));
});
it("resets session id via __test_clearSessionId", () => {
const randomUuidSpy = vi
.spyOn(globalThis.crypto, "randomUUID")
.mockReturnValueOnce("123e4567-e89b-12d3-a456-426614174000")
.mockReturnValueOnce("123e4567-e89b-12d3-a456-426614174001");
const first = getOnboardingSessionId();
__test_clearSessionId();
const second = getOnboardingSessionId();
expect(first).not.toBe(second);
expect(randomUuidSpy).toHaveBeenCalledTimes(2);
});
});