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';
}
}
}
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
- Add products to cart
- Proceed to checkout
- Complete purchase
- Check GA4 DebugView for events:
view_itemadd_to_cartview_cartbegin_checkoutadd_shipping_infoadd_payment_infopurchase
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)