让我们一起写一个前端监控系统吧(2)

语言: CN / TW / HK

theme: fancy

“我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第3篇文章,点击查看活动详情

  • 相关链接

让我们一起写一个前端监控系统吧!(1)

让我们一起写一个前端监控系统吧!(3)

上一期中我们讲了前端系统中前端的基本架构,大家想必对我们的项目有了深入的了解,本篇文章中,我们将详细介绍被监控网站,有啥值得监控,以及由此 npm 包如何书写。

由上一章的介绍我们可以知道,被监控网站分为四个部分,分别是淘宝首页实时聊天表单按钮在线博客.

在开始讲解之前,我先把 npm 包的链接放上来

淘宝首页

  • 此页面主要负责监控
    1. 组件加载时间
    2. 加载白屏时间、
    3. FCP(First Contentful Paint) : 首次绘制任何文本,图像,非空白canvas或SVG的时间点.
    4. LCP(Largest Contentful Paint) : 可视区域“内容”最大的可见元素开始出现在页面上的时间点。

那么话不多说,让我们顺序来看一下这些页面是如何被监控的!

npm 插件mixin,监控组件加载时间。

此组件并非无脑使用,我们设置了一个开关this.$options.computeTime,如果您想要使用,在组件中打开开关即可「即设置为 true 」,实现了组件化的粒度细分

```js import http from '../utils/request'

let mixin = {

beforeCreate() {
    // 我们在这里添加了一个属性
    let shouldcompute = this.$options.computeTime
    // 如果用户设置了这个属性,那么就可以启用mixin,获得加载时间。
    if (!shouldcompute) return
    // 获取创建之初的时间
    this.createTime = new Date().getTime()

},

mounted() {

    let shouldcompute = this.$options.computeTime

    if (!shouldcompute) return
    // 获取挂载完成的时间
    this.endTime = new Date().getTime()
    // 得到加载时间
    let mountTime = this.endTime - this.createTime
    // 获取所有的节点
    let componentNameArr = this.$vnode.tag.split('-')
    // 从而获取当前节点
    let componentName = componentNameArr[componentNameArr.length - 1]
    // 将得到的数据发送给后台
    http.post('plugin/mount', {

        kind: 'experience',

        type: 'ComponentMountTime',
        // 组件名称
        componentName,
        // 组件加载时间
        mountTime,
        // 发送当前时间
        timeStamp: Date.now(),

    })

},

} // 将mixin封装起来 export default {

install(Vue, options) {

    const oldRevue = Vue.prototype.$revue

    Vue.prototype.$revue = Object.assign({}, oldRevue, {

    compnentMount: mixin

})

// Vue.mixin(mixin)

},
immediate: {

    install(Vue, options) {

        Vue.mixin(mixin)

    },

},

m: mixin

} 我们特意设置了 immediate这个属性,你可以通过如下方法在`main.js`中进行调用。js Vue.use(revue.immediate) ```

npm插件 判断是否白屏

```js import onload from '../utils/onload' import http from '../utils/request'

let blankScreen = () => { let wrapperElements = ['html', 'body', '#app'] let emptyPoints = 0 // function getSelector(element) { if (element.id) { return '#' + element.id } else if (element.className) { // a b c => .a.b.c return ( '.' + element.className .split(' ') .filter((item) => !!item) .join('.') ) } else { return element.nodeName.toLowerCase() } } function isWrapper(element) { let selector = getSelector(element) if (wrapperElements.indexOf(selector) !== -1) { emptyPoints++ } } // 使用 elementsFromPoint 与 isWrapper 来判断是否白屏 onload(function () { for (let i = 1; i <= 9; i++) { let xElements = document.elementsFromPoint( (window.innerWidth * i) / 10, window.innerHeight / 2 ) let yElements = document.elementsFromPoint( window.innerWidth / 2, (window.innerHeight * i) / 10 ) isWrapper(xElements[0]) isWrapper(yElements[0]) }

if (emptyPoints >= 18) {
  let centerElements = document.elementsFromPoint(
    window.innerWidth / 2,
    window.innerHeight / 2
  )

  http.post('/plugin/blank', {
    kind: 'stability',
    type: 'blank',
    emptyPoints,
    screen: window.screen.width + 'X' + window.screen.height,
    viewPoint: window.innerWidth + 'X' + window.innerHeight,
    timeStamp: Date.now(),
    selector: getSelector(centerElements[0]),
  })
}

}) }

export default { install(Vue, options) { const oldRevue = Vue.prototype.$revue Vue.prototype.$revue = Object.assign({}, oldRevue, { blankScreen }) }, immediate: { install(Vue, options) { blankScreen() const oldRevue = Vue.prototype.$revue Vue.prototype.$revue = Object.assign({}, oldRevue, { blankScreen }) }, }, b: blankScreen } ```

判断是否是白屏的思路是:我们对页面上的9个点分横纵进行判断是否有元素,如果18次判断全部都没有检索到元素,我们就需要向后端发送数据,让它知道这个页面发生白屏了!

npm插件,获取性能数据

```js import onload from '../utils/onload' import http from '../utils/request'

let timing = () => { let FMP, LCP // 加一个 if 是因为有时候这玩意是 undefined if (PerformanceObserver) { // 增加一个性能条目的观察者 new PerformanceObserver((entryList, observer) => { let perfEntries = entryList.getEntries() FMP = perfEntries[0] //startTime 2000以后 observer.disconnect() //不再观察了 }).observe({ entryTypes: ['element'] }) //观察页面中的意义的元素

new PerformanceObserver((entryList, observer) => {
  let perfEntries = entryList.getEntries()
  LCP = perfEntries[0]
  observer.disconnect() //不再观察了
}).observe({ entryTypes: ['largest-contentful-paint'] }) //观察页面中的意义的元素

}

//用户的第一次交互 点击页面 onload(function () { setTimeout(() => { const { fetchStart, loadEventStart } = performance.timing // 此处直接使用 API 了 let FP = performance.getEntriesByName('first-paint')[0] let FCP = performance.getEntriesByName('first-contentful-paint')[0] let loadTime = loadEventStart - fetchStart //开始发送性能指标 //console.log('FP', FP) //console.log('FCP', FCP) //console.log('FMP', FMP) //console.log('LCP', LCP) http.post('/plugin/paint', { kind: 'experience', //用户体验指标 type: 'paint', //统计每个阶段的时间 firstPaint: FP.startTime, firstContentfulPaint: FCP.startTime, firstMeaningfulPaint: FMP?.startTime || -1, largestContentfulPaint: LCP?.startTime || -1, timeStamp: Date.now(), }) http.post('/plugin/load', { kind: 'experience', //用户体验指标 type: 'load', //统计每个阶段的时间 loadTime, timeStamp: Date.now(), }) }, 3000) }) }

export default { install(Vue, options) { const oldRevue = Vue.prototype.$revue Vue.prototype.$revue = Object.assign({}, oldRevue, { timing }) }, immediate: { install(Vue, options) { timing(Vue, options) const oldRevue = Vue.prototype.$revue Vue.prototype.$revue = Object.assign({}, oldRevue, { timing }) }, }, t: timing }

```

表单页面

表单按钮 => 『监控错误内容』

  • 表单按钮这里的逻辑主要是对常见的JS错误进行汇总,然后收集起来,发送到后端。

这里有一张图,涵盖了JS的主要错误。 7951663055478_.pic.jpg

  1. EvalError错误

// html <button @click="EvalError"> // js代码 /* * 如果此处非法使用 eval(),则抛出 EvalError 异常 * 根据 ES9 * 此异常不再会被JavaScript抛出,但是EvalError对象仍然保持兼容性 **/ EvalError() { return eval( '(' + obj + ')' ) } 复制代码

  1. InternalError错误

// html <button @click="InternalError"> // js代码 /** * 该错误在JS引擎内部发生,特别是当它有太多数据要处理并且堆栈增长超过其关键限制时。 */ InternalError() { function foo() { foo() } foo() } 复制代码

  1. RangeError错误

// html <button @click="RangeError"> // js // 当数字超出允许的值范围时,将抛出此错误 RangeError() { const arr = [99, 88] arr.length = 99 ** 99 } 复制代码

  1. ReferenceError错误

// html <button @click="ReferenceError"> // js // 当出现非法引用的时候报错 ReferenceError() { foo.substring(1); } 复制代码

  1. URIError错误

// html <button @click="URIError"> // js /** * 用 encodeURI 等编码含有不合法字符的字符串,导致编解码失败 * 编码操作会将每一个字符实例替换为一到四个相对应的UTF-8编码形式的转义序列。 * 如果试图一个非高-低位完整的代理自负,将会抛出一个URIError错误 */ URIError() { let a = encodeURI('\uD800%') console.log(a) } 复制代码

  1. TypeError错误

  2. 此处访问到了undefined

// html <button @click="TypeError"> // js代码 TypeError() { window.someVar.error = 'error' } 复制代码

  1. AsyncError错误 | Promise错误

// html <button @click="Async"> // js代码 AsyncError() { Promise.reject('this is an error message'); } 复制代码

npm 封装 「我们如何获取错误信息?」

概述JSError有同步错误也有异步错误,window.onerror既可以捕获同步错误也可以捕获异步错误,在Vue中有一个API叫Vue.config.errorHandler,会截取同步错误,window.onerror自然就接收被筛选出来的异步错误了,但是这个时候我们发现Promise错误并没有被window.onerror捕获到,所以我们还需要unhandledrejection来捕获这个错误,至此,所有的错误就捕获完毕了。

我们在写入我们自己的方法之前,不能直接覆盖,需要确认用户是否使用过我们使用的方法,如果没有使用过,那么我们就可以直接使用,如果使用过,那我们就调用一下call方法

ts const oldErrorHandler = Vue.config.errorHandler Vue.config.errorHandler = (error, vm, info) => { if(oldErrorHandler) oldErrorHandler.call(this, error, vm, info) }

我们需要使用一个包「StackTracey」把错误处理一下,处理成我们好处理的样子

ts import StackTracey from 'stacktracey' const stack = new StackTracey(error)

使用Vue.config.errorHandler来捕获同步错误

