Skip to content

Data Layer and GTM

The Data Layer is a structured JavaScript object that acts as the single source of truth for all tracking on a site. With Google Tag Manager, server-side GTM, and Consent Mode, it forms a tracking architecture that meets modern privacy and performance requirements.

What the Data Layer Is

Concept

The Data Layer is a JavaScript array holding structured information about the page, user, and interactions. It separates data from tracking logic.

window.dataLayer = window.dataLayer || [];
dataLayer.push({
  'event': 'pageview',
  'page': {
    'type': 'product',
    'category': 'electronics',
    'subcategory': 'smartphones'
  },
  'user': {
    'status': 'logged_in',
    'type': 'returning',
    'loyaltyTier': 'gold'
  }
});

Why Use It

Standardization. One data structure for all tracking systems. Every tag reads from a single source.

Independence from site changes. Update the Data Layer once, not every tag.

Performance. Data is collected once and reused, instead of every tag parsing the DOM.

How It Works

  1. Initialization: Created on page load
  2. Population: Data added via push
  3. Listening: GTM watches for changes
  4. Processing: Tags fire based on data

Architecture

Structure

A solid Data Layer structure scales:

// Basic structure for e-commerce
dataLayer.push({
  'event': 'view_item',
  'ecommerce': {
    'currency': 'USD',
    'value': 159.99,
    'items': [{
      'item_id': 'SKU_12345',
      'item_name': 'Smartphone XYZ Pro',
      'item_category': 'Electronics',
      'item_category2': 'Smartphones',
      'item_category3': 'Android',
      'item_brand': 'XYZ',
      'price': 159.99,
      'quantity': 1,
      'item_variant': '128GB_Black'
    }]
  },
  'page_data': {
    'page_type': 'product_detail',
    'page_category': 'electronics/smartphones',
    'page_subcategory': 'android'
  },
  'user_data': {
    'user_id': 'USER_789456',
    'user_type': 'returning',
    'customer_lifetime_value': 4500,
    'loyalty_status': 'gold',
    'logged_in': true
  }
});

Events and Variables

Events signal user actions:

// Custom events
dataLayer.push({
  'event': 'custom_engagement',
  'engagement_type': 'video_milestone',
  'video_title': 'Product Demo 2024',
  'milestone_percent': 50
});

Standard events:

  • gtm.js - GTM loaded
  • gtm.dom - DOM ready
  • gtm.load - page loaded

Variables store data for tags:

// Static variables
dataLayer.push({
  'site_version': '2.4.1',
  'environment': 'production',
  'ab_test_variant': 'variant_b'
});

// Dynamic variables
dataLayer.push({
  'cart_value': calculateCartTotal(),
  'user_segment': getUserSegment(),
  'page_load_time': performance.now()
});

Nested structures for detail:

// Nested objects
dataLayer.push({
  'transaction': {
    'id': 'TRX_123456',
    'revenue': 259.99,
    'tax': 43.33,
    'shipping': 5.00,
    'coupon': 'SUMMER20',
    'payment': {
      'method': 'credit_card',
      'provider': 'stripe',
      'installments': 3
    }
  }
});

Lifecycle

graph TD
    A[Page Load] --> B[dataLayer Initialization]
    B --> C[Initial Data]
    C --> D[GTM Container Loaded]
    D --> E[User Events]
    E --> F[Push to dataLayer]
    F --> G[GTM Processes]
    G --> H[Tags Fire]
    H --> I[Data Sent]

Google Tag Manager

Components

Container. Holds all configuration for a site.

<!-- GTM Container Snippet -->
<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-XXXXXX');</script>

Data Layer Variables in GTM:

Variable TypeExample PathUse
Data Layer Variableecommerce.valueTransaction value
Data Layer Variableuser_data.user_idUser ID
JavaScript Variablewindow.location.pathnamePage URL
Custom JavaScriptfunction()Calculated values

Advanced Triggers

Composite conditions:

