PrestaShop GTM Data Layer | Blue Frog Docs

PrestaShop GTM Data Layer

Build a comprehensive data layer for PrestaShop using hooks, Smarty templates, and JavaScript to power Google Tag Manager tracking.

PrestaShop GTM Data Layer

Implement a robust data layer structure for your PrestaShop store to enable advanced tracking with Google Tag Manager. This guide covers PrestaShop-specific implementations using hooks, Smarty templates, and the override system.

Data Layer Fundamentals

What is the Data Layer?

The data layer is a JavaScript object that stores information about the page, user, products, and interactions. GTM reads this data to fire tags with the correct information.

// Basic data layer structure
window.dataLayer = window.dataLayer || [];
dataLayer.push({
  'event': 'page_view',
  'pageType': 'product',
  'pageCategory': 'Electronics',
  'user': {
    'id': '12345',
    'status': 'logged_in'
  }
});

PrestaShop Context Data

PrestaShop provides rich context data that should be included in your data layer:

  • Page Information: Controller name, page type, category
  • User Information: Customer ID, group, login status
  • Product Data: ID, name, price, category, stock status
  • Cart Data: Products, quantities, totals
  • Transaction Data: Order details, payment method, shipping
  • Shop Context: Store ID, language, currency (multi-store)

Core Data Layer Structure for PrestaShop

Base Page Data Layer

Implement in every page:

<?php
// In your GTM module - hookDisplayHeader

public function hookDisplayHeader($params)
{
    // Get current page controller
    $controller = $this->context->controller;
    $page_name = get_class($controller);

    // Build base data layer
    $data_layer = array(
        'pageType' => $this->getPageType(),
        'pageName' => $page_name,
        'language' => $this->context->language->iso_code,
        'currency' => $this->context->currency->iso_code,
        'country' => $this->context->country->iso_code,
    );

    // Add shop context (multi-store)
    if (Shop::isFeatureActive()) {
        $data_layer['shop'] = array(
            'id' => (int)$this->context->shop->id,
            'name' => $this->context->shop->name,
            'groupId' => (int)$this->context->shop->id_shop_group
        );
    }

    // Add customer data
    $data_layer['user'] = $this->getUserData();

    // Add cart data
    if ($this->context->cart) {
        $data_layer['cart'] = $this->getCartData();
    }

    $this->context->smarty->assign(array(
        'dataLayer' => $data_layer
    ));

    return $this->display(__FILE__, 'views/templates/hook/datalayer-base.tpl');
}

private function getPageType()
{
    $controller = Tools::getValue('controller');

    $page_types = array(
        'index' => 'home',
        'category' => 'category',
        'product' => 'product',
        'cart' => 'cart',
        'order' => 'checkout',
        'order-confirmation' => 'purchase',
        'search' => 'search',
        'cms' => 'content',
        'authentication' => 'login',
        'my-account' => 'account'
    );

    return isset($page_types[$controller]) ? $page_types[$controller] : 'other';
}

private function getUserData()
{
    $customer = $this->context->customer;

    $user_data = array(
        'status' => $customer->isLogged() ? 'logged_in' : 'guest'
    );

    if ($customer->isLogged()) {
        $user_data['id'] = (int)$customer->id;
        $user_data['email'] = $customer->email; // Use hashed version for privacy
        $user_data['email_hash'] = hash('sha256', strtolower($customer->email));
        $user_data['groupId'] = (int)$customer->id_default_group;
        $user_data['newsletter'] = (bool)$customer->newsletter;
    }

    return $user_data;
}

private function getCartData()
{
    $cart = $this->context->cart;

    if (!$cart || !$cart->id) {
        return null;
    }

    $products = $cart->getProducts(true);
    $total = $cart->getOrderTotal(true);

    return array(
        'id' => (int)$cart->id,
        'itemCount' => $cart->nbProducts(),
        'total' => (float)$total,
        'currency' => $this->context->currency->iso_code
    );
}

Base Template:

{* views/templates/hook/datalayer-base.tpl *}
<script>
  window.dataLayer = window.dataLayer || [];
  dataLayer.push({
    'pageType': '{$dataLayer.pageType|escape:'javascript':'UTF-8'}',
    'pageName': '{$dataLayer.pageName|escape:'javascript':'UTF-8'}',
    'language': '{$dataLayer.language|escape:'javascript':'UTF-8'}',
    'currency': '{$dataLayer.currency|escape:'javascript':'UTF-8'}',
    'country': '{$dataLayer.country|escape:'javascript':'UTF-8'}',
    {if isset($dataLayer.shop)}
    'shop': {
      'id': '{$dataLayer.shop.id|intval}',
      'name': '{$dataLayer.shop.name|escape:'javascript':'UTF-8'}',
      'groupId': '{$dataLayer.shop.groupId|intval}'
    },
    {/if}
    'user': {
      'status': '{$dataLayer.user.status|escape:'javascript':'UTF-8'}',
      {if $dataLayer.user.status == 'logged_in'}
      'id': '{$dataLayer.user.id|intval}',
      'emailHash': '{$dataLayer.user.email_hash|escape:'javascript':'UTF-8'}',
      'groupId': '{$dataLayer.user.groupId|intval}',
      'newsletter': {if $dataLayer.user.newsletter}true{else}false{/if}
      {/if}
    },
    {if isset($dataLayer.cart)}
    'cart': {
      'id': '{$dataLayer.cart.id|intval}',
      'itemCount': {$dataLayer.cart.itemCount|intval},
      'total': {$dataLayer.cart.total|floatval},
      'currency': '{$dataLayer.cart.currency|escape:'javascript':'UTF-8'}'
    }
    {/if}
  });
</script>

Page-Specific Data Layers

Homepage Data Layer

// Hook: displayHome
public function hookDisplayHome($params)
{
    $data_layer = array(
        'event' => 'homepage_view',
        'pageCategory' => 'homepage'
    );

    $this->context->smarty->assign('homeDataLayer', $data_layer);
    return $this->display(__FILE__, 'views/templates/hook/datalayer-home.tpl');
}
{* Template *}
<script>
  dataLayer.push({
    'event': 'homepage_view',
    'pageCategory': 'homepage'
  });
</script>

Category Page Data Layer

// Hook: displayFooterCategory or actionProductSearchAfter
public function hookDisplayFooterCategory($params)
{
    $category = $params['category'];
    $products = $this->getCurrentCategoryProducts();

    // Format products for data layer
    $product_items = array();
    $index = 0;

    foreach ($products as $product) {
        $product_items[] = $this->formatProductForDataLayer($product, $index, 'category_' . $category->id);
        $index++;
    }

    $data_layer = array(
        'event' => 'view_item_list',
        'ecommerce' => array(
            'item_list_id' => 'category_' . $category->id,
            'item_list_name' => $category->name,
            'items' => $product_items
        ),
        'category' => array(
            'id' => (int)$category->id,
            'name' => $category->name,
            'parentId' => (int)$category->id_parent,
            'level' => $category->level_depth
        )
    );

    $this->context->smarty->assign('categoryDataLayer', json_encode($data_layer));
    return $this->display(__FILE__, 'views/templates/hook/datalayer-category.tpl');
}

