Data Layers | Blue Frog Docs

Data Layers

Complete guide to implementing data layers for analytics, with structure examples, GTM integration, e-commerce events, and debugging techniques.

Data Layers

A data layer is a JavaScript object that stores and organizes data for use by analytics tools, tag managers, and marketing platforms. It acts as a neutral intermediary between your website's data and tracking tools, enabling consistent, reliable data collection.

Why Use a Data Layer?

Without a data layer, analytics implementations typically:

  • Scrape data directly from the DOM (fragile)
  • Rely on hardcoded values (inflexible)
  • Have inconsistent naming across pages
  • Break when designers change the UI

A data layer solves these problems by providing a structured, centralized data source.

Before vs After Data Layer

Without data layer (fragile):

// Scraping price from the page - breaks if HTML changes
var price = document.querySelector('.product-price').innerText.replace('$', '');
gtag('event', 'view_item', { value: parseFloat(price) });

With data layer (reliable):

// Price provided by backend, always accurate
gtag('event', 'view_item', { value: dataLayer.find(e => e.productPrice).productPrice });

// Or via GTM variable
// {{DL - Product Price}}

Data Layer Basics

Structure

The data layer is simply a JavaScript array:

// Initialize data layer BEFORE GTM/gtag loads
window.dataLayer = window.dataLayer || [];

Data is added by pushing objects onto this array:

// Push static data
dataLayer.push({
  pageType: 'product',
  productId: 'SKU-12345',
  productName: 'Blue Widget',
  productPrice: 29.99,
  productCategory: 'Widgets'
});

// Push an event (triggers GTM tags)
dataLayer.push({
  event: 'productView',
  productId: 'SKU-12345'
});

Key Concepts

Concept Description
Push Adding data to the data layer array
Event Special key that triggers GTM tag firing
Persistence Later pushes override earlier values with same keys
Variables GTM reads values from data layer via Data Layer Variables

Data Layer vs Event

// Data only - no trigger fires
dataLayer.push({
  userType: 'customer',
  userId: '12345'
});

// Event - triggers any tag listening for 'login' event
dataLayer.push({
  event: 'login',
  userType: 'customer',
  userId: '12345'
});

Standard Data Layer Structure

Page-Level Data

Push this data on every page, before GTM loads:

<script>
  window.dataLayer = window.dataLayer || [];
  dataLayer.push({
    // Page information
    pageType: 'product',          // home, category, product, cart, checkout, confirmation
    pageName: 'Blue Widget - Product Page',
    pageCategory: 'Widgets',

    // User information (if logged in)
    userLoggedIn: true,
    userId: 'U123456',
    userType: 'customer',         // guest, customer, subscriber, vip

    // Site information
    siteName: 'Example Store',
    siteSection: 'Products',
    siteLanguage: 'en-US',
    siteCurrency: 'USD'
  });
</script>

<!-- GTM snippet goes here -->
<script>(function(w,d,s,l,i){...})(window,document,'script','dataLayer','GTM-XXXXX');</script>

E-Commerce Data Layer (GA4 Format)

Product Page

dataLayer.push({ ecommerce: null });  // Clear previous ecommerce data
dataLayer.push({
  event: 'view_item',
  ecommerce: {
    currency: 'USD',
    value: 29.99,
    items: [{
      item_id: 'SKU-12345',
      item_name: 'Blue Widget',
      item_brand: 'WidgetCo',
      item_category: 'Widgets',
      item_category2: 'Blue Widgets',
      item_variant: 'Large',
      price: 29.99,
      quantity: 1
    }]
  }
});

Add to Cart

dataLayer.push({ ecommerce: null });
dataLayer.push({
  event: 'add_to_cart',
  ecommerce: {
    currency: 'USD',
    value: 29.99,
    items: [{
      item_id: 'SKU-12345',
      item_name: 'Blue Widget',
      item_brand: 'WidgetCo',
      item_category: 'Widgets',
      price: 29.99,
      quantity: 1
    }]
  }
});

View Cart