js Vue.config.errorHandler = (error, vm, info) => { if (oldErrorHandler) oldErrorHandler.call(this, err, vm, info) const stack = new StackTracey(error) const log = { kind: "stability", errorType: "jsError", //jsError simpleUrl: window.location.href.split('?')[0].replace('#', ''), // 页面的url timeStamp: new Date().getTime(), // 日志发生时间 position: `${stack.items[0].column}:${stack.items[0].line}`, // 需要处理掉无效的前缀信息 fileName: stack.items[0].fileName, //错误文件名 message: stack.items[0].callee, //错误信息 detail: `${error.toString()}`, isYibu: 'false', //是否是异步 } console.error(error) axios.post('/plugin/postErrorMessage', log) }

使用window.onerror来捕获异步错误

js window.addEventListener("error", function (event) { // console.log(event) let log = { kind: "stability", //稳定性指标 errorType: "jsError", //jsError simpleUrl: window.location.href.split('?')[0].replace('#', ''), // 页面的url timeStamp: new Date().getTime(), // 日志发生时间 position: (event.lineno || 0) + ":" + (event.colno || 0), //行列号 fileName: event.filename, //报错链接 message: event.message, //报错信息 detail: "null", isYibu: "ture" }; axios.post('/plugin/postErrorMessage', log) }, true ); // true代表在捕获阶段调用,false代表在冒泡阶段捕获,使用true或false都可以

使用unhandledrejection来捕获Promise错误

js window.addEventListener("unhandledrejection", function (event) { // console.log(event) let log = { kind: "stability", //稳定性指标 errorType: "jsError", //jsError simpleUrl: window.location.href.split('?')[0].replace('#', ''), // 页面的url timeStamp: new Date().getTime(), // 日志发生时间 message: event.reason, //报错信息 fileName: "null", //报错链接 position: (event.lineno || 0) + ":" + (event.colno || 0), //行列号 detail: "null", isYibu: "ture" }; axios.post('/plugin/postErrorMessage', log) }, true ); // true代表在捕获阶段调用,false代表在冒泡阶段捕获,使用true或false都可以

博客页面「HTTP Error 页面」

主要功能: 上报页面请求报错信息

实现:

通过修改原型链的形式实现对原ajax请求、fetch请求的封装和增强,捕获并向后台发送错误数据。

以下是对监控Ajax错误的实现,我们对浏览器内置的XMLHttpRequest的open和send方法进行保存,并通过修改原型链的形式对以上两个方法进行重写和增强,正是通过这种方法我们既没有影响被监控页面原本请求业务的实现,又完成了我们捕获请求错误的目的。

``` let XMLHttpRequest = window.XMLHttpRequest; let oldOpen = XMLHttpRequest.prototype.open; //缓存老的open方法 XMLHttpRequest.prototype.open = function (method, url, async) { //重写open方法 if (!url.match(/plugin/)) { //防止死循环 this.logData = { method, url, async }; //增强功能,把初始化数据保存为对象的属性 } return oldOpen.apply(this, arguments); };

let oldSend = XMLHttpRequest.prototype.send; //缓存老的send方法 XMLHttpRequest.prototype.send = function (body) { //重写sned方法 if (this.logData) { //如果有值,说明已经被拦截了 // ...... let handler = (type) => (e) => { let data = { //...把我们想要的数据给记录下来 }; tracker.postHTTP(data); }; this.addEventListener("load", handler("load"), false); //传输完成,所有数据保存在 response 中 this.addEventListener("error", handler("error"), false); //500也算load,只有当请求发送不成功时才是error this.addEventListener("abort", handler("abort"), false); //放弃 } return oldSend.apply(this, arguments); }; ```

以下是捕获fetch请求错误的实现,其实是基于类似的思路

if (!window.fetch) return; let oldFetch = window.fetch; window.fetch = function (url, obj = { method: "GET", body: "" }) { //...... return oldFetch .apply(this, arguments) .then((response) => { if (!response?.ok) { console.log("test", response); // True if status is HTTP 2xx if (!url.match(/plugin/)) { tracker.postHTTP({ //这里写入我们想向后台发送的错误数据 }); } } }) .catch((error) => { // 上报错误 console.error("I am error", error); tracker.postHTTP({ //这里写入我们想向后台发送的错误数据 }); // throw error; }); };

websocketError部分

监听 websocket 错误 通过保留原型链方法再扩展的形式实现对websocket监听,捕获到错误向后台发送错误数据。

const monitor = () => { /* */ WebSocket.prototype.oldsend = WebSocket.prototype.send; WebSocket.prototype.send = function (data) { // 记得开始时间 WebSocket.prototype.startTime = new Date(); // 调用原方法 WebSocket.prototype.oldsend.call(this, data); }; WebSocket.prototype.oldclose = WebSocket.prototype.close; WebSocket.prototype.close = function (err) { /* 错误逻辑发送数据 */ WebSocket.prototype.oldclose.call(this); } }; // WebSocket.prototype... };

至此,我们详细介绍了 npm 包,并讲述了我们创建 npm 包的思路,希望这篇文章能让你有所收获!

往期文章: + 让我们一起写一个前端监控系统吧(1)