实现一个简易版的前端监控 SDK
【简易版的前端监控系统】
1、Promise的错误如何监控?–promise不是所有都是接口请求
2、接口的报错如何监控?–全局监控sdk,不改动公共的请求方法、不改动业务代码;一般接口使用axios请求
3、资源的报错如何监控?
4、监控: 埋点上报报错
注意:
(1)埋点监控报错死循环报错 – 重试机制、另一个埋点
(2)运行监控代码如何判断Vue/React,Vue/React有无内部监控api直接调用?
(3)window.error?? 能否捕获到接口的错误?
(4)所有监控放到同一个SDK监控
答:
(2)判断是否存在 Vue/React 可以通过检查 window.Vue
和 window.React
是否定义。
Vue: 有内部监控 API,可通过 Vue.config.errorHandler
捕获 Vue 实例中的错误。
React: 类组件可用 ErrorBoundary 捕获子组件错误,函数组件实验性地能用 useErrorBoundary Hook 。
(3)window.onerror
不能捕获接口的错误。接口请求通常使用 XMLHttpRequest
或 fetch
,其错误会在各自的回调或 Promise
中处理,不会触发 window.onerror
。
- 【整体思路】:
SDK 监控错误是通过多种方式实现的,具体如下:
try...catch
:用于在可预见的代码块中捕获特定错误,例如在模拟埋点上报时捕获可能出现的错误。
window.onerror
:用于捕获预料之外的同步错误,不过不能捕获异步错误。
window.unhandledrejection
:专门用于监听和捕获未处理的 Promise 错误。
(在业务代码里,通常:使用 Promise .catch() 处理 Promise 错误;使用 async/await 结合 try…catch 处理 Promise 错误)
网络错误捕获:
(1)XMLHttpRequest:重写 window.XMLHttpRequest
并监听其 error
事件,捕获 XMLHttpRequest 请求的网络错误。
(2)Axios:使用 Proxy 代理重写 axios.request
方法,捕获 Axios 请求的网络错误。
资源加载错误捕获:
重写 window.addEventListener
方法,监听 error
事件,捕获 HTML 资源(如脚本、样式表、图片)加载失败的错误。
// 定义前端监控 SDK 类
class FrontendMonitoringSDK {
constructor(options) {
this.options = options;
this.init();
this.monitorVueErrors();
this.monitorReactErrors();
}
// 初始化监控
init() {
this.monitorPromiseErrors();
this.monitorApiErrors();
this.monitorResourceErrors();
this.monitorWindowErrors();
if (this.options.track) {
this.monitorTrackErrors(this.options.track);
}
}
// 监控 Promise 错误 -- Promise内部无需重试机制,上报前端监控仍然使用retryReport
/**
* 通常不建议对 Promise 错误使用重试机制。
原因:Promise 错误一般是由代码逻辑错误、异步操作的异常(如数据库查询失败、函数调用
参数错误)等引发的。重试并不能解决这些根源问题,反而可能导致程序陷入无限重试的循环,消耗
大量资源。例如,在处理 Promise 时,如果是因为传入的参数不符合要求而抛出错误,重试同样的
操作依旧会失败。
*/
monitorPromiseErrors() {
window.addEventListener('unhandledrejection', (event) => {
this.retryReport({
type: 'promise',
message: event.reason instanceof Error ? event.reason.message : String(event.reason),
stack: event.reason instanceof Error ? event.reason.stack : null
});
});
}
// 监控接口错误
monitorApiErrors() {
const originalXHR = window.XMLHttpRequest;
window.XMLHttpRequest = function () {
const xhr = new originalXHR();
const self = this;
xhr.addEventListener('error', function () {
self.retryReport({
type: 'api',
message: `API 请求错误: ${xhr.status} ${xhr.statusText}`,
url: xhr.responseURL
});
});
return xhr;
}.bind(this);
if (window.axios) {
const originalAxios = window.axios;
const maxRetries = 3;
window.axios = new Proxy(originalAxios, {
get(target, prop) {
if (prop === 'request') {
return function (config) {
let retries = 0;
const makeRequest = () => {
return originalAxios.request(config).catch((error) => {
if (retries < maxRetries) {
retries++;
return makeRequest();
} else {
this.retryReport({
type: 'api',
message: `Axios 请求错误: ${error.message}`,
url: config.url
});
throw error;
}
});
};
return makeRequest();
}.bind(this);
}
return target[prop];
}
});
}
}
// 监控资源加载错误
monitorResourceErrors() {
const maxRetries = 3;
const originalAddEventListener = window.addEventListener;
window.addEventListener = function (type, listener, options) {
if (type === 'error') {
const newListener = (event) => {
if (event.target instanceof HTMLScriptElement || event.target instanceof HTMLLinkElement || event.target instanceof HTMLImageElement) {
let retries = 0;
const retryResourceLoad = () => {
if (retries < maxRetries) {
if (event.target instanceof HTMLScriptElement) {
const src = event.target.src;
event.target.src = '';
event.target.src = src;
} else if (event.target instanceof HTMLLinkElement) {
const href = event.target.href;
event.target.href = '';
event.target.href = href;
} else if (event.target instanceof HTMLImageElement) {
const src = event.target.src;
event.target.src = '';
event.target.src = src;
}
retries++;
} else {
this.retryReport({
type: 'resource',
message: `资源加载错误: ${event.target.src || event.target.href}`,
url: event.target.src || event.target.href
});
}
};
retryResourceLoad();
} else {
listener.call(this, event);
}
};
return originalAddEventListener.call(this, type, newListener, options);
}
return originalAddEventListener.call(this, type, listener, options);
}.bind(this);
}
// 监控全局错误
/**
1. message: 错误的具体描述信息
2. source: 发生错误的脚本文件的 URL;如果错误出现在内联脚本中,返回当前页面的 URL。
3. lineno: 错误发生所在行的行号
4. colno 错误发生所在列的列号
5. error: 一个 Error 对象,它包含了更详尽的错误信息,像错误堆栈(stack)之类的。
*/
monitorWindowErrors() {
window.onerror = (message, source, lineno, colno, error) => {
this.retryReport({
type: 'window',
message: message,
stack: error ? error.stack : null,
source: source,
lineno: lineno,
colno: colno
});
return true;
};
}
// 监控埋点库上报错误
monitorTrackErrors(track) {
const { Track, config, errorType } = track;
const maxRetries = 3;
const trackInstance = new Track(config);
// 假设库有一个错误回调
trackInstance.onError = (error) => {
let retries = 0;
const retryTrackReport = () => {
if (retries < maxRetries) {
// 这里需要根据埋点库具体逻辑实现重试上报
// 假设埋点库有一个重新上报的方法 retryReport
if (trackInstance.retryReport) {
trackInstance.retryReport();
}
retries++;
} else {
this.retryReport({
type: errorType,
message: `${errorType} 埋点上报错误: ${error.message}`,
stack: error.stack || null
});
}
};
retryTrackReport();
};
}
// 监控 Vue 错误
monitorVueErrors() {
if (typeof window.Vue !== 'undefined') {
window.Vue.config.errorHandler = (err, vm, info) => {
this.retryReport({
type: 'vue',
message: err.message,
stack: err.stack,
info: info
});
};
}
}
// 监控 React 错误
monitorReactErrors() {
if (typeof window.React !== 'undefined' && typeof window.ReactDOM !== 'undefined') {
const sdk = this;
const { useErrorBoundary } = window.React;
const ErrorBoundary = ({ children }) => {
const { error, resetErrorBoundary } = useErrorBoundary({
onError: (error, errorInfo) => {
sdk.retryReport({
type: 'react',
message: error.message,
stack: error.stack,
info: errorInfo.componentStack
});
}
});
if (error) {
return window.React.createElement('div', null, 'Something went wrong.');
}
return children;
};
// 可以考虑在这里将 ErrorBoundary 包裹在根组件上
// 假设根组件是 RootComponent
const originalRender = window.ReactDOM.render;
window.ReactDOM.render = function (element, container, callback) {
const errorBoundaryWrappedElement = window.React.createElement(ErrorBoundary, null, element);
return originalRender.call(this, errorBoundaryWrappedElement, container, callback);
};
}
}
// 上报错误
reportError(errorData) {
const xhr = new XMLHttpRequest();
xhr.open('POST', this.options.reportUrl, true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
console.log('错误上报成功');
} else {
console.error('错误上报失败');
}
}
};
xhr.send(JSON.stringify(errorData));
}
// 重试上报错误
retryReport(errorData) {
const maxRetries = 3;
let retries = 0;
const sendReport = () => {
const xhr = new XMLHttpRequest();
xhr.open('POST', this.options.reportUrl, true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
console.log('错误上报成功');
} else {
if (retries < maxRetries) {
retries++;
sendReport();
} else {
console.error('错误上报失败,达到最大重试次数');
}
}
}
};
xhr.send(JSON.stringify(errorData));
};
sendReport();
}
}
// 【使用示例】
// 假设已经引入了 @company/example-tracking 库(业务埋点库)
import Tracking from '@company/example-tracking';
const sdk = new FrontendMonitoringSDK({
// 错误上报接口地址
reportUrl: 'https://your-report-url.com',
// 业务埋点
track: {
Track: Tracking,
config: {
enable: true,
// 业务埋点上报地址
domain: 'https://test-maidian.company.cn',
mdParams: {
cv: new URLSearchParams(window.location.search).get('cv'),
md_etype: 'h5log',
},
},
errorType: 'Tracking'
}
});
- 【模拟报错】
// 模拟业务埋点库
class MockTracking {
constructor(config) {
this.config = config;
}
// 模拟上报方法
report() {
try {
// 模拟上报失败
throw new Error('埋点上报失败');
} catch (error) {
if (this.onError) {
this.onError(error);
}
}
}
// 模拟重试上报方法
retryReport() {
this.report();
}
// 定义 onError 方法
onError(error) {
console.log('MockTracking 捕获到错误:', error.message);
// 可以在这里添加更多的错误处理逻辑
}
}
// 初始化 SDK
const sdk = new FrontendMonitoringSDK({
// 错误上报接口地址
reportUrl: 'https://your-report-url.com',
// 业务埋点
track: {
Track: MockTracking,
config: {
enable: true,
// 业务埋点上报地址
domain: 'https://test-maidian.company.cn',
mdParams: {
cv: new URLSearchParams(window.location.search).get('cv'),
md_etype: 'h5log',
},
},
errorType: 'Tracking'
}
});
// 1. 模拟 Promise 的错误
const promiseError = new Promise((_, reject) => {
reject(new Error('Promise 错误'));
});
// 2. 模拟接口的报错 -- 使用 axios 请求
import axios from 'axios';
// 模拟一个不存在的接口地址
const apiError = axios.get('https://nonexistent-api-url.com');
// 3. 模拟资源的报错
const script = document.createElement('script');
script.src = 'https://nonexistent-script-url.js';
document.body.appendChild(script);
// 4. 模拟埋点上报报错
const trackInstance = new MockTracking({
enable: true,
domain: 'https://test-maidian.company.cn',
mdParams: {
cv: new URLSearchParams(window.location.search).get('cv'),
md_etype: 'h5log',
},
});
// 业务代码调用时无需再写 try...catch
trackInstance.report();