Heap Server-Side vs Client-Side | OpsBlu Docs

Heap Server-Side vs Client-Side

Server-side vs client-side tracking approaches for Heap. Covers implementation trade-offs, data accuracy, privacy compliance, ad blocker resilience, and.

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

  1. Use client-side for interactions: Let autocapture handle UI events
  2. Use server-side for revenue: Track purchases, subscriptions server-side for 100% reliability
  3. Maintain consistent identity: Use the same user ID format across client and server
  4. Mark event source: Include event_source property to distinguish client from server events
  5. Implement retry logic: Add exponential backoff for server-side tracking
  6. Don't block responses: Track server events asynchronously
  7. Deduplicate carefully: Choose one source per event to avoid double-counting
  8. Test both paths: Verify events appear in Heap Live View from both sources
  9. Monitor failed events: Log tracking failures for investigation
  10. 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);