Drupalwoo ☉

Integrating Drupal with Google Analytics 4 using custom code

Google Analytics 4 (GA4) can give a Drupal site a clear view of visits, search traffic, content engagement and conversion paths. A contributed module is optional: a small custom module or isolated theme code can install the global site tag, preserve campaign parameters and report events for forms, downloads and commerce. This approach keeps the implementation in version control and makes clear what the site collects.

The code must also fit Australian privacy obligations and the organisation's marketing practices. The setup below uses a measurement ID, a consent gate and data-layer events, then explains how to test it. Standard Drupal page requests are assumed, with separate guidance for views that change without a full reload.

Prepare the GA4 property and Drupal environment

Create a GA4 property and web data stream, then copy its measurement ID, which starts with G-. Decide whether one property will cover the main domain or separate properties will cover campaign sites, staging and regional operations. Keep production IDs out of templates. Store the ID in Drupal settings or environment-specific configuration so deployments can change it without editing theme files. Record the stream URL too, as it is useful for server-side validation and consent checks.

Before injecting code, map redirects, URL aliases, www and non-www hosts, and campaign subdomains. A Drupal alias does not make different hostnames one GA4 page location, so canonical links and domain grouping must match the intended journey. Plan how staff pages will be excluded: restricting a Drupal role does not automatically stop a browser from sending a page view after the tag loads.

Choose the code owner before writing anything. A custom Drupal module is usually easier to review and release than snippets copied into several templates, while a theme can suit a small prototype. Whichever route you take, document tag placement, consent behaviour and test events. The implementation notes offer a practical way to record those decisions before a Drupal upgrade changes rendering or caching.

Install a consent-aware Google tag

A module keeps the tag outside presentation and gives one place to control it. This example reads the measurement ID from Drupal settings, attaches a small JavaScript library and exposes the ID through drupalSettings. It does not create Google's remote script until the site's consent component calls drupalGa4Load, preventing the tag from appearing before analytics permission is granted.

Create ga4.libraries.yml and ga4.module with these definitions:

# ga4.libraries.yml
ga4:
  version: 1.0
  js:
    - js/ga4-init.js
  dependencies:
    - core/drupal
<?php

use Drupal\Core\Site\Settings;

function ga4_page_attachments(array &$attachments): void {
  $measurement_id = Settings::get('ga4_measurement_id', '');

  if ($measurement_id === '') {
    return;
  }

  $attachments['#attached']['library'][] = 'ga4/ga4';
  $attachments['#attached']['drupalSettings']['ga4']['measurementId'] = $measurement_id;
}

Set the ID in settings.php with $settings['ga4_measurement_id'] = 'G-XXXXXXXXXX';. The loader reuses any existing dataLayer, rejects an invalid ID and prevents the remote script from being inserted twice:

(function (settings) {
  const measurementId = settings.ga4 && settings.ga4.measurementId;

  if (!measurementId || !/^G-[A-Z0-9]+$/.test(measurementId)) {
    return;
  }

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

  window.drupalGa4Load = function () {
    if (document.querySelector('script[data-drupal-ga4]')) {
      return;
    }

    window.drupalGa4Granted = true;
    window.gtag('js', new Date());
    window.gtag('config', measurementId);

    const script = document.createElement('script');
    script.async = true;
    script.dataset.drupalGa4 = 'true';
    script.src = 'https://www.googletagmanager.com/gtag/js?id=' + encodeURIComponent(measurementId);
    document.head.appendChild(script);
  };
})(window.drupalSettings || {});

Call window.drupalGa4Load() from the consent manager's analytics-granted callback. If the site's policy and legal review permit analytics without an interactive choice, it can be called during page startup instead. This example implements consent-before-load. For Google Consent Mode advanced or a different CMP, adapt the default, update and load sequence to the provider's documented consent flow. For template-level selectors and event metadata, the advanced Twig templating examples show how Drupal can expose clean attributes without placing analytics scripts in templates.

Track meaningful Drupal interactions

Once the loader runs, GA4 sends the normal page_view. It does not infer every meaningful Drupal action, so custom events should represent a small set of business outcomes. Use consistent snake_case names, keep event names under GA4's limits and register unusual parameters in the GA4 admin interface. Recommended names such as generate_lead, begin_checkout and purchase make reports easier to compare with other acquisition channels.

