Heap Event Tracking | OpsBlu Docs

Heap Event Tracking

How to implement custom event tracking in Heap. Covers event naming conventions, required and optional parameters, ecommerce events, debugging with.

Overview

Heap's event tracking combines two powerful approaches: automatic capture of all user interactions (autocapture) and manually defined custom events for business-critical actions. This hybrid model enables comprehensive analytics without extensive instrumentation while maintaining control over strategic metrics.

Understanding when to rely on autocapture versus when to implement custom tracking is crucial for building a scalable, maintainable analytics system.

Heap's Event Model

Autocapture vs Named Events

Aspect Autocapture Named Events
Setup Automatic, no code required Manual heap.track() calls
Coverage All clicks, form submissions, pageviews Only events you explicitly define
Flexibility Retroactive analysis Must be predefined
Stability Depends on DOM structure Independent of DOM changes
Use Case Exploration, discovery Business KPIs, conversions
Autocapture (80% of events)
├── General navigation clicks
├── Page views and scrolling
├── Form field interactions
└── Link clicks

Named Events (20% of events)
├── Purchase completed
├── Subscription created
├── Trial started
├── Key feature used
└── Account settings changed

Autocapture Events

What Heap Autocaptures

Heap automatically tracks:

  • Pageviews: Each page load or SPA route change
  • Clicks: All clicks on buttons, links, and interactive elements
  • Form submissions: All form submit events
  • Form field changes: Input, select, checkbox, radio button changes
  • Text selections: Text highlighted by users

Autocapture Event Properties

Each autocaptured event includes:

{
  // Element identification
  "target_text": "Add to Cart",
  "target_class": "btn btn-primary add-to-cart",
  "target_id": "product-cta",
  "href": "/cart/add?sku=12345",

  // Page context
  "path": "/products/wireless-headphones",
  "title": "Wireless Headphones - AudioTech",
  "referrer": "https://google.com",

  // Hierarchy
  "hierarchy": "body > main > section > button",
  "target_tag": "button",

  // Session data
  "session_id": "abc123...",
  "user_id": "user_12345",
  "timestamp": "2024-12-26T10:30:00Z"
}

Creating Defined Events from Autocapture

Promote important autocaptured events to Defined Events in the Heap UI:

  1. Navigate to Events > Create Event
  2. Select From Autocapture
  3. Define targeting criteria:
    • Element text contains "Add to Cart"
    • Class contains "checkout-button"
    • URL path contains "/products/"
  4. Name the event: "Product Added to Cart"
  5. Add filters to refine the definition

Example targeting rules:

Rule Type Operator Value Purpose
Target Text equals "Subscribe Now" Exact text match
Target Class contains "primary-cta" Flexible class matching
Page URL contains "/pricing" Page context
Target ID equals "checkout-submit" Unique identifier

Selector Stability Best Practices

Ensure autocapture selectors remain stable:

<!-- BAD: Unstable selectors -->
<button class="btn-1234 btn-primary">
  Subscribe
</button>

<!-- GOOD: Stable selectors -->
<button
  class="btn btn-primary"
  data-heap-event="subscribe-cta"
  id="subscribe-button"
>
  Subscribe
</button>

Add stable attributes for critical elements:

<button
  data-heap-id="product-add-to-cart"
  data-heap-context="product-page"
  data-product-id="SKU-12345"
>
  Add to Cart
</button>

Named Events (Custom Tracking)

When to Use Named Events

Create Named Events for:

  • Revenue events: Purchases, subscriptions, upgrades
  • Conversion milestones: Sign-ups, trials, onboarding completion
  • Feature adoption: First use of key features
  • User lifecycle: Account creation, profile completion
  • Error states: Failed transactions, validation errors
  • Server-side actions: Backend processes without UI interaction

Basic Event Tracking

// Simple event
heap.track('Trial Started');

// Event with properties
heap.track('Product Purchased', {
  product_id: 'SKU-12345',
  product_name: 'Wireless Headphones',
  price: 79.99,
  currency: 'USD',
  category: 'Electronics'
});

Event Naming Conventions

Convention Pattern Examples
Object + Action [Noun] [Past Tense Verb] Product Purchased, Trial Started
Module prefixes [Module]: [Action] Checkout: Payment Added, Settings: Password Changed
Progressive tense Use for ongoing actions Video Playing, File Uploading
Completed tense Use for finished actions Video Completed, File Uploaded

