微信小程序的操作日志收集模块

自动记录页面生命周期事件、用户点击事件等,并将这些日志数据存储到本地,在适当的时候(如APP_HIDE事件)上报到服务器

1、模块导入和常量定义

  • 导入moment库用于时间格式化。
  • 定义页面生命周期方法数组PAGE_LIFE_METHOD
  • 定义一些全局变量,如subscribersReleaseFlag(队列释放标识)、subscribers(操作日志存储队列)、cacheStorageOperLogData(缓存的操作日志数据)等。

2、页面事件控制(controlPageEvent)

这个函数用于包装页面的生命周期方法和自定义方法,以便在特定时机记录日志。

  • 遍历页面选项,对非生命周期方法(即自定义方法)进行包装,使其能够记录用户点击事件。
  • 重写页面的onLoadonShowonHideonUnloadonShareAppMessage方法,在原有逻辑前后插入日志记录。
  • 在页面生命周期事件中,通过saveOperLog函数记录相应的日志。

3、 组件事件控制(controlCommponentEvent)

类似页面事件控制,但针对组件。它遍历组件的方法,并对每个方法进行包装,以记录用户操作。

4、 事件代理(_proxyHooks)

这个函数是包装原始函数的核心,它返回一个新的函数,这个新函数在执行原始函数之前,会尝试记录操作日志。

  • 首先检查事件对象中是否包含businamebusidesc数据,如果有,则使用这些数据记录日志。
  • 如果没有,则通过getFunParam函数从原始函数的参数中提取busiNamebusiDesc,然后记录日志。
  • 最后执行原始函数并返回结果。

5、 获取函数参数(getFunParam)

这个函数通过将函数转换为字符串,然后解析字符串来获取函数的参数名和默认值,从而提取出busiNamebusiDesc。这种方法依赖于函数被转换为字符串后的特定格式,因此有一定的脆弱性。

6、 保存操作日志(saveOperLog)

这个函数构建操作日志对象,并根据条件将其直接保存或加入队列。

  • 构建操作日志对象,包括事件类型、参数、时间、页面路径、经纬度、业务名称和描述、用户ID等。
  • 如果当前允许保存日志(app.globalData.isCanSaveLog为真),则直接保存,否则加入队列。

7、 保存App操作日志(saveAppOperLog)

类似于saveOperLog,但专门用于App级别的事件(如启动、隐藏等)。在APP_LAUNCH事件中会记录更多系统信息。

8. 日志队列管理

  • addSaveLogSubscribe: 将日志对象加入队列,并在条件满足时触发队列释放。
  • subscribersRelease: 释放队列中的日志,逐个保存,并设置一个定时器来控制释放频率(每秒最多一次)。
  • saveLog: 将日志对象存储到本地缓存中,并在APP_HIDE事件时触发上报并清除本地缓存。

9、 补全openid(completionOpenid)

当获取到openid后,遍历本地缓存中的日志,为没有openid的日志记录补全openid。

js 复制代码
import moment from '../lib/moment.min'
// 页面生命周期方法
const PAGE_LIFE_METHOD = ['onLoad', 'onShow', 'onReady', 'onHide', 'onUnload', 'onPullDownRefresh', 'onReachBottom', 'onShareAppMessage', 'onShareTimeline', 'onAddToFavorites', 'onPageScroll', 'onResize', 'onTabItemTap', 'onSaveExitState'];
let subscribersReleaseFlag = true; // 队列释放标识
let subscribers = []; // 操作日志存储队列
let cacheStorageOperLogData = {}; // 缓存操作日志数据
let sysInfo = {};
let systemSettingInfo = {};
let appAuthorizeSettingInfo = {};

/**
 * 控制页面事件
 * @param {} oldPage 原始页面
 */