dataLayer.push({ ecommerce: null });
dataLayer.push({
  event: 'view_cart',
  ecommerce: {
    currency: 'USD',
    value: 89.97,
    items: [
      {
        item_id: 'SKU-12345',
        item_name: 'Blue Widget',
        price: 29.99,
        quantity: 2
      },
      {
        item_id: 'SKU-67890',
        item_name: 'Red Widget',
        price: 29.99,
        quantity: 1
      }
    ]
  }
});

Begin Checkout

dataLayer.push({ ecommerce: null });
dataLayer.push({
  event: 'begin_checkout',
  ecommerce: {
    currency: 'USD',
    value: 89.97,
    coupon: 'SAVE10',
    items: [/* same items array */]
  }
});

Add Payment Info

dataLayer.push({ ecommerce: null });
dataLayer.push({
  event: 'add_payment_info',
  ecommerce: {
    currency: 'USD',
    value: 89.97,
    payment_type: 'Credit Card',
    items: [/* same items array */]
  }
});

Purchase

dataLayer.push({ ecommerce: null });
dataLayer.push({
  event: 'purchase',
  ecommerce: {
    transaction_id: 'T-123456',
    value: 89.97,
    tax: 7.20,
    shipping: 5.99,
    currency: 'USD',
    coupon: 'SAVE10',
    items: [
      {
        item_id: 'SKU-12345',
        item_name: 'Blue Widget',
        item_brand: 'WidgetCo',
        item_category: 'Widgets',
        price: 29.99,
        quantity: 2
      },
      {
        item_id: 'SKU-67890',
        item_name: 'Red Widget',
        item_brand: 'WidgetCo',
        item_category: 'Widgets',
        price: 29.99,
        quantity: 1
      }
    ]
  }
});

Why Clear Ecommerce First?

dataLayer.push({ ecommerce: null });  // Always do this before ecommerce pushes

This prevents data from previous ecommerce events from persisting. Without this:

  • Old items might merge with new items
  • Previous transaction_id might carry over
  • Category data could bleed between pages

GTM Data Layer Variables

Creating a Data Layer Variable

  1. In GTM, go to Variables → New
  2. Choose "Data Layer Variable"
  3. Enter the variable name exactly as it appears in the data layer
Data Layer Key GTM Variable Name Access Pattern
productName DL - Product Name productName
ecommerce.currency DL - Ecommerce Currency ecommerce.currency
ecommerce.items.0.item_id DL - First Item ID ecommerce.items.0.item_id
user.id DL - User ID user.id

Nested Data Access

For nested objects, use dot notation:

// Data layer push
dataLayer.push({
  user: {
    id: '12345',
    type: 'customer',
    preferences: {
      newsletter: true,
      sms: false
    }
  }
});

// GTM variable paths:
// user.id → '12345'
// user.preferences.newsletter → true

Array Access

// Data layer push
dataLayer.push({
  ecommerce: {
    items: [
      { item_id: 'SKU-1', item_name: 'Widget A' },
      { item_id: 'SKU-2', item_name: 'Widget B' }
    ]
  }
});

// GTM variable paths:
// ecommerce.items.0.item_id → 'SKU-1'
// ecommerce.items.1.item_name → 'Widget B'
// ecommerce.items → entire array (for GA4 tags)

Implementation Patterns

Pattern 1: Server-Side Rendering

Best for traditional websites with page reloads:

<!-- PHP example -->
<script>
  window.dataLayer = window.dataLayer || [];
  dataLayer.push({
    pageType: '<?= $pageType ?>',
    <?php if ($product): ?>
    productId: '<?= $product->id ?>',
    productName: '<?= addslashes($product->name) ?>',
    productPrice: <?= $product->price ?>,
    <?php endif; ?>
    <?php if ($user): ?>
    userId: '<?= $user->id ?>',
    userType: '<?= $user->type ?>'
    <?php endif; ?>
  });
</script>

Pattern 2: JavaScript Framework (React)

For SPAs, push data when components mount or state changes:

// React component
import { useEffect } from 'react';

