Drupal Commerce GA4 E-commerce Tracking | Blue Frog Docs

Drupal Commerce GA4 E-commerce Tracking

Complete guide to implementing GA4 e-commerce tracking with Drupal Commerce

Drupal Commerce GA4 E-commerce Tracking

Overview

This guide covers implementing comprehensive GA4 e-commerce tracking for Drupal Commerce, including product views, add to cart, checkout steps, purchases, and refunds. Learn how to leverage Commerce hooks and events for accurate revenue tracking.


Prerequisites

  • Drupal Commerce 2.x or 8.x installed
  • GA4 property configured
  • Basic GA4 implementation (see GA4 Setup)
# Install Drupal Commerce if not already installed
composer require drupal/commerce
drush en commerce commerce_product commerce_cart commerce_checkout commerce_order -y

E-commerce Module Setup

Create Custom Commerce Analytics Module

# Create module directory
mkdir -p modules/custom/commerce_ga4
cd modules/custom/commerce_ga4

File: commerce_ga4.info.yml

name: 'Commerce GA4 Analytics'
type: module
description: 'Google Analytics 4 e-commerce tracking for Drupal Commerce'
package: Commerce
core_version_requirement: ^9 || ^10
dependencies:
  - drupal:commerce_product
  - drupal:commerce_cart
  - drupal:commerce_checkout
  - drupal:commerce_order
  - drupal:commerce_payment

File: commerce_ga4.libraries.yml

ecommerce:
  version: 1.x
  js:
    js/commerce-ga4.js: {}
  dependencies:
    - core/drupal
    - core/drupalSettings
    - core/once

Product View Tracking

View Item Event

File: commerce_ga4.module

<?php

use Drupal\commerce_product\Entity\ProductInterface;
use Drupal\commerce_product\Entity\ProductVariationInterface;

/**
 * Implements hook_page_attachments().
 */
function commerce_ga4_page_attachments(array &$attachments) {
  $route_match = \Drupal::routeMatch();

  // Track product page views
  if ($route_match->getRouteName() === 'entity.commerce_product.canonical') {
    /** @var \Drupal\commerce_product\Entity\ProductInterface $product */
    $product = $route_match->getParameter('commerce_product');

    if ($product instanceof ProductInterface) {
      $default_variation = $product->getDefaultVariation();

      if ($default_variation) {
        $item_data = _commerce_ga4_format_item($default_variation, $product);

        $attachments['#attached']['drupalSettings']['commerceGa4']['viewItem'] = [
          'currency' => $default_variation->getPrice()->getCurrencyCode(),
          'value' => (float) $default_variation->getPrice()->getNumber(),
          'items' => [$item_data],
        ];
        $attachments['#attached']['library'][] = 'commerce_ga4/ecommerce';
      }
    }
  }
}

/**
 * Format product variation as GA4 item.
 */
function _commerce_ga4_format_item(ProductVariationInterface $variation, ProductInterface $product = null, $quantity = 1) {
  if (!$product) {
    $product = $variation->getProduct();
  }

  $price = $variation->getPrice();

  // Get category from taxonomy
  $category = '';
  if ($product->hasField('field_category') && !$product->get('field_category')->isEmpty()) {
    $category = $product->get('field_category')->entity->label();
  }

  return [
    'item_id' => $variation->getSku(),
    'item_name' => $product->label(),
    'item_variant' => $variation->label(),
    'price' => (float) $price->getNumber(),
    'currency' => $price->getCurrencyCode(),
    'quantity' => $quantity,
    'item_category' => $category,
    'item_brand' => _commerce_ga4_get_brand($product),
  ];
}

/**
 * Get product brand.
 */
function _commerce_ga4_get_brand(ProductInterface $product) {
  if ($product->hasField('field_brand') && !$product->get('field_brand')->isEmpty()) {
    return $product->get('field_brand')->entity->label();
  }
  return '';
}

File: js/commerce-ga4.js

(function (Drupal, drupalSettings, once) {
  'use strict';

  Drupal.behaviors.commerceGa4ViewItem = {
    attach: function (context, settings) {
      if (settings.commerceGa4 && settings.commerceGa4.viewItem) {
        var data = settings.commerceGa4.viewItem;

        gtag('event', 'view_item', {
          currency: data.currency,
          value: data.value,
          items: data.items
        });

        // Clear to prevent double firing
        delete drupalSettings.commerceGa4.viewItem;
      }
    }
  };

})(Drupal, drupalSettings, once);

Add to Cart Tracking

Using Commerce Cart Events

<?php

use Drupal\commerce_cart\Event\CartEvents;
use Drupal\commerce_cart\Event\CartEntityAddEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
 * Commerce GA4 Event Subscriber.
 */
