Skip to content

fix(analytics): one PostHog person per user (alias merge) + Supabase-id identity - #570

Merged
cravenceiling merged 3 commits into
mainfrom
fix/posthog-identity-alias-merge
Jun 25, 2026
Merged

fix(analytics): one PostHog person per user (alias merge) + Supabase-id identity#570
cravenceiling merged 3 commits into
mainfrom
fix/posthog-identity-alias-merge

Conversation

@cravenceiling

@cravenceiling cravenceiling commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

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 own install_id distinct_id; the shared alias merges them). install_id stays the distinct_id, so the website /welcome UTM bridge and the onboarding funnel are untouched.
  • setPersonProperties stamps supabase_user_id (+ email $set, signup_date $set_once) so the id is a queryable join key to Supabase, not only an internal alias.
  • flips the auth_status super property to authenticated.

Activation also moves to the user's first chat_message_sent (the user acts), not chat_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 per install_id (21 persons for a single test email), and production relies on the alias merge today ($create_alias events). The PR's premise that alias is "a no-op" conflated alias (which merges) with identify (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 == Supabase auth.users.id (91e81ad9-...) -- the join works
  • email, signup_date ($set_once), is_activated=true on the person
  • auth_status=authenticated on post-sign-in events incl. chat_message_sent
  • $create_alias fired -- the merge path is live

Files

  • app/src/lib/analytics.ts -- identifyUser aliases + sets properties; activation on chat_message_sent
  • app/src/App.tsx -- sign-in effect wiring + comment
  • knowledge-base/production-infra.md, knowledge-base/data-rituals.md -- identity + activation model docs

Typecheck (pnpm tsc --noEmit) passes.

🤖 Generated with Claude Code

felipesalinasr and others added 3 commits June 21, 2026 18:59
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>
@cravenceiling
cravenceiling merged commit 096b45e into main Jun 25, 2026
@cravenceiling
cravenceiling deleted the fix/posthog-identity-alias-merge branch June 25, 2026 21:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants