Heap Data Layer Setup | OpsBlu Docs

Heap Data Layer Setup

Set up the data layer for Heap Analytics — structure events, define variables, and ensure consistent tracking data.

Overview

While Heap's autocapture automatically tracks user interactions without manual instrumentation, a well-structured data layer significantly enhances the quality and utility of your analytics. Unlike traditional tag management platforms that require a data layer for all tracking, Heap's data layer serves to enrich autocaptured events with business context, user attributes, and structured metadata.

The Heap data layer provides:

  • User properties for segmentation and analysis
  • Event properties to add context to autocaptured interactions
  • Ecommerce data for revenue tracking and product analytics
  • Custom metadata for filtering and grouping events

Data Layer Architecture

Heap vs Traditional Data Layers

Aspect Traditional (GTM) Heap
Purpose Required for all tracking Optional enrichment layer
Format window.dataLayer array User/event properties via API
Initialization Before tag manager Can be set anytime
Event triggering Push events to array Autocapture + manual track calls
Dependencies GTM must parse array Direct API calls

Implementation Philosophy

Heap's data layer follows a different pattern:

// Traditional data layer (GTM)
dataLayer.push({
  'event': 'button_click',
  'button_name': 'Subscribe',
  'user_id': '12345'
});

// Heap approach
// 1. Set user properties once
heap.addUserProperties({
  'user_id': '12345',
  'plan': 'premium',
  'signup_date': '2024-01-15'
});

// 2. Autocapture handles the click automatically
// 3. Optionally track custom events with properties
heap.track('Subscribe CTA Clicked', {
  'button_location': 'homepage_hero',
  'experiment_variant': 'A'
});

User Properties

Core User Properties

Set user properties immediately after authentication or profile updates:

// On login or user data load
heap.identify('user_12345');

heap.addUserProperties({
  // Identity
  'email': 'user@example.com',        // If permitted by privacy policy
  'account_id': 'acc_67890',

  // Subscription
  'plan': 'enterprise',
  'plan_tier': 'annual',
  'mrr': 499,
  'trial_status': 'converted',
  'subscription_start': '2024-01-15',

  // Profile
  'user_role': 'admin',
  'company_size': '50-200',
  'industry': 'SaaS',
  'signup_date': '2024-01-15',

  // Consent
  'marketing_consent': true,
  'analytics_consent': true,

  // Lifecycle
  'lifecycle_stage': 'customer',
  'account_age_days': 180,
  'ltv': 2995
});

User Property Naming Conventions

Category Example Properties Format
Identity user_id, account_id, email lowercase_underscore
Subscription plan, mrr, trial_status lowercase_underscore
Demographics company_size, industry, country lowercase_underscore
Behavior last_login, feature_usage_score lowercase_underscore
Consent analytics_consent, marketing_consent boolean

Updating User Properties

User properties persist until explicitly updated:

// Update a single property
heap.addUserProperties({
  'plan': 'enterprise'
});

// Update multiple properties atomically
heap.addUserProperties({
  'plan': 'enterprise',
  'mrr': 999,
  'upgrade_date': new Date().toISOString()
});

// Remove a property (set to null)
heap.addUserProperties({
  'trial_coupon': null
});

Dynamic Property Loading

Load user properties from your backend:

// Fetch user data from API
async function initializeHeapUserData() {
  try {
    const response = await fetch('/api/user/profile');
    const userData = await response.json();

    // Identify user
    heap.identify(userData.user_id);

    // Set user properties
    heap.addUserProperties({
      'plan': userData.subscription.plan,
      'mrr': userData.subscription.mrr,
      'account_id': userData.account_id,
      'signup_date': userData.created_at,
      'user_role': userData.role,
      'company_size': userData.company.size,
      'industry': userData.company.industry
    });
  } catch (error) {
    console.error('Failed to load Heap user data:', error);
  }
}

// Call after authentication
initializeHeapUserData();

Server-Side User Property Management

For sensitive or authoritative data, set user properties server-side:

// Node.js example using Heap Server-Side API
const fetch = require('node-fetch');

async function updateHeapUserProperties(userId, properties) {
  const response = await fetch(
    `https://heapanalytics.com/api/add_user_properties`,
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        app_id: process.env.HEAP_APP_ID,
        identity: userId,
        properties: properties
      })
    }
  );

  return response.json();
}

// Usage
await updateHeapUserProperties('user_12345', {
  'plan': 'enterprise',
  'mrr': 999,
  'account_status': 'active'
});

