前端异常监控的重要性

软件异常监控常常直接关联到软件本身的质量,完备的异常监控体系常常能够快速定位到软件运行中发生的问题,并能帮助我们快速定位问题的源头,提升软件质量。

在服务器开发中,我们常常使用日志来记录请求的错误和服务器异常问题,但是在前端开发中,前端工程师按照需求完成页面开发,通过产品体验确认和测试,页面就可以上线了。但不幸的是,产品很快就收到了用户的投诉。用户反映页面点击按钮没反应而且能复现,我们试了一下却一切正常,于是追问用户所用的环境,最后结论是用户使用了一个非常小众的浏览器打开页面,因为该浏览器不支持某个特性,因此页面报错,整个页面停止响应。在这种情况下,用户反馈的投诉花掉了我们很多时间去定位问题,然而这并不是最可怕的,更让我们担忧的是更多的用户遇到这种场景后便会直接抛弃这个有问题的“垃圾产品”。这个问题唯一的解决办法就是在尽量少的用户遇到这样的场景时就把问题即时修复掉,保证尽量多的用户可以正常使用。

首先我们需要在少数用户使用产品出错时知道有用户出错,而且尽量定位到是什么错误。由于用户的运行环境是在浏览器端的,因此可以在前端页面脚本执行出错时将错误信息上传到服务器,然后打开服务器收集的错误信息进行分析来改进产品的质量,下面我们主要讨论下错误的捕获方案。

 

 

前端错误的分类

  • 即时运行错误(代码错误)
  • 资源加载错误,比如图片加载失败、JS加载失败、CSS加载失败等;

 

 

即时运行错误的捕获方式

window.onerror 算是一种特别暴力的容错手段,try..catch 也是如此,他们底层的实现就是利用 C/C++ 中的 goto 语句实现,一旦发现错误,不管目前的堆栈有多深,不管代码运行到了何处,直接跑到顶层或者 try..catch 捕获的那一层,这种一脚踢开错误的处理方式并不是很好。

try..catch 捕获,判断一个代码段中存在的错误

一般来说,使用try...catch可以捕捉前端JavaScript的运行时错误,同时拿到出错的信息,例如错误信息描述、堆栈、行号、列号、具体的出错文件信息等。我们也可以在这个阶段将用户浏览器信息等静态内容一起记录下来,快速地定位问题发生的原因。需要注意的是,try...catch无法捕捉到语法错误,只能在单一的作用域内有效捕获错误信息,如果是异步函数里面的内容,就需要把function函数块内容全部加入到try...catch中执行。

  • 无法捕捉到语法错误,只能捕捉运行时错误;
  • 可以拿到出错的信息,堆栈,出错的文件、行号、列号;
  • 需要借助工具把所有的function块以及文件块加入try,catch,可以在这个阶段打入更多的静态信息。
try{
    // 单一作用域try...catch可以捕获错误信息并进行处理
    console.log(obj);
}catch(e){
    console.log(e); //处理异常,ReferenceError: obj is not defined
}
try{
    // 不同作用域不能捕获到错误信息
    setTimeout(function() {
        console.log(obj); // 直接报错,不经过catch处理
    }, 200);
}catch(e){
    console.log(e);
}
// 同一个作用域下能捕获到错误信息
setTimeout(function() {
    try{
        // 当前作用域try...catch可以捕获错误信息并进行处理
        console.log(obj); 
    }catch(e){
        console.log(e); //处理异常,ReferenceError: obj is not defined
    }
}, 200);

但是在上面的这个例子中,try...catch无法获取异步函数setTimeout或其他作用域中的错误信息,这样就只能在每个函数里面添加try...catch了。

虽然使用window.onerror可以获取页面的出错信息、出错文件和行号,但是window. onerror有跨域限制,如果需要获取错误发生的具体描述、堆栈内容、行号、列号和具体的出错文件等详细日志,就必须使用try…catch,但是try…catch又不能在多个作用域中统一处理错误。

幸运的是,我们可以对前端脚本中常用的异步方法入口函数或模块引用的入口方法统一使用try…catch进行一层封装,这样就可以使用try…catch捕获每个引用模块作用域下的主要错误信息了。例如我们就可以对setTimeout函数用如下方式进行封装并捕获错误信息。另外,使用try-catch会带来一定的性能损耗,根据循环测试,平均大概会损失6%~10%的性能,但是为了提升应用的质量和稳定性,这些是可以接受的。

function wrapFunction(fn) {
    return function() {
        try {
            return fn.apply(this, arguments);
        } catch (e) {
            console.log(e);
            _errorProcess(e);
            return;
        }
    };
}

// 之后fn函数里面的代码运行出错时则是可以被捕获到的了
fn = wrapFunction(fn);

// 或者异步函数里面的回调函数中的错误也可以被捕获到
var _setTimeout = setTimeout;
setTimeout = function(fn, time){
    return _setTimeout(wrapFunction(fn), time);
}

// 模块定义函数也可以做重写定义
var _require = require;
require = function(id, deps, factory) {
    if (typeof(factory) !== 'function' || !factory) {
        return _require(id, deps);
    } else {
        return _require(id, deps, wrapFunction(factory));
    }
};

