gpt4 book ai didi

shin-monitor源码分析

转载 作者:我是一只小鸟 更新时间:2023-02-13 14:34:14 26 4
gpt4 key购买 nike

  在经过两年多的线上沉淀后,将监控代码重新用 TypeScript 编写,删除冗余逻辑,正式开源.

  根据 shin-monitor 的目录结构可知,源码集中在 src 目录中。关于监控系统的迭代过程,可以参考 专栏 .

1、入口

  入口文件是 index.ts,旁边的 utils.ts 是一个工具库.

  在 index.ts 中,将会引入 lib 目录中的 error、action 和 performance 三个文件.

1)defaults 。

  声明 defaults 变量,配置了各个参数的默认属性,各个参数的 使用指南 可以查看注释、readme 或 demo 目录中的文件.

                      const defaults: TypeShinParams =
                      
                         {
  src: 
                      
                      '//127.0.0.1:3000/ma.gif',       
                      
                        //
                      
                      
                         采集监控数据的后台接收地址
                      
                      
  psrc: '//127.0.0.1:3000/pe.gif',      
                      
                        //
                      
                      
                         采集性能参数的后台接收地址
                      
                      
  pkey: '',                             
                      
                        //
                      
                      
                         性能监控的项目key
                      
                      
  subdir: '',                           
                      
                        //
                      
                      
                         一个项目下的子目录
                      
                      
  rate: 5,                              
                      
                        //
                      
                      
                         随机采样率,用于性能搜集,范围是 1~10,10 表示百分百发送
                      
                      
  version: '',                          
                      
                        //
                      
                      
                         版本,便于追查出错源
                      
                      
                          record: {
    isOpen: 
                      
                      
                        true
                      
                      ,                       
                      
                        //
                      
                      
                         是否开启录像
                      
                      
    src: '//cdn.jsdelivr.net/npm/rrweb@latest/dist/rrweb.min.js'   
                      
                        //
                      
                      
                         录像地址
                      
                      
                          },
  error: {
    isFilterErrorFunc: 
                      
                      
                        null
                      
                      ,            
                      
                        //
                      
                      
                         需要过滤的代码错误
                      
                      
    isFilterPromiseFunc: 
                      
                        null
                      
                      ,          
                      
                        //
                      
                      
                         需要过滤的Promise错误
                      
                      
                          },
  console: {
    isOpen: 
                      
                      
                        true
                      
                      ,               
                      
                        //
                      
                      
                         默认是开启,在本地调试时,可以将其关闭
                      
                      
    isFilterLogFunc: 
                      
                        null
                      
                      ,      
                      
                        //
                      
                      
                         过滤要打印的内容
                      
                      
                          },
  crash: {
    isOpen: 
                      
                      
                        true
                      
                      ,               
                      
                        //
                      
                      
                         是否监控页面奔溃,默认开启
                      
                      
    validateFunc: 
                      
                        null
                      
                      ,         
                      
                        //
                      
                      
                         自定义页面白屏的判断条件,返回值包括 {success: true, prompt:'提示'}
                      
                      
                          },
  event: {
    isFilterClickFunc: 
                      
                      
                        null
                      
                      ,    
                      
                        //
                      
                      
                         在点击事件中需要过滤的元素
                      
                      
                          },
  ajax: {
    isFilterSendFunc: 
                      
                      
                        null
                      
                      
                        //
                      
                      
                         在发送监控日志时需要过滤的通信
                      
                      
                          },
  identity: {
    value: 
                      
                      '',                  
                      
                        //
                      
                      
                         自定义的身份信息字段
                      
                      
    getFunc: 
                      
                        null
                      
                      ,              
                      
                        //
                      
                      
                         自定义的身份信息获取函数
                      
                      
                          }, 
};
                      
                    

