跳到主要内容

异常监控详解

全局监控 JS 错误

通过window.onerror事件,可以得到具体的异常信息、异常文件的URL、异常的行号与列号及异常的堆栈信息,再捕获异常后,统一上报至我们的日志服务器。

window.onerror = function(errorMessage, scriptURI, lineNo, columnNo, error) {
console.log('errorMessage: ' + errorMessage); // 异常信息
console.log('scriptURI: ' + scriptURI); // 异常文件路径
console.log('lineNo: ' + lineNo); // 异常行号
console.log('columnNo: ' + columnNo); // 异常列号
console.log('error: ' + error); // 异常堆栈信息
// ...
// 异常上报
};
throw new Error('这是一个错误');

跨域脚本无法准确捕获异常,跨域之后window.onerror根本捕获不到正确的异常信息,而是统一返回一个Script error。 解决方案:对script标签增加一个crossorigin=”anonymous”,并且服务器添加Access-Control-Allow-Origin。

<script src="http://cdn.xxx.com/index.js" crossorigin="anonymous"></script>

Webpack项目可能看不到准确的错误信息,需要开启sourceMap。 开启source-map的缺陷是兼容性,目前只有Chrome浏览器和Firefox浏览器才对source-map支持。

var path = require('path');
module.exports = {
devtool: 'source-map',
mode: 'development',
entry: './client/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'client')
}
}

Vue捕获异常 - errorHandler

在Vue中,异常可能被Vue自身给try ... catch了,不会传到window.onerror事件触发

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

Vue.config.errorHandler = function (err, vm, info) {
// handle error
// `info` 是 Vue 特定的错误信息,比如错误所在的生命周期钩子
// 只在 2.2.0+ 可用
}

React中捕获异常 - componentDidCatch

在React中,可以使用ErrorBoundary组件包括业务组件的方式进行异常捕获,配合React 16.0+新出的componentDidCatch API,可以实现统一的异常捕获和日志上报。

class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}

componentDidCatch(error, info) {
// Display fallback UI
this.setState({ hasError: true });
// You can also log the error to an error reporting service
logErrorToMyService(error, info);
}

render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}

<ErrorBoundary>
<MyWidget />
</ErrorBoundary>

performance 性能监控

PV、UV

PV:PV即Page View,网站浏览量

指页面的浏览次数,用于衡量网站用户访问的网页数量。用户每次打开一个页面便记录1次PV,多次打开同一页面则浏览量累计。

度量方法就是从浏览器发出一个对网络服务器的请(Request),网络服务器接到这个请求后,会将该请求对应的一个网页(Page)发送给浏览器,从而产生了一个PV。

UV:UV即Unique Visitor。独立访客数

指一天内访问某站点的人数,以cookie为依据。1天内同一访客的多次访问只记录为一个访客。通过IP和cookie是判断UV值的两种方式。

当客户端第一次访问某个网站服务器的时候,网站服务器会给这个客户端的电脑发出一个Cookie,Cookie来辨别身份,同一个Cookie多次访问UV不变

日志上报

单独的日志域名

对于日志上报使用单独的日志域名的目的是避免对业务造成影响。

  • 其一,对于服务器来说,我们肯定不希望占用业务服务器的计算资源,也不希望过多的日志在业务服务器堆积,造成业务服务器的存储空间不够的情况。
  • 其二,我们知道在页面初始化的过程中,会对页面加载时间、PV、UV等数据进行上报,这些上报请求会和加载业务数据几乎是同时刻发出,而浏览器一般会对同一个域名的请求量有并发数的限制,如Chrome会有对并发数为6个的限制。

因此需要对日志系统单独设定域名,最小化对页面加载性能造成的影响。

解决跨域的问题

对于单独的日志域名,肯定会涉及到跨域的问题,采取的解决方案一般有以下两种:

  • 一种是构造空的Image对象的方式,其原因是请求图片并不涉及到跨域的问题;
function error(msg,url,line){
var REPORT_URL = "xxxx/cgi"; // 收集上报数据的信息
var m =[msg, url, line, navigator.userAgent, +new Date];// 收集错误信息,发生错误的脚本文件网络地址,用户代理信息,时间
var url = REPORT_URL + m.join('||');// 组装错误上报信息内容URL
var img = new Image;
img.onload = img.onerror = function(){
img = null;
};
img.src = url;// 发送数据到后台cgi
}
// 监听错误上报
window.onerror = function(msg,url,line){
error(msg,url,line);
}
  • 还有就是直接发ajax请求,一般使用跨域资源共享方案来解析跨域

可以合并上报 在页面关闭时一起全部上报

浏览器有两个事件可以用来监听页面关闭,beforeunload和unload。 beforeunload是在文档和资源将要关闭的时候调用的, 这时候文档还是可见的,并且在这个关闭的事件还是可以取消的。比如下面这种写法就会让用户导致在刷新或者关闭页面时候,有个弹窗提醒用户是否关闭。

window.addEventListener("beforeunload", function (event) {
// Cancel the event as stated by the standard.
event.preventDefault();
// Chrome requires returnValue to be set.
event.returnValue = '';
});

unload则是在页面已经正在被卸载时发生,此时文档所处的状态是:

  • 1.所有资源仍存在(图片,iframe等);
  • 2.对于用户所有资源不可见;
  • 3.界面交互无效(window.open, alert, confirm 等);
  • 4.错误不会停止卸载文档的过程。

截图上报

分散流量,让每个用户为我们贡献至少一次页面截图:

  • 每个用户都在随机的页面,随机的时间上传一个页面截图,以及一个点击区域截图,有且仅上传一次,一个用户的生命周期中只贡献一次页面截图
  • 每个用户发生某一类错误时,也只需上传一个截图即可,多个类型的错误,则上传多个截图。这样可以大量节省用户的上传次数。
  • 用户的截图数据很大, 时间长了需要很大的硬盘空间, 所以我的建议是,每个流程页面,只需要对应一个(点击区域截图,同理)。 每个用户的某一种类型的错误页面也只对应一个(方便定位错误原因)

html2Canvas 执行html页面截图, lz-string 执行对字符串长度的压缩

// 加载js文件的小工具
this.loadJs = function(url, callback) {
var script = document.createElement("script");
script.async = 1;
script.src = url;
script.onload = callback;
var dom = document.getElementsByTagName("script")[0];
dom.parentNode.insertBefore(script, dom);
return dom;
};

// html2Canvas 库文件加载完成后,通知全局变量,lz-string 同理
utils.loadJs("//html2canvas.hertzen.com/dist/html2canvas.min.js", function() {
html2CanvasLoaded = true;
});


// js处理截图
this.screenShot = function(cntElem, callback) {
var shareContent = cntElem; //需要截图的包裹的(原生的)DOM 对象
var width = shareContent.offsetWidth; //获取dom 宽度
var height = shareContent.offsetHeight; //获取dom 高度
var canvas = document.createElement("canvas"); //创建一个canvas节点
var scale = 0.6; //定义任意放大倍数 支持小数
canvas.style.display = "none";
canvas.width = width * scale; //定义canvas 宽度 * 缩放
canvas.height = height * scale; //定义canvas高度 *缩放
canvas.getContext("2d").scale(scale, scale); //获取context,设置scale
var opts = {
scale: scale, // 添加的scale 参数
canvas: canvas, //自定义 canvas
logging: false, //日志开关,便于查看html2canvas的内部执行流程
width: width, //dom 原始宽度
height: height,
useCORS: true // 【重要】开启跨域配置
};
html2canvas(cntElem, opts).then(function(canvas) {
var dataURL = canvas.toDataURL();
var tempCompress = dataURL.replace("data:image/png;base64,", "");
var compressedDataURL = Base64String.compress(tempCompress);
callback(compressedDataURL);
});
};

方案1: 发送同步的ajax请求

把这个请求写到beforeunload的回调里就行了

var oAjax = new XMLHttpRequest();

oAjax.open('POST', url + '/user/register', false);//false表示同步请求

oAjax.setRequestHeader("Content-type", "application/x-www-form-urlencoded");

oAjax.onreadystatechange = function() {
if (oAjax.readyState == 4 && oAjax.status == 200) {
var data = JSON.parse(oAjax.responseText);
} else {
console.log(oAjax);
}
};

oAjax.send('a=1&b=2');

省去响应主体 对于我们上报日志,其实对于客户端来说,并不需要考虑上报的结果,甚至对于上报失败,我们也不需要在前端做任何交互,所以上报来说,其实使用HEAD请求就够了,接口返回空的结果,最大地减少上报日志造成的资源浪费。

HEAD请求常常被忽略,但是能提供很多有用的信息,特别是在有限的速度和带宽下。主要有以下特点:

1、只请求资源的首部; 2、检查超链接的有效性; 3、检查网页是否被修改; 4、多用于自动搜索机器人获取网页的标志信息,获取rss种子信息,或者传递安全认证信息等 HEAD方法:它与GET方法几乎是一样的,对于HEAD请求的回应部分来说,它的HTTP头部中包含的信息与通过GET请求所得到的信息是相同的。利用这个方法,不必传输整个资源内容,就可以得到Request-URI所标识的资源的信息。该方法常用于测试超链接的有效性,是否可以访问,以及最近是否更新。