这是我们可以对常用的模块入口函数进行重定义,包括setTimeout、setInterval、define、require等,这样模块中的主要作用域中的异常都可以通过try-catch来捕获了。在之前的处理方法中,这种方法是非常有效的,直接可以拿到大多数错误栈中的异常和堆栈信息。

  我们可以对不同作用域的setTimeout参数函数的引入方式使用try…catch进行封装,让try…catch能捕获到setTimeout脚本中的错误并使用setTimeoutTry函数来代替。对于异步引入模块定义函数require或define也可以进行类似的封装,这样就可以获取到不同模块里面作用域的错误信息了。因此,这里捕获错误的方式可以根据具体的条件和场景灵活选择,在没有特别限制的情况下,使用window.onerror是比较高效、便捷的。

try {
  init();
  // code...
} catch(e){
  Reporter.send(format(e));
}

以 init 为程序的入口,代码中所有同步执行出现的错误都会被捕获,这种方式也可以很好的避免程序刚跑起来就挂。

window.onerror,捕获全局错误

当JavaScript运行时错误(包括语法错误)发生时,window会触发一个ErrorEvent接口的error事件,并执行window.onerror()

window.onerror的方法可以在任何执行上下文中执行,如果给window对象增加一个错误处理函数,便既能处理捕获错误又能保持代码的优雅性了。window.onerror一般用于捕捉脚本语法错误和运行时错误,可以获得出错的文件信息,如出错信息、出错文件、行号等,当前页面执行的所有JavaScript脚本出错都会被捕捉到。

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

  • 可以捕捉语法错误,也可以捕捉运行时错误;
  • 可以拿到出错的信息,堆栈,出错的文件、行号、列号;
  • 只要在当前页面执行的js脚本出错都会捕捉到,例如:浏览器插件的javascript、或者flash抛出的异常等。
  • 跨域的资源需要特殊头部支持。
window.onerror = function() {
    var errInfo = format(arguments);
    Reporter.send(errInfo);
    return true;
};

在上面的函数中返回 return true,错误便不会暴露到控制台中。下面是它的参数信息:

/**
 * @param {String}  errorMessage   错误信息
 * @param {String}  scriptURI      出错的文件
 * @param {Long}    lineNumber     出错代码的行号
 * @param {Long}    columnNumber   出错代码的列号
 * @param {Object}  errorObj       错误的详细信息,Anything
 */
window.onerror = function(errorMessage, scriptURI, lineNumber,columnNumber,errorObj) {
  console.log(errorMessage)//字符串错误信息
  console.log(scriptURI)//发生错误的脚本
  console.log(lineNumber)//发生错误的行号
  console.log(columnNumber)//发生错误的列号
  console.log(errorObj)//Erroe对象

  return true//将代码错误定格在捕获阶段
}

然而,使用onerror要注意,在不同浏览器中实现函数处理返回的异常对象是不相同的,而且如果报错的JavaScript和HTML不在同一个域名下,错误时window.onerror中的errorMsg全部为script error而不是具体的错误描述信息,此时需要添加JavaScript脚本的跨域设置。

需要给跨域资源的服务器的response header设置允许跨域:Access-Control-Allow-Origin:*

<script src="//www.domain.com/main.js" crossorigin></script>

如果服务器因为一些原因不能设置跨域或设置起来比较麻烦,那就只能在每个引用的文件里添加try...catch进行处理。

addEventListener全局监听

promise...catch主动捕获

在promise中使用catch可以非常方便的捕获到异步error,catch 其实是 then(undefined, () => {}) 的语法糖,如下:

let p = Promise.reject("error");

// catch
p.catch(err => {
  console.log("catch " + err); // catch error
});

// then
p.then(undefined, err => {
  console.log("catch " + err); // catch error
});

捕获错误

Promise对象内部其实自带了try catch,当同步代码发生运行时错误时,会自动将错误对象作为值reject,这样就会触发catch注册的回调,如下:

let p = new Promise((resolve, reject) => {
  throw "error";
});

p.catch(err => {
  console.log("catch " + err); // catch error
});

但需要注意的是:异步代码的运行时错误无法被自动 reject 进而被 catch 捕获,而是直接报错:

let p = new Promise((resolve, reject) => {
  setTimeout(() => {
    throw "error";
  }, 0);
});

p.catch(err => {
  console.log("catch " + err); // 不会被执行
});

关于异步错误是否会被 catch 捕获其实还有点坑,事实证明之所以 setTimeout 中产生的错误不会被 catch 捕获到,原因是 js 的事件队列中,Promise 的 catch 会在 setTimeout 回调之前执行;但是若异步操作抛出错误的回调是在 Promise 的 catch 之前执行的,其实还是可以被 catch 所捕获到的,比如 Promise 的 then 方法所抛出的错误:
 

let p = Promise.resolve();

let p1 = new Promise(function (resolve, reject) {
  p.then(()=>{
    throw 123;
  }).catch(e => {
    reject(e);
  });
});

p1.catch(err => {
  console.log(err); // 123,错误成功被捕获
});

