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
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
// 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
// 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
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.
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
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
// 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:
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
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
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
// 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
- Publique um jogo web que carrega rápido
- Salvamentos de jogos com IndexedDB
- Para criadores
- Como lançar seu jogo no itch.io — analytics do itch.io e rastreamento de downloads
- Estratégia para o Steam Next Fest — medição de listas de desejos e taxas de conversão da demo
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