Overview
The data layer is a JavaScript object that stores structured data about:
- Page context (type, category, product)
- User information (logged in status, customer ID)
- Product data (IDs, names, prices, categories)
- Transaction details (order ID, revenue, items)
- Custom events and interactions
Benefits:
- Tag independence: Change tags without modifying data layer
- Consistent data: Single source of truth for all tags
- Debugging: Easier to troubleshoot tracking issues
- Flexibility: Add new tags without code changes
Data Layer Structure
Base Implementation
Initialize the data layer before GTM loads:
File: catalog/view/theme/[your-theme]/template/common/header.twig
<head>
<script>
// Initialize dataLayer before GTM
window.dataLayer = window.dataLayer || [];
// Push base page data
dataLayer.push({
'pageType': '{{ page_type|default('other') }}',
'pageName': '{{ page_name|default(heading_title) }}',
'language': '{{ language }}',
'currency': '{{ currency }}',
'customerId': '{{ customer_id|default('') }}',
'customerGroup': '{{ customer_group|default('Guest') }}',
'customerEmail': '{{ customer_email_hash|default('') }}'
});
</script>
<!-- GTM Container Code -->
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-XXXXXXX');</script>
<!-- End GTM -->
</head>
Controller-Side Data Preparation
File: catalog/controller/common/header.php
Add to index() method:
public function index() {
// ... existing code ...
// Determine page type
$data['page_type'] = $this->getPageType();
$data['page_name'] = $this->document->getTitle();
// Customer data
if ($this->customer->isLogged()) {
$data['customer_id'] = $this->customer->getId();
$data['customer_group'] = $this->customer->getGroupId();
$data['customer_email_hash'] = hash('sha256', strtolower($this->customer->getEmail()));
} else {
$data['customer_id'] = '';
$data['customer_group'] = 'Guest';
$data['customer_email_hash'] = '';
}
// Store info
$data['language'] = $this->session->data['language'];
$data['currency'] = $this->session->data['currency'];
// ... rest of existing code ...
return $this->load->view('common/header', $data);
}
private function getPageType() {
// Detect page type from route
$route = isset($this->request->get['route']) ? $this->request->get['route'] : 'common/home';
if ($route == 'common/home') {
return 'home';
} elseif (strpos($route, 'product/product') !== false) {
return 'product';
} elseif (strpos($route, 'product/category') !== false) {
return 'category';
} elseif (strpos($route, 'product/search') !== false) {
return 'search';
} elseif (strpos($route, 'checkout/cart') !== false) {
return 'cart';
} elseif (strpos($route, 'checkout/checkout') !== false) {
return 'checkout';
} elseif (strpos($route, 'checkout/success') !== false) {
return 'purchase';
} elseif (strpos($route, 'account/') !== false) {
return 'account';
} else {
return 'other';
}
}
Page-Specific Data Layers
Home Page
File: catalog/controller/common/home.php
public function index() {
// ... existing code ...
// Data layer for homepage
$data['ecommerce_data'] = array(
'pageType' => 'home',
'impressions' => array()
);
// Get featured products if available
if (isset($data['products'])) {
foreach ($data['products'] as $index => $product) {
$data['ecommerce_data']['impressions'][] = array(
'id' => $product['product_id'],
'name' => $product['name'],
'price' => $this->currency->format($product['price'], $this->session->data['currency'], '', false),
'list' => 'Homepage Featured',
'position' => $index + 1
);
}
}
// ... rest of code ...
}
File: catalog/view/theme/[your-theme]/template/common/home.twig
{% if ecommerce_data %}
<script>
dataLayer.push({
'event': 'productImpressions',
'ecommerce': {
'currencyCode': '{{ currency }}',
'impressions': {{ ecommerce_data.impressions|json_encode|raw }}
}
});
</script>
{% endif %}
Category Page
File: catalog/controller/product/category.php
public function index() {
// ... existing code after products are loaded ...
// Data layer for category page
$data['ecommerce_data'] = array(
'pageType' => 'category',
'categoryName' => $this->document->getTitle(),
'categoryId' => $category_id,
'impressions' => array()
);
foreach ($data['products'] as $index => $product) {
$data['ecommerce_data']['impressions'][] = array(
'id' => $product['product_id'],
'name' => $product['name'],
'price' => $this->currency->format($product['price'], $this->session->data['currency'], '', false),
'category' => $this->document->getTitle(),
'list' => 'Category: ' . $this->document->getTitle(),
'position' => $index + 1
);
}
// ... rest of code ...
}
File: catalog/view/theme/[your-theme]/template/product/category.twig
{% if ecommerce_data %}
<script>
dataLayer.push({
'event': 'productImpressions',
'ecommerce': {
'currencyCode': '{{ currency }}',
'impressions': {{ ecommerce_data.impressions|json_encode|raw }}
}
});
// Track product clicks
$(document).on('click', '.product-thumb a', function() {
var productIndex = $(this).closest('.product-thumb').index();
var impressions = {{ ecommerce_data.impressions|json_encode|raw }};
if (impressions[productIndex]) {
dataLayer.push({
'event': 'productClick',
'ecommerce': {
'click': {
'actionField': {'list': '{{ ecommerce_data.categoryName }}'},
'products': [impressions[productIndex]]
}
}
});
}
});
</script>
{% endif %}
Product Detail Page
File: catalog/controller/product/product.php
public function index() {
// ... existing code after product_info is loaded ...
if ($product_info) {
// Get category
$categories = $this->model_catalog_product->getCategories($product_id);
$category_name = '';
if ($categories) {
$this->load->model('catalog/category');
$category_info = $this->model_catalog_category->getCategory($categories[0]['category_id']);
$category_name = $category_info ? $category_info['name'] : '';
}
// Get brand/manufacturer
$manufacturer_name = '';
if ($product_info['manufacturer_id']) {
$manufacturer_info = $this->model_catalog_product->getManufacturer($product_info['manufacturer_id']);
$manufacturer_name = $manufacturer_info ? $manufacturer_info['name'] : '';
}
// Format price
$price = $this->currency->format($product_info['price'], $this->session->data['currency'], '', false);
$data['ecommerce_data'] = array(
'pageType' => 'product',
'product' => array(
'id' => $product_info['product_id'],
'name' => $product_info['name'],
'price' => $price,
'brand' => $manufacturer_name,
'category' => $category_name,
'variant' => '', // Add option selection if needed
'quantity' => 1
)
);
}
// ... rest of code ...
}
File: catalog/view/theme/[your-theme]/template/product/product.twig
{% if ecommerce_data %}
<script>
// Product detail view
dataLayer.push({
'event': 'productDetail',
'ecommerce': {
'currencyCode': '{{ currency }}',
'detail': {
'products': [{{ ecommerce_data.product|json_encode|raw }}]
}
}
});
// Add to cart event
$('#button-cart').on('click', function() {
var quantity = parseInt($('input[name="quantity"]').val()) || 1;
var product = {{ ecommerce_data.product|json_encode|raw }};
product.quantity = quantity;
dataLayer.push({
'event': 'addToCart',
'ecommerce': {
'currencyCode': '{{ currency }}',
'add': {
'products': [product]
}
}
});
});
</script>
{% endif %}
Cart Page
File: catalog/controller/checkout/cart.php
public function index() {
// ... existing code after cart products are loaded ...
$data['ecommerce_data'] = array(
'pageType' => 'cart',
'products' => array(),
'cartTotal' => 0
);
foreach ($this->cart->getProducts() as $product) {
$price = $this->currency->format($product['price'], $this->session->data['currency'], '', false);
$data['ecommerce_data']['products'][] = array(
'id' => $product['product_id'],
'name' => $product['name'],
'price' => $price,
'quantity' => $product['quantity']
);
$data['ecommerce_data']['cartTotal'] += $price * $product['quantity'];
}
// ... rest of code ...
}
File: catalog/view/theme/[your-theme]/template/checkout/cart.twig
{% if ecommerce_data %}
<script>
// Cart page view
dataLayer.push({
'event': 'cartView',
'ecommerce': {
'currencyCode': '{{ currency }}',
'cartValue': {{ ecommerce_data.cartTotal }},
'products': {{ ecommerce_data.products|json_encode|raw }}
}
});
// Remove from cart
$(document).on('click', '.btn-danger[onclick*="cart.remove"]', function() {
var productId = $(this).attr('onclick').match(/cart\.remove\('(\d+)'\)/)[1];
var products = {{ ecommerce_data.products|json_encode|raw }};
var removedProduct = products.find(p => p.id == productId);
if (removedProduct) {
dataLayer.push({
'event': 'removeFromCart',
'ecommerce': {
'remove': {
'products': [removedProduct]
}
}
});
}
});
</script>
{% endif %}
Checkout Page
File: catalog/controller/checkout/checkout.php
public function index() {
// ... existing code ...
// Get cart products for data layer
$data['ecommerce_data'] = array(
'pageType' => 'checkout',
'step' => 1,
'products' => array(),
'checkoutValue' => 0
);
foreach ($this->cart->getProducts() as $product) {
$price = $this->currency->format($product['price'], $this->session->data['currency'], '', false);
$data['ecommerce_data']['products'][] = array(
'id' => $product['product_id'],
'name' => $product['name'],
'price' => $price,
'quantity' => $product['quantity']
);
$data['ecommerce_data']['checkoutValue'] += $price * $product['quantity'];
}
// ... rest of code ...
}
File: catalog/view/theme/[your-theme]/template/checkout/checkout.twig
{% if ecommerce_data %}
<script>
// Checkout initiation
dataLayer.push({
'event': 'checkout',
'ecommerce': {
'currencyCode': '{{ currency }}',
'checkout': {
'actionField': {'step': {{ ecommerce_data.step }}},
'products': {{ ecommerce_data.products|json_encode|raw }}
}
}
});
// Track checkout steps
$(document).on('click', '#button-payment-address', function() {
dataLayer.push({
'event': 'checkoutStep',
'ecommerce': {
'checkout': {
'actionField': {'step': 2, 'option': 'Payment Address'}
}
}
});
});
$(document).on('click', '#button-shipping-address', function() {
dataLayer.push({
'event': 'checkoutStep',
'ecommerce': {
'checkout': {
'actionField': {'step': 3, 'option': 'Shipping Address'}
}
}
});
});
$(document).on('click', '#button-shipping-method', function() {
var shippingMethod = $('input[name="shipping_method"]:checked').val();
dataLayer.push({
'event': 'checkoutStep',
'ecommerce': {
'checkout': {
'actionField': {'step': 4, 'option': shippingMethod}
}
}
});
});
$(document).on('click', '#button-payment-method', function() {
var paymentMethod = $('input[name="payment_method"]:checked').val();
dataLayer.push({
'event': 'checkoutStep',
'ecommerce': {
'checkout': {
'actionField': {'step': 5, 'option': paymentMethod}
}
}
});
});
</script>
{% endif %}
Purchase Confirmation
File: catalog/controller/checkout/success.php
public function index() {
// ... existing code ...
$order_id = isset($this->session->data['order_id']) ? $this->session->data['order_id'] : 0;
if ($order_id && !isset($this->session->data['gtm_tracked_' . $order_id])) {
$this->load->model('checkout/order');
$order_info = $this->model_checkout_order->getOrder($order_id);
if ($order_info) {
$order_products = $this->model_checkout_order->getOrderProducts($order_id);
$products = array();
foreach ($order_products as $product) {
$price = $this->currency->format($product['price'], $order_info['currency_code'], '', false);
$products[] = array(
'id' => $product['product_id'],
'name' => $product['name'],
'price' => $price,
'quantity' => $product['quantity'],
'category' => '', // Add if available
'variant' => '' // Add if options available
);
}
// Calculate tax and shipping
$tax = 0;
$shipping = 0;
$order_totals = $this->model_checkout_order->getOrderTotals($order_id);
foreach ($order_totals as $total) {
if ($total['code'] == 'tax') {
$tax += $this->currency->format($total['value'], $order_info['currency_code'], '', false);
} elseif ($total['code'] == 'shipping') {
$shipping = $this->currency->format($total['value'], $order_info['currency_code'], '', false);
}
}
$data['ecommerce_data'] = array(
'transactionId' => $order_id,
'affiliation' => $this->config->get('config_name'),
'revenue' => $this->currency->format($order_info['total'], $order_info['currency_code'], '', false),
'tax' => $tax,
'shipping' => $shipping,
'currency' => $order_info['currency_code'],
'products' => $products
);
// Mark as tracked
$this->session->data['gtm_tracked_' . $order_id] = true;
}
// Clear order_id from session
unset($this->session->data['order_id']);
}
// ... rest of code ...
}
File: catalog/view/theme/[your-theme]/template/common/success.twig
{% if ecommerce_data %}
<script>
// Purchase event
dataLayer.push({
'event': 'purchase',
'ecommerce': {
'purchase': {
'actionField': {
'id': '{{ ecommerce_data.transactionId }}',
'affiliation': '{{ ecommerce_data.affiliation }}',
'revenue': {{ ecommerce_data.revenue }},
'tax': {{ ecommerce_data.tax }},
'shipping': {{ ecommerce_data.shipping }}
},
'products': {{ ecommerce_data.products|json_encode|raw }}
}
}
});
</script>
{% endif %}
Custom Events
User Interactions
Newsletter Signup:
<script>
$(document).on('submit', '#newsletter-form', function() {
dataLayer.push({
'event': 'newsletterSignup',
'formLocation': 'footer',
'emailProvided': true
});
});
</script>
Contact Form:
<script>
$(document).on('submit', '#contact-form', function() {
dataLayer.push({
'event': 'contactFormSubmit',
'formType': 'contact',
'enquiryType': $('select[name="enquiry"]').val()
});
});
</script>
Product Reviews:
<script>
$(document).on('submit', '#form-review', function() {
var rating = $('input[name="rating"]:checked').val();
dataLayer.push({
'event': 'productReview',
'productId': '{{ product_id }}',
'productName': '{{ product_name }}',
'rating': rating
});
});
</script>
Search Tracking
<script>
$(document).on('submit', '#search input[name="search"]').closest('form'), function() {
var searchTerm = $(this).find('input[name="search"]').val();
dataLayer.push({
'event': 'siteSearch',
'searchTerm': searchTerm
});
});
</script>
Enhanced Data Layer with User Properties
File: catalog/view/theme/[your-theme]/template/common/header.twig
<script>
window.dataLayer = window.dataLayer || [];
dataLayer.push({
// Page data
'pageType': '{{ page_type }}',
'pageTitle': '{{ page_name }}',
'pageUrl': window.location.href,
'pageReferrer': document.referrer,
// Store data
'storeName': '{{ config_name }}',
'storeId': '{{ config_store_id }}',
'language': '{{ language }}',
'currency': '{{ currency }}',
// User data
'userId': '{{ customer_id|default('') }}',
'userType': '{{ customer_id ? "logged_in" : "guest" }}',
'customerGroup': '{{ customer_group|default('Guest') }}',
// Session data
'sessionId': '{{ session_id|default('') }}',
// Cart data
'cartValue': {{ cart_total|default(0) }},
'cartItemCount': {{ cart_item_count|default(0) }}
});
</script>
Debugging Data Layer
Console Inspection
// View entire dataLayer
console.table(dataLayer);
// View latest push
console.log(dataLayer[dataLayer.length - 1]);
// Filter specific events
dataLayer.filter(item => item.event === 'addToCart');
GTM Preview Mode
- In GTM, click Preview
- Enter your store URL
- Browse your store
- Check Data Layer tab in debugger
- Verify variables are populated correctly
Data Layer Monitor Extension
Install dataLayer Inspector+
Shows real-time dataLayer pushes in browser DevTools.
Best Practices
1. Consistent Naming
Use camelCase for all variables:
{
'productId': '123', // Good
'product_id': '123', // Avoid
'ProductId': '123' // Avoid
}
2. Data Types
Ensure correct data types:
{
'productId': '123', // String
'productPrice': 29.99, // Number (not "29.99")
'inStock': true, // Boolean (not "true")
'categories': ['cat1', 'cat2'] // Array
}
3. Clear Before Push
For ecommerce events, clear previous data:
dataLayer.push({
'ecommerce': null // Clear previous ecommerce object
});
dataLayer.push({
'event': 'addToCart',
'ecommerce': {
// Fresh data
}
});
4. Avoid PII
Never send personally identifiable information:
// Bad
'customerEmail': 'user@example.com'
// Good
'customerEmail': hash('user@example.com') // Hashed