class CommerceGa4EventSubscriber implements EventSubscriberInterface {

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents() {
    return [
      CartEvents::CART_ENTITY_ADD => ['onCartEntityAdd', -100],
      CartEvents::CART_ORDER_ITEM_UPDATE => ['onCartItemUpdate', -100],
      CartEvents::CART_ORDER_ITEM_REMOVE => ['onCartItemRemove', -100],
    ];
  }

  /**
   * Tracks when an item is added to cart.
   */
  public function onCartEntityAdd(CartEntityAddEvent $event) {
    $order_item = $event->getOrderItem();
    $variation = $order_item->getPurchasedEntity();
    $product = $variation->getProduct();
    $quantity = (float) $order_item->getQuantity();

    $item_data = _commerce_ga4_format_item($variation, $product, $quantity);

    // Store in session for JavaScript to pick up
    $_SESSION['ga4_events'][] = [
      'event' => 'add_to_cart',
      'parameters' => [
        'currency' => $variation->getPrice()->getCurrencyCode(),
        'value' => (float) $variation->getPrice()->getNumber() * $quantity,
        'items' => [$item_data],
      ],
    ];
  }

  /**
   * Tracks when cart item quantity is updated.
   */
  public function onCartItemUpdate(CartOrderItemUpdateEvent $event) {
    $order_item = $event->getOrderItem();
    $original_item = $event->getOriginalOrderItem();

    $new_quantity = (float) $order_item->getQuantity();
    $old_quantity = (float) $original_item->getQuantity();

    if ($new_quantity > $old_quantity) {
      // Item quantity increased
      $this->trackAddToCart($order_item, $new_quantity - $old_quantity);
    } elseif ($new_quantity < $old_quantity) {
      // Item quantity decreased
      $this->trackRemoveFromCart($order_item, $old_quantity - $new_quantity);
    }
  }

  /**
   * Tracks when an item is removed from cart.
   */
  public function onCartItemRemove(CartOrderItemRemoveEvent $event) {
    $order_item = $event->getOrderItem();
    $this->trackRemoveFromCart($order_item, $order_item->getQuantity());
  }

  /**
   * Helper to track remove from cart.
   */
  protected function trackRemoveFromCart($order_item, $quantity) {
    $variation = $order_item->getPurchasedEntity();
    $product = $variation->getProduct();

    $item_data = _commerce_ga4_format_item($variation, $product, $quantity);

    $_SESSION['ga4_events'][] = [
      'event' => 'remove_from_cart',
      'parameters' => [
        'currency' => $variation->getPrice()->getCurrencyCode(),
        'value' => (float) $variation->getPrice()->getNumber() * $quantity,
        'items' => [$item_data],
      ],
    ];
  }
}

Register the service:

File: commerce_ga4.services.yml

services:
  commerce_ga4.event_subscriber:
    class: Drupal\commerce_ga4\EventSubscriber\CommerceGa4EventSubscriber
    tags:
      - { name: event_subscriber }

View Cart Tracking

<?php

/**
 * Implements hook_page_attachments().
 */
function commerce_ga4_page_attachments(array &$attachments) {
  $route_match = \Drupal::routeMatch();

  // Track cart page view
  if ($route_match->getRouteName() === 'commerce_cart.page') {
    /** @var \Drupal\commerce_cart\CartProviderInterface $cart_provider */
    $cart_provider = \Drupal::service('commerce_cart.cart_provider');
    $carts = $cart_provider->getCarts();

    if (!empty($carts)) {
      $cart = reset($carts);
      $items = [];
      $total_value = 0;
      $currency = null;

      foreach ($cart->getItems() as $order_item) {
        $variation = $order_item->getPurchasedEntity();
        $product = $variation->getProduct();

        $items[] = _commerce_ga4_format_item(
          $variation,
          $product,
          (float) $order_item->getQuantity()
        );

        $total_value += (float) $order_item->getTotalPrice()->getNumber();
        $currency = $order_item->getTotalPrice()->getCurrencyCode();
      }

      $attachments['#attached']['drupalSettings']['commerceGa4']['viewCart'] = [
        'currency' => $currency,
        'value' => $total_value,
        'items' => $items,
      ];
      $attachments['#attached']['library'][] = 'commerce_ga4/ecommerce';
    }
  }
}

JavaScript:

Drupal.behaviors.commerceGa4ViewCart = {
  attach: function (context, settings) {
    if (settings.commerceGa4 && settings.commerceGa4.viewCart) {
      var data = settings.commerceGa4.viewCart;

      gtag('event', 'view_cart', {
        currency: data.currency,
        value: data.value,
        items: data.items
      });

      delete drupalSettings.commerceGa4.viewCart;
    }
  }
};

Checkout Process Tracking

Begin Checkout

