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:

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:

  1. Network: filter by Doc and JS. 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.
  2. 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.
  3. 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.
  4. Performance: record a 3-second trace from before the impression. Look for long tasks (red triangles) and the gap between load and 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:

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:

  1. Mixed content: a wrapper served over HTTPS points at an HTTP VASTAdTagURI. Players on secure pages refuse the upgrade and time out.
  2. 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.
  3. 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:

A 60-second triage checklist

When a publisher reports “the ad is broken,” run this sequence:

  1. Get the bid.adm from logs. Drop it into our ORTB renderer.
  2. Does it render in the renderer? If yes, the bug is in the publisher’s environment (CSP, sandbox, ad slot CSS).
  3. If no, open the renderer’s console. The first uncaught exception is almost always the cause.
  4. Search the adm for mraid, document.write, and ${. Each is a likely culprit.
  5. If the creative is video, fetch the VAST URL with curl and pretty-print the XML. Walk the wrapper chain.
  6. If you reach the bottom and everything looks valid, take a .har file 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.


Related reading