微信小程序数据统计和错误统计的实现

某些情况下我们需要对小程序某些用户的行为进行数据进行统计,比如统计某个页面的UV,
PV等,统计某个功能的使用情况等。好让产品对于产品的整个功能有所了解。
在网页里,我们很多人都用过谷歌统计,小程序里也有一些第三方数据统计的库, 比如腾讯的MTA等等。
但是,第三方的数据统计库要么功能太简单,满足不了需求,要么就是要收费。(留下了贫穷的泪水。)
等等,又不是你出钱,怕啥? 贵一点就贵一点呀。

嗯,说的没错。但是,公司团队内部想实现一套完整的自己的数据统计系统以满足自己的需求。所以,还是没有用第三方的。

所以,具体要统计些啥?

产品经理

  • 想知道用户都是怎么进入我们的小程序的?
  • 用户在我们小程序里那个页面停留的时间最长?平均用户停留时间是多少?
  • 想知道我们最近开发的那个功能用的人多不多?
  • 想统计小程序里的一些按钮有多少用户点击了

开发自己

  • 总是很难复现用户端出现的bug,
  • 要是可以知道用户端发生错误时,知道用户当时的用的手机型号,微信版本,网络环境,页面参数,和错误信息就好了
  • 想知道我们小程序启动时间是多少?
  • 接口在用户端的平均响应时间是多少ms? 哪些接口报错了

针对产品经理的需求,我们可以知道,Ta想要的是就是数据统计要实现的功能。对于开发来说,我们关注的更多就是错误统小程序性能这块的东西。

好,到这里,我们需求是明白了。就是要实现一套既能统计普通的埋点数据,也要能统计到小程序里一些特殊触发的事件,比如appLaunch, appHide 等,还要可以统计错误。

好,那先来看看如何实现产品的需求吧

用户进入小程序可以在 小程序 onLaunch 回调里拿到参数 的scene 值,这样就可以知道用户是怎么进入小程序的了。小case, 难不到我。

嗯,第一个需求实现了,那如何统计第二个呢?如何统计某个页面的停留时间呢?

这也难不倒我,用户在进入页面时会触发onShow 事件, 同样,在离开页面(或者切后台时)会触发onHide事件,我只需要在onShow里记录一下时间,同时在onHide 里也记录一下时间,把两个时间一减就可以了。

Page({
       data: {
        beginTime: 0,
        endTime: 0
       },
       onShow: function() {
         // Do something when page show.
         this.setData({
           beginTime:  new Date().getTime()
         })
       },
       onHide: function() {
         // Do something when page hide.
         let stayTime = new Date().getTime() - this.beginTime;
         // 这个就是用户在这个页面的停留时间了
       },
   })

等等,这样确实实现了需求,万一产品要统计所有也面的停留时长? 那我们岂不要在每一个页面都这样写一遍?有没有更好的方法呢?

好,接下来就是数据统计实现的要点了,即拦截微信原生事件,这样可以在某个特殊事件触发时,做一些我们统计的事情。同时,还要拦截微信发生网络请求的方法,这样可以拿到网络请求相关的数据,最后,为了能统计到错误,还需要拦截微信发生错误的方法。

1.特殊事件的监听

App(Object object)

注册小程序。接受一个 Object 参数,其指定小程序的生命周期回调等。

App() 必须在 app.js 中调用,必须调用且只能调用一次。不然会出现无法预期的后果。

  • 拦截全局的事件:
  • 下面是小程序官方文档对于App 注册方法的文档:
App({
  onLaunch (options) {
    // Do something initial when launch.
  },
  onShow (options) {
    // Do something when show.
  },
  onHide () {
    // Do something when hide.
  },
  onError (msg) {
    console.log(msg)
  },
  globalData: 'I am global data'
})

假如我们要在小程序onLaunch 时打印一句hello Word,我们有哪些方法实现?

方法1:

直接写在onLaunch方法里

onLaunch (options) {
     console.log('hello World')
  }

方法2:

使用 monkey patch方法 猴子补丁(monkey patch)

猴子补丁主要有以下几个用处:

  1. 在运行时替换方法、属性等
  2. 在不修改第三方代码的情况下增加原来不支持的功能
  3. 在运行时为内存中的对象增加patch而不是在磁盘的源代码中增加

举个栗子,假如我们在console.log 方法里都先打印出当前的时间戳,我们可以这样:

var oldLog = console.log
console.log = function() {
  oldLog.call(this, new Date().getTime())
  oldLog.apply(this, arguments)
}

同理,我们针对onLaunch 进行猴子补丁

var oldAp = App
App = function(options) {
  var oldOnLaunch = options.onLaunch
  options['onLaunch'] = function(t) {
    // 做一些我们自己想做的事情
    console.log('hello word....')
    // 调用原来的onLaunch 方法
    oldOnLaunch.call(this, t)
  }
  
  // 调用原来的App 方法
  oldApp(options)
  
  // 想像一下,小程序内部调用onLaunch 方法应该是这样子的:
  options.onLaunch(params)
}

// 问题,有的时候,我们可能没有注册某一个事件,比如页面的onShow, 所有,我们在替换的时候还需要判断一下参数是否传了对应的方法
Page({
  onLoad (options) {},
  onHide (options) {}
})

