首页IT科技前端埋点sdk(前端组件化埋点方案与实现)

前端埋点sdk(前端组件化埋点方案与实现)

时间2025-04-29 14:59:35分类IT科技浏览3110
导读:背景 埋点,是收集产品的数据的一种方式,其目的是上报相关行为数据(PV/UV/时长/曝光/点击等),由相关人员以此分析用户的使用习惯,助力产品不断迭代和优化。对于开发来说,通常不仅仅需要完成基础的业务需求,还需要完成埋点需求,所以,追求的是简单快捷的埋点工作。而一个完整的埋点体系由以下三个部分构成:...

背景

埋点           ,是收集产品的数据的一种方式                ,其目的是上报相关行为数据(PV/UV/时长/曝光/点击等)    ,由相关人员以此分析用户的使用习惯        ,助力产品不断迭代和优化          。对于开发来说                 ,通常不仅仅需要完成基础的业务需求      ,还需要完成埋点需求     ,所以                  ,追求的是简单快捷的埋点工作               。而一个完整的埋点体系由以下三个部分构成:

产品应用(产生行为数据) 数据分析平台(展示           、分析行为数据) 数据平台 SDK(上报行为数据):封装数据分析平台的各种接口         ,暴露简单的方法供调用  ,实现简易的埋点上传      。

目前                 ,前端埋点存在的痛点一般是:

埋点字段的手动拼接            ,存在出错风险;

复杂场景的曝光埋点实现繁琐:分页列表                、虚拟列表等;

埋点代码的侵入性:尤其是曝光代码导致逻辑复用困难        。

埋点类型一般有:

页面埋点:统计用户进入或离开页面的各种维度信息,如页面浏览次数(PV)    、浏览页面人数(UV)        、页面停留时间                 、浏览器信息等              。 点击埋点:统计用户在应用内的每一次点击事件              ,如新闻的浏览次数      、文件下载的次数     、推荐商品的命中次数等         。 曝光埋点:统计具体区域是否被用户浏览到               ,如活动的引流入口的显示                  、投放广告的显示等      。

市场上常见的埋点方案:

全埋点(无埋点):由前端自动采集全部事件并上报  ,前端也就没有埋点成本           ,由数据分析平台或后端过滤有用数据                ,优点是数据全面    ,缺点是数据量大        ,噪声数据多              。 可视化埋点:由可视化工具进行配置采集指定元素——查找 dom 并绑定事件                 ,优点是简单      ,缺点是准确性较低     ,针对性和自定义埋点能力较弱            。 代码埋点:用户触发某个动作后手动上报数据                  ,优点时准确性高         ,能满足自定义的场景  ,缺点是埋点逻辑容易与业务逻辑耦合(命令式埋点)                 ,不利于维护与复用   。

综上            ,需要的是一种简单快速且准确,同时埋点逻辑与业务逻辑解耦的埋点方案              ,也就是本文分析的声明式的组件化埋点方案              。

声明式的组件化埋点方案

名词解释

页面 (Page):在浏览器中打开的网页               ,不同页面以路径 location.pathname 来作区分; 页面可见时长:一个页面对用户可见的累计时长; 页面活跃时长:用户在页面上进行有效的鼠标         、键盘及触控活动的累计时长; 组件 (Component):DOM 元素的集合  ,是页面的组成部分              。一个页面内可包含多个组件; 组件可见时长:一个组件对用户可见的累计时长。 可见性(visiability) visible:页面 viewport 中且位于前台; invisible - 页面不 viewport 中           ,或处于后台            。 活跃性 (activity) active - 用户在网页中有活动(例如鼠标  、键盘活动及页面滚动等); inactive - 用户在网页中没有任何活动                。

根据概念可知                ,一个页面不可见时    ,则一定不活跃        ,且其中的所有组件一定也都不可见;页面活跃时长 ≤ 页面可见时长;组件可见时长 ≤ 页面可见时长                 ,

原理与思路

该方案总体思路如下: 