Add stable data attributes to the element that causes the action, rather than binding code to a theme-specific selector:

<a class="button" href="/contact"
   data-ga4-event="contact_start"
   data-ga4-label="Contact form">Request a quote</a>

A delegated listener then captures the event, including for elements added later by JavaScript. It sends a controlled label and a safe, query-free path instead of arbitrary link text or URL parameters:

document.addEventListener('click', function (event) {
  const trigger = event.target.closest('[data-ga4-event]');

  if (!trigger || !window.drupalGa4Granted) {
    return;
  }

  const href = trigger.getAttribute('href') || window.location.pathname;
  const safePath = href.startsWith('/') ? href.split(/[?#]/)[0] : window.location.pathname;

  window.gtag('event', trigger.dataset.ga4Event, {
    link_text: trigger.dataset.ga4Label || trigger.tagName.toLowerCase(),
    link_url: safePath
  });
});

Commerce sites should send purchase only after the order is confirmed. Include a unique transaction ID, an AUD currency, the value and item data, but never a customer's email, phone number, street address or full order payload. Check GA4's enhanced measurement settings because an automatic purchase event can otherwise be counted twice. Contributed modules that swap page content without a browser reload also need an explicit route-change event; a normal page_view will not describe those transitions.

Choose useful events and reporting checks

Useful event patterns include:

For routine reporting, check:

National reporting needs a deliberate time-zone choice. Sydney and Melbourne move between standard and daylight-saving time, while Brisbane remains on Australian Eastern Standard Time, so daily conversion boundaries can differ around clock changes. Select the GA4 property time zone used by the business and document the rule for campaigns that cross midnight. Mobile conditions also matter: a visitor checking a site during a train journey in Sydney or Melbourne may abandon a slow form, so page speed and accidental-click events deserve attention.

For many Australian organisations, the Privacy Act 1988 and Australian Privacy Principles govern the personal information in marketing systems. State and territory laws, such as Victoria's Privacy and Data Protection Act 2014 or NSW's Privacy and Personal Information Protection Act 1998, may add obligations. A consent-management platform, accurate privacy notice and vendor review are practical safeguards, but legal advice should confirm the organisation's responsibilities. The Spam Act 2003 also matters if GA4 audiences are later used for commercial email. Avoid sending names, addresses or raw customer identifiers to GA4, and apply appropriate retention and access controls to reports.

Compare implementation and maintenance approaches

The custom module is the strongest default for a production Drupal site because it has one attachment point, a reviewable code change and a clear owner. A theme snippet is quicker for a proof of concept, but it can be lost during a theme update or copied inconsistently. Google Tag Manager offers marketing teams a central interface and preview tools, yet it introduces consent, security-policy and troubleshooting work of its own. A contributed module may shorten initial setup, but check its Drupal compatibility and whether it supports the specific GA4 features needed.

Whichever route is chosen, remove any old Universal Analytics or duplicate GA4 snippets before launch. Test an ordinary page view, a campaign visit, consent acceptance, consent refusal and each important event. GA4 Realtime and DebugView can confirm that events arrive once, while browser developer tools reveal blocked scripts, duplicate listeners and cache-related differences. Keep the measurement ID in deployment configuration, and record the exact GA4 and Drupal versions used for each test.

Roll the change out to staging first, where Drupal's render and page caches can be cleared safely. Confirm that the tag appears after the intended consent action, that internal traffic is excluded, and that ecommerce totals agree with Drupal Commerce or another order system. A short rollback procedure is valuable if a template, module update or content-security-policy change interrupts the tag.

Approach Best suited to Main advantage Main caution
Custom Drupal module Production sites needing controlled integration Versioned code and one attachment point Requires Drupal development and release testing
Theme-level snippet Prototypes and very small sites Fast initial setup Can be overwritten or duplicated
Google Tag Manager Teams needing marketing-led tag control Central interface and testing previews Adds consent, CSP and troubleshooting complexity
Contributed module Teams wanting packaged GA4 features Quicker initial configuration Confirm maintenance, compatibility and event coverage