前言
针对用户线上问题难复现问,大多公司都用上了错误监控,有付费的也有不付费的。sentry ,神策,阿里云,基本都是开箱即用;sentry 是我们公司当下的监控工具,但错误监控都是怎么实现的呢?自己想实现一个简易的怎么做?希望我的文章对正在阅读的你有帮助
准备
初始化
- npm init -y
- 入口文件:index.js ;模板文件:html
- npm i webpack webpack-cli webpack-dev-server html-webpack-plugin user-agent -D
user-agent用户代理,解析浏览器版本,解析成一个对象html-webpack-plugin生成产出 html 文件webpack-cli命令行工具webpack打包项目webpack-dev-server启动webpack服务
- 目录结构
脚本命令 script配置
- package.json
"scripts": {
"build": "webpack",
"dev": "webpack server"//不是webpack5 就用"webpack-dev-server"
},
- 遇到的问题 :打包出错 webpack-cli 的版本和 webpack-dev-server 版本的不兼容
- 解决 npm i webpack-cli@3.3.12 -D 降版本
测试
- yarn build
- yarn dev
上报地址配置
我这里参考的是阿里云 Webtracking
- host,project,logStore的配置方法
- 阿里云登陆搜索
日志服务(SLS) - 点击开通
- 找到这个位置,点击创建 (选离自己最近的地域)
- 阿里云登陆搜索
- 代码位置:src/utils/tracker.js
- 代码实现:
保留了我的log 过程,有兴趣可以跟着这些log看看 每个位置都是什么信息
const userAgent = require("user-agent")
let host = "cn-hangzhou.log.aliyuncs.com";
let project = "zoemonitor";
let logStore = "zoemonitor-store";
function getExtractData() {
return {
title: document.title,
url: location.href,
timestamp: Date.now(),
userAgent: userAgent.parse(navigator.userAgent).name, //将用户浏览器信息专成对象
};
}
class SendTracker {
constructor() {
this.url = `http://${project}.${host}/logstores/${logStore}/track`; //上报的路径
this.xhr = new XMLHttpRequest();
}
send(data = {}) {
let extraData = getExtractData();
let log = { ...extraData, ...data };
// body 里对象都值不能是数字
for (let key in log) {
if (typeof log[key] === "number") {
log[key] = `${log[key]}`;
}
}
console.log({ log });
this.xhr.open("POST", this.url, true);
let body = JSON.stringify({
__logs__: [log],
});
// console.log({ body }, this.xhr, this, new XMLHttpRequest());
this.xhr.setRequestHeader("Content-Type", "application/json"); //请求体类型
this.xhr.setRequestHeader("x-log-apiversion", " 0.6.0"); //版本号
this.xhr.setRequestHeader("x-log-bodyrawsize", body.length); //请求体大小
this.xhr.onload = function () {
// console.log(this.xhr.response);
};
this.xhr.onerror = function (error) {
console.log(error);
};
this.xhr.send(body);
}
}
export default new SendTracker();
实现
src/index.html
<div id="container">
<div class="content main">
<!-- XXX -->
<!-- --------普通错误和promise错误-------- -->
<input type="button" value="点击抛出错误" onclick="errorClick()" />
<input
type="button"
value="点击抛出Promise错误"
onclick="promiseErrorClick()"
/>
</div>
</div>
monitor
文件夹对应下图新建文件
实现思路
-
监听用户click事件,捕获最后一个交互事件和元素
-
监听全局error 事件
window.addEventListener( "error",function (event) {}) -
监听全局promise的rejetc事件
window.addEventListener("unhandledrejection", function (event) {})4.对应的错误回调事件里tracker.send({"上报的name":"上报的value"})
代码实现
- 捕获最后一个交互事件:src/utils/getLastEvent.js
let lastEvent;
["click", "touchstart", "mousedown", "keydown", "mouseover"].forEach(
(eventType) => {
document.addEventListener(
eventType,
(event) => {
lastEvent = event;
},
{
capture: true, //m默认是捕获阶段 ,false 是 冒泡阶段
passive: true, //默认不阻止默认事件
}
);
}
);
export default function () {
return lastEvent;
}
- 获取最后一个操作的元素:src/utils/getSelector.js
function getSelectors(path) {
return path
.reverse()
.filter((element) => {
return element !== document && element !== window;
})
.map((element) => {
let selector = "";
if (element.id) {
return `${element.nodeName.toLowerCase()}#${element.id}`;
} else if (element.className && typeof element.className === "string") {
return `${element.nodeName.toLowerCase()}.${element.className}`;
} else {
selector = element.nodeName.toLowerCase();
}
return selector;
})
.join(" ");
}
export default function (pathOrTarget) {
//这里其实要考虑资源加载错误的情况, 如果是一个资源加载的标签 css 和js 这种 就直接是一个对象,就不会数组,偷懒.....
return getSelectors(pathOrTarget);
}
- js错误监控(运行错误):src/monitor/lib/jsError.js
- 文件内容大致结构
import { getLastEvent, getSelector, tracker } from "../../utils";
export function injectJSError() {
//监听全局error 事件
window.addEventListener(
"error",
function (event) {
//普通js执行错误
},
true
);
window.addEventListener(
"unhandledrejection",
function (event) {
//promise错误
},
true
);
}
//1.原生报错太长,可读性差: "TypeError: Cannot set property 'error' of undefined\n at errorClick (http://localhost:8080/:22:29)\n at HTMLInputElement.onclick (http://localhost:8080/:12:70)"
// 2:切割数据 去除Cannot set property 'error' of undefined, 将at和at前面的空格去除
function getLines(stack) {
return stack
.split("\n")
.slice(1)
.map((item) => item.replace(/^\s+at\s+/g, ""))
.join("^");
}
- 普通error的回调
console.log({ event }); //可以直接看 原生的错误回调event打印
let lastEvent = getLastEvent(); //最后一个交互事件
let log = {
kind: "stability", //监指标的大类
type: "error", //小类型
errorType: "jsError", //js执行错误
url: "", //访问哪个路径,报错了
message: event.message, //报错信息
filename: event.filename, //报错文件位置
position: `${event.lineno}:${event.colno}`, //报错的具体行数和列数
stack: getLines(event.error.stack), //报错代码的前后调用关系 调用栈
selector: lastEvent ? getSelector(lastEvent.path) : "", //代表最后一个操作的元素,先获取操作方法
};
tracker.send(log);
- promise错误的回调
let lastEvent = getLastEvent(); //最后一个交互事件
let message;
let filename;
let line = 0;
let column = 0;
let stack = "";
let reason = event.reason;
if (typeof reason === "string") {
message = reason;
} else if (typeof reason === "object") {
//说明是一个错误对象
if (reason.stack) {
// at http://localhost:8080/:17:11
let matchResult = reason.stack.match(/at\s+(.+):(\d+):(\d+)/); //at 开头任意字符 : 任意数字:任意数字的就是域名
console.log({ matchResult });
filename = matchResult[1];
line = matchResult[2];
column = matchResult[3];
}
message = reason.reason;
stack = getLines(reason.stack);
}
tracker.send({
kind: "stability", //监指标的大类
type: "error", //小类型
errorType: "promiseError", //prmise执行错误
message, //报错信息
filename, //报错文件位置
position: `${line}:${column}`, //报错的具体行数和列数
stack, //报错代码的前后调用关系 调用栈
selector: lastEvent ? getSelector(lastEvent.path) : "", //代表最后一个操作的元素,先获取操作方法
});
使用
入口文件:src/index.js
import "./monitor";
测试结果
- 本地控制台
成功发送上报请求
- 阿里云
promiseErrorjsError
小结
- 监听 全局 unhandledrejection,error
- 根据回调事件的参数event 获取我们的报错位置,报错元素,报错事件等等
- 发送 我们format过的tracker 信息
- 实际业务场景下 不仅需要监控运行错误,还需要监控网络错误,网页性能等等
- 完整的监控
- nginx:上报的数据发给nginx,nginx负责记录日志
- mysql: 所有的监控,用户,报警,都放在mysql里的
- redis: 做缓存,可以实现高速读写,主要用来做报警
最后码字不易,如果你有收获,别忘了点赞鼓励