数据埋点监控系统设计

数据监控和数据埋点

一、数据监控分类

(1)数据监控

数据监控,顾名思义就是监听用户的行为。常见的数据监控包括:

  • PV/UV:PV(page view),即页面浏览量或点击量。UV:指访问某个站点或点击某条新闻的不同IP地址的人数
  • 用户在每一个页面的停留时间
  • 用户通过什么入口来访问该网页
  • 用户在相应的页面中触发的行为

统计这些数据是有意义的,比如我们知道了用户来源的渠道,可以促进产品的推广,知道用户在每一个页面停留的时间,可以针对停留较长的页面,增加广告推送等等。

(2)性能监控

性能监控指的是监听前端的性能,主要包括监听网页或者说产品在用户端的体验。常见的性能监控数据包括:

  • 不同用户,不同机型和不同系统下的首屏加载时间
  • 白屏时间
  • http等请求的响应时间
  • 静态资源整体下载时间
  • 页面渲染时间
  • 页面交互动画完成时间

这些性能监控的结果,可以展示前端性能的好坏,根据性能监测的结果可以进一步的去优化前端性能,比如兼容低版本浏览器的动画效果,加快首屏加载等等。

(3)异常监控

此外,产品的前端代码在执行过程中也会发生异常,因此需要引入异常监控。及时的上报异常情况,可以避免线上故障的发上。虽然大部分异常可以通过try catch的方式捕获,但是比如内存泄漏以及其他偶现的异常难以捕获。常见的需要监控的异常包括:

  • Javascript的异常监控
  • 样式丢失的异常监控

二、常见的埋点方案

1. 代码埋点

代码埋点是最早的埋点方式,根据业务的分析需求,将埋点的采集代码加入到应用端。按照埋点实施方,又分为前端(客户端)埋点和后端(服务端)埋点两种类型。

1)客户端埋点

由前端开发手动定义数据采集时机、内容等将数据采集的代码代码段加入到前端业务代码中,当用户在前端产生对应行为时,触发数据采集代码。

优点:
  • 按需埋点,采集数据更全面,几乎可覆盖所有数据采集场景
  • 行为数据和业务数据可充分联合分析
缺点:
  • 延迟上报,数据丢失率高(5%-10%)
  • 需要客户端发版,用户端更新App
  • 埋点开发工作量大
  • 埋点流程需要多方协作,容易漏埋、错埋
适用场景:

全面分析用户在客户端的操作行为,对于一些电商交易类的产品,需要把行为和业务数据充分结合分析

2)服务端埋点

由服务端开发将埋点采集代码加入到后端服务请求中,当用户前端操作请求服务端数据时,按照约定规则触发埋点代码

优点
  • 按需埋点,采集数据更全面,几乎可覆盖所有数据采集场景
  • 行为数据和业务数据可充分联合分析
  • 数据采集实时上报,准确性高,丢失率低
  • 服务端更新,不需要客户端发版或用户更新版本
缺点
  • 纯前端操作不触发服务请求的按钮点击无法采集数据
  • 埋点开发工作量大
  • 埋点流程需要多方协作,容易漏埋、错埋
适用场景:

对于一些非点击、不可见的行为,或者要获取用户身份信息、更多的业务相关的属性信息。如果前后端都可以采集到,优先后端埋点

2. 全埋点

全埋点也有称之为无埋点或无痕埋点的,主要是将埋点采集代码封装成标准的SDK,应用端接入后,按照SDK的采集规则自动化地进行数据采集和上报

优点:
  • 接入SDK后,可自动采集数据,无需按需开发,节省开发成本
  • 页面可见元素均可自动采集,数据更全面
  • 埋点流程简单,业务使用埋点系统自助定义事件,新增埋点需求无需业务开发参与
缺点
  • 动态页面或页面不可见行为数据无法采集
  • 和业务强相关的属性信息采集困难
  • 数据全部采集,数据存储压力大
适用场景:

业务场景简单,如工具、应用类的产品,或者业务发展初期,产品快速迭代需求比精细化分析优先级更高,只需要分析简单的PV、UV

3. 可视化埋点

默认不采集数据,当数据分析人员通过设备连接用户行为分析工具的数据接入管理界面,在页面可视化定义需要采集的位点后下发采集请求,采集代码生效

优点:
  • 默认不上报数据,可视化圈选才按需触发埋点,节约存储和传输成本
  • 业务可视化圈选,埋点操作简单方便
缺点
  • 数据只在埋点圈选定义之后才有,历史数据无法回溯
  • 只能覆盖基本的点击、展示等用户行为,和业务强相关的属性信息采集困难
适用场景:

业务场景简单,如工具、应用类的产品,或者业务发展初期,产品快速迭代需求比精细化分析优先级更高,只需要分析简单的PV、UV

三、sdk设计

SDK的设计

