Of the people who first appeared in week 0, how many came back in week 1, week 2, week 3.
Pixelex.Query.Retention.cohorts("shop", range, bucket: "week")
%{
bucket: "week",
cohorts: [
%{cohort: ~U[2026-08-03 00:00:00Z], size: 412,
periods: [%{period: 0, users: 412, rate: 1.0},
%{period: 1, users: 173, rate: 0.42},
%{period: 2, users: 98, rate: 0.238}]},
…
]
}This works on user_id, and it has to
visitor_id is a keyed hash under a salt that rotates every UTC day.
Tomorrow the same person hashes to a different value — that is what makes
pixelex cookieless, and it makes anonymous retention arithmetically
impossible, not merely unimplemented. Retaining a stable identifier across
days means putting one on the visitor's device, and putting one there is
exactly what ePrivacy Article 5(3) governs.
So this counts signed-in users, from Pixelex.identify/3 onward. If nothing
calls identify/3, every cohort is empty and that is the honest answer
rather than a plausible-looking wrong one.
Cohorts are scoped to the window
A user's cohort is the bucket of their first event inside the range, not their first event ever. Someone who signed up last year and returned today appears in today's cohort. Widening the range fixes it and costs more; there is no third option that is both bounded and correct, and an unbounded scan of the events table is not an option at all.
:first_seen_from moves just the first-seen lookback earlier while keeping
the activity window narrow, which is the useful middle:
cohorts("shop", last_30_days, first_seen_from: ninety_days_ago)
Summary
Functions
Retention grid for signed-in users.
Functions
@spec cohorts(String.t(), Pixelex.Query.Range.t(), keyword()) :: map()
Retention grid for signed-in users.
Options
:bucket—"day","week"(default) or"month":first_seen_from— aDateTimeto look further back for first-seen:event— only count activity on this event name:limit— maximum cohorts returned (default 26)