微信小程序數據統計和錯誤統計的實現

某些狀況下咱們須要對小程序某些用戶的行爲進行數據進行統計,好比統計某個頁面的UV,
PV等,統計某個功能的使用狀況等。好讓產品對於產品的整個功能有所瞭解。
在網頁裏,咱們不少人都用過谷歌統計,小程序裏也有一些第三方數據統計的庫, 好比騰訊的MTA等等。
可是,第三方的數據統計庫要麼功能太簡單,知足不了需求,要麼就是要收費。(留下了貧窮的淚水。)
等等,又不是你出錢,怕啥? 貴一點就貴一點呀。javascript

嗯,說的沒錯。可是,公司團隊內部想實現一套完整的本身的數據統計系統以知足本身的需求。因此,仍是沒有用第三方的。java

因此,具體要統計些啥?git

產品經理

  • 想知道用戶都是怎麼進入咱們的小程序的?
  • 用戶在咱們小程序裏那個頁面停留的時間最長?平均用戶停留時間是多少?
  • 想知道咱們最近開發的那個功能用的人多很少?
  • 想統計小程序裏的一些按鈕有多少用戶點擊了

開發本身

  • 老是很難復現用戶端出現的bug,
  • 要是能夠知道用戶端發生錯誤時,知道用戶當時的用的手機型號,微信版本,網絡環境,頁面參數,和錯誤信息就行了
  • 想知道咱們小程序啓動時間是多少?
  • 接口在用戶端的平均響應時間是多少ms? 哪些接口報錯了

針對產品經理的需求,咱們能夠知道,Ta想要的是就是數據統計要實現的功能。對於開發來講,咱們關注的更多就是錯誤統小程序性能這塊的東西。github

好,到這裏,咱們需求是明白了。就是要實現一套既能統計普通的埋點數據,也要能統計到小程序裏一些特殊觸發的事件,好比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;
         // 這個就是用戶在這個頁面的停留時間了
       },
   })

等等,這樣確實實現了需求,萬一產品要統計全部也面的停留時長? 那咱們豈不要在每個頁面都這樣寫一遍?有沒有更好的方法呢?app

好,接下來就是數據統計實現的要點了,即攔截微信原生事件,這樣能夠在某個特殊事件觸發時,作一些咱們統計的事情。同時,還要攔截微信發生網絡請求的方法,這樣能夠拿到網絡請求相關的數據,最後,爲了能統計到錯誤,還須要攔截微信發生錯誤的方法。

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吧

大家是如何實現小程序數據統計的呢? 歡迎在評論裏留言交流~~

相關文章
相關標籤/搜索