// Trigger for VIP users with high cart value
// Conditions in GTM:
// Event equals 'add_to_cart'
// AND user_data.loyalty_status equals 'gold'
// AND ecommerce.value greater than 1000

dataLayer.push({
  'event': 'add_to_cart',
  'ecommerce': {
    'value': 1500
  },
  'user_data': {
    'loyalty_status': 'gold'
  }
});

Sequential triggers:

Funnel Tracking

Triggers can fire only after a sequence:

  1. User viewed product
  2. Added to cart within 5 minutes
  3. Started checkout

Useful for tracking quality conversions.

Debugging

Preview Mode:

GTM Preview Mode tests changes before publishing:

// Adding debug information to dataLayer
if (document.location.search.includes('gtm_debug')) {
  dataLayer.push({
    'debug_mode': true,
    'timestamp': new Date().toISOString(),
    'page_render_time': performance.now()
  });
}

Validation:

// Validation function before pushing to dataLayer
function validateAndPush(data) {
  // Check required fields
  const required = ['event', 'ecommerce'];
  const hasRequired = required.every(field => data.hasOwnProperty(field));

  if (!hasRequired) {
    console.error('Missing required fields:', required);
    return false;
  }

  // Check data types
  if (data.ecommerce && typeof data.ecommerce.value !== 'number') {
    console.warn('Invalid value type, converting...');
    data.ecommerce.value = parseFloat(data.ecommerce.value);
  }

  dataLayer.push(data);
  return true;
}

Server-side GTM (sGTM)

Architecture

Server-side GTM moves tag processing to the server. More control, more security.

graph LR
    A[Browser] --> B[Client GTM]
    B --> C[Server Container]
    C --> D[Tag Processing]
    D --> E[Google Analytics]
    D --> F[Facebook]
    D --> G[Custom Endpoints]

    style C fill:#f9f,stroke:#333,stroke-width:4px

Benefits

Bypasses blockers. sGTM runs on your domain. Requests look like normal traffic.

// Client side sends to your domain
fetch('https://analytics.example.com/collect', {
  method: 'POST',
  body: JSON.stringify({
    event: 'purchase',
    transaction_id: 'TRX_123',
    value: 599.99
  })
});

// Server processes and routes to analytics systems

Data enrichment. The server adds info the client doesn't have:

// Server-side enrichment
const enrichedData = {
  ...clientData,
  server_data: {
    user_lifetime_value: getUserLTV(clientData.user_id),
    weather: await getWeatherData(clientData.geo),
    inventory_status: checkInventory(clientData.product_id),
    fraud_score: calculateFraudScore(clientData)
  }
};

Data control:

// Remove personal data before sending
function sanitizeData(data) {
  const piiFields = ['email', 'phone', 'ssn', 'credit_card'];

  piiFields.forEach(field => {
    if (data[field]) {
      data[field] = hashValue(data[field]);
    }
  });

  return data;
}
// Check data correctness
function validateServerData(data) {
  // Bot check
  if (isBot(data.user_agent)) {
    return null;
  }

  // Duplicate check
  if (isDuplicate(data.event_id)) {
    return null;
  }

  return data;
}

Setting Up the Server Container

Deployment options:

PlatformProsCons
Google CloudAuto-scaling, simple setupCost at high load
AWSFull control, service integrationComplex setup
Self-hostedMaximum control, fixed costNeeds DevOps

Transport configuration:

// Configure client to send to sGTM
gtag('config', 'G-XXXXXXXXXX', {
  'transport_url': 'https://gtm.example.com',
  'first_party_collection': true
});

Event Processing

Client templates:

// Example Client Template for processing incoming requests
const requestPath = getRequestPath();
const requestBody = getRequestBody();

if (requestPath === '/collect') {
  // Parse GA4 data
  const measurementId = requestBody.measurement_id;
  const events = requestBody.events;

  // Enrichment
  events.forEach(event => {
    event.server_timestamp = Date.now();
    event.ip_country = getGeoFromIP(getRemoteAddress());
  });

  // Pass to tags
  runContainer(events, {client: 'ga4'});
}

