Craft Commerce + GA4 E-commerce Tracking | OpsBlu Docs

Craft Commerce + GA4 E-commerce Tracking

Implement complete GA4 e-commerce tracking for Craft Commerce including product views, add to cart, checkout, and purchase events.

Complete guide to implementing Google Analytics 4 e-commerce tracking with Craft Commerce, covering product impressions, cart events, checkout flow, and purchase tracking.

Prerequisites

  • Craft Commerce 4.x or 5.x installed
  • GA4 property with e-commerce measurement enabled
  • Basic understanding of Craft Commerce order flow
  • GA4 measurement ID configured in templates

E-commerce Data Structure

Product Data Helper

Create a reusable Twig macro for product data:

{# templates/_macros/ga4-ecommerce.twig #}

{% macro productData(product, variant, quantity = 1) %}
  {%- set data = {
    item_id: variant.sku ?? variant.id,
    item_name: product.title,
    item_variant: variant.title != product.title ? variant.title : null,
    price: variant.price|number_format(2, '.', ''),
    quantity: quantity,
    item_brand: product.brand ?? siteName,
    item_category: product.productCategory.first().title ?? 'Uncategorized',
  } -%}
  {{ data|json_encode|raw }}
{% endmacro %}

Product List/Category Pages

View Item List Event

Track product impressions on category pages:

{# templates/shop/category.twig #}

{% import '_macros/ga4-ecommerce' as ga4 %}

{% set products = craft.products()
  .relatedTo(category)
  .all() %}

<script>
  gtag('event', 'view_item_list', {
    'item_list_id': '{{ category.id }}',
    'item_list_name': '{{ category.title|e('js') }}',
    'items': [
      {% for product in products %}
        {% set variant = product.defaultVariant %}
        {{ ga4.productData(product, variant) }}{{ not loop.last ? ',' : '' }}
      {% endfor %}
    ]
  });
</script>

{# Track individual product clicks #}
{% for product in products %}
  {% set variant = product.defaultVariant %}
  <a href="{{ product.url }}"
     class="product-link" 'select_item', {
       'item_list_id': '{{ category.id }}',
       'item_list_name': '{{ category.title|e('js') }}',
       'items': [{{ ga4.productData(product, variant) }}]
     });">
    <h3>{{ product.title }}</h3>
    <p>{{ variant.price|currency(cart.currency) }}</p>
  </a>
{% endfor %}

Product Detail Page

View Item Event

Track when users view product details:

{# templates/shop/product.twig #}

{% import '_macros/ga4-ecommerce' as ga4 %}

{% set variant = product.defaultVariant %}

<script>
  gtag('event', 'view_item', {
    'currency': '{{ cart.currency }}',
    'value': {{ variant.price|number_format(2, '.', '') }},
    'items': [{{ ga4.productData(product, variant) }}]
  });
</script>

{# Product details #}
<h1>{{ product.title }}</h1>
<p>{{ variant.price|currency(cart.currency) }}</p>

{# Variant selector with tracking #}
{% if product.variants|length > 1 %}
  <select id="variant-select"
    {% for variant in product.variants.all() %}
      <option value="{{ variant.id }}" data-variant='{{ ga4.productData(product, variant) }}'>
        {{ variant.title }} - {{ variant.price|currency(cart.currency) }}
      </option>
    {% endfor %}
  </select>

  <script>
    function trackVariantChange(variantId) {
      var select = document.getElementById('variant-select');
      var option = select.options[select.selectedIndex];
      var variantData = JSON.parse(option.dataset.variant);

      gtag('event', 'view_item', {
        'currency': '{{ cart.currency }}',
        'value': variantData.price,
        'items': [variantData]
      });
    }
  </script>
{% endif %}

Add to Cart Tracking

Add to Cart Event

Track when products are added to cart:

{# templates/shop/product.twig - Add to Cart Form #}

<form method="post" id="add-to-cart-form">
  {{ csrfInput() }}
  {{ actionInput('commerce/cart/update-cart') }}
  {{ redirectInput('shop/cart') }}

  <input type="hidden" name="purchasableId" value="{{ variant.id }}">

  <input type="number"
         name="qty"
         value="1"
         min="1"
         id="quantity">

  <button type="submit">Add to Cart</button>
</form>

<script>
  document.getElementById('add-to-cart-form').addEventListener('submit', function(e) {
    var quantity = parseInt(document.getElementById('quantity').value);
    var variant = {{ ga4.productData(product, variant, '${quantity}') }};

    gtag('event', 'add_to_cart', {
      'currency': '{{ cart.currency }}',
      'value': {{ variant.price|number_format(2, '.', '') }} * quantity,
      'items': [
        {
          ...JSON.parse('{{ ga4.productData(product, variant) }}'),
          quantity: quantity
        }
      ]
    });
  });
</script>

AJAX Add to Cart

For AJAX-based cart additions:

{# templates/shop/_includes/ajax-add-to-cart.twig #}

<button type="button"
        class="ajax-add-to-cart"
        data-variant-id="{{ variant.id }}"
        data-product='{{ ga4.productData(product, variant) }}'>
  Add to Cart
</button>

<script>
  document.querySelectorAll('.ajax-add-to-cart').forEach(function(button) {
    button.addEventListener('click', function() {
      var variantId = this.dataset.variantId;
      var productData = JSON.parse(this.dataset.product);
      var quantity = 1;

      // AJAX request to add to cart
      fetch('/actions/commerce/cart/update-cart', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
          'X-CSRF-Token': '{{ craft.app.request.csrfToken }}'
        },
        body: new URLSearchParams({
          'purchasableId': variantId,
          'qty': quantity
        })
      })
      .then(response => response.json())
      .then(data => {
        if (data.success) {
          // Track add to cart
          gtag('event', 'add_to_cart', {
            'currency': '{{ cart.currency }}',
            'value': parseFloat(productData.price) * quantity,
            'items': [productData]
          });
        }
      });
    });
  });
</script>

Cart Page Tracking

View Cart Event

Track cart views and removals:

{# templates/shop/cart.twig #}

{% import '_macros/ga4-ecommerce' as ga4 %}

{% set cart = craft.commerce.carts.cart %}

<script>
  // Track cart view
  gtag('event', 'view_cart', {
    'currency': '{{ cart.currency }}',
    'value': {{ cart.total|number_format(2, '.', '') }},
    'items': [
      {% for item in cart.lineItems %}
        {% set product = item.purchasable.product %}
        {{ ga4.productData(product, item.purchasable, item.qty) }}{{ not loop.last ? ',' : '' }}
      {% endfor %}
    ]
  });
</script>

{# Cart items with remove tracking #}
{% for item in cart.lineItems %}
  <div class="cart-item">
    <h3>{{ item.description }}</h3>
    <p>{{ item.price|currency(cart.currency) }} x {{ item.qty }}</p>

    <form method="post" {{ item.id }})">
      {{ csrfInput() }}
      {{ actionInput('commerce/cart/update-cart') }}

      <input type="hidden" name="lineItems[{{ item.id }}][remove]" value="1">
      <button type="submit">Remove</button>
    </form>
  </div>
{% endfor %}

<script>
  function trackRemoveFromCart(event, lineItemId) {
    var cartItems = {
      {% for item in cart.lineItems %}
        '{{ item.id }}': {{ ga4.productData(item.purchasable.product, item.purchasable, item.qty) }}{{ not loop.last ? ',' : '' }}
      {% endfor %}
    };

    var item = cartItems[lineItemId];

    gtag('event', 'remove_from_cart', {
      'currency': '{{ cart.currency }}',
      'value': parseFloat(item.price) * item.quantity,
      'items': [item]
    });
  }
</script>

Checkout Flow Tracking

Begin Checkout Event

Track when users start checkout:

{# templates/shop/checkout/index.twig #}

{% import '_macros/ga4-ecommerce' as ga4 %}

{% set cart = craft.commerce.carts.cart %}

<script>
  gtag('event', 'begin_checkout', {
    'currency': '{{ cart.currency }}',
    'value': {{ cart.total|number_format(2, '.', '') }},
    'coupon': '{{ cart.couponCode ?? '' }}',
    'items': [
      {% for item in cart.lineItems %}
        {{ ga4.productData(item.purchasable.product, item.purchasable, item.qty) }}{{ not loop.last ? ',' : '' }}
      {% endfor %}
    ]
  });
</script>

Add Shipping Info Event

Track shipping information step:

{# templates/shop/checkout/shipping.twig #}

<form method="post" id="shipping-form">
  {{ csrfInput() }}
  {{ actionInput('commerce/cart/update-cart') }}

  {# Shipping address fields #}
  <input type="text" name="shippingAddress[firstName]" required>
  <input type="text" name="shippingAddress[lastName]" required>
  {# ... more fields ... #}

  <button type="submit">Continue to Payment</button>
</form>

<script>
  document.getElementById('shipping-form').addEventListener('submit', function(e) {
    gtag('event', 'add_shipping_info', {
      'currency': '{{ cart.currency }}',
      'value': {{ cart.total|number_format(2, '.', '') }},
      'shipping_tier': '{{ cart.shippingMethodHandle ?? 'standard' }}',
      'items': [
        {% for item in cart.lineItems %}
          {{ ga4.productData(item.purchasable.product, item.purchasable, item.qty) }}{{ not loop.last ? ',' : '' }}
        {% endfor %}
      ]
    });
  });
</script>

Add Payment Info Event

Track payment method selection:

{# templates/shop/checkout/payment.twig #}

<form method="post" id="payment-form">
  {{ csrfInput() }}

  <select name="gatewayId"
    {% for gateway in craft.commerce.gateways.allCustomerEnabledGateways %}
      <option value="{{ gateway.id }}">{{ gateway.name }}</option>
    {% endfor %}
  </select>

  <button type="submit">Complete Order</button>
</form>

<script>
  function trackPaymentInfo(gatewayId) {
    var gateways = {
      {% for gateway in craft.commerce.gateways.allCustomerEnabledGateways %}
        '{{ gateway.id }}': '{{ gateway.name }}'{{ not loop.last ? ',' : '' }}
      {% endfor %}
    };

    gtag('event', 'add_payment_info', {
      'currency': '{{ cart.currency }}',
      'value': {{ cart.total|number_format(2, '.', '') }},
      'payment_type': gateways[gatewayId],
      'items': [
        {% for item in cart.lineItems %}
          {{ ga4.productData(item.purchasable.product, item.purchasable, item.qty) }}{{ not loop.last ? ',' : '' }}
        {% endfor %}
      ]
    });
  }
</script>

Purchase Tracking

Purchase Event (Order Confirmation)

Track completed purchases on the order confirmation page:

{# templates/shop/checkout/complete.twig #}

{% import '_macros/ga4-ecommerce' as ga4 %}

{% set order = craft.commerce.orders.number(craft.app.request.getParam('number')).one() %}

{% if order %}
<script>
  gtag('event', 'purchase', {
    'transaction_id': '{{ order.number }}',
    'value': {{ order.total|number_format(2, '.', '') }},
    'tax': {{ order.totalTax|number_format(2, '.', '') }},
    'shipping': {{ order.totalShippingCost|number_format(2, '.', '') }},
    'currency': '{{ order.currency }}',
    'coupon': '{{ order.couponCode ?? '' }}',
    'items': [
      {% for item in order.lineItems %}
        {% set product = item.purchasable.product %}
        {
          'item_id': '{{ item.purchasable.sku ?? item.purchasable.id }}',
          'item_name': '{{ product.title|e('js') }}',
          'item_variant': '{{ item.purchasable.title|e('js') }}',
          'price': {{ item.price|number_format(2, '.', '') }},
          'quantity': {{ item.qty }},
          'item_brand': '{{ product.brand ?? siteName }}',
          'item_category': '{{ product.productCategory.first().title ?? 'Uncategorized' }}',
          'discount': {{ item.discount|number_format(2, '.', '') }}
        }{{ not loop.last ? ',' : '' }}
      {% endfor %}
    ]
  });
</script>

{# Display order confirmation #}
<h1>Order Complete!</h1>
<p>Order Number: {{ order.number }}</p>
<p>Total: {{ order.totalPrice|currency(order.currency) }}</p>
{% endif %}

For more reliable purchase tracking, use Craft Commerce events:

<?php
// modules/analytics/Module.php

namespace modules\analytics;

use Craft;
use craft\commerce\elements\Order;
use craft\commerce\events\OrderStatusEvent;
use craft\commerce\services\OrderStatuses;
use yii\base\Event;
use yii\base\Module as BaseModule;

class Module extends BaseModule
{
    public function init()
    {
        parent::init();

        // Track when order is completed
        Event::on(
            Order::class,
            Order::EVENT_AFTER_COMPLETE_ORDER,
            [$this, 'handleOrderComplete']
        );
    }

    public function handleOrderComplete(Event $event)
    {
        /** @var Order $order */
        $order = $event->sender;

        // Store order data in session for client-side tracking
        Craft::$app->session->set('ga4_purchase_data', [
            'transaction_id' => $order->number,
            'value' => $order->total,
            'tax' => $order->totalTax,
            'shipping' => $order->totalShippingCost,
            'currency' => $order->currency,
            'coupon' => $order->couponCode,
            'items' => $this->getOrderItems($order)
        ]);
    }

    private function getOrderItems(Order $order): array
    {
        $items = [];

        foreach ($order->lineItems as $item) {
            $product = $item->purchasable->product ?? null;

            $items[] = [
                'item_id' => $item->purchasable->sku ?? $item->purchasable->id,
                'item_name' => $product->title ?? $item->description,
                'item_variant' => $item->purchasable->title,
                'price' => $item->price,
                'quantity' => $item->qty,
                'item_brand' => $product->brand ?? Craft::$app->systemName,
                'item_category' => $product->productCategory->first()->title ?? 'Uncategorized',
                'discount' => $item->discount
            ];
        }

        return $items;
    }
}

Retrieve in template:

{# templates/shop/checkout/complete.twig #}

{% set purchaseData = craft.app.session.get('ga4_purchase_data') %}

{% if purchaseData %}
<script>
  gtag('event', 'purchase', {{ purchaseData|json_encode|raw }});
</script>

{# Clear the session data #}
{% do craft.app.session.remove('ga4_purchase_data') %}
{% endif %}

Refund Tracking

Track refunds via admin actions or customer returns:

{# For customer-initiated refund requests #}

<script>
  gtag('event', 'refund', {
    'transaction_id': '{{ order.number }}',
    'value': {{ refundAmount|number_format(2, '.', '') }},
    'currency': '{{ order.currency }}',
    'items': [
      {# Include items being refunded #}
    ]
  });
</script>

Testing E-commerce Tracking

Debug Mode

Enable debug mode in development:

{% if craft.app.config.general.devMode %}
<script>
  // Enable GA4 debug mode
  gtag('config', '{{ analyticsId }}', {
    'debug_mode': true
  });

  // Log all e-commerce events
  console.log('E-commerce tracking enabled in debug mode');
</script>
{% endif %}

Verify in GA4

  1. DebugView: Use GA4 DebugView for real-time validation
  2. E-commerce Purchases Report: Check Monetization > E-commerce purchases
  3. Event Count: Verify events in Reports > Events

Best Practices

1. Prevent Duplicate Tracking

Use session flags to prevent duplicate purchase events:

{% set orderTracked = craft.app.session.get('order_' ~ order.number ~ '_tracked') %}

{% if not orderTracked %}
  {# Track purchase event #}
  {% do craft.app.session.set('order_' ~ order.number ~ '_tracked', true) %}
{% endif %}

2. Currency Consistency

Always use the cart/order currency:

'currency': '{{ cart.currency }}',  {# Not hardcoded #}

3. SKU vs ID

Prefer SKU over ID for item_id:

'item_id': '{{ variant.sku ?? variant.id }}',

Next Steps

Resources