2)setParams() 。

  在 setParams() 函数中,会初始化引入的 3 个类,然后开始监控页面错误、计算性能参数、监控用户行为.

                      
                        function
                      
                      
                         setParams(params: TypeShinParams): TypeShinParams {
  
                      
                      
                        if
                      
                       (!
                      
                        params) {
    
                      
                      
                        return
                      
                      
                        null
                      
                      
                        ;
  }
  const combination 
                      
                      =
                      
                         defaults;
  
                      
                      
                        //
                      
                      
                         只重置 params 中的参数
                      
                      
                        for
                      
                      (const key 
                      
                        in
                      
                      
                         params) {
    combination[key] 
                      
                      =
                      
                         params[key];
  }
  
                      
                      
                        //
                      
                      
                         埋入自定义的身份信息
                      
                      
  const { getFunc } =
                      
                         combination.identity;
  getFunc 
                      
                      &&
                      
                         getFunc(combination);
  
  
                      
                      
                        //
                      
                      
                         监控页面错误
                      
                      
  const error = 
                      
                        new
                      
                      
                         ErrorMonitor(combination);
  error.registerErrorEvent();                   
                      
                      
                        //
                      
                      
                         注册 error 事件
                      
                      
  error.registerUnhandledrejectionEvent();      
                      
                        //
                      
                      
                         注册 unhandledrejection 事件
                      
                      
  error.registerLoadEvent();                    
                      
                        //
                      
                      
                         注册 load 事件
                      
                      
                          error.recordPage();
  shin.reactError 
                      
                      = error.reactError.bind(error);   
                      
                        //
                      
                      
                         对外提供 React 的错误处理
                      
                      
  shin.vueError = error.vueError.bind(error);       
                      
                        //
                      
                      
                         对外提供 Vue 的错误处理
                      
                      
                        //
                      
                      
                         启动性能监控
                      
                      
  const pe = 
                      
                        new
                      
                      
                         PerformanceMonitor(combination);
  pe.observerLCP();      
                      
                      
                        //
                      
                      
                         监控 LCP
                      
                      
  pe.observerFID();      
                      
                        //
                      
                      
                         监控 FID
                      
                      
  pe.registerLoadAndHideEvent();    
                      
                        //
                      
                      
                         注册 load 和页面隐藏事件
                      
                      
                        //
                      
                      
                         为原生对象注入自定义行为
                      
                      
  const action = 
                      
                        new
                      
                      
                         ActionMonitor(combination);
  action.injectConsole();   
                      
                      
                        //
                      
                      
                         监控打印
                      
                      
  action.injectRouter();    
                      
                        //
                      
                      
                         监听路由
                      
                      
  action.injectEvent();     
                      
                        //
                      
                      
                         监听事件
                      
                      
  action.injectAjax();      
                      
                        //
                      
                      
                         监听Ajax
                      
                      
                        return
                      
                      
                         combination;
}
                      
                    

  函数中做了大量初始化工作,若不需要某些监控行为,可自行删除.

2、lib 目录

  在 lib 目录中,存放着整个监控系统的核心逻辑.

