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 %}
Server-Side Purchase Tracking (Recommended)
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
- DebugView: Use GA4 DebugView for real-time validation
- E-commerce Purchases Report: Check Monetization > E-commerce purchases
- 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
- GTM Setup - Use GTM for advanced e-commerce tracking
- Event Tracking - Track additional user interactions
- Performance Optimization - Optimize tracking performance