第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()
    });
});
避坑指南:我曾经在某个项目中,页面还没渲染完就触发了 PV 埋点,导致上报的 DOM 信息不完整。后来我加了个 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);
});
关键点:曝光埋点一定要设置合理的阈值。我见过有人设成 0,结果元素刚出现 1 像素就算曝光了,数据严重失真。我个人习惯设 0.5,也就是元素露出一半才算真正被看到。

还有一个细节:曝光埋点通常只上报一次。用户反复滑动页面,同一个元素被看到多次,你总不能每次都上报吧?那数据就乱套了。

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));
});
注意:移动端没有 hover 事件,得用 touchstart/touchend 来模拟。而且用户手指滑动时,可能会误触多个元素。我一般会加一个 300ms 的防抖,避免短时间内的误报。

4.6 埋点数据的校验

埋点写完了,怎么知道对不对?我见过太多人写完埋点就上线,结果数据全是脏的。

我的习惯是:

  1. 本地调试:在控制台打印所有埋点事件,肉眼检查参数是否正确
  2. 数据对比:用埋点上报的 PV 数和服务器日志的请求数做对比,偏差超过 5% 就要排查
  3. 异常检测:检查有没有异常值,比如停留时长出现负数、点击坐标超出屏幕范围等

我曾经因为埋点参数名写错了,导致推荐系统拿到的用户 ID 全是 undefined,整整一周的推荐效果都是随机乱推的。从那以后,我每次上线前都会写一个自动化测试脚本,模拟用户操作,验证埋点数据是否完整。

好了,前端埋点的核心实践就这些。下一章我们会聊聊后端埋点——服务端的日志采集和清洗。到时候你会发现,前端埋点只是冰山一角,真正的挑战还在后面。