Report the presentation rotation, and refuse to fake the rate - #322
Merged
Merged
Conversation
#316 records which medium every fill was served as. Nothing read it, which made the rotation randomised delivery that taught nothing. This adds the read. A rollup rather than a query over raw events, for the reason 20260902140000 exists: ad_impressions is ~376k rows growing ~90k/day, a 30-day window selects about half the table, and the 8s statement_timeout on `authenticated` was already cancelling reporting RPCs that scanned it. A `group by media` over the same range walks straight back into that. Grain is (owner_id, day, media) — daily, not the account series' hourly, because this is a comparison table and nothing here is plotted at 4-hour buckets. The part worth arguing about is what the card does NOT show. CTR per medium is the obvious report and it is unreadable on this network: every slot and every campaign belong to one account, so clicks book as free self-deal and there has not been a valid click since 2026-07-29. Five rows of 0.000% is not a neutral presentation of that — it reads as a finished experiment that found motion worthless. So the rate column is withheld below 30 attributed clicks and replaced by a note naming the structural cause, because "not enough data yet" would tell the reader to wait and waiting will not fix it. Playback is the signal that does discriminate today (#320), and the note says so. Two denominators that would each have read as a real number if got wrong: * share is denominated on ROTATED delivery, not on everything in the window. The pre-rotation archive is far larger than anything the rotation has served, so including it would show all five arms at ~0% indefinitely. * a NULL media is 'unknown', never 'static'. Folding the archive into static would make static the permanent winner of an experiment it never ran in. Clicks whose impression_id no longer resolves land there too — ad_clicks is `on delete set null` and serveAd synthesises an id when its insert failed. A click's medium comes from the impression it came from, not from its creative: the same creative serves several arms, so inferring it would be wrong by construction. Migration applied on dev2 ahead of this, with the PostgREST schema reload. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ThreatCrush Security Scan49 finding(s) HIGH/CRITICAL: 2 | MEDIUM: 32 | LOW: 15
Snippets are redacted; ThreatCrush never prints matched credential material. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
#316 records which medium every fill was served as. Nothing read it — which made the rotation randomised delivery that taught nothing. This adds the read: a Delivery by medium card on
/dashboard/ads, under the trend chart.Why a rollup and not a query
ad_impressionsis ~376k rows growing ~90k/day, a 30-day window selects about half the table, and the 8sstatement_timeoutonauthenticatedwas already cancelling reporting RPCs that scanned it — the whole reason20260902140000_ad_stats_rollupsexists. Agroup by mediaover the same raw range walks straight back into it.So:
ad_stats_owner_media_daily (owner_id, day, media), refreshed on pg_cron beside the existing rollup, read byad_owner_media_split(p_since)which blends rollup rows for closed days with raw for the live edge — the same exact split the other five reporting RPCs use, so the raw read is at most one day wide however long the window.Daily grain, not the account series' hourly: this is a comparison table and nothing in it is plotted at 4-hour buckets, so the finer grain would cost rows and buy nothing. ~6 rows per owner per day.
The part worth arguing about: what the card does not show
CTR per medium is the obvious report, and it is structurally unreadable on this network. Every slot and every campaign belong to one account, so clicks book as free self-deal; there has not been a valid click since 2026-07-29. Verified again on prod while building this:
Five rows of
0.000%is not a neutral way to present that — it reads as a finished experiment that found motion worthless, which is the single most likely way this table gets misread. So the rate column is withheld below 30 attributed clicks and replaced by a note naming the structural cause. "Not enough data yet" would tell the reader to wait, and waiting will not fix a one-account network. The note points at playback (#320) as the signal that does discriminate today.Two denominators that would each have read as a real number if got wrong
mediaisunknown, neverstatic. Folding the archive into static would make static the permanent winner of an experiment it never ran in. Clicks whoseimpression_idno longer resolves land there too —ad_clicksison delete set null, andserveAdsynthesises an impression id when its own insert failed.Both are pinned by tests rather than left to review.
Also: a click's medium comes from the impression it came from, not from its creative. The same creative serves several arms, so inferring it from the creative would be wrong by construction.
Verified
Migration applied on dev2 ahead of this, with the
notify pgrst, 'reload schema'that a hand-applied column needs. Then exercised for real:tsc --noEmit: clean.tests/ads-media-stats.One live-testing gotcha worth recording
Rapid same-IP sampling cannot produce rotated rows in this report.
isDuplicateImpressionORs on visitor or IP hash inside a 5s window, so a burst of probe fills all flagduplicateand the rollup (correctly) excludes them — the media columns read all-static no matter what was actually served. Sampling at 6s intervals with distinct visitor ids produces the real mix; over 21 such fills on one live 300x250 slot: image 8, static 7, gif 3, audio 2, video 1.(And separately: curl gets unmetered house ads, which are always
static. Both traps cost me a wrong conclusion before I caught them.)Follow-ups filed
/api/ads/frame; the canonicalad.jsinstall is unmeasured, and playback is the one signal that can separate these arms today. Includes the double-metering trap to design around.🤖 Generated with Claude Code