跳到主要内容

异常监控与上报

2024年04月21日
柏拉文
越努力,越幸运

一、认识


目前, Web 监控 SDK 主要是通过以下方式收集错误:

window.addEventListener('unhandlerejection', function(event){
console.log("Unhandled Rejection:", event.reason.message, event.reason.stack)
});

window.addEventListener('error', function(event){
console.log("Error:", event.error, 'Error message:', event.message, 'File:', event.fileName, 'Line:', event.lineNumber, 'Column:', event.colNumber)
});

可以看到上面的收集方式主要是通过监听错误的回调,在接收到错误后进行错误信息上报。在微前端场景下,如果微前端主应用和子应用分别使用了上述代码进行错误监听和上报,将会造成两个问题:接收到的不一定来自自身应用、微应用销毁后还会接收到其他应用的错误。

通过对于 errorunhandledrejection 两种事件的监听发现, 想要在同一个上下文里面区分错误是来自哪个应用貌似是一件非常困难的事情,因为它没法限定监听特定的错误,那有什么办法在监到错误后根据错误信息区分来源呢?

二、收集 js 静态资源信息


通过对于错误信息的分析,错误中的 filenameerror.stack 可以区分错误来源,因此只要找到静态资源信息的来源即可,针对目前 Qiankun 微前端框架里面的运行时沙箱和构建插件,目前有两种收集静态资源的方式:

  1. 在微应用中接入特定的构建插件,生成 manifest 文件, manifest 中包括所有的静态资源信息,可以用这份信息进行过滤:

    • 优点: 基本上没有什么运行时损耗,基于这份信息在直接运行时监听到错误信息后进行过滤上报

    • 缺点: 无法收集到动态创建的 js, 动态的部分只能手动配置解决,需要额外接入一个构建插件

  2. 利用运行时沙箱能力收集分类

    • 优点: 无论是静态还是动态 js 信息都能有效收集没有遗漏,而且不需要额外接入构建插件

    • 缺点: 强依赖运行时沙箱,存在一定的性能损耗

2.1 应用构建时收集信息

2.2 基于运行时沙箱能力收集

const varBox = {};

const faceWindow = new Proxy(window, {
get(target,key){
return varBox[key] || Reflect.get(window,key);
},
set(target,key,value){
varBox[key] = value;
return true;
}
});

const fn = new Function('window', `console.log('proxyWindow', window)`);
fn(faceWindow);

通过上面代码, 就实现了一个非常简单的运行时 sandbox, 它能够代理对于真实的 window 的访问, 那么基于这样的一个基础设计,怎么样能够收集到微应用的所有静态 js 资源,在常规的 Web 应用中静态 js 资源的使用无非有两个地方: 在 HTML 中直接声明的 script 标签、在 js 文件中动态创建添加的 script, 前者在 Qiankun 中将会经过 Loader 解析直接获取完整的 js 静态资源,但是后者需要如何去支持。

添加动态 script 分为两个步骤: 创建、添加

  1. 劫持创建元素操作: 在运行时 API 中主要通过 document 对象, 在 createElement 方法中对通过代理对象创建的 Element 元素增加标记

    // script 带上 sandbox id 
    const fakeDocument = new Proxy(document, {
    get(target, key){
    const value = Reflect.get(document,p);
    if(p === 'createElement'){
    return function (tagName){
    const el = value.call(document, tagName, options);
    el['__elementSandboxTag__'] = sandbox.id;
    return el;
    }
    }
    return document[key];
    }
    });

    const fn = new Function('document', `
    let script = document.createElement('script');
    script.src = 'xxxx';
    document.head.appendChild(script);
    `);
    fn(fakeDocument);
  2. 劫持添加元素操作: 在添加节点时,根据节点上的信息查找到添加的来源,将节点上的 src 等信息进行收集

    // 在添加 script 的时候根据 sandbox id 存储静态资源
    const mountElementMethods = [
    'append',
    'appendChild',
    'insertBefore',
    'insertAdjacentElement'
    ]

    function injector(current: Function, methodName: string){
    return function (this: Element){
    const sandbox = sandboxMap.get(el);
    const originProcess = ()=> current.apply(this, arguments);

    if(sandbox){
    // 获取到 el.src, 存储到列表中
    }else{
    return originProcess();
    }
    }
    }

    export function makeElInjector(sandboxConfig: SandboxOptions){
    if(typeof window.Element === 'function'){
    const rewrite = (methods: Array<string>, builder: typeof injector | typeof injectorRemoveChild)=>{
    for(const name of methods){
    const fn = window.Element.prototype[name];
    if(typeof fn !== 'function' || fn[__domWrapper__]){
    continue;
    }
    rawElementMethods[name] = fn;
    const wrapper = builder(fn,name);
    wrapper[__domWrapper__] = true;
    window.Element.prototype[name] = wrapper;
    }
    };
    rewrite(mountElementMethods,injector);
    rewrite(removeChildElementMethods,injectorRemoveChild);
    }
    injectHandlerParams();
    }