微信小程序接入全局日志

871 阅读4分钟

前言

我们在开发小程序的时候,明明在本地编辑器上好好的,但是总会有那么坑人的BUG在线上出错。 怎么搞,让我一个一个去看代码好烦啊,这个时候日志的重要性就出来了,它可以帮助我们快速定位线上BUG出现的问题,及时改进。

本文通过微信自带的日志方法二次封装

实现

1、基础

封装一个简单的日志对象

  • info、warn、error是上报日志的等级
  • setFilterMsg、addFilterMsg可以添加过滤条件,方便后续过滤日志
  • BaseInfo可以收集基础的信息,当前页面,页面栈等,可以根据自己的业务添加信息
  • Response可以收集网络信息,添加到请求和响应拦截器里。本文的网络是基于axios封装的,可根据自己的需求修改
const log = wx.getRealtimeLogManager ? wx.getRealtimeLogManager() : null
const mylog = {
  info() {
    if (!log) return
    log.info.apply(log, arguments)
  },
  warn() {
    if (!log) return
    log.warn.apply(log, arguments)
  },
  error() {
    if (!log) return
    log.error.apply(log, arguments)
  },
  setFilterMsg(msg) {
    if (!log || !log.setFilterMsg) return
    if (typeof msg !== 'string') return
    log.setFilterMsg(msg)
  },
  addFilterMsg(msg) {
    if (!log || !log.addFilterMsg) return
    if (typeof msg !== 'string') return
    log.addFilterMsg(msg)
  },
  /**
   * @description: 
   * @param {*} mode 用于区分是是哪里收集的信息
   * @return {*}
   */  
  BaseInfo(mode='') {  // 收集基础信息
    let pages = getCurrentPages() ? getCurrentPages() : ''
    let pageStack = {}
    if (pages.length >= 1) {
      pages.forEach((item, index) => {
        pageStack[index] = item.route
      })
    }
    let currentPage = pages.length ? pages[pages.length - 1] : ''
    let path = currentPage.route ? currentPage.route : ''
    const BaseInfo = {  // 基础信息
      Path: path ? path : '',
      pageStack,
      mode
    }
    return BaseInfo
  },
  Response(data) {  // 收集网路信息
    return {
      Header: data.config ? data.config.headers : '',
      ResponseData: data.data ? data.data : '',
      XTraceId: data.headers['X-Trace-Id'] ? data.headers['X-Trace-Id'] : '',
      status: data.status ? data.status : '',
      Url: data.request ? data.request.url : '',
      requestData: data.request ? data.request.data : '',
      requestMethod: data.request ? data.request.method : ''
    }
  },
  /**
   * 
   * @param {*} data.data 待捕获对象
   * @param {*} data.conditions 捕获条件
   * @param {*} mode 捕获模式,true:存在即调用回调,反之则不存在调用
   * @param {*} fun 回调
   */
  catchError(data={data:null,conditions: null}, fun = ()=>{},  mode=true) {  // 错误捕获器
    if(mode ? (data.data.indexOf(data.conditions) >= 0) : (data.data.indexOf(data.conditions) < 0)){
      fun()
    }
  }
}

2、重写Page方法

重写Page方法可以使得我们不必每个页面都添加一次日志。 并且可以知道用户当前页面的层级(栈),页面停留时间等,定位BUG的时候模拟用户行为等

