Analítica y telemetría para juegos web
La analítica te ayuda a entender qué hacen los jugadores, dónde encuentran dificultades y qué mantiene su interés. Este tutorial muestra qué registrar y cómo hacerlo.
1) Qué registrar
Métricas de interacción:
- Inicios y duración de las sesiones
- Niveles iniciados y completados
- Funciones utilizadas
- Retención (visitas recurrentes)
Métricas de rendimiento:
- Tiempo de carga
- Tasa de fotogramas
- Uso de memoria
- Errores y cierres inesperados
Métricas de conversión:
- Finalización del tutorial
- Primera compra
- Contenido compartido en redes sociales
2) Registro sencillo 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) Registro de sesiones
// 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 del juego
// 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) Supervisión del rendimiento
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)Atribuir los tirones con Long Animation Frames
El contador de FPS anterior te indica cuándo se pierden fotogramas, pero no por qué. La API Long Animation Frames (LoAF), incorporada en Chrome y Edge 123, cubre esa carencia. Detecta cualquier fotograma que tarde más de 50 ms y desglosa qué scripts provocaron el retraso, para que puedas atribuir los tirones a código específico en lugar de hacer conjeturas.
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 ahora, LoAF solo está disponible en Chromium (no en Firefox ni Safari), así que comprueba si la función está disponible antes de usarla y conserva el contador de FPS como referencia compatible con todos los navegadores.
6) Registro de errores
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) Registro del tiempo de carga
// 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) Registro de embudos
Registra el progreso de los jugadores a través de los flujos clave:
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) Compatibilidad con pruebas 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) Consideraciones sobre la privacidad
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>Utilizamos analítica para mejorar el juego. ¿Estás de acuerdo?</p>
<button id="accept">Aceptar</button>
<button id="decline">Rechazar</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 terceros
Si no quieres crear tu propia solución:
- Plausible — Sencillo y centrado en la privacidad
- Amplitude — Analítica de producto y embudos
- Mixpanel — Registro de eventos y recorridos de usuario
- Sentry — Especializado en el registro de errores
// 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' } })Relacionado
- Publica un juego web que cargue rápido
- Partidas guardadas con IndexedDB
- Para creadores
- Cómo lanzar tu juego en itch.io — analítica de itch.io y seguimiento de descargas
- Estrategia para Steam Next Fest — medición de listas de deseos y tasas de conversión de la demo
Recursos externos
- Plausible Analytics — analítica ligera y respetuosa con la privacidad
- PostHog — analítica de producto de código abierto con registro de eventos
- Sentry — supervisión de errores e informes de cierres inesperados para aplicaciones web