Overview
Heap supports both client-side (browser JavaScript) and server-side event tracking, each with distinct strengths and use cases. A mature analytics implementation typically combines both approaches, using client-side tracking for user interactions and server-side tracking for authoritative business events.
Understanding the trade-offs between these approaches is critical for building a reliable, complete analytics system that maintains data integrity while providing comprehensive user insights.
Client-Side Tracking
What is Client-Side Tracking?
Client-side tracking runs JavaScript in the user's browser, capturing interactions as they happen on the web page. Heap's client-side SDK automatically tracks all user interactions through autocapture while also supporting manual event tracking.
Client-Side Implementation
// Basic Heap client-side setup
<script type="text/javascript">
window.heap=window.heap||[],heap.load=function(e,t){
window.heap.appid=e,window.heap.config=t=t||{};
var r=document.createElement("script");
r.type="text/javascript",r.async=!0,
r.src="https://cdn.heapanalytics.com/js/heap-"+e+".js";
var a=document.getElementsByTagName("script")[0];
a.parentNode.insertBefore(r,a);
// ... rest of snippet
};
heap.load("YOUR-ENVIRONMENT-ID");
</script>
// Track custom events
heap.track('Button Clicked', {
button_name: 'Sign Up',
page: 'Homepage'
});
// Identify users
heap.identify('user_12345');
// Add user properties
heap.addUserProperties({
plan: 'enterprise',
signup_date: '2024-01-15'
});
Advantages of Client-Side Tracking
| Advantage | Description |
|---|---|
| Autocapture | Automatically captures all clicks, pageviews, form interactions without code |
| Rich context | Access to DOM, page URL, referrer, screen size, device type |
| Session tracking | Built-in session management and user journey tracking |
| Low latency | Events tracked immediately as they occur |
| No backend changes | Can implement without server-side code changes |
| Retroactive analysis | Can analyze past autocaptured events without prior definition |
Limitations of Client-Side Tracking
| Limitation | Impact |
|---|---|
| Ad blockers | 25-40% of users may block analytics scripts |
| Browser crashes | Events may be lost if browser closes before sending |
| Client-side manipulation | Users can modify or spoof event data |
| Privacy restrictions | Safari ITP, Firefox ETP limit cookie persistence |
| Network issues | Offline users can't send events |
| Bot traffic | Difficult to filter automated/non-human traffic |
| Limited to web | Can't track backend processes, API calls, or system events |
When to Use Client-Side Tracking
Use client-side tracking for:
- User interactions: Clicks, scrolls, form submissions, navigation
- Pageviews: Page loads, SPA route changes
- Front-end behavior: Modal opens, tab switches, hover events
- A/B test assignment: Variant assignment and exposure tracking
- Feature usage: UI-driven feature interactions
- Search and filters: User-initiated search queries and filter applications
- Non-critical events: Events where some data loss is acceptable
Server-Side Tracking
What is Server-Side Tracking?
Server-side tracking sends events directly from your application backend to Heap's API, bypassing the browser entirely. This provides authoritative, reliable tracking for business-critical events.
Server-Side Implementation
Node.js Example
const fetch = require('node-fetch');
async function trackHeapEvent(userId, eventName, properties = {}) {
const response = await fetch('https://heapanalytics.com/api/track', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
app_id: process.env.HEAP_APP_ID,
identity: userId,
event: eventName,
properties: {
...properties,
timestamp: new Date().toISOString()
}
})
});
if (!response.ok) {
console.error('Heap tracking failed:', await response.text());
}
return response.json();
}
// Usage
await trackHeapEvent('user_12345', 'Order Completed', {
transaction_id: 'ORD-98765',
revenue: 148.96,
currency: 'USD',
item_count: 3
});
Python Example
import requests
import os
from datetime import datetime
def track_heap_event(user_id, event_name, properties=None):
"""Track an event to Heap server-side"""
if properties is None:
properties = {}
properties['timestamp'] = datetime.utcnow().isoformat()
payload = {
'app_id': os.environ['HEAP_APP_ID'],
'identity': user_id,
'event': event_name,
'properties': properties
}
response = requests.post(
'https://heapanalytics.com/api/track',
json=payload
)
if not response.ok:
print(f'Heap tracking failed: {response.text}')
return response.json()
# Usage
track_heap_event('user_12345', 'Subscription Created', {
'plan': 'enterprise',
'mrr': 499,
'billing_cycle': 'annual'
})
Ruby Example
require 'net/http'
require 'json'
require 'uri'
def track_heap_event(user_id, event_name, properties = {})
properties[:timestamp] = Time.now.utc.iso8601
uri = URI('https://heapanalytics.com/api/track')
request = Net::HTTP::Post.new(uri)
request.content_type = 'application/json'
request.body = {
app_id: ENV['HEAP_APP_ID'],
identity: user_id,
event: event_name,
properties: properties
}.to_json
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(request)
end
unless response.is_a?(Net::HTTPSuccess)
puts "Heap tracking failed: #{response.body}"
end
JSON.parse(response.body)
end
# Usage
track_heap_event('user_12345', 'Payment Failed', {
error_code: 'card_declined',
amount: 148.96,
payment_method: 'credit_card'
})
PHP Example
<?php
function trackHeapEvent($userId, $eventName, $properties = []) {
$properties['timestamp'] = date('c'); // ISO 8601 format
$data = [
'app_id' => $_ENV['HEAP_APP_ID'],
'identity' => $userId,
'event' => $eventName,
'properties' => $properties
];
$ch = curl_init('https://heapanalytics.com/api/track');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
error_log("Heap tracking failed: $response");
}
return json_decode($response, true);
}
// Usage
trackHeapEvent('user_12345', 'Trial Expired', [
'plan' => 'professional',
'trial_duration_days' => 14,
'converted' => false
]);
?>
Advantages of Server-Side Tracking
| Advantage | Description |
|---|---|
| 100% reliable | Not blocked by ad blockers or privacy extensions |
| Authoritative data | Events come from trusted backend, not modifiable by users |
| Complete coverage | Can track backend processes, cron jobs, API calls |
| Better privacy control | Sensitive data never exposed to client |
| Offline events | Track events that occur without user interaction |
| No browser dependency | Works for mobile apps, IoT devices, server processes |
| Guaranteed delivery | Retry logic ensures events are delivered |
Limitations of Server-Side Tracking
| Limitation | Impact |
|---|---|
| No autocapture | Must manually instrument every event |
| Missing context | No access to DOM, page URL (unless passed explicitly) |
| Development overhead | Requires backend code changes for every event |
| Identity management | Must manually map server user IDs to Heap identities |
| Increased latency | Events processed after backend operations complete |
| No session context | Must manually maintain session information |
When to Use Server-Side Tracking
Use server-side tracking for:
- Revenue events: Purchases, subscriptions, refunds, invoices
- Authoritative transactions: Payment processing, order completion
- Backend processes: Cron jobs, scheduled tasks, background workers
- API events: Webhook receipts, external integrations
- System-level events: Database migrations, cache invalidation
- Sensitive data: Events containing PII that shouldn't touch the client
- Mobile app events: Events from iOS/Android native apps
- IoT device events: Events from connected devices
- Critical conversions: Events where 100% reliability is required
Decision Matrix
Event Type Decision Framework
| Event Type | Client-Side | Server-Side | Both | Rationale |
|---|---|---|---|---|
| Page Views | ✓ | Autocapture handles automatically | ||
| Button Clicks | ✓ | UI interaction, autocapture available | ||
| Form Submissions | ✓ | Can be captured client-side | ||
| Add to Cart | ✓ | UI-driven, immediate feedback | ||
| Checkout Started | ✓ | UI interaction | ||
| Payment Attempted | ✓ | Client tracks attempt, server tracks result | ||
| Order Completed | ✓ | Authoritative, revenue-critical | ||
| Subscription Created | ✓ | Backend transaction | ||
| Feature Usage | ✓ | UI-driven interaction | ||
| Search Query | ✓ | User interaction | ||
| API Request | ✓ | Backend event | ||
| Cron Job Run | ✓ | Server-only event | ||
| Email Sent | ✓ | Backend process | ||
| User Login | ✓ | Client tracks attempt, server tracks success | ||
| Account Created | ✓ | Client tracks form submit, server tracks DB insert |
Identity Management and Coordination
Aligning Client and Server Identities
For client and server events to merge into a single user profile, use consistent identity:
Client-Side Identity
// After user logs in
const userId = getUserIdFromSession();
heap.identify(userId);
heap.addUserProperties({
email: user.email,
plan: user.subscription.plan
});
Server-Side Identity
// Use the same user ID
await trackHeapEvent(userId, 'Subscription Created', {
plan: 'enterprise',
mrr: 499
});
// Add user properties server-side
await addHeapUserProperties(userId, {
account_status: 'active',
payment_method: 'credit_card'
});
User Properties API
// Node.js example
async function addHeapUserProperties(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 addHeapUserProperties('user_12345', {
plan: 'enterprise',
mrr: 499,
account_status: 'active',
ltv: 5988
});
Anonymous to Identified User Flow
// Client-side: Track as anonymous
heap.track('Browsing Products', {
category: 'Electronics'
});
// User signs up
const anonymousId = heap.userId; // Capture before identify
// Server-side: Create account
const newUserId = await createAccount(formData);
// Client-side: Identify
heap.identify(newUserId);
// Server-side: Track account creation
await trackHeapEvent(newUserId, 'Account Created', {
signup_method: 'email',
anonymous_id: anonymousId // For manual reconciliation if needed
});
Coordination and Deduplication
Preventing Double-Counting
When the same event could be tracked both client and server-side:
Strategy 1: Choose One Source
// CLIENT-SIDE ONLY: Track checkout start
heap.track('Checkout Started', {
cart_total: 142.97,
source: 'client'
});
// SERVER-SIDE ONLY: Track order completion
await trackHeapEvent(userId, 'Order Completed', {
transaction_id: 'ORD-98765',
revenue: 148.96,
source: 'server'
});
Strategy 2: Mark Event Source
// Client tracks attempt
heap.track('Payment Attempted', {
amount: 148.96,
source: 'client',
attempt_id: 'attempt_abc123'
});
// Server tracks result
await trackHeapEvent(userId, 'Payment Succeeded', {
amount: 148.96,
source: 'server',
attempt_id: 'attempt_abc123'
});
Strategy 3: Different Event Names
// Client: User intent
heap.track('Checkout Button Clicked', {
cart_total: 142.97
});
// Server: Authoritative result
await trackHeapEvent(userId, 'Order Completed', {
transaction_id: 'ORD-98765',
revenue: 148.96
});
Event Property Standards
Always include source indicator:
// Client-side events
heap.track('Event Name', {
// ... event properties
event_source: 'client',
client_timestamp: new Date().toISOString()
});
// Server-side events
await trackHeapEvent(userId, 'Event Name', {
// ... event properties
event_source: 'server',
server_timestamp: new Date().toISOString()
});
Hybrid Implementation Patterns
Pattern 1: Client Initiates, Server Confirms
// CLIENT: User clicks "Purchase"
heap.track('Purchase Button Clicked', {
cart_total: 142.97,
attempt_id: generateAttemptId()
});
// SERVER: Process payment
app.post('/api/checkout', async (req, res) => {
const paymentResult = await processPayment(req.body);
if (paymentResult.success) {
await trackHeapEvent(req.user.id, 'Order Completed', {
transaction_id: paymentResult.transaction_id,
revenue: paymentResult.amount,
attempt_id: req.body.attempt_id
});
} else {
await trackHeapEvent(req.user.id, 'Payment Failed', {
error_code: paymentResult.error_code,
attempt_id: req.body.attempt_id
});
}
res.json(paymentResult);
});
Pattern 2: Client for UX, Server for Revenue
// CLIENT: Optimistic UI update
function addToCart(product) {
heap.track('Product Added to Cart (Client)', {
product_id: product.id,
product_price: product.price
});
// Show loading state...
fetch('/api/cart/add', {
method: 'POST',
body: JSON.stringify({ product_id: product.id })
});
}
// SERVER: Authoritative cart update
app.post('/api/cart/add', async (req, res) => {
const cartItem = await addToCart(req.user.id, req.body.product_id);
await trackHeapEvent(req.user.id, 'Product Added to Cart (Server)', {
product_id: cartItem.product_id,
product_price: cartItem.price,
cart_total: await getCartTotal(req.user.id)
});
res.json(cartItem);
});
Pattern 3: Server Enriches Client Events
// CLIENT: Track basic event
heap.track('Feature Used', {
feature_name: 'export_data',
export_format: 'csv'
});
// SERVER: Enrich with backend context
app.post('/api/export', async (req, res) => {
const exportJob = await createExport(req.user.id, req.body);
await trackHeapEvent(req.user.id, 'Export Completed', {
feature_name: 'export_data',
export_format: req.body.format,
row_count: exportJob.row_count,
file_size_mb: exportJob.file_size_mb,
processing_time_ms: exportJob.processing_time
});
res.json(exportJob);
});
Testing and Validation
Testing Client-Side Events
// Browser console
heap.track('Test Event', {
test_property: 'test_value',
timestamp: new Date().toISOString()
});
// Check network tab for request to heapanalytics.com
Testing Server-Side Events
// Node.js test
const assert = require('assert');
describe('Server-side tracking', () => {
it('sends order completion event', async () => {
const mockFetch = jest.fn().mockResolvedValue({
ok: true,
json: async () => ({ status: 'success' })
});
global.fetch = mockFetch;
await trackHeapEvent('user_12345', 'Order Completed', {
transaction_id: 'ORD-TEST',
revenue: 100.00
});
expect(mockFetch).toHaveBeenCalledWith(
'https://heapanalytics.com/api/track',
expect.objectContaining({
method: 'POST',
body: expect.stringContaining('Order Completed')
})
);
});
});
Validation Checklist
| Check | Client-Side | Server-Side |
|---|---|---|
| Events appear in Live View | ✓ | ✓ |
| Correct user identity | ✓ | ✓ |
| Properties have correct types | ✓ | ✓ |
| No duplicate events | ✓ | ✓ |
| Events merge to same profile | N/A | ✓ |
| Works without JavaScript | N/A | ✓ |
| Survives ad blockers | N/A | ✓ |
| Has retry logic | N/A | ✓ |
Error Handling and Retry Logic
Client-Side Error Handling
// Heap handles retries automatically
// But you can detect failures
heap.track('Important Event', properties);
// Check if Heap loaded
if (!window.heap) {
console.warn('Heap not loaded, event may be lost');
// Optional: Store event for later retry
storeEventForRetry('Important Event', properties);
}
Server-Side Error Handling
async function trackHeapEventWithRetry(userId, eventName, properties, retries = 3) {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
const response = await fetch('https://heapanalytics.com/api/track', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
app_id: process.env.HEAP_APP_ID,
identity: userId,
event: eventName,
properties: properties
})
});
if (response.ok) {
return await response.json();
}
console.error(`Heap tracking failed (attempt ${attempt}):`, await response.text());
} catch (error) {
console.error(`Heap tracking error (attempt ${attempt}):`, error);
}
// Exponential backoff
if (attempt < retries) {
await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000));
}
}
// All retries failed, log to dead letter queue
await logFailedEvent(userId, eventName, properties);
throw new Error('Failed to track Heap event after retries');
}
Performance Considerations
Client-Side Performance
// Non-blocking tracking
heap.track('Event Name', properties); // Asynchronous, doesn't block
// For critical navigation, add small delay
link.addEventListener('click', (e) => {
e.preventDefault();
heap.track('External Link Clicked', {
destination: link.href
});
setTimeout(() => {
window.location.href = link.href;
}, 100); // Small delay ensures event sends
});
Server-Side Performance
// Track asynchronously to avoid blocking
app.post('/api/order', async (req, res) => {
const order = await createOrder(req.body);
// Don't await tracking - fire and forget
trackHeapEvent(req.user.id, 'Order Completed', {
transaction_id: order.id,
revenue: order.total
}).catch(err => console.error('Heap tracking failed:', err));
// Respond immediately
res.json(order);
});
// For critical events, use background queue
import { queueJob } from './queue';
app.post('/api/subscription', async (req, res) => {
const subscription = await createSubscription(req.body);
// Queue for reliable background processing
await queueJob('track_heap_event', {
userId: req.user.id,
eventName: 'Subscription Created',
properties: {
plan: subscription.plan,
mrr: subscription.mrr
}
});
res.json(subscription);
});
Troubleshooting
| Symptom | Likely Cause | Solution |
|---|---|---|
| Client events missing | Ad blocker enabled | Use server-side for critical events |
| Server events not merging | Different user IDs | Ensure consistent identity between client/server |
| Duplicate events | Both client and server tracking same event | Choose single source or differentiate event names |
| User properties not syncing | Properties set on different identities | Use same user ID for client identify() and server identity |
| Server events delayed | Network latency or retry logic | Normal; events process within minutes |
| Events not in Live View | Wrong environment ID | Verify app_id matches your Heap environment |
| 401/403 errors | Invalid or missing app_id | Check environment variable configuration |
| Event properties missing | Not included in API payload | Verify properties object in request body |
Best Practices
- Use client-side for interactions: Let autocapture handle UI events
- Use server-side for revenue: Track purchases, subscriptions server-side for 100% reliability
- Maintain consistent identity: Use the same user ID format across client and server
- Mark event source: Include
event_sourceproperty to distinguish client from server events - Implement retry logic: Add exponential backoff for server-side tracking
- Don't block responses: Track server events asynchronously
- Deduplicate carefully: Choose one source per event to avoid double-counting
- Test both paths: Verify events appear in Heap Live View from both sources
- Monitor failed events: Log tracking failures for investigation
- Document your strategy: Maintain clear documentation of which events track where
Migration Strategies
Adding Server-Side to Client-Only Implementation
// Phase 1: Add server-side for revenue events only
await trackHeapEvent(userId, 'Order Completed', properties);
// Phase 2: Add server-side for all conversions
await trackHeapEvent(userId, 'Subscription Created', properties);
await trackHeapEvent(userId, 'Trial Started', properties);
// Phase 3: Deprecate client-side revenue tracking
// Remove client-side: heap.track('Order Completed', ...)
// Keep client-side: heap.track('Add to Cart', ...)
Adding Client-Side to Server-Only Implementation
// Phase 1: Add Heap client snippet
heap.load("ENVIRONMENT-ID");
// Phase 2: Identify users
heap.identify(userId);
// Phase 3: Let autocapture start collecting
// No code changes needed, autocapture runs automatically
// Phase 4: Create Defined Events from autocapture
// Use Heap UI to promote important autocaptured events
// Phase 5: Add manual tracking for key UI events
heap.track('Feature Used', properties);