feat: randomize mobile feed ad positions via adJitter#5899
feat: randomize mobile feed ad positions via adJitter#5899
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
@claude please review |
|
Claude finished @nimrodkra's task in 3m 27s —— View job Code Review
Overall this is a clean, well-reasoned implementation. The FNV-1a hash is a solid choice for a lightweight deterministic seed, and the clamping invariant ( Issues1. First ad can appear at index 0 — contradicts the PR descriptionWith // useFeed.ts:185
if (index < adStart - safeJitter) { // 2 - 2 = 0 → only guards index < 0
return undefined;
}For 2. Pure utility function lives in a hook file
Minor / NitsTest coverage gap — The "returns undefined before first possible slot" test only asserts on // useFeed.spec.ts:22-26
it('returns undefined for indices before the first possible ad slot', () => {
expect(
getAdSlotIndex({ index: -1, adStart, adRepeat, adJitter, seed }),
).toBeUndefined();
});A more meaningful test would also cover e.g.
Seed stability note — What's solid
|
Add optional `adJitter` to `FeedAdTemplate` so ad positions can vary deterministically per feed while keeping average cadence unchanged. Jitter only activates when the `feed_ad_template` GrowthBook flag sets `adJitter` under the `default` key, so only the mobile/1-column list layout is affected. - Extend FeedAdTemplate with optional adJitter - Pure getAdSlotIndex helper with seeded FNV-1a hash keyed off feedQueryKey - Clamp jitter to floor((adRepeat - 1) / 2) so consecutive ads never overlap or reorder - Unit tests for parity, window bounds, determinism, and clamp Made-with: Cursor
1c7b875 to
13f6f1a
Compare
- Move getAdSlotIndex and hashSeed from useFeed.ts to lib/feed.ts since they are pure, React-free utilities. - Clamp first ad to never land before adStart by using one-sided jitter (0..+adJitter) for n=0; subsequent slots keep symmetric jitter. This matches the documented behavior "first ad in posts 3-5" rather than 1-5. - Co-locate spec next to source (lib/feed.spec.ts) and add tests for the adStart lower bound (no jitter), the adRepeat <= 0 guard, and the first-ad lower bound across many seeds. Made-with: Cursor
Summary
Adds an optional
adJitterknob to the existingfeed_ad_templateGrowthBook flag so mobile ad positions (first ad + gaps) vary deterministically per feed. Desktop is untouched because the flag is already per-breakpoint andadJitteris only set under thedefaultkey at rollout time.Today mobile injects an ad at post 3 and then at a fixed cadence, which feels predictable. With
adJitter: 2ondefault, the first ad lands in posts 3–5 and each subsequent gap varies betweenadRepeat ± 2, keeping average frequency unchanged.Changes
FeedAdTemplategains optionaladJitter?: number.getAdSlotIndexinpackages/shared/src/lib/feed.tswith a seeded FNV-1a hash keyed offfeedQueryKey. Positions are stable across re-renders / pagination but differ per feed / user.getAdinuseFeeduses the helper; deps updated to includeadTemplate?.adJitterandfeedQueryKey.n = 0is one-sided (0..+adJitter) so the first ad can never land beforeadStart; subsequent slots use symmetric jitter (-adJitter..+adJitter).floor((adRepeat - 1) / 2)so consecutive ads can never overlap or reorder, even with misconfigured flag values.adStartlower bound with and without jitter,adRepeat <= 0guard, window bounds per slot, determinism, seed variance, and overlap clamp.Rollout
Only affects mobile when the
feed_ad_templatepayload setsadJitterunder thedefaultkey. Example:{ "default": { "adStart": 2, "adRepeat": 5, "adJitter": 2 }, "tablet": { "adStart": 2, "adRepeat": 5 }, "laptop": { "adStart": 2, "adRepeat": 5 }, "laptopL": { "adStart": 2, "adRepeat": 5 }, "laptopXL": { "adStart": 2, "adRepeat": 5 }, "desktop": { "adStart": 2, "adRepeat": 5 } }Absent
adJittermeans today's behavior — safe to merge before flipping the flag.Test plan
pnpm --filter shared lintpnpm exec jest src/lib/feed.spec.ts(8/8 pass)defaultbreakpoint (adJitter: 0vsadJitter: 2) to measure CTR