fix(analytics): one PostHog person per user (alias merge) + Supabase-id identity - #570
Merged
Merged
Conversation
PostHog identifies the device (install_id), not the user, so events could not be joined to Supabase and a user across devices/reinstalls counted as multiple people. Verified against the live DB: signed-in users' PostHog distinct_ids did not match their auth.users.id. Root cause: init() calls posthog.identify(install_id) at startup, which locks the distinct_id to the device. The sign-in path then called posthog.identify(userId), which PostHog silently ignores once a person is already identified — so the Supabase id never landed anywhere queryable (only email was $set, which masked the gap). Fix: keep install_id as the distinct_id (the website /welcome UTM bridge and the sequential onboarding funnel depend on it) and attach supabase_user_id + email + signup_date as PERSON PROPERTIES at sign-in. Every authenticated person now carries a reliable join key to Supabase with pre-login attribution untouched. Replaces the misleading alias() with identifyUser(). Also aligns activation with the agreed definition: is_activated now flips on the user's first chat_message_sent (user sends a message) instead of chat_message_received (agent reply). Join user-level metrics on supabase_user_id (not distinct_id) so a user on two devices (two install_ids, one supabase_user_id) dedupes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nt activation PR #562 changed the PostHog identity model and the activation milestone but left the analytics KBs describing the old model. Bring them in sync. production-infra.md: - Install identity now STAYS install_id as distinct_id after sign-in (the /welcome UTM bridge + onboarding funnel depend on it); was documented as "alias/identify merges history to the Supabase user". - User identity: supabase_user_id is the queryable join key (person property), not distinct_id. Join user-level metrics on supabase_user_id so one human on two devices dedupes. Drop the email_domain claim (never set in code). - Activation milestone chat_message_received -> chat_message_sent, with a note to migrate the PostHog-side activation event so server insights match the is_activated person property, and that the cutover is a discontinuity in longitudinal activation comparisons. - PostHog merge bullet rewritten to identifyUser(userId,{email,signupDate}) + setPersonProperties (supabase_user_id/email $set, signup_date $set_once). data-rituals.md: - Activation tile, weekly activated-users count, time-to-activation, and the Activated-users cohort all keyed on chat_message_sent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…er-device PR #562 removed posthog.alias() and attached the Supabase id only as a person property. Verified against the live project (396231): that leaves a human fragmented into one PostHog person per install_id (21 persons for a single test email), and production today still relies on the alias merge ($create_alias events). Shipping the removal would fragment every prod user and force supabase_user_id dedupe in every insight. The PR's premise that alias is "a no-op" conflated alias with identify: identify(userId) is ignored once a person is identified, but alias(userId) DOES merge -- each device/reinstall aliases the same Supabase id, so PostHog stitches them into one person. Fix: do both. alias(userId) merges the human across devices/reinstalls (distinct_id stays install_id, so the /welcome UTM bridge + onboarding funnel are untouched) AND setPersonProperties keeps supabase_user_id as the queryable Supabase join key. reset() on sign-out still hands out a fresh distinct_id, so a shared device can't merge two people. Docs (production-infra.md) updated to the alias-merge + property model. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The complete, verified PostHog <-> Supabase identity fix. Builds on #562 and corrects the one flaw that made it fragment users.
On sign-in,
analytics.identifyUser:alias(supabase_user_id)stitches a human's per-device / per-reinstall PostHog persons into ONE (each keeps its owninstall_iddistinct_id; the shared alias merges them).install_idstays the distinct_id, so the website/welcomeUTM bridge and the onboarding funnel are untouched.setPersonPropertiesstampssupabase_user_id(+email$set,signup_date$set_once) so the id is a queryable join key to Supabase, not only an internal alias.auth_statussuper property toauthenticated.Activation also moves to the user's first
chat_message_sent(the user acts), notchat_message_received(agent reply).Why not #562 as-is
#562 removed
posthog.alias()and attached the Supabase id only as a person property. Verified against the live project (396231): that leaves one human fragmented into one PostHog person perinstall_id(21 persons for a single test email), and production relies on the alias merge today ($create_aliasevents). The PR's premise that alias is "a no-op" conflatedalias(which merges) withidentify(which is the no-op once a person is identified). This PR keeps both: alias for the merge, property for the queryable join key.Verified locally against live PostHog
person.supabase_user_id== Supabaseauth.users.id(91e81ad9-...) -- the join worksemail,signup_date($set_once),is_activated=trueon the personauth_status=authenticatedon post-sign-in events incl.chat_message_sent$create_aliasfired -- the merge path is liveFiles
app/src/lib/analytics.ts--identifyUseraliases + sets properties; activation onchat_message_sentapp/src/App.tsx-- sign-in effect wiring + commentknowledge-base/production-infra.md,knowledge-base/data-rituals.md-- identity + activation model docsTypecheck (
pnpm tsc --noEmit) passes.🤖 Generated with Claude Code