Server-Side vs Client-Side Tracking in Umami
Umami supports both client-side and server-side tracking approaches, each with distinct advantages and use cases. While Umami is primarily designed for client-side tracking through its JavaScript library, server-side tracking via the API enables tracking in environments where JavaScript isn't available or for backend events that have no frontend representation.
Client-Side Tracking
Client-side tracking is the default and recommended approach for most websites and web applications.
Standard Implementation:
<script async src="https://umami.example.com/script.js" data-website-id="YOUR_WEBSITE_ID"></script>
How Client-Side Tracking Works
- Script Loading: Umami script loads asynchronously on the page
- Automatic Page Views: Page views are captured automatically when pages load
- Event Collection: Custom events fire from user interactions
- Data Transmission: Data is sent to Umami server via fetch/beacon API
- Session Management: Sessions tracked client-side without cookies
Automatic Data Collection:
// These are captured automatically with client-side tracking:
// - Page URL
// - Page title
// - Referrer
// - Screen resolution
// - Browser language
// - Device type (mobile/desktop/tablet)
// - Operating system
// - Browser name
Advantages of Client-Side Tracking
1. Ease of Implementation
<!-- Single script tag, no backend code required -->
<script async src="https://umami.example.com/script.js" data-website-id="abc123"></script>
2. Rich Context Automatically Captured
Client-side tracking captures browser and device information without manual configuration:
// All of this is automatic:
// - User's screen size
// - Browser and OS
// - Referrer URL
// - Page title
// - Current URL
3. Real-Time User Interactions
// Track user actions as they happen
umami.track('button_click', {
button_name: 'signup',
location: 'header'
});
4. No Server Load
Analytics tracking happens client-side, reducing load on your application servers.
5. Simple Event Tracking
<!-- Data attributes for no-code tracking -->
<button data-umami-event="cta_click">Sign Up</button>
Limitations of Client-Side Tracking
1. Ad Blockers
Many users have ad blockers that may prevent Umami script from loading:
// Check if Umami loaded
if (!window.umami) {
// Umami blocked or failed to load
console.warn('Analytics not available');
}
2. JavaScript Disabled
Small percentage of users browse with JavaScript disabled.
3. Single-Page Applications Require Extra Setup
// Must manually trigger page views on route changes
router.afterEach(() => {
if (window.umami) {
umami.pageView();
}
});
4. Can't Track Server-Only Events
Backend processes like cron jobs, API calls, or server-side business logic can't use client-side tracking.
Client-Side Best Practices
Async Loading:
<!-- Always use async to prevent blocking page load -->
<script async src="https://umami.example.com/script.js" data-website-id="abc123"></script>
Defensive Event Tracking:
// Always check if umami exists
function trackEvent(name, data) {
if (window.umami) {
umami.track(name, data);
}
}
SPA Page View Tracking:
// React Router example
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
function App() {
const location = useLocation();
useEffect(() => {
if (window.umami) {
umami.pageView();
}
}, [location.pathname]);
return <Routes>{/* ... */}</Routes>;
}
Server-Side Tracking
Server-side tracking sends data directly from your backend to Umami's API.
Basic API Request:
curl -X POST "https://umami.example.com/api/send" \
-H "Content-Type: application/json" \
-H "User-Agent: Mozilla/5.0" \
-d '{
"type": "event",
"payload": {
"website": "YOUR_WEBSITE_ID",
"url": "/api/checkout",
"name": "purchase_completed",
"data": {
"value": 99.99,
"currency": "USD"
}
}
}'
Server-Side Implementation Examples
Node.js:
const fetch = require('node-fetch');
async function trackServerEvent(eventName, eventData) {
try {
const response = await fetch('https://umami.example.com/api/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'User-Agent': 'Mozilla/5.0 (compatible; MyApp/1.0)'
},
body: JSON.stringify({
type: 'event',
payload: {
website: process.env.UMAMI_WEBSITE_ID,
url: '/api/purchase',
name: eventName,
data: eventData
}
})
});
if (!response.ok) {
console.error('Umami tracking failed:', response.statusText);
}
} catch (error) {
console.error('Error tracking event:', error);
}
}
// Usage
await trackServerEvent('subscription_created', {
plan: 'enterprise',
value: 1200,
billing_cycle: 'annual'
});
Python:
import requests
import json
def track_server_event(event_name, event_data):
payload = {
"type": "event",
"payload": {
"website": "YOUR_WEBSITE_ID",
"url": "/api/event",
"name": event_name,
"data": event_data
}
}
headers = {
"Content-Type": "application/json",
"User-Agent": "Mozilla/5.0 (compatible; MyApp/1.0)"
}
try:
response = requests.post(
"https://umami.example.com/api/send",
json=payload,
headers=headers,
timeout=5
)
response.raise_for_status()
except requests.exceptions.RequestException as e:
print(f"Error tracking event: {e}")
# Usage
track_server_event("data_export", {
"format": "CSV",
"row_count": 5000
})
PHP:
<?php
function trackServerEvent($eventName, $eventData) {
$payload = [
'type' => 'event',
'payload' => [
'website' => $_ENV['UMAMI_WEBSITE_ID'],
'url' => '/api/event',
'name' => $eventName,
'data' => $eventData
]
];
$ch = curl_init('https://umami.example.com/api/send');
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'User-Agent: Mozilla/5.0 (compatible; MyApp/1.0)'
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$result = curl_exec($ch);
curl_close($ch);
return $result;
}
// Usage
trackServerEvent('invoice_generated', [
'invoice_id' => 'INV-12345',
'amount' => 499.99
]);
?>
Advantages of Server-Side Tracking
1. Bypass Ad Blockers
Server-to-server communication can't be blocked by browser ad blockers.
2. Track Backend Events
// Track events that happen only on server
trackServerEvent('cron_job_completed', {
job_name: 'daily_report',
duration_ms: 1500,
records_processed: 10000
});
trackServerEvent('api_call_made', {
endpoint: '/v1/data/export',
response_time_ms: 250
});
3. Reliable Tracking
Server-side tracking ensures events are captured even if client disconnects or navigates away.
4. Sensitive Operations
Track server-side operations without exposing logic to client:
trackServerEvent('payment_processed', {
payment_gateway: 'stripe',
amount: 99.99,
currency: 'USD',
status: 'success'
// Don't send credit card info or other sensitive data
});
5. Accurate Conversion Tracking
Track conversions at the exact moment they're confirmed server-side:
// After successful payment processing
if (paymentSuccessful) {
await trackServerEvent('purchase_completed', {
order_id: order.id,
value: order.total,
items_count: order.items.length
});
}
Limitations of Server-Side Tracking
1. No Automatic Browser/Device Info
Must manually provide context that's automatic in client-side tracking.
2. More Complex Implementation
Requires backend code and error handling.
3. Server Load
Analytics requests add (minimal) load to your application servers.
4. No Automatic Page Views
Must manually trigger page views if needed.
Hybrid Approach (Recommended)
Combine both approaches for comprehensive tracking:
Client-Side for User Interactions:
// Track user behavior client-side
umami.track('add_to_cart', {
product_id: '123',
quantity: 2
});
Server-Side for Business Events:
// Track business events server-side
await trackServerEvent('order_fulfilled', {
order_id: orderId,
fulfillment_time_hours: 24,
shipping_method: 'express'
});
Hybrid Implementation Example
Client-Side (Frontend):
// Track user initiating checkout
document.getElementById('checkout-button').addEventListener('click', () => {
umami.track('checkout_initiated', {
cart_value: getCartTotal()
});
});
Server-Side (Backend):
// Track actual successful payment
app.post('/api/process-payment', async (req, res) => {
const payment = await processPayment(req.body);
if (payment.successful) {
// Server-side tracking for confirmed transaction
await trackServerEvent('payment_confirmed', {
amount: payment.amount,
payment_method: payment.method
});
}
res.json(payment);
});
Use Case Decision Matrix
| Scenario | Recommended Approach | Why |
|---|---|---|
| Page views | Client-side | Automatic, includes browser context |
| Button clicks | Client-side | Immediate user interaction feedback |
| Form submissions | Client-side | Track when form is submitted |
| Successful purchase | Server-side | Confirm after payment processing |
| API usage | Server-side | Backend-only event |
| Cron jobs | Server-side | No frontend component |
| Feature usage | Client-side | User interaction in UI |
| Data exports | Server-side | Backend operation |
| Video playback | Client-side | Rich interaction tracking |
| Email sent | Server-side | Backend process |
| File upload | Hybrid | Start (client), completion (server) |
Testing Server-Side Tracking
Test API Endpoint:
# Test with curl
curl -X POST "https://umami.example.com/api/send" \
-H "Content-Type: application/json" \
-H "User-Agent: Mozilla/5.0" \
-d '{
"type": "event",
"payload": {
"website": "YOUR_WEBSITE_ID",
"url": "/test",
"name": "test_event",
"data": {
"test": true
}
}
}'
Verify in Dashboard:
- Send test event from server
- Check Umami dashboard
- Navigate to Events tab
- Look for
test_event - Verify event data appears correctly
Best Practices
1. Use Client-Side for User Interactions
Track what users do in the browser with client-side tracking.
2. Use Server-Side for Business Logic
Track backend processes, confirmations, and system events server-side.
3. Don't Duplicate Events
Avoid tracking the same event both client-side and server-side unless intentional.
4. Handle Errors Gracefully
// Server-side tracking should never break your application
try {
await trackServerEvent('event_name', data);
} catch (error) {
console.error('Analytics error:', error);
// Continue with application logic
}
5. Use Environment Variables
const UMAMI_ENDPOINT = process.env.UMAMI_ENDPOINT;
const UMAMI_WEBSITE_ID = process.env.UMAMI_WEBSITE_ID;
6. Implement Timeout
await fetch(UMAMI_ENDPOINT, {
method: 'POST',
headers: { /* ... */ },
body: JSON.stringify(payload),
signal: AbortSignal.timeout(5000) // 5 second timeout
});
7. Queue for Reliability
// Use queue for critical server-side events
const eventQueue = [];
async function trackWithQueue(eventName, eventData) {
eventQueue.push({ eventName, eventData });
await processQueue();
}
async function processQueue() {
while (eventQueue.length > 0) {
const event = eventQueue[0];
try {
await trackServerEvent(event.eventName, event.eventData);
eventQueue.shift(); // Remove on success
} catch (error) {
console.error('Queue processing failed:', error);
break; // Retry later
}
}
}
Performance Considerations
Client-Side:
- Minimal impact: Async script loading
- No blocking of page render
- Events sent via efficient beacon API when available
Server-Side:
- Fire-and-forget for non-critical events
- Use async/background jobs for high-volume tracking
- Implement circuit breaker pattern for failures
Summary
Choose your tracking approach based on where events originate and what data you need:
- Client-Side: User interactions, page views, frontend behavior
- Server-Side: Backend processes, confirmed transactions, system events
- Hybrid: Combine both for complete picture of user journey and business operations
Both approaches respect Umami's privacy-first principles - neither requires cookies or persistent user identification across sessions.