Skip to content
Go back

营销平台埋点设计:从协议到采集的完整方案

营销页面的目标不是”页面好看”,而是让用户产生行为:点击、领取、提交、预约、购买、咨询、留资。

但如果没有埋点,这些行为就是黑盒。你不知道用户从哪来、看了什么、点了什么、在哪里流失。页面发出去,只能证明”页面存在”,不能证明”页面有效”。

所以埋点不是锦上添花,而是营销平台的基础能力。

埋点的本质是什么

埋点的本质是把用户的隐性行为显性化。

用户打开页面、滚动屏幕、点击按钮、填写表单——这些动作在前端发生时是瞬时的,不留痕迹。埋点的作用就是把这些行为记录下来,变成结构化数据,供后续分析使用。

一条埋点数据至少包含:

事件名(what happened)事件时间(when)用户标识(who)页面标识(where)业务参数(context)

缺少任何一项,数据的价值都会大打折扣。事件名不规范,分析时无法聚合;时间不准,漏斗分析会错乱;用户标识缺失,无法做用户级归因。

埋点协议设计

埋点协议是埋点系统的地基。协议不清晰,后面的采集、存储、分析都会出问题。

一个合理的埋点事件结构:

interface TrackEvent {  // 事件名,如 page_view、button_click、form_submit  event: string;    // 事件发生时间戳(毫秒)  timestamp: number;    // 用户标识  userId?: string;  anonymousId: string;    // 页面上下文  pageId: string;  componentId?: string;    // 渠道归因  channel?: string;  campaignId?: string;  utm_source?: string;  utm_medium?: string;  utm_campaign?: string;    // 业务参数  properties: Record<string, any>;    // 设备信息  device: {    platform: 'ios' | 'android' | 'web';    screenWidth: number;    screenHeight: number;    userAgent: string;  };}

事件命名要统一规范。常见做法是用 对象_动作 的格式:

page_view          // 页面浏览page_leave         // 页面离开button_click       // 按钮点击component_exposure // 组件曝光form_submit        // 表单提交form_submit_success // 表单提交成功form_submit_fail   // 表单提交失败coupon_claim       // 优惠券领取product_click      // 商品点击

命名规范的好处是可预测。分析时不需要每次都去查事件名,看到 form_submit 就知道是表单提交,看到 _success 就知道是成功。

组件级埋点声明

如果每个业务方自己写埋点,长期会很混乱。事件名不统一、参数不一致、漏埋错埋很常见。

更合理的方式是让组件自己声明支持哪些埋点事件。

在组件协议中增加 trackingSchema

{  "type": "CouponCard",  "props": {    "couponId": "string",    "title": "string",    "discount": "string"  },  "trackingSchema": {    "exposure": {      "event": "coupon_exposure",      "description": "优惠券卡片曝光",      "params": {        "couponId": "string",        "position": "number"      }    },    "click": {      "event": "coupon_click",      "description": "优惠券卡片点击",      "required": ["couponId"],      "params": {        "couponId": "string"      }    },    "claim": {      "event": "coupon_claim",      "description": "优惠券领取成功",      "required": ["couponId"],      "params": {        "couponId": "string"      }    }  }}

这样做的好处:

