让我们一起写一个前端监控系统吧(2)
theme: fancy
“我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第3篇文章,点击查看活动详情”
- 相关链接
上一期中我们讲了前端系统中前端的基本架构,大家想必对我们的项目有了深入的了解,本篇文章中,我们将详细介绍被监控网站,有啥值得监控,以及由此 npm 包如何书写。
由上一章的介绍我们可以知道,被监控网站分为四个部分,分别是
淘宝首页
、实时聊天
、表单按钮
、在线博客
.
在开始讲解之前,我先把 npm 包的链接放上来
- ==> npm包
淘宝首页
- 此页面主要负责监控
- 组件加载时间
- 加载白屏时间、
- FCP(First Contentful Paint) : 首次绘制任何文本,图像,非空白canvas或SVG的时间点.
- LCP(Largest Contentful Paint) : 可视区域“内容”最大的可见元素开始出现在页面上的时间点。
那么话不多说,让我们顺序来看一下这些页面是如何被监控的!
npm 插件mixin,监控组件加载时间。
此组件并非无脑使用,我们设置了一个开关
this.$options.computeTime
,如果您想要使用,在组件中打开开关即可「即设置为true
」,实现了组件化的粒度细分
```js import http from '../utils/request'
let mixin = {
beforeCreate() {
// 我们在这里添加了一个属性
let shouldcompute = this.$options.computeTime
// 如果用户设置了这个属性,那么就可以启用mixin,获得加载时间。
if (!shouldcompute) return
// 获取创建之初的时间
this.createTime = new Date().getTime()
},
mounted() {
let shouldcompute = this.$options.computeTime
if (!shouldcompute) return
// 获取挂载完成的时间
this.endTime = new Date().getTime()
// 得到加载时间
let mountTime = this.endTime - this.createTime
// 获取所有的节点
let componentNameArr = this.$vnode.tag.split('-')
// 从而获取当前节点
let componentName = componentNameArr[componentNameArr.length - 1]
// 将得到的数据发送给后台
http.post('plugin/mount', {
kind: 'experience',
type: 'ComponentMountTime',
// 组件名称
componentName,
// 组件加载时间
mountTime,
// 发送当前时间
timeStamp: Date.now(),
})
},
} // 将mixin封装起来 export default {
install(Vue, options) {
const oldRevue = Vue.prototype.$revue
Vue.prototype.$revue = Object.assign({}, oldRevue, {
compnentMount: mixin
})
// Vue.mixin(mixin)
},
immediate: {
install(Vue, options) {
Vue.mixin(mixin)
},
},
m: mixin
}
我们特意设置了 immediate这个属性,你可以通过如下方法在`main.js`中进行调用。
js
Vue.use(revue.immediate)
```
npm插件 判断是否白屏
```js import onload from '../utils/onload' import http from '../utils/request'
let blankScreen = () => { let wrapperElements = ['html', 'body', '#app'] let emptyPoints = 0 // function getSelector(element) { if (element.id) { return '#' + element.id } else if (element.className) { // a b c => .a.b.c return ( '.' + element.className .split(' ') .filter((item) => !!item) .join('.') ) } else { return element.nodeName.toLowerCase() } } function isWrapper(element) { let selector = getSelector(element) if (wrapperElements.indexOf(selector) !== -1) { emptyPoints++ } } // 使用 elementsFromPoint 与 isWrapper 来判断是否白屏 onload(function () { for (let i = 1; i <= 9; i++) { let xElements = document.elementsFromPoint( (window.innerWidth * i) / 10, window.innerHeight / 2 ) let yElements = document.elementsFromPoint( window.innerWidth / 2, (window.innerHeight * i) / 10 ) isWrapper(xElements[0]) isWrapper(yElements[0]) }
if (emptyPoints >= 18) {
let centerElements = document.elementsFromPoint(
window.innerWidth / 2,
window.innerHeight / 2
)
http.post('/plugin/blank', {
kind: 'stability',
type: 'blank',
emptyPoints,
screen: window.screen.width + 'X' + window.screen.height,
viewPoint: window.innerWidth + 'X' + window.innerHeight,
timeStamp: Date.now(),
selector: getSelector(centerElements[0]),
})
}
}) }
export default { install(Vue, options) { const oldRevue = Vue.prototype.$revue Vue.prototype.$revue = Object.assign({}, oldRevue, { blankScreen }) }, immediate: { install(Vue, options) { blankScreen() const oldRevue = Vue.prototype.$revue Vue.prototype.$revue = Object.assign({}, oldRevue, { blankScreen }) }, }, b: blankScreen } ```
判断是否是白屏的思路是:我们对页面上的9个点分横纵进行判断是否有元素,如果18次判断全部都没有检索到元素,我们就需要向后端发送数据,让它知道这个页面发生白屏了!
npm插件,获取性能数据
```js import onload from '../utils/onload' import http from '../utils/request'
let timing = () => { let FMP, LCP // 加一个 if 是因为有时候这玩意是 undefined if (PerformanceObserver) { // 增加一个性能条目的观察者 new PerformanceObserver((entryList, observer) => { let perfEntries = entryList.getEntries() FMP = perfEntries[0] //startTime 2000以后 observer.disconnect() //不再观察了 }).observe({ entryTypes: ['element'] }) //观察页面中的意义的元素
new PerformanceObserver((entryList, observer) => {
let perfEntries = entryList.getEntries()
LCP = perfEntries[0]
observer.disconnect() //不再观察了
}).observe({ entryTypes: ['largest-contentful-paint'] }) //观察页面中的意义的元素
}
//用户的第一次交互 点击页面 onload(function () { setTimeout(() => { const { fetchStart, loadEventStart } = performance.timing // 此处直接使用 API 了 let FP = performance.getEntriesByName('first-paint')[0] let FCP = performance.getEntriesByName('first-contentful-paint')[0] let loadTime = loadEventStart - fetchStart //开始发送性能指标 //console.log('FP', FP) //console.log('FCP', FCP) //console.log('FMP', FMP) //console.log('LCP', LCP) http.post('/plugin/paint', { kind: 'experience', //用户体验指标 type: 'paint', //统计每个阶段的时间 firstPaint: FP.startTime, firstContentfulPaint: FCP.startTime, firstMeaningfulPaint: FMP?.startTime || -1, largestContentfulPaint: LCP?.startTime || -1, timeStamp: Date.now(), }) http.post('/plugin/load', { kind: 'experience', //用户体验指标 type: 'load', //统计每个阶段的时间 loadTime, timeStamp: Date.now(), }) }, 3000) }) }
export default { install(Vue, options) { const oldRevue = Vue.prototype.$revue Vue.prototype.$revue = Object.assign({}, oldRevue, { timing }) }, immediate: { install(Vue, options) { timing(Vue, options) const oldRevue = Vue.prototype.$revue Vue.prototype.$revue = Object.assign({}, oldRevue, { timing }) }, }, t: timing }
```
表单页面
表单按钮 => 『监控错误内容』
- 表单按钮这里的逻辑主要是对常见的JS错误进行汇总,然后收集起来,发送到后端。
这里有一张图,涵盖了JS的主要错误。
EvalError
错误
// html
<button @click="EvalError">
// js代码
/*
* 如果此处非法使用 eval(),则抛出 EvalError 异常
* 根据 ES9
* 此异常不再会被JavaScript抛出,但是EvalError对象仍然保持兼容性
**/
EvalError() {
return eval( '(' + obj + ')' )
}
复制代码
InternalError
错误
// html
<button @click="InternalError">
// js代码
/**
* 该错误在JS引擎内部发生,特别是当它有太多数据要处理并且堆栈增长超过其关键限制时。
*/
InternalError() {
function foo() {
foo()
}
foo()
}
复制代码
RangeError
错误
// html
<button @click="RangeError">
// js
// 当数字超出允许的值范围时,将抛出此错误
RangeError() {
const arr = [99, 88]
arr.length = 99 ** 99
}
复制代码
ReferenceError
错误
// html
<button @click="ReferenceError">
// js
// 当出现非法引用的时候报错
ReferenceError() {
foo.substring(1);
}
复制代码
URIError
错误
// html
<button @click="URIError">
// js
/**
* 用 encodeURI 等编码含有不合法字符的字符串,导致编解码失败
* 编码操作会将每一个字符实例替换为一到四个相对应的UTF-8编码形式的转义序列。
* 如果试图一个非高-低位完整的代理自负,将会抛出一个URIError错误
*/
URIError() {
let a = encodeURI('\uD800%')
console.log(a)
}
复制代码
-
TypeError
错误 -
此处访问到了
undefined
// html
<button @click="TypeError">
// js代码
TypeError() {
window.someVar.error = 'error'
}
复制代码
AsyncError
错误 |Promise
错误
// html
<button @click="Async">
// js代码
AsyncError() {
Promise.reject('this is an error message');
}
复制代码
npm 封装 「我们如何获取错误信息?」
概述:JSError
有同步错误也有异步错误,window.onerror
既可以捕获同步错误也可以捕获异步错误,在Vue中有一个API叫Vue.config.errorHandler
,会截取同步错误,window.onerror
自然就接收被筛选出来的异步错误了,但是这个时候我们发现Promise
错误并没有被window.onerror
捕获到,所以我们还需要unhandledrejection
来捕获这个错误,至此,所有的错误就捕获完毕了。
我们在写入我们自己的方法之前,不能直接覆盖,需要确认用户是否使用过我们使用的方法,如果没有使用过,那么我们就可以直接使用,如果使用过,那我们就调用一下
call
方法
ts
const oldErrorHandler = Vue.config.errorHandler
Vue.config.errorHandler = (error, vm, info) => {
if(oldErrorHandler) oldErrorHandler.call(this, error, vm, info)
}
我们需要使用一个包「
StackTracey
」把错误处理一下,处理成我们好处理的样子
ts
import StackTracey from 'stacktracey'
const stack = new StackTracey(error)
使用
Vue.config.errorHandler
来捕获同步错误
js
Vue.config.errorHandler = (error, vm, info) => {
if (oldErrorHandler) oldErrorHandler.call(this, err, vm, info)
const stack = new StackTracey(error)
const log = {
kind: "stability",
errorType: "jsError", //jsError
simpleUrl: window.location.href.split('?')[0].replace('#', ''), // 页面的url
timeStamp: new Date().getTime(), // 日志发生时间
position: `${stack.items[0].column}:${stack.items[0].line}`, // 需要处理掉无效的前缀信息
fileName: stack.items[0].fileName, //错误文件名
message: stack.items[0].callee, //错误信息
detail: `${error.toString()}`,
isYibu: 'false', //是否是异步
}
console.error(error)
axios.post('/plugin/postErrorMessage', log)
}
使用
window.onerror
来捕获异步错误
js
window.addEventListener("error", function (event) {
// console.log(event)
let log = {
kind: "stability", //稳定性指标
errorType: "jsError", //jsError
simpleUrl: window.location.href.split('?')[0].replace('#', ''), // 页面的url
timeStamp: new Date().getTime(), // 日志发生时间
position: (event.lineno || 0) + ":" + (event.colno || 0), //行列号
fileName: event.filename, //报错链接
message: event.message, //报错信息
detail: "null",
isYibu: "ture"
};
axios.post('/plugin/postErrorMessage', log)
},
true
); // true代表在捕获阶段调用,false代表在冒泡阶段捕获,使用true或false都可以
使用
unhandledrejection
来捕获Promise
错误
js
window.addEventListener("unhandledrejection", function (event) {
// console.log(event)
let log = {
kind: "stability", //稳定性指标
errorType: "jsError", //jsError
simpleUrl: window.location.href.split('?')[0].replace('#', ''), // 页面的url
timeStamp: new Date().getTime(), // 日志发生时间
message: event.reason, //报错信息
fileName: "null", //报错链接
position: (event.lineno || 0) + ":" + (event.colno || 0), //行列号
detail: "null",
isYibu: "ture"
};
axios.post('/plugin/postErrorMessage', log)
},
true
); // true代表在捕获阶段调用,false代表在冒泡阶段捕获,使用true或false都可以
博客页面「HTTP Error 页面」
主要功能: 上报页面请求报错信息
实现:
通过修改原型链的形式实现对原ajax请求、fetch请求的封装和增强,捕获并向后台发送错误数据。
以下是对监控Ajax错误的实现,我们对浏览器内置的XMLHttpRequest的open和send方法进行保存,并通过修改原型链的形式对以上两个方法进行重写和增强,正是通过这种方法我们既没有影响被监控页面原本请求业务的实现,又完成了我们捕获请求错误的目的。
``` let XMLHttpRequest = window.XMLHttpRequest; let oldOpen = XMLHttpRequest.prototype.open; //缓存老的open方法 XMLHttpRequest.prototype.open = function (method, url, async) { //重写open方法 if (!url.match(/plugin/)) { //防止死循环 this.logData = { method, url, async }; //增强功能,把初始化数据保存为对象的属性 } return oldOpen.apply(this, arguments); };
let oldSend = XMLHttpRequest.prototype.send; //缓存老的send方法 XMLHttpRequest.prototype.send = function (body) { //重写sned方法 if (this.logData) { //如果有值,说明已经被拦截了 // ...... let handler = (type) => (e) => { let data = { //...把我们想要的数据给记录下来 }; tracker.postHTTP(data); }; this.addEventListener("load", handler("load"), false); //传输完成,所有数据保存在 response 中 this.addEventListener("error", handler("error"), false); //500也算load,只有当请求发送不成功时才是error this.addEventListener("abort", handler("abort"), false); //放弃 } return oldSend.apply(this, arguments); }; ```
以下是捕获fetch请求错误的实现,其实是基于类似的思路
if (!window.fetch) return;
let oldFetch = window.fetch;
window.fetch = function (url, obj = { method: "GET", body: "" }) {
//......
return oldFetch
.apply(this, arguments)
.then((response) => {
if (!response?.ok) {
console.log("test", response);
// True if status is HTTP 2xx if (!url.match(/plugin/)) {
tracker.postHTTP({
//这里写入我们想向后台发送的错误数据
});
}
}
})
.catch((error) => {
// 上报错误 console.error("I am error", error);
tracker.postHTTP({
//这里写入我们想向后台发送的错误数据
});
// throw error; });
};
websocketError部分
监听 websocket 错误 通过保留原型链方法再扩展的形式实现对websocket监听,捕获到错误向后台发送错误数据。
const monitor = () => {
/* */
WebSocket.prototype.oldsend = WebSocket.prototype.send;
WebSocket.prototype.send = function (data) {
// 记得开始时间
WebSocket.prototype.startTime = new Date();
// 调用原方法
WebSocket.prototype.oldsend.call(this, data);
};
WebSocket.prototype.oldclose = WebSocket.prototype.close;
WebSocket.prototype.close = function (err) {
/* 错误逻辑发送数据 */
WebSocket.prototype.oldclose.call(this);
}
};
// WebSocket.prototype...
};
至此,我们详细介绍了 npm 包,并讲述了我们创建 npm 包的思路,希望这篇文章能让你有所收获!
往期文章: + 让我们一起写一个前端监控系统吧(1)