当前位置: 首页 > news >正文

实现一个简易版的前端监控 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.Vuewindow.React 是否定义。
Vue: 有内部监控 API,可通过 Vue.config.errorHandler 捕获 Vue 实例中的错误。
React: 类组件可用 ErrorBoundary 捕获子组件错误,函数组件实验性地能用 useErrorBoundary Hook 。
(3)window.onerror 不能捕获接口的错误。接口请求通常使用 XMLHttpRequestfetch,其错误会在各自的回调或 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();
    

相关文章:

  • ​AI训练中的专有名词大白话版
  • Linux《进程概念(上)》
  • PGD对抗样本生成算法实现(pytorch版)
  • React编程模型:React Streams规范详解
  • 阿里:多模态大模型预训练数据治理
  • VBA第三十四期 VBA中怎么用OnKey事件
  • Java与代码审计-Java基础语法
  • 【Pandas DataFrame】
  • SpringBoot学习Day2
  • SAP学习笔记 - 用Deepseek 整理SAP 09 - SAP中 BAPI 的核心概念,以及常用 BAPI 一览
  • (二十三)Dart 中的 Mixins 使用教程
  • C之(16)scan-build与clang-tidy使用
  • Spring Boot 3.4.3 基于 Spring WebFlux 实现 SSE 功能
  • 小白电路设计-设计5-可调式单电源直流稳压电路设计
  • 力扣经典算法篇-4-删除有序数组中的重复项 II(中等)
  • Python-Django入手
  • git的clone报错unable to access 443
  • 批量将 PDF 文档中的图片提取到指定文件夹
  • 云服务器Ubuntu安装宝塔面板MongoDB修改配置文件本地连接
  • AI Agent 实战:搭建个人在线旅游助手