大家好,我是年年!

这是小伙伴上周被问到的一个综合性设计题,如果是没有用过埋点监控零碎,或者没有深刻理解,根本就凉凉。

原文首发在我的公众号:前端私教年年

这篇文章会讲清楚:

  1. 埋点监控零碎负责解决哪些问题,须要怎么设计api?
  2. 为什么用img的src做申请的发送,sendBeacon又是什么?
  3. 在react、vue的谬误边界中要怎么解决?

什么是埋点监控SDK

举个例子,公司开发上线了一个网站,但开发人员不可能预测,用户理论应用时会产生什么:用户浏览过哪几个页面?几成用户会点击某个弹窗的确认按钮,几成会点击勾销?有没有呈现页面解体?

所以咱们须要一个埋点监控SDK去做数据的收集,后续再统计分析。有了剖析数据,能力有针对性对网站进行优化:PV特地少的页面就不要节约大量人力;有bug的页面连忙修复,不然要325了。

比拟有名的埋点监控有Google Analytics,除了web端,还有iOS、安卓的SDK。

公众号后盾回复「ReactSDK」可获取react版本的github

埋点监控的职能范畴

因为业务须要的不同,大部分公司都会本人开发一套埋点监控零碎,但基本上都会涵盖这三类性能:

用户行为监控

负责统计PV(页面拜访次数)、UV(页面拜访人数)以及用户的点击操作等行为。

这类统计是用的最多的,有了这些数据能力量化咱们的工作成绩。

页面性能监控

开发和测试人员诚然在上线之前会对这些数据做评估,但用户的环境和咱们不一样,兴许是3G网,兴许是很老的机型,咱们须要晓得在理论应用场景中的性能数据,比方页面加载工夫、白屏工夫等。

谬误报警监控

获取谬误数据,及时处理能力防止大量用户受到影响。除了全局捕捉到的错误信息,还有在代码外部被catch住的谬误告警,这些都须要被收集到。

上面会从api的设计登程,对上述三种类型进一步开展。

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的告警;

当然如果不在意这个报错能够采取返回空对象,事实上也有一些工具是这样做的
  1. 有一些埋点须要实在的加到页面上,比方垃圾邮件的发送者会增加这样一个暗藏标记来验证邮件是否被关上,如果返回204或者是200空对象会导致一个显著图片占位符

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

更优雅的web beacon

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

应用很简略

Navigator.sendBeacon(url,data)

相较于图片的src,这种形式的更有劣势:

  1. 不会和次要业务代码抢占资源,而是在浏览器闲暇时去做发送;
  2. 并且在页面卸载时也能保障申请胜利发送,不阻塞页面刷新和跳转;

当初的埋点监控工具通常会优先应用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获取到,获取的数据是单位为毫秒的工夫戳。


下面的不须要全副理解,但比拟要害的数据有上面几个,依据它们能够计算出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的谬误边界

谬误边界是心愿当利用外部产生渲染谬误时,不会整个页面解体。咱们提前给它设置一个兜底组件,并且能够细化粒度,只有产生谬误的局部被替换成这个「兜底组件」,不至于整个页面都不能失常工作。

它的应用很简略,就是一个带有非凡生命周期的类组件,用它把业务组件包裹起来。

这两个生命周期是getDerivedStateFromErrorcomponentDidCatch

代码如下:

// 定义谬误边界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>
建了一个在线sandbox能够体验,公众号后盾回复「谬误边界demo」获取地址

回到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我的项目应该怎么接入。

理论生产应用的SDK会更强壮,但思路也不外乎,感兴趣的能够去读一读源码。

结语

文章比拟长,但想答好这个问题,这些常识储备都是必须的。

咱们要设计SDK,首先要分明它的根本应用办法,才晓得前面的代码框架要怎么搭;而后是明确SDK的职能范畴:须要能解决用户行为、页面性能以及谬误报警三类监控;最初是react、vue的我的项目,通常会做谬误边界解决,要怎么接入咱们本人的SDK。

如果感觉这篇文章对你有用,点赞关注是对我最大的激励!

你的反对是我创作的能源!