Fathom Analytics Server-Side vs Client-Side Tracking | OpsBlu Docs

Fathom Analytics Server-Side vs Client-Side Tracking

Understanding when and how to use server-side vs client-side tracking with Fathom Analytics

Overview

Fathom Analytics supports both client-side (browser-based) and server-side (backend) tracking. Each approach has distinct advantages and use cases. Understanding when to use each method - or a hybrid of both - ensures reliable, accurate analytics.

This guide covers implementation patterns, trade-offs, and best practices for both approaches.

Client-Side Tracking (Default)

How It Works

Client-side tracking uses a JavaScript snippet loaded in the browser:

<script src="https://cdn.usefathom.com/script.js" data-site="ABCDEFGH" defer></script>

Process:

  1. User visits your website
  2. Browser loads Fathom script
  3. Script automatically tracks pageview
  4. Goals tracked via fathom.trackGoal()
  5. Data sent to Fathom servers from user's browser

Advantages

1. Automatic Pageview Tracking

<!-- Just add the script - pageviews tracked automatically -->
<script src="https://cdn.usefathom.com/script.js" data-site="ABCDEFGH" defer></script>

No additional code needed for basic pageview analytics.

2. Rich Browser Context Fathom automatically collects:

  • Referrer source
  • Browser type
  • Device type (desktop, mobile, tablet)
  • Operating system
  • Screen resolution (approximate)
  • Country (from IP, then discarded)

3. Simple Implementation

// Track goals with one line
fathom.trackGoal('SIGNUP01', 0);

4. No Server Load Analytics processing happens client-side and on Fathom's servers - your backend isn't involved.

5. Real-Time User Interaction Track user behavior as it happens:

// Scroll tracking
window.addEventListener('scroll', () => {
  if (scrollPercentage > 75) {
    fathom.trackGoal('SCROLL75', 0);
  }
});

// Video plays
video.addEventListener('play', () => {
  fathom.trackGoal('VIDEOPLY', 0);
});

Disadvantages

1. Ad Blockers Many ad blockers also block analytics scripts, including Fathom.

Mitigation:

  • Use custom domain (e.g., stats.yourdomain.com)
  • Implement server-side tracking for critical events

2. JavaScript Required Users with JavaScript disabled won't be tracked.

3. Page Must Load If user closes page before script loads, no tracking occurs.

4. Client-Side Manipulation Technically, users can prevent tracking or manipulate events (though rarely done).

When to Use Client-Side

Ideal for:

  • Standard website pageview tracking
  • User interaction events (clicks, scrolls, video plays)
  • Form submissions
  • Real-time engagement tracking
  • A/B test variant assignment
  • Single-page applications (SPAs)

Example: Blog or content site

<!-- Simple client-side setup -->
<script src="https://cdn.usefathom.com/script.js" data-site="ABCDEFGH" defer></script>

All pageviews and basic interactions tracked automatically.

Server-Side Tracking

How It Works

Server-side tracking uses Fathom's Events API to send data from your backend:

// Node.js example
await fetch('https://cdn.usefathom.com/api/event', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    site: 'ABCDEFGH',
    name: 'goal',
    goal: 'PURCHASE',
    value: 9999
  })
});

Process:

  1. User completes action (e.g., purchase)
  2. Your backend processes the action
  3. Backend sends event to Fathom API
  4. Data recorded on Fathom's servers

Advantages

1. Reliable for Critical Events Cannot be blocked by ad blockers or browser settings.

// Purchase event from backend - always tracked
await trackFathomGoal('PURCHASE', 9999);

2. No JavaScript Required Works even if user has JavaScript disabled or blocks scripts.

3. Secure Sensitive Data Keep revenue and business logic server-side:

// Server-side calculation ensures accuracy
const orderTotal = calculateOrderTotal(cart);
const cents = Math.round(orderTotal * 100);
await trackFathomGoal('PURCHASE', cents);

4. Backend Event Tracking Track events that don't have a frontend component:

  • Webhook processing
  • Scheduled jobs
  • API calls
  • Background tasks

5. Data Accuracy Backend knows ground truth - no client-side discrepancies.

Disadvantages

1. No Automatic Pageviews Must manually send pageview events:

await fetch('https://cdn.usefathom.com/api/event', {
  method: 'POST',
  body: JSON.stringify({
    site: 'ABCDEFGH',
    name: 'pageview',
    url: 'https://yourdomain.com/page'
  })
});

2. Limited Browser Context Server doesn't know:

  • Referrer (unless passed from client)
  • Browser type
  • Device type
  • Screen size

