Skip to content

Analytics e telemetria para jogos web

As ferramentas de analytics ajudam você a entender o que os jogadores fazem, onde encontram dificuldades e o que mantém o interesse deles. Este tutorial mostra o que acompanhar e como fazer isso.

1) O que acompanhar

Métricas de engajamento:

  • Inícios e duração das sessões
  • Fases iniciadas e concluídas
  • Recursos utilizados
  • Retenção (visitas recorrentes)

Métricas de desempenho:

  • Tempo de carregamento
  • Taxa de quadros
  • Uso de memória
  • Erros e travamentos

Métricas de conversão:

  • Conclusão do tutorial
  • Primeira compra
  • Compartilhamentos em redes sociais

2) Rastreamento simples de eventos

js
class Analytics {
  constructor(endpoint) {
    this.endpoint = endpoint
    this.sessionId = crypto.randomUUID()
    this.queue = []
    this.flushInterval = 30000 // 30 seconds
    
    setInterval(() => this.flush(), this.flushInterval)
    // Flush when the page is being hidden, not on beforeunload.
    // beforeunload/unload are unreliable (often don't fire on mobile,
    // and block the back/forward cache). visibilitychange + pagehide
    // are the recommended events for end-of-session sends.
    document.addEventListener('visibilitychange', () => {
      if (document.visibilityState === 'hidden') this.flush()
    })
    window.addEventListener('pagehide', () => 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) Rastreamento de sessões

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()

// Send session_end when the page is hidden, not on beforeunload.
// beforeunload is unreliable on mobile and breaks the bfcache.
function recordSessionEnd() {
  analytics.track('session_end', {
    duration: Date.now() - sessionStart,
  })
}
document.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'hidden') recordSessionEnd()
})
window.addEventListener('pagehide', recordSessionEnd)

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

4) Eventos específicos do jogo

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) Monitoramento de desempenho

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)

Identifique a origem dos engasgos com Long Animation Frames

O contador de FPS acima informa quando há quedas de quadros, mas não por quê. A API Long Animation Frames (LoAF), lançada no Chrome e no Edge 123, preenche essa lacuna. Ela sinaliza qualquer quadro que leve mais de 50 ms e detalha quais scripts causaram o atraso, permitindo atribuir os engasgos a trechos específicos de código em vez de tentar adivinhar.

js
if (PerformanceObserver.supportedEntryTypes?.includes('long-animation-frame')) {
  const observer = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      analytics.track('long_frame', {
        durationMs: Math.round(entry.duration),
        blockingMs: Math.round(entry.blockingDuration),
        // scripts[] names the source URLs that ate the frame
        scripts: entry.scripts?.map((s) => s.sourceURL),
      })
    }
  })
  observer.observe({ type: 'long-animation-frame', buffered: true })
}

Por enquanto, a LoAF está disponível apenas em navegadores baseados em Chromium (não no Firefox nem no Safari). Portanto, detecte a compatibilidade antes de usá-la e mantenha o contador de FPS como referência compatível com diferentes navegadores.

6) Rastreamento de erros

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) Rastreamento do tempo de carregamento

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) Rastreamento de funil

Acompanhe o progresso dos jogadores pelos fluxos principais:

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) Suporte a testes A/B

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) Considerações sobre privacidade

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>Usamos dados de analytics para melhorar o jogo. Tudo bem para você?</p>
    <button id="accept">Aceitar</button>
    <button id="decline">Recusar</button>
  `
  document.body.appendChild(dialog)
  
  dialog.querySelector('#accept').onclick = () => {
    analytics.setConsent(true)
    dialog.remove()
  }
  
  dialog.querySelector('#decline').onclick = () => {
    analytics.setConsent(false)
    dialog.remove()
  }
}

Alternativas de terceiros

Se você não quiser criar sua própria solução:

  • Plausible — Simples e focado em privacidade
  • Amplitude — Analytics de produto e funis
  • Mixpanel — Rastreamento de eventos e jornadas dos usuários
  • Sentry — Específico para rastreamento de erros
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' } })

Conteúdo relacionado

Recursos externos

  • Plausible Analytics — analytics leve e focado em privacidade
  • PostHog — analytics de produto de código aberto com rastreamento de eventos
  • Sentry — monitoramento de erros e relatórios de travamentos para aplicativos web