/** * @class StatResult 基础数据结果统计模型 */ const BaseMod = require('./base') const Platform = require('./platform') const Channel = require('./channel') const Version = require('./version') const SessionLog = require('./sessionLog') const UserSessionLog = require('./userSessionLog') const ErrorLog = require('./errorLog') const ActiveDevices = require('./activeDevices') const ActiveUsers = require('./activeUsers') const UniIDUsers = require('./uniIDUsers') const { DateTime } = require('../lib') module.exports = class StatResult extends BaseMod { constructor() { super() this.tableName = 'result' this.platforms = [] this.channels = [] this.versions = [] } /** * 基础数据统计 * @param {String} type 统计类型 hour:实时统计 day:按天统计,week:按周统计 month:按月统计 * @param {Date|Time} date 指定日期或时间戳 * @param {Boolean} reset 是否重置,为ture时会重置该批次数据 */ async stat(type, date, reset) { const allowedType = ['hour', 'day', 'week', 'month'] if (!allowedType.includes(type)) { return { code: 1002, msg: 'This type is not allowed' } } if (this.debug) { console.log('result --type:' + type + ', date:' + date + ', reset:' + reset) } this.fillType = type const dateTime = new DateTime() const dateDimension = dateTime.getTimeDimensionByType(type, -1, date) this.startTime = dateDimension.startTime this.endTime = dateDimension.endTime if (this.debug) { console.log('dimension time', this.startTime + '--' + this.endTime) } // 查看当前时间段日志是否已存在,防止重复生成 if (!reset) { const checkRes = await this.getCollection(this.tableName).where({ dimension: this.fillType, start_time: this.startTime, end_time: this.endTime }).get() if (checkRes.data.length > 0) { console.log('log have existed') return { code: 1003, msg: 'This log have existed' } } } else { const delRes = await this.delete(this.tableName, { start_time: this.startTime, end_time: this.endTime }) console.log('delete old data result:', JSON.stringify(delRes)) } // 周月数据单独统计 if (['week', 'month'].includes(this.fillType)) { return await this.statWeekOrMonth() } // 数据获取 this.sessionLog = new SessionLog() const statRes = await this.aggregate(this.sessionLog.tableName, { project: { appid: 1, version: 1, platform: 1, channel: 1, is_first_visit: 1, page_count: 1, duration: 1, create_time: 1 }, match: { create_time: { $gte: this.startTime, $lte: this.endTime } }, group: { _id: { appid: '$appid', version: '$version', platform: '$platform', channel: '$channel' }, new_device_count: { $sum: '$is_first_visit' }, page_view_count: { $sum: '$page_count' }, total_duration: { $sum: '$duration' }, session_times: { $sum: 1 } }, sort: { new_device_count: 1, page_view_count: 1, session_times: 1 }, getAll: true }) let res = { code: 0, msg: 'success' } if (this.debug) { console.log('statRes', JSON.stringify(statRes)) } this.fillData = [] this.composes = [] if (statRes.data.length > 0) { for (const i in statRes.data) { await this.fill(statRes.data[i]) } } //补充数据 await this.replenishStat() if (this.fillData.length > 0) { res = await this.batchInsert(this.tableName, this.fillData) } return res } /** * 按周/月统计 */ async statWeekOrMonth() { const statRes = await this.aggregate(this.tableName, { project: { appid: 1, version_id: 1, platform_id: 1, channel_id: 1, new_device_count: 1, new_user_count: 1, page_visit_count: 1, user_visit_times: 1, app_launch_count: 1, error_count: 1, bounce_times: 1, duration: 1, user_duration: 1, dimension: 1, start_time: 1 }, match: { dimension: 'day', start_time: { $gte: this.startTime, $lte: this.endTime } }, group: { _id: { appid: '$appid', version_id: '$version_id', platform_id: '$platform_id', channel_id: '$channel_id' }, new_device_count: { $sum: '$new_device_count' }, new_user_count: { $sum: '$new_user_count' }, error_count: { $sum: '$error_count' }, page_count: { $sum: '$page_visit_count' }, total_duration: { $sum: '$duration' }, total_user_duration: { $sum: '$user_duration' }, total_user_session_times: { $sum: '$user_session_times' }, session_times: { $sum: '$app_launch_count' }, total_bounce_times: { $sum: '$bounce_times' } }, sort: { new_device_count: 1, error_count: 1, page_count: 1 }, getAll: true }) let res = { code: 0, msg: 'success' } if (this.debug) { console.log('statRes', JSON.stringify(statRes)) } this.activeDevices = new ActiveDevices() this.activeUsers = new ActiveUsers() this.fillData = [] this.composes = [] if (statRes.data.length > 0) { for (const i in statRes.data) { await this.getWeekOrMonthData(statRes.data[i]) } } //补充数据 await this.replenishStat() if (this.fillData.length > 0) { res = await this.batchInsert(this.tableName, this.fillData) } return res } /** * 获取周/月维度的填充数据 * @param {Object} data 统计数据 */ async getWeekOrMonthData(data) { const matchCondition = { ...data._id, create_time: { $gte: this.startTime, $lte: this.endTime } } // 查询活跃设备数 const statVisitDeviceRes = await this.getCollection(this.activeDevices.tableName).where({ ...matchCondition, dimension: this.fillType }).count() let activeDeviceCount = 0 if (statVisitDeviceRes && statVisitDeviceRes.total > 0) { activeDeviceCount = statVisitDeviceRes.total } // 设备次均停留时长 let avgSessionTime = 0 if (data.total_duration > 0 && data.session_times > 0) { avgSessionTime = Math.round(data.total_duration / data.session_times) } // 设均停留时长 let avgDeviceTime = 0 if (data.total_duration > 0 && activeDeviceCount > 0) { avgDeviceTime = Math.round(data.total_duration / activeDeviceCount) } // 跳出率 let bounceRate = 0 if (data.total_bounce_times > 0 && data.session_times > 0) { bounceRate = data.total_bounce_times * 100 / data.session_times bounceRate = parseFloat(bounceRate.toFixed(2)) } // 累计设备数 let totalDevices = data.new_device_count const totalDeviceRes = await this.getCollection(this.tableName).where({ ...matchCondition, dimension: this.fillType, start_time: { $lt: this.startTime } }).orderBy('start_time', 'desc').limit(1).get() if (totalDeviceRes && totalDeviceRes.data.length > 0) { totalDevices += totalDeviceRes.data[0].total_devices } //活跃用户数 const statVisitUserRes = await this.getCollection(this.activeUsers.tableName).where({ ...matchCondition, dimension: this.fillType }).count() let activeUserCount = 0 if (statVisitUserRes && statVisitUserRes.total > 0) { activeUserCount = statVisitUserRes.total } // 平台信息 let platformInfo = null if (this.platforms && this.platforms[data._id.platform_id]) { platformInfo = this.platforms[data._id.platform_id] } else { const platform = new Platform() platformInfo = await this.getById(platform.tableName, data._id.platform_id) if (!platformInfo || platformInfo.length === 0) { platformInfo.code = '' } this.platforms[data._id.platform_id] = platformInfo } // 渠道信息 let channelInfo = null if (this.channels && this.channels[data._id.channel_id]) { channelInfo = this.channels[data._id.channel_id] } else { const channel = new Channel() channelInfo = await this.getById(channel.tableName, data._id.channel_id) if (!channelInfo || channelInfo.length === 0) { channelInfo.channel_code = '' } this.channels[data._id.channel_id] = channelInfo } // 版本信息 let versionInfo = null if (this.versions && this.versions[data._id.version_id]) { versionInfo = this.versions[data._id.version_id] } else { const version = new Version() versionInfo = await this.getById(version.tableName, data._id.version_id, false) if (!versionInfo || versionInfo.length === 0) { versionInfo.version = '' } this.versions[data._id.version_id] = versionInfo } //总用户数 const uniIDUsers = new UniIDUsers() let totalUserCount = await uniIDUsers.getUserCount(data._id.appid, platformInfo.code, channelInfo.channel_code, versionInfo.version, { $lte: this.endTime }) //人均停留时长 let avgUserTime = 0 if (data.total_user_duration > 0 && activeUserCount > 0) { avgUserTime = Math.round(data.total_user_duration / activeUserCount) } //用户次均访问时长 let avgUserSessionTime = 0 if (data.total_user_duration > 0 && data.total_user_session_times > 0) { avgUserSessionTime = Math.round(data.total_user_duration / data.total_user_session_times) } const insertParam = { appid: data._id.appid, platform_id: data._id.platform_id, channel_id: data._id.channel_id, version_id: data._id.version_id, //总设备数 total_devices: totalDevices, //本时间段新增设备数 new_device_count: data.new_device_count, //登录用户会话次数 user_session_times: data.total_user_session_times, //活跃设备数 active_device_count: activeDeviceCount, //总用户数 total_users: totalUserCount, //新增用户数 new_user_count: data.new_user_count, //活跃用户数 active_user_count: activeUserCount, //应用启动次数 = 设备会话次数 app_launch_count: data.session_times, //页面访问次数 page_visit_count: data.page_view_count, //错误次数 error_count: data.error_count, //会话总访问时长 duration: data.total_duration, //用户会话总访问时长 user_duration: data.total_user_duration, avg_device_session_time: avgSessionTime, avg_user_session_time: avgUserSessionTime, avg_device_time: avgDeviceTime, avg_user_time: avgUserTime, bounce_times: data.total_bounce_times, bounce_rate: bounceRate, retention: {}, dimension: this.fillType, stat_date: new DateTime().getDate('Ymd', this.startTime), start_time: this.startTime, end_time: this.endTime } this.fillData.push(insertParam) this.composes.push(data._id.appid + '_' + data._id.platform_id + '_' + data._id.channel_id + '_' + data ._id.version_id) return insertParam } /** * 基础填充数据-目前只有小时和天维度的数据 * @param {Object} data 统计数据 */ async fill(data) { // 平台信息 let platformInfo = null if (this.platforms && this.platforms[data._id.platform]) { //暂存下数据,减少读库 platformInfo = this.platforms[data._id.platform] } else { const platform = new Platform() platformInfo = await platform.getPlatformAndCreate(data._id.platform, null) if (!platformInfo || platformInfo.length === 0) { platformInfo._id = '' } this.platforms[data._id.platform] = platformInfo if (this.debug) { console.log('platformInfo', JSON.stringify(platformInfo)) } } // 渠道信息 let channelInfo = null const channelKey = data._id.appid + '_' + platformInfo._id + '_' + data._id.channel if (this.channels && this.channels[channelKey]) { channelInfo = this.channels[channelKey] } else { const channel = new Channel() channelInfo = await channel.getChannelAndCreate(data._id.appid, platformInfo._id, data._id.channel) if (!channelInfo || channelInfo.length === 0) { channelInfo._id = '' } this.channels[channelKey] = channelInfo if (this.debug) { console.log('channelInfo', JSON.stringify(channelInfo)) } } // 版本信息 let versionInfo = null const versionKey = data._id.appid + '_' + data._id.platform + '_' + data._id.version if (this.versions && this.versions[versionKey]) { versionInfo = this.versions[versionKey] } else { const version = new Version() versionInfo = await version.getVersionAndCreate(data._id.appid, data._id.platform, data._id.version) if (!versionInfo || versionInfo.length === 0) { versionInfo._id = '' } this.versions[versionKey] = versionInfo if (this.debug) { console.log('versionInfo', JSON.stringify(versionInfo)) } } // 访问设备数统计 const matchCondition = data._id Object.assign(matchCondition, { create_time: { $gte: this.startTime, $lte: this.endTime } }) const statVisitDeviceRes = await this.aggregate(this.sessionLog.tableName, { project: { appid: 1, version: 1, platform: 1, channel: 1, device_id: 1, create_time: 1 }, match: matchCondition, group: [{ _id: { device_id: '$device_id' } }, { _id: {}, total_devices: { $sum: 1 } }] }) let activeDeviceCount = 0 if (statVisitDeviceRes.data.length > 0) { activeDeviceCount = statVisitDeviceRes.data[0].total_devices } // 错误数量统计 const errorLog = new ErrorLog() const statErrorRes = await this.getCollection(errorLog.tableName).where(matchCondition).count() let errorCount = 0 if (statErrorRes && statErrorRes.total > 0) { errorCount = statErrorRes.total } // 设备的次均停留时长,设备访问总时长/设备会话次数 let avgSessionTime = 0 if (data.total_duration > 0 && data.session_times > 0) { avgSessionTime = Math.round(data.total_duration / data.session_times) } // 设均停留时长 let avgDeviceTime = 0 if (data.total_duration > 0 && activeDeviceCount > 0) { avgDeviceTime = Math.round(data.total_duration / activeDeviceCount) } // 跳出率 let bounceTimes = 0 const bounceRes = await this.getCollection(this.sessionLog.tableName).where({ ...matchCondition, page_count: 1 }).count() if (bounceRes && bounceRes.total > 0) { bounceTimes = bounceRes.total } let bounceRate = 0 if (bounceTimes > 0 && data.session_times > 0) { bounceRate = bounceTimes * 100 / data.session_times bounceRate = parseFloat(bounceRate.toFixed(2)) } // 应用启动次数 = 会话次数 const launchCount = data.session_times // 累计设备数 let totalDevices = data.new_device_count const totalDeviceRes = await this.getCollection(this.tableName).where({ appid: data._id.appid, platform_id: platformInfo._id, channel_id: channelInfo._id, version_id: versionInfo._id, dimension: this.fillType, start_time: { $lt: this.startTime } }).orderBy('start_time', 'desc').limit(1).get() if (totalDeviceRes && totalDeviceRes.data.length > 0) { totalDevices += totalDeviceRes.data[0].total_devices } //活跃用户数 const userSessionLog = new UserSessionLog() let activeUserCount = 0 const activeUserCountRes = await this.aggregate(userSessionLog.tableName, { project: { appid: 1, version: 1, platform: 1, channel: 1, uid: 1, create_time: 1 }, match: matchCondition, group: [{ _id: { uid: '$uid' } }, { _id: {}, total_users: { $sum: 1 } }] }) if (activeUserCountRes && activeUserCountRes.data.length > 0) { activeUserCount = activeUserCountRes.data[0].total_users } //新增用户数 const uniIDUsers = new UniIDUsers() let newUserCount = await uniIDUsers.getUserCount(matchCondition.appid, matchCondition.platform, matchCondition.channel, matchCondition.version, { $gte: this.startTime, $lte: this.endTime }) //总用户数 let totalUserCount = await uniIDUsers.getUserCount(matchCondition.appid, matchCondition.platform, matchCondition.channel, matchCondition.version, { $lte: this.endTime }) //用户停留总时长及总会话次数 let totalUserDuration = 0 let totalUserSessionTimes = 0 const totalUserDurationRes = await this.aggregate(userSessionLog.tableName, { project: { appid: 1, version: 1, platform: 1, channel: 1, duration: 1, create_time: 1 }, match: matchCondition, group: [{ _id: {}, total_duration: { $sum: "$duration" }, total_session_times: { $sum: 1 } }] }) if (totalUserDurationRes && totalUserDurationRes.data.length > 0) { totalUserDuration = totalUserDurationRes.data[0].total_duration totalUserSessionTimes = totalUserDurationRes.data[0].total_session_times } //人均停留时长 let avgUserTime = 0 //用户次均访问时长 let avgUserSessionTime = 0 if (totalUserDuration > 0 && activeUserCount > 0) { avgUserTime = Math.round(totalUserDuration / activeUserCount) avgUserSessionTime = Math.round(totalUserDuration / totalUserSessionTimes) } //设置填充数据 const datetime = new DateTime() const insertParam = { appid: data._id.appid, platform_id: platformInfo._id, channel_id: channelInfo._id, version_id: versionInfo._id, total_devices: totalDevices, new_device_count: data.new_device_count, active_device_count: activeDeviceCount, total_users: totalUserCount, new_user_count: newUserCount, active_user_count: activeUserCount, user_session_times: totalUserSessionTimes, app_launch_count: launchCount, page_visit_count: data.page_view_count, error_count: errorCount, duration: data.total_duration, user_duration: totalUserDuration, avg_device_session_time: avgSessionTime, avg_user_session_time: avgUserSessionTime, avg_device_time: avgDeviceTime, avg_user_time: avgUserTime, bounce_times: bounceTimes, bounce_rate: bounceRate, retention: {}, dimension: this.fillType, stat_date: datetime.getDate('Ymd', this.startTime), start_time: this.startTime, end_time: this.endTime } //数据填充 this.fillData.push(insertParam) this.composes.push(data._id.appid + '_' + platformInfo._id + '_' + channelInfo._id + '_' + versionInfo ._id) return insertParam } /** * 基础统计数据补充,防止累计数据丢失 */ async replenishStat() { if (this.debug) { console.log('composes data', this.composes) } const datetime = new DateTime() // const { // startTime // } = datetime.getTimeDimensionByType(this.fillType, -1, this.startTime) //上一次统计时间 let preStatRes = await this.getCollection(this.tableName).where({ start_time: {$lt: this.startTime}, dimension: this.fillType }).orderBy('start_time', 'desc').limit(1).get() let preStartTime = 0 if(preStatRes && preStatRes.data.length > 0) { preStartTime = preStatRes.data[0].start_time } if (this.debug) { console.log('replenishStat-preStartTime', preStartTime) } if(!preStartTime) { return false } // 上一阶段数据 const preStatData = await this.selectAll(this.tableName, { start_time: preStartTime, dimension: this.fillType }, { appid: 1, platform_id: 1, channel_id: 1, version_id: 1, total_devices: 1, total_users: 1 }) if (!preStatData || preStatData.data.length === 0) { return false } if (this.debug) { console.log('preStatData', JSON.stringify(preStatData)) } let preKey const preKeyArr = [] for (const pi in preStatData.data) { preKey = preStatData.data[pi].appid + '_' + preStatData.data[pi].platform_id + '_' + preStatData .data[pi].channel_id + '_' + preStatData.data[pi].version_id if (!this.composes.includes(preKey) && !preKeyArr.includes(preKey)) { preKeyArr.push(preKey) if (this.debug) { console.log('preKey -add', preKey) } this.fillData.push({ appid: preStatData.data[pi].appid, platform_id: preStatData.data[pi].platform_id, channel_id: preStatData.data[pi].channel_id, version_id: preStatData.data[pi].version_id, total_devices: preStatData.data[pi].total_devices, new_device_count: 0, active_device_count: 0, total_users: preStatData.data[pi].total_users, new_user_count: 0, active_user_count: 0, user_session_times: 0, app_launch_count: 0, page_visit_count: 0, error_count: 0, duration: 0, user_duration: 0, avg_device_session_time: 0, avg_user_session_time: 0, avg_device_time: 0, avg_user_time: 0, bounce_times: 0, bounce_rate: 0, retention: {}, dimension: this.fillType, stat_date: datetime.getDate('Ymd', this.startTime), start_time: this.startTime, end_time: this.endTime }) } else if (this.debug) { console.log('preKey -have', preKey) } } return true } /** * 留存数据统计 * @param {String} type 统计类型 hour:实时统计 day:按天统计,week:按周统计 month:按月统计 * @param {Date|Time} date 指定日期或时间戳 * @param {String} mod 统计模块 device:设备,user:用户 */ async retentionStat(type, date, mod = 'device') { date = date ? date : new DateTime().getTimeBySetDays(-1, date) const allowedType = ['day', 'week', 'month'] if (!allowedType.includes(type)) { return { code: 1002, msg: 'This type is not allowed' } } let days = [] switch (type) { case 'week': case 'month': days = [1, 2, 3, 4, 5, 6, 7, 8, 9] break default: days = [1, 2, 3, 4, 5, 6, 7, 14, 30] break } let res = { code: 0, msg: 'success' } for (const day in days) { //留存统计数据库查询较为复杂,因此这里拆分为多个维度 if (mod && mod === 'user') { //用户留存统计 if (type === 'day') { res = await this.userRetentionFillDayly(type, days[day], date) } else { res = await this.userRetentionFillWeekOrMonth(type, days[day], date) } } else { //设备留存统计 if (type === 'day') { res = await this.deviceRetentionFillDayly(type, days[day], date) } else { res = await this.deviceRetentionFillWeekOrMonth(type, days[day], date) } } } return res } /** * 设备日留存统计数据填充 * @param {String} type 统计类型 hour:实时统计 day:按天统计,week:按周统计 month:按月统计 * @param {Date|Time} date 指定日期或时间戳 * @param {Boolean} reset 是否重置,为ture时会重置该批次数据 */ async deviceRetentionFillDayly(type, day, date) { if (type !== 'day') { return { code: 301, msg: 'Type error:' + type } } const dateTime = new DateTime() const { startTime, endTime } = dateTime.getTimeDimensionByType(type, 0 - day, date) if (!startTime || !endTime) { return { code: 1001, msg: 'The statistic time get failed' } } // 截止时间范围 const lastTimeInfo = dateTime.getTimeDimensionByType(type, 0, date) // 获取当时批次的统计日志 const resultLogRes = await this.selectAll(this.tableName, { dimension: type, start_time: startTime, end_time: endTime }) // const resultLogRes = await this.getCollection(this.tableName).where({ // dimension: type, // start_time: startTime, // end_time: endTime // }).get() if (this.debug) { console.log('resultLogRes', JSON.stringify(resultLogRes)) } if (!resultLogRes || resultLogRes.data.length === 0) { if (this.debug) { console.log('Not found this log --' + type + ':' + day + ', start:' + startTime + ',endTime:' + endTime) } return { code: 1000, msg: 'Not found this log' } } const sessionLog = new SessionLog() const platform = new Platform() const channel = new Channel() const version = new Version() let res = null for (const resultIndex in resultLogRes.data) { const resultLog = resultLogRes.data[resultIndex] // 平台信息 let platformInfo = null if (this.platforms && this.platforms[resultLog.platform_id]) { platformInfo = this.platforms[resultLog.platform_id] } else { platformInfo = await this.getById(platform.tableName, resultLog.platform_id) if (!platformInfo || platformInfo.length === 0) { platformInfo.code = '' } this.platforms[resultLog.platform_id] = platformInfo if (this.debug) { console.log('platformInfo', JSON.stringify(platformInfo)) } } // 渠道信息 let channelInfo = null if (this.channels && this.channels[resultLog.channel_id]) { channelInfo = this.channels[resultLog.channel_id] } else { channelInfo = await this.getById(channel.tableName, resultLog.channel_id) if (!channelInfo || channelInfo.length === 0) { channelInfo.channel_code = '' } this.channels[resultLog.channel_id] = channelInfo if (this.debug) { console.log('channelInfo', JSON.stringify(channelInfo)) } } // 版本信息 let versionInfo = null if (this.versions && this.versions[resultLog.version_id]) { versionInfo = this.versions[resultLog.version_id] } else { versionInfo = await this.getById(version.tableName, resultLog.version_id, false) if (!versionInfo || versionInfo.length === 0) { versionInfo.version = '' } this.versions[resultLog.version_id] = versionInfo if (this.debug) { console.log('versionInfo', JSON.stringify(versionInfo)) } } // 获取该批次的活跃设备数 const activeDeviceRes = await this.aggregate(sessionLog.tableName, { project: { appid: 1, version: 1, platform: 1, channel: 1, device_id: 1, create_time: 1 }, match: { appid: resultLog.appid, version: versionInfo.version, platform: platformInfo.code, channel: channelInfo.channel_code, create_time: { $gte: startTime, $lte: endTime } }, group: { _id: { device_id: '$device_id' }, create_time: { $min: '$create_time' }, sessionCount: { $sum: 1 } }, sort: { create_time: 1, sessionCount: 1 }, getAll: true }) if (this.debug) { console.log('activeDeviceRes', JSON.stringify(activeDeviceRes)) } let activeDeviceRate = 0 let activeDevices = 0 if (activeDeviceRes && activeDeviceRes.data.length > 0) { const thisDayActiveDevices = activeDeviceRes.data.length const thisDayActiveDeviceIds = [] for (const tau in activeDeviceRes.data) { thisDayActiveDeviceIds.push(activeDeviceRes.data[tau]._id.device_id) } if (this.debug) { console.log('thisDayActiveDeviceIds', JSON.stringify(thisDayActiveDeviceIds)) } // 留存活跃设备查询 const retentionActiveDeviceRes = await this.aggregate(sessionLog.tableName, { project: { appid: 1, version: 1, platform: 1, channel: 1, device_id: 1, create_time: 1 }, match: { appid: resultLog.appid, version: versionInfo.version, platform: platformInfo.code, channel: channelInfo.channel_code, device_id: { $in: thisDayActiveDeviceIds }, create_time: { $gte: lastTimeInfo.startTime, $lte: lastTimeInfo.endTime } }, group: [{ _id: { device_id: '$device_id' } }, { _id: {}, total_devices: { $sum: 1 } }] }) if (this.debug) { console.log('retentionActiveDeviceRes', JSON.stringify(retentionActiveDeviceRes)) } if (retentionActiveDeviceRes && retentionActiveDeviceRes.data.length > 0) { // 活跃设备留存数 activeDevices = retentionActiveDeviceRes.data[0].total_devices // 活跃设备留存率 activeDeviceRate = parseFloat((activeDevices * 100 / thisDayActiveDevices).toFixed(2)) } } // 获取该批次新增设备数 const newDeviceRes = await this.aggregate(sessionLog.tableName, { project: { appid: 1, version: 1, platform: 1, channel: 1, is_first_visit: 1, device_id: 1, create_time: 1 }, match: { appid: resultLog.appid, version: versionInfo.version, platform: platformInfo.code, channel: channelInfo.channel_code, is_first_visit: 1, create_time: { $gte: startTime, $lte: endTime } }, group: { _id: { device_id: '$device_id' }, create_time: { $min: '$create_time' }, sessionCount: { $sum: 1 } }, sort: { create_time: 1, sessionCount: 1 }, getAll: true }) let newDeviceRate = 0 let newDevices = 0 if (newDeviceRes && newDeviceRes.data.length > 0) { const thisDayNewDevices = newDeviceRes.data.length const thisDayNewDeviceIds = [] for (const tau in newDeviceRes.data) { thisDayNewDeviceIds.push(newDeviceRes.data[tau]._id.device_id) } if (this.debug) { console.log('thisDayNewDeviceIds', JSON.stringify(thisDayNewDeviceIds)) } // 留存的设备查询 const retentionNewDeviceRes = await this.aggregate(sessionLog.tableName, { project: { appid: 1, version: 1, platform: 1, channel: 1, device_id: 1, create_time: 1 }, match: { appid: resultLog.appid, version: versionInfo.version, platform: platformInfo.code, channel: channelInfo.channel_code, device_id: { $in: thisDayNewDeviceIds }, create_time: { $gte: lastTimeInfo.startTime, $lte: lastTimeInfo.endTime } }, group: [{ _id: { device_id: '$device_id' } }, { _id: {}, total_devices: { $sum: 1 } }] }) if (retentionNewDeviceRes && retentionNewDeviceRes.data.length > 0) { // 新增设备留存数 newDevices = retentionNewDeviceRes.data[0].total_devices // 新增设备留存率 newDeviceRate = parseFloat((newDevices * 100 / thisDayNewDevices).toFixed(2)) } } // 数据更新 const retentionData = resultLog.retention const dataKey = type.substr(0, 1) + '_' + day if (!retentionData.active_device) { retentionData.active_device = {} } retentionData.active_device[dataKey] = { device_count: activeDevices, device_rate: activeDeviceRate } if (!retentionData.new_device) { retentionData.new_device = {} } retentionData.new_device[dataKey] = { device_count: newDevices, device_rate: newDeviceRate } if (this.debug) { console.log('retentionData', JSON.stringify(retentionData)) } res = await this.update(this.tableName, { retention: retentionData }, { _id: resultLog._id }) } if (res && res.updated) { return { code: 0, msg: 'success' } } else { return { code: 500, msg: 'retention data update failed' } } } /** * 设备周/月留存数据填充 * @param {String} type 统计类型 hour:实时统计 day:按天统计,week:按周统计 month:按月统计 * @param {Date|Time} date 指定日期或时间戳 * @param {Boolean} reset 是否重置,为ture时会重置该批次数据 */ async deviceRetentionFillWeekOrMonth(type, day, date) { if (!['week', 'month'].includes(type)) { return { code: 301, msg: 'Type error:' + type } } const dateTime = new DateTime() const { startTime, endTime } = dateTime.getTimeDimensionByType(type, 0 - day, date) if (!startTime || !endTime) { return { code: 1001, msg: 'The statistic time get failed' } } // 截止时间范围 const lastTimeInfo = dateTime.getTimeDimensionByType(type, 0, date) // 获取当时批次的统计日志 const resultLogRes = await this.selectAll(this.tableName, { dimension: type, start_time: startTime, end_time: endTime }) if (this.debug) { console.log('resultLogRes', JSON.stringify(resultLogRes)) } if (!resultLogRes || resultLogRes.data.length === 0) { if (this.debug) { console.log('Not found this session log --' + type + ':' + day + ', start:' + startTime + ',endTime:' + endTime) } return { code: 1000, msg: 'Not found this session log' } } const activeDevicesObj = new ActiveDevices() let res = null; let activeDeviceRes; let activeDeviceRate; let activeDevices; let newDeviceRate; let newDevices for (const resultIndex in resultLogRes.data) { const resultLog = resultLogRes.data[resultIndex] // 获取该批次的活跃设备数 activeDeviceRes = await this.selectAll(activeDevicesObj.tableName, { appid: resultLog.appid, platform_id: resultLog.platform_id, channel_id: resultLog.channel_id, version_id: resultLog.version_id, dimension: type, create_time: { $gte: startTime, $lte: endTime } }, { device_id: 1 }) if (this.debug) { console.log('activeDeviceRes', JSON.stringify(activeDeviceRes)) } activeDeviceRate = 0 activeDevices = 0 if (activeDeviceRes && activeDeviceRes.data.length > 0) { const thisDayActiveDevices = activeDeviceRes.data.length const thisDayActiveDeviceIds = [] for (const tau in activeDeviceRes.data) { thisDayActiveDeviceIds.push(activeDeviceRes.data[tau].device_id) } if (this.debug) { console.log('thisDayActiveDeviceIds', JSON.stringify(thisDayActiveDeviceIds)) } // 留存活跃设备数 const retentionActiveDeviceRes = await this.getCollection(activeDevicesObj.tableName).where({ appid: resultLog.appid, platform_id: resultLog.platform_id, channel_id: resultLog.channel_id, version_id: resultLog.version_id, device_id: { $in: thisDayActiveDeviceIds }, dimension: type, create_time: { $gte: lastTimeInfo.startTime, $lte: lastTimeInfo.endTime } }).count() if (this.debug) { console.log('retentionActiveDeviceRes', JSON.stringify(retentionActiveDeviceRes)) } if (retentionActiveDeviceRes && retentionActiveDeviceRes.total > 0) { // 活跃设备留存数 activeDevices = retentionActiveDeviceRes.total // 活跃设备留存率 activeDeviceRate = parseFloat((activeDevices * 100 / thisDayActiveDevices).toFixed(2)) } } // 获取该批次的新增设备数 const newDeviceRes = await this.selectAll(activeDevicesObj.tableName, { appid: resultLog.appid, platform_id: resultLog.platform_id, channel_id: resultLog.channel_id, version_id: resultLog.version_id, is_new: 1, dimension: type, create_time: { $gte: startTime, $lte: endTime } }, { device_id: 1 }) newDeviceRate = 0 newDevices = 0 if (newDeviceRes && newDeviceRes.data.length > 0) { const thisDayNewDevices = newDeviceRes.data.length const thisDayNewDeviceIds = [] for (const tau in newDeviceRes.data) { thisDayNewDeviceIds.push(newDeviceRes.data[tau].device_id) } // 新增设备留存数 const retentionNewDeviceRes = await this.getCollection(activeDevicesObj.tableName).where({ appid: resultLog.appid, platform_id: resultLog.platform_id, channel_id: resultLog.channel_id, version_id: resultLog.version_id, device_id: { $in: thisDayNewDeviceIds }, dimension: type, create_time: { $gte: lastTimeInfo.startTime, $lte: lastTimeInfo.endTime } }).count() if (retentionNewDeviceRes && retentionNewDeviceRes.total > 0) { // 新增设备留存数 newDevices = retentionNewDeviceRes.total // 新增设备留存率 newDeviceRate = parseFloat((newDevices * 100 / thisDayNewDevices).toFixed(2)) } } // 数据更新 const retentionData = resultLog.retention const dataKey = type.substr(0, 1) + '_' + day if (!retentionData.active_device) { retentionData.active_device = {} } retentionData.active_device[dataKey] = { device_count: activeDevices, device_rate: activeDeviceRate } if (!retentionData.new_device) { retentionData.new_device = {} } retentionData.new_device[dataKey] = { device_count: newDevices, device_rate: newDeviceRate } if (this.debug) { console.log('retentionData', JSON.stringify(retentionData)) } res = await this.update(this.tableName, { retention: retentionData }, { _id: resultLog._id }) } if (res && res.updated) { return { code: 0, msg: 'success' } } else { return { code: 500, msg: 'retention data update failed' } } } /** * 用户日留存数据填充 * @param {String} type 统计类型 hour:实时统计 day:按天统计,week:按周统计 month:按月统计 * @param {Date|Time} date 指定日期或时间戳 * @param {Boolean} reset 是否重置,为ture时会重置该批次数据 */ async userRetentionFillDayly(type, day, date) { if (type !== 'day') { return { code: 301, msg: 'Type error:' + type } } const dateTime = new DateTime() const { startTime, endTime } = dateTime.getTimeDimensionByType(type, 0 - day, date) if (!startTime || !endTime) { return { code: 1001, msg: 'The statistic time get failed' } } // 截止时间范围 const lastTimeInfo = dateTime.getTimeDimensionByType(type, 0, date) // 获取当时批次的统计日志 const resultLogRes = await this.selectAll(this.tableName, { dimension: type, start_time: startTime, end_time: endTime }) if (this.debug) { console.log('resultLogRes', JSON.stringify(resultLogRes)) } if (!resultLogRes || resultLogRes.data.length === 0) { if (this.debug) { console.log('Not found this log --' + type + ':' + day + ', start:' + startTime + ',endTime:' + endTime) } return { code: 1000, msg: 'Not found this log' } } const userSessionLog = new UserSessionLog() const uniIDUsers = new UniIDUsers() const platform = new Platform() const channel = new Channel() const version = new Version() let res = null for (const resultIndex in resultLogRes.data) { const resultLog = resultLogRes.data[resultIndex] // 平台信息 let platformInfo = null if (this.platforms && this.platforms[resultLog.platform_id]) { platformInfo = this.platforms[resultLog.platform_id] } else { platformInfo = await this.getById(platform.tableName, resultLog.platform_id) if (!platformInfo || platformInfo.length === 0) { platformInfo.code = '' } this.platforms[resultLog.platform_id] = platformInfo if (this.debug) { console.log('platformInfo', JSON.stringify(platformInfo)) } } // 渠道信息 let channelInfo = null if (this.channels && this.channels[resultLog.channel_id]) { channelInfo = this.channels[resultLog.channel_id] } else { channelInfo = await this.getById(channel.tableName, resultLog.channel_id) if (!channelInfo || channelInfo.length === 0) { channelInfo.channel_code = '' } this.channels[resultLog.channel_id] = channelInfo if (this.debug) { console.log('channelInfo', JSON.stringify(channelInfo)) } } // 版本信息 let versionInfo = null if (this.versions && this.versions[resultLog.version_id]) { versionInfo = this.versions[resultLog.version_id] } else { versionInfo = await this.getById(version.tableName, resultLog.version_id, false) if (!versionInfo || versionInfo.length === 0) { versionInfo.version = '' } this.versions[resultLog.version_id] = versionInfo if (this.debug) { console.log('versionInfo', JSON.stringify(versionInfo)) } } // 获取该时间段内的活跃用户 const activeUserRes = await this.aggregate(userSessionLog.tableName, { project: { appid: 1, version: 1, platform: 1, channel: 1, uid: 1, create_time: 1 }, match: { appid: resultLog.appid, version: versionInfo.version, platform: platformInfo.code, channel: channelInfo.channel_code, create_time: { $gte: startTime, $lte: endTime } }, group: { _id: { uid: '$uid' }, create_time: { $min: '$create_time' }, sessionCount: { $sum: 1 } }, sort: { create_time: 1, sessionCount: 1 }, getAll: true }) if (this.debug) { console.log('activeUserRes', JSON.stringify(activeUserRes)) } //活跃用户留存率 let activeUserRate = 0 //活跃用户留存数 let activeUsers = 0 if (activeUserRes && activeUserRes.data.length > 0) { const thisDayActiveUsers = activeUserRes.data.length //获取该时间段内的活跃用户编号,这里没用lookup联查是因为数据量较大时lookup效率很低 const thisDayActiveUids = [] for (let tau in activeUserRes.data) { thisDayActiveUids.push(activeUserRes.data[tau]._id.uid) } if (this.debug) { console.log('thisDayActiveUids', JSON.stringify(thisDayActiveUids)) } // 留存活跃用户数 const retentionActiveUserRes = await this.aggregate(userSessionLog.tableName, { project: { appid: 1, version: 1, platform: 1, channel: 1, uid: 1, create_time: 1 }, match: { appid: resultLog.appid, version: versionInfo.version, platform: platformInfo.code, channel: channelInfo.channel_code, uid: { $in: thisDayActiveUids }, create_time: { $gte: lastTimeInfo.startTime, $lte: lastTimeInfo.endTime } }, group: [{ _id: { uid: '$uid' } }, { _id: {}, total_users: { $sum: 1 } }] }) if (this.debug) { console.log('retentionActiveUserRes', JSON.stringify(retentionActiveUserRes)) } if (retentionActiveUserRes && retentionActiveUserRes.data.length > 0) { // 活跃用户留存数 activeUsers = retentionActiveUserRes.data[0].total_users // 活跃用户留存率 activeUserRate = parseFloat((activeUsers * 100 / thisDayActiveUsers).toFixed(2)) } } //新增用户编号 const thisDayNewUids = await uniIDUsers.getUserIds(resultLog.appid, platformInfo.code, channelInfo.channel_code, versionInfo.version, { $gte: startTime, $lte: endTime }) //新增用户留存率 let newUserRate = 0 //新增用户留存数 let newUsers = 0 if (thisDayNewUids.length > 0) { // 现在依然活跃的用户数 const retentionNewUserRes = await this.aggregate(userSessionLog.tableName, { project: { appid: 1, version: 1, platform: 1, channel: 1, uid: 1, create_time: 1 }, match: { appid: resultLog.appid, version: versionInfo.version, platform: platformInfo.code, channel: channelInfo.channel_code, uid: { $in: thisDayNewUids }, create_time: { $gte: lastTimeInfo.startTime, $lte: lastTimeInfo.endTime } }, group: [{ _id: { uid: '$uid' } }, { _id: {}, total_users: { $sum: 1 } }] }) if (retentionNewUserRes && retentionNewUserRes.data.length > 0) { // 新增用户留存数 newUsers = retentionNewUserRes.data[0].total_users // 新增用户留存率 newUserRate = parseFloat((newUsers * 100 / thisDayNewUids.length).toFixed(2)) } } // 数据更新 const retentionData = resultLog.retention const dataKey = type.substr(0, 1) + '_' + day if (!retentionData.active_user) { retentionData.active_user = {} } retentionData.active_user[dataKey] = { user_count: activeUsers, user_rate: activeUserRate } if (!retentionData.new_user) { retentionData.new_user = {} } retentionData.new_user[dataKey] = { user_count: newUsers, user_rate: newUserRate } if (this.debug) { console.log('retentionData', JSON.stringify(retentionData)) } res = await this.update(this.tableName, { retention: retentionData }, { _id: resultLog._id }) } if (res && res.updated) { return { code: 0, msg: 'success' } } else { return { code: 500, msg: 'retention data update failed' } } } /** * 用户周/月留存数据填充 * @param {String} type 统计类型 hour:实时统计 day:按天统计,week:按周统计 month:按月统计 * @param {Date|Time} date 指定日期或时间戳 * @param {Boolean} reset 是否重置,为ture时会重置该批次数据 */ async userRetentionFillWeekOrMonth(type, day, date) { if (!['week', 'month'].includes(type)) { return { code: 301, msg: 'Type error:' + type } } const dateTime = new DateTime() const { startTime, endTime } = dateTime.getTimeDimensionByType(type, 0 - day, date) if (!startTime || !endTime) { return { code: 1001, msg: 'The statistic time get failed' } } // 截止时间范围 const lastTimeInfo = dateTime.getTimeDimensionByType(type, 0, date) // 获取当时批次的统计日志 const resultLogRes = await this.selectAll(this.tableName, { dimension: type, start_time: startTime, end_time: endTime }) if (this.debug) { console.log('resultLogRes', JSON.stringify(resultLogRes)) } if (!resultLogRes || resultLogRes.data.length === 0) { if (this.debug) { console.log('Not found this session log --' + type + ':' + day + ', start:' + startTime + ',endTime:' + endTime) } return { code: 1000, msg: 'Not found this session log' } } const activeUserObj = new ActiveUsers() const uniIDUsers = new UniIDUsers() const platform = new Platform() const channel = new Channel() const version = new Version() let res = null //活跃用户留存率 let activeUserRate //活跃用户留存数 let activeUsers //新增用户留存率 let newUserRate //新增用户留存数 let newUsers for (const resultIndex in resultLogRes.data) { const resultLog = resultLogRes.data[resultIndex] // 平台信息 let platformInfo = null if (this.platforms && this.platforms[resultLog.platform_id]) { platformInfo = this.platforms[resultLog.platform_id] } else { platformInfo = await this.getById(platform.tableName, resultLog.platform_id) if (!platformInfo || platformInfo.length === 0) { platformInfo.code = '' } this.platforms[resultLog.platform_id] = platformInfo if (this.debug) { console.log('platformInfo', JSON.stringify(platformInfo)) } } // 渠道信息 let channelInfo = null if (this.channels && this.channels[resultLog.channel_id]) { channelInfo = this.channels[resultLog.channel_id] } else { channelInfo = await this.getById(channel.tableName, resultLog.channel_id) if (!channelInfo || channelInfo.length === 0) { channelInfo.channel_code = '' } this.channels[resultLog.channel_id] = channelInfo if (this.debug) { console.log('channelInfo', JSON.stringify(channelInfo)) } } // 版本信息 let versionInfo = null if (this.versions && this.versions[resultLog.version_id]) { versionInfo = this.versions[resultLog.version_id] } else { versionInfo = await this.getById(version.tableName, resultLog.version_id, false) if (!versionInfo || versionInfo.length === 0) { versionInfo.version = '' } this.versions[resultLog.version_id] = versionInfo if (this.debug) { console.log('versionInfo', JSON.stringify(versionInfo)) } } // 获取该批次的活跃用户数 const activeUserRes = await this.selectAll(activeUserObj.tableName, { appid: resultLog.appid, platform_id: resultLog.platform_id, channel_id: resultLog.channel_id, version_id: resultLog.version_id, dimension: type, create_time: { $gte: startTime, $lte: endTime } }, { uid: 1 }) if (this.debug) { console.log('activeUserRes', JSON.stringify(activeUserRes)) } activeUserRate = 0 activeUsers = 0 if (activeUserRes && activeUserRes.data.length > 0) { const thisDayactiveUsers = activeUserRes.data.length const thisDayActiveDeviceIds = [] for (const tau in activeUserRes.data) { thisDayActiveDeviceIds.push(activeUserRes.data[tau].uid) } if (this.debug) { console.log('thisDayActiveDeviceIds', JSON.stringify(thisDayActiveDeviceIds)) } // 留存活跃用户数 const retentionactiveUserRes = await this.getCollection(activeUserObj.tableName).where({ appid: resultLog.appid, platform_id: resultLog.platform_id, channel_id: resultLog.channel_id, version_id: resultLog.version_id, uid: { $in: thisDayActiveDeviceIds }, dimension: type, create_time: { $gte: lastTimeInfo.startTime, $lte: lastTimeInfo.endTime } }).count() if (this.debug) { console.log('retentionactiveUserRes', JSON.stringify(retentionactiveUserRes)) } if (retentionactiveUserRes && retentionactiveUserRes.total > 0) { // 活跃用户留存数 activeUsers = retentionactiveUserRes.total // 活跃用户留存率 activeUserRate = parseFloat((activeUsers * 100 / thisDayactiveUsers).toFixed(2)) } } //新增用户编号 const thisDayNewUids = await uniIDUsers.getUserIds(resultLog.appid, platformInfo.code, channelInfo.channel_code, versionInfo.version, { $gte: startTime, $lte: endTime }) // 新增用户留存率 newUserRate = 0 // 新增用户留存数 newUsers = 0 if(thisDayNewUids && thisDayNewUids.length > 0) { // 新增用户留存数 const retentionnewUserRes = await this.getCollection(activeUserObj.tableName).where({ appid: resultLog.appid, platform_id: resultLog.platform_id, channel_id: resultLog.channel_id, version_id: resultLog.version_id, uid: { $in: thisDayNewUids }, dimension: type, create_time: { $gte: lastTimeInfo.startTime, $lte: lastTimeInfo.endTime } }).count() if (retentionnewUserRes && retentionnewUserRes.total > 0) { // 新增用户留存数 newUsers = retentionnewUserRes.total // 新增用户留存率 newUserRate = parseFloat((newUsers * 100 / thisDayNewUids.length).toFixed(2)) } } // 数据更新 const retentionData = resultLog.retention const dataKey = type.substr(0, 1) + '_' + day if (!retentionData.active_user) { retentionData.active_user = {} } retentionData.active_user[dataKey] = { user_count: activeUsers, user_rate: activeUserRate } if (!retentionData.new_user) { retentionData.new_user = {} } retentionData.new_user[dataKey] = { user_count: newUsers, user_rate: newUserRate } if (this.debug) { console.log('retentionData', JSON.stringify(retentionData)) } res = await this.update(this.tableName, { retention: retentionData }, { _id: resultLog._id }) } if (res && res.updated) { return { code: 0, msg: 'success' } } else { return { code: 500, msg: 'retention data update failed' } } } /** * 清理实时统计的日志 * @param {Number} days 实时统计日志保留天数 */ async cleanHourLog(days = 7) { console.log('clean hour logs - day:', days) const dateTime = new DateTime() const res = await this.delete(this.tableName, { dimension: 'hour', start_time: { $lt: dateTime.getTimeBySetDays(0 - days) } }) if (!res.code) { console.log('clean hour logs - res:', res) } } }