目前为止整理最全的前端监控体系搭建篇(长文预警)
参考:
https://cloud.tencent.com/developer/article/1983779
https://github.com/miracle90/monitor
https://wpk.ucweb.com/index
概览
- 为什么要做前端监控
- 前端监控目标
- 前端监控流程
- 编写采集脚本
- 日志系统监控
- 错误监控
- 接口异常
- 白屏监控
- 加载时间
- 性能指标
- 卡顿
- pv
- 扩展问题
- 性能监控指标
- 前端怎么做性能监控
- 线上错误监控怎么做
- 导致内存泄漏的方法,怎么监控内存泄漏
- Node 怎么做性能监控
源码地址 (https://github.com/miracle90/monitor)
1. 为什么要做前端监控
- 更快的发现问题和解决问题
- 做产品的决策依据
- 为业务扩展提供了更多可能性
- 提升前端工程师的技术深度和广度,打造简历亮点
2. 前端监控目标
2.1 稳定性 stability
- js错误:js执行错误、promise异常
- 资源错误:js、css资源加载异常
- 接口错误:ajax、fetch请求接口异常
- 白屏:页面空白
2.2 用户体验 experience
2.3 业务 business
- pv:页面浏览量和点击量
- uv:访问某个站点的不同ip的人数
- 用户在每一个页面的停留时间
3. 前端监控流程
- 前端埋点
- 数据上报
- 加工汇总
- 可视化展示
- 监控报警
3.1 常见的埋点方案
3.1.1 代码埋点
- 嵌入代码的形式
- 优点:精确(任意时刻,数据量全面)
- 缺点:代码工作量点
3.1.2 可视化埋点
- 通过可视化交互的手段,代替代码埋点
- 将业务代码和埋点代码分离,提供一个可视化交互的页面,输入为业务代码,通过这个系统,可以在业务代码中自定义的增加埋点事件等等,最后输出的代码耦合了业务代码和埋点代码
- 用系统来代替手工插入埋点代码
3.1.3 无痕埋点
- 前端的任意一个事件被绑定一个标识,所有的事件都被记录下来
- 通过定期上传记录文件,配合文件解析,解析出来我们想要的数据,并生成可视化报告供专业人员分析
- 无痕埋点的优点是采集全量数据,不会出现漏埋和误埋等现象
- 缺点是给数据传输和服务器增加压力,也无法灵活定制数据结构
4. 编写采集脚本
4.1 接入日志系统
- 各公司一般都有自己的日志系统,接收数据上报,例如:阿里云
4.2 监控错误
4.2.1 错误分类
- js错误(js执行错误,promise异常)
- 资源加载异常:监听error
4.2.2 数据结构分析
1. jsError
{
"title": "前端监控系统", // 页面标题
"url": "http://localhost:8080/", // 页面URL
"timestamp": "1590815288710", // 访问时间戳
"userAgent": "Chrome", // 用户浏览器类型
"kind": "stability", // 大类
"type": "error", // 小类
"errorType": "jsError", // 错误类型
"message": "Uncaught TypeError: Cannot set property 'error' of undefined", // 类型详情
"filename": "http://localhost:8080/", // 访问的文件名
"position": "0:0", // 行列信息
"stack": "btnClick (http://localhost:8080/:20:39)^HTMLInputElement.onclick (http://localhost:8080/:14:72)", // 堆栈信息
"selector": "HTML BODY #container .content INPUT" // 选择器
}
2. promiseError
{
...
"errorType": "promiseError",//错误类型
"message": "someVar is not defined",//类型详情
"stack": "http://localhost:8080/:24:29^new Promise (<anonymous>)^btnPromiseClick (http://localhost:8080/:23:13)^HTMLInputElement.onclick (http://localhost:8080/:15:86)",//堆栈信息
"selector": "HTML BODY #container .content INPUT"//选择器
}
3. resourceError
...
"errorType": "resourceError",//错误类型
"filename": "http://localhost:8080/error.js",//访问的文件名
"tagName": "SCRIPT",//标签名
"timeStamp": "76",//时间
4.2.3 实现
1. 资源加载错误 + js执行错误
//一般JS运行时错误使用window.onerror捕获处理
window.addEventListener(
"error",
function (event) {
let lastEvent = getLastEvent();
// 有 e.target.src(href) 的认定为资源加载错误
if (event.target && (event.target.src || event.target.href)) {
tracker.send({
//资源加载错误
kind: "stability", //稳定性指标
type: "error", //resource
errorType: "resourceError",
filename: event.target.src || event.target.href, //加载失败的资源
tagName: event.target.tagName, //标签名
timeStamp: formatTime(event.timeStamp), //时间
selector: getSelector(event.path || event.target), //选择器
});
} else {
tracker.send({
kind: "stability", //稳定性指标
type: "error", //error
errorType: "jsError", //jsError
message: event.message, //报错信息
filename: event.filename, //报错链接
position: (event.lineNo || 0) + ":" + (event.columnNo || 0), //行列号
stack: getLines(event.error.stack), //错误堆栈
selector: lastEvent
? getSelector(lastEvent.path || lastEvent.target)
: "", //CSS选择器
});
}
},
true
); // true代表在捕获阶段调用,false代表在冒泡阶段捕获,使用true或false都可以
2. promise异常
//当Promise 被 reject 且没有 reject 处理器的时候,会触发 unhandledrejection 事件
window.addEventListener(
"unhandledrejection",
function (event) {
let lastEvent = getLastEvent();
let message = "";
let line = 0;
let column = 0;
let file = "";
let stack = "";
if (typeof event.reason === "string") {
message = event.reason;
} else if (typeof event.reason === "object") {
message = event.reason.message;
}
let reason = event.reason;
if (typeof reason === "object") {
if (reason.stack) {
var matchResult = reason.stack.match(/at\s+(.+):(\d+):(\d+)/);
if (matchResult) {
file = matchResult[1];
line = matchResult[2];
column = matchResult[3];
}
stack = getLines(reason.stack);
}
}
tracker.send({
//未捕获的promise错误
kind: "stability", //稳定性指标
type: "error", //jsError
errorType: "promiseError", //unhandledrejection
message: message, //标签名
filename: file,
position: line + ":" + column, //行列
stack,
selector: lastEvent
? getSelector(lastEvent.path || lastEvent.target)
: "",
});
},
true
); // true代表在捕获阶段调用,false代表在冒泡阶段捕获,使用true或false都可以
4.3 接口异常采集脚本
4.3.1 数据设计
{
"title": "前端监控系统", //标题
"url": "http://localhost:8080/", //url
"timestamp": "1590817024490", //timestamp
"userAgent": "Chrome", //浏览器版本
"kind": "stability", //大类
"type": "xhr", //小类
"eventType": "load", //事件类型
"pathname": "/success", //路径
"status": "200-OK", //状态码
"duration": "7", //持续时间
"response": "{\"id\":1}", //响应内容
"params": "" //参数
}
{
"title": "前端监控系统",
"url": "http://localhost:8080/",
"timestamp": "1590817025617",
"userAgent": "Chrome",
"kind": "stability",
"type": "xhr",
"eventType": "load",
"pathname": "/error",
"status": "500-Internal Server Error",
"duration": "7",
"response": "",
"params": "name=zhufeng"
}
4.3.2 实现
使用webpack devServer模拟请求
- 重写xhr的open、send方法
- 监听load、error、abort事件
import tracker from "../util/tracker";
export function injectXHR() {
let XMLHttpRequest = window.XMLHttpRequest;
let oldOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (
method,
url,
async,
username,
password
) {
// 上报的接口不用处理
if (!url.match(/logstores/) && !url.match(/sockjs/)) {
this.logData = {
method,
url,
async,
username,
password,
};
}
return oldOpen.apply(this, arguments);
};
let oldSend = XMLHttpRequest.prototype.send;
let start;
XMLHttpRequest.prototype.send = function (body) {
if (this.logData) {
start = Date.now();
let handler = (type) => (event) => {
let duration = Date.now() - start;
let status = this.status;
let statusText = this.statusText;
tracker.send({
//未捕获的promise错误
kind: "stability", //稳定性指标
type: "xhr", //xhr
eventType: type, //load error abort
pathname: this.logData.url, //接口的url地址
status: status + "-" + statusText,
duration: "" + duration, //接口耗时
response: this.response ? JSON.stringify(this.response) : "",
params: body || "",
});
};
this.addEventListener("load", handler("load"), false);
this.addEventListener("error", handler("error"), false);
this.addEventListener("abort", handler("abort"),