Website Retention Tracker
Problem
Design WebsiteRetentionTracker to record the distinct users who visit a website on each calendar date and calculate adjacent-day retention. Multiple threads may call one shared tracker instance.
Requirements
- Distinct daily visitors.
recordVisitadds the user to the supplied date's visitor cohort. Repeating the same user and date has no additional effect. - Adjacent-day retention. If the immediately previous calendar date has visitors,
calculateRetentionRate(date)returns the number of those visitors who also visited ondate, divided by the number of distinct previous-day visitors. - Empty previous day. If no visitors were recorded for the immediately previous calendar date,
calculateRetentionRatereturns0.0. - Valid inputs. A user ID must be from
1through2147483647. A date must be a zero-paddedYYYY-MM-DDstring naming a valid proleptic Gregorian date from0001-01-01through9999-12-31. - Shared access. Calls on one shared instance are linearizable.
API
| Signature | Returns | Behavior |
|---|---|---|
WebsiteRetentionTracker() | Not applicable | Creates an empty tracker. |
recordVisit(userId: integer, date: string) | void | Records one visit. Invalid input raises InvalidArgumentError without changing any cohort. |
calculateRetentionRate(date: string) | number | Calculates retention for date. An invalid date or 0001-01-01 raises InvalidArgumentError. |
Examples
| Step | Operation | Result |
|---|---|---|
| 1 | recordVisit(3, "2026-07-29") | void |
| 2 | recordVisit(5, "2026-07-29") | void |
| 3 | recordVisit(2, "2026-07-29") | void |
| 4 | recordVisit(7, "2026-07-29") | void |
| 5 | recordVisit(4, "2026-07-30") | void |
| 6 | recordVisit(7, "2026-07-30") | void |
| 7 | recordVisit(9, "2026-07-30") | void |
| 8 | calculateRetentionRate("2026-07-30") | 0.25 |
Constraints
- At most
100000distinct dates,1000000recorded visits, and200000public calls occur per testcase. - At most
100000distinct visitors occur on one date. - Visitor counts fit in a signed 64-bit integer.
- Retention results are finite values from
0.0through1.0.
Notes
Both methods may run concurrently. Linearizable means that each call takes effect at one instant between its invocation and return. A query observes both adjacent cohorts at the same such instant, so completed recordings are not lost and the result matches one legal ordering of overlapping calls. Calls within one thread retain program order; calls across threads may use any ordering with that property. Neither method waits for future visits or external events, and finite lock contention is allowed, but calls must not deadlock.