Skip to content
Pierre Bossetti

dataLayer

Tagging Plan & Audit

Your GA4 sales do not match your CRM? I audit your tracking, find what breaks it, and give you a clear tagging plan built on clean vanilla JS.

Why GA4 and your CRM never match

You open GA4. It shows 120 sales this month. You open your CRM. It shows 150. Which one is right? Usually the CRM is. GA4 is missing data, and you can't tell how much.

Three technical causes explain most of the gap:

  • Timing gaps. GA4 records a sale when the thank-you page loads. Your CRM records it when the payment clears. If the visitor closes the tab too early, GA4 never sees the sale. Refunds and offline deals never reach GA4 at all.
  • Browser ad-blockers. Many blockers stop the GA4 request before it leaves the browser. The visitor still buys. Your CRM sees it. GA4 does not.
  • Broken redirect parameters. Ad clicks carry parameters such as gclid and utm_source. A bad redirect (http to https, www to non-www, a login step) drops them. The sale stays, but its source is gone.

This matters because you steer your budget with these numbers. If GA4 under-counts a channel, you cut it. If it over-counts another, you feed it. You spend more on what looks good and less on what really works. Growth stalls, and nobody knows why.

Costly tracking failures for SMEs

Two bugs show up again and again. Both look small. Both cost real money.

Event duplication

A purchase event fires twice. Common causes: a plugin and a GTM tag both send it, or the visitor reloads the thank-you page.

Google Ads and Meta use these events to learn who your best buyers are. This is called smart bidding. If you send two events per sale, the algorithm sees double the conversions and learns from the wrong profiles. It bids on the wrong people and your cost per sale goes up. The fix is simple: one event per sale, with a uniquetransaction_id.

Payment gateway attribution leaks

Your customer clicks an ad, picks a product, and pays on Stripe or PayPal. Then they come back to your site. GA4 sees a new visit that comes from stripe.com orpaypal.com. The original ad source is lost.

Without a referral exclusion, the sale is credited to the payment processor. Your ads show no sales. Your report says PayPal "brought" the customer. The fix takes two minutes in GA4 (unwanted referrals list), but only if someone checks for it.

The engineering solution: clean vanilla JS architecture

Plugins and tag soup vs. native code

Most sites track with a pile of plugins and GTM triggers that read the page: "when this button with that CSS class is clicked, send an event". It works until a designer renames a class. Then tracking breaks and nobody notices for weeks. Plugins also load extra scripts, which slows the page.

I build tracking the way I build the rest of your app: in vanilla JS, with small, readable code. It is fast, it's easy to maintain, and it adds no weight to your page rendering.

Decouple tracking from the DOM with CustomEvent

Your application code should not know that GA4 exists. It should only say what happened. The browser has a built-in tool for this: the CustomEvent API. When a lead is confirmed, the app announces it:

const addLeadEvent = new CustomEvent('app:lead_success', {
  detail: {
    formId: 'contact_pme',
    leadQuality: 'qualified',
    revenue: 500
  },
  bubbles: true
});
document.dispatchEvent(addLeadEvent);

The event name is yours (app:lead_success). The payload sits indetail. Because bubbles istrue, any listener above it can catch it. No CSS selector is involved. You can redesign the form tomorrow and tracking keeps working.

One global listener feeds the dataLayer

A single listener catches the event and sends it to window.dataLayerwith dataLayer.push():

document.addEventListener('app:lead_success', (event) => {
  const { formId, leadQuality, revenue } = event.detail;

  window.dataLayer = window.dataLayer || [];
  window.dataLayer.push({
    event: 'generate_lead',
    form_id: formId,
    lead_quality: leadQuality,
    value: revenue
  });
});

Note the line window.dataLayer = window.dataLayer || []. It creates the array only if it does not exist yet. The code then usespush() and never assigns a new array. That matters: if you writewindow.dataLayer = [...] later, you wipe every event already queued, including the ones from Google Tag Manager itself.

The result: one place to read, one place to fix, and a clean event list that I document in your tagging plan. The audit gives you that plan, plus a list of what is broken today and in what order to fix it.