网页游戏的分析与遥测
分析能帮你搞清楚玩家在干什么,在哪里卡壳,又是什么让他们持续在玩。本教程讲清楚要跟踪什么、怎么跟踪。
1) 要跟踪什么
参与度指标:
- 会话开始数和时长
- 关卡开始数和完成数
- 功能使用情况
- 留存(回访)
性能指标:
- 加载时间
- 帧率
- 内存占用
- 错误和崩溃
转化指标:
- 教程完成率
- 首次购买
- 社交分享
2) 简单的事件跟踪
js
class Analytics {
constructor(endpoint) {
this.endpoint = endpoint
this.sessionId = crypto.randomUUID()
this.queue = []
this.flushInterval = 30000 // 30 秒
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,
})
// 重要事件立即上报
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, // 对 beforeunload 很重要
})
} catch {
// 把事件放回队列
this.queue.unshift(...events)
}
}
}
const analytics = new Analytics('/api/analytics')3) 会话跟踪
js
// 跟踪会话开始
analytics.track('session_start', {
referrer: document.referrer,
screen: `${screen.width}x${screen.height}`,
devicePixelRatio: window.devicePixelRatio,
userAgent: navigator.userAgent,
})
// 跟踪会话结束
let sessionStart = Date.now()
window.addEventListener('beforeunload', () => {
analytics.track('session_end', {
duration: Date.now() - sessionStart,
})
})
// 跟踪可见性变化
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
analytics.track('tab_hidden')
} else {
analytics.track('tab_visible')
}
})4) 游戏特定事件
js
// 关卡跟踪
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'
})
}
// 成就跟踪
function onAchievementUnlocked(achievementId) {
analytics.track('achievement', { achievementId })
}
// 教程跟踪
function onTutorialStep(step, skipped = false) {
analytics.track('tutorial', { step, skipped })
}5) 性能监控
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
// 保留最近 60 帧
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,
})
}
}
// 每分钟上报一次
const perfMonitor = new PerformanceMonitor(analytics)
setInterval(() => perfMonitor.reportPerformance(), 60000)6) 错误跟踪
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,
})
})
// 自定义错误跟踪
function trackGameError(context, error) {
analytics.track('game_error', {
context,
message: error.message,
stack: error.stack,
})
}7) 加载时间跟踪
js
// 跟踪初次加载
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,
})
})
// 跟踪游戏特定的加载阶段
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) 漏斗跟踪
跟踪玩家走完关键流程的过程:
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,
})
}
}
// 用法
const onboarding = new FunnelTracker(analytics, 'onboarding')
onboarding.step('welcome_shown')
// ……玩家点击继续
onboarding.step('name_entered')
// ……玩家完成教程
onboarding.complete()9) A/B 测试支持
js
class ABTest {
constructor(testName, variants) {
this.testName = testName
this.variants = variants
// 取出或分配实验组
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)
}
// 跟踪分配
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,
})
}
}
// 用法
const difficultyTest = new ABTest('difficulty', ['easy', 'normal', 'hard'])
game.difficulty = difficultyTest.getVariant()
// 玩家通关时
difficultyTest.trackConversion('level_complete')10) 隐私考量
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
// 去掉 PII
const sanitized = { ...data }
delete sanitized.email
delete sanitized.name
delete sanitized.ip
super.track(event, sanitized)
}
}
// 展示同意对话框
function showConsentDialog() {
const dialog = document.createElement('div')
dialog.innerHTML = `
<p>我们用分析来改进游戏。可以吗?</p>
<button id="accept">接受</button>
<button id="decline">拒绝</button>
`
document.body.appendChild(dialog)
dialog.querySelector('#accept').onclick = () => {
analytics.setConsent(true)
dialog.remove()
}
dialog.querySelector('#decline').onclick = () => {
analytics.setConsent(false)
dialog.remove()
}
}第三方备选方案
如果你不想自己搭:
- Plausible —— 注重隐私,简单
- Amplitude —— 产品分析、漏斗
- Mixpanel —— 事件跟踪、用户旅程
- Sentry —— 专门做错误跟踪
js
// 示例: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)
// 跟踪自定义事件
window.plausible('level_complete', { props: { level: '1' } })相关阅读
- 让网页游戏快速加载
- 用 IndexedDB 做游戏存档
- 面向创作者
- 如何在 itch.io 上发布游戏 —— itch.io 的分析和下载量跟踪
- Steam 新品节策略 —— 衡量愿望单和试玩版转化率
外部资源
- Plausible Analytics —— 注重隐私的轻量分析
- PostHog —— 开源产品分析,带事件跟踪
- Sentry —— 网页应用的错误监控和崩溃报告