function controlPageEvent(options) {
  const {
    onLoad,
    onShow,
    onHide,
    onUnload,
    onShareAppMessage
  } = options;

  // // 获取要代理的页面生命周期方法
  for (const prop in options) {
    if (typeof options[prop] == 'function' && !PAGE_LIFE_METHOD.includes(prop)) {
      options[prop] = usePageClickEvent(options[prop], prop);
    }
  }

  let opts = {
    ...options,
    onLoad(pageOptions) {

      if (pageOptions.busiName && pageOptions.busiDesc) {
        saveOperLog('PAGE_LOAD', pageOptions, 'load' + pageOptions.busiName + 'Page', '打开' + pageOptions.busiDesc + '页面')
        this.data.busiName = pageOptions.busiName;
        this.data.busiDesc = pageOptions.busiDesc;
      } else if (this.data.busiName && this.data.busiDesc) {
        saveOperLog('PAGE_LOAD', pageOptions, 'load' + this.data.busiName + 'Page', '打开' + this.data.busiDesc + '页面')
      }

      onLoad && onLoad.call(this, pageOptions)
    },
    onShow(pageOptions) {
      if (this.data.busiName && this.data.busiDesc) {
        saveOperLog('PAGE_SHOW', "{}", 'show' + this.data.busiName + 'Page', '显示' + this.data.busiDesc + '页面')
      }
      onShow && onShow.call(this, pageOptions)
    },
    onHide() {
      if (this.data.busiName && this.data.busiDesc) {
        saveOperLog('PAGE_HIDE', "{}", 'hide' + this.data.busiName + 'Page', '关闭' + this.data.busiDesc + '页面')
      }
      onHide && onHide.call(this)
    },
    onUnload() {
      if (this.data.busiName && this.data.busiDesc) {
        saveOperLog('PAGE_UNLOAD', "{}", 'unload' + this.data.busiName + 'Page', '卸载' + this.data.busiDesc + '页面')
      }
      onUnload && onUnload.call(this)
    },
    onShareAppMessage(pageOptions) {
      // 分享开发工具上会执行onHide,取消后会执行onShow
      return onShareAppMessage && onShareAppMessage.call(this, pageOptions)
    },
  }

  return opts
}

/**
 * 控制组件事件
 * @param {} oldCommponent 原始组件 
 */
function controlCommponentEvent(options) {
  // 自定义事件监听
  let target = options.methods
  for (let prop in target) {
    // 需要保证是函数
    if (typeof target[prop] == 'function') {
      target[prop] = usePageClickEvent(target[prop]);
    }
  }
  return options;
}

const usePageClickEvent = (oldEvent, prop) => _proxyHooks(oldEvent, prop)

function _proxyHooks(fn = function () {}, prop) {
  return function () {

    if (arguments[0] && typeof arguments[0] == 'object' && arguments[0].type && arguments[0].currentTarget && arguments[0].currentTarget.dataset && arguments[0].currentTarget.dataset.businame && arguments[0].currentTarget.dataset.busidesc) {

      let param = JSON.parse(JSON.stringify(arguments[0].currentTarget.dataset));
      delete param.busidesc;
      delete param.businame;
      if (JSON.stringify(param) == "{}" && arguments[0].detail.value) {
        param = arguments[0].detail.value
      }
      saveOperLog('PAGE_CLICK', param, arguments[0].currentTarget.dataset.businame, arguments[0].currentTarget.dataset.busidesc)
    } else {
      const argsInfo = getFunParam(fn);
      if (argsInfo.busiName) {
        let param = {}
        if (arguments.length > 0) {
          if (typeof arguments[0] != "string" && arguments[0].detail && arguments[0].detail.value && JSON.stringify(arguments[0].detail.value) != "{}") {
            param = arguments[0].detail
          } else if (typeof arguments[0] != "string" && arguments[0].currentTarget && arguments[0].currentTarget.dataset && JSON.stringify(arguments[0].currentTarget.dataset) != '{}') {
            param = arguments[0].currentTarget.dataset
          } else {
            for (let i = 0; i < arguments.length; i++) {
              if (typeof arguments[i] == 'string' || (!arguments[i].type || arguments[i].type != 'tap')) {
                console.log(arguments[i])
                param['data'] = (param['data'] ? param['data'] + ',' : '') + JSON.stringify(arguments[i])
              }
            }
          }
        }
        saveOperLog('PAGE_CLICK', param, argsInfo.busiName, argsInfo.busiDesc)
      }
    }
    let originalResultData = fn.apply(this, arguments)
    return originalResultData;
  };
}

/**
 * 获取方法的desc参数
 */
function getFunParam(func) {
  const argsString = func.toString().match(/function\s.*?\(([^)]+)/) ? func.toString().match(/function\s.*?\(([^)]+)/)[1] : null;
  let resultbusiName = null;
  let resultbusiDesc = null;
  if (argsString) {
    argsString.split(',').forEach(arg => {
      const eqIndex = arg.indexOf('=');
      const hasDefaultValue = eqIndex !== -1;
      const name = arg.trim();
      const defaultValue = hasDefaultValue ? arg.substring(eqIndex + 1).trim() : undefined;
      if (name == 'busiName') {
        resultbusiName = defaultValue;
      }
      if (name == 'busiDesc') {
        resultbusiDesc = defaultValue;
      }
    });
  }
  if (!resultbusiName && !resultbusiDesc) {
    let funStr = func.toString().replace(/\s/g, "");
    let busiNameIdx = funStr.indexOf('varbusiName=');
    let busiDescIdx = funStr.indexOf('varbusiDesc=');
    if (busiNameIdx != -1 && busiDescIdx != -1) {
      resultbusiName = funStr.slice(funStr.indexOf(':', busiNameIdx) + 2, funStr.indexOf(';', busiNameIdx) - 1)
      resultbusiDesc = funStr.slice(funStr.indexOf(':', busiDescIdx) + 2, funStr.indexOf(';', busiDescIdx) - 1)
    }
  }

  return {
    busiName: resultbusiName,
    busiDesc: resultbusiDesc
  };
}