在开始设计之前,先看一下SDK怎么使用。

import StatisticSDK from 'StatisticSDK';
// 全局初始化一次
window.insSDK = new StatisticSDK('uuid-12345');
<button onClick={()=>{
  window.insSDK.event('click','confirm');
  ...// 其他业务代码
}}>确认</button>

首先把SDK实例挂载到全局,之后在业务代码中调用,这里的新建实例时需要传入一个id,因为这个埋点监控系统往往是给多个业务去使用的,通过id去区分不同的数据来源。

首先实现实例化部分:

class StatisticSDK {
  constructor(productID){
    this.productID = productID;
  }
}

数据发送

数据发送是一个最基础的api,后面的功能都要基于此进行。通常这种前后端分离的场景会使用AJAX的方式发送数据,但是这里使用图片的src属性。原因有两点:

  1. 没有跨域的限制,像srcipt标签、img标签都可以直接发送跨域的GET请求,不用做特殊处理。
  2. 兼容性好,一些静态页面可能禁用了脚本,这时script标签就不能使用了。

但要注意,这个图片不是用来展示的,我们的目的是去「传递数据」,只是借助img标签的的src属性,在其url后面拼接上参数,服务端收到再去解析。

class StatisticSDK {
  constructor(productID){
    this.productID = productID;
  }
  send(baseURL,query={}){
    query.productID = this.productID;
    let queryStr = Object.entries(query).map(([key, value]) => `${key}=${value}`).join('&')
    let img = new Image();
    img.src = `${baseURL}?${queryStr}`
  }
}

img标签的优点是不需要将其append到文档,只需设置src属性便能成功发起请求。

通常请求的这个url会是一张1X1px的GIF图片,网上的文章对于这里为什么返回图片的是一张GIF都是含糊带过,这里查阅了一些资料并测试了:

1.同样大小,不同格式的的图片中GIF大小是最小的,所以选择返回一张GIF,这样对性能的损耗更小。

2.如果返回204,会走到img的onerror事件,并抛出一个全局错误;如果返回200和一个空对象会有一个CORB的告警。

3.当然如果不在意这个报错可以采取返回空对象,事实上也有一些工具是这样做的。

有一些埋点需要真实的加到页面上,比如垃圾邮件的发送者会添加这样一个隐藏标志来验证邮件是否被打开,如果返回204或者是200空对象会导致一个明显图片占位符。

<img src="http://www.example.com/logger?event_id=1234">1.

更优雅的web beacon

这种打点标记的方式被称web beacon(网络信标)。除了gif图片,从2014年开始,浏览器逐渐实现专门的API,来更优雅的完成这件事:Navigator.sendBeacon。

使用很简单。

Navigator.sendBeacon(url,data)

相较于图片的src,这种方式的更有优势:

  • 不会和主要业务代码抢占资源,而是在浏览器空闲时去做发送。
  • 并且在页面卸载时也能保证请求成功发送,不阻塞页面刷新和跳转。

现在的埋点监控工具通常会优先使用sendBeacon,但由于浏览器兼容性,还是需要用图片的src兜底。

用户行为监控

上面实现了数据发送的api,现在可以基于它去实现用户行为监控的api。

class StatisticSDK {
  constructor(productID){
    this.productID = productID;
  }
  // 数据发送
  send(baseURL,query={}){
    query.productID = this.productID;
      let queryStr = Object.entries(query).map(([key, value]) => `${key}=${value}`).join('&')
      let img = new Image();
      img.src = `${baseURL}?${queryStr}`
  }
  // 自定义事件
  event(key, val={}) {
    let eventURL = 'http://demo/'
    this.send(eventURL,{event:key,...val})
  }
  // pv曝光
  pv() {
    this.event('pv')
  }
}

用户行为包括自定义事件和pv曝光,也可以把pv曝光看作是一种特殊的自定义行为事件。

页面性能监控

页面的性能数据可以通过performance.timing这个API获取到,获取的数据是单位为毫秒的时间戳。

img上面的不需要全部了解,但比较关键的数据有下面几个,根据它们可以计算出FP/DCL/Load等关键事件的时间点:

  1. 页面首次渲染时间:FP(firstPaint)=domLoading-navigationStart。
  2. DOM加载完成:DCL(DOMContentEventLoad)=domContentLoadedEventEnd-navigationStart。
  3. 图片、样式等外链资源加载完成:L(Load)=loadEventEnd-navigationStart。

上面的数值可以跟performance面板里的结果对应。

回到SDK,我们只用实现一个上传所有性能数据的api就可以了:

class StatisticSDK {
  constructor(productID){
    this.productID = productID;
    // 初始化自动调用性能上报
    this.initPerformance()
  }
  // 数据发送
  send(baseURL,query={}){
    query.productID = this.productID;
      let queryStr = Object.entries(query).map(([key, value]) => `${key}=${value}`).join('&')
      let img = new Image();
      img.src = `${baseURL}?${queryStr}`
  }
  // 性能上报
  initPerformance(){
    let performanceURL = 'http://performance/'
    this.send(performanceURL,performance.timing)
  }
}

