From 2c18c3dfe26fa9bcb84e4beaee671f37cfc0f98d Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Mon, 10 Aug 2026 12:35:54 +0000 Subject: [PATCH] fix(ads): tune the impression dedupe window from 60s to 5s #192 shipped a 60s window picked by reasoning about human behaviour rather than by measurement. Backtesting the exact rule against 7 days of real impressions shows that was wrong: the machine bursts it targets are compressed into seconds, so a wide window costs a great deal of real delivery to catch almost nothing extra. window terminal flagged (target) web flagged (cost) 3s 84.5% 12.2% 5s 84.6% 14.2% 10s 84.7% 17.5% 60s 85.4% 36.6% 60s bought +0.9pp on the target and suppressed 36.6% of real web impressions. 5s keeps essentially all of the burst suppression -- the observed burst spans ~2.5s -- with margin for a slower run, since each fetch in the loop can take up to its own timeout. Worth recording why the web number is not noise. Of the 36.6% flagged at 60s, only 0.1pp came from distinct visitors colliding on a shared ip_hash; the rest was the same visitor_id re-fetching the same slot. Broken down by gap, the sub-second repeats are /ad.js clearing data-cp-filled in its .catch() path and SPA callers re-firing scan() -- a real double-count bug, which the 5s window still catches. The 10-60s repeats are human reloads and SPA navigation, which are genuine delivery and should never have been suppressed. No migration: the stored column comment does not name a duration, and existing flagged rows are left as they are. Co-Authored-By: Claude Opus 5 (1M context) --- lib/ads/fraud.ts | 31 ++++++++++++++++---- tests/contract/ads-impression-dedupe.test.ts | 10 +++++-- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/lib/ads/fraud.ts b/lib/ads/fraud.ts index a8077ba..4b93212 100644 --- a/lib/ads/fraud.ts +++ b/lib/ads/fraud.ts @@ -30,12 +30,31 @@ function safeId(v: string | null | undefined): string | null { // (the shape used for clicks) collapses none of it. Slot-keyed dedupe collapses // the whole burst to one counted impression. // -// 60s rather than the click path's 6h because a repeat view is a real thing: a -// human reloading an article an hour later genuinely saw the ad twice, and an -// hours-long window would erase legitimate delivery. 60s is long enough to -// swallow a machine-driven burst (the observed one fires 12 fetches in ~3s) and -// short enough that no human pattern lands inside it twice by accident. -export const IMPRESSION_DEDUPE_WINDOW_MS = 60 * 1000; +// 5s rather than the click path's 6h, because a repeat view is a real thing: a +// human reloading an article genuinely saw the ad twice, and a long window +// erases legitimate delivery. +// +// This shipped at 60s, chosen by reasoning about human behaviour. Backtesting +// the rule against 7 days of real impressions showed that was wrong — the +// machine bursts are compressed into seconds, so a wide window costs a great +// deal of real delivery to catch almost nothing extra: +// +// window terminal flagged (target) web flagged (cost) +// 3s 84.5% 12.2% +// 5s 84.6% 14.2% +// 10s 84.7% 17.5% +// 60s 85.4% 36.6% +// +// 60s bought +0.9pp on the target and cost 22pp of real web impressions. 5s +// keeps essentially all of the burst suppression — the observed burst spans +// ~2.5s — with a little margin for a slower run, since each fetch in the loop +// can take up to its own timeout. +// +// The ~12-14% web floor that remains at short windows is not noise: it is the +// same visitor re-fetching the same slot sub-second, which is /ad.js clearing +// data-cp-filled in its .catch() path and SPA callers re-firing scan(). That is +// a real double-count bug and dedupe only masks it. +export const IMPRESSION_DEDUPE_WINDOW_MS = 5 * 1000; export type ClickValidity = { valid: boolean; reason?: string }; diff --git a/tests/contract/ads-impression-dedupe.test.ts b/tests/contract/ads-impression-dedupe.test.ts index d3bea29..fbc5413 100644 --- a/tests/contract/ads-impression-dedupe.test.ts +++ b/tests/contract/ads-impression-dedupe.test.ts @@ -131,8 +131,12 @@ describe("isDuplicateImpression", () => { }); it("uses a much shorter window than the click path", async () => { - // A repeat view hours later is real delivery; only a machine-driven burst - // lands twice inside a minute. - expect(IMPRESSION_DEDUPE_WINDOW_MS).toBe(60_000); + // Tuned against 7 days of real impressions, not intuition. At 60s the rule + // flagged 36.6% of web impressions to gain 0.9pp on the machine bursts it + // targets; at 5s it flags 14.2% of web and still catches 84.6% of terminal, + // because the bursts are compressed into ~2.5s. + expect(IMPRESSION_DEDUPE_WINDOW_MS).toBe(5_000); + // Must stay far below the click window — these measure different things. + expect(IMPRESSION_DEDUPE_WINDOW_MS).toBeLessThan(60_000); }); });