Documentation
¶
Overview ¶
Package retention computes cohort retention — the other core product-analytics primitive: group users by the day they first showed up, then track what % come back on day 1, 2, ... N. Deterministic and storage-agnostic, like funnel.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func DayN ¶ added in v0.2.0
DayN is the daily-period alias for PeriodN, kept so existing callers read unchanged.
func Observable ¶ added in v0.29.0
Observable reports whether period n of the cohort starting at cohortDate is a FINISHED, countable period as of now — measured in the grid's OWN bucket unit (day, 7-day week or 30-day month), never in days.
It exists because the dashboard grid re-derived this rule by hand with `/ 86400` on both sides while `n` was a PERIOD index, so on a weekly grid it compared a day index against a day index plus a week count. Measured on a same-week cohort with no returns: the row rendered `20 | 100% | 0% | 0% | 0% | 0% | 0%` — W1..W5 presented as finished churn although those weeks begin 2 to 37 days in the FUTURE — while /v1/retention?bucket=week for the same instant returned [20,null,null,...]. Month buckets were off by 30x. One exported predicate so the grid, the JSON and the summary cannot answer the same question three ways.
n<=0 is the cohort baseline (its size, known the moment the cohort forms) and is always observable. n>=1 is observable only once the period has FULLY elapsed: the in-progress period is a partial count, and rendering a partial as final is what made the grid contradict the summary that had (correctly) excluded that cohort.
func PeriodN ¶ added in v0.9.0
PeriodN aggregates period-n retention across cohorts HONESTLY: only cohorts whose period-n has fully elapsed as of `now` enter the denominator. Users who signed up yesterday cannot have day-7 (or week-2) activity yet — counting them would systematically understate retention (the classic retention-triangle mistake), and reporting period-n at all when no cohort is old enough would be a fabricated 0%. Uses the Result's own bucket, so it is correct for daily, weekly, and monthly grids alike. Every surface that summarizes retention (verdict, MCP, ask) must use this.
Types ¶
type Cohort ¶
type Cohort struct {
Date time.Time `json:"date"`
Size int `json:"size"` // users first seen on this day
Returned []int `json:"returned"` // Returned[n] = users active n days after Date (Returned[0] == Size)
}
Cohort is one first-seen day and how many of its users returned on each later day.
type CohortJSON ¶ added in v0.9.1
type CohortJSON struct {
Date time.Time `json:"date"`
Size int `json:"size"`
Returned []*int `json:"returned"`
}
Summarize builds the honest headline retention percentages for a grid, picking a period set + labels that match the bucket (day 1/7/30, week 1/2/4, month 1/2/3). A period no cohort is old enough to observe is OMITTED, never reported as a fabricated 0%. This is the single source both the HTTP API and the MCP tool serialize, so the two can never disagree (agreement_test enforces it). Does NOT include the raw cohorts grid — callers add that. CohortJSON is the serialization shape shared by the HTTP API and the MCP tool, so the two can never disagree (agreement_test locks them). Returned[n] is nil for any period whose window has not started relative to now — an unobservable future day must serialize as null, never 0, or it reads as "retention cratered to 0%".
func SerializeCohorts ¶ added in v0.9.1
func SerializeCohorts(r Result, now time.Time) []CohortJSON
SerializeCohorts nulls out unobservable periods, one definition for every surface. A period n>=1 is observable ONLY once it has FULLY elapsed — the SAME rule PeriodN uses for the summary denominator (cohort-bucket + n < current-bucket). The current IN-PROGRESS period (cp+n == cur) is not final, so it serializes as null, never a partial count: rendering it as a finished number made the grid contradict the summary (which excluded that cohort). Period 0 is the cohort baseline (its size, known at signup) and is always shown.
type Result ¶
type Result struct {
Cohorts []Cohort `json:"cohorts"`
MaxDays int `json:"max_days"` // max periods measured (kept name for compat)
Bucket string `json:"bucket,omitempty"` // "day" (default), "week", or "month" (30-day)
Rolling bool `json:"rolling,omitempty"` // true = "active on OR AFTER period n" (unbounded)
}
Result is the full retention grid (one row per cohort period).
func Compute ¶
Compute builds daily n-day retention over maxDays — the default. A user belongs to the cohort of their first event's (UTC) day; they "return on day n" if they have any event on the day n days after their first. retentionEvent optionally filters which events count as activity (empty = any event).
func ComputeBucketed ¶ added in v0.9.0
func ComputeBucketed(events []event.Event, maxPeriods int, retentionEvent, bucket string, rolling bool) Result
ComputeBucketed generalizes Compute to week/month periods and rolling mode:
- bucket "week"/"month" groups cohorts + return periods into 7-/30-day blocks, so a weekly product's retention isn't understated by a daily read.
- rolling=true counts a user as retained at period n if they were active on period n OR ANY LATER period (unbounded retention), instead of exactly on period n (classic).