乐趣区

关于前端:腾讯二面现在要你实现一个埋点监控SDK你会怎么设计

大家好,我是年年!

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

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

这篇文章会讲清楚:

  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。

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

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

退出移动版