前端监控系统总结,一步一步从零复盘(三)

257 阅读6分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 2 天、第 2 篇,点击查看活动详情

书接上回

上一篇我们完成了对JS运行时错误,未捕获的Promise错误以及自定义错误的监控以及数据上报,本篇文章则是对静态资源监控的分享

静态资源请求错误上报

我们监听JS运行时错误的方法是重写window.onerror方法,那么可不可以用window.addEventListener("error",callback)去监听呢,答案是可以的,那么他们的区别在哪里

首先当产生JS错误时后者会先于前者触发,但是传参只有一个保存所有错误信息的参数,并且不能阻止默认事件处理函数的执行,其他都大体相同,但最重要的一点是,后者还可以全局捕获资源加载异常的错误,因此我们就让window.onerror去专门处理JS运行时错误,而window.addEventListener("error",callback)我们来专门处理静态资源加载的错误。

参考代码

  window.addEventListener(
    "error",
    (event) => {
      if (event.target &&(event.target.src || event.target.href)) {//筛选出静态资源异常的错误,如果采用gif打点的方式上报还要过滤上报的请求
        tracker.send({
          type: "resourceError", // 资源获取错误
          filename: event.target.src || event.target.href, // 报错文件
          tagName: event.target.tagName,//标签名名称
          triggerTimeStamp: event.timeStamp, //触发时间
          selector: getSelector(event.target), // 最后一个操作的元素
        });
      }
    },
    true
  );

数据模型

{
    '...':'...',//公共字段
    "type": "resourceError",
    "filename": "http://localhost:8080/error.js",
    "tagName": "SCRIPT",
    "triggerTimestamp": "76",
    "selector": "HTML BODY #container .content img"
}

静态资源请求上报

