Skip to content

feat(identity): write profile_aliases on sign-in (anon → identified) - #322

Merged
ayushjhanwar-png merged 1 commit into
mainfrom
feat/profile-alias-write-path
Jun 17, 2026
Merged

feat(identity): write profile_aliases on sign-in (anon → identified)#322
ayushjhanwar-png merged 1 commit into
mainfrom
feat/profile-alias-write-path

Conversation

@ayushjhanwar-png

@ayushjhanwar-png ayushjhanwar-png commented Jun 16, 2026

Copy link
Copy Markdown

What

Start populating the (currently unused) profile_aliases ClickHouse table with (anonymous id) → (identified profile id) mappings when a user signs in.

This is the write path only of identity merge. Nothing reads the table yet — we ship collection first so it fills with real data to validate the read side against.

Why

Anonymous events and post-sign-in events land on different profile_ids (the Mixpanel/anon id vs the identified backend_userid). Because funnels group by profile_id, one user looks like two people, collapsing first-open → conversion funnels.

Upstream OpenPanel hasn't solved this (issues #77 "Stitching", #270, #173) — their blocker is rewriting event rows. We sidestep it the way Mixpanel does: keep a small profile_aliases mapping and resolve at query time (a later PR) — zero event rows rewritten.

Changes

  • packages/db/src/services/profile.service.ts — new upsertAlias({ projectId, profileId, alias }). Direct insert (one row per sign-in; no buffer needed). Guards drop empty ids and self-maps (alias === profileId) so we never create a chain.
  • apps/api/src/controllers/track.controller.ts — re-enable the alias track op (was a 400 "Alias is not supported" stub) to call upsertAlias.

Activation (required companion — separate repo)

The code is inert until the openpanel/mixpanel-proxy change emits the alias op on sign-in. PR: Dashverse/openpanel#80. Ship together.

Not in this PR (separate follow-ups)

  • Query-time resolution in funnels (conversion.service + funnel.service), gated by a project allowlist, deduped with argMax(profile_id, created_at).
  • Historical backfill of aliases from Databricks gold.
  • Chart / retention / overview resolution.

Notes

  • profile_aliases is plain MergeTree, so the proxy emits once per sign-in (deduped within batch), not per event. Occasional dupes are tolerated — the read PR resolves with argMax(profile_id, created_at).
  • Pre-existing typecheck errors in chart/import/notification/reports are unrelated (in-progress distribution-chart work); the two files changed here typecheck clean.

Start populating the (unused) profile_aliases table so anonymous events can
later be merged onto the identified profile at query time — the Mixpanel
model: never rewrite event rows, keep a small mapping and resolve on read.

- add upsertAlias() writer in profile.service.ts (direct insert; guards drop
  empty ids and self-maps so we never create a chain)
- re-enable the `alias` track op in track.controller.ts to call it
  (was stubbed to "Alias is not supported")

Write path only. Nothing reads profile_aliases yet — query-time resolution
(funnels), historical backfill, and chart/retention resolution are each a
separate follow-up PR. The companion mixpanel-proxy change (emit the alias
on sign-in) is what activates this; the code is inert until that ships.
@ayushjhanwar-png
ayushjhanwar-png merged commit 0ba4547 into main Jun 17, 2026
4 checks passed
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.

1 participant