埋点事件命名、上报规范
- Requirement:事件名使用小写字母 + 下划线(snake_case),格式为
模块_动作_对象。
- Requirement:事件名必须见名知意,禁止使用
event1、click 等无意义名称。
│ │ └── 对象:被操作的元素,如 button、modal、list
│ └─────── 动作:用户行为,如 click、view、submit、scroll
└──────────── 模块:业务模块,如 user、order、login
| 事件名 | 说明 |
|---|
user_click_login_button | 用户点击登录按钮 |
user_view_login_page | 用户查看登录页 |
order_submit_order_form | 用户提交订单表单 |
order_view_order_list | 用户查看订单列表 |
product_click_add_cart | 用户点击加入购物车 |
- Standard:动作词统一使用以下词汇,禁止混用同义词:
| 动作 | 英文 | 说明 |
|---|
| 点击 | click | 用户点击元素 |
| 查看 | view | 页面或元素曝光 |
| 提交 | submit | 提交表单 |
| 滚动 | scroll | 滚动到指定位置 |
| 搜索 | search | 执行搜索 |
| 筛选 | filter | 设置筛选条件 |
| 切换 | switch | 切换 Tab/状态 |
| 悬停 | hover | 鼠标悬停 |
| 反模式 | 问题 | 建议 |
|---|
事件名用驼峰 userClickLogin | 与后端约定不一致 | 使用 snake_case |
事件名 click1、event_xxx | 无法理解含义 | 使用 模块_动作_对象 |
动作词混用 tap/press/click | 数据分析困难 | 统一用 click |
- Requirement:埋点上报必须统一封装,禁止在业务代码中直接调用第三方 SDK。
- Standard:封装
track() 方法,统一处理公共参数、批量上报、错误兜底。
// src/utils/track.ts
interface TrackEvent {
event: string;
properties?: Record<string, any>;
}
const queue: TrackEvent[] = [];
const FLUSH_INTERVAL = 5000; // 5 秒批量上报
const MAX_QUEUE_SIZE = 20; // 满 20 条立即上报
function flush() {
if (queue.length === 0) return;
const events = queue.splice(0, queue.length);
navigator.sendBeacon('/api/track', JSON.stringify({ events }));
}
// 定时批量上报
setInterval(flush, FLUSH_INTERVAL);
export function track(event: string, properties: Record<string, any> = {}) {
const commonProperties = {
user_id: getUserStore().id,
session_id: getSessionId(),
timestamp: Date.now(),
page_url: window.location.href,
...getCommonProperties(),
};
queue.push({
event,
properties: { ...commonProperties, ...properties },
});
// 队列满立即上报
if (queue.length >= MAX_QUEUE_SIZE) {
flush();
}
}
| 事件类型 | 上报时机 |
|---|
| 页面浏览(view) | 页面加载完成后 |
| 点击(click) | 点击事件触发时 |
| 表单提交(submit) | 提交成功后(失败不上报) |
| 曝光(view) | 元素进入视口时(用 IntersectionObserver) |
| 离开页面 | visibilitychange 或 pagehide 时 flush |
- Standard:元素曝光使用
IntersectionObserver 判断,避免用 scroll 事件监听。
- Standard:同一元素只上报一次曝光,用 Set 记录已曝光的元素。
import { useEffect, useRef } from 'react';
import { track } from '@/utils/track';
function useExposureTrack(event: string, properties?: Record<string, any>) {
const ref = useRef<HTMLDivElement>(null);
const tracked = useRef(false);
useEffect(() => {
if (!ref.current || tracked.current) return;
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting && !tracked.current) {
track(event, properties);
tracked.current = true;
observer.disconnect();
}
});
},
{ threshold: 0.5 }
);
observer.observe(ref.current);
return () => observer.disconnect();
}, [event, properties]);
return ref;
}
| 反模式 | 问题 | 建议 |
|---|
| 业务代码直接调用 SDK | 耦合、难以更换 | 统一封装 track() |
| 每条事件立即上报 | 请求过多 | 批量上报 |
| 曝光用 scroll 事件判断 | 性能差 | 用 IntersectionObserver |
| 上报失败无重试 | 数据丢失 | 失败重试或存入队列 |
- Requirement:所有事件必须携带以下公共参数,由封装层自动注入,业务代码无需传入。
| 参数 | 类型 | 说明 |
|---|
| user_id | string | 用户 ID,未登录为匿名 ID |
| session_id | string | 会话 ID |
| timestamp | number | 事件时间戳(毫秒) |
| page_url | string | 当前页面 URL |
| referrer | string | 来源页面 |
| platform | string | 平台:web / h5 / mini |
| app_version | string | 应用版本 |
- Standard:业务参数使用 snake_case,类型为基本类型(string/number/boolean),禁止传对象或数组。
- Standard:参数值必须是枚举或有限值,禁止传自由文本(如用户输入的搜索词需脱敏后上报)。
// 正确
track('order_submit_order_form', {
order_type: 'normal', // 枚举值
total_amount: 199.00, // 数字
item_count: 3, // 数字
pay_method: 'alipay', // 枚举值
});
// 错误
track('order_submit_order_form', {
order: { ... }, // 传了对象
items: [ ... ], // 传了数组
remark: '请尽快发货', // 自由文本
});
- Requirement:禁止上报手机号、身份证、密码、银行卡号等敏感信息,必须脱敏或不上报。
// 正确:脱敏
track('user_view_profile', {
phone: maskPhone(user.phone), // 138****1234
});
// 错误:明文上报
track('user_view_profile', {
phone: user.phone, // 13812341234
});
| 反模式 | 问题 | 建议 |
|---|
| 参数传对象/数组 | 后端难以解析 | 扁平化基本类型 |
| 上报明文手机号 | 隐私合规风险 | 脱敏处理 |
| 参数名用驼峰 | 与后端约定不一致 | 统一 snake_case |
| 参数值为自由文本 | 难以统计 | 枚举化或脱敏 |
- Requirement:开发环境在控制台输出埋点日志,格式为
[track] 事件名 {参数},便于核对。
- Standard:开发环境提供可视化埋点调试面板,展示最近上报的事件列表。
export function track(event: string, properties: Record<string, any> = {}) {
if (import.meta.env.DEV) {
console.log(`[track] ${event}`, properties);
}
// ... 上报逻辑
}
- Standard:提供全局开关控制埋点上报,开发环境可关闭上报仅打印日志。
- Caution:生产环境必须开启上报,且日志打印必须关闭。
const TRACK_ENABLED = import.meta.env.PROD ? true : false;
export function track(event: string, properties: Record<string, any> = {}) {
if (import.meta.env.DEV) {
console.log(`[track] ${event}`, properties);
if (!TRACK_ENABLED) return;
}
// 上报逻辑...
}
- Suggestion:开发环境在控制台校验事件名和参数是否符合规范,不符合时给出警告。
const VALID_EVENTS = new Set([
'user_click_login_button',
'user_view_login_page',
// ...
]);
function validate(event: string) {
if (!VALID_EVENTS.has(event)) {
console.warn(`[track] 未注册的事件: ${event}`);
}
}
| 反模式 | 问题 | 建议 |
|---|
| 开发环境无日志 | 难以核对埋点 | 控制台输出日志 |
| 生产环境打印日志 | 信息泄露、性能损耗 | 生产关闭日志 |
| 无埋点开关 | 测试环境产生脏数据 | 提供开关控制 |
- Requirement:所有埋点事件必须在事件字典(
track-events.ts)中注册,包含事件名、说明、参数定义。
- Standard:事件字典作为埋点的单一数据源,开发时从中引入,避免硬编码事件名。
// src/constants/track-events.ts
export const TrackEvents = {
USER_CLICK_LOGIN_BUTTON: 'user_click_login_button',
USER_VIEW_LOGIN_PAGE: 'user_view_login_page',
ORDER_SUBMIT_ORDER_FORM: 'order_submit_order_form',
} as const;
export interface TrackEventParams {
[TrackEvents.USER_CLICK_LOGIN_BUTTON]: {
login_method: 'password' | 'sms' | 'oauth';
};
[TrackEvents.ORDER_SUBMIT_ORDER_FORM]: {
order_type: 'normal' | 'presale';
total_amount: number;
pay_method: 'alipay' | 'wechat';
};
}
- Standard:利用 TypeScript 类型系统,在编译时校验事件名和参数是否匹配。
export function track<K extends keyof TrackEventParams>(
event: K,
properties: TrackEventParams[K]
) {
// ...
}
// 正确
track(TrackEvents.USER_CLICK_LOGIN_BUTTON, { login_method: 'password' });
// 错误:参数类型不匹配
track(TrackEvents.USER_CLICK_LOGIN_BUTTON, { login_method: 'face' });
- Standard:后端对上报的事件做参数校验,不符合规范的事件丢弃并记录告警。
- Caution:前端上报失败时静默处理,不影响主流程,避免埋点故障导致业务不可用。
| 反模式 | 问题 | 建议 |
|---|
| 事件名硬编码散落各处 | 难以维护、易写错 | 统一在事件字典注册 |
| 无类型校验 | 参数遗漏或传错 | 用 TypeScript 约束 |
| 埋点报错阻断业务 | 影响用户体验 | 埋点失败静默处理 |