RN 线上错误搜集

最近项目组收到用户反馈无法进入收到一个 RN 页面,怀疑是客户端加载页面时发生异常,但也不排除是前端代码异常导致的。

为此打算接入早先别组同学搭建的 Sentry,以便于观测与搜集前端异常。不过现有的 sdk 是基于 h5 与 node 的,rn 这方面没有对应的 sdk。sentry 是有开源的 rn sdk,但由于项目特殊性,与客户端耦合较多,因此简单自行封装一个 sdk 作为过渡,等后续负责 sentry 的同学提供统一的 RN sdk 再替换掉,这里记录一下封装的思路。

原理

公司的 sentry 项目不是我这里负责的,不过大概的原理在封装 sdk 的过程中也猜的差不多了。

  1. client 捕获到异常
  2. client 通过请求 / 图片将异常信息发送到日记统计平台
  3. 定时从日志平台拉取 sentry 异常相关的日志
  4. 将日志整理成 sentry 识别的字段,存入数据库
  5. 用户在 sentry 的平台查看 issue 即可

过程

  1. 提供一个 HOC 用于捕获未处理的异常,就叫 withErrorBoundary 吧,这个与 React 捕获异常其实都差不多

    import React from 'react';
    import { report } from '@hooks/useSentry';
    
    /** 捕获页面异常并上报到 sentry */
    class ErrorBoundary extends React.Component {
        state: { error?: any } = {};
        static getDerivedStateFromError(error: any) {
            return { error };
        }
    
        componentDidCatch(error: any) {
            report(error);
        }
    
        render() {
            return this.state.error ? (
              <div>oops!</div>
            ) : (
                this.props.children
            );
        }
    }
    
    export const withErrorBoundary =(WrappedComponent) => (props) => (<ErrorBoundary {...props}>
    			<WrappedComponent {...props} />
    		</ErrorBoundary>)
    
    export default ErrorBoundary;

    给需要的页面包裹上 withErrorBoundary(Page) 即可

  2. 实现 report,实际上 report 无非就是将信息整理成需要的格式,然后发送到日志平台。

    那么需要的信息包括什么呢?

    首先肯定是错误栈,这里通过 ErrorBoundary 里的 error 能获取 error.message 以及错误栈

    {
      column: 2410, 
      line: 1100, 
      componentStack: 
          in p
         // ...
      isComponentError: True, 
      sourceURL: ...
    }

    其次是错误发生的时间 / 版本信息 / 机型 等辅助信息

    最后是业务相关的信息,通常包括 用户 / 页面路径

    sentry 管这个叫 event ,具体的参数可以参考sentry 的开发文档

    那么当我们整理好所需的信息就需要上报上去,顺带说一下前端常用的手段是用 1x1 像素的透明 gif 图片,也叫 image beacon。通过 new Image 构造一个图片请求,主要原因是

    1. 对比 ajax,不会有跨域问题
    1. 对比 js 标签,不阻塞页面加载
    1. 对比 png / jpg,这种图片体积最小
  3. source map,线上的代码都是压缩过的,报错的信息就很模糊,这个时候我们需要借助 source map 来还原源码的报错位置。rn 也是有提供 source map 的,在执行 react-native bundle 的时候添加参数 --bundle-output [sourcemap-name].map 即可输出对应平台的 source map,通过下述代码即可定位源码报错位置

    const sourceMap = require('source-map')
    const fs = require('fs-extra')
    
    // 读取 source map 为 json
    const rawSourceMapJsonData = await fs.readJson('[sourcemap-name].map')
    const consumer = await new sourceMap.SourceMapConsumer(rawSourceMapJsonData);
    // line 与 column 指压缩后的报错位置
    console.log(consumer.originalPositionFor({ line, column }));

    当然 sentry 也提供了 source map 的配置,这块还没尝试,可自行查看文档 https://docs.sentry.io/platforms/react-native/sourcemaps/

    不过遗憾的是 component stack 似乎是没有办法还原出报错所在组件的位置,参考 https://github.com/facebook/create-react-app/issues/3753