前端埋点 SDK 的设计与实现
SDK 配置初始化
通过实例化追踪器完成基础配置,支持环境识别、事件代理、曝光检测等能力的开关控制。
const trackerConfig = {
env: import.meta.env.VITE_APP_ENV ?? 'dev',
runtime: import.meta.env.MODE ?? 'development',
autoTrackClick: true,
autoTrackView: true,
trackHiddenElements: true,
};
window.__TRACKER__ = new AnalyticsTracker(trackerConfig);核心数据模型
定义统一的上报数据结构,涵盖用户身份、页面位置、交互事件等维度。
this._basePayload = {
env: options.env,
deviceId: '',
openId: '',
unionId: '',
userId: '',
internalId: '',
userRole: '',
identityType: '',
corpId: '',
staffId: '',
sessionBranchId: '',
eventName: '',
pagePath: '',
moduleName: '',
elementName: '',
extraParams: {},
};数据注入接口
提供数据合并方法,支持增量更新基础字段,避免直接覆盖参数对象。
public injectData(partialData) {
this._basePayload = {
...this._basePayload,
...partialData,
extraParams: {
...this._basePayload.extraParams,
...partialData.extraParams,
},
};
}冷启动追踪
在应用启动时自动触发,用于统计用户来源渠道和首次进入行为。
public recordColdLaunch() {
const source = this._basePayload.entrySource;
if (!source) return;
this.dispatchEvent({
eventName: 'app_cold_launch',
});
}SPM 层级定位体系
借鉴电商平台的页面定位模型,通过层级编码实现精确的页面元素寻址。
采用 a.b.c.d 四级编码结构:
- 一级:站点/应用标识
- 二级:页面标识
- 三级:区块标识
- 四级:元素标识
手动事件上报
__TRACKER__.dispatchEvent({
eventName: 'module_exposure',
moduleName: 'product_list',
elementName: 'recommend_card',
extraParams: { skuId: '10086' },
});
public dispatchEvent(eventData) {
const payload = {
...this._basePayload,
...eventData,
extraParams: {
...this._basePayload.extraParams,
...eventData.extraParams,
},
};
this._transportLayer.send(payload);
}声明式埋点与事件代理
利用 HTML 数据属性标记埋点元素,通过事件委托减少内存占用,支持动态元素的自动绑定。
// 组件层标记
<button
data-track-action={JSON.stringify({
eventName: 'form_submit',
moduleName: 'checkout_page',
elementName: 'confirm_btn',
extraParams: { orderAmount: 299 },
})}
>
提交订单
</button>
// 代理层实现
document.body.addEventListener('click', (event) => {
let currentNode = event.target;
const spmChain = {};
while (currentNode?.dataset) {
const ds = currentNode.dataset;
if (ds.trackSpmb) spmChain.pagePath = ds.trackSpmb;
if (ds.trackSpmc) spmChain.moduleName = ds.trackSpmc;
if (ds.trackSpmd) spmChain.elementName = ds.trackSpmd;
if (ds.trackAction) spmChain.actionConfig = ds.trackAction;
currentNode = currentNode.parentElement;
}
if (spmChain.actionConfig) {
const config = JSON.parse(spmChain.actionConfig);
this.dispatchEvent({
pagePath: spmChain.pagePath,
moduleName: spmChain.moduleName,
elementName: spmChain.elementName,
...config,
});
}
});区块曝光监测
基于 IntersectionObserver API 实现可视区域检测,支持自定义曝光阈值和重复曝光控制。
// 组件绑定
<section
data-track-view={JSON.stringify(viewData)}
ref={__TRACKER__.observeViewableArea}
>
{children}
</section>
// 观察者初始化
ctx._viewObserver = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.intersectionRatio < 0.5) return;
const finalData = resolveSpmFromCache(ctx, entry);
ctx.dispatchEvent(finalData);
});
},
{ threshold: 0.5 }
);
// 动态刷新绑定(防抖处理)
export function observeViewableArea() {
clearTimeout(this._viewDebounceTimer);
this._viewDebounceTimer = setTimeout(() => {
this._viewObserver.disconnect();
const targets = document.querySelectorAll('[data-track-view]');
targets.forEach((el) => this._viewObserver.observe(el));
}, 500);
}缓存辅助函数
// 利用 WeakMap 存储 DOM 的 SPM 解析结果,自动处理内存回收
ctx._spmCache = new WeakMap();
function resolveSpmFromCache(ctx, entry) {
const target = entry.target;
let cached = ctx._spmCache.get(target);
if (!cached) {
cached = {};
let node = target;
while (node?.dataset) {
const ds = node.dataset;
if (ds.trackSpmb) cached.pagePath = ds.trackSpmb;
if (ds.trackSpmc) cached.moduleName = ds.trackSpmc;
if (ds.trackSpmd) cached.elementName = ds.trackSpmd;
node = node.parentElement;
}
ctx._spmCache.set(target, cached);
}
const userData = JSON.parse(target.dataset.trackView);
return { ...cached, ...userData };
}隐藏元素检测
针对弹窗、下拉菜单等由隐藏变为可见的元素,提供独立的观察策略。
ctx._hiddenObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.intersectionRatio <= 0) return;
ctx.dispatchEvent(resolveSpmFromCache(ctx, entry));
});
});
// 刷新方法
public refreshHiddenTracking() {
if (!this._config.trackHiddenElements) return;
clearTimeout(this._hiddenDebounceTimer);
this._hiddenDebounceTimer = setTimeout(() => {
this._hiddenObserver.disconnect();
const nodes = document.querySelectorAll('[data-track-hidden]');
nodes.forEach((el) => this._hiddenObserver.observe(el));
}, 200);
}React 封装组件
提供声明式组件降低接入成本,通过 type 字段区分不同的追踪模式。
// 使用示例
<Trackable
mode="view"
payload={{
eventName: 'banner_show',
moduleName: 'homepage',
elementName: 'promo_banner',
extraParams: { campaignId: 'summer2024' },
}}
>
<Banner content={promoData} />
</Trackable>
// 组件实现
export default function Trackable({ mode = 'click', as: Component = 'div', ...props }: TrackableProps) {
const enrichedProps = {
...props,
mode,
};
return strategyMap[mode](enrichedProps);
}
const strategyMap = {
click: ({ payload, children, className, hidden, ...rest }) => {
return React.createElement(
Component,
{
'data-track-action': JSON.stringify(payload),
className: clsx(className, hidden && 'hidden'),
...rest,
},
children
);
},
view: ({ payload, children, className, hidden, ...rest }) => {
return React.createElement(
Component,
{
'data-track-view': JSON.stringify(payload),
ref: __TRACKER__.observeViewableArea,
className: clsx(className, hidden && 'hidden'),
...rest,
},
children
);
},
hidden: ({ payload, children, className, hidden, ...rest }) => {
return React.createElement(
Component,
{
'data-track-hidden': JSON.stringify(payload),
ref: __TRACKER__.refreshHiddenTracking,
className: clsx(className, hidden && 'hidden'),
...rest,
},
children
);
},
};