@ked3/ktrace
Version:
跨平台埋点分析SDK
282 lines (240 loc) • 7.52 kB
text/typescript
import Tracker from './Tracker';
import { ErrorInfo } from './types';
/**
* 错误监控类
* 负责捕获和处理各种类型的错误
*/
class ErrorMonitor {
private tracker: Tracker;
private initialized: boolean = false;
private ignoreList: RegExp[] = [];
/**
* 构造函数
*/
constructor(tracker: Tracker) {
this.tracker = tracker;
}
/**
* 初始化错误监控
*/
public init(options: {
ignoreErrors?: RegExp[];
captureJsError?: boolean;
capturePromiseError?: boolean;
captureAjaxError?: boolean;
captureResourceError?: boolean;
} = {}): void {
if (this.initialized) return;
// 设置忽略规则
if (options.ignoreErrors) {
this.ignoreList = options.ignoreErrors;
}
// 捕获JS异常
if (options.captureJsError !== false) {
this.captureJsErrors();
}
// 捕获Promise错误
if (options.capturePromiseError !== false) {
this.capturePromiseErrors();
}
// 捕获Ajax错误
if (options.captureAjaxError !== false) {
this.captureAjaxErrors();
}
// 捕获资源加载错误
if (options.captureResourceError !== false) {
this.captureResourceErrors();
}
this.initialized = true;
}
/**
* 捕获JavaScript错误
*/
private captureJsErrors(): void {
window.addEventListener('error', (event) => {
// 忽略资源加载错误,这些会由captureResourceErrors处理
if (event.error && event.error instanceof Error) {
const error = event.error;
// 检查是否应该忽略
if (this.shouldIgnore(error.message)) {
return;
}
const errorInfo: ErrorInfo = {
name: error.name,
message: error.message,
stack: error.stack,
category: 'js_error',
context: {
url: window.location.href,
line: event.lineno,
column: event.colno,
filename: event.filename
}
};
this.reportError(errorInfo);
}
}, true);
}
/**
* 捕获Promise未处理的拒绝
*/
private capturePromiseErrors(): void {
window.addEventListener('unhandledrejection', (event) => {
let errorInfo: ErrorInfo;
if (event.reason instanceof Error) {
// 检查是否应该忽略
if (this.shouldIgnore(event.reason.message)) {
return;
}
errorInfo = {
name: event.reason.name,
message: event.reason.message,
stack: event.reason.stack,
category: 'unhandled_promise_rejection',
context: {
url: window.location.href
}
};
} else {
let reason = String(event.reason);
// 检查是否应该忽略
if (this.shouldIgnore(reason)) {
return;
}
errorInfo = {
name: 'UnhandledPromiseRejection',
message: reason,
category: 'unhandled_promise_rejection',
context: {
url: window.location.href
}
};
}
this.reportError(errorInfo);
});
}
/**
* 捕获Ajax请求错误
*/
private captureAjaxErrors(): void {
if (!window.XMLHttpRequest) return;
const originalSend = XMLHttpRequest.prototype.send;
const originalOpen = XMLHttpRequest.prototype.open;
// 保存this的引用
const self = this;
// 重写open方法以记录请求信息
XMLHttpRequest.prototype.open = function(method: string, url: string | URL, async?: boolean, username?: string | null, password?: string | null): void {
(this as any).__ktrace_request = {
method,
url: url.toString(),
startTime: Date.now()
};
originalOpen.call(this, method, url, async === undefined ? true : async, username ?? null, password ?? null);
};
// 重写send方法以添加错误监听
XMLHttpRequest.prototype.send = function(body?: Document | XMLHttpRequestBodyInit | null): void {
const xhr = this;
// 添加load事件监听
xhr.addEventListener('load', function() {
if (xhr.status >= 400) {
const request = (xhr as any).__ktrace_request || {};
const duration = Date.now() - (request.startTime || Date.now());
const errorInfo: ErrorInfo = {
name: 'HttpError',
message: `HTTP错误 ${xhr.status} (${xhr.statusText})`,
category: 'http_error',
context: {
url: request.url,
method: request.method,
status: xhr.status,
response: xhr.responseText?.substring(0, 200),
duration
}
};
self.reportError(errorInfo);
}
});
// 添加error事件监听
xhr.addEventListener('error', function() {
const request = (xhr as any).__ktrace_request || {};
const duration = Date.now() - (request.startTime || Date.now());
const errorInfo: ErrorInfo = {
name: 'HttpRequestError',
message: `请求失败 ${request.url}`,
category: 'http_request_error',
context: {
url: request.url,
method: request.method,
duration
}
};
self.reportError(errorInfo);
});
originalSend.call(xhr, body);
};
}
/**
* 捕获资源加载错误
*/
private captureResourceErrors(): void {
window.addEventListener('error', (event) => {
const target = event.target as HTMLElement;
// 仅处理资源元素的错误
if (target && (
target.tagName === 'SCRIPT' ||
target.tagName === 'LINK' ||
target.tagName === 'IMG' ||
target.tagName === 'AUDIO' ||
target.tagName === 'VIDEO'
)) {
let url = '';
if (target instanceof HTMLScriptElement ||
target instanceof HTMLImageElement ||
target instanceof HTMLAudioElement ||
target instanceof HTMLVideoElement) {
url = target.src;
} else if (target instanceof HTMLLinkElement) {
url = target.href;
}
// 检查是否应该忽略
if (this.shouldIgnore(url)) {
return;
}
const errorInfo: ErrorInfo = {
name: 'ResourceError',
message: `无法加载资源: ${url}`,
category: 'resource_error',
context: {
url: window.location.href,
resourceUrl: url,
resourceType: target.tagName.toLowerCase()
}
};
this.reportError(errorInfo);
}
}, true);
}
/**
* 手动报告错误
*/
public reportError(errorInfo: ErrorInfo): void {
if (this.shouldIgnore(errorInfo.message)) {
return;
}
// 使用tracker发送错误信息
this.tracker.trackError(new Error(errorInfo.message), {
name: errorInfo.name,
category: errorInfo.category,
context: errorInfo.context,
stack: errorInfo.stack
});
}
/**
* 检查是否应该忽略错误
*/
private shouldIgnore(message: string): boolean {
if (!message) return false;
return this.ignoreList.some(pattern => pattern.test(message));
}
}
export default ErrorMonitor;