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
});
}
});
}
}
Consent Management Integration
Consent State Properties
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
- Propose: Create proposal with property name, type, purpose
- Review: Analytics team reviews for conflicts and naming consistency
- Document: Add to properties catalog
- Implement: Deploy code changes
- Validate: Verify in Heap Live View
- 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
- Navigate to Heap dashboard
- Go to Live View
- Perform actions on your site
- Click on events to view properties
- 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
- Set user properties early: Call
addUserPropertiesimmediately after authentication - Use consistent naming: Follow lowercase_underscore convention
- Validate data types: Ensure numbers are numbers, not strings
- Document all properties: Maintain a central schema catalog
- Test in staging: Verify properties appear in Heap before production deployment
- Respect privacy: Only collect data permitted by consent
- Use server-side for sensitive data: Set authoritative properties via server API
- Clean up deprecated properties: Remove old properties after transition periods
- Monitor property cardinality: Avoid high-cardinality properties that create too many unique values
- Version your schema: Track schema changes over time for historical analysis