错误监听实现原理
监听js错误和资源加载错误
ts
handleError(event: ErrorTarget) {
console.log('Error occurred:', event);
const target = event.target;
// 区分导致错误的目标是否为 DOM 元素 (资源加载错误会有 localName,如 img/script)
// 如果没有 target 或者 target 没有 localName,则认为是 JS 执行错误
if (!target || (event.target && !event.target.localName)) {
// vue和react捕获的报错使用event解析,异步错误使用event.error解析
// ErrorStackParser 用于解析错误堆栈,获取文件名、行列号
const stackFrame = ErrorStackParser.parse(!target ? event : event.error)[0];
const { fileName, columnNumber, lineNumber } = stackFrame;
// 组装错误上报数据
const errorData = {
type: EVENTTYPES.ERROR,
status: STATUS_CODE.ERROR,
time: getTimestamp(),
message: event.message,
fileName,
line: lineNumber,
column: columnNumber
};
// 将错误记录添加到行为栈(面包屑)中
breadcrumb.push({
type: EVENTTYPES.ERROR,
category: breadcrumb.getCategory(EVENTTYPES.ERROR),
data: errorData,
time: getTimestamp(),
status: STATUS_CODE.ERROR
});
// 生成错误的唯一 hash,用于去重判断
const hash: string = getErrorUid(
`${EVENTTYPES.ERROR}-${event.message}-${fileName}-${columnNumber}`
);
// 开启repeatCodeError第一次报错才上报
// 如果允许重复上报,或者该错误 hash 尚未存在(即第一次出现),则进行上报
if (!options.repeatCodeError || (options.repeatCodeError && !hashMapExist(hash))) {
return transportData.send(errorData);
}
}
// 资源加载报错 (target 存在且有 localName)
if (target?.localName) {
// 提取资源加载的详细信息
const data = resourceTransform(target);
// 记录资源错误到行为栈
breadcrumb.push({
type: EVENTTYPES.RESOURCE,
category: breadcrumb.getCategory(EVENTTYPES.RESOURCE),
status: STATUS_CODE.ERROR,
time: getTimestamp(),
data
});
// 上报资源加载错误
return transportData.send({
...data,
type: EVENTTYPES.RESOURCE,
status: STATUS_CODE.ERROR
});
}
},监听Promise异常未捕获的错误
ts
handleUnhandleRejection(event: PromiseRejectionEvent) {
const stackFrame = ErrorStackParser.parse(event.reason)[0];
const { fileName, columnNumber, lineNumber } = stackFrame;
const message = unknownToString(event.reason.message || event.reason.stack);
const data = {
type: EVENTTYPES.UNHANDLEDREJECTION,
status: STATUS_CODE.ERROR,
time: getTimestamp(),
message,
fileName,
line: lineNumber,
column: columnNumber
};
breadcrumb.push({
type: EVENTTYPES.UNHANDLEDREJECTION,
category: breadcrumb.getCategory(EVENTTYPES.UNHANDLEDREJECTION),
time: getTimestamp(),
status: STATUS_CODE.ERROR,
data
});
const hash: string = getErrorUid(
`${EVENTTYPES.UNHANDLEDREJECTION}-${message}-${fileName}-${columnNumber}`
);
// 开启repeatCodeError第一次报错才上报
if (!options.repeatCodeError || (options.repeatCodeError && !hashMapExist(hash))) {
transportData.send(data);
}
},handleError 是监控 SDK 中用于统一处理全局捕获到的错误的核心函数。它主要负责区分错误的类型(代码执行错误 vs 资源加载错误),并执行相应的数据解析、上下文记录(面包屑)、去重以及上报逻辑。
核心流程
该函数主要接受一个 ErrorTarget 类型的 event 对象,根据 event.target 的特征来判断错误类型。
1. 错误类型区分
代码执行错误 (Code Error):
- 包括 JS 运行时错误、Promise 异常(部分场景)、框架(Vue/React)捕获的错误。
- 判断依据:
event.target不存在,或者event.target存在但没有localName属性(非 DOM 元素)。
资源加载错误 (Resource Error):
- 包括图片、脚本、CSS 链接等资源加载失败。
- 判断依据:
event.target存在且具有localName属性(如img,script,link)。 - 注意: 资源加载错误不会冒泡,因此必须在捕获阶段(capturing phase)进行获取。SDK 在注册 error 事件监听时设置了
useCapture: true以确保能捕获此类错误。
Promise异常未捕获的错误:
- 通过
unhandledrejection事件捕获
- 通过
2. 代码执行错误处理流程
当判定为代码错误时,执行以下步骤:
- 堆栈解析: 使用
ErrorStackParser解析错误对象(event或event.error),提取关键堆栈帧信息(文件名fileName、行号lineNumber、列号columnNumber)。 - 构建错误数据: 组装标准化的错误上报数据对象 (
errorData),包含错误类型、时间戳、错误信息及堆栈位置。 - 记录面包屑 (Breadcrumb): 将错误信息压入用户行为栈,类型为
EVENTTYPES.ERROR,用于还原错误发生前的用户路径。 - 生成 Hash 指纹: 基于
错误类型 + 错误信息 + 文件名 + 列号生成唯一的 Hash 字符串。 - 上报与去重:
- 检查配置项
options.repeatCodeError。 - 如果允许重复上报,或者该错误 Hash 尚不存在(通过
hashMapExist(hash)校验),则调用transportData.send(errorData)进行上报。
- 检查配置项
3. 资源加载错误处理流程
当判定为资源错误时,执行以下步骤:
- 信息提取: 调用
resourceTransform(target)从 DOM 元素中提取资源 URL、标签名等信息。 - 记录面包屑: 将资源错误信息压入用户行为栈,类型为
EVENTTYPES.RESOURCE。 - 直接上报: 组装数据并调用
transportData.send进行上报。
关键代码解析
typescript
// 区分逻辑
if (!target || (event.target && !event.target.localName)) {
// === 代码错误处理 ===
// 1. 解析堆栈 (兼容同步与异步/框架错误)
const stackFrame = ErrorStackParser.parse(!target ? event : event.error)[0];
// 2. 生成 Hash 用于去重
const hash = getErrorUid(`${EVENTTYPES.ERROR}-${event.message}...`);
// 3. 校验并上报
if (!options.repeatCodeError || !hashMapExist(hash)) {
transportData.send(errorData);
}
}
if (target?.localName) {
// === 资源错误处理 ===
// 1. 提取 DOM 资源信息
const data = resourceTransform(target);
// 2. 直接上报
transportData.send({ ...data, type: EVENTTYPES.RESOURCE ... });
}