前端异常监控


前端异常是我们开发中经常出现的,由于一些条件限制,往往线上的前端异常比较难查找定位,所以如何快速、准确的查找到异常并上报,是快速解决前端问题的关键一步。

一、前端错误类型:

1、ECMAScript exceptions;

2、DOMException;

3、网络静态资源加载错误;

4、跨域引用script导致的script error;

5、页面崩溃。

ECMAScript exceptions

ECMAScript异常就是javascript在执行过程中所发生的错误。每一种错误都有对应的错误类型。当错误发生的时候,就会抛出相应类型的错误对象。主要有以下六种错误类型:

(1) SyntaxError:语法错误

// 1. Syntax Error: 语法错误
// 1.1 变量名不符合规范
var 1       // Uncaught SyntaxError: Unexpected number
var 1a       // Uncaught SyntaxError: Invalid or unexpected token
// 1.2 给关键字赋值
function = 5     // Uncaught SyntaxError: Unexpected token = 
// 1.3 关键词写错
lett a = 1

(2) Uncaught ReferenceError:引用错误

引用一个不存在的变量时发生的错误。将一个值分配给无法分配的对象,比如对函数的运行结果或者函数赋值。

// 2.1 引用了不存在的变量
a()       // Uncaught ReferenceError: a is not defined
console.log(b)     // Uncaught ReferenceError: b is not defined
// 2.2 给一个无法被赋值的对象赋值
console.log("abc") = 1   // Uncaught ReferenceError: Invalid left-hand side in assignment

(3)RangeError:范围错误

// 3.1 数组长度为负数
[].length = -5      // Uncaught RangeError: Invalid array length
// 3.2 Number对象的方法参数超出范围
12.89.toFixed(-1)   // Uncaught RangeError: toFixed() digits argument must be between 0 and 20 at Number.toFixed
// 说明: toFixed方法的作用是将数字四舍五入为指定小数位数的数字,参数是小数点后的位数,范围为0-20.

(4) TypeError类型错误

变量和参数不是预期类型时报的错,比如调用不存在的方法,使用new操作符后面跟基本类型

// 4.1 调用不存在的方法
123()        // Uncaught TypeError: 123 is not a function
var o = {}
o.run()        // Uncaught TypeError: o.run is not a function
// 4.2 new关键字后接基本类型
var p = new 456      // Uncaught TypeError: 456 is not a constructor

(5) URIError,URL错误

decodeURI("%")     // Uncaught URIError: URI malformed at decodeURI

URI相关参数不正确时抛出的错误,主要涉及encodeURI、decodeURI()、encodeURIComponent()、decodeURIComponent()、escape()和unescape()六个函数。

(6)EvalError eval()函数执行错误

eval("2+3")	// 返回 5
var myeval = eval;	// 可能会抛出 EvalError 异常
myeval("2+3");	// 可能会抛出 EvalError 异常

需要注意的是:ES5以上的JavaScript中已经不再抛出该错误,但依然可以通过new关键字来自定义该类型的错误提示。

DOMException

DOMException就是我们在调用web API方法或者属性的时候所发生的错误,或者简单地说是在执行DOM操作的时候所抛出的错误。

比如在错误地调用了DOM接口:


    


    


浏览器报错:Uncaught DOMException: Failed to execute 'createAttribute' on 'Document': The localName provided ('123') contains an invalid character.

网络静态资源加载错误

常见的网络静态资源包括有,html文件,css文件,javascript文件,图片,音频,视频,iframe等等。所有的网络静态资源都有可能遇到加载错误这个问题。加载错误的原因也不一而定,有可能是url写错了,有可能是服务器根本就没有这个资源,有可能是服务器出现内部错误,也有可能是网络忙,请求超时而导致资源加载失败。不管什么原因,凡是客户端没有加载成功的,一律视为出现了加载错误。

script error

当跨域引用了另外一个域下javascript资源,并且这个javascript执出错的情况下,浏览器就会抛出这个错误

假如,我们在your.com域名下有这么一个js文件:

const a = {};
console.log(a.b.c);

我们在mine.com 去引用这个文件


那么这种场景下,浏览器就会抛出一个script error类型的错误。script error类型的错误基本上是在告诉你,你跨域引用的脚本执行出错了,但是你没有知道具体错误信息的权利。

页面崩溃

当你访问一个不靠谱的web应用的时候,它可能会做出一个疯狂和不可预测的操作,这个时候,就会导致整个浏览器的崩溃..

image

二、错误处理

错误处理的几个方法:

  • try...catch
  • window.onerror
  • window.addEventListener('error',()=>{})
  • Promise Catch与window.addEventListener("unhandledrejection",()=> {})
  • iframe与iframe.onload
  • 其他

try...catch

try-catch 只能捕获到同步的运行时错误,对语法和异步错误却无能为力,捕获不到。

1、同步运行时错误:

try {
  let name = 'jartto';
  console.log(nam);
} catch(e) {
  console.log('捕获到异常:',e);
}

捕获到异常:ReferenceError: nam is not defined
    at :3:15

2、不能捕获到具体的语法错误,只有一个语法错误提示。我们修改一下代码,删掉一个单引号:

try {
  let name = 'jartto;
  console.log(nam);
} catch(e) {

  console.log('捕获到异常:',e);
}

Uncaught SyntaxError: Invalid or unexpected token
不过语法错误在我们开发阶段就可以看到,应该不会顺利上到线上环境。

3、异步错误

try {
  setTimeout(() => {
    undefined.map(v => v);
  }, 1000)
} catch(e) {
  console.log('捕获到异常:',e);
}

Uncaught TypeError: Cannot read property 'map' of undefined
    at setTimeout (:3:11)

window.onerror

当 JS 运行时错误发生时,window 会触发一个 ErrorEvent 接口的 error 事件,并执行 window.onerror()。

/**
* @param {String}  message    错误信息
* @param {String}  source    出错文件
* @param {Number}  lineno    行号
* @param {Number}  colno    列号
* @param {Object}  error  Error对象(对象)
*/

window.onerror = function(message, source, lineno, colno, error) {
   console.log('捕获到异常:',{message, source, lineno, colno, error});
}

1、同步代码运行错误

window.onerror = function(message, source, lineno, colno, error) {
// message:错误信息(字符串)。
// source:发生错误的脚本URL(字符串)
// lineno:发生错误的行号(数字)
// colno:发生错误的列号(数字)
// error:Error对象(对象)
console.log('捕获到异常:',{message, source, lineno, colno, error});
}
Jartto;

可以看到,我们捕获到了异常:
image

2、再试试语法错误

window.onerror = function(message, source, lineno, colno, error) {
console.log('捕获到异常:',{message, source, lineno, colno, error});
}
let name = 'Jartto

Uncaught SyntaxError: Invalid or unexpected token

没有捕获语法错误

3、异步代码错误

window.onerror = function(message, source, lineno, colno, error) {
    console.log('捕获到异常:',{message, source, lineno, colno, error});
}
setTimeout(() => {
    Jartto;
});

