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:
- User visits your website
- Browser loads Fathom script
- Script automatically tracks pageview
- Goals tracked via
fathom.trackGoal() - 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:
- User completes action (e.g., purchase)
- Your backend processes the action
- Backend sends event to Fathom API
- 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')
Hybrid Approach (Recommended)
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:
- Open browser DevTools
- Go to Network tab
- Trigger event
- Look for POST to
/api/event - Verify 200 OK response
Server-Side:
- Add logging to tracking function
- Trigger event from backend
- Check logs for success/failure
- 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: