PrestaShop GA4 Ecommerce Tracking
Complete guide to implementing GA4 Enhanced Ecommerce tracking on PrestaShop. Track the entire customer journey from product discovery to purchase completion.
Ecommerce Events Overview
GA4 ecommerce tracking uses these key events:
| Event | Description | PrestaShop Context |
|---|---|---|
view_item_list |
Products displayed in list | Category pages, search results, related products |
select_item |
Product clicked in list | Product link clicked |
view_item |
Product details viewed | Product page |
add_to_cart |
Item added to cart | Add to cart action |
remove_from_cart |
Item removed from cart | Cart removal |
view_cart |
Shopping cart viewed | Cart page |
begin_checkout |
Checkout started | First checkout step |
add_shipping_info |
Shipping selected | Shipping method chosen |
add_payment_info |
Payment selected | Payment method chosen |
purchase |
Transaction completed | Order confirmation |
Product Data Structure
Standard GA4 Item Parameters
All ecommerce events use this product data structure:
{
item_id: "12345", // Required: Product ID
item_name: "Product Name", // Required: Product name
affiliation: "PrestaShop Store", // Optional: Store name
coupon: "SUMMER2024", // Optional: Applied coupon
discount: 5.00, // Optional: Discount amount
index: 0, // Optional: Position in list
item_brand: "Brand Name", // Optional: Manufacturer
item_category: "Category", // Optional: Main category
item_category2: "Subcategory", // Optional: Sub-category
item_category3: "", // Optional: Further subcategory
item_list_id: "related_products", // Optional: List identifier
item_list_name: "Related Products", // Optional: List name
item_variant: "Red-XL", // Optional: Combination/variant
location_id: "1", // Optional: Shop/warehouse ID
price: 29.99, // Optional: Product price
quantity: 1 // Optional: Quantity
}
PrestaShop-Specific Product Data Helper
Create a PHP helper function in your module:
<?php
// Format PrestaShop product for GA4
private function formatProductForGA4($product, $context = 'view')
{
// Get product object if array passed
if (is_array($product)) {
$id_product = isset($product['id_product']) ? (int)$product['id_product'] : 0;
$productObj = new Product($id_product, true, $this->context->language->id);
} else {
$productObj = $product;
$id_product = $productObj->id;
}
// Get category path
$category = new Category($productObj->id_category_default, $this->context->language->id);
$categories = $this->getCategoryPath($category);
// Get manufacturer/brand
$brand = '';
if ($productObj->id_manufacturer > 0) {
$manufacturer = new Manufacturer($productObj->id_manufacturer, $this->context->language->id);
$brand = $manufacturer->name;
}
// Get combination/variant if applicable
$variant = '';
if (isset($product['id_product_attribute']) && $product['id_product_attribute'] > 0) {
$combination = new Combination($product['id_product_attribute']);
$variant = $this->getCombinationName($combination);
}
// Get discount if applicable
$discount = 0;
$price = $productObj->getPrice(true, null, 2);
$price_without_reduction = $productObj->getPriceWithoutReduct(true);
if ($price_without_reduction > $price) {
$discount = $price_without_reduction - $price;
}
return array(
'item_id' => (string)$id_product,
'item_name' => $productObj->name,
'item_brand' => $brand,
'item_category' => isset($categories[0]) ? $categories[0] : '',
'item_category2' => isset($categories[1]) ? $categories[1] : '',
'item_category3' => isset($categories[2]) ? $categories[2] : '',
'item_variant' => $variant,
'price' => $price,
'discount' => $discount,
'quantity' => isset($product['quantity']) ? (int)$product['quantity'] : 1,
'affiliation' => $this->context->shop->name,
'location_id' => (string)$this->context->shop->id
);
}
// Get category hierarchy
private function getCategoryPath($category)
{
$categories = array();
$current = $category;
while ($current->id > 2) { // Stop at root (id 2)
$categories[] = $current->name;
$current = new Category($current->id_parent, $this->context->language->id);
}
return array_reverse($categories);
}
// Get combination/variant name
private function getCombinationName($combination)
{
$attributes = $combination->getAttributesName($this->context->language->id);
$variant_parts = array();
foreach ($attributes as $attribute) {
$variant_parts[] = $attribute['name'];
}
return implode('-', $variant_parts);
}
Implementing Ecommerce Events
1. Product Impressions (view_item_list)
Category Page Implementation:
// Hook: actionProductSearchAfter or displayFooterCategory
public function hookDisplayFooterCategory($params)
{
$category = $params['category'];
$products = $this->getDisplayedProducts($category);
$items = array();
$index = 0;
foreach ($products as $product) {
$item = $this->formatProductForGA4($product);
$item['index'] = $index;
$item['item_list_id'] = 'category_' . $category->id;
$item['item_list_name'] = $category->name;
$items[] = $item;
$index++;
}
$this->context->smarty->assign(array(
'ga4_items' => $items,
'ga4_list_name' => $category->name,
'ga4_list_id' => 'category_' . $category->id
));
return $this->display(__FILE__, 'views/templates/hook/view-item-list.tpl');
}
{* views/templates/hook/view-item-list.tpl *}
<script>
// Fire view_item_list event
gtag('event', 'view_item_list', {
item_list_id: '{$ga4_list_id|escape:'javascript':'UTF-8'}',
item_list_name: '{$ga4_list_name|escape:'javascript':'UTF-8'}',
items: [
{foreach from=$ga4_items item=item}
{
item_id: '{$item.item_id|escape:'javascript':'UTF-8'}',
item_name: '{$item.item_name|escape:'javascript':'UTF-8'}',
item_brand: '{$item.item_brand|escape:'javascript':'UTF-8'}',
item_category: '{$item.item_category|escape:'javascript':'UTF-8'}',
{if $item.item_category2}item_category2: '{$item.item_category2|escape:'javascript':'UTF-8'}',{/if}
price: {$item.price|floatval},
quantity: {$item.quantity|intval},
index: {$item.index|intval}
}{if !$item@last},{/if}
{/foreach}
]
});
</script>
2. Product Click (select_item)
JavaScript Implementation:
// Track product clicks in category/search listings
document.addEventListener('click', function(e) {
var productLink = e.target.closest('a.product-thumbnail, a.product-title, .product-miniature a');
if (productLink) {
var miniature = productLink.closest('.product-miniature');
if (!miniature) return;
var productId = miniature.dataset.idProduct;
var productName = miniature.querySelector('.product-title')?.textContent.trim();
var productPrice = miniature.querySelector('.product-price .price')?.textContent.replace(/[^0-9.]/g, '');
var listName = document.querySelector('.page-category-heading')?.textContent.trim() || 'product_list';
var index = Array.from(miniature.parentElement.children).indexOf(miniature);
if (typeof gtag !== 'undefined') {
gtag('event', 'select_item', {
item_list_name: listName,
items: [{
item_id: productId,
item_name: productName,
price: parseFloat(productPrice) || 0,
index: index
}]
});
}
}
}, true); // Use capture phase to ensure tracking before navigation
3. Product View (view_item)
Product Page Implementation:
// Hook: displayFooterProduct
public function hookDisplayFooterProduct($params)
{
$product = $params['product'];
// Get selected combination if any
$id_product_attribute = Tools::getValue('id_product_attribute', 0);
$product_data = array(
'id_product' => $product->id,
'id_product_attribute' => $id_product_attribute
);
$item = $this->formatProductForGA4($product_data);
$this->context->smarty->assign(array(
'ga4_item' => $item,
'ga4_currency' => $this->context->currency->iso_code,
'ga4_value' => $item['price']
));
return $this->display(__FILE__, 'views/templates/hook/view-item.tpl');
}
Template:
{* views/templates/hook/view-item.tpl *}
<script>
gtag('event', 'view_item', {
currency: '{$ga4_currency|escape:'javascript':'UTF-8'}',
value: {$ga4_value|floatval},
items: [{
item_id: '{$ga4_item.item_id|escape:'javascript':'UTF-8'}',
item_name: '{$ga4_item.item_name|escape:'javascript':'UTF-8'}',
item_brand: '{$ga4_item.item_brand|escape:'javascript':'UTF-8'}',
item_category: '{$ga4_item.item_category|escape:'javascript':'UTF-8'}',
{if $ga4_item.item_category2}item_category2: '{$ga4_item.item_category2|escape:'javascript':'UTF-8'}',{/if}
{if $ga4_item.item_variant}item_variant: '{$ga4_item.item_variant|escape:'javascript':'UTF-8'}',{/if}
price: {$ga4_item.price|floatval},
quantity: 1
}]
});
</script>
4. Add to Cart (add_to_cart)
AJAX Cart Addition:
// Hook: actionCartSave
public function hookActionCartSave($params)
{
if (!isset($params['cart'])) {
return;
}
$cart = $params['cart'];
$last_product = $cart->getLastProduct();
if (!$last_product) {
return;
}
$item = $this->formatProductForGA4($last_product);
// Store in cookie for JavaScript retrieval
$event_data = array(
'event' => 'add_to_cart',
'currency' => $this->context->currency->iso_code,
'value' => $item['price'] * $item['quantity'],
'items' => array($item)
);
// Use PrestaShop cookie to pass data to frontend
$this->context->cookie->__set('ga4_cart_event', json_encode($event_data));
}
JavaScript to Read Cookie and Fire Event:
// Check for cart event on page load
document.addEventListener('DOMContentLoaded', function() {
var ga4Event = getCookie('ga4_cart_event');
if (ga4Event && typeof gtag !== 'undefined') {
try {
var eventData = JSON.parse(ga4Event);
gtag('event', eventData.event, {
currency: eventData.currency,
value: eventData.value,
items: eventData.items
});
// Clear cookie after firing
document.cookie = 'ga4_cart_event=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
} catch (e) {
console.error('GA4 cart event error:', e);
}
}
});
function getCookie(name) {
var value = "; " + document.cookie;
var parts = value.split("; " + name + "=");
if (parts.length == 2) return parts.pop().split(";").shift();
}
5. Remove from Cart (remove_from_cart)
JavaScript Implementation:
// Track cart item removal
document.addEventListener('click', function(e) {
if (e.target.matches('.cart-item-remove, .remove-from-cart, [data-link-action="delete-from-cart"]')) {
var cartItem = e.target.closest('.cart-item, .product-line-grid');
if (!cartItem) return;
var productId = cartItem.dataset.idProduct;
var productName = cartItem.querySelector('.product-name, .label')?.textContent.trim();
var price = cartItem.querySelector('.product-price, .price')?.textContent.replace(/[^0-9.]/g, '');
var quantity = cartItem.querySelector('.qty input, .product-quantity')?.value || 1;
if (typeof gtag !== 'undefined') {
gtag('event', 'remove_from_cart', {
currency: prestashop.currency.iso_code,
value: parseFloat(price) * parseInt(quantity),
items: [{
item_id: productId,
item_name: productName,
price: parseFloat(price),
quantity: parseInt(quantity)
}]
});
}
}
});
6. Begin Checkout (begin_checkout)
Checkout Page Hook:
// Hook: displayShoppingCart or custom checkout controller override
public function hookDisplayBeforeCheckout($params)
{
$cart = $this->context->cart;
$products = $cart->getProducts(true);
$items = array();
foreach ($products as $product) {
$items[] = $this->formatProductForGA4($product);
}
$cart_total = $cart->getOrderTotal(true);
$this->context->smarty->assign(array(
'ga4_items' => $items,
'ga4_cart_value' => $cart_total,
'ga4_currency' => $this->context->currency->iso_code
));
return $this->display(__FILE__, 'views/templates/hook/begin-checkout.tpl');
}
Template:
{* views/templates/hook/begin-checkout.tpl *}
<script>
gtag('event', 'begin_checkout', {
currency: '{$ga4_currency|escape:'javascript':'UTF-8'}',
value: {$ga4_cart_value|floatval},
items: [
{foreach from=$ga4_items item=item}
{
item_id: '{$item.item_id|escape:'javascript':'UTF-8'}',
item_name: '{$item.item_name|escape:'javascript':'UTF-8'}',
item_brand: '{$item.item_brand|escape:'javascript':'UTF-8'}',
price: {$item.price|floatval},
quantity: {$item.quantity|intval}
}{if !$item@last},{/if}
{/foreach}
]
});
</script>
7. Add Shipping Info (add_shipping_info)
Checkout Step Tracking:
// Track shipping method selection
document.addEventListener('change', function(e) {
if (e.target.matches('input[name="delivery_option"], .delivery-option')) {
var shippingMethod = e.target.value;
var shippingCost = document.querySelector('.shipping-cost .value')?.textContent.replace(/[^0-9.]/g, '') || '0';
var cartItems = getCartItemsFromCheckout();
var cartValue = getCheckoutTotal();
if (typeof gtag !== 'undefined') {
gtag('event', 'add_shipping_info', {
currency: prestashop.currency.iso_code,
value: parseFloat(cartValue),
shipping_tier: shippingMethod,
items: cartItems
});
}
}
});
8. Add Payment Info (add_payment_info)
Payment Selection Tracking:
// Track payment method selection
document.addEventListener('change', function(e) {
if (e.target.matches('input[name="payment-option"], .payment-option')) {
var paymentMethod = e.target.dataset.moduleName || e.target.value;
var cartItems = getCartItemsFromCheckout();
var cartValue = getCheckoutTotal();
if (typeof gtag !== 'undefined') {
gtag('event', 'add_payment_info', {
currency: prestashop.currency.iso_code,
value: parseFloat(cartValue),
payment_type: paymentMethod,
items: cartItems
});
}
}
});
9. Purchase (purchase)
Order Confirmation Page - Most Critical Event:
// Hook: displayOrderConfirmation
public function hookDisplayOrderConfirmation($params)
{
$order = $params['order'];
// Get order products
$order_detail = $order->getOrderDetailList();
$items = array();
foreach ($order_detail as $product) {
$item = $this->formatProductForGA4($product);
$items[] = $item;
}
// Get applied cart rules (discounts/coupons)
$cart_rules = $order->getCartRules();
$coupon = '';
if (!empty($cart_rules)) {
$coupon_names = array();
foreach ($cart_rules as $rule) {
$coupon_names[] = $rule['name'];
}
$coupon = implode(',', $coupon_names);
}
// Get shipping and tax
$shipping = $order->total_shipping_tax_incl;
$tax = $order->total_paid_tax_incl - $order->total_paid_tax_excl;
$this->context->smarty->assign(array(
'ga4_transaction_id' => $order->reference,
'ga4_value' => $order->total_paid_tax_incl,
'ga4_tax' => $tax,
'ga4_shipping' => $shipping,
'ga4_currency' => $this->context->currency->iso_code,
'ga4_coupon' => $coupon,
'ga4_items' => $items,
'ga4_affiliation' => $this->context->shop->name
));
return $this->display(__FILE__, 'views/templates/hook/purchase.tpl');
}
Purchase Event Template:
{* views/templates/hook/purchase.tpl *}
<script>
gtag('event', 'purchase', {
transaction_id: '{$ga4_transaction_id|escape:'javascript':'UTF-8'}',
value: {$ga4_value|floatval},
tax: {$ga4_tax|floatval},
shipping: {$ga4_shipping|floatval},
currency: '{$ga4_currency|escape:'javascript':'UTF-8'}',
{if $ga4_coupon}coupon: '{$ga4_coupon|escape:'javascript':'UTF-8'}',{/if}
affiliation: '{$ga4_affiliation|escape:'javascript':'UTF-8'}',
items: [
{foreach from=$ga4_items item=item}
{
item_id: '{$item.item_id|escape:'javascript':'UTF-8'}',
item_name: '{$item.item_name|escape:'javascript':'UTF-8'}',
item_brand: '{$item.item_brand|escape:'javascript':'UTF-8'}',
item_category: '{$item.item_category|escape:'javascript':'UTF-8'}',
{if $item.item_variant}item_variant: '{$item.item_variant|escape:'javascript':'UTF-8'}',{/if}
price: {$item.price|floatval},
discount: {$item.discount|floatval},
quantity: {$item.quantity|intval}
}{if !$item@last},{/if}
{/foreach}
]
});
</script>
Refund Tracking
Server-Side Refund Event:
For refunds, you need to send data to GA4's Measurement Protocol API:
// When processing refund
public function sendRefundToGA4($order)
{
$measurement_id = Configuration::get('GA4_MEASUREMENT_ID');
$api_secret = Configuration::get('GA4_API_SECRET'); // From GA4 Admin > Data Streams
$client_id = $this->getClientIdForOrder($order);
$refund_data = array(
'client_id' => $client_id,
'events' => array(
array(
'name' => 'refund',
'params' => array(
'transaction_id' => $order->reference,
'value' => $order->total_paid_tax_incl,
'currency' => Currency::getCurrencyInstance($order->id_currency)->iso_code
)
)
)
);
$url = "https://www.google-analytics.com/mp/collect?measurement_id={$measurement_id}&api_secret={$api_secret}";
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($refund_data));
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json'));
$response = curl_exec($ch);
curl_close($ch);
return $response;
}
Testing Ecommerce Tracking
Using GA4 DebugView
Enable Debug Mode:
gtag('config', 'G-XXXXXXXXXX', {
'debug_mode': true
});
Test Complete Funnel:
- Browse category (view_item_list)
- Click product (select_item)
- View product page (view_item)
- Add to cart (add_to_cart)
- View cart (view_cart)
- Start checkout (begin_checkout)
- Select shipping (add_shipping_info)
- Select payment (add_payment_info)
- Complete test order (purchase)
Verify each event appears in DebugView with correct parameters.
Data Validation Checklist
- All required parameters present (item_id, item_name, etc.)
- Currency is correct 3-letter ISO code
- Prices are numeric (not strings)
- Transaction ID is unique for each purchase
- Items array contains all purchased products
- Tax and shipping values are accurate
- Coupon codes are captured when applied
- No duplicate purchase events on page refresh
Multi-Store Ecommerce Tracking
Add store context to transactions:
// In formatProductForGA4 or purchase event
$item['affiliation'] = $this->context->shop->name;
$item['location_id'] = (string)$this->context->shop->id;
Segment by store in GA4:
Create custom dimension for shop_id to analyze performance per store in multi-store setups.
Common Issues and Solutions
Duplicate Purchase Events
Problem: Purchase event fires multiple times on order confirmation refresh.
Solution: Set cookie to prevent duplicate firing:
// Check if purchase already tracked
if (!getCookie('order_tracked_' + transactionId)) {
gtag('event', 'purchase', purchaseData);
// Set cookie to prevent re-firing
document.cookie = 'order_tracked_' + transactionId + '=1; path=/; max-age=86400';
}
Missing Product Data
Problem: Product category or brand is empty.
Solution: Ensure all products have categories and manufacturers assigned in PrestaShop.
Currency Mismatch
Problem: Wrong currency in GA4 reports.
Solution: Verify $this->context->currency->iso_code returns correct 3-letter code (USD, EUR, GBP).
Next Steps
- GTM Data Layer - Advanced ecommerce with GTM
- Events Not Firing - Debug tracking issues
- Meta Pixel Ecommerce - Facebook conversion tracking