本文作者 杨运心
头图来自Carlos Muza on Unsplash
1、前言
开始注释前先介绍一下相干概念,相熟的读者能够略过。
前端埋点:一种收集产品数据的形式,它的目标是上报相干行为数据,相干人员以数据为根据来剖析产品在用户端的应用状况,依据剖析进去的后果辅助产品优化、迭代。
BI:商业智能,公司外部做数据分析相干的部门。
2、背景
在流量红利逐步隐没的当初,数据的采集、剖析和精细化的经营显得更加重要,所以埋点在互联网产品中是很常见的,它能够更好的辅助咱们去迭代、欠缺产品性能。
平时咱们在实现根底的业务需要之后,还须要开发实现埋点需要。所以咱们谋求的是简略快捷的做好埋点工作,且不会占用咱们太多的精力。然而事实却不那么美妙,目前咱们团队在前端埋点方面存在一些痛点:
- 在结构埋点字段的时候须要依据 BI 的规定,把若干个字段拼接成一个,这样费时费力还有谬误的危险;
- 一些曝光场景下的点不好打比方:分页列表、虚构列表;他们的的曝光埋点实现较为繁琐;
- 逻辑复用问题:特地是曝光相干的点须要在业务代码外面做额定的解决,所以逻辑复用很艰难,对现有代码的侵入也很重大;
所以咱们须要一种适宜咱们的埋点计划解决咱们目前的问题,晋升咱们的开发效率,不再为埋点而困扰。
3、常见前端埋点计划
咱们对目前市场上几种埋点计划进行了一些调研,惯例有 3 种计划:
手动代码埋点:用户触发某个动作后手动上报数据
- 长处:是最精确的,能够满足很多定制化的需要。
- 毛病:埋点逻辑与业务代码耦合到一起,不利于代码保护和复用。
可视化埋点:通过可视化工具配置采集节点,指定本人想要监测的元素和属性。外围是查找 dom 而后绑定事件,业界比拟有名的是 Mixpanel
- 长处:能够做到按需配置,又不会像全埋点那样产生大量的无用数据。
- 毛病:比拟难加载一些运行时参数;页面构造发生变化的时候,可能就须要进行局部重新配置。
无埋点:也叫“全埋点”,前端主动采集全副事件并上报埋点数据,在后端数据计算时过滤出有用数据
- 长处:收集用户的所有端上行为,很全面。
- 毛病:有效的数据很多、上报数据量大。
4、埋点计划
在调研完这些计划后,我认为上述计划并不齐全适宜咱们,咱们须要的计划是精确、疾速埋点,同时把埋点的代码与业务逻辑解耦,并且咱们的音街挪动站能够绝对平滑的迁徙到咱们新的埋点库下面来。联合咱们目前的技术栈 React,以及现状和经营、产品侧的需要咱们决定采纳申明式的组件化埋点 + 缓冲队列计划,这里论述一下咱们的大抵思路。
- 为了解决埋点代码与业务逻辑耦合的问题,咱们认为能够在视图层解决,埋点能够演绎为两大类,点击与曝光埋点。咱们能够形象出两个组件别离解决这两种场景。
- 在一些场景下疾速滑动、频繁点击会在短时间打出大量的点,造成频繁的接口调用,这在挪动端是要防止的,针对这种场景咱们引入了缓冲队列,产生的点位信息先进入队列,通过定时工作分批次上报数据,针对不同类型的点也能够利用不同的上报频率。
- 目前对于一些字段采纳的是人工拼接,比方 BI 定义的 _mspm2 等相干通用字段,相似这种咱们齐全能够在库对立解决,既不容易出错,也不便前期拓展。
- 对于页面级曝光,咱们能够在埋点库初始化后主动注册对于页面曝光的相干事件,不须要使用者关怀。
以页面为维度治理埋点配置
- 咱们的站点是同构利用,跟咱们的架构比拟符合
- 更加清晰,便于保护
- 目前也是采纳这种计划治理,迁徙老本会更小
5、要害节点
5.1 流程梳理
这里存在一个问题,可能库还没初始化结束,一些点曾经产生了,比方曝光类的,如果这时候生成对应的点进入缓冲队列,就是属于有效的点因为没有加载到坑位信息、配置参数等,所以针对这种场景下产生的点位信息,咱们新开一个队列存储,等到初始化实现再去解决;
流程图:
5.2 点击埋点
点击埋点咱们开始的思考是提供一个组件,包裹须要进行点击埋点的 dom
元素,也有可能是组件
,而后给子元素绑定点击事件,当用户触发事件时进行埋点相干解决。
依照上述思路咱们就必须绑定点击事件到 dom 上,然而咱们又不想引入额定的 dom 元素,因为这会减少 dom 构造层级,给使用者带来麻烦,这样留给咱们的操作空间就剩下 props.children
,所以咱们去递归 TrackerClick
组件的 children
,找到最外层的dom元素,同时要求 TrackerClick
上面必须有一个 container
元素,依照这个思路咱们进行了解决。
export default function TrackerClick({ name, extra, immediate, children,}) { handleClick = () => { // todo append queue }; function AddClickEvent(ele) { return React.cloneElement(ele, { onClick: (e) => { const originClick = ele.props.onClick || noop; originClick.call(ele, e); handleClick(); } }); } function findHtmlElement(ele) { if (typeof ele.type === 'function') { if (ele.type.prototype instanceof React.Component) { ele = new ele.type(ele.props).render(); } else { ele = ele.type(ele.props); } } if (typeof ele.type === 'string') { return AddClickEvent(ele); } return React.cloneElement(ele, { children: findHtmlElement(ele.props.children) }); } return findHtmlElement(React.Children.only(children));}// case1<TrackerClick name='namespace.click'> <button>点击</button></TrackerClick>// case2<TrackerClick name='namespace.click'> <CustomerComp> <button>点击</button> </CustomerComp></TrackerClick>
从应用上来说很简便,达到了咱们的目标。然而通过咱们的实际也发现了一些问题,比方使用者并不分明外面的实现细节,有可能外面没有一个 container
包裹,也可能应用了 React.Fragment
造成一些不可预估的行为、同时也有形的减少了dom构造层级(尽管咱们没有引入,然而咱们在通知用户,你最好有个 container
)。
咱们又在反思这种计划的合理性,尽管应用上带来了便捷,然而带来了不确定性。通过探讨咱们决定把绑定的工作交给组件使用者,咱们只须要明确通知他能够应用哪些办法,这是确定性的工作。应用方只须要把触发的回调绑定到对应的事件上即可。
革新后如下:
<TrackerClick name='namespace.click'>{ ({ handleClick }) => <button onClick={handleClick}>点击坑位</button>}</TrackerClick>
5.3 曝光埋点
曝光对于咱们来说始终是比拟麻烦的,咱们先来看看曝光埋点的一些要求:
- 元素呈现在视窗内肯定的比例才算一次非法的曝光
- 元素在视窗内停留的时长达到肯定的规范才算曝光
- 统计元素曝光时长
站在前端的角度看实现这三点就比较复杂了,再加上一些分页、虚列表的场景就更加繁琐,带着这些问题调研了 IntersectionObserver。
IntersectionObservers calculate how much of a target element overlaps (or "intersects with") the visible portion of a page, also known as the browser's "viewport"IntersectionObservers
计算指标元素与页面可见局部的重叠水平(或 "相交"),也被称为浏览器的 "视口"。
const intersectionObserver = new IntersectionObserver(function(entries) { // If intersectionRatio is 0, the target is out of view // and we do not need to do anything. if (entries[0].intersectionRatio <= 0) return; console.log('Loaded new items');}, { // 曝光阈值 threshold: 0});// start observingintersectionObserver.observe(document.querySelector('.scrollerFooter'));
下面是 MDN 的一个例子,所以咱们是能够晓得元素什么时候进入以及什么时候来到 viewport,间接的下面三点需要咱们都能够实现。
通过调研,在能力方面能够满足咱们的需要、兼容性方面有对应的intersection-observer polyfill; 对于分页、虚列表,咱们只须要关注咱们须要观测的列表item
,所以咱们须要实现一个高性能的 ReactObserver
组件来提供 intersection-observer
的能力并对外提供相应的回调。如何实现一个高性能的Observer此处不做赘述。
上面是曝光组件绑定 dom 的两种形式
// case1: 间接绑定domrender() { return ( <div styleName='tracker-exposure'> { arr.map((item, i) => ( <TrackerExposure name='pagination.impress' extra={{ modulePosition: i + 1 }} > {({ addRef }) => <div ref={addRef}>{i + 1}</div>} </TrackerExposure> )) } </div> );}// case2: 自定义组件const Test = React.forwardRef((props, ref) => (<div ref={ref} style={{ width: '150px', height: '150px', border: '1px solid gray' }}>TEST</div>))render() { return (<div styleName="tracker-exposure"> { arr.map((item, i) => <TrackerExposure name="pagination.impress" extra={{ modulePosition: i + 1 }}> { ({ addRef }) => <Test ref={addRef} /> } </TrackerExposure> ) } </div>)}
应用上咱们仅提供一个 addRef 用以获取 dom 执行监听工作,其余工作都交给库来解决,曝光变得如此简略。针对上述3点要求,咱们提供配置如下:
- threshold: 曝光阈值,当 element 呈现在视窗多少比例触发
- viewingTime:元素曝光时长,用来判断是否是一次时长合规的曝光
- once:是否反复打曝光埋点
5.4 运行时参数
个别固定的参数咱们会放在config配置文件中治理,当然也有一些运行时的参数,比方 userId,modulePosition
等运行时字段,针对这种场景咱们提供 extra props
通过组件的 props
传递,在组件外部拼装,应用时只须要传入对应业务字段即可。
5.5 appendQueue
一些场景下咱们没法绑定事件到dom上,比方原生的元素:audio、video
,以及封装层级很深的业务组件,相似这种只对外提供了回调,针对这种场景咱们提供了 appendQueue
办法,把点退出到缓冲队列中。
appendQueue({ name: 'module.click', action: 'click', extra: { userId: 'xxx', }})
5.6 定时工作
咱们的设计是所有产生的点都会进入缓冲队列中,通过定时工作上报。目前策略是点击类上报频率 1000ms,曝光类 3000ms,当然这个距离也不是凭空想象的,通过跟算法、BI 探讨约定进去的,兼顾了前端的需要与算法那边实时性的要求,目前这两个值也是反对配置的。
对于定时工作的工夫距离,咱们取点击和曝光上报频率的最大公约数,以缩小执行次数。
5.7 页面曝光
咱们在初始化的时候会依据配置文件中约定的字段判断是否须要解决页面曝光;
页面曝光的要害是采集页面曝光的机会,浏览器的页面生命周期规范和标准才开始制订没多久,各个厂商反对的都不是很好,参考 Chrome 的页面生命周期中的 visibilitychange 事件作为采集页面曝光的机会。
visiblitychange 的浏览器兼容状况
6、应用
import Tracker, { TrackerExposure, appendQueue} from 'music/tracker';const generateConfig = () => ({ opus: { mspm: 'xxxx091781c235b0c828xxxx' }, 'playstart': { mspm: 'xxxx91981c235b0c8286xxxx', _resource_1_id: '', _resource_1_type: 'school' }, viewstart: { mspm: 'xxxxd091781c235b0c828xxx', type: 'page' }, viewend: { mspm: 'xxxx17b1b200b0c2e3xxxxxx', type: 'page', _time: '' }});export default Tracker;export { generateConfig, TrackerExposure, appendQueue};
import React, { useEffect, useState } from 'react';import Tracker, { generateConfig, TrackerExposure, appendQueue } from './tracker.js';const Demo = () => { const [opusList, setOpusList] = useState([]); useEffect(() => { Tracker.init({ common: { osVer: 'xxx', activityId: 'xxx', }, config: generateConfig() }); // fetch opuslist setOpusList(opus); }, []); const handleStart = () => { appendQueue({ name: 'playstart', action: 'playstart' }); } return <> { opusList.map(opus => <TrackerExposure start="opus" startExtra={{opusId: opus.id}} threshold={0.5}> { ({ addRef }) => <div ref={addRef} >{opus.name}</div> } </TrackerExposure>) } <Player onStart={handleStart}> <>;}
7、总结
咱们在音街挪动站中进行了迁徙、在多个经营流动中进行了应用,达到了咱们预期的指标;在提效方面,埋点库把费时的局部解决了,咱们须要做的就是从埋点平台把坑位信息放入配置文件,业务开发的时候应用对应的组件就能够了,简直没有太大的老本,且对于代码复用和保护来说也达到了目标。
在应用过程中发现对于点击类埋点 appendQueue 应用频率远高于 TrackerClick 组件,因为大部分元素的点击事件都有他本人的回调函数,然而咱们应用 TrackerClick 的初衷是埋点代码和业务代码解耦,这个也要依据理论场景去抉择。
8、参考资料
- MDN/IntersectionObserver
- react-intersection-observer
- page-lifecycle
本文公布自 网易云音乐大前端团队,文章未经受权禁止任何模式的转载。咱们长年招收前端、iOS、Android,如果你筹备换工作,又恰好喜爱云音乐,那就退出咱们 grp.music-fe(at)corp.netease.com!