1)Http 。

  Http 的主要工作是通信,也就是将搜集起来的监控日志或性能参数,统一发送到后台.

  并且在 Http 中,还会根据算法生成身份标识字符串,以及做最后的参数组装工作.

  监控日志原先采用的发送方式是 Image,目的是跨域,但是发送的数据量有限,像 Ajax 通信,如果需要记录响应,那么长度就会不够.

  因此后期就改成了 fetch() 函数,默认只会上传 8000 长度的数据.

                      public send(data: TypeSendParams, callback?: ParamsCallback): 
                      
                        void
                      
                      
                         {
  
                      
                      
                        //
                      
                      
                         var ts = new Date().getTime().toString();
                      
                      
                        //
                      
                      
                         var img = new Image(0, 0);
                      
                      
                        //
                      
                      
                         img.src = shin.param.src + "?m=" + _paramify(data) + "&ts=" + ts;
                      
                      
  const m = 
                      
                        this
                      
                      
                        .paramify(data);
  
                      
                      
                        //
                      
                      
                         大于8000的长度,就不在上报,废弃掉
                      
                      
                        if
                      
                       (m.length >= 8000
                      
                        ) {
    
                      
                      
                        return
                      
                      
                        ;
  }
  const body: TypeSendBody 
                      
                      =
                      
                         { m };
  callback 
                      
                      && callback(data, body); 
                      
                        //
                      
                      
                         自定义的参数处理回调
                      
                      
                        //
                      
                      
                         如果修改headers,就会多一次OPTIONS预检请求
                      
                      
  fetch(
                      
                        this
                      
                      
                        .params.src, {
    method: 
                      
                      "POST"
                      
                        ,
    
                      
                      
                        //
                      
                      
                         headers: {
                      
                      
                        //
                      
                      
                           'Content-Type': 'application/json',
                      
                      
                        //
                      
                      
                         },
                      
                      
                            body: JSON.stringify(body)
  });
}
                      
                    

  而性能参数的发送采用了 sendBeacon() 方法,在页面关闭时也能上报,这是普通的请求所不具备的特性.

  它能将少量数据异步 POST 到后台,并且支持跨域,而少量是指多少并没有特别指明,由浏览器控制,网上查到的资料说一般在 64KB 左右.

                      public sendPerformance(data: TypeCaculateTiming): 
                      
                        void
                      
                      
                         {
  
                      
                      
                        //
                      
                      
                         如果传了数据就使用该数据,否则读取性能参数,并格式化为字符串
                      
                      
                        var
                      
                       str = 
                      
                        this
                      
                      
                        .paramifyPerformance(data);
  
                      
                      
                        var
                      
                       rate = randomNum(10, 1); 
                      
                        //
                      
                      
                         选取1~10之间的整数
                      
                      
                        if
                      
                       (
                      
                        this
                      
                      .params.rate >= rate && 
                      
                        this
                      
                      
                        .params.pkey) {
    navigator.sendBeacon(
                      
                      
                        this
                      
                      
                        .params.psrc, str);
  }
}
                      
                    