<?php

use Drupal\commerce_checkout\Event\CheckoutEvents;
use Drupal\commerce_checkout\Event\CheckoutCompletionRegisterEvent;

/**
 * Track checkout step.
 */
public function onCheckoutProgress(CheckoutEvent $event) {
  $order = $event->getOrder();
  $step_id = $event->getStepId();

  // Track begin_checkout on first step
  if ($step_id === 'login' || $step_id === 'order_information') {
    $items = [];
    $total_value = 0;
    $currency = null;

    foreach ($order->getItems() as $order_item) {
      $variation = $order_item->getPurchasedEntity();
      $product = $variation->getProduct();

      $items[] = _commerce_ga4_format_item(
        $variation,
        $product,
        (float) $order_item->getQuantity()
      );

      $total_value += (float) $order_item->getTotalPrice()->getNumber();
      $currency = $order_item->getTotalPrice()->getCurrencyCode();
    }

    $_SESSION['ga4_events'][] = [
      'event' => 'begin_checkout',
      'parameters' => [
        'currency' => $currency,
        'value' => $total_value,
        'items' => $items,
        'coupon' => $this->getCouponCode($order),
      ],
    ];
  }

  // Track checkout progress
  $_SESSION['ga4_events'][] = [
    'event' => 'checkout_progress',
    'parameters' => [
      'checkout_step' => $step_id,
      'checkout_option' => $this->getCheckoutStepName($step_id),
    ],
  ];
}

/**
 * Get coupon code from order.
 */
protected function getCouponCode($order) {
  if ($order->hasField('coupons') && !$order->get('coupons')->isEmpty()) {
    $coupons = [];
    foreach ($order->get('coupons')->referencedEntities() as $coupon) {
      $coupons[] = $coupon->getCode();
    }
    return implode(',', $coupons);
  }
  return '';
}

/**
 * Get human-readable step name.
 */
protected function getCheckoutStepName($step_id) {
  $steps = [
    'login' => 'Login/Guest',
    'order_information' => 'Order Information',
    'review' => 'Review Order',
    'payment' => 'Payment',
  ];
  return $steps[$step_id] ?? $step_id;
}

Add Shipping Info

<?php

public function onCheckoutShippingInfo(CheckoutEvent $event) {
  $order = $event->getOrder();

  if ($order->hasField('shipments') && !$order->get('shipments')->isEmpty()) {
    $shipment = $order->get('shipments')->first()->entity;
    $shipping_method = $shipment->getShippingMethod();

    $_SESSION['ga4_events'][] = [
      'event' => 'add_shipping_info',
      'parameters' => [
        'currency' => $order->getTotalPrice()->getCurrencyCode(),
        'value' => (float) $order->getTotalPrice()->getNumber(),
        'shipping_tier' => $shipping_method ? $shipping_method->label() : 'unknown',
        'items' => $this->getOrderItems($order),
      ],
    ];
  }
}

Add Payment Info

<?php

public function onCheckoutPaymentInfo(CheckoutEvent $event) {
  $order = $event->getOrder();
  $payment_gateway = null;

  if ($order->hasField('payment_gateway') && !$order->get('payment_gateway')->isEmpty()) {
    $payment_gateway = $order->get('payment_gateway')->entity->label();
  }

  $_SESSION['ga4_events'][] = [
    'event' => 'add_payment_info',
    'parameters' => [
      'currency' => $order->getTotalPrice()->getCurrencyCode(),
      'value' => (float) $order->getTotalPrice()->getNumber(),
      'payment_type' => $payment_gateway,
      'items' => $this->getOrderItems($order),
    ],
  ];
}

Purchase Tracking

Track Completed Orders

<?php

use Drupal\state_machine\Event\WorkflowTransitionEvent;
use Drupal\commerce_order\Entity\OrderInterface;

/**
 * Subscribe to order completion.
 */
public static function getSubscribedEvents() {
  return [
    'commerce_order.place.post_transition' => ['onOrderPlace'],
  ];
}

/**
 * Track purchase when order is placed.
 */
