前端监控简介

埋点与数据上报

掌握常见的埋点方式与数据上报手段,学会用 sendBeacon 可靠地把数据发到服务端。

🎯 引言

上一篇收集了「页面出了什么错」,本篇解决另一个问题:用户在页面上做了什么。学完本篇,你将能够:

  • 分清手动埋点和无埋点的区别,知道什么场景用哪种。
  • 写出基础的点击埋点和曝光埋点代码。
  • navigator.sendBeacon 把数据可靠地发到服务端,页面关闭也不丢。

🧱 常见的埋点方式

埋点就是在想统计的位置写一段记录代码,把用户行为收集上来,就像超市统计顾客在哪个货架前停留、拿起了什么商品。完整链路是「采集 → 上报 → 分析」,本文负责前两步:怎么埋、怎么发。

埋法主要有两种:手动埋点精确灵活但要手写代码,无埋点省事但缺少业务语义,核心业务指标建议用手动埋点。

手动埋点(代码埋点):在想统计的位置手写采集代码,比如统计按钮点击量:

monitor.js
// 封装的统一采集函数,后面会讲它内部怎么上报
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 在 性能优化课程 的懒加载一篇详细讲过,这里直接用:

index.html
<!-- 要统计曝光的 banner -->
<div class="banner" data-track="home-banner">夏日活动</div>
exposure.js
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 请求:

report-image.js
function report(data) {
    const img = new Image();
    // 服务端收到这个请求就算一条数据,返回一张 1x1 像素的空白图
    img.src = '/collect?data=' + encodeURIComponent(JSON.stringify(data));
}

简单、天然跨域,但只能发 GET、受 URL 长度限制,请求成没成功也无从得知。

fetch:灵活可控,但页面卸载时没发完的请求会被取消,不适合离开页面时上报。

sendBeacon:浏览器专为「页面卸载时也要把数据发出去」设计的 API:

report-beacon.js
function report(data) {
    const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
    navigator.sendBeacon('/collect', blob);
}

调用后浏览器接管这次发送、排队处理,用户立刻关闭页面,数据也会在后台继续发出。就像把包裹交给快递员,之后的路不用你管。

对比项图片打点fetchsendBeacon
请求方式只能 GET任意POST
页面关闭时可能丢失大概率被取消浏览器保证继续发送
数据大小受 URL 长度限制较大有上限(约 64KB)
适用场景简单计数、兼容老站常规接口交互埋点、日志上报

需要拿响应结果做后续处理的普通接口,继续用 fetch。

sendBeacon 返回 true 只表示数据成功进入浏览器的发送队列,不代表服务端一定收到了。可靠性要求高的数据,服务端要做好去重和补录的容灾设计。

🛠 串起来:一个最小可用的埋点工具

monitor.js
// 公共信息:每条数据都带上,省去每次手写
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 会是什么?


🎉 恭喜你已经掌握埋点与数据上报技能啦!下一篇我们把目光转向「页面跑得有多快」,看看性能数据是怎么采集和上报的。