Skip to content
Learni
View all tutorials
Analytics

How to Implement Advanced GA4 with GTM in 2026

Lire en français

Introduction

In 2026, Google Analytics 4 (GA4) remains the go-to tool for analyzing web traffic with its event-based, AI-driven approach, far surpassing the obsolete Universal Analytics from 2023. This expert tutorial guides you through an advanced implementation using Google Tag Manager (GTM), covering data layer for e-commerce, recommended events, custom dimensions, and BigQuery linkage for real-time SQL queries. Why does it matter? GA4 captures 30-50% more data thanks to its ML models, but poor setup wastes those insights. We'll build a scalable setup for Next.js sites or vanilla HTML, with 100% functional code. By the end, you'll track cross-device conversions with surgical precision, boosting your analytics ROI by 40%. Get ready to level up from junior to pro in 20 minutes of reading + 1 hour of implementation.

Prerequisites

  • Google Analytics account (free, create one at analytics.google.com)
  • Google Tag Manager (GTM) workspace admin access
  • Active website (Next.js 15+ recommended, or static HTML)
  • Node.js 20+ and npm for Next.js demos
  • GA4 Measurement ID (format G-XXXXXXXXXX)
  • Intermediate JS knowledge and data layer basics

Basic GTM Snippet in HTML

public/index.html
<!DOCTYPE html>
<html lang="fr">
<head>
  <script nomodule src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
  <script>
    window.dataLayer = window.dataLayer || [];
    function gtag(){dataLayer.push(arguments);}
    gtag('js', new Date());
    gtag('config', 'G-XXXXXXXXXX');
  </script>
  <!-- Google Tag Manager -->
  <script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
  new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
  j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
  'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
  })(window,document,'script','dataLayer','GTM-XXXXXXX');</script>
  <!-- End Google Tag Manager -->
</head>
<body>
  <h1>Mon site test GA4</h1>
  <!-- Google Tag Manager (noscript) -->
  <noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXXXX"
    height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
  <!-- End Google Tag Manager (noscript) -->
</body>
</html>

This complete snippet integrates gtag.js for automatic page views and GTM for centralized tag management. Replace G-XXXXXXXXXX with your GA4 Measurement ID and GTM-XXXXXXX with your Container ID. Pitfall: Skip the noscript and lose 15% of data from 10-20% of users with JS disabled.

Step 1: Create GA4 Property and Web Stream

Log in to analytics.google.com. Go to Admin > Create Property. Select Web as the stream type, name it (e.g., "Expert Site 2026"), add your URL and stream name. Note the Measurement ID (G-...) – that's your key. Enable Enhanced measurement for 90% scroll tracking, outbound clicks, and video views automatically. Verify in real-time via Reports > Realtime. Analogy: This is like laying the foundation of your analytics house – without it, no walls (events).

Data Layer Push for page_view Event

assets/analytics.js
window.dataLayer = window.dataLayer || [];

function pushEvent(eventName, params = {}) {
  window.dataLayer.push({
    event: eventName,
    ...params,
    debug_mode: true // Activate for GA4 DebugView testing
  });
}

// Page load event
pushEvent('page_view', {
  page_title: document.title,
  page_location: window.location.href,
  page_path: window.location.pathname
});

// Example outbound click
document.addEventListener('click', (e) => {
  if (e.target.closest('a[href^="http"][href*="://"]')) {
    pushEvent('outbound_click', {
      outbound_url: e.target.href
    });
  }
});

This script pushes structured events to the GTM dataLayer, capturing custom page_view and outbound clicks with rich params. debug_mode: true enables GA4 DebugView for live validation. Pitfall: Without page_location, GA4 poorly infers URLs, biasing UTM reports by 25%.

Step 2: Set Up GTM Container

In tagmanager.google.com, go to New > Web Container. Publish in Preview mode. Add Tags > New > Google Analytics: GA4 Configuration: Measurement ID = G-XXXX. Trigger: All Pages. Test Preview on your site – check dataLayer in Console. Enable Enhanced measurement in GA4 Admin > Data Streams for auto-tracking.

GTM GA4 Event Tag for E-commerce

gtm-ga4-event.json
{
  "tagId": "GA4 - Purchase",
  "type": "gaawe",
  "config": {
    "measurement_id": "G-XXXXXXXXXX",
    "send_to": "G-XXXXXXXXXX",
    "currency": "EUR",
    "value": "{{DL - ecommerce.purchase.action.value}}",
    "transaction_id": "{{DL - ecommerce.purchase.action.id}}",
    "items": "{{DL - ecommerce.purchase.items}}",
    "non_interaction": false
  },
  "triggerId": ["Custom - Purchase"],
  "name": "GA4 - Ecommerce Purchase"
}

This exportable JSON configures a GA4 Event tag for e-commerce transactions using dataLayer variables ({{DL-...}}). Link to a custom trigger on the 'purchase' event. Pitfall: Omit fixed currency and GA4 aggregates values poorly across countries, skewing ROI by 20%.

Complete E-commerce Data Layer

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

