第4章:前端埋点实践
前端埋点,说白了就是在前端代码里「安插眼线」。用户点了哪里、看了什么、停留了多久,这些行为数据都得靠埋点来捕获。我做了这么多年推荐系统,可以负责任地告诉你——埋点质量直接决定了推荐效果的上限。数据不准,再牛的算法也是白搭。
4.1 前端事件监听机制
先聊聊事件监听。这是所有埋点的基础,你总得知道用户干了什么吧?
我个人习惯用 addEventListener,而不是直接在 HTML 里写 onclick。为什么?因为前者可以绑定多个处理函数,而且更容易解耦。举个例子:
// 不推荐的做法
<button onclick="trackClick()">点击</button>
// 推荐的做法
document.getElementById('myBtn').addEventListener('click', function(e) {
// 埋点逻辑
sendEvent('button_click', { id: 'myBtn' });
});
我在项目中遇到过一个问题:同一个按钮绑定了多个事件,结果埋点被重复触发了。后来我养成了一个习惯——用事件委托来统一管理。把监听器挂到父容器上,通过 e.target 判断具体元素。这样既减少了监听器数量,也方便统一处理。
4.2 页面浏览埋点
页面浏览埋点,也叫 PV 埋点。这个看起来简单,但坑不少。
最基础的做法是在页面加载时触发一次上报:
window.addEventListener('load', function() {
sendEvent('page_view', {
url: window.location.href,
referrer: document.referrer,
timestamp: Date.now()
});
});
嗯,这里要注意——单页应用(SPA)不能这么搞。SPA 的页面切换不会触发 load 事件。我记得有一次接手一个 React 项目,PV 数据一直对不上,排查了半天才发现是路由变化没被捕获。
对于 SPA,我建议监听路由变化:
// Vue Router 示例
router.afterEach((to, from) => {
sendEvent('page_view', {
from: from.path,
to: to.path,
timestamp: Date.now()
});
});
requestIdleCallback 或者 setTimeout 0,确保页面内容已经呈现。
4.3 点击埋点
点击埋点是最常见的交互埋点。但你真的会做吗?
很多人直接给每个按钮写一个 onClick 埋点,代码冗余不说,还容易遗漏。我推荐用「声明式埋点」的方式——通过自定义属性来标记需要埋点的元素:
<button data-track="true" data-track-id="submit_btn">提交</button>
// 统一监听
document.addEventListener('click', function(e) {
const target = e.target.closest('[data-track]');
if (target) {
sendEvent('click', {
id: target.dataset.trackId,
text: target.innerText,
x: e.clientX,
y: e.clientY
});
}
});
这样做的好处是:埋点逻辑和业务逻辑完全分离。前端同学只需要在 HTML 上加几个属性,埋点 SDK 自动处理上报。我在团队里推广这个方案后,埋点的覆盖率从 60% 提升到了 95% 以上。
你可能会问:点击位置坐标有什么用?其实在推荐系统中,这个数据很有价值。比如用户点击了推荐列表的第几个位置,能反映出用户的注意力分布。
4.4 曝光埋点
曝光埋点,就是记录「某个元素是否被用户看到了」。这在推荐场景中特别重要——用户没看到的内容,你就不该算作「推荐失败」。
实现曝光埋点,核心是判断元素是否在可视区域内。我常用的方案是 IntersectionObserver:
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const el = entry.target;
sendEvent('exposure', {
id: el.dataset.trackId,
visibleRatio: entry.intersectionRatio
});
// 只上报一次
observer.unobserve(el);
}
});
}, {
threshold: 0.5 // 元素露出50%才算曝光
});
// 监听所有需要曝光的元素
document.querySelectorAll('[data-exposure]').forEach(el => {
observer.observe(el);
});
还有一个细节:曝光埋点通常只上报一次。用户反复滑动页面,同一个元素被看到多次,你总不能每次都上报吧?那数据就乱套了。
4.5 停留时长埋点
停留时长,反映的是用户对内容的「沉浸度」。这个指标在推荐系统中很常用——停留时间长的内容,说明用户感兴趣。
实现思路其实不复杂:记录进入时间,离开时计算差值。但难点在于「离开」怎么定义?
let enterTime = Date.now();
// 页面进入
document.addEventListener('visibilitychange', function() {
if (document.visibilityState === 'visible') {
enterTime = Date.now();
} else {
const duration = Date.now() - enterTime;
sendEvent('stay_duration', {
duration: duration,
url: window.location.href
});
}
});
这里用了 visibilitychange 事件,而不是 beforeunload。为什么?因为用户切到其他标签页再切回来,beforeunload 根本不会触发。我踩过这个坑,数据少了一大截。
对于推荐列表中的单个 item,停留时长需要更精细的计算:
// 记录每个推荐项的停留时间
const stayTimers = new Map();
function startStayTrack(itemId) {
stayTimers.set(itemId, Date.now());
}
function endStayTrack(itemId) {
const start = stayTimers.get(itemId);
if (start) {
const duration = Date.now() - start;
sendEvent('item_stay', { itemId, duration });
stayTimers.delete(itemId);
}
}
// 结合曝光和离开事件
document.querySelectorAll('[data-item-id]').forEach(el => {
el.addEventListener('mouseenter', () => startStayTrack(el.dataset.itemId));
el.addEventListener('mouseleave', () => endStayTrack(el.dataset.itemId));
});
4.6 埋点数据的校验
埋点写完了,怎么知道对不对?我见过太多人写完埋点就上线,结果数据全是脏的。
我的习惯是:
- 本地调试:在控制台打印所有埋点事件,肉眼检查参数是否正确
- 数据对比:用埋点上报的 PV 数和服务器日志的请求数做对比,偏差超过 5% 就要排查
- 异常检测:检查有没有异常值,比如停留时长出现负数、点击坐标超出屏幕范围等
我曾经因为埋点参数名写错了,导致推荐系统拿到的用户 ID 全是 undefined,整整一周的推荐效果都是随机乱推的。从那以后,我每次上线前都会写一个自动化测试脚本,模拟用户操作,验证埋点数据是否完整。
好了,前端埋点的核心实践就这些。下一章我们会聊聊后端埋点——服务端的日志采集和清洗。到时候你会发现,前端埋点只是冰山一角,真正的挑战还在后面。