import mylog from './mylog'  // 这个是1中上文所封装的日志方法,通过export default暴露出去
const resetPage = (_data) => {
  data = _data
  let oldPage = Page
  // 写function,怕有的基础库不兼容
  Page = function (obj) {
    let oldOnShow = obj.onShow
    let oldOnHide = obj.onHide
    let oldOnLoad = obj.onLoad
    let oldOnUnload = obj.onUnload
    obj.timer = null  // unload的定时器
    obj.timer2 = null // onhide的定时器
    obj.timer3 = null // onshow的定时器
    obj.pageTime = 0  // unload的计数器
    obj.pageTime2 = 0 // onhide的计数器
    obj.pageTime3 = 0 // onshow的计数器
    // 不可以直接写oldOnShow()或者oldOnHide(),否则会报错没有this。这里的this在Page构造函数实例化的时候才会指定
    obj.onShow = function () {
      const BaseInfo = mylog.BaseInfo('onShow')
      mylog.setFilterMsg('onShow')
      mylog.info(({BaseInfo,
        loadTime: {
          message: '该时间是最后一个pageStack停留的时间',
          mode: 'onshow',
          time: this.pageTime
        }
      }))
      oldOnShow.call(this)
    }
    obj.onHide = function () {
      const BaseInfo = mylog.BaseInfo('onHide')
      mylog.setFilterMsg(BaseInfo.userinfo.userName)
      mylog.addFilterMsg('onHide')
      mylog.info({BaseInfo,
        loadTime: {
          message: '该时间是最后一个pageStack停留的时间',
          mode: 'onHide',
          time: this.pageTime
        }
      })
      oldOnHide.call(this)
    }
    obj.onLoad = function (options) {
      const BaseInfo = mylog.BaseInfo('onLoad')
      mylog.setFilterMsg('onLoad')
      mylog.info({BaseInfo, options})
      // 记录用户行为,页面停留时间
      obj.timer = setInterval(()=>{
        this.pageTime += 1
      }, 1000)
      obj.timer2 = setInterval(()=>{
        this.pageTime2 += 1
      }, 1000)
      obj.timer3 = setInterval(()=>{
        this.pageTime3 += 1
      }, 1000)
      oldOnLoad.call(this, options)
    }
    obj.onUnload = function() {
      clearInterval(this.timer)
      clearInterval(this.timer2)
      clearInterval(this.timer3)
      const BaseInfo = mylog.BaseInfo('loadTime')
      mylog.setFilterMsg('loadTime')
      mylog.info({BaseInfo,
        loadTime: {
          message: '该时间是最后一个pageStack停留的时间',
          mode: 'onUnload',
          time: this.pageTime
        }
      })
      this.pageTime = 0
      this.pageTime2 = 0
      this.pageTime3 = 0
      oldOnUnload.call(this)
    }
    return oldPage(obj)
  }
}

3、日志的使用

3.1、应用重写的Page

app.js中

import reSetPage from './reSetPage' // 这个就是上文2中所重写的Page。通过export default暴露出去
import mylog from './mylog'  // 这个是1中上文所封装的日志方法,通过export default暴露出去
App({
  data: {},
  onLaunch() {
      resetPage(this.data)
  },
  onShow() {},
  onError(error) {
      const BaseInfo = mylog.BaseInfo('weChatError')
      mylog.addFilterMsg('wechatError')
      mylog.error({ BaseInfo, errorMsg: error })
  },
  globalData: {}
})

3.2、在网络请求中,加入日志

在请求拦截器和响应拦截器中加入日志

本文只在响应拦截器中添加,如请求拦截器中有需要,可自行添加

这边以axios为例

import axios from 'axios'
import mylog from '../log'  // 这个是1中上文所封装的日志方法,通过export default暴露出去

...... // 这边是你自己封装的网络请求类或者方法,本文不做解释
// 注:axios要在小程序中使用,需要使用axios-miniprogram-adapter,有需要可自行搜索使用方法

// 响应拦截器
instance.interceptors.response.use(
  response => {
    const BaseInfo = mylog.BaseInfo('netWrokSuccess')
    const successResponse = mylog.Response(response)
    if (JSON.stringify(response.data).length <= 700) {  // 微信后台限制日志长度
      mylog.setFilterMsg('networkSuccess')
      mylog.info({ successResponse, BaseInfo })
    } else {
      mylog.setFilterMsg('networkSuccess')
      delete successResponse.ResponseData
      successResponse.ResponseData = JSON.stringify(response.data).slice(0, 700)
      mylog.info({ successResponse, BaseInfo })
    }
    //否则的话抛出错误
    if (response.status === 200) {
      return Promise.resolve(response)
    } else {
      return Promise.reject(response)
    }
  },
  error => {
    if (error.response) {
      const BaseInfo = mylog.BaseInfo('netWorkError')
      const errResponse = mylog.Response(error.response)
      mylog.setFilterMsg('networkError')
      mylog.addFilterMsg(error.response.status)
      mylog.error({ errResponse, BaseInfo })
    }
    return Promise.reject(error)
  }
)

4、可能出现的问题

  • 使用重写Page的方法添加日志的话,会在不同的页面报错。报错内容大致就是onHide、onShow等生命周期无法cell。这是因为你报错的页面没有实例化这个生命周期。需要你手动在这个页面添加这个生命周期即可,不需要向里面写什么东西
  • 查看日志的时候,腾讯的日志并没有帮你格式化,看起来很不方便。写了个油猴的插件,这个是基础的格式化,可以根据自己的业务进行二次修改

5、查看日志

可以在小程序的后台-开发-开发管理里查看

image.png