Progressive Web Apps (PWA)
PWA Analytics and Monitoring
PWA Analytics and Monitoring
Understanding how users interact with your Progressive Web App is crucial for improving performance and user experience. This lesson covers tracking PWA installs, monitoring offline usage, measuring web vitals, and implementing error tracking.
Tracking PWA Installs
Monitor when users install your PWA and track installation sources.
<script>
// Track PWA installation events
let deferredPrompt;
let installSource = 'unknown';
// Listen for beforeinstallprompt event
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
deferredPrompt = e;
// Track that install prompt was shown
trackEvent('pwa_install_prompt_shown', {
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent
});
// Show custom install button
document.getElementById('installButton').style.display = 'block';
});
// Handle install button click
document.getElementById('installButton').addEventListener('click', async () => {
if (!deferredPrompt) return;
installSource = 'custom_button';
// Show install prompt
deferredPrompt.prompt();
// Wait for user choice
const { outcome } = await deferredPrompt.userChoice;
// Track install decision
trackEvent('pwa_install_decision', {
outcome: outcome, // 'accepted' or 'dismissed'
source: installSource,
timestamp: new Date().toISOString()
});
if (outcome === 'accepted') {
console.log('PWA installed successfully');
}
deferredPrompt = null;
});
// Track successful installation
window.addEventListener('appinstalled', (e) => {
trackEvent('pwa_installed', {
source: installSource,
timestamp: new Date().toISOString(),
platform: getPlatform()
});
console.log('PWA was installed');
});
// Detect if PWA is running as installed app
function isPWAInstalled() {
return window.matchMedia('(display-mode: standalone)').matches ||
window.navigator.standalone === true; // iOS
}
if (isPWAInstalled()) {
trackEvent('pwa_launched', {
displayMode: 'standalone',
timestamp: new Date().toISOString()
});
}
function getPlatform() {
const ua = navigator.userAgent;
if (/android/i.test(ua)) return 'android';
if (/iPad|iPhone|iPod/.test(ua)) return 'ios';
if (/Win/.test(ua)) return 'windows';
if (/Mac/.test(ua)) return 'macos';
if (/Linux/.test(ua)) return 'linux';
return 'unknown';
}
</script>
Note: The beforeinstallprompt event is not supported on iOS Safari. Use alternative detection methods for iOS PWA installation tracking.
Offline Analytics
Track user behavior even when offline and sync analytics data when connection is restored.
// offline-analytics.js - Queue analytics when offline
class OfflineAnalytics {
constructor() {
this.dbName = 'pwa-analytics';
this.storeName = 'events';
this.db = null;
this.init();
}
async init() {
this.db = await this.openDatabase();
this.setupSyncListener();
}
openDatabase() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(this.storeName)) {
db.createObjectStore(this.storeName, {
keyPath: 'id',
autoIncrement: true
});
}
};
});
}
async track(eventName, eventData = {}) {
const event = {
name: eventName,
data: eventData,
timestamp: Date.now(),
synced: false,
offline: !navigator.onLine
};
if (navigator.onLine) {
// Send immediately if online
await this.sendToServer(event);
} else {
// Queue for later if offline
await this.queueEvent(event);
console.log('Event queued for offline sync:', eventName);
}
}
async queueEvent(event) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
const request = store.add(event);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async sendToServer(event) {
try {
const response = await fetch('/api/analytics', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(event)
});
if (response.ok) {
event.synced = true;
return true;
}
} catch (error) {
console.error('Analytics send failed:', error);
await this.queueEvent(event);
}
return false;
}
async syncQueuedEvents() {
const events = await this.getQueuedEvents();
if (events.length === 0) {
console.log('No queued events to sync');
return;
}
console.log(`Syncing ${events.length} queued events...`);
for (const event of events) {
const success = await this.sendToServer(event);
if (success) {
await this.removeEvent(event.id);
}
}
console.log('Event sync completed');
}
async getQueuedEvents() {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([this.storeName], 'readonly');
const store = transaction.objectStore(this.storeName);
const request = store.getAll();
request.onsuccess = () => {
const events = request.result.filter(e => !e.synced);
resolve(events);
};
request.onerror = () => reject(request.error);
});
}
async removeEvent(id) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
const request = store.delete(id);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
setupSyncListener() {
window.addEventListener('online', () => {
console.log('Connection restored, syncing analytics...');
this.syncQueuedEvents();
});
}
}
// Initialize offline analytics
const analytics = new OfflineAnalytics();
// Track events
function trackEvent(name, data) {
analytics.track(name, data);
}
// Usage examples
trackEvent('page_view', { page: '/home' });
trackEvent('button_click', { button: 'cta', location: 'hero' });
trackEvent('form_submit', { form: 'contact', success: true });
Service Worker Analytics
Monitor service worker events and caching performance.
// service-worker.js - Analytics tracking
self.addEventListener('install', (event) => {
// Track service worker installation
event.waitUntil(
sendAnalytics('sw_installed', {
version: 'v1.0.0',
timestamp: Date.now()
})
);
});
self.addEventListener('activate', (event) => {
// Track service worker activation
event.waitUntil(
sendAnalytics('sw_activated', {
version: 'v1.0.0',
timestamp: Date.now()
})
);
});
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
event.respondWith(
caches.match(event.request).then((cachedResponse) => {
if (cachedResponse) {
// Track cache hit
trackCacheHit(url.pathname);
return cachedResponse;
}
// Track cache miss
trackCacheMiss(url.pathname);
return fetch(event.request).then((response) => {
// Cache response for future
if (response.status === 200) {
const responseClone = response.clone();
caches.open('pwa-cache-v1').then((cache) => {
cache.put(event.request, responseClone);
});
}
return response;
}).catch((error) => {
// Track network failure
trackNetworkError(url.pathname, error.message);
return caches.match('/offline.html');
});
})
);
});
// Analytics tracking functions
function trackCacheHit(path) {
sendAnalytics('cache_hit', { path, timestamp: Date.now() });
}
function trackCacheMiss(path) {
sendAnalytics('cache_miss', { path, timestamp: Date.now() });
}
function trackNetworkError(path, error) {
sendAnalytics('network_error', { path, error, timestamp: Date.now() });
}
async function sendAnalytics(eventName, eventData) {
try {
await fetch('/api/sw-analytics', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
event: eventName,
data: eventData
})
});
} catch (error) {
// Queue for later if offline
console.log('Analytics queued:', eventName);
}
}
Tip: Use service worker analytics to identify caching inefficiencies and optimize your cache strategy based on real user data.
Web Vitals Monitoring
Track Core Web Vitals (LCP, FID, CLS) to measure user experience quality.
<script>
// Web Vitals tracking with web-vitals library
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';
// Track all Core Web Vitals
function trackWebVitals() {
getCLS((metric) => {
trackEvent('web_vital_cls', {
value: metric.value,
rating: getRating(metric.rating),
delta: metric.delta,
id: metric.id
});
});
getFID((metric) => {
trackEvent('web_vital_fid', {
value: metric.value,
rating: getRating(metric.rating),
delta: metric.delta,
id: metric.id
});
});
getFCP((metric) => {
trackEvent('web_vital_fcp', {
value: metric.value,
rating: getRating(metric.rating),
delta: metric.delta,
id: metric.id
});
});
getLCP((metric) => {
trackEvent('web_vital_lcp', {
value: metric.value,
rating: getRating(metric.rating),
delta: metric.delta,
id: metric.id
});
});
getTTFB((metric) => {
trackEvent('web_vital_ttfb', {
value: metric.value,
rating: getRating(metric.rating),
delta: metric.delta,
id: metric.id
});
});
}
function getRating(rating) {
const ratings = {
good: 'good',
'needs-improvement': 'needs_improvement',
poor: 'poor'
};
return ratings[rating] || 'unknown';
}
// Custom performance tracking
function trackPerformance() {
if (window.performance && window.performance.timing) {
const timing = window.performance.timing;
const navigationStart = timing.navigationStart;
const metrics = {
dns_lookup: timing.domainLookupEnd - timing.domainLookupStart,
tcp_connection: timing.connectEnd - timing.connectStart,
request_time: timing.responseStart - timing.requestStart,
response_time: timing.responseEnd - timing.responseStart,
dom_processing: timing.domComplete - timing.domLoading,
load_time: timing.loadEventEnd - navigationStart
};
trackEvent('performance_metrics', metrics);
}
}
// Initialize tracking
trackWebVitals();
window.addEventListener('load', trackPerformance);
</script>
Error Tracking and Monitoring
Implement comprehensive error tracking to identify and fix issues quickly.
<script>
// Error tracking system
class ErrorTracker {
constructor() {
this.setupGlobalErrorHandler();
this.setupUnhandledRejectionHandler();
this.setupServiceWorkerErrorHandler();
}
setupGlobalErrorHandler() {
window.addEventListener('error', (event) => {
this.trackError({
type: 'javascript_error',
message: event.message,
filename: event.filename,
line: event.lineno,
column: event.colno,
stack: event.error?.stack,
timestamp: Date.now(),
userAgent: navigator.userAgent,
url: window.location.href
});
});
}
setupUnhandledRejectionHandler() {
window.addEventListener('unhandledrejection', (event) => {
this.trackError({
type: 'unhandled_promise_rejection',
message: event.reason?.message || event.reason,
stack: event.reason?.stack,
timestamp: Date.now(),
userAgent: navigator.userAgent,
url: window.location.href
});
});
}
setupServiceWorkerErrorHandler() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.addEventListener('error', (event) => {
this.trackError({
type: 'service_worker_error',
message: event.message || 'Service worker error',
timestamp: Date.now()
});
});
}
}
trackError(errorData) {
// Log to console
console.error('Error tracked:', errorData);
// Send to server
this.sendToServer(errorData);
// Store locally for debugging
this.storeLocally(errorData);
}
async sendToServer(errorData) {
try {
await fetch('/api/errors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(errorData)
});
} catch (error) {
console.error('Failed to send error report:', error);
}
}
storeLocally(errorData) {
const errors = JSON.parse(localStorage.getItem('pwa_errors') || '[]');
errors.push(errorData);
// Keep only last 50 errors
if (errors.length > 50) {
errors.shift();
}
localStorage.setItem('pwa_errors', JSON.stringify(errors));
}
getStoredErrors() {
return JSON.parse(localStorage.getItem('pwa_errors') || '[]');
}
clearStoredErrors() {
localStorage.removeItem('pwa_errors');
}
}
// Initialize error tracker
const errorTracker = new ErrorTracker();
// Manually track errors
function reportError(error, context = {}) {
errorTracker.trackError({
type: 'manual_error',
message: error.message,
stack: error.stack,
context: context,
timestamp: Date.now()
});
}
// Example: Catch and report API errors
async function fetchWithErrorTracking(url, options) {
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response;
} catch (error) {
reportError(error, {
url: url,
method: options?.method || 'GET'
});
throw error;
}
}
</script>
Warning: Be careful not to track sensitive user information in analytics. Always respect user privacy and comply with GDPR/CCPA regulations.
User Engagement Metrics
Track how users interact with your PWA to understand engagement patterns.
<script>
// User engagement tracking
class EngagementTracker {
constructor() {
this.sessionStart = Date.now();
this.pageViewStart = Date.now();
this.totalEngagementTime = 0;
this.isActive = true;
this.setupTracking();
}
setupTracking() {
// Track page visibility
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
this.pauseEngagement();
} else {
this.resumeEngagement();
}
});
// Track user interactions
['click', 'scroll', 'keydown', 'mousemove'].forEach(event => {
document.addEventListener(event, () => this.recordActivity(), { passive: true });
});
// Track session end
window.addEventListener('beforeunload', () => {
this.endSession();
});
// Periodic engagement check
setInterval(() => this.checkEngagement(), 5000);
}
recordActivity() {
this.isActive = true;
this.lastActivityTime = Date.now();
}
checkEngagement() {
const now = Date.now();
const timeSinceActivity = now - (this.lastActivityTime || now);
// Consider user inactive after 30 seconds
if (timeSinceActivity > 30000) {
this.isActive = false;
}
if (this.isActive && !document.hidden) {
this.totalEngagementTime += 5000;
}
}
pauseEngagement() {
trackEvent('page_hidden', {
timeOnPage: Date.now() - this.pageViewStart,
timestamp: Date.now()
});
}
resumeEngagement() {
this.pageViewStart = Date.now();
trackEvent('page_visible', {
timestamp: Date.now()
});
}
endSession() {
const sessionDuration = Date.now() - this.sessionStart;
trackEvent('session_end', {
duration: sessionDuration,
engagementTime: this.totalEngagementTime,
engagementRate: (this.totalEngagementTime / sessionDuration * 100).toFixed(2),
timestamp: Date.now()
});
}
getMetrics() {
return {
sessionDuration: Date.now() - this.sessionStart,
engagementTime: this.totalEngagementTime,
isActive: this.isActive
};
}
}
// Initialize engagement tracker
const engagement = new EngagementTracker();
// Track specific user actions
function trackUserAction(action, details = {}) {
trackEvent('user_action', {
action: action,
details: details,
metrics: engagement.getMetrics(),
timestamp: Date.now()
});
}
// Usage examples
document.getElementById('shareButton')?.addEventListener('click', () => {
trackUserAction('share_click', { content: 'article' });
});
document.getElementById('addToCart')?.addEventListener('click', () => {
trackUserAction('add_to_cart', { product: 'item-123' });
});
</script>
Exercise:
- Implement PWA install tracking with Google Analytics or a custom solution
- Create an offline analytics queue that syncs when connection is restored
- Set up Web Vitals monitoring and create alerts for poor performance
- Implement comprehensive error tracking with stack traces and context
- Build an analytics dashboard that displays PWA engagement metrics
Analytics Best Practices
- Privacy First: Always respect user privacy and obtain consent before tracking
- Offline Support: Queue analytics events when offline and sync when online
- Performance Impact: Minimize analytics overhead to avoid affecting PWA performance
- Data Quality: Filter out bot traffic and validate data before storage
- Error Context: Include relevant context with errors (user agent, URL, state)
- Real User Monitoring: Track real user metrics, not just synthetic tests
- Actionable Insights: Focus on metrics that drive decisions and improvements
- Data Retention: Implement appropriate data retention policies
Popular Analytics Tools for PWAs
- Google Analytics 4: Full PWA support with custom events and offline tracking
- Matomo: Open-source analytics with privacy-focused features
- Amplitude: Product analytics with user journey tracking
- Sentry: Error tracking and performance monitoring
- LogRocket: Session replay and error tracking
- New Relic: Full-stack observability and performance monitoring