We built a screener that loads each game in headless Chrome and records which ad networks it contacts. It ran across the whole catalogue and told us which games were clean. Then somebody opened one of the "clean" ones on a phone and got a full-screen ad gate before the game even started.
The three wrong theories
First guess: the ad fires on a gameplay event, after our observation window closes. Wrong — it was a pre-roll, firing at load.
Second guess: the dwell time was too short. We measured when the ad requests actually appear. The first one lands 0.1 seconds after navigation. Our window was six seconds.
Third guess, suggested by someone else: the network signal cannot see this ad implementation, so we need to detect it in the rendered DOM instead. Also wrong, and it would have failed anyway — these are Unity canvas games where the page has essentially no text, and the ad gate renders inside a nested frame.
The actual cause
Headless Chrome advertises itself as HeadlessChrome in its user-agent string, and it has no viewport at all until you set one. Ad providers check both and decline to initialise. The screener was not failing to detect ads. It was being shown none.
We isolated it on a single game — same URL, same dwell, only the browser setup varying:
- Without a user-agent or viewport override: 9 requests across 5 hosts, no ad networks
- With both set: 33 requests across 16 hosts, including Google IMA and DoubleClick
The second bug, which was worse
While fixing that we found a page that failed to load returned an empty list of ad networks — recorded identically to a genuinely clean game. Failure was certifying games as safe.
That one was more dangerous than the user-agent bug, because it was non-deterministic. The user-agent bug under-detected uniformly and any spot check would have caught it. A transient network failure silently marking a game clean would have survived the first fix and kept producing bad data on every subsequent run, with nothing in the output to suggest anything was wrong.
If you are building something similar
Set a real user-agent and a real viewport before you navigate. And make failure loud: a check that could not complete must not be recorded as a pass. The default should be "unknown", never "fine".