/**
 * 保存操作日志
 */
async function saveOperLog(event, param, busiName, busiDesc) {
  const app = getApp();
  let operLogObj = {
    event: event,
    param: param == "" || typeof param == 'string' ? param : JSON.stringify(param),
    eventTime: moment().format('YYYY-MM-DD HH:mm:ss'),
    pageUrl: getCurrentPages().length > 0 ? getCurrentPages()[getCurrentPages().length - 1].route : 'onLaunch',
    lng: app && app.globalData.userLongitude ? app.globalData.userLongitude : '',
    lat: app && app.globalData.userLatitude ? app.globalData.userLatitude : '',
    busiName: busiName,
    busiDesc: busiDesc,
    userId: wx.getStorageSync('storageMerchant') ? JSON.parse(wx.getStorageSync('storageMerchant')).id : '',
  }
  if (app && app.globalData.isCanSaveLog) {
    if (JSON.stringify(sysInfo) != JSON.stringify(app.globalData.sysInfo)) {
      operLogObj['systemInfo'] = JSON.stringify(app.globalData.sysInfo);
      sysInfo = app.globalData.sysInfo;
    }
    if (JSON.stringify(systemSettingInfo) != JSON.stringify(app.globalData.systemSettingInfo)) {
      operLogObj['systemSetting'] = JSON.stringify(app.globalData.systemSettingInfo);
      systemSettingInfo = app.globalData.systemSettingInfo;
    }
    if (JSON.stringify(appAuthorizeSettingInfo) != JSON.stringify(app.globalData.appAuthorizeSettingInfo)) {
      operLogObj['appAuthorizeSetting'] = JSON.stringify(app.globalData.appAuthorizeSettingInfo);
      appAuthorizeSettingInfo = app.globalData.appAuthorizeSettingInfo;
    }

    if (subscribers.length < 1) {
      saveLog(operLogObj);
    } else {
      addSaveLogSubscribe(operLogObj);
    }
  } else {
    addSaveLogSubscribe(operLogObj);
  }
}

/**
 * 保存App操作日志
 */
function saveAppOperLog(event, param, busiName, busiDesc, miniApp) {
  const app = !miniApp ? getApp() : miniApp;

  let pageUrl = getCurrentPages().length > 0 ? getCurrentPages()[getCurrentPages().length - 1].route : param.path
  let operLogObj = {
    event: event,
    param: JSON.stringify(param),
    eventTime: moment().format('YYYY-MM-DD HH:mm:ss'),
    pageUrl: pageUrl,
    lng: app.globalData.userLongitude ? app.globalData.userLongitude : '',
    lat: app.globalData.userLatitude ? app.globalData.userLatitude : '',
    busiName: busiName,
    busiDesc: busiDesc,
    userId: wx.getStorageSync('storageMerchant') ? JSON.parse(wx.getStorageSync('storageMerchant')).id : '',
  }
  if (event == 'APP_LAUNCH') {
    if (JSON.stringify(sysInfo) != JSON.stringify(app.globalData.sysInfo)) {
      operLogObj['systemInfo'] = JSON.stringify(app.globalData.sysInfo);
      sysInfo = app.globalData.sysInfo;
    }
    if (JSON.stringify(systemSettingInfo) != JSON.stringify(app.globalData.systemSettingInfo)) {
      operLogObj['systemSetting'] = JSON.stringify(app.globalData.systemSettingInfo);
      systemSettingInfo = app.globalData.systemSettingInfo;
    }
    if (JSON.stringify(appAuthorizeSettingInfo) != JSON.stringify(app.globalData.appAuthorizeSettingInfo)) {
      operLogObj['appAuthorizeSetting'] = JSON.stringify(app.globalData.appAuthorizeSettingInfo);
      appAuthorizeSettingInfo = app.globalData.appAuthorizeSettingInfo;
    }
    saveLog(operLogObj, event);
  } else {
    addSaveLogSubscribe(operLogObj, app);
  }
}

/**
 * 加入保存日志队列
 * @param {*} operLogObj 
 */
function addSaveLogSubscribe(operLogObj, miniApp) {
  const app = miniApp ? miniApp : getApp();
  subscribers.push(operLogObj)

  if (app && app.globalData.isCanSaveLog) {
    if ((JSON.stringify(cacheStorageOperLogData) != "{}" && cacheStorageOperLogData[app.globalData.sessionId].length > 0) || app.globalData.isAppHideBack) {
      subscribersRelease();
    }
  }
}

