监控 SDK 的作用是收集客户端产生的日志,如何全面的收集日志是本篇讨论的重点。监控日志可以分为 3 类,异常日志、正常日志、性能日志,每次日志上报时还会上报 1 类通用的信息:

日志类型二级分类说明
异常日志JS错误、Server/Native 接口异常、资源下载异常监控关键,需实时上报
正常日志用户行为、log信息、路由信息日志量大,由用户主动上报
性能日志首屏时间、耗时明细、PV/UV上报一次,同时统计PV/UV
通用信息项目标识、用户标识、环境信息其他 3 类日志附加的通用信息

异常日志

JS 错误是最常见的异常日志,但并不是所有异常都是 JS 错误。接下来会介绍可能被大家忽略的但对线上问题定位很重要的三类异常的捕获方法,包括

  1. Server 接口异常的捕获方法
  2. Native 接口异常的捕获方法
  3. 资源下载异常的捕获方法

image-20210506170950355

如何收集 Server 接口异常?

在第一篇中,我们顺带介绍过通过 onerrorunhandledrejection 收集全局 JS 错误。但是在我们的实际业务中,其实是一些错误或异常不会抛到全局,也就不会被 onerrorunhandledrejection 收集到。比如,Server 接口异常,就不会被抛到全局。

const serverAPI = '58.com/api'
const response = await fetch(serverAPI)

if(response.status < 400) {
  setState(() => ({data: response.json()}))   
} else {
  // 手动上报异常
  errorReport({serverAPI, status: response.status})
}

else 这块代码,有些业务同学可能会不写。如果没有手动上报,这些 Server 异常就会被忽略,但是要求所有业务同学将每个 Server 异常都手动上报,也又不现实。怎么办呢?在监控 SDK 中统一上报。

重写fetch方法

Server 接口异常的捕获方法,就是重写 window.fetch

const originalFetch = window.fetch

window.fetch = function wrapperFetch(...args) {
  return originalFetch.apply(window, args).then(
      (response) => {  
           // 自动上报异常
          if (response.status >= 400) {
              errorReport({ serverApi: args[0], status: response.status })
          }
          return response;
      })
}

使用类似的方法:

  • 我们也可以 wrapper XMLHttpRequest,捕获 status >= 400 的情况。
  • 捕获 fetch 走到 catch 中的情况。
  • 捕获 fetch 超时的情况。

绕过Hybrid SDK与Native交互

我们再往前想一步,调用 Server 接口异常不会被抛到全局,Native 接口异常也不会被抛到全局。Native 接口异常,也可以由监控 SDK 专门捕获上传。思路和 wrapperFetch 方法思路类似,我们可以 wrapperHybrid

这里先介绍一下业务怎么使用 Hybrid SDK 手动上报。接下来再说怎么自动上报。

const request = {action: 'getUser', params: 'useName'}
const response = await hybrid.action(request)
// 手动上报
if(response.code === 1) {
  errorReport({ ...request, code: response.code })
}

但是我们有些老页面,一些业务自定义的交互协议,业务直接通过协议就和 Native 进行交互了,并没有使用通用的 Hybrid SDK。通过重写 Hybrid SDK 的方式就不生效了。怎么办呢?直接监听最底层的 JSBridge。

jsbridge('wbmain://hybrid/jsbridge?action=getUser&parmas=userName&callback=windowGetUser')

监听 JSBridge

JSBridge 包括请求和响应两个部分:

  • Web 请求 Native

    • 请求方式有多种,这些方式是由 Native 和 Web 事先进行约定的,也基本不会改动,因此比上层接口更为稳定。
    • 常用方法一:Web 调用全局方法,Native 重写该方法进行拦截。如重写拦截 window.prompt() 、 自定义全局函数。
    • 常用方法二:Web 通过 iframe 发送请求,Native 拦截 iframe 请求。
  • Native 响应 Web

    • Web 事先自定义全局函数,并把全局函数名通过 schemacallback 参数传过去
    • Native 调用该全局函数,Web 就能拿到 Native 响应的参数了

image-20210416190536263

以 iframe 方案为例,实现 Native 接口异常的捕获方法是监控 iframe src 的变化,如下:

// 创建监听
const observer = new MutationObserver((mutationList) => {
  mutationList.forEach((mutation) => {
    // 获取请求 的 schema
    const schema = mutation.target.src
    // 获取事先定义的全局函数名
    const callback = getURLSearchParams(schema,'callback');
    // 通过 wrapper 拦截响应,获取 native params ,当 params 异常时上报
    // params 异常,由规范决定,如 errorCode = 1
    window[callback] = wrapperCallback(window[callback])
  })
});
// 开始监听 iframe src 变化
observer.observe(iframe, {attributes: true,attributeFilter: ['src']});

使用监听的方式获取更多信息