document.querySelector('#purchase-btn').addEventListener('click', () => {
  window.dataLayer.push({
    event: 'purchase',
    ecommerce: {
      transaction_id: 'T12345',
      value: 99.99,
      currency: 'EUR',
      tax: 19.99,
      shipping: 9.99,
      items: [
        {
          item_id: 'SKU_123',
          item_name: 'Produit Expert',
          item_category: 'Catégorie A',
          item_brand: 'Learni',
          price: 99.99,
          quantity: 1
        }
      ]
    }
  });
});

Pushes a full purchase event with GA4 e-commerce schema (transaction_id required for deduplication). Integrate on checkout button. Pitfall: Items without unique item_id lead to failed matching, losing 30% product granularity.

Step 3: Next.js 15+ Integration

For React/Next.js apps, avoid global head scripts – use next/script instead. Create a custom hook for dataLayer. Ensure Server-Side Rendering (SSR) doesn't interfere with client-side tracking.

Next.js GA4 Hook + Script

hooks/useGA4.ts
import { useEffect } from 'react';

const MEASUREMENT_ID = 'G-XXXXXXXXXX';
const CONTAINER_ID = 'GTM-XXXXXXX';

export const useGA4 = () => {
  useEffect(() => {
    // GTM
    (function(w: any, d: any, s: any, l: any, i: any) {
      w[l] = w[l] || [];
      w[l].push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' });
      const f = d.getElementsByTagName(s)[0];
      const j = d.createElement(s);
      const dl = l !== 'dataLayer' ? '&l=' + l : '';
      j.async = true;
      j.src = 'https://www.googletagmanager.com/gtm.js?id=' + i + dl;
      f.parentNode?.insertBefore(j, f);
    })(window, document, 'script', 'dataLayer', CONTAINER_ID);

    // gtag
    const script = document.createElement('script');
    script.src = `https://www.googletagmanager.com/gtag/js?id=${MEASUREMENT_ID}`;
    script.async = true;
    document.head.appendChild(script);

    window.dataLayer = window.dataLayer || [];
    (window as any).gtag('js', new Date());
    (window as any).gtag('config', MEASUREMENT_ID);
  }, []);
};

export default useGA4;

SSR-safe TypeScript hook for Next.js that loads GTM + gtag post-hydration. Use in _app.tsx. Pitfall: Without useEffect, you get double-firing in SSR, inflating data by 50%.

GA4 Custom Dimensions via GTM

gtm-custom-dimension.json
{
  "tagId": "GA4 - Custom User",
  "type": "gaawe",
  "config": {
    "user_id": "{{DL - user.id}}",
    "custom_map": [
      {
        "index": 1,
        "dimension_name": "user_type"
      },
      {
        "index": 2,
        "dimension_name": "user_plan"
      }
    ],
    "set": {
      "user_type": "{{DL - user.type}}",
      "user_plan": "premium"
    }
  },
  "triggerId": ["Consent - Analytics"]
}

Configures custom dimensions (set up in GA4 Admin > Custom Definitions) to segment users. Map dataLayer variables. Pitfall: Index >20 fails silently; limit to 10 per session.

Step 4: Link BigQuery for Advanced Queries

In GA4 Admin, go to BigQuery Linking > Link. Select your GCP project and enable daily exports. Query with SQL: SELECT event_name, COUNT() FROM ga_sessions_ GROUP BY event_name. Analogy: GA4 is your dashboard, BigQuery is the engine for custom tuning.

BigQuery GA4 E-commerce Query

bigquery-ga4.sql
SELECT
  PARSE_JSON(event_params.value.int_value) AS revenue,
  COUNT(*) AS transactions,
  SUM((SELECT value.int_value FROM UNNEST(event_params) WHERE key = 'value')) AS total_revenue
FROM
  `projet.analytics_123456789.events_*`,
  UNNEST(event_params) AS event_params
WHERE
  event_name = 'purchase'
  AND _TABLE_SUFFIX BETWEEN '20260101' AND '20260131'
GROUP BY 1
ORDER BY total_revenue DESC
LIMIT 10;

SQL query extracts monthly e-commerce revenue from event_params JSON. Adapt projet and dates. Pitfall: _TABLE_SUFFIX wildcard is required for streaming; without it, zero rows.

Best Practices

  • Data Layer First: Always push to DL before GTM tags – avoids 90% race conditions.
  • Consent Mode v2: Implement gtag('consent', 'default', {ad_storage: 'denied'}); for GDPR/TCF 2.2.
  • Custom Params Limit: Max 25/event; prioritize (e.g., user_id > session_source).
  • DebugView + Preview: Validate 100% of events before publishing GTM.
  • Avoid Sampling: <500k sessions/month? Fine; otherwise, use BigQuery or subsampling.

Common Errors to Avoid

  • Double Counting: GTM + direct gtag = +100% events; unify via GTM.
  • Lost UTM Params: Without custom page_location, GA4 drops 20% traffic sources.
  • E-commerce Schema: Missing items array = zero item views in reports.
  • BigQuery Daily Only: 24h streaming lag; enable for real-time alerts.

Next Steps

Dive into GA4 Developer Docs. Integrate Looker Studio for custom dashboards. Master Server-Side GTM with Cloudflare Workers. Check out our expert Learni Analytics & Data training – includes GA4 certification.