3. More Development Work Requires backend integration and testing.

4. Server Load API calls add (minimal) load to your backend.

When to Use Server-Side

Ideal for:

  • Critical conversion tracking (purchases, signups)
  • Revenue tracking
  • Subscription events
  • Payment processing
  • API-driven actions
  • Webhook events
  • Background processes

Example: Ecommerce purchase

// Stripe webhook - server-side
app.post('/webhook', async (req, res) => {
  const event = req.body;

  if (event.type === 'checkout.session.completed') {
    const session = event.data.object;

    // Track purchase server-side (reliable)
    await trackFathomGoal('PURCHASE', session.amount_total);
  }

  res.json({ received: true });
});

Server-Side Implementation

Node.js

const fetch = require('node-fetch');

async function trackFathomGoal(goalId, value = 0, url = null) {
  const payload = {
    site: 'ABCDEFGH',
    name: 'goal',
    goal: goalId,
    value: value
  };

  if (url) {
    payload.url = url;
  }

  try {
    const response = await fetch('https://cdn.usefathom.com/api/event', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'User-Agent': 'YourApp/1.0'
      },
      body: JSON.stringify(payload)
    });

    return response.ok;
  } catch (error) {
    console.error('Fathom tracking error:', error);
    return false;
  }
}

// Usage
await trackFathomGoal('PURCHASE', 9999, 'https://yourdomain.com/checkout/success');

Python

import requests

def track_fathom_goal(goal_id, value=0, url=None):
    payload = {
        'site': 'ABCDEFGH',
        'name': 'goal',
        'goal': goal_id,
        'value': value
    }

    if url:
        payload['url'] = url

    try:
        response = requests.post(
            'https://cdn.usefathom.com/api/event',
            json=payload,
            headers={'User-Agent': 'YourApp/1.0'}
        )
        return response.status_code == 200
    except Exception as e:
        print(f'Fathom tracking error: {e}')
        return False

# Usage
track_fathom_goal('PURCHASE', 9999, 'https://yourdomain.com/checkout/success')

PHP

<?php
function trackFathomGoal($goalId, $value = 0, $url = null) {
    $payload = [
        'site' => 'ABCDEFGH',
        'name' => 'goal',
        'goal' => $goalId,
        'value' => $value
    ];

    if ($url) {
        $payload['url'] = $url;
    }

    $ch = curl_init('https://cdn.usefathom.com/api/event');
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'Content-Type: application/json',
        'User-Agent: YourApp/1.0'
    ]);

    $result = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    return $httpCode === 200;
}

// Usage
trackFathomGoal('PURCHASE', 9999, 'https://yourdomain.com/checkout/success');
?>

Ruby

require 'net/http'
require 'json'

def track_fathom_goal(goal_id, value = 0, url = nil)
  payload = {
    site: 'ABCDEFGH',
    name: 'goal',
    goal: goal_id,
    value: value
  }

  payload[:url] = url if url

  uri = URI('https://cdn.usefathom.com/api/event')
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = true

  request = Net::HTTP::Post.new(uri.path, {
    'Content-Type' => 'application/json',
    'User-Agent' => 'YourApp/1.0'
  })
  request.body = payload.to_json

  response = http.request(request)
  response.code.to_i == 200
rescue StandardError => e
  puts "Fathom tracking error: #{e}"
  false
end

# Usage
track_fathom_goal('PURCHASE', 9999, 'https://yourdomain.com/checkout/success')

Combine client-side and server-side tracking for best results.

Strategy

Client-Side:

  • Pageviews
  • User interactions (clicks, scrolls, video plays)
  • Form submissions
  • Navigation events

Server-Side:

  • Purchases
  • Subscription events
  • Payment processing
  • Critical conversions
  • API actions

Implementation Example

Frontend (client-side):

<!-- Client-side script for pageviews -->
<script src="https://cdn.usefathom.com/script.js" data-site="ABCDEFGH" defer></script>

<script>
// Track user clicked checkout button
document.getElementById('checkout-btn').addEventListener('click', function() {
  fathom.trackGoal('CHECKOUT', 0);
});
</script>

Backend (server-side):

// After payment processed
app.post('/api/purchase', async (req, res) => {
  try {
    // Process payment...
    const orderTotal = req.body.total;

    // Track purchase server-side (reliable)
    const cents = Math.round(orderTotal * 100);
    await trackFathomGoal('PURCHASE', cents);

    res.json({ success: true });
  } catch (error) {
    res.status(500).json({ error: 'Payment failed' });
  }
});