另外 2 类异常的监听方式:

  • 跨域报错(左):使用 CDN 托管 JS 资源,JS 和 Web url 不在同一个域,跨域 JS 的报错浏览器只会给出 script error 的提示。script error 的提示并不是真的报错信息,也没有错误堆栈。通过重写监听函数,就能通过 try catch 捕获这些函数的报错了
  • 资源报错(右):加载资源报错不会被 window.onerror 捕获到,但是资源异常可以被 window.addEventListener('error') 捕获到。

image-20210127202810805

性能收集

性能收集常用的有两种方案,一种是业务自定义的性能标准,一种是业内通用的性能标准。

业务自定义的性能标准,需要入侵业务代码进行埋点收集,各个业务方案之间的都有一些差别,业务之间也不能横向对比,可用性较差。本篇重点讨论业内通用的性能标准,并创新性地实现了 FP、LCP 指标在 RN 端的收集方法。

image-20210506171943502

性能标准

另一种是 W3C 的性能标准,由于是浏览器的标准实现,可以不入侵业务代码,就能获取到页面加载耗时。W3C 的标准包括了 Navigation Timing 和 Navigation Timing Level 2,两份标准。第一份标准浏览器已经支持很好了,第二份新标准支持性差一些,但标准之间差别不大,收集的时候做下向下兼容即可。

性能收集包括两块

  • 耗时明细
  • 白屏时间

其中耗时明细的 Level 2 定义如下,所有耗时明细都需要收集:

图片 1

白屏时间统计

相比耗时明细,大家可能更关注白屏时间,也就是页面整体渲染耗时。白屏时间大家定义各不相同,这是因为大家业务场景不一样导致的,大致可以分为两种定义。

  • 一种是后端直接生成模板,白屏时间的定义为 DOMContentLoaded(DCL) 或 Load(L) 这种老指标
  • 一种是前端渲染页面,比如 React/Vue 为代表的 JS 生成页面,白屏时间的定义为 FP、FCP、LCP 这类新指标

图片 1

获取 FP、FCP、LCP 新指标的方式是 performance.getEntries() ,但是这些新指标并未纳入 W3C 规范,只是 Google 提出来的一种规范,目前兼容性比较差,可用性我们也在验证中。

FP (first paint)指的是第一个像素渲染到屏幕上所用的时间,FCP(first contentful paint)指的是第一个文字、图片等渲染到屏幕上所用的时间。在大多数场景下,二者基本相等。

LCP(largest contentful paint) 指的是显示面积最大的文字或图片渲染到屏幕上所用的时间。Google 提出的以 LCP 指标,已经可以在最新 Chrome 和 Android 机型上收集到了,大家设计 SDK 时不妨一起收集上来,作为一种辅助指标来衡量页面性能。

RN 收集 FP LCP 指标

FP、LCP 指标非常适合 React Web 页面,那么也同样适合 React Native 页面。业内并未有 FP、LCP 在 RN 上的实现,北斗 SDK 参考了 W3C 的 FP 和 LCP 标准,在 RN 中实现了 FP、LCP 指标的收集。

image-20210506160747671

实现思路如下,在 RN 中监听所有 Text Image 组件的 onLayout 事件,就能获取该组件的渲染时间点。这样 FP 第一个像素的渲染耗时,就可以算出来。 onLayout 中包含渲染元素的 widthheight ,就可以知道那个是渲染面积最大的文字或图片,从而计算出 LCP 的耗时。 伪代码如下:

class RNVitals {
  // 记录 FP
  private fp;
  
  // 记录 LCP
  private lcp;

  // 监听所有 Image 的 onLayout 事件
  private setWrapperImage() {}
  
  // 监听所有 Text 的 onLyaout 事件
  private setWrapperText () {
    const TextRender = Text.render
    Text.render =  (...args) => {
      const originImage = TextRender.apply(this, args);
      const { onLayout } = originImage.props ;
      return React.cloneElement(originImage, {
        onLayout: (event: LayoutChangeEvent) => {
          this.track(event)
          onLayout && onLayout(event)
        }
      });
    }
  }
  
  // 计算 FP、LCP 指标
  private track(event: LayoutChangeEvent): void {
    const size = event.height * event.width
    
    // 记录第一个元素渲染的时间戳
    if( this.fp == null) {
     this.fp = {
        size,
        layoutTime: Date.now() 
      }
    }
    
    // 记录&更新最大面积元素渲染的时间戳
    if (this.lcp.size < size) {
        this.lcp = {
          size,
          layoutTime: Date.now() 
        }
    }
  }
  
  // 如果用户点击了页面或 lcp 2s都没有更新,则进行性能上报
  publish report(){}
}

fitfish
1.6k 声望950 粉丝

前端第七年,写一个 RN 专栏。