【简易版的前端监控系统】
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) {// 这里需要根据埋点库具体逻辑实现重试上报// 假设埋点库有一个重新上报的方法 retryReportif (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 包裹在根组件上// 假设根组件是 RootComponentconst 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();