Skip to content
Lebtaglebtag
Published on5 min read

The gtag bug that silently drops every event you send

The script loads, the dataLayer fills, the console reports nothing — and no event reaches GA4. The cause is one line that most modern implementations rewrite incorrectly.

  • Analytics
  • GA4
  • JavaScript
  • Debugging

There is a class of bug worse than the error that breaks the page: the one that breaks nothing. The Google Analytics script loads fine, the dataLayer array fills on every interaction, the console stays clean — and no event shows up in the report. You spend days hunting for a configuration problem, a property problem, an ad blocker. The problem is one line of JavaScript.

The symptom

You open the console, inspect the dataLayer and see the events sitting there. Everything suggests the instrumentation works. In the GA4 dashboard, Realtime shows nothing. No request goes out to the collection endpoint. No error is thrown.

That silence is what fools you. A visible error gets fixed in minutes; a silent drop can cost weeks of lost data before anyone notices.

The cause

Google's official snippet declares the gtag function like this:

window.dataLayer = window.dataLayer || [];
function gtag(){ dataLayer.push(arguments); }

Notice two unusual things: the function declares no parameters, and it pushes arguments — the special object every non-arrow JavaScript function receives. That looks like a leftover of old style. It isn't.

Anyone modernizing that code tends to write the version that looks equivalent:

// WRONG: looks identical, drops everything
const gtag = (...args) => dataLayer.push(args);

The difference: the rest parameter args is a real Array. The arguments object in the original snippet is array-like — similar to an array, but not one. And gtag.js only processes dataLayer items that are arguments objects. Anything else is ignored. No error, no warning, no log.

Why Google did it that way

gtag.js has to tell apart two kinds of item in the same array: commands it should execute, and data other tools placed there. The dataLayer is shared with Google Tag Manager and with any script on the page. Using the signature of arguments as a marker is how it says "this one is my command", without relying on a naming convention anyone could imitate.

The practical consequence is that the shape of the function is part of the contract. It isn't style — it's protocol.

The fix

Keep the declaration exactly as Google publishes it and solve typing from the outside. In TypeScript:

declare global {
  interface Window {
    dataLayer: unknown[];
    gtag: (...args: unknown[]) => void;
  }
}

// the implementation declares NO parameters
window.dataLayer = window.dataLayer || [];
function gtag() {
  // eslint-disable-next-line prefer-rest-params
  window.dataLayer.push(arguments);
}

The public signature comes from the type annotation; the implementation stays the snippet's. It is ugly to anyone who likes modern code, and it is what works.

How to confirm it is right

Don't trust a full dataLayer — it fills either way. Trust the network:

  1. Open the browser's Network tab and filter by collect or google-analytics.
  2. Fire an event on the page.
  3. A request should leave for the collection endpoint. If the dataLayer grows and no request appears, this is the bug.
  4. Confirm in GA4 Realtime as well, the only report with no processing delay.

The other three causes of the same symptom

When the gtag implementation is correct and events still don't arrive, the suspect list is short:

  • Consent denied or not yet answered. In a true opt-in setup nothing is sent before acceptance — and that is expected behaviour, not a defect.
  • An ad blocker. Testing in a private window with no extensions settles it in ten seconds.
  • A Content Security Policy missing Google's domains. Here the console does complain, so it's the easiest of the three.
The rule that sticks: a full dataLayer proves nothing. A request leaving the browser proves something.

Why we wrote this

This bug cost us time on a production project, and the answer wasn't anywhere obvious — it is implicit in the shape of the official snippet, which almost every tutorial rewrites "improved". It is the kind of trap that only shows up for people instrumenting analytics seriously, which is why it deserves to be written down.