Result:

  • User interactions tracked client-side
  • Critical conversions tracked server-side
  • Reliable data even if ad blockers interfere

Real-World Patterns

Pattern 1: SaaS Application

Client-Side:

// Track feature usage
fathom.trackGoal('FEAT_EXPORT', 0);
fathom.trackGoal('FEAT_REPORTS', 0);
fathom.trackGoal('FEAT_SHARE', 0);

Server-Side:

// Track subscription events
await trackFathomGoal('SUB_TRIAL', 0);          // Trial started
await trackFathomGoal('SUB_CONVERT', 2999);     // Trial converted
await trackFathomGoal('SUB_UPGRADE', 4999);     // Upgraded plan
await trackFathomGoal('SUB_CANCEL', 0);         // Cancelled

Pattern 2: Ecommerce

Client-Side:

// Product browsing
fathom.trackGoal('PRODVIEW', 0);    // Product viewed
fathom.trackGoal('ADDCART', 0);     // Added to cart
fathom.trackGoal('CHECKOUT', 0);    // Checkout started

Server-Side:

// Payment webhook
app.post('/stripe-webhook', async (req, res) => {
  const event = req.body;

  if (event.type === 'payment_intent.succeeded') {
    const amount = event.data.object.amount;
    await trackFathomGoal('PURCHASE', amount);
  }

  res.json({ received: true });
});

Pattern 3: API-Driven Product

Client-Side:

// Landing page tracking only
// (app is API-driven, minimal frontend)

Server-Side:

// API endpoint tracking
app.post('/api/signup', async (req, res) => {
  // Create account...
  await trackFathomGoal('SIGNUP', 0);

  res.json({ success: true });
});

app.post('/api/subscribe', async (req, res) => {
  // Process subscription...
  const plan = req.body.plan;
  const amount = getPlanPrice(plan);

  await trackFathomGoal(`SUB_${plan.toUpperCase()}`, amount);

  res.json({ success: true });
});

Decision Matrix

Use Client-Side When:

Scenario Reason
Tracking pageviews Automatic, no code needed
User interactions Real-time browser events
Content engagement Scroll, video, time on page
A/B testing Need browser context
Simple website Easy setup, minimal development

Use Server-Side When:

Scenario Reason
Critical conversions Ad blocker proof
Payment processing Secure, accurate revenue
Subscription events Backend-driven actions
Webhook events No frontend involved
API actions Server-side only

Use Hybrid When:

Scenario Reason
Ecommerce site Browsing (client) + purchases (server)
SaaS application Usage (client) + subscriptions (server)
High ad blocker audience Pageviews (client) + conversions (server)
Complex funnel Engagement (client) + conversions (server)

Best Practices

Error Handling

Client-Side:

function trackGoal(goalId, value = 0) {
  try {
    if (window.fathom) {
      window.fathom.trackGoal(goalId, value);
    }
  } catch (error) {
    console.error('Client tracking error:', error);
  }
}

Server-Side:

async function trackGoal(goalId, value = 0) {
  try {
    const response = await fetch('https://cdn.usefathom.com/api/event', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        site: 'ABCDEFGH',
        name: 'goal',
        goal: goalId,
        value: value
      })
    });

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }

    return true;
  } catch (error) {
    console.error('Server tracking error:', error);
    // Don't let tracking errors break app functionality
    return false;
  }
}

Testing

Client-Side:

  1. Open browser DevTools
  2. Go to Network tab
  3. Trigger event
  4. Look for POST to /api/event
  5. Verify 200 OK response

Server-Side:

  1. Add logging to tracking function
  2. Trigger event from backend
  3. Check logs for success/failure
  4. Verify in Fathom dashboard

Monitoring

Track tracking failures:

// Server-side
async function trackGoal(goalId, value = 0) {
  const success = await sendToFathom(goalId, value);

  if (!success) {
    // Log to your monitoring system
    logger.error('Fathom tracking failed', { goalId, value });

    // Could also retry or queue for later
  }

  return success;
}

Conclusion

Both client-side and server-side tracking have their place in a comprehensive analytics strategy:

Client-Side:

  • Simple, automatic pageview tracking
  • Rich user interaction data
  • Easy implementation

Server-Side:

  • Reliable critical event tracking
  • Ad blocker resistant
  • Secure revenue tracking

Hybrid Approach:

  • Best of both worlds
  • Most reliable data
  • Recommended for most applications

Choose the approach that fits your needs, or combine both for maximum reliability and insight.


Additional Resources: