浅析前端异常与性能监测

浅析前端异常与性能监测

1. 接口加载时间和是否成功监控

// fetch
function FetchReset () {
    if(!window.fetch) return;
    let _oldFetch = window.fetch;
   
    window.fetch = function () {
        return _oldFetch.apply(this, arguments)
        .then(res => {
            if (!res.ok) { 
                // 调用失败,上报错误
                return Promise.reject(res)
            }
            return res;
        }, (err) => {
            // 错误执行函数,上报错误
            return Promise.reject(res)
        })
        // 当fetch方法错误时上报
        .catch(error => {
            // error.message,
            // error.stack
            // 抛出错误并且上报
            throw new Error(JSON.stringify(error)); 
        })
    }
}

//XMLHttpRequest

function AjaxReset () {
    let protocol = window.location.protocol;
    if (protocol === 'file:') return;
    // 处理XMLHttpRequest
    if (!window.XMLHttpRequest) {
        return;  
    }
    let xmlhttp = window.XMLHttpRequest;    
    // 保存原生send方法
    let _oldSend = xmlhttp.prototype.send;
    let _handleEvent = function (event) {
        try {
            if (event && event.currentTarget && event.currentTarget.status !== 200) {
                    // event.currentTarget 即为构建的xhr实例
                    // event.currentTarget.response
                    // event.currentTarget.responseURL || event.currentTarget.ajaxUrl
                    // event.currentTarget.status
                    // event.currentTarget.statusText
                });
            }
        } catch (e) {va
            console.log('Tool\'s error: ' + e);
        }
    }
    xmlhttp.prototype.send = function () {
        this.addEventListener('error', _handleEvent); // 失败
        this.addEventListener('load', _handleEvent);  // 完成
        this.addEventListener('abort', _handleEvent); // 取消
        return _oldSend.apply(this, arguments);
    }
}

2. 页面性能(dns解析等)

Performance.png

白屏时间和首屏时间有争议,可以再进一步查阅书籍和文档确定,这里按我认为正确的版本讲述

先看看以下几个重要指标,或许对上面的几个统计,就会明朗许多:

  • fetchStart:浏览器准备好使用http请求抓取文档的时间(发生在检查本地缓存之前)。

  • domainLookupStart:DNS域名查询开始的时间,如果使用了本地缓存话,或 持久链接,该值则与fetchStart值相同。

  • domainLookupEnd:DNS域名查询完成的时间,如果使用了本地缓存话,或 持久链接,该值则与fetchStart值相同。

  • connectStart:HTTP 开始建立连接的时间,如果是持久链接的话,该值则和fetchStart值相同,如果在传输层发生了错误且需要重新建立连接的话,那么在这里显示的是新建立的链接开始时间。
  • secureConnectionStart:HTTPS 连接开始的时间,如果不是安全连接,则值为 0
  • connectEnd:HTTP完成建立连接的时间(完成握手)。如果是持久链接的话,该值则和fetchStart值相同,如果在传输层发生了错误且需要重新建立连接的话,那么在这里显示的是新建立的链接完成时间。
  • requestStart:http请求读取真实文档开始的时间,包括从本地读取缓存,链接错误重连时。
  • responseStart:开始接收到响应的时间(获取到第一个字节的那个时候)。包括从本地读取缓存。
  • responseEnd:HTTP响应全部接收完成时的时间(获取到最后一个字节)。包括从本地读取缓存。
  • domLoading:开始解析DOM树的时间。
  • domInteractive:完成解析DOM树的时间(只是DOM树解析完成,但是并没有开始加载网页的资源)。
  • domContentLoadedEventStart:DOM解析完成后,网页内资源加载开始的时间。
  • domContentLoadedEventEnd:DOM解析完成后,网页内资源加载完成的时间。(异步脚本加载完没有执行)
  • domComplete:DOM树解析完成,且资源也准备就绪的时间。Document.readyState 变为 complete,并将抛出 readystatechange 相关事件。 (异步脚本加载完成,也执行完成)
  • loadEventStart:load事件发送给文档。也即load回调函数开始执行的时间,如果没有绑定load事件,则该值为0.(所有文件都已经加载完成 css js img等)
  • loadEventEnd:load事件的回调函数执行完毕的时间,如果没有绑定load事件,该值为0.
  • 白屏时间 从我们打开网站到有内容渲染出来的时间点。domContentLoadedEventStart - fetchStart
  • 首屏时间 首屏内容渲染完毕的时间节点。 domComplete - fetchStart
  • DNS 解析 domainLookupEnd - domainLookupStart
  • TCP 连接 connectEnd - connectStart
  • 页面总下载时间 responseEnd - requestStart
  • DOM 解析 用户可操作(dom ready)时间节点。 domInteractive - domLoading
  • 资源加载时间 window.onload的触发节点。 loadEventStart - domContentLoadedEventStart

3. 页面报错监控

1. try{} catch(e) {}    无法捕获异步异常

2. window.onerror       无法捕获静态资源获取异常

3. window.addEventListener('error', event => {
  // 过滤js error
  let target = event.target || event.srcElement;
  let isElementTarget = target instanceof HTMLScriptElement || target instanceof HTMLLinkElement || target instanceof HTMLImageElement;
  if (!isElementTarget) return false;
  
  // 上报静态资源异常
  // 无法捕获接口异常,比如404, 但是图片和js css加载失败能监测
}) 