一旦 Promise 对象已经 resolve,其后的运行时错误将被忽略

let p = new Promise((resolve, reject) => {
  resolve();
  throw "error";
});

p.catch(err => {
  console.log("catch " + err); // 不会被执行
});

return的值

与 then 方法相同,执行后会 return 一个新的 Promise 对象,以实现链式调用,你可以显式的 return 一个 Promise 对象,若不显式的 return 则会 return 一个以 undefined 值 resolve 的 Promise 对象;
 

let p = Promise.reject("error");
 
// catch
p.catch(err => {
  console.log("catch " + err); // catch error
  // 这里会默认return Promise.resolve(undefined);
}).then(res => {
  console.log(res); // undefined
});
 
let p2 = Promise.resolve("success");
 
// then
p2.catch(err => { // 由于p2正常resolve,所以此处不会执行
  console.log("catch " + err);
}).then(res => { // 由于前一个catch未执行,所以此处调用then的还是p2
  console.log(res); // success
  // 这里会默认return Promise.resolve(undefined);
}).then(res => {
  console.log(res); // undefined
});

重写XMLHttpRequest对象方法

该方法主要针对AJAX请求异常,附上参考代码:

function XMLHttpRequest() {
  var _self = this;

  // 重写 open
  XMLHttpRequest.prototype.open = function(){
    // 先在此处取得请求的url、method
    _self.reqUrl = arguments[1];
    _self.reqMethod = arguments[0];
    // 在调用原生 open 实现重写
    _self.xhrOpen.apply(this, arguments);
  }

  // 重写 send
  XMLHttpRequest.prototype.send = function () {
    // 记录xhr
    var xhrmsg = {
      'url': _self.reqUrl,
      'type': _self.reqMethod,
      // 此处可以取得 ajax 的请求参数
      'data': arguments[0] || {}
    }

    this.addEventListener('readystatechange', function () {
      if (this.readyState === 4) {
        // 此处可以取得一些响应信息
        // 响应信息
        xhrmsg['res'] = this.response;
        xhrmsg['status'] = this.status;
        this.status >= 200 && this.status < 400 ?
          xhrmsg['level'] = 'success' : xhrmsg['level'] = 'error';
        xhrArray.push(xhrmsg);
      }
    });

    _self.xhrSend.apply(this, arguments);
  }
}

 

 

资源加载错误的捕获方式

object.onerror

addEventListener全局监听

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

使用时需要注意的点:

  • 不同浏览器下返回的error对象可能不同,需要注意兼容处理。
  • 需要注意避免addEventListener重复监听。

写一个简单的例子,加载一个不存在的资源,然后使用Error事件捕获的方式来捕获错误

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>错误监控</title>
  <script>
    window.addEventListener('error', function (e) {
      console.log(e)
    }, true) // 最后一个参数一定是要true才能捕获到错误,如果为false就是冒泡,捕获不到错误
  </script>
</head>
<body>
  <script src="//baidu.com/test.js" charset="utf-8"></script>
</body>
</html>

window.addEventListener在运行时错误和资源加载错误时返回的错误对象不同,资源加载错误:

performance.getEntries(),可以获取到所有资源加载的时长,从而间接的捕获到资源的加载有没有错误

下面以一个网站为例子,直接再控制台输入这个方法

前端异常监控免费 前端错误监控平台_错误信息

以上的资源就是成功加载的,可以通过下面方式打印出所有的img集合,这个img集合减去上面成功加载的img集合,剩下的就是没有成功加载的img,这样就间接的获取到了没有成功加载的img资源

前端异常监控免费 前端错误监控平台_前端异常监控免费_02

 

 

跨域的js运行错误可以捕获吗,错误提示是什么,应该怎么处理

答案是可以捕获到的,错误的提示如下:

前端异常监控免费 前端错误监控平台_错误信息_03

处理方式如下:

  • 第一步:在script标签增加crossorigin属性(客户端)
  • 第二步:设置js资源响应头Access-Control-Allow-Origin:*(这个是在服务端设置的,可以设置为*表示所有域名下都可以,也可以设置为具体某个域名或者多个域名)

 

 

错误上报

需上报的错误类型有几种

  • 静态资源加载失败
  • AJAX请求失败
  • JavaScript异常
  • 运行时报错
  • 同步错误
  • 异步错误
  • 语法错误
  • promise异常

上报错误的的方式

上报错误的目的是因为本地定位不到错误,所以使用ajax或者image把错误传到后端,在后端看日志

采用ajax通信的方式上报(不推荐的做法)

利用Image对象上报(重要,推荐的方式)

只需要动态创建一个Image对象即可

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>错误监控</title>
  <script>
    (new Image()).src='http://baidu.com/tesjk?r=tesjk'; // 上报的地址,后面可以跟上参数
  </script>
</head>
<body>

</body>
</html>

或者写一个通用的方法来实现上报

function logError(sev, msg){
    var img = new Image();
    img.src = "log.php?sev=" + encodeURIComponent(sev) + "&msg=" + encodeURIComponent(msg)";
}

通过network可以看到请求已经发出了

前端异常监控免费 前端错误监控平台_作用域_04