Offline & Service Worker Tracking
What This Means
Offline tracking refers to capturing analytics events when users don't have an internet connection or when service workers intercept network requests. Without proper offline tracking implementation, you lose valuable data about user behavior during network outages, slow connections, or when users interact with your Progressive Web App (PWA) offline.
Common Offline Tracking Problems
Lost Analytics Data:
- Events not queued when offline
- Failed requests not retried
- Service worker blocking analytics requests
- No offline fallback strategy
Service Worker Conflicts:
- Analytics requests cached when they shouldn't be
- Service worker intercepting and blocking analytics
- Measurement Protocol requests timing out
- Workbox caching analytics endpoints
Implementation Challenges:
- Events sent out of order when reconnected
- Duplicate events after retry
- Timestamp inaccuracies
- User context lost during offline period
Impact on Your Business
Data Loss:
- Missing user interactions during offline periods
- Incomplete user journeys
- Lost conversion data
- Inaccurate engagement metrics
Analytics Accuracy:
- Underreported page views
- Missing events and conversions
- Inaccurate session duration
- Broken funnel analysis
PWA Challenges:
- Can't track offline app usage
- Missing service worker installation tracking
- No offline engagement metrics
- Lost A/B test data
Business Intelligence:
- Incomplete understanding of user behavior
- Can't optimize offline experience
- Missing data for decision-making
- Poor mobile analytics (offline more common)
How to Diagnose
Method 1: Simulate Offline Mode
- Open Chrome DevTools (
F12) - Navigate to "Network" tab
- Set throttling to "Offline"
- Interact with your site
- Check if analytics requests are queued
What to Look For:
- Failed analytics requests
- No retry mechanism
- Data lost when offline
- Errors in console
Method 2: Service Worker Debugging
- Open Chrome DevTools
- Navigate to "Application" tab
- Click "Service Workers"
- Check "Offline" checkbox
- Interact with site
What to Look For:
- Analytics requests intercepted
- Requests blocked or cached incorrectly
- Failed fetch events
- No offline fallback
Method 3: Check Network Tab
- DevTools → Network tab
- Go offline (DevTools offline mode)
- Trigger analytics events
- Go back online
- Watch for queued requests
What to Look For:
- Requests queued and sent when online
- Failed requests (red)
- No retry attempts
- Analytics data lost
Method 4: Google Analytics DebugView
- Enable debug mode
- Go offline
- Trigger events
- Go back online
- Check DebugView for events
What to Look For:
- Missing events from offline period
- Events arriving when reconnected
- Correct timestamps
- No duplicate events
Method 5: Service Worker Logs
Check console for service worker logs:
// In service worker
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
// Log analytics requests
if (url.hostname.includes('google-analytics.com') ||
url.hostname.includes('googletagmanager.com')) {
console.log('Analytics request:', event.request.url);
}
});
General Fixes
Fix 1: Implement Offline Analytics Queue
Queue analytics requests when offline:
// Create offline queue
class OfflineAnalyticsQueue {
constructor() {
this.queue = [];
this.storageKey = 'analytics_offline_queue';
this.loadQueue();
this.setupOnlineListener();
}
loadQueue() {
try {
const stored = localStorage.getItem(this.storageKey);
this.queue = stored ? JSON.parse(stored) : [];
} catch (e) {
console.error('Failed to load queue:', e);
this.queue = [];
}
}
saveQueue() {
try {
localStorage.setItem(this.storageKey, JSON.stringify(this.queue));
} catch (e) {
console.error('Failed to save queue:', e);
}
}
add(event) {
this.queue.push({
...event,
timestamp: Date.now(),
queued: true
});
this.saveQueue();
}
setupOnlineListener() {
window.addEventListener('online', () => {
this.flush();
});
}
async flush() {
if (this.queue.length === 0) return;
const queueCopy = [...this.queue];
this.queue = [];
this.saveQueue();
for (const event of queueCopy) {
try {
await this.sendEvent(event);
} catch (e) {
// If failed, add back to queue
this.queue.push(event);
}
}
this.saveQueue();
}
async sendEvent(event) {
// Send to Google Analytics or your analytics provider
gtag('event', event.name, event.params);
}
}
// Initialize
const offlineQueue = new OfflineAnalyticsQueue();
// Intercept gtag calls when offline
const originalGtag = window.gtag;
window.gtag = function(...args) {
if (!navigator.onLine && args[0] === 'event') {
offlineQueue.add({
name: args[1],
params: args[2]
});
} else {
originalGtag.apply(this, args);
}
};
Fix 2: Configure Service Worker Properly
Don't cache analytics requests:
// service-worker.js
const ANALYTICS_DOMAINS = [
'google-analytics.com',
'googletagmanager.com',
'analytics.google.com',
'stats.g.doubleclick.net'
];
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
// Never cache analytics requests
const isAnalytics = ANALYTICS_DOMAINS.some(domain =>
url.hostname.includes(domain)
);
if (isAnalytics) {
// Let analytics requests pass through
// Don't cache, don't queue
event.respondWith(
fetch(event.request).catch(() => {
// Failed - will be handled by client-side queue
return new Response('', { status: 503 });
})
);
return;
}
// Handle other requests normally
event.respondWith(
caches.match(event.request)
.then(response => response || fetch(event.request))
);
});
Fix 3: Use Workbox with Analytics Plugin
Workbox provides offline analytics support:
// service-worker.js
importScripts('https://storage.googleapis.com/workbox-cdn/releases/6.5.4/workbox-sw.js');
// Initialize Google Analytics offline support
workbox.googleAnalytics.initialize();
// Configure caching strategies
workbox.routing.registerRoute(
({ request }) => request.destination === 'document',
new workbox.strategies.NetworkFirst()
);
workbox.routing.registerRoute(
({ request }) => request.destination === 'image',
new workbox.strategies.CacheFirst({
cacheName: 'images',
plugins: [
new workbox.expiration.ExpirationPlugin({
maxEntries: 60,
maxAgeSeconds: 30 * 24 * 60 * 60 // 30 days
})
]
})
);
Client-side initialization:
// app.js
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js')
.then(registration => {
console.log('Service Worker registered');
// Track service worker installation
gtag('event', 'service_worker_installed', {
'event_category': 'pwa',
'event_label': 'registration_successful'
});
})
.catch(error => {
console.error('Service Worker registration failed:', error);
gtag('event', 'service_worker_error', {
'event_category': 'pwa',
'event_label': error.message
});
});
}
Fix 4: Implement Background Sync API
Retry failed analytics requests:
// Register background sync
async function registerBackgroundSync() {
if ('serviceWorker' in navigator && 'sync' in ServiceWorkerRegistration.prototype) {
try {
const registration = await navigator.serviceWorker.ready;
await registration.sync.register('analytics-sync');
} catch (error) {
console.log('Background sync registration failed:', error);
}
}
}
// In service worker
self.addEventListener('sync', (event) => {
if (event.tag === 'analytics-sync') {
event.waitUntil(syncAnalytics());
}
});
async function syncAnalytics() {
// Get queued analytics events
const db = await openAnalyticsDB();
const events = await db.getAll('offline-events');
for (const event of events) {
try {
await fetch('https://www.google-analytics.com/collect', {
method: 'POST',
body: event.data
});
// Success - remove from queue
await db.delete('offline-events', event.id);
} catch (error) {
// Still offline or failed - will retry on next sync
console.log('Event sync failed:', error);
}
}
}
Fix 5: Track Offline Events
Monitor offline/online transitions:
// Track when user goes offline/online
window.addEventListener('online', () => {
gtag('event', 'connection_restored', {
'event_category': 'connectivity',
'event_label': 'online'
});
});
window.addEventListener('offline', () => {
// Queue this event
offlineQueue.add({
name: 'connection_lost',
params: {
'event_category': 'connectivity',
'event_label': 'offline',
'offline_timestamp': Date.now()
}
});
});
// Track network type
if ('connection' in navigator) {
const connection = navigator.connection;
gtag('event', 'network_info', {
'event_category': 'connectivity',
'connection_type': connection.effectiveType,
'downlink': connection.downlink,
'rtt': connection.rtt
});
}
Fix 6: Use Measurement Protocol for Server-Side
Send events from server as backup:
// Client-side: Save critical events to server
async function trackCriticalEvent(eventName, params) {
// Try client-side first
if (navigator.onLine) {
gtag('event', eventName, params);
}
// Also send to server as backup
try {
await fetch('/api/analytics', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ eventName, params })
});
} catch (e) {
// Failed - will be sent when online
}
}
// Server-side (Node.js example)
app.post('/api/analytics', async (req, res) => {
const { eventName, params } = req.body;
// Send to Google Analytics via Measurement Protocol
await fetch('https://www.google-analytics.com/mp/collect', {
method: 'POST',
body: JSON.stringify({
client_id: params.client_id,
events: [{
name: eventName,
params
}]
}),
headers: {
'Content-Type': 'application/json'
}
});
res.json({ success: true });
});
Fix 7: Implement IndexedDB for Large Queues
Use IndexedDB for persistent storage:
class AnalyticsDB {
constructor() {
this.dbName = 'analytics_offline';
this.version = 1;
}
async open() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains('events')) {
const store = db.createObjectStore('events', {
keyPath: 'id',
autoIncrement: true
});
store.createIndex('timestamp', 'timestamp', { unique: false });
}
};
});
}
async addEvent(event) {
const db = await this.open();
return new Promise((resolve, reject) => {
const transaction = db.transaction(['events'], 'readwrite');
const store = transaction.objectStore('events');
const request = store.add({
...event,
timestamp: Date.now()
});
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async getEvents() {
const db = await this.open();
return new Promise((resolve, reject) => {
const transaction = db.transaction(['events'], 'readonly');
const store = transaction.objectStore('events');
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async deleteEvent(id) {
const db = await this.open();
return new Promise((resolve, reject) => {
const transaction = db.transaction(['events'], 'readwrite');
const store = transaction.objectStore('events');
const request = store.delete(id);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
}
Platform-Specific Guides
Detailed implementation instructions for your specific platform:
Verification
After implementing offline tracking:
Test offline mode:
- Go offline in DevTools
- Trigger analytics events
- Check localStorage/IndexedDB for queued events
- Go online and verify events sent
Service worker testing:
- Check Application tab
- Verify analytics requests not cached
- Test fetch event handling
- Check console for errors
Background sync testing:
- Go offline
- Trigger events
- Close browser
- Reopen and verify events synced
Check analytics reports:
- Verify no data loss
- Check event timestamps
- Ensure no duplicates
Network throttling:
- Test with Slow 3G
- Verify events queued during slow connection
- Check retry behavior
Common Mistakes
- Service worker caching analytics - Blocks tracking completely
- No offline queue - All offline data lost
- Not handling failed requests - No retry mechanism
- Duplicate events - Same event sent multiple times
- Lost timestamps - Using current time instead of event time
- Ignoring consent - Tracking without user permission
- No storage limits - Queue grows indefinitely
- Not testing offline - Assumes always online
- Blocking service worker - Analytics prevents SW installation
- No fallback strategy - Single point of failure