Skip to content

Repository files navigation

adpreflight

Catch playable ad rejections before you submit, not three days later in a rejection email.

adpreflight checks a hand-coded HTML5 playable against Unity Ads and Google App-campaigns rule packs. It reads the build statically, then runs it once in a headless container shim to verify one thing dynamically. It prints a verdict per network naming the exact rule that failed.

Quickstart

No install, no clone:

npx github:marko-builds/adpreflight ./build.zip --networks unity,google

Input can be a .zip, a directory, or a single .html file. Add --json for machine-readable output, --static-only to skip the dynamic stage.

Try it on the committed fixtures:

git clone https://github.com/marko-builds/adpreflight && cd adpreflight
npm install
node dist/cli.js fixtures/broken.zip --networks unity,google   # exits 1, names 12 rule failures
node dist/cli.js fixtures/clean.zip  --networks unity,google   # exits 0

The rejection this exists to prevent

A reconstructed pattern, not a specific real creative. This is what the classic Unity round trip looks like:

Before. You build a playable, zip it, upload it, and wait. Two or three days later the rejection arrives: "creative does not meet specifications." No rule name, no line number. You start guessing. The build was 5.4 MB extracted (the limit is 5 MB, measured on the inlined file, not the zip you looked at). It also pulled a font from a CDN, and its call-to-action navigated with window.location instead of mraid.open. Three separate problems, one unhelpful email, another round trip for each guess.

After.

$ npx github:marko-builds/adpreflight ./build.zip --networks unity,google

network  verdict  rule
unity    fail     unity/extracted-size-exceeded: Extracted build exceeds Unity Ads' 5 MB playable limit
unity    fail     unity/external-resource-reference: References an external URL, Unity requires all assets inlined
unity    fail     unity/redirect-window-location: Navigates via window.location, Unity expects the store link to go through mraid.open

Seconds, on your machine, with the rule names to search for.

What it actually checks

The honest version. This tool claims only what the run performs.

Checked statically (no browser, runs in seconds)

Check Unity Google
Extracted total size yes yes
Submitted .zip size not applicable (Unity takes a single inlined file) yes
File count cap (512) no yes
index.html at archive root yes no, see below
Double-zipped archive no yes
Single inlined bundle yes no
External resource references yes (none permitted) yes (Google's allowlist)
Insecure http:// references yes yes
Frame tags no yes
MRAID hook present yes no, see below
window.location redirect yes yes

Verified dynamically (one assertion, needs a browser)

The ready handshake: the build is served from a local static server, opened in headless Chromium, and its <script src="mraid.js"> request is intercepted with a recording shim. Every container call is logged with a timestamp. If the creative touches the API before the container signals ready, handshake-before-ready fires.

The recorded conversation is in the --json output, so you can read what your creative actually said to the container:

getState             ms=0    beforeReady=true
addEventListener     ms=0    beforeReady=true
getPlacementType     ms=1    beforeReady=false

Deliberately NOT checked

These need a synthetic tap to ever fire, and this version has no interaction probe. Asserting them would mean reporting "pass" for checks that never ran:

  • exit-call-per-CTA counts
  • actual redirect behaviour at runtime (the window.location check above is a source check)
  • sizeChange and viewableChange handling
  • rendering, visual correctness, performance, or anything about whether the ad is any good

Exit codes

Code Meaning
0 Every enabled rule passed
1 At least one rule failed
2 No verdict could be established

Code 2 is not "worse than 1", it is "the run did not complete". It fires when Playwright's browser is missing, or when the creative never talked to the container at all, so the handshake could not be observed. A build that never loads a container is not passing the dynamic check, it is failing to take it. Code 2 outranks code 1 because an incomplete run's list of failures may itself be incomplete.

The two-minute story, split honestly

The static stages need nothing but Node and run in seconds.

The dynamic stage needs a browser, once:

npx playwright install chromium

Without it you still get every static result, and the dynamic stage reports dynamic-stage-unavailable rather than quietly stepping aside.

Where the rules come from

Rule packs are data, not code, one YAML per network in rulepacks/. Every limit carries the exact quote, the source URL and the retrieval date inline. Anything not confirmed from a primary source is marked unverified rather than asserted.

Each rule also carries a basis field, and this distinction matters if you are deciding whether to trust a verdict:

  • cited means a network's own documentation states it, and the quote is in the pack.
  • derived means it follows from something cited but no network states it in those words.

Rules currently marked derived: both insecure-http-reference rules, both redirect-window-location rules, unity/mraid-hook-missing, google/double-zipped, and google/extracted-size-exceeded (Google states its limit on the .zip, so measuring the extracted tree is a conservative approximation that can fail a compressible build Google would accept).

Two things the packs record but this tool cannot enforce: Google's 5 MB and 5.2 MB figures conflict across two of its own help pages (the conservative number is enforced), and ExitApi is unsupported in Google Web Designer creatives while being required for hand-authored ones, which cannot be told apart from the build alone.

Fixtures

fixtures/ is the calibration pair, and CI asserts it both directions on every push. A planted rule that stops firing fails the build, and so does an unplanted rule that starts. See fixtures/README.md.

Scope

Unity Ads and Google App campaigns only. Not AppLovin, Meta, ironSource or Vungle.

License

MIT. See LICENSE.

About

Pre-flight hand-coded HTML5 playable ads against ad-network rule packs, catching rejections before submission.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages