Analitik dan telemetri untuk game web
Analitik membantu Anda memahami apa yang dilakukan pemain, di mana mereka mengalami kesulitan, dan apa yang membuat mereka tetap terlibat. Tutorial ini menunjukkan apa saja yang perlu dilacak dan caranya.
1) Apa yang perlu dilacak
Metrik keterlibatan:
- Awal dan durasi sesi
- Level yang dimulai dan diselesaikan
- Fitur yang digunakan
- Retensi (kunjungan kembali)
Metrik performa:
- Waktu pemuatan
- Laju bingkai
- Penggunaan memori
- Kesalahan dan crash
Metrik konversi:
- Penyelesaian tutorial
- Pembelian pertama
- Pembagian ke media sosial
2) Pelacakan peristiwa sederhana
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) Pelacakan sesi
// 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) Peristiwa khusus game
// 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) Pemantauan performa
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)Temukan penyebab patah-patah dengan Long Animation Frames
Penghitung FPS di atas memberi tahu Anda kapan laju bingkai menurun, tetapi tidak menjelaskan mengapa. Long Animation Frames API (LoAF), yang tersedia sejak Chrome dan Edge 123, mengisi kekosongan tersebut. API ini menandai setiap bingkai yang memerlukan waktu lebih dari 50 md dan menguraikan skrip mana yang menyebabkan penundaan, sehingga Anda dapat mengaitkan patah-patah dengan kode tertentu alih-alih sekadar menebak.
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 })
}Untuk saat ini, LoAF hanya tersedia di Chromium (belum ada di Firefox atau Safari), jadi lakukan deteksi fitur sebelum menggunakannya dan pertahankan penghitung FPS sebagai patokan lintas peramban Anda.
6) Pelacakan kesalahan
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) Pelacakan waktu pemuatan
// 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) Pelacakan funnel
Lacak kemajuan pemain melalui alur-alur utama:
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) Dukungan pengujian 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) Pertimbangan privasi
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>Kami menggunakan analitik untuk meningkatkan kualitas game. Apakah Anda menyetujuinya?</p>
<button id="accept">Setuju</button>
<button id="decline">Tolak</button>
`
document.body.appendChild(dialog)
dialog.querySelector('#accept').onclick = () => {
analytics.setConsent(true)
dialog.remove()
}
dialog.querySelector('#decline').onclick = () => {
analytics.setConsent(false)
dialog.remove()
}
}Alternatif pihak ketiga
Jika Anda tidak ingin membuatnya sendiri:
- Plausible — Berfokus pada privasi dan sederhana
- Amplitude — Analitik produk dan funnel
- Mixpanel — Pelacakan peristiwa dan perjalanan pengguna
- Sentry — Khusus untuk pelacakan kesalahan
// 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' } })Terkait
- Merilis game web yang dimuat dengan cepat
- Penyimpanan game dengan IndexedDB
- Untuk kreator
- Cara Meluncurkan Game Anda di itch.io — analitik itch.io dan pelacakan unduhan
- Strategi Steam Next Fest — mengukur daftar keinginan dan tingkat konversi demo
Sumber Daya Eksternal
- Plausible Analytics — analitik ringan dan ramah privasi
- PostHog — analitik produk sumber terbuka dengan pelacakan peristiwa
- Sentry — pemantauan kesalahan dan pelaporan crash untuk aplikasi web