用户行为埋点
埋点是产品决策的数据基础。没有埋点,老板问「这功能有没有人用」都答不上来。
一、避坑指南
先想清楚再写代码:
- ❌ 边写边埋 → 数据零散、字段不统一
- ✅ 先画事件表(事件名 + 触发时机 + 携带字段)→ 再开发
事件名要规范:
❌ bad:click_buy、userClickedBuy、点击购买、buyBtn_click
✅ good:button_click(动作)+ module=shop + target=buy(属性)必须带
userId和sessionId:track("button_click", {
userId, // 必带
sessionId, // 必带
module: "shop", // 必带
target: "buy",
});PV / UV 不要重复埋:
- 路由切换算一次 PV
- 同会话不重复算 UV
关键节点要服务端埋:
场景 客户端埋点 服务端埋点 按钮点击 ✅ ❌ 太重 订单创建 ❌ 用户可能刷新不报 ✅ 以服务端为准 支付成功 ❌ 不准 ✅ 页面访问 ✅ ❌ SSR 才好做 不要全埋点当默认:
- 全埋点(无埋码)省开发但数据噪音大
- 关键路径走代码埋点 + 辅助探索用全埋点
二、埋点分类
| 分类 | 方式 | 优点 | 缺点 | 适用 |
|---|---|---|---|---|
| 代码埋点 | 手动 track("xxx", data) | 精准、可控 | 开发量大 | 关键路径 |
| 全埋点 | 自动采集所有点击/曝光 | 省开发 | 数据噪音大、成本高 | 辅助探索 |
| 服务端埋点 | 后端关键节点上报 | 最准 | 不能采前端交互 | 订单/支付 |
2025+ 趋势:代码埋点为主 + 服务端埋点兜底关键节点,全埋点作为探索工具。
三、数据模型设计
3.1 必备字段
interface TrackEvent {
// 身份
userId: string; // 用户 ID(未登录用 anonymousId)
anonymousId: string; // 匿名 ID(localStorage 持久化)
sessionId: string; // 会话 ID
deviceId: string; // 设备 ID
// 事件
event: string; // 事件名(button_click、page_view)
timestamp: number; // 毫秒时间戳
// 来源
url: string; // 当前页面
referrer: string; // 来源
appVersion: string; // 客户端版本
env: string; // dev / production
platform: "web" | "ios" | "android";
// 上下文
properties: { // 业务字段
module?: string; // 业务模块
target?: string; // 操作对象
value?: number; // 数值
[key: string]: any; // 其他自定义
};
}
3.2 事件命名规范
通用事件(前端框架可自动采集):
| 事件 | 触发时机 | 携带字段 |
|---|---|---|
page_view | 路由切换 / 首屏 | url, referrer, duration |
button_click | 按钮点击 | module, target, text |
form_submit | 表单提交 | form, success, error_code |
element_exposure | 元素曝光(视口中) | module, target, position |
app_launch | App 启动 | from, is_first_visit |
app_exit | App 退出 | duration |
业务事件(按业务定义):
| 业务 | 事件 | 字段 |
|---|---|---|
| 电商 | product_view | product_id, category, price |
| 电商 | add_to_cart | product_id, quantity |
| 电商 | order_create | order_id, amount |
| 电商 | payment_success | order_id, amount, channel |
| 内容 | article_read | article_id, duration, scroll_percent |
| 营销 | coupon_claim | coupon_id, source |
3.3 事件表模板
# events.yaml(事件字典)
events:
- name: button_click
desc: 按钮点击
module: 通用
properties:
- module: string
- target: string
- text: string
- name: order_create
desc: 订单创建
module: 电商
properties:
- order_id: string
- amount: number
- item_count: number
四、代码埋点实战
4.1 封装 track 函数
// lib/track.ts
import { logger } from "./logger";
interface TrackEvent {
event: string;
properties?: Record<string, any>;
}
class Tracker {
private userId = "";
private anonymousId = localStorage.getItem("anonymous_id") || (() => {
const id = crypto.randomUUID();
localStorage.setItem("anonymous_id", id);
return id;
})();
private sessionId = crypto.randomUUID();
setUser(userId: string) {
this.userId = userId;
}
track(event: string, properties: Record<string, any> = {}) {
// 1. 上报到埋点服务
fetch("/api/track", {
method: "POST",
body: JSON.stringify({
event,
userId: this.userId,
anonymousId: this.anonymousId,
sessionId: this.sessionId,
timestamp: Date.now(),
url: location.href,
properties,
}),
keepalive: true,
});
// 2. 同步到 console(开发环境)
if (process.env.NODE_ENV === "development") {
console.log(`[track] ${event}`, properties);
}
}
}
export const tracker = new Tracker();
// React Hook 封装
export function useTrack() {
return useCallback((event: string, properties?: any) => {
tracker.track(event, properties);
}, []);
}
4.2 路由 PV 自动采集
// app/layout.tsx (Next.js)
import { usePathname } from "next/navigation";
import { useEffect } from "react";
import { tracker } from "@/lib/track";
export function PageviewTracker() {
const pathname = usePathname();
useEffect(() => {
tracker.track("page_view", { url: pathname });
}, [pathname]);
return null;
}
4.3 组件曝光埋点
// hooks/useExposure.ts
import { useEffect, useRef } from "react";
import { tracker } from "@/lib/track";
export function useExposure(
eventName: string,
properties: Record<string, any>,
options: IntersectionObserverInit = { threshold: 0.5 }
) {
const ref = useRef<HTMLElement>(null);
const reported = useRef(false);
useEffect(() => {
if (!ref.current) return;
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting && !reported.current) {
tracker.track(eventName, properties);
reported.current = true;
observer.disconnect();
}
}, options);
observer.observe(ref.current);
return () => observer.disconnect();
}, [eventName, JSON.stringify(properties)]);
return ref;
}
// 用法
function Banner() {
const ref = useExposure("banner_exposure", { banner_id: "b1" });
return <div ref={ref}>...banner...</div>;
}
五、ABTest 流量分配
// lib/abtest.ts
function getBucket(userId: string, experimentKey: string): "control" | "treatment" {
// 一致性 hash:相同 userId 永远进同一组
const hash = simpleHash(`${userId}-${experimentKey}`);
return hash % 2 === 0 ? "control" : "treatment";
}
// 用法
const bucket = getBucket(user.id, "new_checkout_flow");
if (bucket === "treatment") {
// 新流程
} else {
// 旧流程
}
关键:同一个 user + 同一个 experiment 必须永远进同一组,否则用户看到流程跳变体验崩坏。
六、推荐方案
| 方案 | 优势 | 劣势 | 适合 |
|---|---|---|---|
| 神策 | 国内最成熟、可私有化 | 贵 | 中大型企业 |
| GrowingIO | 产品体验好 | 贵 | 增长团队 |
| 友盟 | 免费基础版 | 数据深度有限 | 中小项目 / 创业 |
| Mixpanel | 海外主流、事件分析强 | 国内访问慢 | 海外业务 |
| PostHog | 开源、Self-host | 需运维 | 技术团队 |
| 自研 + ClickHouse | 完全可控 | 投入大 | 大厂 |
2025+ 推荐:国内业务神策(或 GrowingIO),海外业务 Mixpanel 或 PostHog。
七、面试高频
Q1:代码埋点 vs 全埋点?
A:
- 代码埋点:精准、可控,开发量大,关键路径必选
- 全埋点:自动采集所有点击/曝光,省开发,数据噪音大、成本高
- 实践:关键路径走代码埋点 + 探索分析用全埋点
Q2:客户端埋点和服务端埋点怎么选?
A:
- 客户端:前端交互(点击、曝光、滚动)
- 服务端:业务关键节点(订单、支付)——客户端不可信(用户可能刷、不一定到)
- 原则:涉及钱、订单的必须服务端埋点
Q3:怎么保证数据准确性?
A:① 事件表统一管理(不发散);② 客户端 + 服务端双埋点对账;③ 异常数据过滤(同一事件 1s 内 100 次 = 异常);④ 抽样校验(服务端日志对照)。
Q4:ABTest 怎么分流?
A:一致性 hash——根据 userId + experimentKey 算 hash 取模,保证同用户同实验永远进同组。实验期间不要中途改流量比例(用户跳变体验崩坏)。
Q5:PV / UV 怎么算?
A:
- PV(Page View):页面访问次数,每次进入算一次
- UV(Unique Visitor):独立访客数,按 userId 或 anonymousId 去重
- 前端实现:路由切换 +1 PV,首次进入记录 userId