Tracking AdSense Revenue Across Devices Without Losing Attribution

AdSense’s Session-Centric Default and Why It Fails Cross-Device

First thing to understand: AdSense works in snapshots, not stories. It sees each browser — each session really — as standalone. That ends up trashing cross-device attribution faster than you’d think.

Let’s say the user sees an ad on their phone at lunch, doesn’t click, forgets. Then they go home and search the brand on desktop, click the organic result, and buy. You get…nothing. AdSense doesn’t pay since the conversion didn’t happen in the tracked session. That hit me hard when one of my top CTR campaigns yielded almost zero attributed earnings during a week where Shopify metrics said otherwise. Turns out, the mobile clicks weren’t translating into conversion credit.

Cross-device behavior is increasingly normal. Users jump between contexts — phone → smart TV → desktop. But AdSense still treats each environment as a completely separate stream. Unless the ad click leads directly to a conversion within a tight timeframe on the same device, good luck tying it together.

Honestly, it’s like trying to follow a movie where every 10 minutes, you’re forced to switch theaters.

Cookie Domain Scoping and Its Implicit Wall

This one gets overlooked constantly. If your site spans subdomains (e.g., shop.example.com and blog.example.com), and you’re relying on AdSense’s automatic behavior, you’re probably borking attribution silently.

By default, cookies set by scripts like adsbygoogle.js get scoped to the subdomain. Which means the UID set on blog.example.com doesn’t travel with the visitor to shop.example.com. That breaks whatever thread existed between the discovery click and the conversion view.

The fix? Set your own visitor ID via first-party labeling using the data-ad-client property and pass a stable identifier across domains — but do not rely on AdSense to do it. Even better, own the session ID tracking in a cookie like _yourcustomUID, scoped to .example.com.

I chased this down after noticing that one of my product reviews (on a subdomain) had great clickthroughs but seemed to contribute zero value to my shop’s funnel. Digging in, different AdSense sessions were being recognized. So the actual valuable traffic wasn’t getting credited. Brutal.

The Gap Between GA4 Attribution and AdSense Click Flow

Google Analytics 4 and AdSense might as well speak different dialects of attribution logic. GA4 offers flexible models – last-click, data-driven, blah blah – but all of them assume events across a known user ID. Meanwhile, AdSense only sees what happens within its window, with its own internal rules. They don’t sync unless you forcibly bind them.

“I had users clicking an AdSense ad (with UTM tags), converting 3 days later, but GA4 claimed credit for organic. AdSense showed ad clicks, but no revenue. It was like two disconnected realities.”

If you’re tagging your campaigns manually, make sure you’re also wiring those UTMs into a client-side identifier. Use something like this in your JS:

const params = new URLSearchParams(location.search);
const campaignId = params.get('gclid') || params.get('utm_campaign');
if (campaignId) {
    document.cookie = `ad_click_id=${campaignId}; path=/; domain=.yourdomain.com; max-age=2592000`;
}

Then pipe that into your CRM or conversion handler later. Otherwise, GA thinks Organic is the hero while AdSense gets ignored. And worse: AdSense doesn’t attribute delayed revenue at all unless there’s a direct correlation within its session window.

Unclickable Ads: Dev Builds, iFrames, and Z-Index Purgatory

Your ads are loading. They look fine. But your RPM is flatlining. Look closer: you may have accidentally made every ad unclickable.

Saw this one during a Gatsby.js dev build — the ads were there, visually perfect, but the ad frame was being covered ever so subtly by a zero-opacity container. Why? Blame the z-index layering plus a global layout wrapper. Depends on how your hydration cycle renders React vs legacy DOM blocks.

Quick checklist if you suspect blocked AdSense clicks:

  • Inspect element and hover over the ad — if you can’t right-click or interact, it’s probably blocked
  • Temporarily add a background color or border to any wrappers above the ad to confirm visibility
  • Try clicking from Incognito with all extensions off (some extensions, esp. privacy filters, eat click handlers)
  • Double-check your CSP headers – overly strict frame-src or child-src rules can silently deoptimize your ads
  • On Firefox, the Enhanced Tracking Protection may block pagead2.googlesyndication.com under some policies

Oh — and if you’re running AdSense inside <iframe sandbox> structures without allow-scripts or allow-popups, kiss interaction goodbye.

Spoofed User Agents and Skewed Attribution Timelines

There’s something deeply annoying (and kind of funny) about bots spoofing mobile browsers. When you’re running placement tests on mobile layouts, you might assume real click patterns. Then you zoom into logs and find out half those sessions were phantom crawlers. They click nothing, convert never, but they skew Impressions and CTR deltas just enough to gaslight you.

On one occasion, over two dozen clicks from a supposed iPhone 14 Safari user came through overnight. Strangely consistent screen dimensions. Spoiler: they weren’t real. Used fingerprinting detection on one endpoint and saw userAgent + screenX + timezone all conflicting — clear bot activity.

Unfortunately, AdSense attribution doesn’t distinguish scrapers and spoofers well in the Clicks count. That messes with your eCPM assumptions if you base them on raw volume. Really wish they’d let us filter traffic anomalies more directly from the dashboard — or at least flag suspicious burst patterns.

Undocumented Behavior: AdSense Losing Attribution on Custom SPA Navigations

This one’s evil. You think your SPA is routing cleanly with pushState changes — all good, no full page reloads, right? Except AdSense treats some navigations as hard page loads and loses ad context. Sometimes. Not always.

I saw this on a React site using react-router. AdSense loads fine on entry pages. But navigating inside the app — say from “Blog” to “Pricing” — would load ads, but clicks didn’t track. Turns out that without a full reload, the ad script wasn’t reinitializing attribution hooks. So technically, the ad showed, the user clicked, but AdSense skipped the event linkage.

The fix? After every navigation, force ad slot redefinition with a refresh. Like:

window.adsbygoogle = window.adsbygoogle || [];
window.adsbygoogle.push({}); // after router change

You also need to make sure that the ad DOM node gets fully removed and re-added, or the script might ignore it entirely.

None of this is in the docs — I only figured it out after loading network logs alongside DOM mutation observers during ad interactions. AdSense’s heuristic for context scope is extremely brittle on custom SPAs.

What You Can Actually Do Right Now to Stitch Attribution

While we wait for Google to embrace persistent user attribution across devices (ha), here’s what I’ve patched together that helps:

  • Use a shared visitor UUID via a root-domain cookie that you control (not AdSense)
  • Capture GCLID or UTM tags into long-lived client cookies and inject into conversions server-side
  • Collect ad click timestamps and tie them loosely to signup timestamps for after-the-fact correlation
  • If you’re allowed, run parallel GA4 + server logs + pixel tracking to triangulate journeys more reliably
  • Map high CTR ad locations to behavioral patterns (e.g., mobile scroll depth, time on ad view)
  • A/B test layouts with and without sticky mobile footers — it’s shocking how placement shifts bias engagement patterns
  • Label known bot traffic early via fingerprinting.js to stop false-positives in attribution guesswork

Nothing here is magical. Bits of this probably break next quarter. But if you’re serious about knowing where your revenue’s actually coming from (especially post-click, cross-device), duct taping your attribution stack is non-negotiable.

Similar Posts