Measured, not asserted
Published
Viewability is not a measurement. It is a definition applied to a measurement. How much of the ad was on screen and for how long are observed facts, and nothing here disturbs them; whether those facts add up to viewable is a pair of thresholds somebody chose. This test renamed those thresholds against a real obfuscation engine. The same observations were reclassified, the campaign was billed a third more, and the viewability rate -- the one number a buyer checks when they suspect a measurement problem -- went UP.
A definition applied to a measurement
The fixture is four hundred thousand impressions with a realistic distribution: pixel coverage and dwell both fall away down the page, a quarter of the inventory is a large-format unit, a quarter is video, and about one impression in nine was counted by the ad server and never actually drawn by the page.
The standard the campaign bought against is the familiar one: half the pixels for one second, thirty per cent for one second above a large-format size cut-off, and half the video played. Only impressions the page actually rendered are in the denominator.
Correct, 355,555 of the 400,000 are measurable, 133,333 are viewable, the viewability rate is 37.50%, and at a 24.00 viewable CPM the campaign is billed 3,199.99 -- exactly what an independent application of the standard to the same observations produces.
The whole of that standard lives in six option keys. A library shipped without them cannot invent them and cannot refuse to measure either, so its defaults are the permissive ones: everything served counts, at any coverage, for any duration.
The unit and its number, one field apart
The dwell requirement is a NUMBER and a UNIT in two separate keys: dwellDuration: 1 and dwellUnit: 's'. Rename the unit and the library falls back to milliseconds, so a one-second requirement becomes a one-millisecond requirement.
^(dwellUnit)$ takes viewable impressions from 133,333 to 177,777, the viewability rate from 37.50% to 50.00%, and the bill from 3,199.99 to 4,266.65. The campaign is over-billed by 1,066.66 on four hundred thousand impressions -- a third of what it should have paid.
The number on the configuration screen is unchanged. It is finite. It is plausible. A quantity in the wrong unit is a perfectly good quantity, which is why the standard mitigation for a lost numeric field -- require it to be finite before comparing it -- is structurally blind to this.
One field away, ^(dwellSeconds)$ on the impression records makes the observation itself unreadable and the classification collapses. Losing the value fails closed. Losing what the value MEANS fails open, and pays the wrong party.
Three names, one answer, and a fourth that goes the other way
^(dwellUnit)$, ^(largeFormatPixelPct)$ and ^(videoCompletionPct)$ all land on the same place: a 50.00% viewability rate and roughly 1,066 of over-billing. Three unrelated names, three unrelated mechanisms, one answer.
^(largeFormatMinPx)$ is on the same object and fails in the OPPOSITE direction. Without the size cut-off there is no large-format category, so large units are held to the fifty per cent display bar they physically cannot meet. Viewable impressions fall to 88,888, the rate to 25.00%, and the campaign is UNDER-billed by 1,066.68.
So the narrow pattern and the wide one disagree about which direction the money moves. ^(largeFormatPixelPct|largeFormatMinPx)$ is byte-identical to losing the cut-off alone, which means the wider pattern is the safer one here -- for the buyer. Reading a matrix by testing only the union would have found the under-billing and missed the over-billing entirely.
The metric that would catch it does not move at all
^(measurableBasis)$ is the quietest arm in the file. It puts impressions the page never rendered back into the denominator, which sounds like it should depress the rate, and it does -- but it also puts them in the numerator, because an unrendered impression with a recorded pixel coverage still clears the bar.
Measurable goes from 355,555 to 400,000. Viewable goes from 133,333 to 150,000. The bill goes from 3,199.99 to 3,600.00, an over-billing of 400.01. And the viewability rate stays at 37.50%, to the second decimal place.
A buyer looking for a measurement problem looks at the rate. In this arm the rate is a perfect match to the correct run and 16,667 impressions that nobody could have seen were billed as viewable.
The pair that turns a loud refusal into a quiet over-bill
^(pixelThresholdPct)$ and ^(dwellDuration)$ each fail closed on their own. The buyer's own measurement rule refuses the report outright -- view-check(no pixel threshold was configured) -- because a viewable impression has to have cleared a real bar and the configuration no longer contains one.
^(requireViewabilityCheck)$ alone and ^(viewabilityRule)$ alone leave the report untouched -- 133,333 viewable at 37.50%, billed 3,199.99. Pair either with a threshold and the refusal disappears: ^(requireViewabilityCheck|dwellDuration)$ reports 177,777 viewable impressions at a 50.00% rate and bills 4,266.65.
The pattern is the same one this series has now measured in software updaters, retention purges, frequency caps and pacing: a guard that fails loudly is only loud if it is still reached, and the flag that decides whether it is reached fails quietly.
What to change, whether or not you obfuscate
Never let a unit live in a different key from the number it describes. Carry the quantity with its unit, or normalise at the boundary where the value enters. A unit separable from its number is the entire precondition here, and a hand-edited config or a library upgrade produces it just as well as a rename.
Refuse to produce a viewability report at all if the thresholds were not explicitly supplied, rather than defaulting to a permissive definition. A measurement standard with no numbers in it is not a lenient standard, it is not a standard.
Reconcile the billed count against an independent application of the standard to the same observations, which is what the fixture's TRULY-VIEWABLE column does. It is the only figure in the file that stayed correct in every arm, and it is cheap: the observations are already there.
If you do obfuscate, keep member renaming narrow and exclude the measurement configuration and the impression records. Renaming identifiers and control flow is unaffected. Verification and measurement code bundled into a front end -- for instance a React bundle where the tag and the app share a build -- needs the exclusion applied to the shared build, not just to the tag.
How this was measured
A grounding harness drives the protection engine directly. Every sample is deterministic and the unprotected original is run twice and required to reproduce itself before anything is compared to it. The simulated measurement library is copied in unprotected, and the verification vendor's report arrives as text that is parsed, so only this bundle's READ of those field names can move.
Before any renaming, each option key and record field was deleted one at a time to establish that it moves the output at all. Two arms in this area were inert on the first run -- a video impression that could never clear the pixel bar and a large-format impression that could never clear the dwell bar -- and both were fixture defects fixed before anything was measured.
The base column ran all five areas on five presets and all twenty-five were byte-identical to the unprotected run, so there is no new engine defect in this pass. Related reading on protection in a bought-media context: obfuscating ad tech JavaScript.
Frequently asked questions
Did obfuscation on its own break viewability measurement?
No. Five presets -- the ES5 default, the modern target, the emit gate's option set on both targets, and the string transforms -- all produced output byte-identical to the unprotected run across all five areas measured this pass.
What actually broke it, then?
Member renaming with a pattern that reached the names the viewability standard is made of: the pixel threshold, the dwell duration, the dwell UNIT, the large-format cut-off and its bar, the video completion bar, and the measurable basis.
How does renaming a unit over-bill a campaign?
The dwell requirement is a number and a unit in two keys. Lose the unit and the library falls back to milliseconds, so a one-second requirement becomes one millisecond. Viewable impressions went from 133,333 to 177,777 and the bill from 3,199.99 to 4,266.65.
Why does the viewability rate go up rather than down?
Because the threshold is what disqualifies impressions. A lower bar admits more of the same observations, so the numerator grows while the denominator does not. A buyer checking the rate sees excellent inventory rather than a broken measurement.
Was there an arm where the rate did not move at all?
Yes, and it is the quietest one. Renaming the measurable basis put unrendered impressions into both the numerator and the denominator, so the rate stayed at 37.50% to two decimal places while 16,667 impressions nobody could have seen were billed as viewable.
Did anything fail in the advertiser's favour?
Yes. Renaming the large-format size cut-off removed the category, so large units were held to the fifty per cent display bar they cannot physically meet on a phone. The rate fell to 25.00% and the campaign was under-billed by 1,066.68.
What is the safest way to use member renaming here?
Keep the pattern narrow and explicit and exclude any options object carrying measurement thresholds or units, along with the impression records they are evaluated against. Renaming identifiers and control flow is unaffected by all of this.
Related reading