对于通用字段进行统一处理      ,既不容易出错     ,也方便后期拓展   。对于运行时字段(异步)                  ,支持 extra 进行传入          。 对于页面级事件         ,埋点库初始化后自动注册关于页面级曝光的相关事件  ,不需要在代码中进行维护               。 考虑到存在高频场景                 ,设置上报缓冲队列 pendingQueue            ,通过定时任务分批次上报数据,支持设置上报频率      。根据实践              ,点击类上报频率 1000ms               ,曝光类 3000ms        。 考虑到埋点 sdk 没初始完  ,上报行为就已经产生了           ,设置 unInitQueue 来存储              。 以页面为维度来管理埋点配置                ,便于维护和迁移         。

考虑到埋点 sdk 没初始完    ,上报行为就已经产生了        ,比如曝光                 ,新增如果这时候生成对应的点进入缓冲队列      ,就是属于无效的点因为没有加载到坑位信息                 、配置参数等     ,所以针对这种场景下产生的点位信息                  ,我们新开一个队列存储         ,等到初始化完成再去处理;

因此  ,埋点上报总体流程为:埋点 sdk 接受返回埋点的函数                 ,将其返回值上报            ,支持上报多个埋点;埋点事件由应用发送给埋点 sdk 后,埋点 sdk 首先会对数据进行处理              ,再调用数据平台暴露的方法               , 将埋点事件上报给数据平台      。 

具体实现

判断页面可见性

虽然 Page Visibility API 的浏览器兼容情况不错  ,但对于Android            、iOS 和最新的 Windows 系统可以随时自主地停止后台进程           ,及时释放系统资源              。因此                ,基于 Google 描述网页生命周期的 Page Lifecycle API 兼容库 PageLifecycle.js 来监听页面可见性变化——一个网页从载入到销毁的过程中    ,会通过浏览器的各种事件在以下六种生命周期状态 (Lifecycle State) 之间相互转化        ,通过监听页面生命周期的变化并记录其时间                 ,就可以相应获取页面可见性的统计数据:

active:网页可见且具有焦点; passive:网页可见但处于失焦状态; hidden:网页不可见但未被浏览器冻结      ,一般由用户切换到别的 tab 或最小化浏览器触发; frozen:网页被浏览器冻结(后台任务比如定时器、fetch等被挂起以节约 CPU 资源); terminated:网页被浏览器卸载并从内存中清理            。一般用户主动将网页关闭时触发此状态; discarded:网页被浏览器强制清理   。一般由系统资源严重不足引起              。

由此可得     ,页面生命周期状态和页面可见状态之间的映射关系为

active +  passive =  visible; hidden + terminated + frozen +  discarded =  invisible              。

因此                  ,通过监听 statechange 来识别页面可见状态的改变         ,在生命周期状态为 active 和 passive 时标记页面为 visible 状态  ,在生命周期状态为其他几个时标记页面为 invisible 状态                 ,更新最后一次可见的时间戳            ,并累加页面可见时间。PageLifecycle.js 是无法推送 discarded 事件的,因为网页已经被销毁并从内存中清理              ,无法向外传递任何事件——解决方案是需要在页面进入 invisible 状态时               ,对数据使用 JSON.stringify 序列化并储存在 localStorage 中  ,若页面后续转为 visible 状态即将其清空           ,否则在页面被强制清除后                ,在下一次初始进入页面时先将 localStorage 中的数据通过事件推送出去            。另外    , PageLifecycle.js 无法感知单页面应用的history 或 hash 路由切换        ,需要在埋点 sdk 中额外添加对路由变化事件(popstate/replacestate)的监听                 ,等同于进入 terminated 生命周期:

