在之前的文章我们已经理清 SDK 的开发流程:
那么本文就来看看怎么封装采集方法。
异常监控
要想监控异常,首先我们要有异常可以监控,因此这里搭建了一个 vue 项目,在页面中做了一些按钮,每个按钮代表一种异常,我们要达到的效果:每点击一个按钮,SDK 要捕获到对应的错误,在控制台打印出捕获到的信息。
sdk 中 example 文件夹下 vue 项目的一个页面:
<template>
<div class="left">
<h1>异常数据</h1>
<h2>前端异常</h2>
<button class="hello" @click="bugJs">JS 代码执行异常</button>
<button class="world" @click="bugPromise">Promise 异常</button>
<button class="hi" @click="bugAsset">静态资源加载异常</button>
<button class="foursheep" @click="bugConsole">console.error 异常</button>
<button class="good" @click="bugCors">跨域异常</button>
<img src="http://localhost:8888/nottrue.jpg" />
<br />
<h2>接口异常</h2>
<button @click="bugNoRespond">未响应/超时响应异常</button>
<button @click="bugInterface4">4xx 请求异常</button>
<button @click="bugInterface5">5xx 服务器异常</button>
<button @click="bugPowerless">权限不足</button>
<h1>白屏异常</h1>
<button @click="bugWhiteScreen">白屏异常</button>
</div>
<div class="right">
<h1>行为数据</h1>
<button>用户设备类型,浏览器版本,webview引擎类型</button>
<button>获取页面性能指标</button>
<button>点击事件</button>
<button>
<RouterLink to="/">路由跳转</RouterLink>
</button>
<button @click="getPv">PV、UV</button>
</div>
</template>
<script setup lang="ts">
import { axiosIntance } from "@/utils/axios";
import axios from "axios";
import { login } from "@/api/modules/user";
const bugJs = () => {
window.someVar.error = "error";
};
const bugPromise = () => {
new Promise(function (_, reject) {
window.someVar.error = "error";
});
};
const bugAsset = function () {
console.log("bugAsset");
};
const bugConsole = function () {
console.error(new Error("错误捕获555"));
};
const bugCors = function () {
login({
url: "/test",
method: "post",
data: '你好foursheep',
})
};
const bugNoRespond = function () {
// timeout
axiosIntance
.get("/api", {
timeout: 10,
})
.then((res) => {
console.log("请求成功", res);
})
.catch((e) => {
console.log("请求失败", e);
});
};
const bugInterface4 = function () {
// 404
axios
.get("/api")
.then((res) => {
console.log("请求成功", res);
})
.catch((e) => {
console.log("请求失败", e);
});
};
const getPv = () => {
console.log("getPv");
};
const bugInterface5 = function () {
fetch("/asdasdasdasd", {
method: "post",
body: "kongming",
});
};
const bugPowerless = function () {
console.log("powerless");
};
const bugWhiteScreen = function () {
console.log("页面 load 时已监控");
};
</script>
<style></style>
页面呈现效果是这样的:
到这里,我们已经简单制造好了各种异常,下面看看怎么捕获。
封装的 ts 类型:
// 错误类型
export enum mechanismType {
JS = 'jsError',
RS = 'resourceError',
UJ = 'unhandledrejectionError',
HP = 'httpError',
CS = 'corsError',
VUE = 'vueError',
}
封装的公共函数:
import { mechanismType } from "../type";
import { AxiosError } from "axios";
// 判断是 JS异常、静态资源异常、还是跨域异常
export function getErrorKey (event: ErrorEvent | Event) {
const target = event.target;
const isElementTarget: boolean = target instanceof HTMLScriptElement || target instanceof HTMLLinkElement || target instanceof HTMLImageElement;
if (isElementTarget) {
return mechanismType.RS;
}
if (event instanceof AxiosError) {
return mechanismType.CS;
}
return mechanismType.JS;
};
// 获取用户最后一个交互事件
export function getLastEvent() {
let lastEvent: Event;
[
'click',
'mousedown',
'mouseover',
'keydown',
'touchstart',
].forEach(eventType => {
window.addEventListener(eventType, (event) => {
lastEvent = event;
}, {
capture: true,
passive: true // 默认不阻止默认事件
});
})
return lastEvent;
}
// 获取选择器
export function getSelector(pathsOrTarget: any) {
const handleSelector = function (pathArr: any) {
return pathArr.reverse().filter((element: any) => {
// 去除 document 和 window
return element !== document && element !== window;
}).map((element: any) => {
const {id, className, tagName} = element;
if (id) {
return `${tagName.toLowerCase()}#${id}`;
} else if (className && typeof className === 'string') {
return `${tagName.toLowerCase()}.${className}`;
} else {
return tagName.toLowerCase();
}
}).join(' ');
}
if (Array.isArray(pathsOrTarget)) {
return handleSelector(pathsOrTarget);
} else {
let pathArr = [];
while (pathsOrTarget) {
pathArr.push(pathsOrTarget);
pathsOrTarget = pathsOrTarget.parentNode;
}
return handleSelector(pathArr);
}
}
JS 代码执行异常
捕获 JS 运行异常有两种方法:
window.onerror
。这是一个全局变量,默认值为null
。当有 js 运行触发错误时,window 会触发 error 事件,并执行window.onerror()
,借助这个特性,我们可以捕获全局的JS运行异常
,并且通过对window.onerror
进行重写以获取异常信息;window.onerror = (msg, url, row, col, error) => { // 1. 获取报错信息 // 2. 上报报错信息 return true; // 返回 true,阻止了默认事件执行,也就是原本将要在控制台打印的错误信息 };
window.addEventListener('error')
。这个方法也可以捕获到JS运行异常
,但它与window.onerror
的区别在于:
- 它除了可以监听
JS运行异常
之外,还可以同时捕获到静态资源加载异常
; - 它会比
window.onerror
先触发; onerror
可以接受多个参数。而addEventListener('error')
只有一个保存所有错误信息的参数。
综上所述,笔者决定采用 window.addEventListener('error')
捕获 js 异常:
window.addEventListener('error', (event) => {
handleJs(event);
}, true)
const handleJs = function (event: any): void {
event.preventDefault();
// 用户最后一个交互事件
const lastEvent: Event = getLastEvent();
let log = null;
// 判断报错种类
const type = getErrorKey(event);
if (type === mechanismType.JS) {
log = {
message: event.message,
type: event.type,
errorType: `${mechanismType.JS}: ${(event.error && event.error.name) || 'UnKnowun'}`,
fileName: event.filename,
position: `${event.lineno}:${event.colno}`,
// stack: getLines(event.error.stack), //错误堆栈
selector: lastEvent ? getSelector((lastEvent as any).path) : '',
}
console.log('jsError log数据', log)
}
}
Promise 异常
当抛出 Promise异常
时,会触发 unhandledrejection
事件,所以我们只需要去监听它就可以进行 Promise 异常
的捕获了:
window.addEventListener('unhandledrejection', (event) => {
handlePromise(event);
}, true)
const handlePromise = function (event: any): void {
// 用户最后一个交互事件
const lastEvent: Event = getLastEvent();
let message: string = '';
let filename: string = '';
let line: number = 0;
let column: number = 0;
let stack: string = '';
let reason = event.reason;
if (typeof reason === 'string') {
message = reason;
} else if (typeof reason === 'object') {
if (reason.stack) {
let matchResult = reason.stack.match(/at\s+(.+):(\d+):(\d+)/);
filename = matchResult[1];
line = matchResult[2];
column = matchResult[3];
}
message = reason.message;
}
const log = {
message, // 报错信息
type: event.type,
errorType: mechanismType.UJ,
fileName: filename,
position: `${line}:${column}`,
selector: lastEvent ? getSelector((lastEvent as any).path) : '',
}
console.log('promise log数据', log)
}
不过值得注意的一点是:相比与上面所述的直接获取报错的行号、列号等信息,Promise异常
我们只能捕获到一个 报错原因
而已,代码的具体位置我们需要通过 sourceMap 映射源码中的位置才能获得,这里暂未处理。
静态资源加载异常
静态资源加载异常也可以通过 window.addEventListener('error')
捕获,我们只需要在捕获 js 异常的 handleJs
函数体里加上:
if (type === mechanismType.RS) {
const target = event.target;
log = {
type: event.type,
url: target.src,
message: `GET ${target.src} net::ERR_CONNECTION_REFUSED`, // TODO
html: target.outerHTML,
errorType: mechanismType.RS,
tagName: target.tagName,
selector: getSelector(event.path),
}
console.log('resourceError log数据', log)
}
console.error 异常
这部分报错我们也是需要捕获的,但却不能向上文那样使用 window.onerror
或 window.addEventListener('error')
直接捕获得到。我们需要重新封装 window.console.error
以捕获异常信息:
let consoleError = window.console.error;
window.console.error = function (error) {
if (error != '参数有缺失') {
const message: string = error.message;
const stack = error.stack;
const url: string = window.location.href;
let row: number = 0, column: number = 0;
if (stack) {
let mres = stack.match(/(.*?)/g) || [];
let firstLine = (mres[0] || "").replace("(", "").replace(")", ""); // 获取到堆栈信息的第一条
// 根据:分隔获取行列
let info = firstLine.split(':')
row = info[info.length - 2] // 行
column = info[info.length - 1] // 列
}
setTimeout(function () {
// 上报的错误内容
let opt = {
url,
row,
column,
message,
stack
}
console.log('error捕获', opt);
}, 0);
}
return consoleError.apply(console, arguments as any);
};
跨域异常
window.addEventListener('error')
捕获不到跨域错误,反而是被window.addEventListener('unhandledrejection')
捕获,这个坑现在暂未解决,后续处理:
暂时使用window.addEventListener('unhandledrejection')
捕获跨域错误:
window.addEventListener('unhandledrejection', (event) => {
handlePromise(event);
}, true)
const handlePromise = function (event: any): void {
// 使用 isCors 判断捕获到的是跨域错误还是 promise 错误
const isCors = event.reason instanceof AxiosError;
if (!isCors) {
// promise 异常信息,上文已贴
} else {
const error = event.reason;
let { url, method, params, data } = error.config;
let corsErrorData = {
errorType: mechanismType.CS,
type: error.name,
message: error.message,
url,
method,
status: error.response.status,
response: error.response || "",
request: error.request || "",
params: { query: params, body: data },
}
console.log('CORSError log数据', corsErrorData)
}
}
异常捕获总结
- 使用
window.addEventListener('error')
捕获 JS 运行时错误和资源加载错误,但要区分两者以进行不同操作;区分 JS 运行错误或资源加载错误:
event.target && (event.target.src || event.target.href)
为 true;event.target instanceof HTMLScriptElement || event.target instanceof HTMLLinkElement || event.target instanceof HTMLImageElement
为 true
满足以上两者之一代表 资源加载错误 ,否则为 JS 运行错误。
- 使用
window.addEventListener('unhandledrejection')
捕获未处理的 promise reject 错误 - 重写
console.error
捕获 console.error 错误
行为监控
路由跳转
遇到的问题
触发一次history.pushState
页面就会跳转两次:
性能监控
更新中…