如果无我们的可视化界面需要展示静态资源请求的数量、请求效率、错误率这些一类关于静态资源请求的信息,我们就需要记录所有静态资源请求并且区分成功的请求以及错误的请求,那么我们该如何监控静态资源呢?就不得不给大家介绍一个超级实用的浏览器api:`PerformanceObserver,我们可以利用它获取到很多的性能指标,在后面关于首屏渲染的指标、白屏时间等等相关的文章中我们都会看到他的身影,那么它究竟是做什么的呢。

PerformanceObserver() 构造函数使用给定的观察者 callback 生成一个新的 PerformanceObserver 对象。当通过 observe() 方法注册的 条目类型性能条目事件 被记录下来时,调用该观察者回调.

观察的性能事件被记录时将调用 PerformanceObserverCallback 回调。调用回调时,其第一个参数是 性能观察条目列表 (en-US),第二个参数是 观察者 对象。

这是MDN对它的介绍,简单点讲它类似与监听器,通过observe()方法注册它的监听对象,当注册对象中产生新的性能条目时会触发回调函数,参数即产生这些新的性能条目。那么它怎么监控静态资源呢。

    let obs=new PerformanceObserver((list,boserver)=>{
        console.log('argumentList:',list);
        console.log('argumentBoserver:',boserver);
    })
    obs.observe({entryTypes:['resource']});

就这么简单,每当有新的静态资源请求完成时(不论成功与否),就会有一条新的性能条目生成,并且调用回调函数,其第一个参数是 性能观察条目列表 (PerformanceObserverEntryList),我们可以调用他内部的getEntries()方法获取性能指标数组。第二个参数是 观察者(PerformanceObserver) 对象,用于接收新的性能条目。

list.getEntries();

entryTypes中包含resource时,并且添加了相应的条目时,回调函数中的list调用list.getEntries()方法会返回一个PerformanceResourceTiming对象的数组

image.png

image.png

对象中会包含文件的大小、一些文件的名称、类型等信息,还有一些相对浏览器时间的时间戳,可以利用他们计算出一些性能指标,具体属性介绍可以参考MDN官网。现在呢我们拿到了网页静态资源请求的性能指标,剩下的事情就是对数据做一些处理以及上报了。

但是还有几点注意点:

  1. 因为它如果是资源请求失败也会产生新的性能条目,并触发回调,因此我们也可以用它来做静态资源请求错误的上报,但是如果我们使用刚才讲的方法监控错误,那么就要在这里过滤一下请求错误的条目
  2. 而且使用中发现**PerformanceObserver()** 的回调调用要先于上面的error监听器,因此我们还需要一步延迟操作。
  3. PerformanceResourceTiming不仅会包含静态资源的性能条目,它还会包含HTTP请求等其他一些资源请求的性能条目(意思就是他甚至还能做HTTP监听),因此我们还需要过滤出资源请求的条目。

参考代码

const ASSETS_TYPE = ['img', 'css', 'script', 'link', 'audio', 'video'];//用于过滤出静态资源let errorImg:Array<string>=[]
//这里代理的目的是为了如果在外部调用也能在这里同步变化
export let errorImgProxy=new Proxy(errorImg,{
    set(target, prop, value) {
        Reflect.set(target, prop, value)
        return true;
    },
    get(target, value) {
        return Reflect.get(target, value);
    }
})
​
// 处理已经加载错误的图片,但是因为懒加载实现方法不同,无法真正通用
function observeStatic() {
    document.querySelectorAll('img').forEach((node) => {
      const lazySrc = node.getAttribute('data-src') || node.getAttribute('lazy-src');
   
      if (!lazySrc) {
         // 加载完成 complete = true,naturalWidth=0 表示图片没高度,那就是加载失败
        if (node.complete && node.naturalWidth === 0 && errorImgProxy.indexOf(node.src)!==-1) {
            errorImgProxy.push(node.src);
        }
      }
    });
}
​
export function resource() {
    observeStatic();//处理没来得及监听的图片错误
    
  // 监听全局未捕获的错误
  window.addEventListener(
    "error",
    (event) => {
      if (event.target &&(event.target.src || event.target.href)) {
          //将产生错误的资源路径存下来
        if (errorImgProxy.indexOf(event.target.src || event.target.href) === -1 && ASSETS_TYPE.indexOf(event.target.tagName.toLowerCase()) !== -1) {
          errorImgProxy.push(event.target.src || event.target.href);
          console.log(errorImgProxy);
        }
        tracker.send({
          type: "resourceError",
          filename: event.target.src || event.target.href,
          tagName: event.target.tagName,
          triggerTimeStamp: event.timeStamp,
          selector: getSelector(event.target),
        });
      }
    },
    true
  );
​
    let obs=new PerformanceObserver((list,boserver)=>{
        //延迟执行回调
        setTimeout(()=>{
            for(let item of list.getEntries()){
                const {initiatorType, name} = item;
                //过滤已经产生错误的资源和非静态资源
                if(errorImgProxy.indexOf(name)===-1&&ASSETS_TYPE.indexOf(initiatorType) !== -1&&name.indexOf('/upload')===-1){
                    const {connectStart, domainLookupStart, domainLookupEnd, connectEnd, duration, encodedBodySize, requestStart, responseEnd, responseStart} = item;
                    tracker.send({
                    type: "resource",
                    name,//文件名
                    parseDNSTime:domainLookupStart-domainLookupEnd,//DNS解析时间
                    connectTime:connectStart-connectEnd,//TCP连接时间
                    duration,//总耗时
                    encodedBodySize,//文件大小
                    ttfbTime: responseStart - requestStart, // 首字节到达时间
                    responseTime: responseEnd - responseStart, // response响应耗时
                    });
                }
            }
        },100)
​
    })
    obs.observe({entryTypes:['resource']});
}

数据模型

{
    '...':'...',//公共字段
    "type": "resource",
    "name": "http://localhost:8080/error.js",//资源名称(地址)
    "duration": "314134",//请求耗时
    "encoded_body_size":"199",//资源大小
    "parse_DNS_time":"123",//域名解析耗时
    "connectTime": "0",//链接耗时
    "ttfbTime": "1",//首字节到达耗时
    "responseTime": "1",//响应耗时
}

总结

我们的自研前端监控系统的最主要的目的还是学习,所以并不需要过分的纠结实现的方法,能够更广泛的涉猎是最好的,不得不感谢PerformanceResourceTiming强大的能力带给我们的方便。到这里我们完成了对静态资源请求监控,下一篇我们就还是利用PerformanceResourceTiming来实现网页加载的各项指标上报吧,文章有不足的地方,欢迎指出、讨论。最后再打个广告,关注公众号程序猿青空,免费领取191本计算机领域黑皮书电子书,更有集赞活动免费挑选精品课程(各个领域的都有),不定期分享各种优秀文章、学习资源、学习课程,能在未来(因为现在还没啥东西)享受更多福利。