方案2: 使用navigator.sendBeacon发送异步请求

这个方法主要用于满足 统计和诊断代码 的需要,这些代码通常尝试在卸载(unload)文档之前向web服务器发送数据。过早的发送数据可能导致错过收集数据的机会。然而, 对于开发者来说保证在文档卸载期间发送数据一直是一个困难。因为用户代理通常会忽略在卸载事件处理器中产生的异步 XMLHttpRequest 。

data可以是ArrayBufferView, Blob, DOMString, 或者 FormData 类型的数据。

navigator.sendBeacon(url [, data]);

var params = new URLSearchParams({ room_id: 123 })
navigator.sendBeacon("/cgi-bin/leave_room", params);

blob = new Blob([`room_id=123`], {type : 'application/x-www-form-urlencoded'});
navigator.sendBeacon("/cgi-bin/leave_room", blob);


var fd = new FormData();
fd.append('room_id', 123);
navigator.sendBeacon("/cgi-bin/leave_room", fd);

第三方资源加载异常

这个方法会监控到很多的error, 所以我们要从中筛选出静态资源加载报错的error, 代码如下:


/**
* 监控页面静态资源加载报错
*/
function recordResourceError() {
// 当浏览器不支持 window.performance.getEntries 的时候,用下边这种方式
window.addEventListener('error',function(e){
var typeName = e.target.localName;
var sourceUrl = "";
if (typeName === "link") {
sourceUrl = e.target.href;
} else if (typeName === "script") {
sourceUrl = e.target.src;
}
var resourceLoadInfo = new ResourceLoadInfo(RESOURCE_LOAD, sourceUrl, typeName, "0");
resourceLoadInfo.handleLogInfo(RESOURCE_LOAD, resourceLoadInfo);
}, true);
}


利用 performance.getEntries()方法,获取到所有加载成功的资源列表,在onload事件中遍历出所有页面资源集合,利用排除法,到所有集合中过滤掉成功的资源列表,即为加载失败的资源。 此方法看似合理,也确实能够排查出加载失败的静态资源,但是检查的时机很难掌握,另外,如果遇到异步加载的js也就歇菜了。

资源加载错误只能在捕获阶段监听

window.onerror 函数只有在返回 true 的时候,异常才不会向上抛出,否则即使是知道异常的发生控制台还是会显示 Uncaught Error: xxxxx.不论是静态资源异常,或者接口异常,window.onerror 错误都无法捕获到。

资源加载错误使用addEventListener去监听error事件捕获。

实现原理:当一项资源(如<img>或<script>)加载失败,加载资源的元素会触发一个Event接口的error事件,并执行该元素上的onerror()处理函数。

这些error事件不会向上冒泡到window。由于网络请求异常不会事件冒泡,因此必须在捕获阶段将其捕捉到才行,但是这种方式虽然可以捕捉到网络请求的异常,但是无法判断 HTTP 的状态是 404 还是其他比如 500 等等,所以还需要配合服务端日志才进行排查分析才可以。

但这里需要注意,由于上面提到了addEventListener也能够捕获js错误,因此需要过滤避免重复上报,判断为资源错误的时候才进行上报。

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;
// 上报资源地址
let url = target.src || target.href;
console.log(url);
}, true);

未处理的promise错误处理方式

实现原理:当promise被reject并且错误信息没有被处理的时候,会抛出一个unhandledrejection。

这个错误不会被window.onerror以及

window.addEventListener('error')捕获,但是有专门的

window.addEventListener('unhandledrejection')方法进行捕获处理。

window.addEventListener('rejectionhandled', event => {
// 错误的详细信息在reason字段
// demo:settimeout error
console.log(event.reason);
});

fetch与xhr错误的捕获

对于fetch和xhr,我们需要通过改写它们的原生方法,在触发错误时进行自动化的捕获和上报。

改写fetch方法:

// fetch的处理
function _errorFetchInit () {
if(!window.fetch) return;
let _oldFetch = window.fetch;
window.fetch = function () {
return _oldFetch.apply(this, arguments)
.then(res => {
if (!res.ok) { // 当status不为2XX的时候,上报错误
}
return res;
})
// 当fetch方法错误时上报
.catch(error => {
// error.message,
// error.stack
// 抛出错误并且上报
throw error;
})
}
}

对于XMLHttpRequest的重写:

xhr改写

// xhr的处理
function _errorAjaxInit () {
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);
}
}

参考