lifecycleInstance.addEventListener("statechange", (event: StateChangeEvent) => { const { newState } = event; if (["active", "passive"].includes(newState)) { // page visible, do something return; } if (["hidden", "terminated", "frozen"].includes(newState)) { // page invisible, do something else } })

判断页面活跃性

通过监听以下的六种浏览器事件      ,就可以判断用户是否在当前页面上有活动     ,此时页面标记为 active 状态                  ,并记录当前时间戳         ,用于累加活跃时长                。:

keydown:用户敲击键盘时触发; mousedown:用户点击鼠标按键时触发; mouseover:用户移动鼠标指针时触发; touchstart:用户手指接触触摸屏时触发(仅限触屏设备); touchend:用户手指离开触摸屏时触发(仅限触屏设备); scroll:用户滚动页面时触发   。

而页面被标记为 inactive 状态  ,有以下两种情况:

在初始化埋点 sdk 时自定义                 ,visible 状态下超过一定的时间阈值(比如 15 秒)没有监测到表示页面活跃的六种事件; 页面状态为 invisible            ,因为如果页面对用户不可见,那么它一定是不活跃的          。

判断组件可见性

首先需要获取需要统计的所有 DOM 元素              ,并指定埋点的标准               。MutationObserver API 提供了 DOM 节点增减以及属性变化检测的能力      。该 API 是异步触发               ,即要等到当前所有 DOM 操作都结束才触发  ,避免DOM频繁变动造成性能损耗           ,因此                ,可以用来监听 DOM 结构变化        。

const mutationObserver = new MutationObserver(function (mutations, observer) { mutations.forEach(function (mutation) { console.log(mutation.target); // target: 发生变动的 DOM 节点 }); }); // 观察整个文档 mutationObserver.observe(document.documentElement, { childList: true, // 子节点的变动(指新增    ,删除或者更改) attributes: true, // 属性的变动 characterData: true, // 节点内容或节点文本的变动 subtree: true, // 表示是否将该观察器应用于该节点的所有后代节点 attributeOldValue: false, // 表示观察 attributes 变动时        ,是否需要记录变动前的属性值 characterDataOldValue: false, // 表示观察 characterData 变动时                 ,是否需要记录变动前的值              。 attributeFilter: false, // 表示需要观察的特定属性      ,比如[class,src] }); mutationObserver.disconnect(); // 用来停止观察         。调用该方法后     ,DOM 再发生变动则不会触发观察器

其中                  , mutations 是所有被触发改动的 MutationRecord 对象数组      。 

考虑到可能存在被监控组件是第三方库的         ,自定义属性 data-tracking-pv 会被过滤  ,为了统一                 ,利用babel 插件在编译过程中寻找添加了 data-tracking-pv 属性的组件            ,并在其外层包裹一个自定义的 <tracking></tracking> 标签,自定义标签的优点是没有任何样式              ,所以包裹该标签也不会影响到原有组件的样式              。埋点 sdk 收集这些元素供 MutationObserver 监听 DOM 变化            。 

const Component = () => { <div data-tracking-pv={ "event": "component_custom_pv", "params": { ... } }> component_custom </div> } const Component = () => { return <Button data-tracking-pv={{ event: "component_antd_pv", params: { ... } }}>点击按钮</Button> } // 插件最终生成的组件为 </tracking data-tracking-pv={ "event": "component_custom_pv", "params": { ... } } > // ...真实组件 </tracking>

组件可见性               ,即曝光的三个判断标准:

是否处于 viewport 中:使用 IntersectionObserver API 判断  , 该 API 提供了一种异步检测目标元素与祖先元素或 viewport 相交情况变化的方法;我们知道           ,用户的感兴趣程度 = 点击率(点击次数/曝光次数)                ,考虑曝光的有效性    ,即需要判断组件出现在 viewpoint 内的达到一定的比例(0.5 或 0.75 或 1.0)和时长(3s)以及次数(是否重复曝光)标准; const intersectionObserver = new IntersectionObserver( (entries) => { entries.forEach(function (entry) { /** entry.boundingClientRect // 返回包含目标元素的边界信息   。 边界的计算方式与 Element.getBoundingClientRect() 相同              。 entry.intersectionRatio // 目标元素的可见比例        ,即 intersectionRect 占 boundingClientRect 的比例                 ,完全可见时为 1      ,完全不可见时小于等于 0 entry.intersectionRect // 目标元素与视口(或根元素)的交叉区域的信息 entry.isIntersecting // 返回一个布尔值, 如果根与目标元素相交(即从不可视状态变为可视状态)     ,则返回 true              。如果返回 false, 变换是从可视状态到不可视状态。 entry.rootBounds // 根元素的矩形区域的信息, getBoundingClientRect 方法的返回值                  ,如果没有根元素(即直接相对于视口滚动)         ,则返回 null entry.target // 被观察的目标元素  ,是一个 DOM 节点对象 entry.time // 可见性发生变化的高精度时间戳                 ,单位为毫秒 **/ }); }, { root: document.querySelector(#root), // 根 dom 元素, 默认 null 为 viewport rootMargin: 0px, // root元素的外边距 threshold: [0, 0.25, 0.5, 0.75, 1], // Number 或 Number 数组            ,该属性决定了什么时候触发回调函数            。默认为 0.0; } ); intersectionObserver.observe(document.getElementById("item")); // 开始监听一个目标元素 intersectionObserver.disconnect(); // 停止全部监听工作

兼容性方面有对应的 intersection-observer-polyfill,

样式是否可见:使用 CSS 的 display/ visibility / opacity 样式属性判断;以下是会被标记为 invisible 的情况: visibility: hidden display: none opacity: 0 页面是否可见:使用页面可见性判断                。页面 invisible 状态下              ,所有组件的状态也标记为 invisible   。 

使用 requestIdleCallback 方法               ,浏览器会在空闲时执行传入的埋点函数  ,避免埋点影响主业务          。其浏览器兼容情况如下:

 可以使用 requestIdleCallback shim/polyfill           ,以下是简化版:

const requestIdleCallback = window.requestIdleCallback || function(callback, options) { var options = options || {}; var relaxation = 1; var timeout = options.timeout || relaxation; var start = performance.now(); return setTimeout(function() { callback({ get didTimeout() { return options.timeout ? false : performance.now() - start - relaxation > timeout; }, timeRemaining: function() { return Math.max(0, relaxation + (performance.now() - start)); } }); }, relaxation); };

MutationObserver + IntersectionObserver + requestIdleCallback 曝光埋点实现

const observerOptions = { childList: true, // 观察目标子节点的变化                ,是否有添加或者删除 attributes: true, // 观察属性变动 subtree: true, // 观察后代节点    ,默认为 false } function callback(mutationList, observer) { mutationList.forEach((mutation) => { switch (mutation.type) { case childList: collectTargets() break case attributes: break } }) } function getParams(el) { const { exposureTrackerAction, exposureTrackerParams } = el.dataset; const Key = exposureTrackerAction; const pageEl = document.querySelector([data-exposure-tracker-page-params]); let pageParams; if (pageEl?.dataset?.exposureTrackerPageParams) { try { pageParams = JSON.parse(pageEl?.dataset?.exposureTrackerPageParams); } catch (error) { console.error(parse pageParams fail); } } let params; if (exposureTrackerParams) { try { params = JSON.parse(exposureTrackerParams); } catch (error) { console.error(parse params fail); } } return { Key, ...pageParams, ...params }; } const intersectionObserver = new IntersectionObserver(function callback(entries, observer) { const list = []; entries.forEach(entry => { if ( entry.target.dataset.exposureTrackerExposed !== 1 && entry.intersectionRatio >= 0.5 ) { list.push(entry); observer.unobserve(entry.target); } else if (entry.intersectionRatio === 0) { delete entry.target.dataset.exposureTrackerExposed; } requestIdleCallback(() => { // ...上报 }); }); }, options); collectTargets() { const els = Array.from( document.querySelectorAll([data-exposure-tracker-action]) ).filter(el => !el.dataset.exposureTrackerTracked); if (els.length > 0) { // console.log(collectTargets, els); els.forEach(el => { intersectionObserver.observe(el); el.dataset.exposureTrackerTracked = true; }); } export function getExposureTrackerPageParamsProps(params) { return { data-exposure-tracker-page-params: params ? JSON.stringify(params) : undefined }; } export function getExposureTrackerParamsProps(action, params) { return { data-exposure-tracker-action: action, data-exposure-tracker-params: params ? JSON.stringify(params) : undefined }; } export default function App() { return ( <div {...getExposureTrackerPageParamsProps({ pageData: some page data })}> <div {...getExposureTrackerParamsProps(item_content_expose, { itemData: xxx })} > item content </div> </div> ); }

自定义类指令式埋点实现

该方式适合简单的埋点上报        ,埋点逻辑与业务逻辑清晰分离                 ,埋点 sdk 给 document 对象加上监听 click / hover 事件触发时      ,从当前触发事件的 target 逐级向上遍历     ,查看是否有对应此事件的指令               。如果有                  ,则上报此埋点事件         ,直至遇到一个没有事件指令的元素节点      。这样也可以在指令中控制是否要继续向上遍历        。

// 类指令式埋点实现逐级上报 <section data-tracking-hover={JSON.stringify({ type: func_operation, params: { value: 3 }})}> <div data-tracking-click={JSON.stringify({ type: func_operation, params: { value: 2 }})}> <Button data-tracking-click={JSON.stringify({ type: func_operation, params: { value: 1 }})}>点击</Button> </div> </section>

但是如果我们需要在上报事件前  ,对所上报的数据进行处理                 ,那么这种方式就无法满足了              。并且            ,并不是所有的场景都可以被 DOM 事件所覆盖         。如果我想在用户在搜索框输入某个值时,上报埋点              ,那么我就需要对用户输入的值进行分析               ,而不能在 input 事件每次触发时都上报埋点      。

装饰器式埋点实现

装饰器只能用于类组件  ,@tracking 修饰器接受一个函数形式的参数           ,其返回值即是要上报的事件              。在 handleClick 函数被调用的时候                ,埋点 sdk 会首先上报埋点事件    ,然后再执行 handleClick 函数的业务逻辑            。

target - 装饰器所在的类 propertyKey - 被装饰的函数的属性名 descriptor - 被装饰的函数的属性描述符 // tracking 函数简化源代码 tracking = (event: TrackingEvent) => { return (target: object, propertyKey: string, descriptor: object) => { if (isFunction(event) || isObject(event) || isArray(event)) { const oldMethod = descriptor.value; const _event = this.evalEvent(event)(...arguments); const composedFn = () => { this.sendEvent(_event); oldMethod.apply(this, arguments); } set(descriptor, "value", composedFn); return descriptor; } } }; /** * @tracking 使用示例 */ class Test extends React.component { ... @tracking((value: string) => ({ type: func_operation, params: { keyword: value }, })) handleClick() { console.log(执行点击的业务逻辑,); } render() { return ( <Button onClick={handleClick} /> ) } }

因此        ,会先上报埋点                 ,然后执行 descriptor.value 的逻辑      ,即被装饰的函数   。

React Hook 埋点实现

与装饰器实现类似     ,useTracking 接受两个函数:埋点函数和业务函数                  ,返回组合函数              。

// useTracking 源代码 useTracking = (fn: () => any /** 业务函数 */, event: TrackingEvent /** 埋点函数 */) => { if (!event) return fn; return (...args) => { const _event = this.evalEvent(event)(...args); this.sendEvent(_event); return fn.apply(this, args); }; }; // useTracking 使用示例 const Example = (props: object) => { const handleClick = useTracking( // 业务逻辑 () => { console.log(业务逻辑); }, // 埋点逻辑 () => { return { type: "func_operation", params: { data, props.data }, }; } ); return <Button onClick={handleClick} />; };

 组件化点击埋点实现

使用者只需要把触发的回调(handleClick)绑定到对应的事件上即可         ,埋点逻辑由 埋点sdk 负责组合上去              。

function setClickEvent(ele) { // 对于列表  ,也可以遍历 return React.cloneElement(ele, { onClick: (event: React.MouseEvent<HTMLElement, MouseEvent>) => { const originClick = ele.props?.onClick || noop; doClickTracking(); originClick.call(ele, event); } }); } <TrackerClick name=button.click> { ({ handleClickTrack }) => <button onClick={() => { handleClick() /** 业务逻辑 */; handleClickTrack() /** 埋点逻辑 */; }}>点击</button> } </TrackerClick> /** class TrackerClick { constructor(props) { super(props); } handleClick() { } render() { return this.props.children({ handleClick }); } } */

组件化曝光埋点实现

仅向外暴露一个 setRef 用以获取 dom 执行监听工作                 ,其他工作都交给埋点 sdk 来处理            ,同时支持配置以下曝光规则:

threshold: 曝光阈值; visibleTime:组件曝光时长; once:是否进行重复曝光埋点监听。 // case1: 直接绑定dom return ( <TrackerExposure name=button.exposure extra={{ data }}> {({ setRef }) => <div ref={setRef}>{i + 1}</div>} </TrackerExposure>)) ); // case2: 自定义组件 const Test = React.forwardRef((props, ref) => (<div ref={ref} style={{ width: 150px, height: 150px, border: 1px solid gray }}>TEST</div>) ) return ( <TrackerExposure name="button.exposure" extra={{ data }}> {({ setRef }) => <Test ref={setRef} />} </TrackerExposure> ) /** class TrackerExposure { constructor(props) { super(props); } setRef() { } render() { return this.props.children({ setRef }); } } */

补充--微信小程序曝光埋点

微信小程序的 节点布局相交状态 API (IntersectionObserver),可用于监听两个或多个组件节点在布局位置上的相交状态            。这一组 API 常常可以用于推断某些节点是否可以被用户看见              、有多大比例可以被用户看见                。

/** 创建并返回一个 IntersectionObserver 对象实例   。 * 在自定义组件或包含自定义组件的页面中              , * 应使用 this.createIntersectionObserver([Object options]) 来代替          。 */ wx.createIntersectionObserver( /** 自定义组件实例 */ [Object component], /** * thresholds: 一个数值数组               ,包含所有阈值               。 * initialRatio: 初始的相交比例  ,如果调用时检测到的相交比例与这个值不相等且达到阈值           ,则会触发一次监听器的回调函数      。 * observeAll: 是否同时观测多个目标节点(而非一个)                ,如果设为 true     ,observe 的 targetSelector 将选中多个节点(注意:同时选中过多节点将影响渲染性能) */ [Object options] )

相关概念:

参照节点:监听的参照节点        ,取它的布局区域作为参照区域        。如果有多个参照节点                 ,则会取它们布局区域的 交集 作为参照区域              。页面显示区域也可作为参照区域之一         。 目标节点:监听的目标      ,默认只能是一个节点(使用 selectAll 选项时     ,可以同时监听多个节点)      。 相交区域:目标节点的布局区域与参照区域的相交区域              。 相交比例:相交区域占参照区域的比例            。 阈值:相交比例如果达到阈值                  ,则会触发监听器的回调函数   。阈值可以有多个              。

IntersectionObserver 一共有四个方法

IntersectionObserver.relativeTo(string selector, Object margins) 使用选择器指定一个节点         ,作为参照区域之一; IntersectionObserver.relativeToViewport(Object margins) 指定页面显示区域作为参照区域之一; IntersectionObserver.observe(string targetSelector, function callback) 指定目标节点并开始监听相交状态变化情况; callback: (res) => {}; res: { id string 节点 ID dataset Record.<string, any> 节点自定义数据属性 intersectionRatio number 相交比例 intersectionRect Object 相交区域的边界 boundingClientRect Object 目标边界 relativeRect Object 参照区域的边界 time number 相交检测时的时间戳 } IntersectionObserver.disconnect() 停止监听              。回调函数将不再触发。

显然  ,曝光上报的默认参照是 viewport                 ,所以使用 IntersectionObserver.relativeToViewport 作为参照物            ,进行曝光埋点:

Page({ data: { list: [ { value: 1, hadReport: false }, { value: 2, hadReport: false }, { value: 3, hadReport: false }, ] }, onLoad() { this._observer = this.createIntersectionObserver({ thresholds: [0.5], observeAll: true }); this._observer.relativeToViewport({ bottom: 0 }) .observe(.item, (res) => { const { index } = res.dataset; // item 下标; if (!this.data.list[index].hadReport) { console.log(`report ${index}`) this.data.list[index].hadReport = true; this.setData({ list: [].concat(this.data.list)}) } }) }, onUnload() { if (this._observer) this._observer.disconnect() } })

创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!

展开全文READ MORE
seo排名优化怎样(SEO快速排名优化方案怎么写的) 如何通过长尾提高SEO排名(掌握长尾的方法和技巧)