Automating AdSense Reports with Custom Metrics That Actually Work

Automating AdSense Reports with Custom Metrics (That Actually Work)

Why the default AdSense reports slowly drove me insane

At some point in late 2022, I realized I had no freaking idea what content was making me money. I mean, the AdSense dashboard says you can do URL-level reporting. But past a few presets, it’s riskier than opening DevTools on a client’s site during a Zoom share: unpredictable and mildly terrifying.

Even worse, the default “Pages” report glues together parameters like UTM strings and query args, which made my pagination traffic look like 800 different pages all earning pennies. I had to set up a custom reporting pipeline just to figure out if /blog and /blog?page=2 were cannibalizing each other.

Minor detail: AdSense does treat every unique URL string as a separate line item in reports unless canonicalized aggressively. So yeah, if you’re not normalizing your pathnames when ingesting this stuff, good luck finding high-performing topics.

Using the AdSense Management API without punching a wall

So, I dug into the AdSense Management API. You need OAuth2 credentials to even begin playing with the toy, which isn’t bad — until you realize the quota system will occasionally rate-limit you into oblivion.

What helped: scoping my queries tightly inside date ranges (7 days max per run), and building results into an SQLite buffer. I was doing this with a half-written Python script at first, but finally copied in pieces of stackoverflow’d Node.js because I couldn’t get proper async behavior from the Google client lib imports.

There’s also a weird delay in revenue attribution when querying the API. You’ll often find “today” has $0.00, even when the dashboard shows something. Turns out, that’s not a bug — the API just lags by a few hours. This is the stuff they don’t paste into the product tour.

Building custom metrics from the raw report firehose

I eventually got the hang of slicing incoming data rows into pseudo-metrics AdSense will never give you, like:

  • Revenue per scroll-depth bucket (logged using an event listener on intersection points)
  • RPM by Lighthouse performance score (manually batched in Cloud Functions)
  • Per-user-ad-unit click probabilities (not individually, just cohort ranges)
  • Delay from page render to first click as a bounce correlation

These aren’t available in AdSense natively, obviously, but when you cash-match event logs with revenue blobs via a key like hashed page URL + user location, you can inch towards building something that answers real questions.

A messy moment: I accidentally filtered out all users from Safari when testing an AMP variant. The AdSense numbers dropped sharply and I couldn’t figure out why for days. Turned out AMP was backfiring only on iOS Safari, and I wasn’t even logging it separately.

What to do when ad_unit data just goes missing

This one made me seriously consider switching to affiliate links exclusively. Sometimes — and this happens maybe once every few weeks — an entire ad unit will just vanish from all reports. Zero impressions, zero clicks, zero spend. But it was rendered. I confirmed it using a session recorder (FullStory) and manual DOM capture.

The gotcha? AdSense will drop units from reporting if the final bid value is null even though the script attempted a fill. Which… happens if your content is too ad-cluttered and the auction skips that slot altogether. But the page still shows an empty iframe.

I added a debug script into my site that logs:

googletag.cmd.push(function() {
  googletag.pubads().addEventListener('slotRenderEnded', function(event) {
    if(!event.isEmpty) {
      console.log('Rendered ad:', event.slot.getAdUnitPath());
    } else {
      console.warn('Ad slot empty:', event.slot.getAdUnitPath());
    }
  });
});

After that, I could correlate which units were genuinely missing vs. zero-rev.

Injecting scraped metrics back into Looker Studio

I thought Looker Studio (née Data Studio) letting you plug in custom JSON connectors would solve everything. Nope. If the data structure doesn’t match AdSense’s schema perfectly — like, overly normalized tables or missing dimension joins — your visualizations will just vanish without warning.

I ended up using Google Sheets as a staging layer instead. My script now flattens the JSON metrics, normalizes field names to match AdSense’s (e.g. “page_url”, “country_code”, “ad_unit_id”), and just dumps it daily to a tab-delimited worksheet auto-linked to the Looker Studio connector.

Bonus discovery: If your calculated fields in Looker include anything over 10 separate filters, the refreshes fail silently. You just won’t see updates, and there’s zero logging unless you inspect the network tab. Found this out the hard way when a date-filtered revenue sum just froze one day. Cleared out unused fields = fixed instantly.

Batching by content category name isn’t reliable

So many people try to group page revenue by blog category, expecting clean splits. I did that too. Bad idea. AdSense doesn’t understand /blog/tag/javascript the same way your CMS does. If you’re applying category data manually — either client-side in meta tags or server-side via URL rules — you have to scrub the paths for things like:

  • Trailing slashes and index.html suffixes (yes, those show up separately)
  • UTM parameters from past campaigns that still get crawled
  • Broken cache rewrites (I had a URL fragment showing up like a separate path)

What worked better: constructing your own category map in a separate table or Cloud Function, and joining against that by partial match or regex fallback. It’s brittle, but at least you know what you broke.

Yes, you can detect adblock with fetch fallbacks

This isn’t strictly “reporting automation” but it helped us bias traffic quality estimates. We added a super-lightweight ad blocker detector that tries to fetch the AdSense script using fetch(). Most blockers don’t just erase the iframe, they also nuke the upstream requests.

Snippet looked like:

fetch("https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js")
  .then(r => {
    if (!r.ok) throw new Error("blocked");
    return r.text();
  })
  .then(() => localStorage.setItem("adCheck", "ok"))
  .catch(() => localStorage.setItem("adCheck", "blocked"));

This lets you stash per-visitor adblock status and even tag sessions for bias-correction in downstream reporting. Side effect: you’ll start noticing weird patterns, like Firefox beta users on Windows 11 disproportionately blocking all ad scripts, but not CSS trackers.

Undocumented quirk: timezone mismatch breaks date pivots in CSV exports

This one broke everything in a spreadsheet I had running for months. AdSense exports its reports in UTC, even if your dashboard is set for local time. But if you then compare that with event logs (e.g. user clicked an ad at 11:07 local time), your pivot tables will show mismatches unless you subtract the timezone offset.

It gets worse: the exported CSV sometimes includes a localized date label depending on your browser locale, but the date dimension field remains UTC-aligned. One line said “05/01/2024” when the actual rows included clicks from May 2 in Pacific time. None of this is in the docs.

Hidden gem: EPMV (earnings per thousand visitors) actually matters more

One of the smarter people I met at an adtech meetup in 2023 dropped a quote that stuck with me:

“RPM looks better on paper, but EPMV pays the rent.”

You’ll find it buried in places like Ezoic and Mediavine dashboards, but nobody talks about this when they’re buried in AdSense’s interface. The truth is: just because your RPM jumps doesn’t mean your total revenue goes up. You need to factor in reduced pageviews from ad fatigue, slower load times, or user suppression via ad block.

I eventually bolted on a second metric layer to my reports that logs raw user count to each page (using server-side UA parse + localstorage deduplication), then relays it through a middle tier to estimate true EPMV. Watching that drop while RPM rose was oddly calming. It confirmed I wasn’t crazy — just measuring the wrong thing.

Similar Posts