/**
 * 队列释放
 */
function subscribersRelease() {
  const app = getApp();

  if (app.globalData.isCanSaveLog) {
    if (subscribersReleaseFlag) {
      subscribersReleaseFlag = false;
      for (let i = 0; i < subscribers.length; i) {
        saveLog(subscribers[i]);
        subscribers.splice(i, 1)
      }
      setTimeout(() => {
        subscribersReleaseFlag = true;
        if (subscribers.length > 0) {
          subscribersRelease();
        }
      }, 1000)
    }
  }
}

/**
 * 保存日志
 * @param {*} operLogObj 
 */
function saveLog(operLogObj, type) {
  const app = getApp();
  operLogObj['sessionId'] = app.globalData.sessionId;
  operLogObj['launchParam'] = JSON.stringify(app.globalData.launchParam);
  operLogObj['openid'] = app.globalData.openid;
  operLogObj['sdkVersion'] = app.globalData.sdkVersion;

  if (type == 'APP_LAUNCH') {
    if (app.globalData.sdkVersion >= '2.20.1') {
      operLogObj['deviceInfo'] = JSON.stringify(app.globalData.deviceInfo);
      operLogObj['windowInfo'] = JSON.stringify(app.globalData.windowInfo);
      operLogObj['appBaseInfo'] = JSON.stringify(app.globalData.appBaseInfo);
      operLogObj['localIpAddress'] = JSON.stringify(app.globalData.localIpAddressInfo);
    }

    if (app.globalData.sdkVersion >= '1.9.6') {
      operLogObj['networkType'] = JSON.stringify(app.globalData.networkTypeInfo);
    }
  }

  if (JSON.stringify(cacheStorageOperLogData) == "{}") {
    cacheStorageOperLogData = wx.getStorageSync('localStorageOperLogData') ? JSON.parse(wx.getStorageSync('localStorageOperLogData')) : {};
  }

  if (cacheStorageOperLogData[app.globalData.sessionId] && cacheStorageOperLogData[app.globalData.sessionId].length > 0) {
    cacheStorageOperLogData[app.globalData.sessionId].push(operLogObj);
  } else {
    cacheStorageOperLogData[app.globalData.sessionId] = [operLogObj];
  }

  wx.setStorageSync('localStorageOperLogData', JSON.stringify(cacheStorageOperLogData))
  if (subscribers.length > 0) {
    subscribersRelease(); // 释放队列
  }
  if (operLogObj.event == 'APP_HIDE') {
    app.saveOpenerRecordApi(wx.getStorageSync('localStorageOperLogData'))
    wx.removeStorageSync('localStorageOperLogData')
    cacheStorageOperLogData = {};
  }
}

/**
 * 补全openid
 */
function completionOpenid(openid) {
  const app = getApp();
  if (cacheStorageOperLogData && cacheStorageOperLogData[app.globalData.sessionId]) {
    for (let i = 0; i < cacheStorageOperLogData[app.globalData.sessionId].length; i++) {
      if (!cacheStorageOperLogData[app.globalData.sessionId][i].openid) {
        cacheStorageOperLogData[app.globalData.sessionId][i]['openid'] = openid
      }
    }
    wx.setStorageSync('localStorageOperLogData', JSON.stringify(cacheStorageOperLogData))
  }
}

module.exports = {
  controlPageEvent,
  controlCommponentEvent,
  completionOpenid,
  saveAppOperLog,
  saveOperLog
}
相关推荐
CAD老兵4 小时前
打造高性能二维图纸渲染引擎系列(一):Batched Geometry 助你轻松渲染百万实体
前端·webgl·three.js
CAD老兵4 小时前
打造高性能二维图纸渲染引擎系列(三):高性能 CAD 文本渲染背后的隐藏工程
前端·webgl·three.js
CAD老兵4 小时前
打造高性能二维图纸渲染引擎系列(二):创建结构化和可扩展的渲染场景
前端·webgl·three.js
王木风4 小时前
1分钟理解什么是MySQL的Buffer Pool和LRU 算法?
前端·mysql
Jerry_Rod4 小时前
vue 项目如何使用 mqtt 通信
前端·vue.js
云中雾丽4 小时前
Flutter中路由配置的各种方案
前端
不一样的少年_4 小时前
女朋友炸了:刚打开的网页怎么又没了?我反手甩出一键恢复按钮!
前端·javascript·浏览器
Renounce4 小时前
【Android】让 Android 界面 “动” 起来:动画知识点大起底
前端
Asort4 小时前
JavaScript设计模式(十四)——命令模式:解耦请求发送者与接收者
前端·javascript·设计模式