消息上报机制
常见方案
- sendBeacon :常用于页面卸载阶段上报,接口简单,不阻塞页面卸载流程;通常存在单次请求体大小限制(常见约
64KB,不同浏览器可能有差异)。 - fetch keepalive :写法更灵活,可自定义方法、请求头与内容类型;同样受卸载场景下请求体大小限制(常见约
64KB)。
发送时机
在 visibilitychange 事件中,当 document.visibilityState === 'hidden' 时触发上报。
- 触发时机早于
beforeunload,标签页切后台、最小化、息屏等场景也能触发。 beforeunload可能不会触发,visiblituchange移动端可能存在兼容问题,beforeunload可作为兼容兜底实现。
typescript
window.addEventListener('visibilitychange', () => {
if (document.visibilityState !== 'hidden') return;
const payload = JSON.stringify(pendingEvents);
// 优先使用 sendBeacon,避免页面卸载时请求被中断。
const ok = navigator.sendBeacon('/collect', payload);
if (ok) return;
// sendBeacon 失败时降级为 keepalive fetch。
fetch('/collect', {
method: 'POST',
keepalive: true, // 允许页面卸载时继续发送请求
headers: { 'Content-Type': 'application/json' },
body: payload
}).catch(() => {
// 失败后重传,离线缓存处理
});
});
fetchLater
注册延迟请求,在浏览器在页面离开或 activateAfter 到时后触发发送理解成 fetch + activeAfter 构成的延迟版本)
解决 sendBeacon 和 fetch keepalive 发送时机选择的问题,由浏览器排队处理
但同样有请求体大小限制,兼容性较差。
typescript
try {
fetchLater('/collect', {
method: 'POST',
body: JSON.stringify(pendingEvents),
activateAfter: 60_000 // 最晚约 60s 后触发发送(实际时机由浏览器决定,不指定可能是页面关闭时发送)
});
} catch (error) {
// 配额超限或浏览器不支持时,降级 sendBeacon / fetch keepalive
}
指数退避重传
服务端返回 429 Too Many Requests(可结合 Retry-After 响应头)或其他异常时,使用指数退避进行重试。
text
delay = baseDelay * 2^attempt * (1 + random(0, 0.5)) // 加入随机抖动
批量合并
上传时,可以通过定时器/maxBatch 批量合并日志消息,在单个请求上传处理
离线缓存
当用户离线或临时网络抖动时,可使用 IndexedDB / 内存维护缓冲队列,待网络恢复后重传。
offline事件监听到用户网络离线 ,将数据存入缓冲队列- 监听 online 事件,将数据取出并重新执行
typescript
// 简化 indexedDB 离线队列
// 如果是内存缓存队列,可以直接在监听 offline 时暂停队列批量发送,同时把发送数据重新推入缓冲队列
class OfflineQueue {
#dbName = 'sdk-queue'
async enqueue(event) {
const db = await this.#openDB()
await db.add('events', { ...event, timestamp: Date.now() })
}
async flush() {
if (!navigator.onLine) return
const db = await this.#openDB()
const events = await db.getAll('events')
for (const event of events) {
const ok = await this.#send(event)
if (ok) await db.delete('events', event.id)
}
}
//...
}
// 监听网络恢复,触发重传
window.addEventListener('online', () => queue.flush())
采样发送
原理:通过条件概率减少发送日志,降低存储和请求开销
- 给定概率值,每次执行发送的时候 random 得到一个随机值
- 如果随机值在采样概率之内,就将数据发送到后台否则不发送
采样类型:
- 固定采样:全局固定采样率,永远不变。
- 确定性采样:通过固定 id 值,比如 userId 取 hash值,然后计算出同个用户/会话的采样率。
- 动态采样:根据业务接口、用户身份、告警等级等业务规则实现采样,即针对业务进行不同频段的采样。
typescript
// 固定采样值
function normalSample(rate: number) {
return Math.random() < rate;
}
// 确定性采样示例:hash id, 采样值保持不变
function deterministicSample(id: string, rate: number) {
const hash = simpleHash(id);
return (hash % 10000) / 10000 < rate;
}
function simpleHash(input: string) {
let hash = 0;
for (let i = 0; i < input.length; i += 1) {
hash = (hash * 31 + input.charCodeAt(i)) >>> 0;
}
return hash;
}
// 动态采样:根据业务、接口、以及下发采样配置实现采样
function dynamicSample(context: { url: string; isVip?: boolean }) {
if (context.url.includes('/checkout')) return 1.0;
if (context.isVip) return 0.5;
return 0.1;
}
function dynamicSend(context: { url: string; isVip?: boolean }) {
const rate = dynamicSample(context);
return Math.random() < rate;
}
去重机制
通过为每个消息日志,错误信息生成独立指纹进行去重处理,减少重复推送,节省请求成本
消息事件去重:
可组合以下字段生成指纹:
- 消息类型。
- 业务指纹(模块、接口、页面标识)。
- 堆栈摘要(
new Error().stack归一化后摘要)。
异常类型去重:
- 判断异常类型和异常值,异常类型是
error、uncaughtrejection这种独立类型,值是错误信息 - 比较指纹是否一致
- 比较堆栈跟踪信息是否一致
总结
- 发送策略 :优先
visibilitychange+sendBeacon,失败时降级fetch keepalive,fetchLater作为尝鲜使用。 - 数据可靠处理:通过限流重试、采样、去重减少无效上报,离线队列保障弱网场景下的数据补偿。