Skip to content

Analytics and telemetry for web games

Analytics help you understand what players do, where they struggle, and what keeps them engaged. This tutorial shows what to track and how.

1) What to track

Engagement metrics:

  • Session starts and duration
  • Levels started and completed
  • Features used
  • Retention (return visits)

Performance metrics:

  • Load time
  • Frame rate
  • Memory usage
  • Errors and crashes

Conversion metrics:

  • Tutorial completion
  • First purchase
  • Social shares

2) Simple event tracking

js
class Analytics {
  constructor(endpoint) {
    this.endpoint = endpoint
    this.sessionId = crypto.randomUUID()
    this.queue = []
    this.flushInterval = 30000 // 30 seconds
    
    setInterval(() => this.flush(), this.flushInterval)
    window.addEventListener('beforeunload', () => this.flush())
  }
  
  track(event, data = {}) {
    this.queue.push({
      event,
      data,
      sessionId: this.sessionId,
      timestamp: Date.now(),
      url: location.href,
    })
    
    // Flush immediately for important events
    if (event === 'error' || event === 'purchase') {
      this.flush()
    }
  }
  
  async flush() {
    if (this.queue.length === 0) return
    
    const events = [...this.queue]
    this.queue = []
    
    try {
      await fetch(this.endpoint, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ events }),
        keepalive: true, // Important for beforeunload
      })
    } catch {
      // Put events back in queue
      this.queue.unshift(...events)
    }
  }
}

const analytics = new Analytics('/api/analytics')

3) Session tracking

js
// Track session start
analytics.track('session_start', {
  referrer: document.referrer,
  screen: `${screen.width}x${screen.height}`,
  devicePixelRatio: window.devicePixelRatio,
  userAgent: navigator.userAgent,
})

// Track session end
let sessionStart = Date.now()

window.addEventListener('beforeunload', () => {
  analytics.track('session_end', {
    duration: Date.now() - sessionStart,
  })
})

// Track visibility changes
document.addEventListener('visibilitychange', () => {
  if (document.hidden) {
    analytics.track('tab_hidden')
  } else {
    analytics.track('tab_visible')
  }
})

4) Game-specific events

js
// Level tracking
function onLevelStart(levelId) {
  analytics.track('level_start', { levelId })
}

function onLevelComplete(levelId, score, time) {
  analytics.track('level_complete', {
    levelId,
    score,
    timeSeconds: time,
  })
}

function onLevelFail(levelId, reason) {
  analytics.track('level_fail', {
    levelId,
    reason, // 'death', 'timeout', 'quit'
  })
}

// Achievement tracking
function onAchievementUnlocked(achievementId) {
  analytics.track('achievement', { achievementId })
}

// Tutorial tracking
function onTutorialStep(step, skipped = false) {
  analytics.track('tutorial', { step, skipped })
}

5) Performance monitoring

js
class PerformanceMonitor {
  constructor(analytics) {
    this.analytics = analytics
    this.frameTimes = []
    this.lastFrame = performance.now()
  }
  
  recordFrame() {
    const now = performance.now()
    this.frameTimes.push(now - this.lastFrame)
    this.lastFrame = now
    
    // Keep last 60 frames
    if (this.frameTimes.length > 60) {
      this.frameTimes.shift()
    }
  }
  
  getAverageFPS() {
    if (this.frameTimes.length === 0) return 0
    const avgFrameTime = this.frameTimes.reduce((a, b) => a + b) / this.frameTimes.length
    return 1000 / avgFrameTime
  }
  
  reportPerformance() {
    const fps = this.getAverageFPS()
    const memory = performance.memory?.usedJSHeapSize
    
    this.analytics.track('performance', {
      avgFPS: Math.round(fps),
      memoryMB: memory ? Math.round(memory / 1024 / 1024) : null,
    })
  }
}

// Report every minute
const perfMonitor = new PerformanceMonitor(analytics)
setInterval(() => perfMonitor.reportPerformance(), 60000)

6) Error tracking

js
window.addEventListener('error', (event) => {
  analytics.track('error', {
    message: event.message,
    filename: event.filename,
    lineno: event.lineno,
    colno: event.colno,
    stack: event.error?.stack,
  })
})

window.addEventListener('unhandledrejection', (event) => {
  analytics.track('error', {
    message: event.reason?.message || String(event.reason),
    type: 'unhandledrejection',
    stack: event.reason?.stack,
  })
})

// Custom error tracking
function trackGameError(context, error) {
  analytics.track('game_error', {
    context,
    message: error.message,
    stack: error.stack,
  })
}