4. promise 捕获异常
window.addEventListener("unhandledrejection", function(e){
  // e.preventDefault(); // 阻止异常向上抛出
  console.log('捕获到异常:', e);
});
Promise.reject('promise error');


5. 网页崩溃异常(用户在线时长)

随着 PWA 概念的流行,大家对 Service Worker 也逐渐熟悉起来。基于以下原因,我们可以使用 Service Worker 来实现网页崩溃的监控:

Service Worker 有自己独立的工作线程,与网页区分开,网页崩溃了,Service Worker 一般情况下不会崩溃;
Service Worker 生命周期一般要比网页还要长,可以用来监控网页的状态;
网页可以通过 navigator.serviceWorker.controller.postMessage API 向掌管自己的 SW 发送消息。
基于以上几点,我们可以实现一种基于心跳检测的监控方案:




p1:网页加载后,通过 postMessage API 每 5s 给 sw 发送一个心跳,表示自己的在线,sw 将在线的网页登记下来,更新登记时间;
p2:网页在 beforeunload 时,通过 postMessage API 告知自己已经正常关闭,sw 将登记的网页清除;
p3:如果网页在运行的过程中 crash 了,sw 中的 running 状态将不会被清除,更新时间停留在奔溃前的最后一次心跳;
sw:Service Worker 每 10s 查看一遍登记中的网页,发现登记时间已经超出了一定时间(比如 15s)即可判定该网页 crash 了。
一些简化后的检测代码,给大家作为参考:

// 页面 JavaScript 代码
if (navigator.serviceWorker) {
    navigator.serviceWorker.register('sw.js', {scope: '/a/b/c/'})
    .then(function (reg) {
         sendStart()
        //console.log(reg.scope);
        // scope => https://yourhost/a/b/c/
    });
}

sendStart = () => {
    if (navigator.serviceWorker.controller !== null) {
      let HEARTBEAT_INTERVAL = 5 * 1000; // 每五秒发一次心跳
      let sessionId = uuid();
      let heartbeat = function () {
        navigator.serviceWorker.controller.postMessage({
          type: 'heartbeat',
          id: sessionId,
          data: {} // 附加信息,如果页面 crash,上报的附加数据
        });
      }
      window.addEventListener("beforeunload", function() {
        navigator.serviceWorker.controller.postMessage({
          type: 'unload',
          id: sessionId
        });
      });
      setInterval(heartbeat, HEARTBEAT_INTERVAL);
      heartbeat();  
    }
}


sessionId 本次页面会话的唯一 id;
postMessage 附带一些信息,用于上报 crash 需要的数据,比如当前页面的地址等等。

// sw.js
const CHECK_CRASH_INTERVAL = 10 * 1000; // 每 10s 检查一次
const CRASH_THRESHOLD = 15 * 1000; // 15s 超过15s没有心跳则认为已经 crash
const pages = {}
let timer
function checkCrash() {
  const now = Date.now()
  for (var id in pages) {
    let page = pages[id]
    if ((now - page.t) > CRASH_THRESHOLD) {
      // 上报 页面崩溃
      delete pages[id]
    }
  }
  if (Object.keys(pages).length == 0) {
    clearInterval(timer)
    timer = null
  }
}

worker.addEventListener('message', (e) => {
  const data = e.data;
  if (data.type === 'heartbeat') {
    pages[data.id] = {
      t: Date.now()
    }
    if (!timer) {
      timer = setInterval(function () {
        checkCrash()
      }, CHECK_CRASH_INTERVAL)
    }
  } else if (data.type === 'unload') {
    delete pages[data.id]
  }
})


6. 页面未崩溃时统计在线时长

使用 sendBeacon() 方法会使用户代理在有机会时异步地向服务器发送数据,同时不会延迟页面的卸载或影响下一导航的载入性能。这就解决了提交分析数据时的所有的问题:数据可靠,传输异步并且不会影响下一页面的加载。

window.addEventListener('unload', logData, false);

function logData() {
    navigator.sendBeacon("/log", analyticsData);
}

5. 框架报错

react 异常捕获
class ErrorCatch extends React.Component {
  constructor(props) {
    super(props);
  }
 
  componentDidCatch(error, info) {
    // 上报错误
  }
 
  render() {
     this.props.children;
  }
}

6. 设备信息获取(浏览器、设备、系统)

  • 获取ip、城市等信息(这个一般是服务端获取,直接统计在数据库)


console.log(
    "IP: " + returnCitySN['cip'] + 
    "地区代码: " + returnCitySN['cid'] + 
    "所在地: " + returnCitySN['cname']
)

  • 获取操作系统分布、浏览器分布、设备分布
// 系统平台
navigator.platform        MacIntel   win32
// 浏览器和系统信息
navigator.userAgent       "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) 
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36"

// 获取设备信息
let MobileDetect = require('mobile-detect')
let md = new MobileDetect(req.headers['user-agent']);

// 或者
var md = new MobileDetect(
    'Mozilla/5.0 (Linux; U; Android 4.0.3; en-in; SonyEricssonMT11i' +
    ' Build/4.1.A.0.562) AppleWebKit/534.30 (KHTML, like Gecko)' +
    ' Version/4.0 Mobile Safari/534.30');
 
console.log( md.mobile() );          // 'Sony'
console.log( md.userAgent() );       // 'Safari'
console.log( md.os() );              // 'AndroidOS'
console.log( md.is('iPhone') );      // false
console.log( md.versionStr('Build') );       // '4.1.A.0.562'

你可能感兴趣的:(浅析前端异常与性能监测)