Skip to main content

用户行为埋点

埋点是产品决策的数据基础。没有埋点,老板问「这功能有没有人用」都答不上来

一、避坑指南

  1. 先想清楚再写代码

    • ❌ 边写边埋 → 数据零散、字段不统一
    • 先画事件表(事件名 + 触发时机 + 携带字段)→ 再开发
  2. 事件名要规范

    ❌ bad:click_buy、userClickedBuy、点击购买、buyBtn_click
    ✅ good:button_click(动作)+ module=shop + target=buy(属性)
  3. 必须带 userIdsessionId

    track("button_click", {
    userId, // 必带
    sessionId, // 必带
    module: "shop", // 必带
    target: "buy",
    });
  4. PV / UV 不要重复埋

    • 路由切换算一次 PV
    • 同会话不重复算 UV
  5. 关键节点要服务端埋

    场景客户端埋点服务端埋点
    按钮点击❌ 太重
    订单创建❌ 用户可能刷新不报以服务端为准
    支付成功❌ 不准
    页面访问❌ SSR 才好做
  6. 不要全埋点当默认

    • 全埋点(无埋码)省开发但数据噪音大
    • 关键路径走代码埋点 + 辅助探索用全埋点

二、埋点分类

分类方式优点缺点适用
代码埋点手动 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_launchApp 启动from, is_first_visit
app_exitApp 退出duration

业务事件(按业务定义):

业务事件字段
电商product_viewproduct_id, category, price
电商add_to_cartproduct_id, quantity
电商order_createorder_id, amount
电商payment_successorder_id, amount, channel
内容article_readarticle_id, duration, scroll_percent
营销coupon_claimcoupon_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),海外业务 MixpanelPostHog

七、面试高频

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

八、相关文档