原因

这几天,公司开发的小程序有部分用户用户打开咨询列表页显示空白,我们查后台日志没有错误。最让人头疼的是,这种前端显示不正确,只是在某些机型上才出现,我们的测试人员无法复现该问题。无法复现,又没有错误日志记录,这种问题咋解决?

方案1

思前想后,我们需要将小程序端所有的错误日志记录下来,并上传给后台。因为小程序的特性,除了App.js,所有其他的逻辑都在Page函数中,我们只需要处理Page函数中所有的方法,结合之前的一篇《微信小程序重写Page函数,实现全局日志记录》,我们可以在那基础上继续改写Page函数,使用try catch捕获所有函数的异常,然后将捕获的异常信息上传到服务端。

微信小程序最近推出了小程序的实时日志,结合小程序的实时日志后,我们不需要写相关的后端代码,可将异常信息上传到小程序的平台。(点击链接可查看实时日志详情和使用方式,这里不在多描述)

function init(_data) {
  data = _data
  // 重写page函数,增加阿里云监控
  let oldPage = Page
  Page = function(obj) {
    // 对Page的传参obj对象的所有函数进行异常捕捉
    for (let key in obj) {
      let oldFunction = obj[key]
      if (typeof oldFunction === 'function') {
        obj[key] = function() {
          try {
            // 日志埋点
            if (['onShow', 'onHide'].includes(key)) {
              log.info('执行了' + this.__route__ + '的' + key + '方法')
              key === 'onShow' ? enterPageLog() : leavePageLog()
            }
            return oldFunction.call(this, ...arguments)
          } catch (e) {
            // 上传信息到小程序实时日志
            var log = wx.getRealtimeLogManager ? wx.getRealtimeLogManager() : null
            log || log.error.call(log, '发生了错误!页面:' + this.__route__ + ',方法:' + key + ',错误信息:' + e.message)
            throw e
          }
        }
      }
    }
    return oldPage(Monitor.hookPage(obj))
  }
}

这样就通过实时日志功能上传了错误日志到微信小程序的管理后台,通过https://mp.weixin.qq.com登录小程序管理端,就能看到实时日志,通过openid筛选就能查看到对应用户的错误日志,从而排查问题了。

方案2

但微信小程序的错误日志感觉不全,微信那边应该是做了流控或是采样,有些错误没有抓到,造成排查问题的困难。

还是自己动手吧,将错误信息上传到后端。代码如下,只修改了上面上传日志的那一部分

function init(_data) {
  data = _data
  // 重写page函数,增加阿里云监控
  let oldPage = Page
  Page = function(obj) {
    // 对Page的传参obj对象的所有函数进行异常捕捉
    for (let key in obj) {
      let oldFunction = obj[key]
      if (typeof oldFunction === 'function') {
        obj[key] = function() {
          try {
            // 日志埋点
            if (['onShow', 'onHide'].includes(key)) {
              log.info('执行了' + this.__route__ + '的' + key + '方法')
              key === 'onShow' ? enterPageLog() : leavePageLog()
            }
            return oldFunction.call(this, ...arguments)
          } catch (e) {
            const errInfo = '发生了错误!页面:' + this.__route__ + ',方法:' + key + ',错误信息:' + e.message
            console.error(errInfo)
            // 上传信息到服务端
            wx.request({
              url: host + '/addWebLog',
              method: 'POST',
              data: {
                errInfo: errInfo
              }
            })
            throw e
          }
        }
      }
    }
    return oldPage(Monitor.hookPage(obj))
  }
}

上传到服务器的错误信息包含当前用户、报错页面、报错方法和错误信息,这样我们在服务端就能查看到前端的异常了。

后端处理

新建一个controller来接收这个请求,用于接收前端上传的数据。

@RestController
public class LogController {
    private Logger logger = LoggerFactory.getLogger(LogController.class);
    private LinkedBlockingQueue<WebLog> queue;
    private Integer Queue_Capacity = 1000; //队列的容量
    @Autowired
    private JdbcTemplate jdbcTemplate;
    @PostConstruct
    public void init() {
        queue = new LinkedBlockingQueue<>(Queue_Capacity);
        Thread thread = new Thread(() -> {
            while (true) {
                try {
                    // 从队列末尾取,取不到则阻塞
                    WebLog webLog = queue.take();
                    logger.info("前端出错:" + JSONObject.toJSONString(webLog));
                    // 数据库操作
                    jdbcTemplate.update("insert into web_log(err_info) values(?)", webLog.getErrInfo());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread.start();
    }
    @PostMapping("/addWebLog")
    public Mono<Void> addWebLog(WebLog webLog) {
        // 到达capacity则抛出异常
        try {
            queue.add(webLog);
        } catch (IllegalStateException e) {
            System.out.println("队列达到上限:" + queue.size());
        }
        return Mono.empty();
    }
    public class WebLog {
        private String errInfo;
        public String getErrInfo() {
            return errInfo;
        }
        public void setErrInfo(String errInfo) {
            this.errInfo = errInfo;
        }
    }
}

为了防止某个功能有bug、大批量出错造成服务器压力,这里添加了一个队列来异步处理请求。Controller收到请求只是将请求丢到队列里,另外一个线程中队列在一个一个取出请求进行处理。队列长度为1000,超过1000的请求直接丢弃。

结语

微信虽然提供了小程序实时日志

其实微信提供了一种错误查询,在微信公众平台小程序可以查看到,但是和小程序实时日志一样有点不好用,我感觉这俩都是对错误进行采样,不是抓取全部错误。例如:开头我描述的那个错误就没找到。

还是自己动手,重写Page函数,catch所有异常,将错误上传给后台比较好排查问题。