Good event names:

  • Product Added to Cart
  • Order Completed
  • Subscription Upgraded
  • Search Performed
  • Filter Applied

Bad event names:

  • click_button (too generic, use autocapture)
  • ProductPurchased (inconsistent casing)
  • user_did_checkout (verbose, unclear tense)
  • event123 (meaningless)

Core Event Catalog

User Lifecycle Events

// Account creation
heap.track('Account Created', {
  signup_method: 'google',
  user_type: 'individual',
  referral_source: 'homepage_cta'
});

// Email verification
heap.track('Email Verified', {
  verification_time_minutes: 5
});

// Profile completed
heap.track('Profile Completed', {
  completion_percentage: 100,
  fields_filled: 12
});

// First login
heap.track('First Login', {
  days_since_signup: 0
});

Subscription & Trial Events

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

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

// Subscription upgraded
heap.track('Subscription Upgraded', {
  previous_plan: 'starter',
  new_plan: 'professional',
  mrr_change: 30,
  upgrade_reason: 'feature_limit'
});

// Subscription downgraded
heap.track('Subscription Downgraded', {
  previous_plan: 'professional',
  new_plan: 'starter',
  mrr_change: -30,
  downgrade_reason: 'cost'
});

// Subscription cancelled
heap.track('Subscription Cancelled', {
  plan: 'professional',
  mrr_lost: 49,
  cancellation_reason: 'too_expensive',
  account_age_days: 180,
  ltv: 880
});

// Subscription reactivated
heap.track('Subscription Reactivated', {
  plan: 'professional',
  mrr_recovered: 49,
  days_since_cancellation: 15
});

Ecommerce Events

// Product viewed
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,
  view_source: 'search_results'
});

// Product added 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'
});

// Cart viewed
heap.track('Cart Viewed', {
  cart_total: 142.97,
  cart_item_count: 3,
  currency: 'USD'
});

// Checkout started
heap.track('Checkout Started', {
  cart_total: 142.97,
  cart_item_count: 3,
  currency: 'USD',
  checkout_id: 'checkout_abc123'
});

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

// Order completed
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',
  new_customer: false
});

// Order refunded
heap.track('Order Refunded', {
  transaction_id: 'ORD-98765',
  refund_amount: 148.96,
  refund_reason: 'customer_request',
  days_since_purchase: 5
});

Feature Engagement Events

// Feature used (first time)
heap.track('Feature First Used', {
  feature_name: 'advanced_filters',
  days_since_signup: 3,
  user_plan: 'professional'
});

// Feature used (ongoing)
heap.track('Feature Used', {
  feature_name: 'export_data',
  usage_count: 15,
  user_plan: 'enterprise'
});

// Search performed
heap.track('Search Performed', {
  search_term: 'wireless headphones',
  results_count: 24,
  search_source: 'header'
});

// Filter applied
heap.track('Filter Applied', {
  filter_type: 'price_range',
  filter_value: '50-100',
  results_count: 12
});

// Content shared
heap.track('Content Shared', {
  content_type: 'blog_post',
  content_id: 'post-123',
  share_destination: 'twitter'
});

// Download initiated
heap.track('Download Started', {
  file_type: 'pdf',
  file_name: 'product_guide.pdf',
  file_size_mb: 2.5
});

Error and Exception Events

// Form validation error
heap.track('Form Validation Error', {
  form_name: 'checkout',
  error_field: 'credit_card_number',
  error_message: 'Invalid card number'
});

// Payment failed
heap.track('Payment Failed', {
  error_code: 'card_declined',
  payment_method: 'credit_card',
  amount: 148.96,
  currency: 'USD'
});

// API error
heap.track('API Error', {
  endpoint: '/api/products',
  error_code: 500,
  error_message: 'Internal server error'
});

// Feature unavailable
heap.track('Feature Unavailable', {
  feature_name: 'advanced_analytics',
  user_plan: 'starter',
  upgrade_required: true
});

Event Properties Best Practices

Required vs Optional Properties

// Good: Include required properties
heap.track('Product Purchased', {
  // Required
  product_id: 'SKU-12345',
  revenue: 79.99,
  currency: 'USD',

  // Optional but valuable
  product_name: 'Wireless Headphones',
  category: 'Electronics',
  discount_applied: false
});

