fix(analytics): identify every user in PostHog by Supabase user id - #562
Draft
felipesalinasr wants to merge 1 commit into
Draft
fix(analytics): identify every user in PostHog by Supabase user id#562felipesalinasr wants to merge 1 commit into
felipesalinasr wants to merge 1 commit into
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>
cravenceiling
added a commit
that referenced
this pull request
Jun 25, 2026
…id identity (#570) * fix(analytics): attach Supabase user id to PostHog person on sign-in 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> * docs(analytics): align KB with Supabase-id identity + chat_message_sent 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> * fix(analytics): alias() so each user is one PostHog person, not one-per-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> --------- Co-authored-by: Felipe Salinas Rangel <felipe.salinasr@gmail.com> Co-authored-by: Claude Opus 4.8 <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.
Problem
PostHog identifies the device (
install_id), not the user, so events can't be joined to Supabase and one human across devices/reinstalls counts as multiple people. Verified against the live DB: signed-in users' PostHogdistinct_ids do not match theirauth.users.id(e.g.juanframartzz@gmail.com→ PostHog13aa0e33…vs Supabase73c26eb8…).Root cause
analytics.init()callsposthog.identify(install_id)on every launch → locksdistinct_idto the device.analytics.alias(userId)→posthog.identify(userId), but PostHog silently ignores re-identifying an already-identified person, so the Supabase id never landed anywhere queryable. Onlyemailgot$set, which masked the gap.Fix
Keep
install_idas thedistinct_id— the website/welcomeUTM bridge and the sequential onboarding funnel both depend on it — and attach the Supabase identity as person properties at sign-in. Every authenticated person now carries a reliable, queryable join key to Supabase, with pre-login attribution untouched.analytics.ts: replace the misleadingalias()withidentifyUser(), which setssupabase_user_id+email($set) andsignup_date($set_once) viaposthog.setPersonProperties, and flipsauth_statustoauthenticated.App.tsx: callanalytics.identifyUser(userId, { email, signupDate })on sign-in (signup date fromsession.user.created_at).alias(),personProps, and related types.Also: activation definition
is_activatednow flips on the user's firstchat_message_sent(agreed definition: activation = the user sends a message) instead ofchat_message_received(agent reply).Why person property, not re-identify
Re-pointing the
distinct_idto the user id would require dropping theinstall_ididentify ininit(), which would strand the website's UTM attribution (the/welcomebridge merges the anonymous website person intoinstall_id). The person-property approach achieves a reliable Supabase join without that risk.Scope / follow-ups (not in this PR)
signup_country(from the geo-ip work) andprofile_complete— needs the profiles row at sign-in.is_internal) — needs the team email list.app_error_shownvserror_shown,session_*,app_active/app_launched/user_returned).Test plan
pnpm/buntypecheck + lint pass.POSTHOG_KEY→ no-ops, no throws.supabase_user_id= the Supabaseauth.users.id,email,signup_date,auth_status = authenticated; pre-login funnel still connects.reset()to anonymous.supabase_user_idmatches Supabaseid.Full context:
posthog-identity-unification.md/product-metrics-definitions.md(product specs).🤖 Generated with Claude Code