现行有一些已经开源的前端异常监控库,如腾讯的badJs,全栈js监控fundebug,国外的sentry等。
错误分类
-
javascript异常
- 语法错误
- 运行时错误
- script文件内错误(跨域和未跨域)
JS文件、CSS文件、img图片等(资源)的404错误(其实是有onerror事件的dom)
promise的异常捕获
ajax请求错误
错误上报
1、采用Ajax通信的方式上报(不常用)
2、利用Image对象上报(常用)
错误捕获类型
- 主动捕获(try catch / promise catch)
- 全局捕获(onerror / addEventListener)
错误捕获原理
error事件的事件处理程序。针对各种目标的不同类型的错误触发了 Error 事件:
当JavaScript运行时错误(包括语法错误)发生时,window会触发一个ErrorEvent接口的error事件,并执行window.onerror()。
当一项资源(如<img>或<script>)加载失败,加载资源的元素会触发一个Event接口的error事件,并执行该元素上的onerror()处理函数。这些error事件不会向上冒泡到window,不过(至少在Firefox中)能被单一的window.addEventListener捕获。
加载一个全局的error事件处理函数可用于自动收集错误报告。
由于历史原因,window.onerror和element.onerror接受不同的参数。
window.onerror = function(message, source, lineno, colno, error) { ... }
通过为页面上的 script 标签添加 crossOrigin 属性完成跨域上报,别忘了服务器也设置 Access-Control-Allow-Origin 的响应头。(解决跨域的js脚本错误上报)
window.addEventListener监听error事件
由于网络请求异常不会事件冒泡,因此必须在捕获阶段将其捕捉到才行,但是这种方式虽然可以捕捉到网络请求的异常,但是无法判断 HTTP 的状态是 404 还是其他比如 500 等等,所以还需要配合服务端日志才进行排查分析才可以。
window.addEventListener('error', function(event) { ... }) //兼容ie9
element.onerror = function(event) { ... } //dom0,兼容低版ie
注意事项:
当加载自不同域的脚本中发生语法错误时,为避免信息泄露(参见bug 363897),语法错误的细节将不会报告,而代之简单的"Script error."。在某些浏览器中,通过在<script>使用crossorigin属性并要求服务器发送适当的 CORS HTTP 响应头,该行为可被覆盖。一个变通方案是单独处理"Script error.",告知错误详情仅能通过浏览器控制台查看,无法通过JavaScript访问。
别忘了服务器也设置 Access-Control-Allow-Origin 的响应头。(解决跨域的js脚本错误上报)
try,catch 和window.onerror 比较
try,catch的方案有如下特点:
无法捕捉到语法错误,只能捕捉运行时错误;
可以拿到出错的信息,堆栈,name,message,stack,有些浏览器不能获取出错的文件、行号、列号;
需要借助工具把所有的function块以及文件块加入try,catch,可以在这个阶段打入更多的静态信息。
不能捕获异步错误(promise,setTimeout),但可以捕获async await,Generator 可以直接使用co 函数库来使用try...catch
window.onerror的方案有如下特点:
可以捕捉语法错误,也可以捕捉运行时错误;
可以拿到出错的信息,堆栈,出错的文件、行号、列号;
只要在当前页面执行的js脚本出错都会捕捉到,例如:浏览器插件的javascript、或者flash抛出的异常等。
跨域的资源需要特殊头部支持。
promise.catch主动捕获
unhandledrejection监听全局没有catch的promise执行.但是这个的兼容性不是很好
在一个JavaScript Promise 被 reject(拒绝) 但是没有 reject 处理函数来处理时触发。
window.onunhandledrejection = function(e) {
console.log(e.reason);
}
目前chrome49支持,在 Firefox 里,有实现这个接口但是默认是禁用的。要打开它的话,去到about:config 将 dom.promise_rejection_events.enabled 启用为真。
附:try catch Error对象属性:
1.Firefox中的Error对象拥有如下属性:
message —— 错误提示信息
fileName —— 表示出错代码所在文件
lineNumber —— 出错代码所在行数
stack —— 出错堆栈信息
name —— 异常对象名/类型
2.在IE下,Error对象只有如下属性:
name —— 异常对象名/类型,和Firefox中显示的名称可能不同
message —— 错误提示信息
description —— 和message属性相同
number —— ErrorCode,错误代码,对于普通开发人员来说基本没意义
3.在Safari中的Error对象拥有如下属性:
message —— 错误提示信息
line —— 出错代码所在行数
sourceId —— 一个数字,不明白什么意思
sourceURL —— 表示出错代码所在文件
name —— 异常对象名/类型
4.Opera下的Error对象拥有如下属性:
message —— 错误提示信息
opera#sourceloc —— 出错代码所在行数
stacktrace —— 出错堆栈信息
参考:https://www.jianshu.com/p/85d9a2778d80
https://developer.mozilla.org/zh-CN/docs/Web/API/GlobalEventHandlers/onerror
代码示例
(function(global, factory) {
'use strict';
if (typeof module === 'object' && typeof module.exports === 'object') {
module.exports = global.document ? factory(global, true) : function(w) {
if (!w.document) {
throw new Error('track.js requires a window with a document');
}
return factory(w);
};
} else {
factory(global);
}
})(typeof window !== 'undefined' ? window : this, function(window, noGlobal) {
window.onerror = function(message, url, line, column, error) {
var stackInfo;
//不一定所有浏览器都支持col参数
column = column || (window.event && window.event.errorCharacter);
if (error && error.stack) {
stackInfo = error.stack.toString();
} else {
var stack = [];
var i = 3;
var f = arguments.callee.caller;
while (f && i > 0) {
var funcName = f.name || f.toString().match(/^function\s*([^\s(]+)/)[1]; //IE不支持function.name
stack.push('function ' + funcName + ' () ');
if (f === f.caller) {
break;//如果有环
}
f = f.caller;
i--;
}
stackInfo = stack.join('\n at ');
}
var data = {
msg: message,
url: url,
page: location.href,
line: line,
col: column,
stack: stackInfo
};
track({
'type': 'error',
'info': data
});
};
window.addEventListener('unhandledrejection', function(error) {
track({
'type': 'error',
'info': error.reason
});
});
var commonParams = function() {
var o = {},
screen;
if (screen = window && window.screen) {
var sc = screen.width,
sh = screen.height;
o.screen = sc + ' x ' + sh;
o.colorDepth = screen.colorDepth && (screen.colorDepth + '-bit');
}
if (navigator) {
o.language = (navigator.language || navigator.browserLanguage).toLowerCase();
o.javaEnabled = navigator.javaEnabled();
}
return o;
};
var track = function(extendParams) {
var o = {};
for(var temp in extendParams){
o[temp]=extendParams[temp];
}
o.commonParams = track.commonParams || (track.commonParams=commonParams(track));
var arr = [];
for (var key in o) {
if (o[key] !== undefined) {
if (o[key] instanceof Object) {
for (var okey in o[key]) {
arr.push(encodeURIComponent(key + '[' + okey + ']') + '=' + encodeURIComponent(o[key][okey]));
}
} else {
arr.push(key + '=' + encodeURIComponent(o[key]));
}
}
}
console.log(arr,11)
var img = new Image();
img.src = 'http://172.31.11.245:3000/_h5track?' + arr.join('&') + '×tamp=' + new Date().getTime();
// img.onload = img.onerror = function() {
// img=null;
// };
};
if (!noGlobal) {
window.track = window.trackLog = track;
}
});
// 监控资源加载错误(img,script,css,以及jsonp)
window.addEventListener('error',function(e){
defaults.t =new Date().getTime();
defaults.msg =e.target.localName+' is load error';
defaults.data = JSON.stringify({
target: e.target.localName,
type: e.type,
resourceUrl:e.target.currentSrc,
pageUrl:location.href,
category:'resource'
});
if(e.target!=window){//抛去js语法错误
// 合并上报的数据,包括默认上报的数据和自定义上报的数据
var reportData=Object.assign({},params.data || {},defaults);
// 把错误信息发送给后台
alert(JSON.stringify(reportData))
}
},true);
window.addEventListener('unhandledrejection', function(err) {
console.log(err);
});
angularjs报错实现:
var app = angular.module('myApp', []);
app.config(function($provide) {
$provide.decorator('$exceptionHandler', [
'$httpParamSerializerJQLike', '$delegate', function($httpParamSerializerJQLike, $delegate) {
return function(exception, cause) {
$delegate(exception, cause);
var data = {
msg: exception.toString(),
cause: cause,
url:exception.fileName||exception.sourceURL,
page: location.href,
line: exception.lineNumber||exception.line ||exception['opera#sourceloc'],
stack: exception.stack||exception.stacktrace
};
track(data);
};
}]);
});
app.controller('myCtrl', function($scope) {
$scope.firstName = 'John';
$scope.lastName = 'Doe';
$scope.c=undefined.a;
// t;
});