2)Error 。

  在 Error 中,会注册 window 的 error 事件,用于监控脚本或资源错误,在脚本错误中,会提示行号和列号.

  不过资源错误是看不到具体的错误原因的,只会给个结果,出现了错误,连错误状态码也没有.

                          window.addEventListener('error', (event: ErrorEvent): 
                      
                        void
                      
                       =>
                      
                         {
      const errorTarget 
                      
                      = event.target as (Window |
                      
                         TypeEventTarget);
      
                      
                      
                        //
                      
                      
                         过滤掉与业务无关或无意义的错误
                      
                      
                        if
                      
                       (isFilterErrorFunc &&
                      
                         isFilterErrorFunc(event)) {
        
                      
                      
                        return
                      
                      
                        ;
      }
      
                      
                      
                        //
                      
                      
                         过滤 target 为 window 的异常
                      
                      
                        if
                      
                      
                         (
        errorTarget 
                      
                      !==
                      
                         window
          
                      
                      &&
                      
                         (errorTarget as TypeEventTarget).nodeName
          
                      
                      &&
                      
                         CONSTANT.LOAD_ERROR_TYPE[(errorTarget as TypeEventTarget).nodeName.toUpperCase()]
      ) {
        
                      
                      
                        this
                      
                      .handleError(
                      
                        this
                      
                      
                        .formatLoadError(errorTarget as TypeEventTarget));
      } 
                      
                      
                        else
                      
                      
                         {
        
                      
                      
                        //
                      
                      
                         过滤无效错误
                      
                      
        event.message && 
                      
                        this
                      
                      
                        .handleError(
          
                      
                      
                        this
                      
                      
                        .formatRuntimerError(
            event.message,
            event.filename,
            event.lineno,
            event.colno,
            
                      
                      
                        //
                      
                      
                         event.error,
                      
                      
                                  ),
        );
      }
    }, 
                      
                      
                        true
                      
                      ); 
                      
                        //
                      
                      
                         捕获
                      
                    

  还会注册 window 的 unhandledrejection 事件,用于监控未处理的 Promise 错误,当 Promise 被 reject 且没有 reject 处理器时触发.

  在 unhandledrejection 事件中,对于响应信息,其实是做了些扩展的,参考《 SDK中的 unhandledrejection 事件 》.

                          window.addEventListener('unhandledrejection',(event: PromiseRejectionEvent): 
                      
                        void
                      
                       =>
                      
                         {
      
                      
                      
                        //
                      
                      
                         处理响应数据,只抽取重要信息
                      
                      
      const { response } =
                      
                         event.reason;
      
                      
                      
                        //
                      
                      
                         若无响应,则不监控
                      
                      
                        if
                      
                       (!response || !
                      
                        response.request) {
        
                      
                      
                        return
                      
                      
                        ;
      }
      const desc: TypeAjaxDesc 
                      
                      =
                      
                         response.request.ajax;
      desc.status 
                      
                      = event.reason.status ||
                      
                         response.status;
      
                      
                      
                        //
                      
                      
                         过滤掉与业务无关或无意义的错误
                      
                      
                        if
                      
                      (isFilterPromiseFunc &&
                      
                         isFilterPromiseFunc(desc)) {
        
                      
                      
                        return
                      
                      
                        ;
      }
      
                      
                      
                        this
                      
                      
                        .handleError({
        type: CONSTANT.ERROR_PROMISE,
        desc,
        
                      
                      
                        //
                      
                      
                         stack: event.reason && (event.reason.stack || "no stack")
                      
                      
                              });
    }, 
                      
                      
                        true
                      
                      );
                    

  这 2 个错误的使用,都在 demo/error.html 中有所记录,另一个重要的错误是白屏.

  在白屏时,还会上报录像内容,白屏的迭代过程可以参考 此处 .

  对 body 的子元素做深度优先搜索,若已找到一个有高度的元素、或若元素隐藏、或元素有高度并且不是 body 元素,则结束搜索.

  为了便于定位白屏原因,在白屏时,还会记录些元素信息,例如元素类型、样式、高度等.

                      
                          private isWhiteScreen(): TypeWhiteScreen {
    const visibles 
                      
                      =
                      
                         [];
    const nodes 
                      
                      = [];       
                      
                        //
                      
                      
                        遍历到的节点的关键信息,用于查明白屏原因
                      
                      
                        //
                      
                      
                         深度优先遍历子元素
                      
                      
    const dfs = (node: HTMLElement): 
                      
                        void
                      
                       =>
                      
                         {
      const tagName 
                      
                      =
                      
                         node.tagName.toLowerCase();
      const rect 
                      
                      =
                      
                         node.getBoundingClientRect();
      
                      
                      
                        //
                      
                      
                         选取节点的属性作记录
                      
                      
      const attrs: TypeWhiteHTMLNode =
                      
                         {
        id: node.id,
        tag: tagName,
        className: node.className,
        display: node.style.display,
        height: rect.height
      };
      const src 
                      
                      =
                      
                         (node as HTMLImageElement).src;
      
                      
                      
                        if
                      
                      
                        (src) {
        attrs.src 
                      
                      = src;    
                      
                        //
                      
                      
                         记录图像的地址
                      
                      
                              }
      const href 
                      
                      =
                      
                        (node as HTMLAnchorElement).href;
      
                      
                      
                        if
                      
                      
                        (href) {
        attrs.href 
                      
                      = href; 
                      
                        //
                      
                      
                         记录链接的地址
                      
                      
                              }
      nodes.push(attrs);
      
                      
                      
                        //
                      
                      
                         若已找到一个有高度的元素,则结束搜索
                      
                      
                        if
                      
                      (visibles.length > 0) 
                      
                        return
                      
                      
                        ;
      
                      
                      
                        //
                      
                      
                         若元素隐藏,则结束搜索
                      
                      
                        if
                      
                       (node.style.display === 'none') 
                      
                        return
                      
                      
                        ;
      
                      
                      
                        //
                      
                      
                         若元素有高度并且不是 body 元素,则结束搜索
                      
                      
                        if
                      
                      (rect.height > 0 && tagName !== 'body'
                      
                        ) {
        visibles.push(node);
        
                      
                      
                        return
                      
                      
                        ;
      }
      node.children 
                      
                      && [].slice.call(node.children).forEach((child: HTMLElement): 
                      
                        void
                      
                       =>
                      
                         {
        const tagName 
                      
                      =
                      
                         child.tagName.toLowerCase();
        
                      
                      
                        //
                      
                      
                         过滤脚本和样式元素
                      
                      
                        if
                      
                      (tagName === 'script' || tagName === 'link') 
                      
                        return
                      
                      
                        ;
        dfs(child);
      });
    };
    dfs(document.body);
    
                      
                      
                        return
                      
                      
                         {
      visibles: visibles,
      nodes: nodes
    };
  }
                      
                    

  监控白屏的时机,是在 load 事件中,延迟 1 秒触发.

  原先是在 DOMContentLoaded 事件内触发,经测试发现,当因为脚本错误出现白屏时,两个事件的触发时机会很接近.

  在线上监控时发现会有一些误报,HTML是有内容的,那很可能是 DOMContentLoaded 触发时,页面内容还没渲染好.

  对于热门的 React 和 Vue 库,声明了两个方法:reactError() 和 vueError(),将这两个方法分别应用于项目中,就能监控框架错误了.

  React 需要在项目中创建一个 ErrorBoundary 类,在类中调用 reactError() 方法.

  如果 Vue 是被模块化引入的,那么就得在模块的某个位置调用该方法,因为此时 Vue 不会绑定到 window 中,即不是全局变量.

