Repository navigation
fix(ads): demote self-owned campaigns instead of dropping them (serving outage) - #177
Merged
Merged
Conversation
Paid serving has been dark since ~06:04 UTC today. Every request on every format fell through to the CrawlProof house ad, and because house fills are unmetered, nothing recorded an impression — so /ads reads as zero delivery rather than as an outage. Cause: the self-deal guard filtered any campaign whose owner matches the slot owner out of the candidate list, then bailed to the house ad when the list came back empty. That is correct on a network with other advertisers. It is fatal on one without: today every active slot and all 34 active campaigns belong to the same profile, so the filter removed 100% of inventory on every request. The intent of the guard is sound — ad_charge_click refuses to bill or accrue on a self-click, so a self-owned campaign must never win PAID inventory ahead of an advertiser who would actually pay. Dropping it outright was the wrong lever. It is now demoted to the free tier, the same place a campaign that has run out of funds goes. Between a real advertiser's creative and the house ad — neither of which can earn on this request — the real creative is strictly the better fill, and it records an impression, so delivery is visible again. Tests cover both halves: a single-tenant network serves and meters again (these three fail on master, reproducing the blackout), and a self-owned campaign still never takes paid inventory from a third-party advertiser. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
vu1nz Security Review0 finding(s) in PR #? No security issues found. |
ralyodio
added a commit
that referenced
this pull request
Aug 18, 2026
) The four tiles on /dashboard/ads read 0 for 1W and every shorter range while the chart directly beneath them drew thousands of impressions. Nothing failed to load. The tiles counted paid inventory only, and since 2026-07-31 there has been no paid inventory: PR #177 demotes a self-owned campaign to the free tier rather than dropping it, and while every slot and every campaign belong to one account, every single fill is a self-deal. 1M and wider still reached back to genuinely paid days, which is exactly why the break looked like a short-range bug. Three fixes, one per layer: * The tiles now report delivery — paid plus free — with the split named underneath, and CTR is computed on the same totals. Spend stays strictly paid, because it is money. The per-campaign rows follow the same rule so a row can't contradict the header above it. * ad_charge_click recorded a self-deal click as `valid=false, tier='paid'`. Reporting counts `valid` as billed clicks and `not valid and tier='free'` as real-but-unbillable ones, so that combination — the one the bot/duplicate/forged path writes — made every self-deal click invisible. The branches either side of it already write 'free' for the same situation. Migration fixes the branch and reclassifies the 661 rows it mislabelled, scoped so no genuine fraud row moves. * bucketAxis stopped one bucket short of the window start. The RPC filters on `ts >= p_since` then date_bins, so it emits a partial leading bucket; getAccountSeries skips any row without a matching point, so up to a full bucket of real delivery was dropped from both the chart and the totals. Verified against prod: 1W now reports 16,220 impressions and 154 clicks where it reported 1 and 0. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Production: paid serving has been dark since ~06:04 UTC today. Every request, on every format, falls through to the house ad. Because house fills are unmetered, nothing records an impression — so
/adsreads as zero delivery rather than as an outage.Cause
The self-deal guard filters any campaign whose owner matches the slot owner out of the candidate list, then bails to the house ad when the list is empty:
Correct on a network with other advertisers. Fatal on one without. Confirmed in production:
47ba37b2…terminal_asciicampaigns are owned by47ba37b2…other_campaigns = 0for every slotSo the filter removes 100% of inventory on every request. Raw SQL shows 34 eligible creatives; the serving path sees none.
Fix
The intent is sound —
ad_charge_clickrefuses to bill or accrue on a self-click, so a self-owned campaign must never win paid inventory ahead of an advertiser who would actually pay. Dropping it outright was the wrong lever.It's now demoted to the free tier, the same place a campaign that has run out of funds goes. Between a real advertiser's creative and the house ad — neither of which can earn on this request — the real creative is strictly the better fill, and it records an impression, so delivery is visible again.
Verification
npm run typecheckclean.master, reproducing the outage exactly.Notes
Not caused by #174/#175/#176 — those deployed at 11:37 and later, hours after serving stopped. The regression is in the #171/#172 window, which matches the last recorded impression at 06:04:17.
Separately and still outstanding:
20260731130000_ad_impression_short_codes.sqlfrom #176 has not been applied, so short codes remain inert (running their intended fallback).🤖 Generated with Claude Code