浅析前端异常与性能监测

浅析前端异常与性能监测

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、城市等信息(这个一般是服务端获取,直接统计在数据库)
<script src="http://pv.sohu.com/cityjson?ie=utf-8"></script>

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'
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容