7) Load time tracking

js
// Track initial load
window.addEventListener('load', () => {
  const timing = performance.timing
  const loadTime = timing.loadEventEnd - timing.navigationStart
  const domReady = timing.domContentLoadedEventEnd - timing.navigationStart
  
  analytics.track('page_load', {
    totalMs: loadTime,
    domReadyMs: domReady,
  })
})

// Track game-specific load phases
async function loadGame() {
  const start = performance.now()
  
  await loadCriticalAssets()
  const criticalTime = performance.now() - start
  
  analytics.track('load_critical', { ms: Math.round(criticalTime) })
  
  await loadGameAssets()
  const totalTime = performance.now() - start
  
  analytics.track('load_complete', { ms: Math.round(totalTime) })
}

8) Funnel tracking

Track player progression through key flows:

js
class FunnelTracker {
  constructor(analytics, funnelName) {
    this.analytics = analytics
    this.funnelName = funnelName
    this.startTime = Date.now()
  }
  
  step(stepName) {
    this.analytics.track('funnel_step', {
      funnel: this.funnelName,
      step: stepName,
      elapsedMs: Date.now() - this.startTime,
    })
  }
  
  complete() {
    this.analytics.track('funnel_complete', {
      funnel: this.funnelName,
      totalMs: Date.now() - this.startTime,
    })
  }
  
  abandon(reason) {
    this.analytics.track('funnel_abandon', {
      funnel: this.funnelName,
      reason,
      elapsedMs: Date.now() - this.startTime,
    })
  }
}

// Usage
const onboarding = new FunnelTracker(analytics, 'onboarding')
onboarding.step('welcome_shown')
// ... player clicks continue
onboarding.step('name_entered')
// ... player completes tutorial
onboarding.complete()

9) A/B testing support

js
class ABTest {
  constructor(testName, variants) {
    this.testName = testName
    this.variants = variants
    
    // Get or assign variant
    const stored = localStorage.getItem(`ab_${testName}`)
    if (stored && variants.includes(stored)) {
      this.variant = stored
    } else {
      this.variant = variants[Math.floor(Math.random() * variants.length)]
      localStorage.setItem(`ab_${testName}`, this.variant)
    }
    
    // Track assignment
    analytics.track('ab_assignment', {
      test: testName,
      variant: this.variant,
    })
  }
  
  getVariant() {
    return this.variant
  }
  
  trackConversion(metric) {
    analytics.track('ab_conversion', {
      test: this.testName,
      variant: this.variant,
      metric,
    })
  }
}

// Usage
const difficultyTest = new ABTest('difficulty', ['easy', 'normal', 'hard'])
game.difficulty = difficultyTest.getVariant()

// When player completes level
difficultyTest.trackConversion('level_complete')

10) Privacy considerations

js
class PrivacyAwareAnalytics extends Analytics {
  constructor(endpoint) {
    super(endpoint)
    this.enabled = this.checkConsent()
  }
  
  checkConsent() {
    return localStorage.getItem('analytics_consent') === 'true'
  }
  
  setConsent(enabled) {
    localStorage.setItem('analytics_consent', enabled ? 'true' : 'false')
    this.enabled = enabled
    
    if (enabled) {
      this.track('consent_granted')
    }
  }
  
  track(event, data = {}) {
    if (!this.enabled) return
    
    // Strip PII
    const sanitized = { ...data }
    delete sanitized.email
    delete sanitized.name
    delete sanitized.ip
    
    super.track(event, sanitized)
  }
}

// Show consent dialog
function showConsentDialog() {
  const dialog = document.createElement('div')
  dialog.innerHTML = `
    <p>We use analytics to improve the game. Is that okay?</p>
    <button id="accept">Accept</button>
    <button id="decline">Decline</button>
  `
  document.body.appendChild(dialog)
  
  dialog.querySelector('#accept').onclick = () => {
    analytics.setConsent(true)
    dialog.remove()
  }
  
  dialog.querySelector('#decline').onclick = () => {
    analytics.setConsent(false)
    dialog.remove()
  }
}

Third-party alternatives

If you don't want to build your own:

  • Plausible — Privacy-focused, simple
  • Amplitude — Product analytics, funnels
  • Mixpanel — Event tracking, user journeys
  • Sentry — Error tracking specifically
js
// Example: Plausible
const script = document.createElement('script')
script.defer = true
script.dataset.domain = 'yourgame.com'
script.src = 'https://plausible.io/js/plausible.js'
document.head.appendChild(script)

// Track custom events
window.plausible('level_complete', { props: { level: '1' } })