private function formatProductForDataLayer($product, $index = 0, $list_id = '')
{
    // Get category path
    $category = new Category($product['id_category_default'], $this->context->language->id);
    $categories = $this->getCategoryPath($category);

    // Get manufacturer/brand
    $brand = '';
    if (isset($product['id_manufacturer']) && $product['id_manufacturer'] > 0) {
        $manufacturer = new Manufacturer($product['id_manufacturer']);
        $brand = $manufacturer->name;
    }

    return array(
        'item_id' => (string)$product['id_product'],
        'item_name' => $product['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_list_id' => $list_id,
        'item_list_name' => $category->name,
        'price' => (float)$product['price'],
        'quantity' => isset($product['quantity']) ? (int)$product['quantity'] : 1,
        'index' => $index
    );
}

private function getCategoryPath($category)
{
    $categories = array();
    $current = $category;

    while ($current->id > 2) { // Stop at root category
        $categories[] = $current->name;
        $current = new Category($current->id_parent, $this->context->language->id);
    }

    return array_reverse($categories);
}
{* views/templates/hook/datalayer-category.tpl *}
<script>
  dataLayer.push({$categoryDataLayer nofilter});
</script>

Product Page Data Layer

// Hook: displayFooterProduct
public function hookDisplayFooterProduct($params)
{
    $product = $params['product'];

    // Get selected combination
    $id_product_attribute = Tools::getValue('id_product_attribute', 0);

    $product_data = new Product($product->id, true, $this->context->language->id);

    // Get product images
    $images = $product_data->getImages($this->context->language->id);
    $cover_image = '';
    foreach ($images as $image) {
        if ($image['cover']) {
            $cover_image = $this->context->link->getImageLink(
                $product_data->link_rewrite,
                $image['id_image'],
                'large_default'
            );
            break;
        }
    }

    // Get stock
    $stock_available = StockAvailable::getQuantityAvailableByProduct($product->id, $id_product_attribute);

    // Format for data layer
    $item = $this->formatProductForDataLayer(array(
        'id_product' => $product->id,
        'name' => $product_data->name,
        'id_manufacturer' => $product_data->id_manufacturer,
        'id_category_default' => $product_data->id_category_default,
        'price' => $product_data->getPrice(true, $id_product_attribute),
        'quantity' => 1
    ));

    $data_layer = array(
        'event' => 'view_item',
        'ecommerce' => array(
            'currency' => $this->context->currency->iso_code,
            'value' => (float)$product_data->getPrice(true, $id_product_attribute),
            'items' => array($item)
        ),
        'product' => array(
            'id' => (int)$product->id,
            'name' => $product_data->name,
            'reference' => $product_data->reference,
            'ean13' => $product_data->ean13,
            'upc' => $product_data->upc,
            'stock' => (int)$stock_available,
            'available' => $stock_available > 0,
            'image' => $cover_image,
            'url' => $this->context->link->getProductLink($product_data)
        )
    );

    $this->context->smarty->assign('productDataLayer', json_encode($data_layer));
    return $this->display(__FILE__, 'views/templates/hook/datalayer-product.tpl');
}
{* views/templates/hook/datalayer-product.tpl *}
<script>
  dataLayer.push({$productDataLayer nofilter});
</script>

Cart Page Data Layer

// Hook: displayShoppingCart
public function hookDisplayShoppingCart($params)
{
    $cart = $this->context->cart;
    $products = $cart->getProducts(true);

    $items = array();
    foreach ($products as $product) {
        $items[] = $this->formatProductForDataLayer($product);
    }

    $data_layer = array(
        'event' => 'view_cart',
        'ecommerce' => array(
            'currency' => $this->context->currency->iso_code,
            'value' => (float)$cart->getOrderTotal(true),
            'items' => $items
        )
    );

    $this->context->smarty->assign('cartDataLayer', json_encode($data_layer));
    return $this->display(__FILE__, 'views/templates/hook/datalayer-cart.tpl');
}

Checkout Data Layer

// Hook: displayBeforeCheckout or custom checkout step
public function hookDisplayBeforeCheckout($params)
{
    $cart = $this->context->cart;
    $products = $cart->getProducts(true);

    $items = array();
    foreach ($products as $product) {
        $items[] = $this->formatProductForDataLayer($product);
    }

    // Get checkout step
    $checkout_step = $this->getCheckoutStep();

    $data_layer = array(
        'event' => 'begin_checkout',
        'ecommerce' => array(
            'currency' => $this->context->currency->iso_code,
            'value' => (float)$cart->getOrderTotal(true),
            'items' => $items
        ),
        'checkout' => array(
            'step' => $checkout_step,
            'option' => $this->getCheckoutStepOption($checkout_step)
        )
    );

    $this->context->smarty->assign('checkoutDataLayer', json_encode($data_layer));
    return $this->display(__FILE__, 'views/templates/hook/datalayer-checkout.tpl');
}

private function getCheckoutStep()
{
    $controller = Tools::getValue('controller');

    // PrestaShop checkout steps vary by version and theme
    // Adjust based on your checkout process

    if (strpos($_SERVER['REQUEST_URI'], 'delivery') !== false) {
        return 'shipping';
    } elseif (strpos($_SERVER['REQUEST_URI'], 'payment') !== false) {
        return 'payment';
    } else {
        return 'information';
    }
}

Order Confirmation Data Layer (Purchase Event)

Most critical ecommerce event:

// Hook: displayOrderConfirmation
public function hookDisplayOrderConfirmation($params)
{
    $order = $params['order'];

    // Get order products
    $order_detail = $order->getOrderDetailList();
    $items = array();

    foreach ($order_detail as $product) {
        $items[] = $this->formatProductForDataLayer($product);
    }

    // Get applied cart rules (coupons/discounts)
    $cart_rules = $order->getCartRules();
    $coupon_codes = array();
    $discount_total = 0;

    foreach ($cart_rules as $rule) {
        $coupon_codes[] = $rule['name'];
        $discount_total += $rule['value_tax_incl'];
    }

    // Get shipping method
    $carrier = new Carrier($order->id_carrier);

    // Get payment method
    $payment_module = Module::getInstanceByName($order->module);
    $payment_method = $payment_module ? $payment_module->displayName : $order->payment;

    $data_layer = array(
        'event' => 'purchase',
        'ecommerce' => array(
            'transaction_id' => $order->reference,
            'affiliation' => $this->context->shop->name,
            'value' => (float)$order->total_paid_tax_incl,
            'tax' => (float)($order->total_paid_tax_incl - $order->total_paid_tax_excl),
            'shipping' => (float)$order->total_shipping_tax_incl,
            'currency' => $this->context->currency->iso_code,
            'coupon' => implode(',', $coupon_codes),
            'items' => $items
        ),
        'transaction' => array(
            'id' => (int)$order->id,
            'reference' => $order->reference,
            'paymentMethod' => $payment_method,
            'shippingMethod' => $carrier->name,
            'discount' => (float)$discount_total
        )
    );

    // Prevent duplicate purchase on refresh
    $this->context->smarty->assign(array(
        'purchaseDataLayer' => json_encode($data_layer),
        'transactionId' => $order->reference
    ));

    return $this->display(__FILE__, 'views/templates/hook/datalayer-purchase.tpl');
}
{* views/templates/hook/datalayer-purchase.tpl *}
<script>
  // Check if already tracked (prevent duplicate on refresh)
  var transactionId = '{$transactionId|escape:'javascript':'UTF-8'}';
  var cookieName = 'purchase_tracked_' + transactionId;

  function getCookie(name) {
    var value = "; " + document.cookie;
    var parts = value.split("; " + name + "=");
    if (parts.length == 2) return parts.pop().split(";").shift();
  }

  if (!getCookie(cookieName)) {
    // Push purchase event
    dataLayer.push({$purchaseDataLayer nofilter});

    // Set cookie to prevent duplicate
    document.cookie = cookieName + '=1; path=/; max-age=86400';
  }
</script>

Dynamic Event Tracking

Add to Cart Event

Using JavaScript to capture AJAX cart additions:

// modules/customgtm/views/js/cart-tracking.js

document.addEventListener('DOMContentLoaded', function() {

  // Listen for PrestaShop cart update events
  prestashop.on('updateCart', function(event) {
    if (event && event.reason && event.reason.linkAction === 'add-to-cart') {

      // Get product data from page or event
      var productData = getProductDataFromPage();

      // Push add_to_cart event to data layer
      dataLayer.push({
        'event': 'add_to_cart',
        'ecommerce': {
          'currency': prestashop.currency.iso_code,
          'value': parseFloat(productData.price),
          'items': [{
            'item_id': productData.id,
            'item_name': productData.name,
            'item_brand': productData.brand,
            'item_category': productData.category,
            'price': parseFloat(productData.price),
            'quantity': parseInt(productData.quantity)
          }]
        }
      });
    }
  });

  // Also capture direct add to cart button clicks
  var addToCartButtons = document.querySelectorAll('[data-button-action="add-to-cart"]');

  addToCartButtons.forEach(function(button) {
    button.addEventListener('click', function(e) {
      var productData = getProductDataFromPage();

      dataLayer.push({
        'event': 'add_to_cart',
        'ecommerce': {
          'currency': prestashop.currency.iso_code,
          'value': parseFloat(productData.price),
          'items': [{
            'item_id': productData.id,
            'item_name': productData.name,
            'price': parseFloat(productData.price),
            'quantity': parseInt(productData.quantity)
          }]
        }
      });
    });
  });
});

function getProductDataFromPage() {
  return {
    id: prestashop.page.id_product || document.querySelector('[data-product-id]')?.dataset.productId,
    name: document.querySelector('h1.product-title')?.textContent.trim(),
    brand: document.querySelector('[itemprop="brand"]')?.textContent.trim() || '',
    category: document.querySelector('.breadcrumb .category')?.textContent.trim() || '',
    price: document.querySelector('[data-product-price]')?.dataset.productPrice ||
           document.querySelector('.product-price .current-price-value')?.textContent.replace(/[^0-9.]/g, ''),
    quantity: document.querySelector('#quantity_wanted')?.value || 1
  };
}

Register JavaScript in module:

// In hookDisplayHeader
$this->context->controller->addJS($this->_path . 'views/js/cart-tracking.js');

Remove from Cart Event

// Track cart item removal
document.addEventListener('click', function(e) {
  if (e.target.matches('[data-link-action="delete-from-cart"]')) {
    var cartItem = e.target.closest('.cart-item, .product-line-grid');

    var productData = {
      id: cartItem.dataset.idProduct,
      name: cartItem.querySelector('.product-name')?.textContent.trim(),
      price: cartItem.querySelector('.product-price')?.textContent.replace(/[^0-9.]/g, ''),
      quantity: cartItem.querySelector('.product-quantity')?.textContent.trim() || 1
    };

    dataLayer.push({
      'event': 'remove_from_cart',
      'ecommerce': {
        'currency': prestashop.currency.iso_code,
        'value': parseFloat(productData.price) * parseInt(productData.quantity),
        'items': [{
          'item_id': productData.id,
          'item_name': productData.name,
          'price': parseFloat(productData.price),
          'quantity': parseInt(productData.quantity)
        }]
      }
    });
  }
});

Product Click Tracking (select_item)

// Track product clicks in listings
document.addEventListener('click', function(e) {
  var productLink = e.target.closest('.product-miniature a');

  if (productLink) {
    var miniature = productLink.closest('.product-miniature');
    var listName = document.querySelector('.page-category-heading')?.textContent.trim() || 'product_list';

    var productData = {
      id: miniature.dataset.idProduct,
      name: miniature.querySelector('.product-title')?.textContent.trim(),
      price: miniature.querySelector('.product-price .price')?.textContent.replace(/[^0-9.]/g, ''),
      index: Array.from(miniature.parentElement.children).indexOf(miniature)
    };

    dataLayer.push({
      'event': 'select_item',
      'ecommerce': {
        'item_list_name': listName,
        'items': [{
          'item_id': productData.id,
          'item_name': productData.name,
          'price': parseFloat(productData.price),
          'index': productData.index
        }]
      }
    });
  }
}, true);

Advanced Data Layer Features

User Registration Event

// Hook: actionCustomerAccountAdd
public function hookActionCustomerAccountAdd($params)
{
    $customer = $params['newCustomer'];

    $data_layer = array(
        'event' => 'sign_up',
        'method' => 'prestashop_registration',
        'user' => array(
            'id' => (int)$customer->id,
            'newsletter' => (bool)$customer->newsletter
        )
    );

    // Store in session/cookie for template retrieval
    $this->context->cookie->__set('gtm_signup_event', json_encode($data_layer));
}

Search Tracking

// Hook: actionSearch
public function hookActionSearch($params)
{
    $search_query = $params['search_query'];
    $results_count = isset($params['total']) ? $params['total'] : 0;

    $data_layer = array(
        'event' => 'search',
        'search_term' => $search_query,
        'results_count' => $results_count
    );

    $this->context->smarty->assign('searchDataLayer', json_encode($data_layer));
    return $this->display(__FILE__, 'views/templates/hook/datalayer-search.tpl');
}

Newsletter Signup

// Track newsletter subscriptions
var newsletterForm = document.querySelector('.block_newsletter form, #newsletter-form');

if (newsletterForm) {
  newsletterForm.addEventListener('submit', function(e) {
    dataLayer.push({
      'event': 'newsletter_signup',
      'method': 'footer_form'
    });
  });
}

GTM Variable Configuration

Create Variables in GTM

Data Layer Variables:

  1. Page Type

    • Variable Type: Data Layer Variable
    • Data Layer Variable Name: pageType
  2. User ID

    • Variable Type: Data Layer Variable
    • Data Layer Variable Name: user.id
  3. Shop ID (Multi-store)

    • Variable Type: Data Layer Variable
    • Data Layer Variable Name: shop.id
  4. Transaction ID

    • Variable Type: Data Layer Variable
    • Data Layer Variable Name: ecommerce.transaction_id
  5. Ecommerce Items

    • Variable Type: Data Layer Variable
    • Data Layer Variable Name: ecommerce.items

Custom JavaScript Variables:

// Get cart item count
function() {
  return window.dataLayer.find(function(item) {
    return item.cart && item.cart.itemCount;
  })?.cart?.itemCount || 0;
}

Testing the Data Layer

Browser Console Testing

// View entire data layer
console.log(window.dataLayer);

// Find specific events
dataLayer.filter(item => item.event === 'purchase')

// Check latest push
dataLayer[dataLayer.length - 1]

GTM Preview Mode

  1. Enable Preview in GTM
  2. Navigate through store
  3. Check "Data Layer" tab in debugger
  4. Verify all expected variables populated
  5. Check ecommerce object structure

Data Layer Validator

Use browser extension or create validation script:

// Validate purchase event structure
function validatePurchaseEvent(event) {
  var required = ['transaction_id', 'value', 'currency', 'items'];
  var missing = [];

  required.forEach(function(field) {
    if (!event.ecommerce[field]) {
      missing.push(field);
    }
  });

  if (missing.length > 0) {
    console.error('Missing required fields:', missing);
    return false;
  }

  return true;
}

// Test
var purchaseEvent = dataLayer.find(e => e.event === 'purchase');
if (purchaseEvent) {
  validatePurchaseEvent(purchaseEvent);
}

Multi-Store Data Layer Strategy

Include shop context in all events:

// Add to all data layer pushes
if (Shop::isFeatureActive()) {
    $data_layer['shop'] = array(
        'id' => (int)$this->context->shop->id,
        'name' => $this->context->shop->name,
        'group' => (int)$this->context->shop->id_shop_group,
        'url' => $this->context->shop->getBaseURL()
    );
}

Create GTM variable to segment by shop:

  • Variable Name: Shop ID
  • Type: Data Layer Variable
  • Data Layer Variable Name: shop.id

Use in GTM triggers:

  • Fire Tag X only when Shop ID equals 1
  • Fire Tag Y only for Shop Group 2

Performance Considerations

Optimize Data Layer Size:

  • Only include necessary data
  • Avoid large nested objects
  • Remove temporary data after use

Async Data Layer Pushes:

  • Don't block page render
  • Use setTimeout for non-critical pushes
  • Batch related pushes

Cache Compatibility:

  • Ensure dynamic data isn't cached
  • Use client-side JavaScript for user-specific data
  • Test with full page cache enabled

Next Steps

// SYS.FOOTER