Tag templates:

// Server-side tag for sending to custom endpoint
const https = require('https');
const JSON = require('JSON');

const eventData = getAllEventData();
const enrichedData = {
  ...eventData,
  server_processing_time: Date.now() - eventData.timestamp,
  server_version: '1.0.0'
};

https.post('https://api.example.com/events', {
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer ' + getSecret('api_key')
  },
  body: JSON.stringify(enrichedData)
});

Basics

Google Consent Mode adapts tag behavior based on user consent:

// Initialize Consent Mode
gtag('consent', 'default', {
  'ad_storage': 'denied',
  'analytics_storage': 'denied',
  'ad_user_data': 'denied',
  'ad_personalization': 'denied',
  'functionality_storage': 'granted',
  'security_storage': 'granted'
});

Categories

CategoryPurposeTracking Impact
analytics_storageAnalytics cookiesGA4 events, sessions
ad_storageAdvertising cookiesRemarketing, conversions
ad_user_dataUser data for adsAudiences, personalization
ad_personalizationAd personalizationTargeting, recommendations
functionality_storageFunctional cookiesSettings, language
security_storageSecurityAuthentication, protection

CMP Integration

// Handler for popular CMPs
window.addEventListener('consent_updated', function(e) {
  const consentState = e.detail;

  gtag('consent', 'update', {
    'analytics_storage': consentState.analytics ? 'granted' : 'denied',
    'ad_storage': consentState.marketing ? 'granted' : 'denied',
    'ad_user_data': consentState.marketing ? 'granted' : 'denied',
    'ad_personalization': consentState.marketing ? 'granted' : 'denied'
  });

  // Update dataLayer
  dataLayer.push({
    'event': 'consent_update',
    'consent_state': consentState
  });
});

Adaptive Tracking

Behavior without cookie consent:

// Cookieless tracking without consent
if (consentState.analytics_storage === 'denied') {
  // Use signals without cookies
  gtag('event', 'page_view', {
    'send_page_view': false,
    'client_id': generateSessionId(), // Temporary session ID
    'engagement_time_msec': 100
  });
}

Consent Mode v2 Requirements

Since March 2024, Google requires Consent Mode v2 for:

  • Remarketing in EEA
  • Personalized advertising
  • Enhanced conversions

Required parameters: - ad_user_data - ad_personalization

Conversion modeling:

With Consent Mode, Google uses ML to recover data:

graph LR
    A[Users with consent<br/>30%] --> B[Full data]
    C[Users without consent<br/>70%] --> D[Cookieless signals]
    B --> E[ML model]
    D --> E
    E --> F[Modeled conversions]

Modeling configuration:

// Enable enhanced modeling
gtag('config', 'G-XXXXXXXXXX', {
  'ads_data_redaction': true,
  'url_passthrough': true,
  'allow_ad_personalization_signals': false,
  'conversion_linker': true
});

Implementation Patterns

Enhanced E-commerce

Full purchase funnel:

// 1. View item list
dataLayer.push({ecommerce: null}); // Clear
dataLayer.push({
  'event': 'view_item_list',
  'ecommerce': {
    'item_list_id': 'category_smartphones',
    'item_list_name': 'Smartphones',
    'items': products.map((product, index) => ({
      'item_id': product.sku,
      'item_name': product.name,
      'price': product.price,
      'index': index,
      'item_category': 'Electronics',
      'item_list_name': 'Search Results',
      'quantity': 1
    }))
  }
});

// 2. Click on item
dataLayer.push({
  'event': 'select_item',
  'ecommerce': {
    'item_list_id': 'related_products',
    'item_list_name': 'Related Products',
    'items': [{
      'item_id': 'SKU_12345',
      'item_name': 'Smartphone XYZ',
      'index': 0,
      'price': 159.99
    }]
  }
});

// 3. Add to cart
dataLayer.push({
  'event': 'add_to_cart',
  'ecommerce': {
    'currency': 'USD',
    'value': 159.99,
    'items': [itemObject]
  }
});

