Offline & Service Worker Tracking | Blue Frog Docs

Offline & Service Worker Tracking

Diagnose and fix tracking issues that occur when users go offline or when service workers cache requests

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

  1. Open Chrome DevTools (F12)
  2. Navigate to "Network" tab
  3. Set throttling to "Offline"
  4. Interact with your site
  5. 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

  1. Open Chrome DevTools
  2. Navigate to "Application" tab
  3. Click "Service Workers"
  4. Check "Offline" checkbox
  5. 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

  1. DevTools → Network tab
  2. Go offline (DevTools offline mode)
  3. Trigger analytics events
  4. Go back online
  5. 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

  1. Enable debug mode
  2. Go offline
  3. Trigger events
  4. Go back online
  5. 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:

Platform Troubleshooting Guide
Shopify Shopify Offline Tracking Guide
WordPress WordPress Offline Tracking Guide
Wix Wix Offline Tracking Guide
Squarespace Squarespace Offline Tracking Guide
Webflow Webflow Offline Tracking Guide

Verification

After implementing offline tracking:

  1. Test offline mode:

    • Go offline in DevTools
    • Trigger analytics events
    • Check localStorage/IndexedDB for queued events
    • Go online and verify events sent
  2. Service worker testing:

    • Check Application tab
    • Verify analytics requests not cached
    • Test fetch event handling
    • Check console for errors
  3. Background sync testing:

    • Go offline
    • Trigger events
    • Close browser
    • Reopen and verify events synced
  4. Check analytics reports:

    • Verify no data loss
    • Check event timestamps
    • Ensure no duplicates
  5. Network throttling:

    • Test with Slow 3G
    • Verify events queued during slow connection
    • Check retry behavior

Common Mistakes

  1. Service worker caching analytics - Blocks tracking completely
  2. No offline queue - All offline data lost
  3. Not handling failed requests - No retry mechanism
  4. Duplicate events - Same event sent multiple times
  5. Lost timestamps - Using current time instead of event time
  6. Ignoring consent - Tracking without user permission
  7. No storage limits - Queue grows indefinitely
  8. Not testing offline - Assumes always online
  9. Blocking service worker - Analytics prevents SW installation
  10. No fallback strategy - Single point of failure

Additional Resources

// SYS.FOOTER