很多项目一开始做埋点时,代码通常很简单:
track('button_click', {
buttonName: 'submit'
});
但真正上线后会发现,埋点不是"调一次接口"这么简单。
实际问题往往包括:
页面关闭时数据没发出去
用户连续点击导致重复上报
曝光事件触发太频繁
接口请求太多影响性能
网络异常时数据丢失
字段命名混乱,后期难以分析
所以,一个可维护的前端埋点方案,至少要考虑事件规范、采集时机、批量发送、失败重试和数据去重。
一、先定义事件模型
埋点最怕字段随手写。
例如:
track('click', {
btn: 'start'
});
另一个页面又写成:
track('buttonClick', {
button_name: 'start'
});
后期分析时会非常痛苦。
更推荐先定义统一事件结构:
{
"eventId": "evt_001",
"eventName": "button_click",
"timestamp": 1718000000000,
"page": "/dashboard",
"userId": "u_10001",
"sessionId": "s_abc",
"properties": {
"buttonName": "start_translation"
}
}
常见基础字段包括:
| 字段 | 说明 |
|---|---|
| eventId | 单条事件唯一 ID |
| eventName | 事件名称 |
| timestamp | 事件发生时间 |
| page | 当前页面 |
| userId | 用户 ID,未登录可为空 |
| sessionId | 会话 ID |
| properties | 业务自定义参数 |
这样无论是点击、曝光还是功能使用事件,都可以放进统一结构里。
二、点击埋点:不要散落在业务代码里
最直接的写法是:
button.onclick = () => {
track('button_click', {
buttonName: 'export'
});
exportFile();
};
这种方式能用,但项目变大后,埋点逻辑会散落在大量业务代码中。
更好的方式是通过自定义属性声明:
<button
data-track="button_click"
data-track-name="export_order"
>
导出订单
</button>
统一监听:
document.addEventListener('click', event => {
const target = event.target.closest('[data-track]');
if (!target) return;
track(target.dataset.track, {
name: target.dataset.trackName
});
});
这样做的好处是:
业务逻辑和埋点逻辑解耦
页面结构更清晰
新增点击埋点成本较低
三、曝光埋点:使用 IntersectionObserver
曝光埋点常用于统计模块是否被用户看到。
不要使用滚动事件频繁计算元素位置。
更推荐使用 IntersectionObserver:
const observer = new IntersectionObserver(
entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const el = entry.target;
track('module_exposure', {
moduleName: el.dataset.module
});
observer.unobserve(el);
}
});
},
{
threshold: 0.5
}
);
页面元素:
<div data-module="pricing_card">
套餐卡片
</div>
初始化:
document
.querySelectorAll('[data-module]')
.forEach(el => observer.observe(el));
这里的 threshold: 0.5 表示元素至少 50% 进入视口后才算曝光。
如果一个模块只需要统计一次,触发后可以 unobserve,避免重复上报。
四、为什么需要批量上报?
如果每个事件都立即请求接口:
点击一次 → 请求一次
曝光一次 → 请求一次
滚动一次 → 请求一次
请求数量会非常多。
更合理的方式是先放入队列,定时批量发送:
const queue = [];
function track(eventName, properties = {}) {
queue.push({
eventId: crypto.randomUUID(),
eventName,
timestamp: Date.now(),
page: location.pathname,
properties
});
if (queue.length >= 20) {
flush();
}
}
批量发送:
async function flush() {
if (queue.length === 0) return;
const events = queue.splice(0, queue.length);
try {
await fetch('/api/track', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(events)
});
} catch (error) {
queue.unshift(...events);
}
}
再加一个定时器:
setInterval(flush, 5000);
这样可以减少请求次数,也能降低埋点对业务接口的影响。
五、页面关闭时如何减少数据丢失?
用户关闭页面时,普通 fetch 请求可能还没发完,页面就已经卸载。
这时可以使用 navigator.sendBeacon:
function flushByBeacon() {
if (queue.length === 0) return;
const events = queue.splice(0, queue.length);
const blob = new Blob(
[JSON.stringify(events)],
{
type: 'application/json'
}
);
navigator.sendBeacon('/api/track', blob);
}
监听页面隐藏:
window.addEventListener('pagehide', () => {
flushByBeacon();
});
相比 beforeunload,pagehide 在移动端和浏览器缓存场景中通常更适合。
但要注意,sendBeacon 适合发送少量数据,不要在页面关闭时塞入过大的埋点队列。
六、失败重试与本地缓存
如果网络异常,埋点请求失败,可以临时保存到 localStorage。
function saveFailedEvents(events) {
const oldEvents = JSON.parse(
localStorage.getItem('track_failed') || '[]'
);
const merged = oldEvents.concat(events).slice(-200);
localStorage.setItem(
'track_failed',
JSON.stringify(merged)
);
}
下次页面加载时再尝试发送:
function restoreFailedEvents() {
const events = JSON.parse(
localStorage.getItem('track_failed') || '[]'
);
if (events.length > 0) {
queue.push(...events);
localStorage.removeItem('track_failed');
flush();
}
}
这里要限制最大数量,例如只保留最近 200 条,避免本地存储无限增长。
七、后端也要做去重
前端失败重试可能导致同一批事件被重复发送。
所以每条事件最好有唯一 eventId。
后端可以基于:
eventId
userId
eventName
timestamp
做去重处理。
如果数据先进入消息队列,也可以在消费者侧做幂等判断。
不要假设前端只会上报一次。
真实环境中,刷新、重试、网络抖动和多标签页都可能造成重复数据。
八、业务埋点不要只看按钮点击
很多团队只统计点击量,但真正有价值的往往是完整路径。
例如一个实时翻译产品,可以统计:
开始翻译
选择语言
开启悬浮字幕
开启语音播报
生成会议总结
导出会议记录
任务异常结束
以**同言翻译(Transync AI)**这类产品为例,如果只看"开始翻译"按钮点击量,很难判断用户实际是否完成了跨语言会议流程。
更有价值的事件链路可能是:
start_translation
→ subtitle_enabled
→ voice_playback_enabled
→ meeting_summary_generated
这样才能分析用户在哪一步流失,以及哪些功能真正被使用。
九、埋点字段要避免记录敏感信息
埋点不是日志,也不应该成为隐私数据收集入口。
不要上报:
密码
Token
完整手机号
身份证号
会议原文内容
用户输入的完整文本
可以上报结构化状态:
{
"eventName": "meeting_summary_generated",
"properties": {
"duration": 1800,
"languagePair": "en-zh",
"success": true
}
}
而不是上报完整会议内容。
埋点的目标是分析行为,不是保存用户数据。
十、埋点检查清单
上线前可以检查:
1. 是否有统一事件命名规范
2. 是否包含 eventId
3. 点击埋点是否与业务逻辑解耦
4. 曝光埋点是否避免重复触发
5. 是否支持批量上报
6. 页面关闭时是否使用 sendBeacon
7. 失败事件是否有临时缓存
8. 本地缓存是否有数量上限
9. 后端是否做幂等去重
10. 是否避免采集敏感内容
总结
前端埋点的核心不是"多打几个 track",而是建立一套稳定的数据采集链路。
一个基础方案至少应该包含:
统一事件模型
点击和曝光采集
批量发送
页面关闭补发
失败重试
后端去重
敏感信息控制
当埋点体系足够清晰后,数据分析才能真正回答问题:
用户用了什么?
在哪一步流失?
哪个功能值得继续优化?
这比单纯统计 PV、UV 更接近产品和工程决策的真实需求。

