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 |
Recommended Event Strategy
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:
- Navigate to Events > Create Event
- Select From Autocapture
- Define targeting criteria:
- Element text contains "Add to Cart"
- Class contains "checkout-button"
- URL path contains "/products/"
- Name the event: "Product Added to Cart"
- 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 CartOrder CompletedSubscription UpgradedSearch PerformedFilter 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
- Open Heap dashboard
- Navigate to Live View
- Perform the tracked action on your site
- Verify event appears with correct name and properties
- 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
- Blend autocapture and custom events: Use autocapture for exploration, custom events for KPIs
- Name events clearly: Use
[Object] [Past Tense Verb]pattern - Include critical properties: Revenue, IDs, and context for all conversion events
- Validate data types: Numbers as numbers, booleans as booleans
- Document event catalog: Maintain central documentation of all events and properties
- Test before deployment: Verify events in Heap Live View
- Use stable selectors: Add
data-heap-*attributes for critical elements - Deduplicate events: Prevent double-firing with idempotency keys
- Track errors: Monitor failed transactions and validation errors
- Version your events: Track schema changes over time for historical analysis