// Bad: Missing critical properties
heap.track('Product Purchased', {
  name: 'Headphones'  // Not enough context
});

Property Naming Standards

Category Naming Pattern Examples
IDs [object]_id product_id, user_id, transaction_id
Names [object]_name product_name, plan_name, feature_name
Counts [object]_count or [metric]_total item_count, cart_total
Amounts [metric] + currency property revenue, tax, shipping + currency: 'USD'
Booleans is_[state] or has_[feature] is_new_customer, has_discount
Dates [event]_date in ISO format trial_end_date: '2024-12-30'

Property Data Types

// Correct data types
heap.track('Order Completed', {
  transaction_id: 'ORD-98765',        // string
  revenue: 148.96,                     // number
  item_count: 3,                       // number (integer)
  new_customer: true,                  // boolean
  order_date: '2024-12-26T10:30:00Z', // string (ISO 8601)
  items: ['SKU-123', 'SKU-456']       // array
});

// Incorrect data types
heap.track('Order Completed', {
  revenue: '$148.96',                  // BAD: string instead of number
  item_count: '3',                     // BAD: string instead of number
  new_customer: 'yes',                 // BAD: string instead of boolean
  order_date: 1703589000000            // BAD: timestamp instead of ISO string
});

Deduplication Strategies

Preventing Duplicate Event Firing

// BAD: Can fire multiple times
document.getElementById('submit').addEventListener('click', () => {
  heap.track('Form Submitted');  // Fires on every click
});

// GOOD: Track once per submission
let formSubmitted = false;

document.getElementById('form').addEventListener('submit', (e) => {
  if (!formSubmitted) {
    heap.track('Form Submitted', {
      form_id: 'contact_form'
    });
    formSubmitted = true;
  }
});

Server-Side Event Deduplication

// Node.js example with idempotency
const processedEvents = new Set();

async function trackHeapEvent(eventName, properties) {
  // Create idempotency key
  const idempotencyKey = `${properties.transaction_id}_${eventName}`;

  // Check if already processed
  if (processedEvents.has(idempotencyKey)) {
    console.log('Event already tracked, skipping');
    return;
  }

  // Track event
  await heapClient.track({
    app_id: process.env.HEAP_APP_ID,
    identity: properties.user_id,
    event: eventName,
    properties: properties
  });

  // Mark as processed
  processedEvents.add(idempotencyKey);
}

// Usage
trackHeapEvent('Order Completed', {
  transaction_id: 'ORD-98765',
  user_id: 'user_12345',
  revenue: 148.96
});

Event Timing and Sequencing

Synchronous vs Asynchronous Tracking

// Synchronous: Track before navigation
document.getElementById('external-link').addEventListener('click', (e) => {
  e.preventDefault();

  // Track event
  heap.track('External Link Clicked', {
    destination: e.target.href
  });

  // Navigate after short delay
  setTimeout(() => {
    window.location.href = e.target.href;
  }, 100);
});

// Asynchronous: Track without blocking
async function submitForm(formData) {
  // Track in background
  heap.track('Form Submitted', {
    form_id: 'contact_form'
  });

  // Continue with form submission
  return await fetch('/api/submit', {
    method: 'POST',
    body: JSON.stringify(formData)
  });
}

Event Sequencing

Track related events in logical order:

// Checkout flow
async function completeCheckout(orderData) {
  // 1. Checkout started
  heap.track('Checkout Started', {
    cart_total: orderData.total,
    item_count: orderData.items.length
  });

  // 2. Process payment
  const paymentResult = await processPayment(orderData);

  if (paymentResult.success) {
    // 3. Payment succeeded
    heap.track('Payment Succeeded', {
      transaction_id: paymentResult.transaction_id,
      amount: orderData.total
    });

    // 4. Order completed
    heap.track('Order Completed', {
      transaction_id: paymentResult.transaction_id,
      revenue: orderData.total,
      currency: 'USD'
    });
  } else {
    // 3. Payment failed
    heap.track('Payment Failed', {
      error_code: paymentResult.error_code,
      amount: orderData.total
    });
  }
}

Framework Integrations

React Event Tracking

// Custom hook for event tracking
import { useCallback } from 'react';