Event Properties

Event Properties vs User Properties

Property Type Scope Example Use Case
User Properties Persist across sessions User role, subscription plan
Event Properties Specific to one event Product SKU, transaction value

Adding Properties to Autocaptured Events

Enrich autocaptured events with contextual data:

// Set properties before user interaction
function enrichProductPage(productData) {
  // These properties will attach to the next autocaptured events
  heap.addEventProperties({
    'product_id': productData.sku,
    'product_name': productData.name,
    'product_price': productData.price,
    'product_category': productData.category,
    'in_stock': productData.inventory > 0
  });
}

// Call when product page loads
enrichProductPage(currentProduct);

// Now all clicks, scrolls, form submissions automatically include these properties

Event Properties for Custom Events

For manually tracked events:

heap.track('Product Added to Cart', {
  'product_id': 'SKU-12345',
  'product_name': 'Wireless Headphones',
  'product_price': 79.99,
  'quantity': 1,
  'cart_total': 142.97,
  'currency': 'USD'
});

Event Property Naming

Category Example Properties Type
Product product_id, product_name, product_price string, number
Transaction transaction_id, revenue, currency string, number
Navigation page_type, content_group, search_term string
Interaction button_text, link_destination, form_id string
Experiment variant, experiment_id, treatment_group string

Ecommerce Data Layer

Product Impressions

Track when products are viewed:

// Product detail page
heap.track('Product Viewed', {
  'product_id': 'SKU-12345',
  'product_name': 'Wireless Headphones',
  'product_brand': 'AudioTech',
  'product_category': 'Electronics',
  'product_price': 79.99,
  'currency': 'USD',
  'in_stock': true
});

// Category/listing page
heap.track('Product List Viewed', {
  'list_id': 'electronics_audio',
  'list_name': 'Audio Equipment',
  'product_count': 24,
  'category': 'Electronics'
});

Add to Cart

heap.track('Product Added to Cart', {
  'product_id': 'SKU-12345',
  'product_name': 'Wireless Headphones',
  'product_price': 79.99,
  'quantity': 1,
  'cart_total': 142.97,
  'cart_item_count': 3,
  'currency': 'USD'
});

Checkout Events

// Begin checkout
heap.track('Checkout Started', {
  'cart_total': 142.97,
  'cart_item_count': 3,
  'currency': 'USD',
  'has_coupon': false
});

// Add shipping info
heap.track('Shipping Info Added', {
  'shipping_method': 'Standard',
  'shipping_cost': 5.99,
  'estimated_delivery': '2024-12-30'
});

// Add payment info
heap.track('Payment Info Added', {
  'payment_method': 'credit_card',
  'payment_provider': 'stripe'
});

Purchase Conversion

// Order confirmation
heap.track('Order Completed', {
  'transaction_id': 'ORD-98765',
  'revenue': 148.96,
  'tax': 8.00,
  'shipping': 5.99,
  'discount': 14.90,
  'currency': 'USD',
  'coupon_code': 'SAVE10',
  'item_count': 3,
  'payment_method': 'credit_card'
});

// Track individual items (optional, for detailed product analysis)
orderItems.forEach(item => {
  heap.track('Order Item', {
    'transaction_id': 'ORD-98765',
    'product_id': item.sku,
    'product_name': item.name,
    'product_price': item.price,
    'quantity': item.quantity
  });
});

Subscription Events

// Trial started
heap.track('Trial Started', {
  'plan': 'professional',
  'trial_duration_days': 14,
  'trial_end_date': '2024-12-30'
});

// Subscription created
heap.track('Subscription Created', {
  'plan': 'professional',
  'billing_cycle': 'annual',
  'mrr': 49,
  'annual_value': 588,
  'currency': 'USD'
});

// Subscription upgraded
heap.track('Subscription Upgraded', {
  'previous_plan': 'starter',
  'new_plan': 'professional',
  'previous_mrr': 19,
  'new_mrr': 49,
  'revenue_impact': 30
});

// Subscription cancelled
heap.track('Subscription Cancelled', {
  'plan': 'professional',
  'mrr': 49,
  'cancellation_reason': 'pricing',
  'account_age_days': 180
});

Page Metadata for SPAs

Virtual Pageview Tracking

For single-page applications, track route changes:

// React Router example
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

