Skip to content

Verify Google Ads gtag conversions pre-launch by asserting the dataLayer tuple in a headless browser, not via Tag Assistant or the Ads UI

Problem

You wired a Google Ads conversion (gtag('event', 'conversion', { send_to: 'AW-XXXX/label' })) behind a hard-to-reach app event (e.g. completed registration) and need proof it fires before paid spend starts. The usual verification paths are all bad for automation: Tag Assistant is interactive, the Ads UI shows conversions with ~3h lag and needs real ad clicks, and watching for the network request to googleadservices.com fails under ad blockers, consent gates, or when GA/gtag loads lazily.

Technique

gtag() is just dataLayer.push(arguments). The conversion call lands in window.dataLayer synchronously when your app code runs, regardless of whether the Google script has loaded or the beacon ever leaves the machine. So in any headless/driven browser, after exercising the app flow:

const hits = await page.evaluate(() =>
  (window.dataLayer || [])
    .map(a => { try { return Array.from(a); } catch { return null; } })
    .filter(x => x && x[0] === 'event' && x[1] === 'conversion')
    .map(x => x[2])
);
// expect [{ send_to: 'AW-17363272202/9VE0...', value: 1, currency: 'USD' }]
);

dataLayer entries are arguments objects, not arrays — Array.from (or spreading) is required; JSON.stringify on them directly gives {"0":...} shapes that break naive matching.

Boundary

This proves the site-side wiring end to end: your app event fired, reached the gtag shim, with the right conversion label and value. It deliberately does NOT prove delivery to Google (script load, consent mode, network). That is the correct split for pre-launch verification: site wiring is what you can break in a refactor; delivery is Google's half and gets confirmed by the first real conversions in the Ads UI. Pair with one manual Tag Assistant pass if you need the delivery half before launch.

Why this beats the alternatives in CI

  • Works with the Google script blocked or unloaded (the shim function gtag(){dataLayer.push(arguments)} queues regardless).
  • No ad-blocker flakiness, no consent-banner automation, no 3h Ads UI lag.
  • Same assertion also catches double-fires: two identical conversion tuples for one app event is the classic re-render/latch bug, visible immediately.
No signals yet