// 针对这种情况,我们需要这样写
var oldPage = Page
Page = function(options) {
  if (options['onShow']) {
    // 如过有注册onShow 这个回调
    var oldOnShow = options.onShow
    // onShow 方法调用时都是 传了一个对象
    options['onShow'] = function(t) {
      // doSomething()
      oldOnShow.call(this, t)
    }
  }
  // 调用原来的Page 方法。
  oldPage.apply(null, [].slice.call(arguments))
  // 注意: 下面这两种写都会报错: VM23356:1 Options is not object: {"0":{}} in pages/Badge.js 问题具体原因暂时未找到。
  // oldPage.call(null, arguments)
  // oldPage(arguments)
}

通过上面的方法,我们可以拦截了 App 方法注册的一些全局方法,比如 onLaunch , onShow, onHide, 和Page 注册的事件如 onShow, onHide, onLoad, onPullDownRefresh, 等页面注册事件。

2.网络请求的监听

思路: 拦截微信的请求事件。

let Request = {
      request: function (e) {
        let success = e[0].success,
          fail = e[0].fail,
          beginTime = smaUtils.getTime(),
          endTime = 0
        // 拦截请求成功方法
        e[0].success = function () {
          endTime = smaUtils.getTime()
          const performance = {
            type: constMap.performance,
            event: eventMap.wxRequest,
            url: e[0].url,
            status: arguments[0].statusCode,
            begin: beginTime,
            end: endTime,
            total: endTime - beginTime
          }
          smaUtils.logInfo('success performance:', performance)
          // 这里做上报的事情
          // SMA.performanceReport(performance)
          success && success.apply(this, [].slice.call(arguments))
        }
        // 拦截请求失败方法
        e[0].fail = function () {
          endTime = smaUtils.getTime()
          const performance = {
            type: constMap.performance,
            event: eventMap.wxRequest,
            url: e[0].url,
            status: arguments[0].statusCode,
            begin: beginTime,
            end: endTime,
            total: endTime - beginTime
          }
          smaUtils.logInfo('fail performance:', performance)
          // 这里做上报的事情
          // SMA.performanceReport(performance)
          fail && fail.apply(this, [].slice.call(arguments))
        }
      },
   }
 
 
    // 替换微信相关属性
    let oldWx = wx,
      newWx = {}
    for (var p in wx) {
      if (Request[p]) {
        let p2 = p.toString()
        newWx[p2] = function () {
          Request[p2](arguments)
          // 调用原来的wx.request 方法
          oldWx[p2].apply(oldWx, [].slice.call(arguments))
        }
      } else {
        newWx[p] = oldWx[p]
      }
    }
    // eslint-disable-next-line
    wx = newWx

疑惑:为什么要使用替换整个wx对象的方法呢? 不直接用我们的request 方法 替换 wx.request 方法

var oldRequest = wx.request
wx.request = function(e) {
  // doSomething();
  console.log('请求拦截操作...')
  oldRequest.call(this, e); // 调用老的request方法
}
// 结果报错了:
//  TypeError: Cannot set property request of [object Object] which has only a getter

3.错误的监听

3.1 拦截App里注册的 onError事件

var oldAp = App
App = function(options) {
  var oldOnError = options.onErrr
  options['onErrr'] = function(t) {
    // 做一些我们自己想做的事情
    console.log('统计错误....', t)
    // 调用原来的onLaunch 方法
    oldOnError.call(this, t)
  }
  
  // 调用原来的App 方法
  oldApp(options)
}

3.2 拦截 conole.error

console.error = function() {
      var e = [].slice.call(arguments)
      if (!e.length) { return true }
      const currRoute = smaUtils.getPagePath()
      // 统计错误事件
      // SMA.errorReport({event: eventMap.onError, route: currRoute, errrMsg: arguments[0]})
      smaUtils.logInfo('捕捉到error 事件,', e)
      oldError.apply(console, e)
  }

至此,我们已经有能力在小程序发起请求时,发生错误时,生命周期或者特殊函数回调时,我们都能在里面做一些我们想要的数据统计功能了。

说了这么多大家估计也看累了。鉴于篇幅,具体的代码就不在这里贴了。

最终实现的数据统计模块大致实现了以下功能:

  • 普通埋点信息上报功能
  • 错误信息上报功能
  • 性能数据上报功能
  • 具体的上报时机支持配置
  • 支持指定网络环境上报
  • 支持统计数据缓存到微信本地功能

整个统计代码的配置文件如下:

const wxaConfig = {
  project: 'myMiniProgram', // 项目名称
  trackUrl: 'https://youhost.com/batch', // 后台数据统计接口
  errorUrl: 'https://youhost.com/batch',  // 后台错误上报接口
  performanceUrl: 'https://youhost.com/batch', // 后台性能上报接口
  version: '0.1',
  prefix: '_wxa_',
  priority: ['track', 'performance', 'error'], // 发送请求的优先级,发送时,会依次发送
  useStorage: true, // 是否开启storage缓存
  debug: false, // 是否开启调试(显示log)
  autoTrack: true, // 自动上报 onShow, onHide, 分享等 内置事件
  errorReport: false, // 是否开启错误上报
  performanceReport: false, // 接口性能上报
  maxReportNum: 20, // 当次上报最大条数
  intervalTime: 15,  // 定时上报的时间间隔,单位 s, 仅当开启了定时上报有效。
  networkList: ['wifi', '4g', '3g'], // 允许上报的网络环境
  opportunity: 'pageHide' // pageHide、appHide、realTime(实时上报)、timing(定时上报) 上报的时机,四选一
}
export default wxaConfig

具体上报时,上报的数据结构大致长这样:
微信小程序数据统计和错误统计的实现

项目已传到GitHub -> GitHub传送门-wxa


如果这篇文章帮到你了,觉得不错的话来点个Star吧

你们是如何实现小程序数据统计的呢? 欢迎在评论里留言交流~~