function HeapSpaTracker() {
  const location = useLocation();

  useEffect(() => {
    // Set page-level event properties
    heap.addEventProperties({
      'page_path': location.pathname,
      'page_title': document.title,
      'page_type': getPageType(location.pathname),
      'content_group': getContentGroup(location.pathname)
    });

    // Optional: Manually track pageview
    heap.track('Pageview', {
      'path': location.pathname,
      'title': document.title,
      'referrer': document.referrer
    });
  }, [location]);

  return null;
}

function getPageType(path) {
  if (path === '/') return 'home';
  if (path.startsWith('/products/')) return 'product';
  if (path.startsWith('/category/')) return 'category';
  if (path === '/cart') return 'cart';
  if (path.startsWith('/checkout')) return 'checkout';
  return 'other';
}

function getContentGroup(path) {
  const segments = path.split('/').filter(Boolean);
  return segments[0] || 'home';
}

Vue.js Integration

// main.js
import { createApp } from 'vue';
import router from './router';

const app = createApp(App);

router.afterEach((to, from) => {
  // Set page context
  heap.addEventProperties({
    'page_path': to.path,
    'page_name': to.name,
    'page_title': to.meta.title || document.title,
    'page_type': to.meta.pageType
  });
});

app.use(router).mount('#app');

Angular Integration

// app.component.ts
import { Component } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';
import { filter } from 'rxjs/operators';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html'
})
export class AppComponent {
  constructor(private router: Router) {
    this.router.events
      .pipe(filter(event => event instanceof NavigationEnd))
      .subscribe((event: NavigationEnd) => {
        if (window.heap) {
          heap.addEventProperties({
            'page_path': event.urlAfterRedirects,
            'page_title': document.title
          });
        }
      });
  }
}

Track user consent preferences:

// Set consent as user properties
heap.addUserProperties({
  'analytics_consent': true,
  'marketing_consent': false,
  'advertising_consent': false,
  'consent_timestamp': new Date().toISOString()
});

Conditional Data Collection

Only collect certain data based on consent:

function setHeapUserData(userData) {
  const consentState = getConsentState();

  // Always collect basic properties
  const properties = {
    'user_id': userData.id,
    'account_id': userData.account_id,
    'plan': userData.plan
  };

  // Only add PII if consent granted
  if (consentState.analytics_consent) {
    properties.email = userData.email;
    properties.company_name = userData.company.name;
  }

  // Only add marketing data if consent granted
  if (consentState.marketing_consent) {
    properties.utm_source = getUtmSource();
    properties.utm_campaign = getUtmCampaign();
  }

  heap.addUserProperties(properties);
}

Delayed Heap Initialization

Wait for consent before loading Heap:

// consent-manager.js
export function initializeHeapWithConsent() {
  const consent = getStoredConsent();

  if (consent.analytics === 'granted') {
    // Load Heap
    window.heap=window.heap||[],heap.load=function(e,t){
      window.heap.appid=e,window.heap.config=t=t||{};
      // ... heap snippet ...
    };
    heap.load("YOUR-ENVIRONMENT-ID");
  }
}

// Update when consent changes
window.addEventListener('consent_update', (event) => {
  if (event.detail.analytics === 'granted' && !window.heap) {
    initializeHeapWithConsent();
  }
});

Data Governance

Schema Documentation

Maintain a properties catalog:

# Heap Properties Catalog

## User Properties

| Property | Type | Description | Owner | Example |
|----------|------|-------------|-------|---------|
| plan | string | Subscription tier | Product Team | "enterprise" |
| mrr | number | Monthly recurring revenue | Finance Team | 499 |
| user_role | string | User's role | Engineering | "admin" |

## Event Properties

| Property | Type | Description | Events | Example |
|----------|------|-------------|--------|---------|
| product_id | string | Product SKU | Product events | "SKU-12345" |
| revenue | number | Transaction value | Order events | 148.96 |

Change Management Process

  1. Propose: Create proposal with property name, type, purpose
  2. Review: Analytics team reviews for conflicts and naming consistency
  3. Document: Add to properties catalog
  4. Implement: Deploy code changes
  5. Validate: Verify in Heap Live View
  6. Announce: Notify stakeholders of new property availability

Naming Standards

Standard Rule Example
Format lowercase_underscore product_price
Prefixes Category-based prefixes experiment_variant, utm_source
Boolean Positive phrasing is_active, has_subscription
Dates ISO 8601 format 2024-01-15T10:30:00Z
Currency Include _currency property revenue: 99.99, currency: 'USD'

Deprecated Properties

When deprecating properties:

// Transition period: support both old and new
heap.addUserProperties({
  'subscription_plan': userData.plan,      // NEW
  'plan_name': userData.plan               // DEPRECATED (remove 2024-12-31)
});

// After transition period, remove old property
heap.addUserProperties({
  'subscription_plan': userData.plan
});

Validation and Debugging

Browser Console Inspection

Check current user properties:

// View current user ID
console.log('Heap User ID:', heap.userId);

// View user identity
console.log('Heap Identity:', heap.identity);

// No direct API to view all properties, but can verify they're set
heap.addUserProperties({ 'test_property': 'test_value' });

Heap Live View

  1. Navigate to Heap dashboard
  2. Go to Live View
  3. Perform actions on your site
  4. Click on events to view properties
  5. Verify user properties and event properties appear correctly

Network Request Inspection

Monitor Heap API calls:

// Intercept and log Heap requests
const originalFetch = window.fetch;
window.fetch = function(...args) {
  if (args[0].includes('heapanalytics.com')) {
    console.log('Heap Request:', args);
  }
  return originalFetch.apply(this, args);
};

Property Validation Function

Create a validation helper:

function validateHeapProperties(properties, schema) {
  const errors = [];

  Object.keys(schema).forEach(key => {
    const value = properties[key];
    const expectedType = schema[key];

    if (value === undefined || value === null) {
      if (schema[key].required) {
        errors.push(`Missing required property: ${key}`);
      }
      return;
    }

    if (typeof value !== expectedType.type) {
      errors.push(`Property ${key} should be ${expectedType.type}, got ${typeof value}`);
    }
  });

  return errors;
}

// Usage
const productSchema = {
  product_id: { type: 'string', required: true },
  product_price: { type: 'number', required: true },
  product_name: { type: 'string', required: true }
};

const properties = {
  product_id: 'SKU-12345',
  product_price: 79.99,
  product_name: 'Wireless Headphones'
};

const errors = validateHeapProperties(properties, productSchema);
if (errors.length > 0) {
  console.error('Validation errors:', errors);
} else {
  heap.track('Product Viewed', properties);
}

Automated Testing

// Jest test example
describe('Heap data layer', () => {
  beforeEach(() => {
    window.heap = {
      identify: jest.fn(),
      addUserProperties: jest.fn(),
      track: jest.fn()
    };
  });

  it('sets user properties after login', () => {
    const userData = {
      id: 'user_12345',
      plan: 'enterprise',
      email: 'test@example.com'
    };

    initializeHeapUserData(userData);

    expect(heap.identify).toHaveBeenCalledWith('user_12345');
    expect(heap.addUserProperties).toHaveBeenCalledWith({
      plan: 'enterprise',
      email: 'test@example.com'
    });
  });

  it('tracks product events with correct properties', () => {
    const product = {
      sku: 'SKU-12345',
      name: 'Wireless Headphones',
      price: 79.99
    };

    trackProductView(product);

    expect(heap.track).toHaveBeenCalledWith('Product Viewed', {
      product_id: 'SKU-12345',
      product_name: 'Wireless Headphones',
      product_price: 79.99
    });
  });
});

Troubleshooting

Symptom Likely Cause Solution
User properties not appearing in Heap Properties set before heap.load() Ensure Heap is loaded before calling addUserProperties
Event properties missing Not set before event occurs Call addEventProperties before user interaction
Properties have wrong data type Sending strings instead of numbers Validate and convert types: parseInt(), parseFloat()
User identity switches unexpectedly Multiple heap.identify() calls Only call identify() once per user session
Properties not updating Using same values Check if values are actually changing
Revenue tracking incorrect Currency conversion issues Always send revenue in base units (cents) or specify currency
PII appearing in autocapture No redaction configured Enable redaction in Heap settings
Duplicate events firing Multiple Heap instances Verify Heap is loaded only once per page

Best Practices

  1. Set user properties early: Call addUserProperties immediately after authentication
  2. Use consistent naming: Follow lowercase_underscore convention
  3. Validate data types: Ensure numbers are numbers, not strings
  4. Document all properties: Maintain a central schema catalog
  5. Test in staging: Verify properties appear in Heap before production deployment
  6. Respect privacy: Only collect data permitted by consent
  7. Use server-side for sensitive data: Set authoritative properties via server API
  8. Clean up deprecated properties: Remove old properties after transition periods
  9. Monitor property cardinality: Avoid high-cardinality properties that create too many unique values
  10. Version your schema: Track schema changes over time for historical analysis