PostHog: who gets the new feature, and what should you watch?
PostHog combines product analytics, session replay, experiments and feature flags. Teams can open a new feature to selected users, then use events, replays and errors to decide whether to expand it.
Break down the problem and try your own design and implementation first. Then explore the reasoning to check states, edge cases, trade-offs, and ways to test your approach. The guide offers a reasoning path, not a complete implementation.
How can many rollout changes be counted without repeated full scans?
Scenario
Design a pure in-memory rollout-impact previewer. It runs no PostHog SDK and calls no release API. The eligible population is fixed and an upstream system supplies each user's bucket. Process many threshold changes without rescanning all users for every change.
Input and output contract
- users contains at most 100,000 entries. Each is a non-null object with a unique nonempty string id and integer bucket, where 0≤bucket<10,000. Different users may share a bucket. Ignore other fields and treat input as read-only. Buckets are already computed: do not implement hashing or assume a uniform distribution. Total ID length is L.
- initial is an integer from 0 through 10,000. targets contains at most 100,000 integers in that same range. Threshold t enables only users with bucket<t. Each target changes from the preceding threshold; the first changes from initial. A 10% setting is 1000; 100% is 10000.
- For every change output beforeCount, afterCount, addedCount and removedCount. Return counts only, without constructing membership lists for every step. An optional detailIndex must be a valid index in targets; omitting it requests no detail.
- Only for the detailIndex step, return actual added and removed IDs in original users input order, as addedIds and removedIds. Both lists are empty if nothing changes. Other steps must not produce complete membership lists.
- Any invalid listed field or index, or a duplicate ID, rejects the whole batch with its location. Return no partial result and mutate no input. Validate only the fields specified here. Empty users and targets are valid, but empty targets cannot have a detailIndex.
Trace the example
In input order the users are A:100, B:1000, C:1000, D:2999 and E:9000. initial=1000, targets=[3000,1000,1000,0,10000], detailIndex=1. Give all five sets of counts and the detail IDs. Explain whether a bucket equal to the threshold is enabled and how B and C move together.
Also check: (a) an added duplicate ID with another bucket; (b) reordered users with unchanged buckets; (c) omitted detailIndex; (d) every user sharing one bucket. A 30% setting does not imply exactly 30% of this population matches.
Deliverables
Describe one preprocessing pass, batch counting and single-step detail extraction. Explain the required indexes or cumulative information. Avoid O(nq) full scans. Analyze time and space using n users, q changes, 10,000 buckets, L and k detail IDs. State at least four invariants. Do not provide a full implementation or line-by-line pseudocode.
Explore the reasoning
- Validate the whole input first. An ID set detects duplicates while retaining both positions. Check integer ranges for buckets and thresholds. Validate detailIndex against targets length; do not implicitly select an empty array.
- Ask how many users are below a threshold. With only 10,000 buckets, count users per bucket and build prefix counts. Threshold 0 means nobody; 10000 means everyone. Sorted buckets and lower_bound are another option, without sorting users in place.
- Calculate differences between adjacent thresholds. Read afterCount at the new threshold and beforeCount at the previous one. A higher threshold only adds; a lower one only removes; an unchanged threshold does neither. Users sharing a bucket cross the boundary together, rather than being split to reach a precise proportion.
- Scan once for the selected detail step. Check the interval between its old and new thresholds in original input order. The example's counts are (1,4,3,0), (4,1,0,3), (1,1,0,0), (1,0,0,1), (0,5,5,0). addedIds is empty; removedIds=[B,C,D].
- Check boundaries and cost. Duplicate IDs reject everything. Reordering changes detail order but not counts. No detailIndex needs no detail scan. Shared buckets move together. Always preserve after=before+added−removed, never both add and remove in one step, keep counts between 0 and n, and leave inputs unchanged. Let B=10,000. With average constant-time hash operations, preprocessing and counts take O(n+B+q); account separately for ID-character work using L. Detail adds O(n+k). Indexes, buckets and output use O(n+B+q+k+L) space. Producing k IDs does not make the scan O(k).