捕获到异常:{message: "Uncaught ReferenceError: Jartto is not defined", source: "http://127.0.0.1:8001/", lineno: 36, colno: 5, error: ReferenceError: Jartto is not defined
    at setTimeout (http://127.0.0.1:8001/:36:5)}

setTimeout、setInterval的错误是可以捕获的,但是其他的比如Promise、async里面的错误不能捕获:

window.onerror = function (message, source, lineno, colno, error) {
        console.log('window.onerror捕获的异常:', message)
        return true
      }
      new Promise((resolve, reject) => {
        console.log(a)
        resolve(1)
      })
      async function test() {
        console.log(a)
      }

image

4、接着,我们试试网络请求异常的情况:



我们发现,不论是静态资源异常,或者接口异常,错误都无法捕获到

window.onerror 函数只有在返回 true 的时候,异常才不会向上抛出,否则即使是知道异常的发生控制台还是会显示 Uncaught Error: xxxxx

需要注意:

  • onerror 最好写在所有 JS 脚本的前面,否则有可能捕获不到错误;
  • onerror 无法捕获语法错误;

在实际的使用过程中,onerror 主要是来捕获预料之外的错误,而 try-catch 则是用来在可预见情况下监控特定的错误,两者结合使用更加高效。

window.addEventListener

当一项资源(如图片或脚本)加载失败,加载资源的元素会触发一个 Event 接口的 error 事件,并执行该元素上的onerror() 处理函数。这些 error 事件不会向上冒泡到 window ,不过(至少在 Firefox 中)能被单一的window.addEventListener 捕获。

 window.addEventListener('error', (error) => {
    console.log('addEventListener捕获到异常:', error);
  }, true)
  

image

这种方法不能判断资源加载异常是404还是500等,需要配合后台日志查询

需要注意:

不同浏览器下返回的 error 对象可能不同,需要注意兼容处理。

需要注意避免 addEventListener 重复监听。

Promise Catch

在 promise 中使用 catch 可以非常方便的捕获到异步 error。
没有写 catch 的 Promise 中抛出的错误无法被 onerror 或 try-catch 捕获到,所以我们务必要在 Promise 中不要忘记写 catch 处理抛出的异常。

解决方案:为了防止有漏掉的 Promise 异常,建议在全局增加一个对 unhandledrejection 的监听,用来全局监听Uncaught Promise Error。

window.addEventListener("unhandledrejection", function(e){
  e.preventDefault() // 用于去除控制台的报错
  console.log('捕获到异常:', e);
  return true;
});
new Promise((resolve, reject) => {
    console.log(cxc)
})

image

iframe与iframe.onload
用window.onerror处理



Page Crash

 if(sessionStorage.getItem('good_exit') &&
      sessionStorage.getItem('good_exit') !== 'true') {
      /*
         insert crash logging code here
     */
      alert('Hey, welcome back from your crash, looks like you crashed on: ' + sessionStorage.getItem('time_before_crash'));
  }
  
window.addEventListener('load', function () {
      sessionStorage.setItem('good_exit', 'pending');
      setInterval(function () {
         sessionStorage.setItem('time_before_crash', new Date().toString());
      }, 1000);
   });

   window.addEventListener('beforeunload', function () {
      sessionStorage.setItem('good_exit', 'true');
   });

  

处理页面崩溃的原理是,崩溃的页面关闭时是无法触发beforeunload的。当第一次进去页面是崩溃的时候,good_exit是pending状态,刷新后,因为无法监听到beforeunload,再次进去的时候还是pending,这时候就可以断定崩溃了。

基于 Service Worker 的崩溃统计方案:

  1. Service Worker 有自己独立的工作线程,与网页区分开,网页崩溃了,Service Worker 一般情况下不会崩溃;
  2. Service Worker 生命周期一般要比网页还要长,可以用来监控网页的状态;
  3. 网页可以通过 navigator.serviceWorker.controller.postMessage API 向掌管自己的 SW 发送消息。

方案:

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

Script Error

一般情况,如果出现 Script error 这样的错误,基本上可以确定是出现了跨域问题。


特别注意,服务器端需要设置:Access-Control-Allow-Origin

Vue中错误的处理

errorHandler

指定组件的渲染和观察期间未捕获错误的处理函数。这个处理函数被调用时,可获取错误信息和 Vue 实例

// main.js
Vue.config.errorHandler = function (err, vm, info) {
  #处理错误信息, 进行错误上报
  #err错误对象
  #vm Vue实例
  #`info` 是 Vue 特定的错误信息,比如错误所在的生命周期钩子
  #只在 2.2.0+ 可用
}

从 2.2.0 起,这个钩子也会捕获组件生命周期钩子里的错误。同样的,当这个钩子是 undefined 时,被捕获的错误会通过 console.error 输出而避免应用崩溃。

从 2.4.0 起,这个钩子也会捕获 Vue 自定义事件处理函数内部的错误了。

从 2.6.0 起,这个钩子也会捕获 v-on DOM 监听器内部抛出的错误。

errorCaptured

errorCaptured
当捕获一个来自子孙组件的错误时被调用。此钩子会收到三个参数:错误对象、发生错误的组件实例以及一个包含错误来源信息的字符串。此钩子可以返回 false 以阻止该错误继续向上传播

举个例子:

Vue.component('cat', {
  template:`

Cat:

`, props:{ name:{ required:true, type:String } }, errorCaptured(err,vm,info) { console.log(`cat EC: ${err.toString()}\ninfo: ${info}`); return false; } }); Vue.component('kitten', { template:'

Kitten: {{ dontexist() }}

', props:{ name:{ required:true, type:String } } });

其中kitten是有bug的

运行结果:

cat EC: TypeError: dontexist is not a function
info: render

三、错误上报

无论是埋点上报还是错误上报,常用方案就是使用Image对象来发送请求。这么做,有以下几点好处:

1、浏览器兼容性好。所有浏览器都支持Image对象。

2、可以避免same-origin-policy的限制。实际开发中,通常都是一台服务器负责接收所有的服务器的错误上报。而这种情况下,就会存在跨域问题,单纯的XMLHttpRequest是解决不了这个问题。

3、在上报过程上,本身出现错误的概率比较低。如果你自己封装了一个ajax库或者使用了外部ajax库,如果这些库本身就有问题的话,你还指望它去上报的话,可想而知,结果是无法上报成功的

下面,我们简单实现一个用于上报错误的方法:

function postError(type, msg) {
    const img = new Image();
    img.src = `log.php?type=${encodeURICoponent(type)}&msg=${encodeURICoponent(msg)}`
}

设置采集率:

postError.send = function(data) {
  // 只采集 30%
  if(Math.random() < 0.3) {
    send(data)      // 上报错误信息
  }
}