V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
爱意满满的作品展示区。
alex1504
V2EX  ›  分享创造

使用 Typescript 开发小程序前端错误及性能监控 SDK

  •  
  •   alex1504 · 2021-06-08 10:12:22 +08:00 · 1858 次点击
    这是一个创建于 1265 天前的主题,其中的信息可能已经有所发展或是发生改变。

    poster

    前言

    上篇文章分析了web 端错误及性能监控 SDK的实现方式,本篇则聚焦于小程序。尽管微信小程序本身就自带了监控能力,但如果我们需要更完善的数据,比如在出错时展示更多的信息如函数调用栈、行为轨迹、缓慢请求等,则需要自己去监控收集。

    原来在开发这套前端监控 SDK 时,我将 web 和小程序的监控糅合在了一起,但后来发现平台差异使得同一个模块产生很多异化的逻辑,甚至在初始化的时候也要增加环境的判断,这种异化的处理降低了后续的可维护性,因此最终还是将 SDK 拆分成两套。

    在开发小程序监控 SDK 时,首先明确的是,SDK 需要提供什么样的能力,或者说帮助我们获取什么数据。由于要获取的数据上篇已提及,这里看看我们设计的小程序端的 SDK 需要提供什么能力。

    SDK 提供的能力

    基础监控

    • 错误监控:包括 js 、promise 、http
    • 性能监控:页面性能

    附加能力

    • 缓慢 HTTP 请求监控
    • 用户行为轨迹监控
    • 错误过滤、错误抽样(本篇不作介绍,SDK 已实现)

    SDK 在最终使用上依然采用基于事件订阅的方式,下面分析下这些能力在小程序端如何实现

    错误监控

    使用小程序生命周期提供了 onError,重写 Page 即可

    App = function (appOptions) {
      appHooks.forEach((methodName) => {
        const originMethod = appOptions[methodName];
    
        (appOptions as any)[methodName] = function (param: any) {
          const error = param as Error;
    
          if (methodName === "onError") {
            monitor.handleErrorEvent(TrackerEvents.jsError, error);
          }
          
          return originMethod && originMethod.call(this, param);
        };
      });
    
      return originApp(appOptions);
    };
    

    对于 promise 错误,小程序提供onUnhandledRejection,但官方文档指出此事件当前在安卓平台并不会生效,因此需要做一下 hack 处理,通过 console 的劫持进行判断

    export function rewriteConsole(monitor: Monitor) {
      for (const key of Object.keys(console)) {
        if (key in console) {
          const methodName = key as KeyofConsole;
    
          if (typeof console[methodName] !== "function") {
            continue;
          }
    
          if (!hackConsoleFn.includes(methodName)) {
            continue;
          }
    
          const originMethod = console[methodName];
          console[methodName] = function (...args: any[]) {
            /**
             * 若是安卓手机则在此捕获 unhandled promise rejection 错误
             */
            if (args[0] === "Unhandled promise rejection") {
              const error = args[1] as Error;
    
              monitor.getSystemInfo().then((res) => {
                const isNeedEmit = hackUnhandledRejectionPlatform.includes(
                  res.platform
                );
                if (isNeedEmit) {
                  monitor.handleErrorEvent(TrackerEvents.unHandleRejection, error);
                }
              });
            }
    
            originMethod.call(this, ...args);
          };
        }
      }
    }
    

    性能监控

    使用小程序提供的 performance api

    export function observePagePerformance(monitor: Monitor): void {
      const canIUse = wx.canIUse("Performance");
    
      if (monitor.performanceData.length) return;
      if (!canIUse) {
        return;
      }
    
      const performance = wx.getPerformance();
    
      const observer = performance.createObserver(
        (entryList: WechatMiniprogram.EntryList) => {
          const performanceData: PerformanceData = entryList.getEntries();
          // ,,,
        }
      );
      observer.observe({ entryTypes: ["render", "script"] });
    }
    
    

    HTTP 监控

    拦截 wx.request 取值,并且对 options.success 及 options.fail 进行重写

    export function interceptRequest(monitor: Monitor) {
      const originRequest = wx.request;
      Object.defineProperty(wx, "request", {
        configurable: false,
        enumerable: false,
        writable: false,
        value: function (options: WechatMiniprogram.RequestOption) {
          const originSuccess = options.success;
          const originFail = options.fail;
    
          options.success = function (...args) {
          };
    
          options.fail = function (...args) {
          };
    
          return originRequest.call(this, options);
        }
      });
    }
    

    用户行为轨迹

    在小程序中,用户行为轨迹我定义为以下类型,并用队列保存:

    • 函数调用
    • console 信息
    • http 请求
    • 页面元素点击
    • 埋点行为(自定义行为)
    export enum IBehaviorItemType {
      fn = "function",
      console = "console",
      http = "http",
      tap = "tap",
      custom = "custom"
    }
    

    我们通过重写 API 就能获取以上的信息

    • 函数调用:重写 App,重写 Page
    • console 信息:重写 console
    • http 请求:重写 wx.request
    • 页面元素点击:重写 Page

    值得注意的是,SDK 要监控页面元素点击仍需要我们做些手动工作。

    由于小程序不存在 dom,并不具备类似 web 提供window.addEventListener的能力,通过重写 Page 只是为了给 PageOptions 注入一个事件处理方法onElementTrack,因此在页面根节点需要对元素做事件绑定才能触达 SDK 收集。

    // index.wxml
    <view class="container" catchtap="onElementTrack"></view>
    

    自定义行为

    某些情况下,你需要收集特定地方的埋点行为,只需要在埋点处调用 pushBehavior 方法即可。

    public pushBehaviorItem(item: IBehaviorItem): IBehaviorItem {
        if (!item.type) {
          item.type = IBehaviorItemType.custom;
        }
    
        if (!item.time) {
          item.time = Date.now();
        }
    
        const { queueLimit } = this.$options.behavior;
    
        if (this.behavior.length >= queueLimit) {
          this.behavior.shift();
        }
    
        this.behavior.push(item);
    
        return item;
    }
    

    小程序 SDK 完整代码实现:欢迎 star 、fork 、issue 。

    https://github.com/alex1504/femonitor-wx

    component
        1
    component  
       2021-06-08 14:11:24 +08:00
    star 支持一下
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   982 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 22:57 · PVG 06:57 · LAX 14:57 · JFK 17:57
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.