export function useHeapTrack() {
  return useCallback((eventName, properties = {}) => {
    if (window.heap) {
      heap.track(eventName, properties);
    }
  }, []);
}

// Component usage
import { useHeapTrack } from './hooks/useHeapTrack';

function ProductCard({ product }) {
  const track = useHeapTrack();

  const handleAddToCart = () => {
    track('Product Added to Cart', {
      product_id: product.id,
      product_name: product.name,
      product_price: product.price
    });

    // Add to cart logic...
  };

  return (
    <button
      Add to Cart
    </button>
  );
}

Vue.js Event Tracking

// Vue plugin
export default {
  install(app) {
    app.config.globalProperties.$heap = {
      track(eventName, properties = {}) {
        if (window.heap) {
          heap.track(eventName, properties);
        }
      }
    };
  }
};

// Component usage
export default {
  methods: {
    addToCart(product) {
      this.$heap.track('Product Added to Cart', {
        product_id: product.id,
        product_name: product.name,
        product_price: product.price
      });

      // Add to cart logic...
    }
  }
};

Angular Event Tracking

// Heap service
import { Injectable } from '@angular/core';

declare global {
  interface Window {
    heap: any;
  }
}

@Injectable({
  providedIn: 'root'
})
export class HeapService {
  track(eventName: string, properties: Record<string, any> = {}): void {
    if (window.heap) {
      window.heap.track(eventName, properties);
    }
  }
}

// Component usage
import { Component } from '@angular/core';
import { HeapService } from './services/heap.service';

@Component({
  selector: 'app-product-card',
  templateUrl: './product-card.component.html'
})
export class ProductCardComponent {
  constructor(private heap: HeapService) {}

  addToCart(product: any): void {
    this.heap.track('Product Added to Cart', {
      product_id: product.id,
      product_name: product.name,
      product_price: product.price
    });

    // Add to cart logic...
  }
}

Validation and Testing

Browser Console Testing

// Test event tracking in console
heap.track('Test Event', {
  test_property: 'test_value',
  timestamp: new Date().toISOString()
});

// Verify event appears in network tab
// Filter for: heapanalytics.com/api/track

Heap Live View Validation

  1. Open Heap dashboard
  2. Navigate to Live View
  3. Perform the tracked action on your site
  4. Verify event appears with correct name and properties
  5. Check property data types and values

Automated Testing

// Jest test for event tracking
describe('Event tracking', () => {
  beforeEach(() => {
    window.heap = {
      track: jest.fn()
    };
  });

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

    trackProductPurchase(product);

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

  it('does not track if heap is not loaded', () => {
    window.heap = undefined;

    trackProductPurchase({ id: 'test' });

    // Should not throw error
  });
});

QA Checklist

Check Description Pass/Fail
Event fires on action Event appears in Live View when action performed
Event name correct Matches naming convention
Required properties present All critical properties included
Property data types correct Numbers are numbers, strings are strings
No duplicate events Single action fires event once
Works across browsers Chrome, Firefox, Safari, Edge
Works on mobile iOS and Android
Autocapture selectors stable DOM changes don't break tracking

Troubleshooting

Symptom Likely Cause Solution
Events not appearing in Live View Heap not loaded Verify heap.load() called before tracking
Duplicate events firing Multiple event listeners Implement deduplication logic
Event properties missing Properties undefined when event fires Check property values before tracking
Wrong data types Converting numbers to strings Use parseInt(), parseFloat()
Autocapture selector broken DOM structure changed Add stable data- attributes
Events firing on wrong pages Missing page context check Add URL/path validation
Server events not appearing Identity not set Call heap.identify() with user ID
Events delayed or batched Normal Heap behavior Events process within minutes

Best Practices

  1. Blend autocapture and custom events: Use autocapture for exploration, custom events for KPIs
  2. Name events clearly: Use [Object] [Past Tense Verb] pattern
  3. Include critical properties: Revenue, IDs, and context for all conversion events
  4. Validate data types: Numbers as numbers, booleans as booleans
  5. Document event catalog: Maintain central documentation of all events and properties
  6. Test before deployment: Verify events in Heap Live View
  7. Use stable selectors: Add data-heap-* attributes for critical elements
  8. Deduplicate events: Prevent double-firing with idempotency keys
  9. Track errors: Monitor failed transactions and validation errors
  10. Version your events: Track schema changes over time for historical analysis