1. 概述
数据上报是埋点系统的核心环节,负责将采集到的用户行为数据发送到服务器。一个高效的数据上报方案需要解决以下问题:
- 上报时机:何时触发数据上报。
- 上报方式:如何发送数据(如 HTTP 请求、WebSocket、图片打点等)。
- 数据压缩:如何减少数据体积,提升传输效率。
- 数据格式:上报数据的结构和内容。
- 性能优化:如何减少对页面性能的影响。
- 错误处理:如何处理上报失败的情况。
2. 上报时机
2.1 实时上报
在用户行为触发时立即上报数据。适用于关键事件(如支付成功)。
判断依据:
- 关键事件:如支付成功、表单提交等。
- 高优先级数据:如错误日志、性能指标等。
示例代码:
function trackCriticalEvent(eventType, eventData) {
const trackingData = {
eventType,
...eventData,
timestamp: new Date().toISOString(),
};
sendTrackingData(trackingData); // 实时上报
}
2.2 延迟上报
在页面空闲时上报数据。适用于非关键事件(如点击、滚动)。
判断依据:
- 非关键事件:如点击、滚动、页面浏览等。
- 低优先级数据:如用户行为日志、页面停留时间等。
示例代码:
function trackNonCriticalEvent(eventType, eventData) {
const trackingData = {
eventType,
...eventData,
timestamp: new Date().toISOString(),
};
scheduleReport(trackingData); // 延迟上报
}
function scheduleReport(data) {
if (window.requestIdleCallback) {
window.requestIdleCallback(() => sendTrackingData(data));
} else {
setTimeout(() => sendTrackingData(data), 5000); // 延迟5秒上报
}
}
2.3 页面离开时上报
在用户离开页面时上报数据。适用于记录页面停留时间等场景。
判断依据:
- 页面离开事件:如关闭页面、刷新页面、跳转到其他页面等。
示例代码:
function trackPageLeave() {
const pageStayTime = Date.now() - this.pageEnterTime; // 计算页面停留时间
const trackingData = {
eventType: 'page_leave',
pageStayTime,
timestamp: new Date().toISOString(),
};
sendTrackingData(trackingData); // 页面离开时上报
}
window.addEventListener('beforeunload', () => trackPageLeave());
3. 上报方式
3.1 使用 fetch
发送数据
fetch
是现代浏览器推荐的网络请求方式,支持 Promise 和异步操作。
示例代码:
function sendTrackingData(data) {
fetch('/api/track', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
console.log('Data reported successfully');
})
.catch(error => {
console.error('Failed to report data:', error);
});
}
3.2 使用 navigator.sendBeacon
发送数据
navigator.sendBeacon
是专为数据上报设计的 API,适用于页面离开时的上报。
示例代码:
function sendTrackingData(data) {
const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
if (navigator.sendBeacon('/api/track', blob)) {
console.log('Data reported successfully');
} else {
console.error('Failed to report data');
}
}
3.3 使用 WebSocket 发送数据
WebSocket 适用于需要实时上报数据的场景。
示例代码:
const socket = new WebSocket('wss://example.com/track');
socket.onopen = () => {
console.log('WebSocket connection established');
};
socket.onerror = (error) => {
console.error('WebSocket error:', error);
};
function sendTrackingData(data) {
if (socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify(data));
} else {
console.error('WebSocket is not open');
}
}
3.4 使用图片打点发送数据
图片打点是一种传统的上报方式,适用于跨域场景。
示例代码:
function sendTrackingData(data) {
const params = new URLSearchParams(data);
const img = new Image();
img.src = `https://example.com/track?${params.toString()}`;
}
4. 数据压缩
4.1 压缩算法选择
常用的压缩算法包括 Gzip、Deflate 和 Brotli。以下是它们的对比和推荐场景:
特性 | Gzip | Deflate | Brotli |
---|---|---|---|
压缩率 | 较高 | 与 Gzip 相似 | 更高(比 Gzip 高 20%-26%) |
压缩速度 | 较快 | 较快 | 较慢(压缩时间比 Gzip 长) |
解压速度 | 快 | 快 | 快 |
浏览器支持 | 所有现代浏览器 | 所有现代浏览器 | 现代浏览器(IE 不支持) |
适用场景 | 通用场景 | 通用场景 | 静态资源、文本数据 |
HTTP 头部支持 | Content-Encoding: gzip | Content-Encoding: deflate | Content-Encoding: br |
推荐场景:
- 动态内容:Gzip。
- 静态资源:Brotli。
- 兼容性要求高:Gzip。
4.2 客户端压缩
在客户端压缩数据,推荐使用 Gzip。
示例代码:
async function compressData(data) {
const stream = new Blob([JSON.stringify(data)]).stream();
const compressedStream = stream.pipeThrough(new CompressionStream('gzip'));
return await new Response(compressedStream).blob();
}
async function sendTrackingData(data) {
const compressedData = await compressData(data);
fetch('/api/track', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Encoding': 'gzip',
},
body: compressedData,
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
console.log('Data reported successfully');
})
.catch(error => {
console.error('Failed to report data:', error);
});
}
4.3 服务端压缩
在服务端压缩数据,推荐使用 Brotli 或 Gzip。
示例代码(Node.js):
const zlib = require('zlib');
function compressData(data, algorithm = 'gzip') {
return new Promise((resolve, reject) => {
const compress = algorithm === 'brotli' ? zlib.brotliCompress : zlib.gzip;
compress(JSON.stringify(data), (err, buffer) => {
if (err) reject(err);
else resolve(buffer);
});
});
}
// 使用示例
compressData({ event: 'click', target: 'button#submit' }, 'brotli')
.then(compressedData => {
console.log('Compressed Data:', compressedData);
})
.catch(error => {
console.error('Compression failed:', error);
});
5. 数据格式
5.1 数据结构
上报数据应包含以下字段:
- 事件类型:如
click
、scroll
、page_leave
。 - 事件目标:如
button#submit
。 - 时间戳:事件发生的时间。
- 页面信息:如页面 URL、页面标题。
- 设备信息:如设备类型、屏幕分辨率。
- 环境信息:如网络类型、语言设置。
示例数据:
{
"eventType": "click",
"eventTarget": "button#submit",
"timestamp": "2023-10-01T12:34:56.789Z",
"pageUrl": "https://example.com/home",
"pageTitle": "Home Page",
"deviceType": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36",
"screenResolution": "1920x1080",
"networkType": "4g",
"language": "zh-CN"
}
6. 性能优化
6.1 批量上报
将多个事件数据缓存后,一次性上报。
示例代码:
class Tracker {
constructor() {
this.queue = []; // 数据缓存队列
this.isSending = false; // 是否正在发送数据
}
// 添加数据到队列
addToQueue(data) {
this.queue.push(data);
this.processQueue();
}
// 处理队列
processQueue() {
if (this.isSending || this.queue.length === 0) return;
this.isSending = true;
const batchData = this.queue.splice(0, 10); // 每次发送10条数据
this.sendTrackingData(batchData).finally(() => {
this.isSending = false;
this.processQueue(); // 继续处理剩余数据
});
}
// 发送数据
async sendTrackingData(data) {
try {
await fetch('/api/track', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
} catch (error) {
console.error('Failed to send tracking data:', error);
// 将失败的数据重新加入队列
this.queue.unshift(...data);
}
}
}
6.2 延迟上报
在页面空闲时上报数据,减少对用户操作的影响。
示例代码:
function scheduleReport(data) {
if (window.requestIdleCallback) {
window.requestIdleCallback(() => sendTrackingData(data));
} else {
setTimeout(() => sendTrackingData(data), 5000); // 延迟5秒上报
}
}
7. 错误处理
7.1 重试机制
在网络请求失败时,自动重试上报。
示例代码:
async function sendTrackingData(data, retryCount = 3) {
try {
await fetch('/api/track', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
} catch (error) {
if (retryCount > 0) {
console.log(`Retrying... (${retryCount} attempts left)`);
setTimeout(() => sendTrackingData(data, retryCount - 1), 1000); // 1秒后重试
} else {
console.error('Failed to report data after retries:', error);
}
}
}
7.2 本地存储
在网络不可用或上报失败时,将数据存储到本地,待网络恢复后重新上报。
示例代码:
function logToLocalStorage(data) {
const logs = JSON.parse(localStorage.getItem('trackingLogs') || '[]');
logs.push(data);
localStorage.setItem('trackingLogs', JSON.stringify(logs));
}
function retryFailedReports() {
const logs = JSON.parse(localStorage.getItem('trackingLogs') || '[]');
if (logs.length > 0) {
sendTrackingData(logs).then(() => {
localStorage.removeItem('trackingLogs');
});
}
}
// 每隔5分钟尝试重新上报
setInterval(retryFailedReports, 5 * 60 * 1000);
8. 总结
本文档提供了一套完整的数据上报方案,包括:
- 上报时机:实时上报、延迟上报、页面离开时上报。
- 上报方式:使用
fetch
、navigator.sendBeacon
、WebSocket 和图片打点。 - 数据压缩:使用 Gzip 或 Brotli 压缩数据。
- 数据格式:规范化的数据结构。
- 性能优化:批量上报和延迟上报。
- 错误处理:重试机制和本地存储。