埋点与数据上报
🎯 引言
上一篇收集了「页面出了什么错」,本篇解决另一个问题:用户在页面上做了什么。学完本篇,你将能够:
- 分清手动埋点和无埋点的区别,知道什么场景用哪种。
- 写出基础的点击埋点和曝光埋点代码。
- 用
navigator.sendBeacon把数据可靠地发到服务端,页面关闭也不丢。
🧱 常见的埋点方式
埋点就是在想统计的位置写一段记录代码,把用户行为收集上来,就像超市统计顾客在哪个货架前停留、拿起了什么商品。完整链路是「采集 → 上报 → 分析」,本文负责前两步:怎么埋、怎么发。
埋法主要有两种:手动埋点精确灵活但要手写代码,无埋点省事但缺少业务语义,核心业务指标建议用手动埋点。
手动埋点(代码埋点):在想统计的位置手写采集代码,比如统计按钮点击量:
// 封装的统一采集函数,后面会讲它内部怎么上报
function track(event, data) {
console.log('采集到事件:', event, data);
}
document.querySelector('#buy-btn').addEventListener('click', () => {
track('click', {
button: 'buy',
page: location.pathname,
});
});
埋哪个点、带什么数据完全由你控制,所以精确;代价是每个点都要写代码,需求变了还得改代码、发版本。
无埋点(全埋点):接入 SDK 自动收集页面上所有点击、浏览行为,事后在后台圈选关心的元素。省事且能回溯,但收集到的多是「某个 div 被点了」这类信息,精细分析时不如手动埋点清晰。
🧱 曝光埋点:统计「被看见」
曝光埋点统计「模块有没有进入过可视区域」,用 IntersectionObserver 实现:元素进入可视区域时上报一次。这个 API 在 性能优化课程 的懒加载一篇详细讲过,这里直接用:
<!-- 要统计曝光的 banner -->
<div class="banner" data-track="home-banner">夏日活动</div>
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
// isIntersecting 为 true,表示元素进入可视区域了
if (entry.isIntersecting) {
track('exposure', {
module: entry.target.dataset.track,
page: location.pathname,
});
observer.unobserve(entry.target); // 只统计一次,不再监听
}
});
});
document.querySelectorAll('.banner').forEach((el) => {
observer.observe(el);
});
注意 unobserve 这行:不取消监听,用户反复滚动经过时同一个 banner 会被重复统计。
🧱 数据上报手段
埋点上报优先用 sendBeacon。数据常在用户离开页面那一刻才发(比如停留时长),而页面一旦卸载,浏览器会取消还没发完的普通异步请求,数据就丢了。常见三种手段:
图片打点:创建 Image 对象,把数据拼在 URL 参数里,设置 src 触发一次 GET 请求:
function report(data) {
const img = new Image();
// 服务端收到这个请求就算一条数据,返回一张 1x1 像素的空白图
img.src = '/collect?data=' + encodeURIComponent(JSON.stringify(data));
}
简单、天然跨域,但只能发 GET、受 URL 长度限制,请求成没成功也无从得知。
fetch:灵活可控,但页面卸载时没发完的请求会被取消,不适合离开页面时上报。
sendBeacon:浏览器专为「页面卸载时也要把数据发出去」设计的 API:
function report(data) {
const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
navigator.sendBeacon('/collect', blob);
}
调用后浏览器接管这次发送、排队处理,用户立刻关闭页面,数据也会在后台继续发出。就像把包裹交给快递员,之后的路不用你管。
| 对比项 | 图片打点 | fetch | sendBeacon |
|---|---|---|---|
| 请求方式 | 只能 GET | 任意 | POST |
| 页面关闭时 | 可能丢失 | 大概率被取消 | 浏览器保证继续发送 |
| 数据大小 | 受 URL 长度限制 | 较大 | 有上限(约 64KB) |
| 适用场景 | 简单计数、兼容老站 | 常规接口交互 | 埋点、日志上报 |
需要拿响应结果做后续处理的普通接口,继续用 fetch。
sendBeacon 返回 true 只表示数据成功进入浏览器的发送队列,不代表服务端一定收到了。可靠性要求高的数据,服务端要做好去重和补录的容灾设计。🛠 串起来:一个最小可用的埋点工具
// 公共信息:每条数据都带上,省去每次手写
const common = {
page: location.pathname,
time: Date.now(),
};
// 统一上报:优先 sendBeacon,不支持的旧环境降级 fetch
function report(data) {
const body = JSON.stringify(data);
if (navigator.sendBeacon) {
navigator.sendBeacon('/collect', new Blob([body], { type: 'application/json' }));
} else {
fetch('/collect', { method: 'POST', body, keepalive: true });
}
}
// 对外暴露的采集函数
function track(event, data = {}) {
report({ event, ...common, ...data });
}
// 点击埋点
document.querySelector('#buy-btn').addEventListener('click', () => {
track('click', { button: 'buy' });
});
// 页面离开前,上报停留时长
const start = Date.now();
window.addEventListener('pagehide', () => {
track('stay', { duration: Math.round((Date.now() - start) / 1000) });
});
两个代码里看不出来的细节:降级分支的 keepalive: true 让浏览器在页面卸载后也把请求发完;离开页面监听 pagehide 而不是 beforeunload,前者在移动端触发更可靠。
🧾 小节总结
- 埋点就是在关键位置写记录代码,链路是:采集 → 上报 → 分析。
- 手动埋点精确灵活但要手写,无埋点省事但业务语义弱,核心指标用手动埋点。
- 曝光埋点用 IntersectionObserver,进入可视区域上报一次,记得
unobserve防重复统计。 - 页面卸载时普通异步请求会被取消,
sendBeacon由浏览器接管发送、关闭页面也能送达,埋点上报优先用它。 - fetch 加
keepalive: true是降级补救,离开页面的监听用pagehide。
❓ 知识问答
Q1:图片打点为什么服务端要返回 1x1 像素的图?
A:打点请求的目的只是让服务端收到参数,响应内容没人看。返回极小的空白图,是为了让请求正常完成、不报错,同时体积尽量小。
Q2:在 beforeunload 里用 fetch 上报,为什么数据经常丢?
A:页面开始卸载后,浏览器会取消还没完成的异步请求。改用 sendBeacon 或 fetch 加 keepalive: true,浏览器才会承诺把请求发完。
Q3:sendBeacon 有数据大小限制吗?
A:有,常见上限在 64KB 左右。单条埋点数据很小,正常用碰不到;真要上报大段日志,应分批发送或压缩后再发。
Q4:无埋点能完全替代手动埋点吗?
A:不能。无埋点收集的是「某个元素被点了」这类原始事件,分不清是「购买商品 A」还是「购买商品 B」,核心业务指标还是建议手动埋点。
Q5:曝光埋点会不会把「快速划过」的模块也算成曝光?
A:会,进入可视区域一瞬间就会触发。只认「停留了一段时间」的有效曝光时,可以在回调里延迟几百毫秒再上报,期间元素离开就取消。
🧪 小练习
练习一:给「立即购买」按钮补上点击埋点,要求带上按钮标识和当前页面路径:
function track(event, data) {
console.log('采集到事件:', event, data);
}
document.querySelector('#buy-btn').addEventListener('click', () => {
// 请在这里编写代码:上报一次点击事件
});
练习二:下面这个上报函数用的是普通 fetch,页面关闭时数据容易丢,把它改成 sendBeacon 的写法(记得处理旧浏览器的降级):
function report(data) {
// 请在这里编写代码:优先 sendBeacon,降级 fetch + keepalive
fetch('/collect', { method: 'POST', body: JSON.stringify(data) });
}
完成后想想:为什么 sendBeacon 分支里要包一层 Blob 并声明 application/json 类型?直接传字符串时,服务端读到的 Content-Type 会是什么?
🎉 恭喜你已经掌握埋点与数据上报技能啦!下一篇我们把目光转向「页面跑得有多快」,看看性能数据是怎么采集和上报的。
