自动记录页面生命周期事件、用户点击事件等,并将这些日志数据存储到本地,在适当的时候(如APP_HIDE事件)上报到服务器
1、模块导入和常量定义
- 导入moment库用于时间格式化。
- 定义页面生命周期方法数组
PAGE_LIFE_METHOD
。 - 定义一些全局变量,如
subscribersReleaseFlag
(队列释放标识)、subscribers
(操作日志存储队列)、cacheStorageOperLogData
(缓存的操作日志数据)等。
2、页面事件控制(controlPageEvent)
这个函数用于包装页面的生命周期方法和自定义方法,以便在特定时机记录日志。
- 遍历页面选项,对非生命周期方法(即自定义方法)进行包装,使其能够记录用户点击事件。
- 重写页面的
onLoad
、onShow
、onHide
、onUnload
和onShareAppMessage
方法,在原有逻辑前后插入日志记录。 - 在页面生命周期事件中,通过
saveOperLog
函数记录相应的日志。
3、 组件事件控制(controlCommponentEvent)
类似页面事件控制,但针对组件。它遍历组件的方法,并对每个方法进行包装,以记录用户操作。
4、 事件代理(_proxyHooks)
这个函数是包装原始函数的核心,它返回一个新的函数,这个新函数在执行原始函数之前,会尝试记录操作日志。
- 首先检查事件对象中是否包含
businame
和busidesc
数据,如果有,则使用这些数据记录日志。 - 如果没有,则通过
getFunParam
函数从原始函数的参数中提取busiName
和busiDesc
,然后记录日志。 - 最后执行原始函数并返回结果。
5、 获取函数参数(getFunParam)
这个函数通过将函数转换为字符串,然后解析字符串来获取函数的参数名和默认值,从而提取出busiName
和busiDesc
。这种方法依赖于函数被转换为字符串后的特定格式,因此有一定的脆弱性。
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
}