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

自动记录页面生命周期事件、用户点击事件等,并将这些日志数据存储到本地,在适当的时候(如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
}
相关推荐
崔庆才丨静觅2 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60612 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了2 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅2 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅3 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅3 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment3 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅4 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊4 小时前
jwt介绍
前端
爱敲代码的小鱼4 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax