Skip to content

feat(db): resolve profile_id via profile_aliases in funnel & conversion - #329

Merged
ayushjhanwar-png merged 1 commit into
mainfrom
feat/funnel-conversion-alias-resolution
Jun 23, 2026
Merged

feat(db): resolve profile_id via profile_aliases in funnel & conversion#329
ayushjhanwar-png merged 1 commit into
mainfrom
feat/funnel-conversion-alias-resolution

Conversation

@ayushjhanwar-png

Copy link
Copy Markdown

What

Resolve anonymous profile_id → canonical (backend_userid) at read time via the profile_aliases identity map, so funnels/conversions that start on an anon event (logInInitiate, $ae_first_open, _first_event) and end on an identified event (logIn, trialStarted) stitch into a single user instead of reading as two separate people.

This is the read side of the identity-merge work (write path shipped earlier: forked #322 + proxy alias emit). Nothing read profile_aliases until now.

How

  • conversion.service.ts — resolve profile_id at the event-source CTE (buildSingleEventCte) via a shared al alias map (argMax(profile_id, created_at) per alias). start_events/end_events dedup, the ASOF join and the end⊆start prefilter then all use the canonical id automatically.
  • funnel.service.ts — resolve at the group level (post-union), so custom events are handled with no special-casing; the time-to-convert path is resolved too.

Gating (no hack — self-gates on data)

There is no env var / allowlist. The resolution is a no-op for projects with no aliases (coalesce(nullIf(al.canonical,''), profile_id) just returns the raw id), so projects without profile_aliases rows are byte-identical to before. The only conditions are correctness, not config:

  • profile-level only (session grouping already stitches within a session);
  • skipped when cohorts are involved (cohort joins match raw profile_id);
  • in conversion, skipped when either event is a custom event (resolving per-CTE would mix raw/resolved); funnel doesn't need this since it resolves after the union.

The raw column is qualified as events.profile_id so the SELECT output alias (also profile_id) doesn't bind into the join key.

Validation (dashreels, dev)

funnel before after
logInInitiate → logIn (7d) 0.19% 76.8%
_first_event → logIn (1d) ~0% 71%, median TTC 20s
$ae_first_open → trialStarted (1mo) ~0% 13.3%

Typecheck clean on both files.

Out of scope / follow-ups

  • Custom-event and cohort funnels stay unresolved (documented guards) — follow-up if needed.
  • Historical cohorts (pre-live-alias) under-resolve due to alias-backfill coverage — separate backfill fix, not a code change.

🤖 Generated with Claude Code

Stitch anonymous -> identified profiles at read time so funnels/conversions
that start on an anon event (logInInitiate, $ae_first_open, _first_event) and
end on an identified event (logIn, trialStarted) connect into one user.

- conversion.service: resolve profile_id at the event-source CTE
  (buildSingleEventCte) via a shared `al` alias map; start/end dedup, the ASOF
  join and the end-in-start prefilter all use the canonical id automatically.
- funnel.service: resolve at the group level (post-union) so custom events are
  handled too; also resolves the time-to-convert path.

Self-gating: no env/allowlist - a no-op for projects with no aliases
(coalesce keeps the raw id). Skipped for session-level grouping, cohorts
(cohort joins match raw profile_id) and, in conversion, custom events (would
mix raw/resolved). Raw column qualified as events.profile_id so the SELECT
output alias does not bind into the join key.

Validated on dashreels: logInInitiate->logIn 0.19% -> 76.8% (7d);
_first_event->logIn 71% with 20s median time-to-convert.
@ayushjhanwar-png
ayushjhanwar-png merged commit c4e3bc0 into main Jun 23, 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.

2 participants