I build and test analytics implementations for a living, and one problem that keeps coming up for marketing teams is simple but costly: revenue not attributed in GA4 because tracking calls never fired or were blocked. I’ve put together a tiny validation script that runs on page and checks the essential signals GA4 needs to record conversions and revenue. It’s lightweight, non-invasive, and focused on the specific checks that prevent lost revenue attribution.
Why a tiny validator matters
Large analytics audits are useful, but they take time and engineering support. For marketers who need fast confidence—before a campaign goes live, after a tag update, or when testing a checkout flow—you want something quick that highlights the high-risk failures that cost money.
Common causes of lost revenue attribution I see:
This script doesn’t replace instrumentation work or a full QA, but it flags the practical problems that lead to unreported purchases.
What the script checks
Design choices were intentionally narrow. The script runs quickly in the browser and performs checks that don’t require backend access:
How I use it in practice
When we onboard a new tracking change—like a new commerce plugin or a checkout redesign—I drop this script into the browser console on a staging purchase flow. It takes seconds and returns clear warnings versus green checks. If something is failing we know where to start: missing measurement ID, blocked request, or malformed purchase payload.
I also use it when working with partner integrations (Shopify apps, Magento plugins, headless setups). These can add layers where parameters are dropped or renamed. The script helps catch those quickly so merchants don’t lose revenue during a rollout.
Lightweight validator script (copy-paste)
The script below is intentionally small and dependency-free. It’s written for the browser console or a bookmarklet. It will not send analytics data anywhere; it only inspects the page and the global objects used for GA4.
(function(){ var results = []; function ok(msg){ results.push({ok:true, msg:msg}); } function warn(msg){ results.push({ok:false, msg:msg}); } // 1. measurement ID check var measurementID = null; if (window.gtag && window.gtag.name === 'gtag') { // try to read configured measurement ID from gtag calls cached on page (best effort) // look for gtag('config', 'G-XXXX') if (window.dataLayer && Array.isArray(window.dataLayer)) { window.dataLayer.forEach(function(item){ if (Array.isArray(item) && item[0] === 'config' && typeof item[1] === 'string' && item[1].indexOf('G-') === 0) { measurementID = item[1]; } }); } } // fallback: check script tags if (!measurementID) { var scripts = document.querySelectorAll('script[src]'); scripts.forEach(function(s){ var m = s.src.match(/G-[A-Z0-9]+/i); if (m) measurementID = m[0]; }); } if (measurementID) ok('Measurement ID found: ' + measurementID); else warn('No GA4 measurement ID detected on page.'); // 2. gtag/dataLayer readiness if (typeof window.gtag === 'function') ok('gtag function available.'); else if (window.dataLayer) ok('dataLayer found; gtag may be using dataLayer.'); else warn('Neither gtag function nor dataLayer found.'); // 3. client id check (best-effort) function readClientIdFromCookies(){ var m = document.cookie.match(/_ga=GA[0-9]\.[0-9]\.([0-9]+)\.([0-9]+)/); return m ? (m[1] + '.' + m[2]) : null; } var clientId = null; try { // gtag global function sometimes exposes get if (typeof window.gtag === 'function' && window.gtag.get) { // Not always available; wrapped in try/catch window.gtag('get', measurementID, 'client_id', function(id){ clientId = id; }); } } catch(e){} if (!clientId) clientId = readClientIdFromCookies(); if (clientId) ok('Client ID present: ' + clientId.substring(0,10) + '...'); else warn('Client ID not found; session stitching may fail.'); // 4. check for typical purchase event on confirmation page var hasPurchaseEvent = false; if (window.dataLayer && Array.isArray(window.dataLayer)) { window.dataLayer.forEach(function(item){ if (item && (item.event === 'purchase' || (item.event && item.ecommerce && item.ecommerce.purchase))) { hasPurchaseEvent = true; // quick payload sanity var p = item.ecommerce && item.ecommerce.purchase ? item.ecommerce.purchase : item; var errs = []; if (!p.value && p.revenue == null) errs.push('no value/revenue'); if (!p.currency) errs.push('no currency'); if (!p.transaction_id) errs.push('no transaction_id'); if (!p.items || !Array.isArray(p.items) || p.items.length === 0) errs.push('no items'); if (errs.length) warn('Purchase event present but missing: ' + errs.join(', ')); else ok('Purchase event present with expected params.'); } }); } if (!hasPurchaseEvent) { // also check for global "purchase" event sent via gtag // This is best-effort and may miss server-side receipts. ok('No purchase event found in dataLayer. If you rely on server-side or Measurement Protocol, verify those separately.'); } // 5. network request check (best-effort using Performance API) var pints = performance.getEntriesByType('resource').filter(function(r){ return r.name.indexOf('google-analytics.com') !== -1 || r.name.indexOf('collect?v=2') !== -1 || r.name.indexOf('mp/collect') !== -1; }); if (pints.length) ok('GA network request observed (count: ' + pints.length + ').'); else warn('No GA network requests observed in resource timing. Could be blocked by adblock or use server-side tagging.'); // print results console.group('GA4 quick validator'); results.forEach(function(r){ if (r.ok) console.log('%c✔ ' + r.msg, 'color:green'); else console.warn('✖ ' + r.msg); }); console.groupEnd(); // also show on page var d = document.createElement('div'); d.style.position='fixed'; d.style.right='12px'; d.style.bottom='12px'; d.style.padding='10px'; d.style.background='#111'; d.style.color='#fff'; d.style.fontSize='12px'; d.style.zIndex=2147483647; d.style.borderRadius='6px'; d.textContent = results.filter(r=>r.ok).length + '/' + results.length + ' checks passed. Open console for details.'; document.body.appendChild(d);})();Limitations and what this script won't do
Be upfront about the limits:
Tips for teams to prevent lost revenue attribution
Where this fits in your toolkit
This is a pragmatic tool for marketers and growth teams who need immediate visibility. For deeper audits, combine it with:
If you want a version adapted to a specific stack (Shopify, WooCommerce, headless checkout) I can share quick variants that look for the provider-specific payloads. Drop me a note and I’ll post a tailored snippet.