3)Action 。

  在 Action 中会监控打印、路由、点击事件和 Ajax 通信。这 4 种行为都会对原生对象进行注入,它们的使用也都可以在 demo 目录中找到.

  以路由为例,不仅要监听 popstate 事件,还要重写 pushState 和 replaceState.

                        public injectRouter(): 
                      
                        void
                      
                      
                         {
    
                      
                      
                        /*
                      
                      
                        *
     * 全局监听跳转
     * 点击后退、前进按钮或者调用 history.back()、history.forward()、history.go() 方法才会触发 popstate 事件
     * 点击 <a href=/xx/yy#anchor>hash</a> 按钮也会触发 popstate 事件
     
                      
                      
                        */
                      
                      
                        
    const _onPopState 
                      
                      =
                      
                         window.onpopstate;
    window.onpopstate 
                      
                      = (args: PopStateEvent): 
                      
                        void
                      
                       =>
                      
                         {
      
                      
                      
                        this
                      
                      
                        .sendRouterInfo();
      _onPopState 
                      
                      && _onPopState.apply(
                      
                        this
                      
                      
                        , args);
    };
    
                      
                      
                        /*
                      
                      
                        *
     * 监听 pushState() 和 replaceState() 两个方法
     
                      
                      
                        */
                      
                      
                        
    const bindEventListener 
                      
                      = (type: string): TypeStateEvent =>
                      
                         {
      const historyEvent: TypeStateEvent 
                      
                      =
                      
                         history[type];
      
                      
                      
                        return
                      
                       (...args): 
                      
                        void
                      
                       =>
                      
                         {
        
                      
                      
                        //
                      
                      
                         触发 history 的原始事件,apply 的第一个参数若不是 history,就会报错
                      
                      
        const newEvent =
                      
                         historyEvent.apply(history, args);
        
                      
                      
                        this
                      
                      
                        .sendRouterInfo();
        
                      
                      
                        return
                      
                      
                         newEvent;
      };
    };
    history.pushState 
                      
                      = bindEventListener('pushState'
                      
                        );
    history.replaceState 
                      
                      = bindEventListener('replaceState'
                      
                        );
  }
                      
                    