并且,在构造函数里自动调用,因为性能数据是必须要上传的,就不需要用户每次都手动调用了。

错误告警监控

错误报警监控分为JS原生错误和React/Vue的组件错误的处理。

JS原生错误

除了try catch中捕获住的错误,我们还需要上报没有被捕获住的错误——通过error事件和unhandledrejection事件去监听。

error

error事件是用来监听DOM操作错误DOMException和JS错误告警的,具体来说,JS错误分为下面8类:

  1. InternalError: 内部错误,比如如递归爆栈。
  2. RangeError: 范围错误,比如new Array(-1)。
  3. EvalError: 使用eval()时错误。
  4. ReferenceError: 引用错误,比如使用未定义变量。
  5. SyntaxError: 语法错误,比如var a = 。
  6. TypeError: 类型错误,比如[1,2].split(‘.’)。
  7. URIError: 给 encodeURI或 decodeURl()传递的参数无效,比如decodeURI(‘%2’)。
  8. Error: 上面7种错误的基类,通常是开发者抛出。

也就是说,代码运行时发生的上述8类错误,都可以被检测到。

unhandledrejection

Promise内部抛出的错误是无法被error捕获到的,这时需要用unhandledrejection事件。

回到SDK的实现,处理错误报警的代码如下:

class StatisticSDK {
  constructor(productID){
    this.productID = productID;
    // 初始化错误监控
    this.initError()
  }
  // 数据发送
  send(baseURL,query={}){
    query.productID = this.productID;
      let queryStr = Object.entries(query).map(([key, value]) => `${key}=${value}`).join('&')
      let img = new Image();
      img.src = `${baseURL}?${queryStr}`
  }
  // 自定义错误上报
  error(err, etraInfo={}) {
    const errorURL = 'http://error/'
    const { message, stack } = err;
    this.send(errorURL, { message, stack, ...etraInfo})
  }
  // 初始化错误监控
  initError(){
    window.addEventListener('error', event=>{
      this.error(error);
    })
    window.addEventListener('unhandledrejection', event=>{
      this.error(new Error(event.reason), { type: 'unhandledrejection'})
    })
  }
}

和初始化性能监控一样,初始化错误监控也是一定要做的,所以需要在构造函数中调用。后续开发人员只用在业务代码的try catch中调用error方法即可。

React/Vue组件错误

成熟的框架库都会有错误处理机制,React和Vue也不例外。

React的错误边界

错误边界是希望当应用内部发生渲染错误时,不会整个页面崩溃。我们提前给它设置一个兜底组件,并且可以细化粒度,只有发生错误的部分被替换成这个「兜底组件」,不至于整个页面都不能正常工作。

它的使用很简单,就是一个带有特殊生命周期的类组件,用它把业务组件包裹起来。

这两个生命周期是getDerivedStateFromError和componentDidCatch。

代码如下:

// 定义错误边界
class ErrorBoundary extends React.Component {
  state = { error: null }
  static getDerivedStateFromError(error) {
    return { error }
  }
  componentDidCatch(error, errorInfo) {
    // 调用我们实现的SDK实例
    insSDK.error(error, errorInfo)
  }
  render() {
    if (this.state.error) {
      return <h2>Something went wrong.</h2>
    }
    return this.props.children
  }
}
...
<ErrorBoundary>
  <BuggyCounter />
</ErrorBoundary>1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.

回到SDK的整合上,在生产环境下,被错误边界包裹的组件,如果内部抛出错误,全局的error事件是无法监听到的,因为这个错误边界本身就相当于一个try catch。所以需要在错误边界这个组件内部去做上报处理。也就是上面代码中的componentDidCatch生命周期。

Vue的错误边界

vue也有一个类似的生命周期来做这件事,不再赘述:errorCaptured。

Vue.component('ErrorBoundary', {
  data: () => ({ error: null }),
  errorCaptured (err, vm, info) {
    this.error = `${err.stack}\n\nfound in ${info} of component`
    // 调用我们的SDK,上报错误信息
    insSDK.error(err,info)
    return false
  },
  render (h) {
    if (this.error) {
      return h('pre', { style: { color: 'red' }}, this.error)
    }
    return this.$slots.default[0]
  }
})
...
<error-boundary>
  <buggy-counter />
</error-boundary>

现在我们已经实现了一个完整的SDK的骨架,并且处理了在实际开发时,react/vue项目应该怎么接入。

参考文献:

https://www.51cto.com/article/706364.html

Donate
  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.
  • Copyrights © 2022-2023 alan_mf
  • Visitors: | Views:

请我喝杯咖啡吧~

支付宝
微信