public function onOrderPlace(WorkflowTransitionEvent $event) {
  /** @var \Drupal\commerce_order\Entity\OrderInterface $order */
  $order = $event->getEntity();

  // Prevent duplicate tracking
  if ($order->getData('ga4_tracked')) {
    return;
  }

  $items = [];
  foreach ($order->getItems() as $order_item) {
    $variation = $order_item->getPurchasedEntity();
    $product = $variation->getProduct();

    $items[] = _commerce_ga4_format_item(
      $variation,
      $product,
      (float) $order_item->getQuantity()
    );
  }

  // Calculate tax and shipping
  $tax_amount = 0;
  if ($order->hasField('tax_adjustments')) {
    foreach ($order->getAdjustments(['tax']) as $adjustment) {
      $tax_amount += (float) $adjustment->getAmount()->getNumber();
    }
  }

  $shipping_amount = 0;
  if ($order->hasField('shipments')) {
    foreach ($order->getAdjustments(['shipping']) as $adjustment) {
      $shipping_amount += (float) $adjustment->getAmount()->getNumber();
    }
  }

  $_SESSION['ga4_events'][] = [
    'event' => 'purchase',
    'parameters' => [
      'transaction_id' => $order->getOrderNumber(),
      'affiliation' => \Drupal::config('system.site')->get('name'),
      'value' => (float) $order->getTotalPrice()->getNumber(),
      'currency' => $order->getTotalPrice()->getCurrencyCode(),
      'tax' => $tax_amount,
      'shipping' => $shipping_amount,
      'coupon' => $this->getCouponCode($order),
      'items' => $items,
    ],
  ];

  // Mark as tracked
  $order->setData('ga4_tracked', TRUE);
  $order->save();
}

Refund Tracking

<?php

/**
 * Track order refunds.
 */
public function onOrderRefund(WorkflowTransitionEvent $event) {
  /** @var \Drupal\commerce_order\Entity\OrderInterface $order */
  $order = $event->getEntity();

  $items = [];
  foreach ($order->getItems() as $order_item) {
    $variation = $order_item->getPurchasedEntity();
    $product = $variation->getProduct();

    $items[] = _commerce_ga4_format_item(
      $variation,
      $product,
      (float) $order_item->getQuantity()
    );
  }

  $_SESSION['ga4_events'][] = [
    'event' => 'refund',
    'parameters' => [
      'transaction_id' => $order->getOrderNumber(),
      'value' => (float) $order->getTotalPrice()->getNumber(),
      'currency' => $order->getTotalPrice()->getCurrencyCode(),
      'items' => $items,
    ],
  ];
}

Product List Tracking (Views)

View Item List

(function (Drupal, once) {
  'use strict';

  Drupal.behaviors.commerceGa4ProductList = {
    attach: function (context, settings) {
      // Track product listings in Views
      once('commerce-product-list', '.view-commerce-products .views-row', context).forEach(function(row, index) {
        var product = row.querySelector('.product-title a, .commerce-product--title a');
        if (!product) return;

        var productData = {
          item_id: row.getAttribute('data-product-sku') || 'unknown',
          item_name: product.textContent.trim(),
          index: index,
          item_list_name: document.querySelector('.view-commerce-products')?.getAttribute('data-view-name') || 'Product Listing',
        };

        // Track when product becomes visible
        var observer = new IntersectionObserver(function(entries) {
          entries.forEach(function(entry) {
            if (entry.isIntersecting) {
              gtag('event', 'view_item_list', {
                items: [productData]
              });
              observer.disconnect();
            }
          });
        }, { threshold: 0.5 });

        observer.observe(row);

        // Track clicks
        product.addEventListener('click', function() {
          gtag('event', 'select_item', {
            item_list_name: productData.item_list_name,
            items: [productData]
          });
        });
      });
    }
  };

})(Drupal, once);

Product Search Tracking

Drupal.behaviors.commerceGa4Search = {
  attach: function (context, settings) {
    once('commerce-search', 'form.views-exposed-form.commerce-product-search', context).forEach(function(form) {
      form.addEventListener('submit', function() {
        var searchInput = form.querySelector('input[name="search"]');
        if (searchInput && searchInput.value) {
          gtag('event', 'search', {
            search_term: searchInput.value,
            event_category: 'ecommerce'
          });
        }
      });
    });
  }
};

Testing E-commerce Tracking

1. Enable GA4 Debug Mode

gtag('config', 'G-XXXXXXXXXX', {
  'debug_mode': true
});

2. Test Purchase Flow

  1. Add products to cart
  2. Proceed to checkout
  3. Complete purchase
  4. Check GA4 DebugView for events:
    • view_item
    • add_to_cart
    • view_cart
    • begin_checkout
    • add_shipping_info
    • add_payment_info
    • purchase

3. Verify E-commerce Reports

GA4 → Reports → Monetization

  • Overview
  • E-commerce purchases
  • Item purchases
  • Item promotions

Common Issues

Events Not Firing After Cache Clear

// Ensure events survive cache rebuilds
$attachments['#cache']['contexts'][] = 'session';
$attachments['#cache']['max-age'] = 0;

Duplicate Purchase Events

// Use order data to track
if ($order->getData('ga4_tracked')) {
  return; // Already tracked
}

Missing Product Data

Ensure products have required fields:

  • SKU (variation.sku)
  • Title (product.title)
  • Price (variation.price)

Resources


Next Steps

// SYS.FOOTER