  1. 统一事件名:所有优惠券组件用同样的事件名,分析时不需要猜。
  2. 参数有文档descriptionparams 就是埋点文档,不需要单独维护。
  3. 必填校验required 字段可以在运行时校验,防止漏传关键参数。
  4. 可复用:同一个组件在不同页面使用,埋点协议保持一致。

页面级埋点配置

组件声明了”能埋哪些点”,页面 DSL 配置”具体怎么埋”。

{  "id": "page_001",  "type": "Page",  "children": [    {      "id": "coupon_1",      "type": "CouponCard",      "props": {        "couponId": "summer_50",        "title": "满300减50"      },      "tracking": {        "exposure": {          "params": {            "position": 1,            "module": "top_banner"          }        },        "click": {          "params": {            "source": "homepage"          }        }      }    }  ]}

注意 tracking 配置里的参数会和组件声明的 trackingSchema 合并。组件提供参数结构,页面提供具体值,运行时负责组装。

这样配置人员不需要知道埋点 SDK 怎么调用,只需要在 DSL 里填写业务参数。埋点细节由平台运行时统一处理。

曝光监听的实现

曝光埋点和点击埋点不同。点击是用户主动触发,曝光是元素进入可视区域时被动触发。

现代浏览器提供了 IntersectionObserver,这是实现曝光监听的标准方案:

class ExposureTracker {  private observer: IntersectionObserver;  private tracked: Set<string> = new Set();  constructor() {    this.observer = new IntersectionObserver(      (entries) => {        entries.forEach(entry => {          if (entry.isIntersecting) {            const elementId = entry.target.getAttribute('data-track-id');            if (elementId && !this.tracked.has(elementId)) {              this.tracked.add(elementId);              this.handleExposure(entry.target);            }          }        });      },      {        // 元素可见 50% 才算曝光        threshold: 0.5,        // 可见区域扩展 10px,提前一点触发        rootMargin: '10px'      }    );  }  observe(element: HTMLElement) {    this.observer.observe(element);  }  private handleExposure(element: HTMLElement) {    const trackId = element.getAttribute('data-track-id');    const trackEvent = element.getAttribute('data-track-event');    const trackParams = JSON.parse(element.getAttribute('data-track-params') || '{}');        TrackingSDK.track(trackEvent, trackParams);  }}

使用时,运行时给需要曝光埋点的组件自动挂载 data-track-id

<div   data-track-id="coupon_1_exposure"  data-track-event="coupon_exposure"  data-track-params='{"couponId":"summer_50","position":1}'>  <!-- CouponCard 组件内容 --></div>

曝光有几个常见坑:

  1. 重复曝光:用户滚动到优惠券,继续往下,再滚回来。需要控制只曝光一次还是每次都曝光。优惠券这种场景通常只要一次,但商品列表可能需要每次进入都曝光。
  2. 页面初始就可见:首屏内容在页面加载时就已经可见,IntersectionObserver 可能不触发。需要页面加载后手动检查一次首屏元素。
  3. SPA 路由切换:单页应用切换路由时,需要重新初始化曝光监听,否则新页面的元素不会被观察。
  4. 容器滚动:如果组件在一个可滚动的容器里,需要指定 root 为该容器,否则计算不准。

批量上报机制

每条埋点数据单独发送请求,会浪费大量网络资源。营销页面用户停留时间短,可能短时间内产生大量事件(曝光 + 点击 + 滚动),必须批量上报。

class TrackingSDK {  private queue: TrackEvent[] = [];  private flushInterval = 3000; // 3秒刷一次  private maxQueueSize = 20;   // 积攒20条刷一次  private timer: number | null = null;  track(event: string, properties: Record<string, any>) {    const fullEvent: TrackEvent = {      event,      timestamp: Date.now(),      anonymousId: this.getAnonymousId(),      pageId: this.pageId,      properties,      device: this.getDeviceInfo()    };    this.queue.push(fullEvent);    // 达到批量阈值,立即发送    if (this.queue.length >= this.maxQueueSize) {      this.flush();    }    // 启动定时器,保证数据不会一直积压    if (!this.timer) {      this.timer = window.setTimeout(() => {        this.flush();      }, this.flushInterval);    }  }  private flush() {    if (this.queue.length === 0) return;    const events = [...this.queue];    this.queue = [];    // 用 sendBeacon 优先,兼容用 fetch    const data = JSON.stringify(events);        if (navigator.sendBeacon) {      navigator.sendBeacon('/api/tracking/batch', data);    } else {      fetch('/api/tracking/batch', {        method: 'POST',        body: data,        headers: { 'Content-Type': 'application/json' },        // keepalive 保证页面关闭后请求还能发出        keepalive: true      });    }    if (this.timer) {      clearTimeout(this.timer);      this.timer = null;    }  }}

sendBeacon 是页面关闭场景的救星。用户关闭页面或跳转时,fetch 可能被浏览器取消,但 sendBeacon 可以保证数据发出。

参数自动补全

营销埋点需要大量上下文参数:页面 ID、组件 ID、渠道、活动 ID、UTM 参数、设备信息。如果让配置人员每次都手动填写,很容易漏。

平台运行时应该自动补全这些参数:

function enrichParams(componentParams: Record<string, any>, context: TrackingContext) {  return {    // 业务参数(来自页面配置)    ...componentParams,        // 自动补全的上下文    pageId: context.pageId,    componentId: context.componentId,        // 渠道归因(来自 URL 或 localStorage)    channel: context.channel,    campaignId: context.campaignId,    utm_source: context.utm_source,    utm_medium: context.utm_medium,    utm_campaign: context.utm_campaign,        // 页面信息    pageUrl: window.location.href,    referrer: document.referrer,        // 时间    timestamp: Date.now(),        // 会话信息    sessionId: context.sessionId,    userId: context.userId,    anonymousId: context.anonymousId  };}

UTM 参数需要在页面加载时从 URL 中解析并存储:

function parseUTMParams(): Record<string, string> {  const params = new URLSearchParams(window.location.search);  return {    utm_source: params.get('utm_source') || '',    utm_medium: params.get('utm_medium') || '',    utm_campaign: params.get('utm_campaign') || '',    utm_content: params.get('utm_content') || '',    utm_term: params.get('utm_term') || ''  };}// 存储到 sessionStorage,保证同页面会话内所有埋点都有 UTM 信息sessionStorage.setItem('utm_params', JSON.stringify(parseUTMParams()));

客户端内嵌场景:用 JSBridge 上报

营销页面最常见的打开方式是在 App 内嵌的 WebView 里。这种场景下,纯 H5 上报有一些天然劣势,而客户端原生上报可以解决这些问题。

H5 上报的局限:

问题原因
页面关闭丢数据sendBeacon 不是 100% 可靠,部分 Android WebView 行为不一致
用户标识不稳定localStorage 可能被清理,换浏览器、清缓存都会丢失
设备信息受限浏览器拿不到 IMEI、OAID、设备型号等原生标识
跨页面归因断裂多个 WebView 之间无法共享会话,用户路径断开
离线场景丢失断网时 H5 请求直接失败,无法缓存后重发

用 JSBridge 上报,可以让客户端接管埋点数据的存储和发送,前端只负责“采集”和“调用桥”。

JSBridge 上报接口设计

客户端暴露一个统一的上报接口:

// JSBridge 接口定义interface NativeBridge {  // 单条上报  track(event: string, properties: string): void;    // 批量上报  trackBatch(events: string): void;    // 获取设备信息(同步)  getDeviceInfo(): string;    // 获取用户标识(异步)  getUserId(callback: (userId: string) => void): void;    // 获取客户端生成的匿名 ID  getAnonymousId(): string;}

前端 SDK 判断运行环境,选择上报通道:

class TrackingSDK {  private isNative: boolean;  private bridge: NativeBridge | null = null;  constructor() {    this.isNative = this.detectNativeEnvironment();    if (this.isNative) {      this.bridge = window.JSBridge;    }  }  private detectNativeEnvironment(): boolean {    // 判断是否在客户端 WebView 内    return !!(window.JSBridge && window.JSBridge.track);  }  track(event: string, properties: Record<string, any>) {    const fullEvent = this.buildEvent(event, properties);    if (this.isNative && this.bridge) {      // 客户端上报:直接调桥,由客户端负责存储和发送      this.bridge.track(event, JSON.stringify(fullEvent));    } else {      // H5 上报:走原有的队列 + sendBeacon 逻辑      this.enqueue(fullEvent);    }  }}

客户端上报的优势

1. 数据可靠性

客户端可以实现本地存储 + 重试机制:

上报请求  -> 写入本地数据库(SQLite)  -> 标记为待发送  -> 网络可用时批量发送  -> 发送成功后删除  -> 发送失败保留重试

即使用户断网、杀 App,数据也不会丢。下次打开时,客户端自动补发之前缓存的数据。

2. 跨页面会话连续

客户端维护全局会话,不受 WebView 生命周期影响:

用户打开活动页 A(WebView 1)  -> 客户端分配 sessionId  -> 用户点击跳转活动页 B(WebView 2)  -> 同一个 sessionId 继续  -> 用户在 B 页提交表单  -> 归因数据可以追溯到 A 页的来源

H5 方案下,WebView 1 和 WebView 2 是两个独立页面,sessionStorage 不共享,会话就断了。

3. 设备信息更完整

客户端可以提供 H5 无法获取的设备信息:

// 客户端返回的设备信息{  deviceId: "oaid_xxxx",       // 设备唯一标识  imei: "xxxxx",               // Android IMEI  idfa: "xxxxx",               // iOS 广告标识  model: "Xiaomi 14",          // 设备型号  osVersion: "Android 14",     // 系统版本  appVersion: "3.2.1",         // App 版本  networkType: "wifi",         // 网络类型  batteryLevel: 0.85,          // 电量  isRooted: false              // 是否越狱/root}

这些信息对风控和用户画像很重要。比如 isRooted 可以辅助判断刷量风险,networkType 可以分析不同网络环境下的转化差异。

4. 性能友好

H5 上报需要在 JS 线程里做序列化、发请求、处理回调。页面动画和用户交互可能受到影响。

客户端上报时,JS 只需要调一个桥方法,数据就交给客户端处理了:

// H5 上报:JS 线程阻塞const data = JSON.stringify(events);  // 序列化耗时fetch(url, { body: data });           // 网络请求// 客户端上报:JS 线程立即释放bridge.trackBatch(JSON.stringify(events));  // 一行调用,客户端异步处理

5. 隐私合规

敏感信息(设备 ID、手机号 hash)在客户端处理,不暴露给前端 JS:

客户端获取设备 ID  -> 客户端内部存储和使用  -> 前端 JS 只拿到匿名化的 sessionId  -> 即使页面被 XSS 攻击,敏感信息也不会泄露

上报链路对比

纯 H5 方案:前端采集 -> JS 队列 -> sendBeacon/fetch -> 服务端 API客户端接管方案:前端采集 -> JSBridge -> 客户端队列 -> 本地持久化 -> 原生网络模块 -> 服务端 API

两种方案可以共存。平台检测到 JSBridge 存在时走客户端通道,否则走 H5 通道。代码层面是统一的 SDK 调用,配置人员不需要关心页面在哪里打开。

埋点数据的采集链路

前端埋点只是起点,完整的链路是:

前端 SDK  -> 批量上报 API  -> 消息队列(Kafka)  -> 数据清洗  -> 数据仓库(ClickHouse)  -> 分析看板(Grafana / Metabase)

API 层的职责很轻,只做两件事:

  1. 接收批量数据。
  2. 校验格式,丢入消息队列。
// 服务端上报接口app.post('/api/tracking/batch', async (req, res) => {  const events = req.body;    // 基本校验  if (!Array.isArray(events)) {    return res.status(400).json({ error: 'Invalid format' });  }  // 校验每条事件的必要字段  const validEvents = events.filter(event => {    return event.event && event.timestamp && event.anonymousId;  });  // 丢入消息队列  await kafka.send('tracking-events', validEvents);    res.json({ success: true, count: validEvents.length });});

为什么用消息队列?

  1. 削峰:营销页面投放后,流量可能瞬间涌入,消息队列可以平滑处理。
  2. 解耦:数据采集和数据处理分离,后端可以独立扩展。
  3. 可靠性:消息队列有持久化机制,即使处理服务临时挂掉,数据也不会丢。

常见埋点事件清单

营销页面有一些通用埋点事件,建议平台内置支持:

事件名触发时机典型参数
page_view页面加载完成pageId, referrer, utm_*
page_leave页面关闭或跳转pageId, stayDuration
component_exposure组件进入可视区域componentId, position, module
button_click按钮点击buttonId, buttonText, action
form_start用户开始填写表单formId, firstField
form_submit用户点击提交formId, fieldCount
form_submit_success提交成功formId, leadId
form_submit_fail提交失败formId, errorMessage
coupon_exposure优惠券曝光couponId, position
coupon_click优惠券点击couponId
coupon_claim优惠券领取couponId, claimResult
product_exposure商品曝光productId, position, listName
product_click商品点击productId, position
scroll_depth页面滚动深度depth(25/50/75/100)

这些事件可以作为平台默认事件,配置人员只需要关注业务特有事件。

漏斗分析依赖埋点规范

埋点数据最终要服务于业务分析。营销页面最常用的分析方式是漏斗分析。

一个典型的营销漏斗:

页面访问(page_view)  -> 优惠券曝光(coupon_exposure)  -> 优惠券点击(coupon_click)  -> 表单开始填写(form_start)  -> 表单提交(form_submit)  -> 提交成功(form_submit_success)

漏斗分析的前提是事件名规范、参数一致、用户标识可关联。如果事件名不统一,比如有的叫 form_submit 有的叫 submit_form,分析时就无法正确聚合。

所以埋点规范不是”建议”,而是”要求”。平台层必须强制执行。

埋点质量保障

埋点上线后最常见的问题是:数据不对。

常见问题:

  1. 事件没上报:代码写漏了,或者组件没正确挂载埋点。
  2. 参数缺失:必填参数没传,分析时无法关联。
  3. 重复上报:同一个事件上报了多次,数据失真。
  4. 上报时机不对:应该在曝光时上报的事件,变成了点击时才上报。

解决方案:

  1. 本地开发时开启埋点日志:在开发环境打印每条埋点数据,方便验证。
class TrackingSDK {  track(event: string, properties: Record<string, any>) {    const fullEvent = this.buildEvent(event, properties);        // 开发环境打印日志    if (process.env.NODE_ENV === 'development') {      console.log('[Tracking]', event, fullEvent);    }        this.queue.push(fullEvent);  }}
  1. 测试环境验证埋点完整性:自动化测试脚本检查页面上报的所有事件,对比埋点文档,找出缺失和多余。

  2. 上线后监控数据量:如果某个事件的数据量突然下降或飙升,可能是埋点出了问题。

小结

埋点看起来简单,但做好不容易。它涉及协议设计、组件声明、运行时采集、参数补全、批量上报、数据落盘、质量保障等多个环节。

营销平台如果把埋点做成内置能力,配置人员只需要关心业务参数,不需要理解 SDK 的实现细节。平台负责统一事件名、统一参数格式、统一上报链路,保证数据质量。

有了可靠的埋点数据,后面的转化分析、渠道归因、A/B 测试才有基础。否则页面再好看,也只是自嗨。


Share this post on: