背景
公司举行黑客马拉松其中一个赛题就是进行前端错误和性能监控,之前一直想过做这么一件事情,当线上发生代码错误或者资源引用错误时不是通过用户反馈得知,而是开发人员能在第一时间知晓,这次终于有机会进行尝试,与另外两名前端同事共同进行了一天一夜的代码长跑。
设计方案
- 监控哪些
前端监控一般分为错误监控和性能监控,而我们此次也是主要从这两方面进行监控。
- 与项目集成方式
集成方式考虑到有非侵入式集成(SDK)和侵入式集成,分析两者利弊后决定采用非侵入式集成,希望可以像JQuery一样,通过一行代码直接引入,在项目中直接使用,最后的理想状态就是:一行代码,开箱即用。
| 类型 | 优点 | 缺点 |
|---|---|---|
| 非侵入式 | 主动监测,指标齐全 | 无法监控复杂应用、监控的数据可能较少,无法捕获已经try-catch的数据 |
| 侵入式 | 可以监控复杂应用,进行细致监控 | 需要侵入源代码 |
- 指标的上报、存储与展示
监控sdk收到的信息通过指定接口(性能和错误接口分开)上报给后台,后台使用MongoDB进行存储和数据的处理,前台页面进行数据的直观显示。
监控数据收集方式
错误处理
- js错误监控方式
我们可以通过try/catch方式进行错误的捕获,但是考虑到无侵入式的原则,最后选择使用全局的error事件进行捕获错误。
var handleWindowError = function (_window, config) {
_oldWindowError = _window.onerror;
_window.onerror = function (msg, url, line, col, error) {
// console.log(error);
var eventId = `${url}${line}${col}`
config.sendError({
title: msg,
msg: {
resourceUrl: url,
rowNum: line,
colNum: col,
info: error,
filename: url,
eventId: eventId,
},
category: 'js',
level: 'error'
})
if (_oldWindowError && isFunction(_oldWindowError)) {
windowError && windowError.apply(window, arguments);
}
}
}
- 异步代码错误监控
通过监听全局'unhandledrejection'事件可以捕获未处理的 reject 。
var handleRejectPromise = function (_window, config) {
_window.addEventListener('unhandledrejection', function (event) {
if (event) {
var reason = event.reason;
config.sendError({
title: '不捕获Promise异步错误',
msg: reason,
category: 'js',
level: 'error'
});
}
}, true);
}
- 资源请求错误
当页面中如【img src='./404.png'】引入不存在的资源时,会出现未找到资源的错误,如果在引入未找到的js将会造成页面严重错误。 因此可以通过监控监听到的错误是否含有html标签,并且标签中是否有src或者href属性进行资源请求的监控。
var handleResourceError = function (_window, config) {
_window.addEventListener('error', function (event) {
if (event) {
var target = event.target || event.srcElement;
var isElementTarget = target instanceof HTMLScriptElement || target instanceof HTMLLinkElement || target instanceof HTMLImageElement;
if (!isElementTarget) return; // js error不再处理
var url = target.src || target.href;
config.sendError({
title: target.nodeName,
msg: {
url: url,
eventId: url,
},
category: 'resource',
level: 'error',
});
}
}, true);
}
- 请求错误
通过监控原生XMLHttpRequest属性进行ajax请求的监控。
var handleAjaxError = function (_window, config) {
var protocol = _window.location.protocol;
if (protocol === 'file:') return;
// 处理fetch
_handleFetchError(_window, config);
// 处理XMLHttpRequest
if (!_window.XMLHttpRequest) {
return;
}
var xmlhttp = _window.XMLHttpRequest;
var _oldSend = xmlhttp.prototype.send;
var _handleEvent = function (event) {
if (event && event.currentTarget && event.currentTarget.status !== 200) {
config.sendError({
title: event.target.responseURL,
msg: {
response: event.target.response,
responseURL: event.target.responseURL,
status: event.target.status,
statusText: event.target.statusText,
eventId: event.target.responseURL,
},
category: 'ajax',
level: 'error'
});
}
}
}
- 控制台错误监控
通过监控浏览器控制台输出的错误日志进行错误的捕获。
var handleConsoleError = function (_window, config) {
if (!_window.console || !_window.console.error) return;
var _oldConsoleError = _window.console.error;
_window.console.error = function () {
config.sendError({
title: 'consoleError',
msg: JSON.stringify(arguments),
category: 'js',
level: 'error'
});
_oldConsoleError && _oldConsoleError.apply(_window, arguments);
};
}
- vue通过指定钩子函数进行错误监控
vue中有指定的钩子函数errorHandler进行错误的监控,可以通过监控errorHandler收集的错误进行错误的监控。
var handleVueError = function (_window, config) {
var vue = _window.Vue;
if (!vue || !vue.config) return; // 没有找到vue实例
var _oldVueError = vue.config.errorHandler;
Vue.config.errorHandler = function VueErrorHandler(error, vm, info) {
var metaData = {};
if (Object.prototype.toString.call(vm) === '[object Object]') {
metaData.componentName = vm._isVue ? vm.$options.name || vm.$options._componentTag : vm.name;
metaData.propsData = vm.$options.propsData;
}
config.sendError({
title: 'vue Error',
msg: {
meta: metaData,
info,
},
category: 'js',
level: 'error'
});
if (_oldVueError && isFunction(_oldVueError)) {
_oldOnError.call(this, error, vm, info);
}
};
}
性能监控
-
性能监控
主要监测当前页面
- 页面完全加载时间
- HTTP请求响应完成时间
- 脚本加载时间
- DOM加载完成时间
- onload事件时间 ...
主要通过window.performance进行页面性能监控。
使用
直接在项目首页进行引入,会在创建项目时生成以下代码,一行代码,开箱即用。(示例代码仅供参考)
<script src="https://kylenxu.github.io/monitor-td/index.js?errorMonitor=true&performanceMonitor=true&projectId=82b12c829722d727e6ca40b8aa166e43&name=test&errorUrl=http://172.30.104.166:8038/api/errors&performanceError=http://172.30.104.166:8038/api/performance&vue=true&js=true"></script>
字段说明
对应上面URL地址中参数:
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| projectId | 项目ID | String | 空 |
| name | 项目名称 | String | 空 |
| errorUrl | 错误监控数据上报地址 | String | 空 |
| performanceError | 性能监控数据上报地址 | String | 空 |
| errorMonitor | 是否进行错误监控 | Boolean | true |
| performanceMonitor | 是否进行性能监控 | Boolean | true |
| vue | 是否进行VUE项目监控 | Boolean | true |
| js | 是否进行JS项目监控 | Boolean | true |
特点
-
js语法报错
主要通过window.onerror()进行页面错误监控。
-
异步代码运行报错
主要通过window.addEventListener('unhandledrejection',fn());进行页面异步代码错误监控。
-
资源加载报错
主要通过window.addEventListener('error',fn());进行页面异步代码错误监控。
-
接口请求报错
主要通过window.fetch();进行接口请求错误监控。
-
ajax请求报错
主要通过对window.XMLHttpRequest监控,进行ajax接口请求错误监控。
-
控制台错误信息
主要通过window.console.error();进行控制台错误监控。
注:html5: console.error会打印日志,显示红色的错误信息,但不会阻挡对下面js的执行。
windows: onsole.error会阻挡程序执行,js异常就是语法或逻辑错误,比如 this._btn1 = abc; //abc是不存在这样会阻挡后面语句的执行,不单阻挡了本函数,函数的外边的后面代码也不执行了,也就是本次调用堆栈就报废了。
-
VUE错误监控
主要通过Vue.config.errorHandler();钩子函数进行VUE错误监控。
-
性能监控
主要检测当前页面完全加载时间、HTTP请求响应完成时间、脚本加载时间等信息。
主要通过window.performance进行页面性能监控。
总结
最终实现了一个最小可用的前端监控系统,通过一行代码直接引入,在项目中直接使用,实现了最终的理想状态:一行代码,开箱即用。
致敬
- 一起熬夜的兄弟们