CreativeCape

How to Build a GA4 dataLayer That Doesn't Lie

Most GA4 data is wrong because the dataLayer was never planned. Here's the tracking plan, dataLayer spec and GTM setup we use in production — with real code.

September 13, 2026·6 min read

Most analytics problems are not reporting problems. They are implementation problems that nobody noticed for six months.

You open GA4, the numbers look plausible, and you make a decision. Then someone reconciles revenue against the orders table and it is off by 18%. Now every number in the property is suspect, including the ones that were fine.

Almost always, the root cause is the same: nobody designed the dataLayer. Tags were added one at a time, by different people, under deadline, and the "data layer" became whatever each tag happened to need that week.

Here is how we build it instead.

Why most GA4 data can't be trusted

Three failure modes account for the majority of bad analytics data:

Events were named by whoever wrote them. addToCart, add_cart, AddToCart and cart_add all end up in the same property. GA4 treats them as four different events, so no report ever shows the real total.

Values are read from the DOM. Scraping a price out of a rendered element works until a designer adds a currency symbol, a discount badge, or a thousands separator. Then revenue silently becomes NaN or 1 instead of 1,299.

Events fire on intent instead of outcome. A "purchase" that fires when the user clicks Pay counts abandoned and failed payments as revenue. This one is extremely common and it inflates your most important number.

None of these are fixed by changing a setting in GA4. They are fixed upstream, in the dataLayer.

What a tracking plan actually is

A tracking plan is a document, agreed before implementation, that answers: what do we measure, what is it called, and what does it carry?

For each event it records the name, when it fires, every parameter with its type, and which business question it answers. That last column matters more than it sounds — it is what stops you shipping forty events nobody ever opens a report for.

The plan is the contract. Developers implement it, the analyst validates against it, and when someone asks "why don't we track X", the answer is a documented decision rather than an archaeology exercise.

Write it before you touch GTM. It takes a day and saves months.

Designing the dataLayer: page context, user, ecommerce

We think of the dataLayer in three layers.

Page context — pushed on every route change. What kind of page is this?


dataLayer.push({

  event: "page_meta",

  page_type: "pdp",        // home | plp | pdp | cart | checkout | account

  page_name: "/products/acme-kit",

  locale: "en",

});

page_type is the single highest-value custom dimension you will ever add. It lets you segment every metric by template rather than by URL pattern, which is what makes cross-site comparison possible.

User context — who is this, in non-identifying terms?


dataLayer.push({ user: { user_id: "8f3c...", logged_in: true } });

Send an opaque internal id. Never push email addresses, names or phone numbers into the dataLayer — it is readable by anything running on the page, and it will end up in places you cannot retract it from.

Ecommerce — the GA4 commerce schema, which we cover below.

Naming conventions that survive a redesign

Pick the rules once and write them down:

  • snake_case everywhere, for events and parameters. GA4's own recommended events use it; matching them means you get the built-in reports for free.

  • Use GA4's recommended event names wherever one exists (view_item, add_to_cart, begin_checkout, purchase, sign_up, login, generate_lead). Do not invent product_viewed — you lose the standard reporting.

  • Name by what happened, not by where. select_item is good. homepage_carousel_click is not: it breaks the moment the carousel moves to another page.

  • Parameters describe the event; dimensions describe the context. Keep page_type in the shared context, not repeated inside every event payload by hand.

The nested ecommerce object (and the ecommerce:null reset)

This is where GTM implementations most often go wrong. GTM reads ecommerce data as a nested object, not the flat gtag.js shape:


dataLayer.push({ ecommerce: null });   // clear the previous payload

dataLayer.push({

  event: "add_to_cart",

  ecommerce: {

    currency: "USD",

    value: 79.00,

    items: [{

      item_id:   "acme-kit",

      item_name: "Acme Starter Kit",

      item_brand: "CreativeCape",

      item_category: "template",

      item_variant: "extended",

      price: 79.00,

      quantity: 1,

    }],

  },

});

The { ecommerce: null } push is not optional. The dataLayer is a persistent array and GTM merges pushes. Without the reset, items from a previous event leak into the next one — you get an add_to_cart carrying the three products from the earlier view_item_list, and your item-level reports quietly become fiction.

Put both pushes in one helper so nobody can forget the reset:


function pushEcom(event, ecommerce) {

  window.dataLayer = window.dataLayer || [];

  window.dataLayer.push({ ecommerce: null });

  window.dataLayer.push({ event, ecommerce });

}

Every ecommerce event in the app should go through that function. One place to fix, one place to audit.

Wiring GTM: variables, triggers, tags

With a consistent dataLayer, the GTM container becomes small:

  1. Variables — a Data Layer Variable per key you need (ecommerce, page_type, user.user_id, method). The "Data Layer Variable Name" must be the raw key (ecommerce), not the variable's display label. This trips up almost everyone once.

  2. One ecommerce trigger — a Custom Event trigger matching a regex:

    ^(view_item_list|select_item|view_item|add_to_cart|remove_from_cart|view_cart|begin_checkout|add_payment_info|purchase|refund)$

  3. One ecommerce tag — a GA4 Event tag with Event Name set to {{Event}} and Send Ecommerce data = Data Layer. That single tag covers the entire funnel.

The instinct is to build one tag per event. Resist it. Ten tags means ten places to update when the measurement ID changes and ten chances for one to drift out of sync.

QA in DebugView

Implementation is not done when the tag fires. It is done when the right data arrives.

Work through the funnel in GTM Preview with GA4 DebugView open beside it, and check:

  • Each event fires exactly once per user action. Double-fires usually mean a React effect without a guard, or a tag on both a click and a route-change trigger.

  • Item arrays contain the right items, not leftovers from the previous event.

  • value is a number, not a string, and matches what the server will charge.

  • purchase fires only after the server confirms the payment.

  • Nothing fires with consent denied if your tags require analytics_storage.

Then reconcile: pull a day of GA4 purchases against the orders table. If they do not match within a percent or two, find out why before you build a single report on top.

Documentation and handover

The last step is the one that gets skipped. Write down the container structure, the dataLayer spec, and how to add a new event. Without it, the implementation degrades the moment the person who built it moves on — which is precisely how the mess you just cleaned up came to exist.


Want this done properly on your site? → Analytics Implementation

Tagged with
#ga4#datalayer#google tag manager#tracking plan#analytics implementation#gtm

Found this useful? Share it.

Keep Reading

Related articles

Booking Q2 2026 Projects

Ready to Build Something Great?

From idea to launch — let our senior engineers build, ship and scale your next product. No commitment, just a conversation.

Senior Engineers
On-Time Delivery
Enterprise-Grade
Free Consultation

Free 30-min discovery call

Talk to a senior engineer — not a salesperson.

We'll review your goals, suggest the leanest path forward, and send a clear proposal within 24 hours.

24h

Response Time

100+

Projects Delivered

No commitment · No automated bots · Fully transparent