4)Performance 。

  Performance 主要是对性能参数的搜集,大部分的性能参数是通过 performance.getEntriesByType('navigation')[0] 或 performance.timing 获取的.

  performance.timing 已被废弃,尽量不要使用,此处只是为了兼容。Performance 的迭代过程可以参考 此处 .

  参数的发送时机有两者,第一种是 window.load 事件中,第二种是页面隐藏的事件中.

  LCP、FID、FP 等参数可通过浏览器提供的对象获取.

                        public observerLCP(): 
                      
                        void
                      
                      
                         {
    const lcpType 
                      
                      = 'largest-contentful-paint'
                      
                        ;
    const isSupport 
                      
                      = 
                      
                        this
                      
                      
                        .checkSupportPerformanceObserver(lcpType);
    
                      
                      
                        //
                      
                      
                         浏览器兼容判断
                      
                      
                        if
                      
                      (!
                      
                        isSupport) {
      
                      
                      
                        return
                      
                      
                        ;
    }
    const po 
                      
                      = 
                      
                        new
                      
                       PerformanceObserver((entryList): 
                      
                        void
                      
                      =>
                      
                         {
      const entries 
                      
                      =
                      
                         entryList.getEntries();
      const lastEntry 
                      
                      = (entries as any)[entries.length - 1
                      
                        ] as TypePerformanceEntry;
      
                      
                      
                        this
                      
                      .lcp =
                      
                         {
        time: rounded(lastEntry.renderTime 
                      
                      || lastEntry.loadTime),                  
                      
                        //
                      
                      
                         时间取整
                      
                      
        url: lastEntry.url,                                                         
                      
                        //
                      
                      
                         资源地址
                      
                      
        element: lastEntry.element ? removeQuote(lastEntry.element.outerHTML) : ''  
                      
                        //
                      
                      
                         参照的元素
                      
                      
                              };
    });
    
                      
                      
                        //
                      
                      
                         buffered 为 true 表示调用 observe() 之前的也算进来
                      
                      
    po.observe({ type: lcpType, buffered: 
                      
                        true
                      
                      
                         } as any);
    
                      
                      
                        //
                      
                      
                         po.observe({ entryTypes: [lcpType] });
                      
                      
                        /*
                      
                      
                        *
     * 当有按键或点击(包括滚动)时,就停止 LCP 的采样
     * once 参数是指事件被调用一次后就会被移除
     
                      
                      
                        */
                      
                      
                        
    [
                      
                      'keydown', 'click'].forEach((type): 
                      
                        void
                      
                       =>
                      
                         {
      window.addEventListener(type, (): 
                      
                      
                        void
                      
                       =>
                      
                         {
        
                      
                      
                        //
                      
                      
                         断开此观察者的连接
                      
                      
                                po.disconnect();
      }, { once: 
                      
                      
                        true
                      
                      , capture: 
                      
                        true
                      
                      
                         });
    });
  }
                      
                    

  FMP 需要自行计算,才能得到,我采用了一套比较简单的规则.

  • 首先,通过 MutationObserver 监听每一次页面整体的 DOM 变化,触发 MutationObserver 的回调。
  • 然后在回调中,为每个 HTML 元素(不包括忽略的元素)打上标记,记录元素是在哪一次回调中增加的,并且用数组记录每一次的回调时间。
  • 接着在触发 load 事件时,先过滤掉首屏外和没有高度的元素,以及元素列表之间有包括关系的祖先元素,再计算各次变化时剩余元素的总分。
  • 最后在得到分数最大值后,从这些元素中挑选出最长的耗时,作为 FMP。

  为了能更好的描述出首屏的时间,将 LCP 和 FMP 两个时间做比较,取最长的那个时间.

  。

最后此篇关于shin-monitor源码分析的文章就讲到这里了,如果你想了解更多关于shin-monitor源码分析的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com