mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-30 07:10:49 +02:00
Issue funnel attribution: `onboarding_completed` previously joined to `onboarding_started` on `distinct_id` alone, which collapsed skip_existing / invite_accept completions into the same bucket as real funnel completions. PostHog's 30-day backfill could only link 20 / 152 completions back to a start. Generate an `onboarding_session_id` on `onboarding_started`, persist it to client storage so it survives reloads, attach it as a property on every onboarding event (client and server), and clear it on `onboarding_completed`. Skip / invite paths never receive a session id; HogQL funnels filter `onboarding_session_id IS NOT NULL` to isolate real funnel completions from soft completions. Co-authored-by: multica-agent <github@multica.ai>
64 lines
2.4 KiB
TypeScript
64 lines
2.4 KiB
TypeScript
// Onboarding session identifier — issued once per funnel entry and attached
|
|
// to every onboarding event so PostHog can correlate the full funnel back to
|
|
// a single `onboarding_started`. Solves the prior funnel-attribution gap
|
|
// where `onboarding_completed` events fired from skip/invite paths (no
|
|
// `onboarding_started` in their lineage) were indistinguishable from real
|
|
// funnel completions when joining on `distinct_id` alone.
|
|
//
|
|
// The id is persisted to client storage because the funnel spans page
|
|
// reloads (especially on web — desktop bundle install, OAuth redirects).
|
|
// It's cleared on completion; entering onboarding again starts a fresh
|
|
// session and a fresh id.
|
|
|
|
import { createSafeId } from "../utils";
|
|
import { defaultStorage } from "../platform/storage";
|
|
|
|
const STORAGE_KEY = "multica_onboarding_session_id";
|
|
|
|
// In-memory cache so the analytics wrapper doesn't hit storage on every
|
|
// event. Storage is read once on first access and on every start/clear.
|
|
let cached: string | null | undefined;
|
|
|
|
function read(): string | null {
|
|
if (cached !== undefined) return cached;
|
|
cached = defaultStorage.getItem(STORAGE_KEY);
|
|
return cached;
|
|
}
|
|
|
|
/**
|
|
* Generate a new session id and persist it. Idempotent — calling twice in
|
|
* the same funnel returns the same id, so a re-mount of the onboarding
|
|
* shell can't accidentally split one funnel across two sessions.
|
|
*
|
|
* The expected fire site is the same place that emits `onboarding_started`.
|
|
*/
|
|
export function startOnboardingSession(): string {
|
|
const existing = read();
|
|
if (existing) return existing;
|
|
const id = createSafeId();
|
|
cached = id;
|
|
defaultStorage.setItem(STORAGE_KEY, id);
|
|
return id;
|
|
}
|
|
|
|
/**
|
|
* Read the current session id. Returns null when no onboarding session is
|
|
* in progress — the analytics wrapper omits the property in that case
|
|
* rather than emitting an empty string, so HogQL queries can filter
|
|
* `onboarding_session_id IS NOT NULL` to isolate real funnel events from
|
|
* skip/invite paths that legitimately have no session.
|
|
*/
|
|
export function getOnboardingSessionId(): string | null {
|
|
return read();
|
|
}
|
|
|
|
/**
|
|
* Clear the session. Called at the funnel terminus (after a successful
|
|
* `onboarding_completed`) so a returning user who somehow re-enters
|
|
* onboarding starts a fresh session.
|
|
*/
|
|
export function clearOnboardingSession(): void {
|
|
cached = null;
|
|
defaultStorage.removeItem(STORAGE_KEY);
|
|
}
|