1、SDK
最后更新于:2022-04-02 06:10:41
目前市面上有许多成熟的前端监控系统,但我们没有选择成品,而是自己动手研发。这里面包括多个原因:
* 填补H5日志的空白
* 节约公司费用支出
* 可灵活地根据业务自定义监控
* 回溯时间能更长久
* 反哺运营和产品,从而优化产品质量
* 一次难得的练兵机会
前端监控地基本目的:了解当前项目实际使用的情况,有哪些异常,在追踪到后,对其进行分析,并提供合适的解决方案。
前端监控地终极目标: 1 分钟感知、5 分钟定位、10 分钟恢复。目前是初版,离该目标还比较遥远。
SDK(采用ES5语法)取名为 shin.js,其作用就是将数据通过 JavaScript 采集起来,统一发送到后台,采集的方式包括监听或劫持原始方法,获取需要上报的数据,并通过 gif 传递数据。
整个系统大致的运行流程如下:
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/19/6b/196bbdb90ddb8e4f33453369c606b128_627x381.jpg =400x)
## 一、异常捕获
异常包括运行时错误、Promise错误、框架错误等。
**1)error事件**
为 window 注册[error](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/error_event)事件,捕获全局错误,过滤掉与业务无关的错误,例如“Script error.”、JSBridge告警等,还需统一资源载入和运行时错误的数据格式。
~~~
// 定义的错误类型码
var ERROR_RUNTIME = "runtime";
var ERROR_SCRIPT = "script";
var ERROR_STYLE = "style";
var ERROR_IMAGE = "image";
var ERROR_AUDIO = "audio";
var ERROR_VIDEO = "video";
var ERROR_PROMISE = "promise";
var ERROR_VUE = "vue";
var ERROR_REACT = "react";
var LOAD_ERROR_TYPE = {
SCRIPT: ERROR_SCRIPT,
LINK: ERROR_STYLE,
IMG: ERROR_IMAGE,
AUDIO: ERROR_AUDIO,
VIDEO: ERROR_VIDEO
};
/**
* 监控脚本运行时的异常
*/
window.addEventListener(
"error",
function (event) {
var errorTarget = event.target;
// 过滤掉与业务无关的错误
if (event.message === "Script error." || !event.filename) {
return;
}
if (
errorTarget !== window &&
errorTarget.nodeName &&
LOAD_ERROR_TYPE[errorTarget.nodeName.toUpperCase()]
) {
handleError(formatLoadError(errorTarget));
} else {
handleError(
formatRuntimerError(
event.message,
event.filename,
event.lineno,
event.colno,
event.error
)
);
}
},
true //捕获
);
/**
* 生成 laod 错误日志
* 需要加载资源的元素
*/
function formatLoadError(errorTarget) {
return {
type: LOAD_ERROR_TYPE[errorTarget.nodeName.toUpperCase()],
desc: errorTarget.baseURI + "@" + (errorTarget.src || errorTarget.href),
stack: "no stack"
};
}
~~~
得用[performance.getEntriesByType("resource")](https://developer.mozilla.org/zh-CN/docs/Web/API/Performance/getEntriesByType)读取到资源列表(由[PerformanceResourceTiming](https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceResourceTiming)组成),然后循环列表。
当数据项的decodedBodySize属性为0时,就可判断无法读取这个资源;或者没有该属性,可认为当前资源缓存在浏览器中。
这种判断的条件不够全,也不够精确,后面就用比较简单粗暴的方式来做判断依据,那就是 duration 大于20秒,就认为请求超时了。
在日志中会将各个阶段的时间参数都保存,便于后期的校验。
~~~
/**
* 监控资源异常,即无法响应的资源
*/
window.addEventListener(
"load",
function () {
// 罗列资源列表,PerformanceResourceTiming类型
var resources = performance.getEntriesByType("resource");
// 映射initiatorType和错误类型
var hashError = {
script: ERROR_SCRIPT,
link: ERROR_STYLE,
// img: ERROR_IMAGE
};
resources && resources.forEach(function(value) {
var type = hashError[value.initiatorType];
/**
* 非监控资源、响应时间在20秒内、监控资源是ma.gif或shin.js,则结束当前循环
*/
if(!type || //非监控资源
value.duration < 20000 || //20秒内
value.name.indexOf("ma.gif") >= 0 ||
value.name.indexOf("shin.js") >= 0) {
return;
}
// 若是CSS文件,则过滤脚本文件
if(type === ERROR_STYLE &&
value.name.indexOf(".js") >= 0) {
return;
}
handleError({
type: type,
desc: handleNumber(value.toJSON()),
});
});
},
false
);
~~~
其实主要是为了监控脚本文本的响应,因为有时候会由于脚本没响应而导致页面空白,直接影响到业务,业务人员也不可能一直盯着页面的,为了避免这种情况,就需要实时监控资源的响应状态。
**2)unhandledrejection事件**
为 window 注册[unhandledrejection](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/unhandledrejection_event)事件,捕获未处理的 Promise 错误,当 Promise 被 reject 且没有 reject 处理器时触发。
~~~
window.addEventListener(
"unhandledrejection",
function (event) {
//处理响应数据,只抽取重要信息
var response = event.reason.response || response.status;
//若无响应,则不监控
if (!response) {
return;
}
var desc = response.request.ajax;
desc.status = event.reason.status;
handleError({
type: ERROR_PROMISE,
desc: desc
});
},
true
);
~~~
Promise 常用于异步通信,例如[axios](https://github.com/axios/axios)库,当响应异常通信时,就能借助该事件将其捕获,得到的结果如下。
~~~
{
"type": "promise",
"desc": {
"response": {
"data": "Error occured while trying to proxy to: localhost:8000/monitor/performance/statistic",
"status": 504,
"statusText": "Gateway Timeout",
"headers": {
"connection": "keep-alive",
"date": "Wed, 24 Mar 2021 07:53:25 GMT",
"transfer-encoding": "chunked",
"x-powered-by": "Express"
},
"config": {
"transformRequest": {},
"transformResponse": {},
"timeout": 0,
"xsrfCookieName": "XSRF-TOKEN",
"xsrfHeaderName": "X-XSRF-TOKEN",
"maxContentLength": -1,
"headers": {
"Accept": "application/json, text/plain, */*",
},
"method": "get",
"url": "/api/monitor/performance/statistic"
},
"request": {
"ajax": {
"type": "GET",
"url": "/api/monitor/performance/statistic",
"status": 504,
"endBytes": 0,
"interval": "13.15ms",
"network": {
"bandwidth": 0,
"type": "4G"
},
"response": "Error occured while trying to proxy to: localhost:8000/monitor/performance/statistic"
}
}
},
"status": 504
},
"stack": "Error: Gateway Timeout
at handleError (http://localhost:8000/umi.js:18813:15)"
}
~~~
这样就能分析出 500、502、504 等响应码所占通信的比例,当高于日常数量时,就得引起注意,查看是否在哪块逻辑出现了问题。
有一点需要注意,上面的结构中包含响应信息,这是需要对[Error](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Error)做些额外扩展的,如下所示。
~~~
import fetch from 'axios';
function handleError(errorObj) {
const { response } = errorObj;
if (!response) {
const error = new Error('你的网络有点问题');
error.response = errorObj;
error.status = 504;
throw error;
}
const error = new Error(response.statusText);
error.response = response;
error.status = response.status;
throw error;
}
export default function request(url, options) {
return fetch(url, options)
.catch(handleError)
.then((response) => {
return { data: response.data };
});
}
~~~
公司中有一套项目依赖的是 jQuery 库,因此要监控此处的异常通信,需要做点改造。
好在所有的通信都会请求一个通用函数,那么只要修改此函数的逻辑,就能覆盖到项目中的所有页面。
搜索了API资料,以及研读了 jQuery 中通信的源码后,得出需要声明一个 xhr() 函数,在函数中初始化 XMLHttpRequest 对象,从而才能监控它的实例。
并且在 error 方法中需要手动触发 unhandledrejection 事件。
~~~
$.ajax({
url,
method,
data,
success: (res) => {
success(res);
},
xhr: function () {
this.current = new XMLHttpRequest();
return this.current;
},
error: function (res) {
error(res);
Promise.reject({
status: res.status,
response: {
request: {
ajax: this.current.ajax
}
}
}).catch((error) => {
throw error;
});
}
});
~~~
**3)框架错误**
框架是指目前流行的React、Vue等,我只对公司目前使用的这两个框架做了监控。
React 需要在项目中创建一个[ErrorBoundary](https://react.docschina.org/docs/error-boundaries.html)类,捕获错误。
~~~
import React from 'react';
export default class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
componentDidCatch(error, info) {
this.setState({ hasError: true });
// 将component中的报错发送到后台
shin && shin.reactError(error, info);
}
render() {
if (this.state.hasError) {
return null
// 也可以在出错的component处展示出错信息
// return
';