// 4. Purchase
dataLayer.push({
  'event': 'purchase',
  'ecommerce': {
    'transaction_id': '12345',
    'value': 159.99,
    'tax': 26.67,
    'shipping': 3.00,
    'currency': 'USD',
    'coupon': 'SUMMER_SALE',
    'items': [itemObjects]
  }
});

SPA and Dynamic Sites

Virtual pageviews:

// For Single Page Applications
function trackVirtualPageview(route) {
  dataLayer.push({
    'event': 'virtual_pageview',
    'page_path': route.path,
    'page_title': route.title,
    'page_location': window.location.href,
    'page_data': {
      'virtual': true,
      'spa_route': route.name
    }
  });
}

// Router integration
router.afterEach((to, from) => {
  trackVirtualPageview(to);
});

Cross-domain Tracking

Multi-domain configuration:

// Data Layer for cross-domain
dataLayer.push({
  'event': 'cross_domain_config',
  'cross_domain_sites': [
    'example.com',
    'shop.example.com',
    'checkout.payment-provider.com'
  ],
  'linker': {
    'domains': ['example.com', 'payment-provider.com'],
    'decorate_forms': true,
    'accept_incoming': true
  }
});

Monitoring

Performance

Measuring impact:

// Track GTM load time
performance.mark('gtm_start');

// After GTM loads
performance.mark('gtm_end');
performance.measure('gtm_load', 'gtm_start', 'gtm_end');

const gtmPerf = performance.getEntriesByName('gtm_load')[0];
dataLayer.push({
  'event': 'performance_metrics',
  'gtm_load_time': gtmPerf.duration,
  'tags_fired': dataLayer.filter(d => d.event).length
});

Data Quality

Automatic validation:

// Middleware for data quality checking
class DataLayerValidator {
  constructor(rules) {
    this.rules = rules;
    this.errors = [];
  }

  validate(data) {
    // Check required fields
    this.rules.required.forEach(field => {
      if (!this.getNestedValue(data, field)) {
        this.errors.push(`Missing required field: ${field}`);
      }
    });

    // Check types
    Object.entries(this.rules.types).forEach(([field, type]) => {
      const value = this.getNestedValue(data, field);
      if (value && typeof value !== type) {
        this.errors.push(`Invalid type for ${field}: expected ${type}`);
      }
    });

    return this.errors.length === 0;
  }

  getNestedValue(obj, path) {
    return path.split('.').reduce((acc, part) => acc?.[part], obj);
  }
}

// Usage
const validator = new DataLayerValidator({
  required: ['event', 'ecommerce.value'],
  types: {
    'ecommerce.value': 'number',
    'user_data.user_id': 'string'
  }
});

What's Next

Server-first

The industry is moving server-side first. The client sends minimal data, the server handles processing. Better control, better security, better privacy.

Event Streaming

Batch processing is giving way to real-time streaming. Instant reaction, more complex personalization.

Privacy Tech

Differential Privacy and Federated Learning enable analytics without compromising individual users.

Statable supports modern Data Layer and tag management. Flexible architecture, both client-side and server-side processing, automatic consent management, and validation. Visual Data Layer builder with code generation is on the roadmap.

About AI participation in writing articles

This article, like many others on our site, was created, written and proofread by a team of developers. Of course, not without the participation of AI assistants. We don't hide this and believe that modern systems are already quite good at handling simple tasks and, relatively speaking, writing an article about Viewport yourself is quite strange. It won't come out significantly better and will take a lot of time. But providing basic understanding to beginner webmasters is necessary. Of course, after the article is written by assistants - there's always proofreading, and this is where not one or two people participate, and only after that the article is published.

Ready to Build a Modern Tracking Architecture?

Try Statable free. Privacy-compliant analytics from Data Layer to server-side processing, with full data control.


Ready to take control of your web analytics? Try Statable free for 30 days — no credit card required, full feature access, GDPR-compliant by default. Start your free trial or view a live demo.