function ProductPage({ product }) {
  useEffect(() => {
    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push({ ecommerce: null });
    window.dataLayer.push({
      event: 'view_item',
      ecommerce: {
        currency: 'USD',
        value: product.price,
        items: [{
          item_id: product.id,
          item_name: product.name,
          price: product.price
        }]
      }
    });
  }, [product.id]);  // Re-push when product changes

  return <div>{/* product content */}</div>;
}

Pattern 3: Custom Hook (React)

Create a reusable hook for consistent tracking:

// hooks/useDataLayer.js
import { useCallback } from 'react';

export function useDataLayer() {
  const push = useCallback((data) => {
    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push(data);
  }, []);

  const pushEvent = useCallback((eventName, eventData = {}) => {
    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push({
      event: eventName,
      ...eventData
    });
  }, []);

  const pushEcommerce = useCallback((eventName, ecommerceData) => {
    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push({ ecommerce: null });
    window.dataLayer.push({
      event: eventName,
      ecommerce: ecommerceData
    });
  }, []);

  return { push, pushEvent, pushEcommerce };
}

// Usage
function AddToCartButton({ product }) {
  const { pushEcommerce } = useDataLayer();

  const handleClick = () => {
    addToCart(product);
    pushEcommerce('add_to_cart', {
      currency: 'USD',
      value: product.price,
      items: [{ item_id: product.id, item_name: product.name, price: product.price }]
    });
  };

  return <button onClick={handleClick}>Add to Cart</button>;
}

Pattern 4: Vue.js

// Vue 3 composable
import { inject } from 'vue';

export function useDataLayer() {
  const push = (data) => {
    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push(data);
  };

  const trackEvent = (eventName, data = {}) => {
    push({ event: eventName, ...data });
  };

  return { push, trackEvent };
}

// In component
<script setup>
import { onMounted } from 'vue';
import { useDataLayer } from '@/composables/useDataLayer';

const props = defineProps(['product']);
const { push } = useDataLayer();

onMounted(() => {
  push({ ecommerce: null });
  push({
    event: 'view_item',
    ecommerce: {
      items: [{
        item_id: props.product.id,
        item_name: props.product.name,
        price: props.product.price
      }]
    }
  });
});
</script>

Debugging the Data Layer

Console Inspection

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

// Pretty print
console.log(JSON.stringify(window.dataLayer, null, 2));

// View as table
console.table(window.dataLayer);

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

// Get current merged state
function getDataLayerState() {
  return window.dataLayer.reduce((acc, item) => {
    if (typeof item === 'object' && !Array.isArray(item) && item !== null) {
      return { ...acc, ...item };
    }
    return acc;
  }, {});
}
console.log(getDataLayerState());

Real-Time Monitoring

// Monitor all pushes
(function() {
  var originalPush = window.dataLayer.push;
  window.dataLayer.push = function() {
    console.group('📊 dataLayer.push');
    console.log('Data:', arguments[0]);
    console.log('Time:', new Date().toLocaleTimeString());
    if (arguments[0].event) {
      console.log('Event:', arguments[0].event);
    }
    console.groupEnd();
    return originalPush.apply(this, arguments);
  };
})();

GTM Preview Mode

  1. Open GTM and click "Preview"
  2. Enter your site URL
  3. Navigate to the page you're testing
  4. In Tag Assistant:
    • Click "Data Layer" tab
    • See all pushes in chronological order
    • Click any push to see its contents
    • Verify variables resolve correctly

Common Issues

Issue: Variable Shows (undefined)

// Problem: Typo or wrong path
dataLayer.push({ productName: 'Widget' });
// GTM variable: 'productsName' → undefined (typo)
// GTM variable: 'product.name' → undefined (wrong structure)

// Solution: Match exact key name
// GTM variable: 'productName' → 'Widget'

Issue: Data Not Available

// Problem: Pushing after tag fires
gtag('event', 'purchase');  // Tag fires here
dataLayer.push({ transactionId: 'T-123' });  // Too late!

// Solution: Push data BEFORE the event
dataLayer.push({ transactionId: 'T-123' });
dataLayer.push({ event: 'purchase' });

Issue: Old Data Persists

