Creative debugging in programmatic advertising — common issues and fixes
A programmatic creative is a black box wrapped in a sandbox wrapped in an iframe. When it does not render, the failure could be anywhere from a missing MRAID stub to a misconfigured sandbox attribute. This is the workflow we use to find the root cause in minutes instead of hours.
What a “creative” actually is
In programmatic, a creative is the executable payload that the buyer ships to the user’s device. Depending on the format, it may be a static image, an HTML5 document, a fragment of JavaScript that pulls in additional resources, a VAST XML pointer to a video file, or a native asset bundle. What unifies these is that the publisher does not author the markup — the DSP does — and the publisher’s page has to host arbitrary third-party code without letting it break the host document.
The sandbox boundary is the most important concept here. On web, creatives normally land inside a <iframe> with a sandbox attribute. On in-app, creatives run inside a WKWebView or Android WebView with a JavaScript bridge called MRAID. When a creative fails, almost every failure traces back to a mismatch between what the creative expects from its environment and what the environment actually provides.
Where it lives in the bid response
OpenRTB defines the creative markup in bid.adm — a single string field on each bid object inside the response. If adm is absent, bid.nurl is used, and the markup is fetched lazily when the bid wins. A minimal banner bid looks like this:
{
"seatbid": [{
"seat": "dsp-acme",
"bid": [{
"id": "b-9c2e",
"impid": "1",
"price": 1.42,
"adid": "creative-77481",
"crid": "77481",
"adomain": ["example-advertiser.com"],
"w": 300,
"h": 250,
"adm": "<script src=\"https://cdn.dsp/r?cb=${CACHEBUSTER}\"></script>"
}]
}],
"cur": "USD"
}
The adm can be HTML, a JavaScript tag, a full VAST document, or a native JSON payload, depending on imp.banner, imp.video, or imp.native on the request. When you paste a creative into a renderer to debug it, you are pasting the contents of adm — nothing more. Our ORTB renderer takes raw adm and previews it in a controlled iframe so you can see what the user would see.
The eight failure modes you will see most often
1. mraid.js 404 on the open web
An app-only creative will frequently include <script src="mraid.js"></script> at the top. On the open web there is no MRAID, so this script 404s and any code that depends on window.mraid throws TypeError: Cannot read properties of undefined. If you are previewing app creatives in a browser, stub it: inject a tiny shim that exposes mraid.getState(), mraid.addEventListener(), mraid.open() as no-ops. A 30-line shim is enough to get most MRAID-2 creatives to first paint.
2. Relative src with no base URL
DSPs sometimes emit <script src="//cdn.example/x.js"> — protocol-relative — or, worse, <script src="x.js">. The former breaks under file:// previews; the latter breaks everywhere the iframe’s base is not cdn.example. Always inject creatives into an iframe with an explicit <base href="https://"> or rewrite to absolute URLs before rendering.
3. CORS inside a sandboxed iframe
The sandbox attribute without allow-same-origin forces the iframe to have a null origin. Any fetch() or XMLHttpRequest with credentials — or any request to an endpoint that requires the Origin header to match an allowlist — will fail. The browser console shows CORS error: Origin null is not allowed. Two fixes, with different tradeoffs:
- Add
allow-same-origin: the creative shares the host’s origin. Fast, but a malicious creative can now reach intodocument.cookieif it can escape the iframe. - Use a friendly subdomain: serve the creative from
ads.publisher.examplewith a permissive CORS policy. Slightly more setup, but the blast radius stays contained.
4. Anti-debug debugger; traps
A small minority of creatives include a tight loop with debugger; statements to make reverse-engineering painful. In Chrome DevTools, this freezes your tab when DevTools is open. The fix is the “Never pause here” option: open Sources, find the line with debugger;, right-click, choose Never pause here. For systematic auditing, use the --disable-features=BlockedDebuggerSync flag or run headless with the debugger disabled.
5. Content Security Policy collisions
Modern publishers ship a Content-Security-Policy header that blocks inline scripts, eval, and unknown CDNs. A creative that uses new Function() or appends <script> nodes with inline source will be silently dropped. Look for Refused to execute inline script because it violates the following Content Security Policy directive in the console. Either move the creative to a subframe with a relaxed CSP, or reject creatives that fail a static check for inline eval.
6. Click macros that never expand
Buyers embed click-tracking URLs as macros: ${CLICK_URL}, %%CLICK_URL_ESC%%, ${CACHEBUSTER}. If your renderer does not substitute them, you get literal strings in the DOM and the click goes to a 404. Worse, when escaped variants pass through twice without decoding, you get double-encoded URLs (%252F instead of %2F) and the destination server rejects them. Use a single, well-documented macro table per format, and validate with the URL decoder before pushing changes.
7. Missing or malformed VAST wrappers
Video creatives often chain: VAST Wrapper → VAST Wrapper → VAST InLine. Each wrapper is a separate XML document served from a separate URL, and the chain must terminate in an <InLine> with at least one <MediaFile>. We see breakage at every step: HTTPS-only players rejecting an HTTP wrapper, redirects with no VASTAdTagURI, namespace mismatches, and chains that exceed the wrapperLimit the player enforces. The VAST tags article walks through the structure in detail.
8. Viewport assumptions
Creatives built for desktop sometimes hard-code window.innerWidth > 800 and refuse to render on mobile, or assume the slot is exactly 300x250 and overflow on a 320x50 slot. Always check imp.banner.w / h against the creative’s declared dimensions before serving, and reject mismatches at the adapter layer.
Diagnosing with browser DevTools
For web creatives, four DevTools panels do most of the work:
- Network: filter by
DocandJS. Look for 4xx/5xx, blocked requests, or requests with a red “CORS error” tag. Sort by initiator to see which script kicked off which subresource. - Console: turn on Preserve log and Selected context only, then switch the context dropdown to the creative’s iframe. Most uncaught exceptions you care about live here.
- Application → Frames: shows the iframe tree, with each frame’s origin and CSP. If the creative renders into a frame whose origin is
null, every cookie- or storage-dependent feature is going to misbehave. - Performance: record a 3-second trace from before the impression. Look for long tasks (red triangles) and the gap between
loadand the first paint inside the ad slot. A 1.5-second gap is a slow creative; a 5-second gap is a broken one.
For in-app, the equivalent is remote WebView debugging: Safari’s Develop menu for WKWebView, chrome://inspect for Android. The trick on Android is enabling WebView.setWebContentsDebuggingEnabled(true) in the host app — without it, the WebView is invisible to chrome://inspect.
Render-detection heuristics
How do you tell, programmatically, that a creative actually painted? Three signals, used together:
- Image
naturalWidth: for any<img>rendered by the creative,img.naturalWidth > 0means the image decoded.0means it 404’d, was blocked, or has not loaded yet. Combined withimg.complete, this is the cheapest reliable signal for static banners. - Iframe content size:
iframe.contentDocument.body.scrollHeight > 0(same-origin only) tells you that something was inserted into the iframe. For cross-origin iframes, fall back to checking that the iframe’soffsetWidthandoffsetHeightare non-zero and that an IntersectionObserver fired with a non-zero ratio. - OMID/IAB SafeFrame events: when the creative is wrapped in an Open Measurement session, the
geometryChangeandimpressionevents are authoritative. If you are integrated with OM, prefer these over DOM heuristics.
None of these alone is perfect. A creative can document.write a blank <div> the right size and look “rendered” to a heuristic. Combine at least two signals, and treat a single positive signal as suspect until confirmed.
Why VAST chains break
Three recurring root causes:
- Mixed content: a wrapper served over HTTPS points at an HTTP
VASTAdTagURI. Players on secure pages refuse the upgrade and time out. - Namespace strictness: VAST 4 requires
xmlns="http://www.iab.com/VAST". A few older wrappers omit the namespace and modern players that switched to strict parsing reject them. - Cookie/identity loss: the wrapper chain hops between three or four hostnames. If the user has third-party cookies disabled (Safari ITP, Firefox ETP, Chrome’s privacy sandbox transitions), each hop loses identity, and downstream bid logic falls back to a default ad — or no ad at all.
When debugging a video failure, capture the full wrapper chain with a network sniffer or by saving the player’s VAST log. Walk the chain top-down; the first wrapper whose response is not a 200 with valid XML is your culprit.
Filtering bad demand at exchange or publisher level
You cannot fix every broken creative one at a time. Build filters at the source:
- Domain allowlist for script
src: parse theadm, extract everysrc, and reject creatives that reference unknown CDNs. This stops a large class of malware in one rule. - Static checks for
document.write,eval, andnew Function: not all uses are malicious, but in 2026 they are also rarely necessary. Treat them as a yellow flag for manual review. - Macro-substitution validation: after substituting your macros, the
admshould contain no remaining${...}or%%...%%tokens. If it does, the creative is misconfigured. - Size and weight caps: reject creatives where
admis over 200 KB without compression. Most legitimate banners are well under this. - Adomain checks: cross-reference
bid.adomainagainst your block list and the IAB’s ads.txt for the buyer’s seat. Mismatches are how malicious advertisers slip past brand-safety filters.
A 60-second triage checklist
When a publisher reports “the ad is broken,” run this sequence:
- Get the
bid.admfrom logs. Drop it into our ORTB renderer. - Does it render in the renderer? If yes, the bug is in the publisher’s environment (CSP, sandbox, ad slot CSS).
- If no, open the renderer’s console. The first uncaught exception is almost always the cause.
- Search the
admformraid,document.write, and${. Each is a likely culprit. - If the creative is video, fetch the VAST URL with
curland pretty-print the XML. Walk the wrapper chain. - If you reach the bottom and everything looks valid, take a
.harfile from the live page and compare it against the renderer’s network log. The difference is the bug.
Most “broken creative” tickets resolve in under five minutes with this loop. The remaining cases are usually a CSP or a sandbox attribute on the publisher’s side, which is a configuration problem rather than a creative problem.