前端埋点sdk的方案十分成熟,之前用的都是公司内部统一的埋点产品,从前端埋点和数据上报后的可视化查询全链路打通。但是在最近的一个私有化项目中就遇到了问题,因为服务都是在客户自己申请的服务器上的,需要将埋点数据存放到自己的数据库中,同时前端埋点的功能简洁,不需要太多花里胡哨的东西。公司内部的埋点产品不适用,外部一些十分成熟的埋点产品又显得太臃肿,因此着手自己在开源包的基础上封了一个简单的埋点sdk,简单聊聊其中的一些功能和解决方式。
模块 | 模块名称 | Appid |
---|---|---|
门户 | portal | 10001 |
公文管理 | gongwen | 10050 |
公文管理移动端 | gongwen-mobile | 10051 |
业务生成器 | ofs | 10100 |
业务生成器移动端 | ofs-mobile | 10101 |
信息发布 | xinxi | 10150 |
信息发布移动端 | xinx-mobile | 10151 |
文档中心 | document | 10200 |
文档中心移动端 | document-mobile | 10201 |
对于产品来说,埋点上首要关心的是页面的pv、uv,其次是一些重要操作(以点击事件为主)的频率,针对某些曝光量高的页面,可能也会关注页面的热力图效果。满足这些关键功能的基础上,同时把一些通用的用户环境参数(设备参数、时间参数、地区参数)携带上来,发送请求到指定的后端服务接口,这就基本上满足了一个埋点skd的功能。
大概就具备了以下一些功能:
1.页面加载完成自动上报pv、uv 2.支持用户手动上报埋点 3.上报时默认携带时间、设备等通用参数 4.支持用户自定义埋点参数上报 5.支持用户标识设置 6.支持自动开始热力图埋点(页面中的任意点击会自动上报) 7.支持dom元素配置化的点击事件上报 8.支持用户自定义埋点上报接口配置
-
配置npm的源为:云效
-
安装包
npm install @linkofffice/tracker
-
配置环境
1、创建文件:\src\tracker\index.js
import Vue from 'vue' import Tracker from '@linkoffice/tracker' Vue.use(Tracker) const tracker = new Tracker({ masterReport: true, // 总开关,控制是否上报,默认开启 appid: '100001', // 应用标识,用来区分埋点数据中的应用 uuid: '', // 设备标识,自动生成并存在浏览器中, extra: {}, // 用户自定义上传字段对象 enableTrackerKey: true, // DOM上报 enableHeatMapTracker: false, // 是否开启热力图自动上报暂不开启 enableLoadTracker: true, // 是否开启页面加载自动上报,适合多页面应用的pv上报 enableHistoryTracker: true, // 是否开启页面history变化自动上报,适合单页面应用的history路由 enableHashTracker: true, // 是否开启页面hash变化自动上报,适合单页面应用的hash路由 requestUrl: '/tracker' // 埋点请求后端接口 }) export default tracker
2、引入main.js文件
import tracker from './tracker' new Vue({ router, store, tracker, // 注入vue render: h => h(App) }).$mount('#app')
-
设置登录人员信息
在有身份信息后请第一时间设置登录人的ID
this.$tracker.setUserId('1001')
-
js上报
在代码中直接加入对应的方法
// 埋点发送方法,3个参数分别是:事件类型,事件标识,上报数据 this.$tracker.sendTracker('click', 'module1', { a: 1, b: 2, c: 'ccc' })
-
DOM上报
需开启enableTrackerKey,直接在DOM 对象上设置属性
tracker-key
值为对应的eventId<Button type="submit" tracker-key="{{eventId}}">确定</Button>
参数 | 说明 | 类型 | 默认值 |
---|---|---|---|
masterReport | 上报总开关 | Boolean | true |
appid | 应用标识 | string | |
uuid | 设备标识,自动生成并存在浏览器中, | string | uuid |
extra | 用户自定义上传字段对象 | json对象 | {} |
enableTrackerKey | dom点击上报 | Boolean | false |
enableHeatMapTracker | 是否开启热力图自动上报 | Boolean | false |
enableLoadTracker | 是否开启页面加载自动上报,适合多页面应用的pv上报 | Boolean | false |
enableHistoryTracker | 是否开启页面history变化自动上报,适合单页面应用的history路由 | Boolean | false |
enableHashTracker | 是否开启页面hash变化自动上报,适合单页面应用的hash路由 | Boolean | false |
requestUrl | 埋点请求后端接口 | string |
事件 | 说明 | 参数 |
---|---|---|
setUserId | 设置登录人ID信息 | string |
sendTracker | 埋点发送方法 | 事件类型,事件标识,上报数据 |
字段 | 简写 | 含义 |
---|---|---|
appid | api | 应用标识 |
uuid | uid | 设备id |
branchid | bid | 分支ID |
eventType | et | 事件类型 |
eventId | ei | 事件ID |
userId | uei | 用户id |
browser | br | 浏览器类型 |
browserVersion | bv | 浏览器版本 |
engine | eng | 浏览器引擎 |
language | lang | 语言 |
device | dv | 客户端 |
os | os | 设备类型 |
osVersion | osv | 设备版本号 |
eventTime | eti | 埋点上报时间 |
title | tit | 页面标题 |
url | ul | 页面地址 |
domPath | dp | 事件触发的dom |
offsetX | ox | 事件触发的dom的x坐标 |
offsetY | oy | 事件触发的dom的y坐标 |
extra | 用户自定义字段对象 |
-
修改代码
-
编译代码
npm run build
-
推送私有化npm 说明
npm publish
pv的统计根据业务方需求有两种方式,第1种是完全由业务方自己来控制,在页面加载或变化的时候调用通用埋点方法来上报。第2种是通过初始化配置开启自动pv统计,由sdk来完成这一部分的埋点上报。第1种方式非常好理解,就不具体展开来,下面具体说一些sdk自动埋点统计的实现原理:
对于多页面应用,每次进一个页面就是一次pv访问,所以配置了 addEventListener = true 之后,sdk内部会对浏览器的load事件进行监听,当页面load后进行埋点上报,所以本质上是对浏览器load事件的监听和处理。
对于单页面应用来说,只有第一次加载页面才会触发load事件,后续路由的变化都不会触发。因此除了监听load事件外,还需要根据路由的变化监听对应的事件,单页面应用有两种路由模式:hash模式和history模式,两者的处理方式有所差异:
- hash模式,单页面应用的hash路由实现原理是通过改变url的hash值来实现无页面刷新的,hash的变化会触发浏览器的hashchange事件,因此埋点sdk中只需要对hashchange事件进行监听,就可以在事件触发时进行埋点上报。
- history模式,单页面应用的history路由实现的原理是通过操纵浏览器原生的history对象,history对象中记录着浏览器会话的历史记录,并提供了一些方法对会话栈进行管理。如:
history.go():
history.forward():
history.back():
history.pushState():
history.replaceState():
和hash模式不同的是,上述的history.go、history.forward 和 history.back 3个方法会触发浏览器的popstate事件,但是history.pushState 和 history.replaceState 这2个方法不会触发浏览器的popstate事件。然而主流的前端框架如react、vue中的单页面应用history模式路由的底层实现是依赖 history.pushState 和 history.replaceState 的。因此并没有原生的事件能够被用来监听触发埋点。为了解决这个问题,可以通过改写history的这两个事件来实现新事件触发:
const createHistoryEvent = function(type) {
var origin = history[type];
return function() {
var res = origin.apply(this, arguments);
var e = new Event(type);
e.arguments = arguments;
window.dispatchEvent(e);
return res;
};
};
history['pushState'] = createHistoryEvent('pushState');
history['replaceState'] = createHistoryEvent('replaceState');
改写完之后,只要在埋点sdk中对pushState和replaceState事件进行监听,就能实现对history模式下路由变化的埋点上报。
埋点对pv的支持是必不可少的,sdk会提供了一个设置用户uid的方法setUserId暴露给业务使用,当业务平台获取到登录用户的信息后,调用该方法,则会在后续的埋点请求中都带上uid,最后在埋点分析的时候以该字段进行uv的统计。但是这样的uv统计是不准确的,因为忽略了用户未登录的情况,统计出来的uv值是小于实际的,因此需要在用户未登录的情况下也给一个区分标识。这种标识常见的有以下几种方式:
- 用户ip地址
- 用户第一次访问时,在cookie或localStorage中存储一个随机生成的uuid
- 浏览器指纹追踪技术,通过获取浏览器具有辨识度的信息,进行一些计算得出一个值,那么这个值就是浏览器指纹,辨识度的信息可以是UA、时区、地理位置或者是你使用的语言等等
这几种方式各自存在着自己的一些弊端,ip地址准确度不够,比如同一个局域网内的共享一个ip、代理、动态ip等原因都会造成数据统计都错误。cookie和localStorage都缺陷是用户可以主动去清除。而浏览器指纹追踪技术的应用目前并不是很成熟。
综合考虑后,sdk中采用了localStorage技术,当用户第一次访问时,会自动生成一个随机的uuid存储下来,后续的埋点上报中都会携带这个uuid,进行用户信息都标识。同时如果业务平台调用了setUserId方法,则会把用户id存储到uid字段中。最后统计uv都时候,根据实际情况参考uid或者uuid字段,准确的uv数据,应该是介于uid和uuid之间的一个数值。
热力图埋点的意思是:监听页面中任意位置的用户点击事件,记录下点击的元素和位置,最后根据点击次数的多少,得到页面中的点击分布热力图。这一块的实现原理比较简单,只需要在埋点sdk中开启对所有元素对点击事件对监听即可,比较关键的一点是要计算出鼠标的点击x、y位置坐标,同时也可以把当前点击的元素名称或者class也一起上报,以便做更精细化的数据分析。
dom点击上报就是通过在dom元素上添加指定属性来达到自动上报埋点数据的功能。具体来说就是在页面的dom元素,配置一个 tracker-key = 'xxx' 的属性,表示需要进行该元素的点击上报,适用于上报通用的埋点数据(没有自定义的埋点数据),但是又不需要热力图上报的程度。这种配置方式是为了节省了要主动调用上报方法的步骤,但是如果埋点中有自定义的数据字段,还是应该在代码中去调用sdk的埋点上报方法。实现的方式也很简单,通过对body上点击事件进行全局监听,当触发事件时,判断当前event的getAttribute('tracker-key')值是否存在,如果存在则说明需要上报埋点事件,调用埋点上报方法即可
埋点上报的方式最常见的是通过img标签的形式,img标签发送埋点使用方便,且不受浏览器跨域影响,但是存在的一个问题就是url的长度会收到浏览器的限制,超过了长度限制,就会被自动截断,不同浏览器的大小限制不同,为了兼容长度限制最严格的IE浏览器,字符长度不能超过2083。 为了解决img上报的字符长度限制问题,可以使用浏览器自带的beacon请求来上报埋点,使用方式为:
navigator.sendBeacon(url, data);
这种方式的埋点上报使用的是post方法,因此数据长度不受限制,同时可将数据异步发送至服务端,且能够保证在页面卸载完成前发送请求,即埋点的上报不受页面意外卸载的影响,解决了ajax页面卸载会终止请求的问题。但是缺点也有两个:
1.存在浏览器的兼容性,主流的大部分浏览器都能支持,ie不支持。 2.需要服务端配置跨域
因此可以将这两种方式结合起来,封装成统一的方法来进行埋点的上报。优先使用img标签,当字符长度超过2083时,改用beacon请求,若浏览器不支持beacon请求,最好换成原生的ajax请求进行兜底。(不过如果不考虑ie浏览器的情况下,img上报的方式其实已经够用,是最适合的方式)
const reportTracker = function (url, data) {
const reportData = stringify(data);
let urlLength = (url + (url.indexOf('?') < 0 ? '?' : '&') + reportData).length;
if (urlLength < 2083) {
imgReport(url, data);
} else if (navigator.sendBeacon){
sendBeacon(url, data);
} else {
xmlHttpRequest(url, data);
}
}