// Problem: Previous ecommerce data merges
// Page 1: view_item for Product A
// Page 2: view_item for Product B (but still has Product A data!)

// Solution: Clear ecommerce before each push
dataLayer.push({ ecommerce: null });
dataLayer.push({
  event: 'view_item',
  ecommerce: { /* new data */ }
});

Issue: Numbers as Strings

// Problem: Price is a string
dataLayer.push({
  ecommerce: {
    value: '29.99'  // String - causes issues in GA4
  }
});

// Solution: Ensure numbers are numbers
dataLayer.push({
  ecommerce: {
    value: parseFloat('29.99')  // Number: 29.99
  }
});

Data Layer Best Practices

Naming Conventions

Use consistent, descriptive names:

// ✅ Good - clear, consistent
dataLayer.push({
  productId: 'SKU-123',
  productName: 'Blue Widget',
  productPrice: 29.99,
  productCategory: 'Widgets'
});

// ❌ Bad - inconsistent, unclear
dataLayer.push({
  id: 'SKU-123',
  name: 'Blue Widget',
  prod_price: 29.99,
  cat: 'Widgets'
});

Initialization

Always initialize before GTM loads:

<script>
  window.dataLayer = window.dataLayer || [];

  // Push page data immediately
  dataLayer.push({
    pageType: 'home',
    siteName: 'Example Store'
  });
</script>

<!-- GTM loads after -->
<script>(function(w,d,s,l,i){...})(window,document,'script','dataLayer','GTM-XXXXX');</script>

Event Naming

Use action-oriented event names:

// ✅ Good - action verbs
'add_to_cart'
'begin_checkout'
'purchase'
'sign_up'
'generate_lead'
'view_item'

// ❌ Bad - vague or noun-based
'cart'
'checkout_page'
'done'
'user'

Data Types

Ensure correct data types:

dataLayer.push({
  // Strings
  productId: 'SKU-123',       // ✅ String
  productName: 'Widget',      // ✅ String

  // Numbers (no quotes!)
  productPrice: 29.99,        // ✅ Number
  quantity: 2,                // ✅ Number

  // Booleans
  inStock: true,              // ✅ Boolean

  // Arrays for items
  items: [{ ... }]            // ✅ Array
});

// ❌ Wrong types
dataLayer.push({
  productPrice: '29.99',      // ❌ String instead of number
  quantity: '2',              // ❌ String instead of number
  inStock: 'true'             // ❌ String instead of boolean
});

Platform-Specific Examples

Shopify

Shopify provides some data layer data automatically. Add custom tracking:

{% if template contains 'product' %}
<script>
  window.dataLayer = window.dataLayer || [];
  dataLayer.push({ ecommerce: null });
  dataLayer.push({
    event: 'view_item',
    ecommerce: {
      currency: '{{ shop.currency }}',
      value: {{ product.price | money_without_currency | remove: ',' }},
      items: [{
        item_id: '{{ product.variants.first.sku | default: product.id }}',
        item_name: '{{ product.title | escape }}',
        item_brand: '{{ product.vendor | escape }}',
        item_category: '{{ product.type | escape }}',
        price: {{ product.price | money_without_currency | remove: ',' }},
        quantity: 1
      }]
    }
  });
</script>
{% endif %}

WordPress/WooCommerce

// In functions.php or custom plugin
add_action('wp_head', 'custom_datalayer', 1);
function custom_datalayer() {
  global $product;
  ?>
  <script>
    window.dataLayer = window.dataLayer || [];
    <?php if (is_product() && $product): ?>
    dataLayer.push({ ecommerce: null });
    dataLayer.push({
      event: 'view_item',
      ecommerce: {
        currency: '<?= get_woocommerce_currency() ?>',
        value: <?= $product->get_price() ?>,
        items: [{
          item_id: '<?= $product->get_sku() ?>',
          item_name: '<?= esc_js($product->get_name()) ?>',
          price: <?= $product->get_price() ?>,
          quantity: 1
        }]
      }
    });
    <?php endif; ?>
  </script>
  <?php
}
// SYS.FOOTER