diff --git a/build/rollup.config.cloudstat.js b/build/rollup.config.cloudstat.js new file mode 100644 index 0000000000000000000000000000000000000000..574abd5b8d9a678f14cf2882d837096046744792 --- /dev/null +++ b/build/rollup.config.cloudstat.js @@ -0,0 +1,9 @@ +module.exports = { + input: 'packages/uni-cloud-stat/src/index.js', + output: { + file: 'packages/uni-cloud-stat/dist/index.js', + format: 'es' + }, + external: ['vue', '../package.json'], + plugins: [] +} diff --git a/build/rollup.config.stat.js b/build/rollup.config.stat.js index f574b7cdf2f4003d9d4cc5488dbc24e11e41445a..0c2532eb739bbade4089d92f07acddc66b140bbb 100644 --- a/build/rollup.config.stat.js +++ b/build/rollup.config.stat.js @@ -1,9 +1,20 @@ -module.exports = { - input: 'packages/uni-stat/src/index.js', - output: { - file: 'packages/uni-stat/dist/index.js', - format: 'es' - }, - external: ['vue', '../package.json'], - plugins: [] -} +module.exports = [ + { + input: 'packages/uni-stat/src/index.js', + output: { + file: 'packages/uni-stat/dist/index.js', + format: 'es' + }, + external: ['vue', '../package.json'], + plugins: [] + }, + { + input: 'packages/uni-cloud-stat/src/index.js', + output: { + file: 'packages/uni-cloud-stat/dist/index.js', + format: 'es' + }, + external: ['vue', '../package.json'], + plugins: [] + } +] diff --git a/package.json b/package.json index 03c8192163fb7bae76ddf1d7db93650620e8361d..cdd36491ba9a2fa5bdd25b8086a90b09bfc33010 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "build:mp-weixin:wxs": "rollup -c build/rollup.config.wxs.js", "build:quickapp-native": "cross-env NODE_ENV=development node build/build.qa.js && cross-env NODE_ENV=production node build/build.qa.js", "build:runtime": "npm run lint && npm run build:mp-weixin && npm run build:mp-qq && npm run build:mp-alipay && npm run build:mp-baidu && npm run build:mp-toutiao && npm run build:mp-jd && npm run build:app-plus && npm run build:quickapp-webview && npm run build:quickapp-native && npm run build:mp-kuaishou && npm run build:mp-lark && npm run build:mp-jd", - "build:stat": "npm run lint && rollup -c build/rollup.config.stat.js", + "build:stat": "npm run lint && rollup -c build/rollup.config.stat.js", "build:web-view": "rollup -c build/rollup.config.web-view.js", "test:cli": "cross-env NODE_ENV=test jest", "test:unit": "cross-env NODE_ENV=test UNI_PLATFORM=h5 mocha-webpack --require tests/unit/setup.js --webpack-config build/webpack.config.test.js tests/unit/**/*.spec.js", diff --git a/packages/uni-cloud-stat/LICENSE b/packages/uni-cloud-stat/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..7a4a3ea2424c09fbe48d455aed1eaa94d9124835 --- /dev/null +++ b/packages/uni-cloud-stat/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/packages/uni-cloud-stat/dist/index.js b/packages/uni-cloud-stat/dist/index.js new file mode 100644 index 0000000000000000000000000000000000000000..4453182ea0b9013f8abee6e22faf01a6257437d8 --- /dev/null +++ b/packages/uni-cloud-stat/dist/index.js @@ -0,0 +1,1067 @@ +import pagesTitle from 'uni-pages?{"type":"style"}'; +import Vue from 'vue'; + +/** + * 获取系统信息 + */ +const sys = uni.getSystemInfoSync(); + +// 访问开始即启动小程序,访问结束结分为:进入后台超过5min、在前台无任何操作超过30min、在新的来源打开小程序; +const STAT_VERSION = '0.0.1'; +const PAGE_PVER_TIME = 1800; // 页面在前台无操作结束访问时间 单位s +const APP_PVER_TIME = 300; // 应用在后台结束访问时间 单位s +const OPERATING_TIME = 10; // 数据上报时间 单位s + +let pagesData = pagesTitle.pages; +let titleJsons = {}; +for (let i in pagesData) { + titleJsons[i] = pagesData[i].navigationBarTitleText || ''; +} + +const UUID_KEY = '__DC_STAT_UUID'; +const UUID_VALUE = '__DC_UUID_VALUE'; + +function getUuid() { + let uuid = ''; + if (get_platform_name() === 'n') { + try { + uuid = plus.runtime.getDCloudId(); + } catch (e) { + uuid = ''; + } + return uuid + } + + try { + uuid = uni.getStorageSync(UUID_KEY); + } catch (e) { + uuid = UUID_VALUE; + } + + if (!uuid) { + uuid = Date.now() + '' + Math.floor(Math.random() * 1e7); + try { + uni.setStorageSync(UUID_KEY, uuid); + } catch (e) { + uni.setStorageSync(UUID_KEY, UUID_VALUE); + } + } + return uuid +} + +const get_uuid = (statData) => { + // 有可能不存在 deviceId(一般不存在就是出bug了),就自己生成一个 + return sys.deviceId || getUuid() +}; + +/** + * 获取当前平台 + * 移动端 : 'n', + * h5 : 'h5', + * 微信 : 'wx', + * 阿里 : 'ali', + * 百度 : 'bd', + * 头条 : 'tt', + * qq : 'qq', + * 快应用 : 'qn', + * 快手 : 'ks', + * 飞书 : 'lark', + * 快应用 : 'qw', + * 钉钉 : 'dt' + */ +const get_platform_name = () => { + // 苹果审核代码中禁止出现 alipay 字样 ,需要特殊处理一下 + const aliArr = ['y', 'a', 'p', 'mp-ali']; + const platformList = { + 'app': 'n', + 'app-plus': 'n', + h5: 'h5', + 'mp-weixin': 'wx', + [aliArr.reverse().join('')]: 'ali', + 'mp-baidu': 'bd', + 'mp-toutiao': 'tt', + 'mp-qq': 'qq', + 'quickapp-native': 'qn', + 'mp-kuaishou': 'ks', + 'mp-lark': 'lark', + 'quickapp-webview': 'qw' + }; + if (platformList[process.env.VUE_APP_PLATFORM] === 'ali') { + if (my && my.env) { + const clientName = my.env.clientName; + if (clientName === 'ap') return 'ali' + if (clientName === 'dingtalk') return 'dt' + // TODO 缺少 ali 下的其他平台 + } + } + return platformList[process.env.VUE_APP_PLATFORM] +}; + +/** + * 获取小程序 appid + */ +const get_pack_name = () => { + let packName = ''; + if (get_platform_name() === 'wx' || get_platform_name() === 'qq') { + // 兼容微信小程序低版本基础库 + if (uni.canIUse('getAccountInfoSync')) { + packName = uni.getAccountInfoSync().miniProgram.appId || ''; + } + } + if (get_platform_name() === 'n') ; + return packName +}; + +/** + * 应用版本 + */ +const get_version = () => { + return get_platform_name() === 'n' ? plus.runtime.version : '' +}; + +/** + * 获取渠道 + */ +const get_channel = () => { + const platformName = get_platform_name(); + let channel = ''; + if (platformName === 'n') { + channel = plus.runtime.channel; + } + return channel +}; + +/** + * 获取小程序场景值 + * @param {Object} options 页面信息 + */ +const get_scene = (options) => { + const platformName = get_platform_name(); + let scene = ''; + if (options) { + return options + } + if (platformName === 'wx') { + scene = uni.getLaunchOptionsSync().scene; + } + return scene +}; + +/** + * 获取拼接参数 + */ +const get_splicing = (data) => { + let str = ''; + for (var i in data) { + str += i + '=' + data[i] + '&'; + } + return str.substr(0, str.length - 1) +}; + +/** + * 获取页面url,不包含参数 + */ +const get_route = (pageVm) => { + let _self = pageVm || get_page_vm(); + if (get_platform_name() === 'bd') { + let mp_route = _self.$mp && _self.$mp.page && _self.$mp.page.is; + let scope_route = _self.$scope && _self.$scope.is; + return mp_route || scope_route || '' + } else { + return _self.route || (_self.$scope && _self.$scope.route) || (_self.$mp && _self.$mp.page.route) + } +}; + +/** + * 获取页面url, 包含参数 + */ +const get_page_route = (pageVm) => { + // 从 app 进入应用 ,没有 $page ,获取不到路由 ,需要获取页面 尝试从 getCurrentPages 获取也页面实例 + // FIXME 尽量不使用 getCurrentPages ,大部分获取路由是从 onHide 获取 ,这时可以获取到,如果是 onload ,则可能获取不到,比如 百度 + + let page = pageVm.$page || (pageVm.$scope && pageVm.$scope.$page); + let lastPageRoute = uni.getStorageSync('_STAT_LAST_PAGE_ROUTE'); + if (!page) return lastPageRoute || '' + return page.fullPath === '/' ? page.route : page.fullPath +}; + +/** + * 获取页面实例 + */ +const get_page_vm = () => { + let pages = getCurrentPages(); + let $page = pages[pages.length - 1]; + if (!$page) return null + return $page.$vm +}; + +/** + * 获取页面类型 + */ +const get_page_types = (self) => { + // XXX 百度有问题 ,获取的都是 componet ,等待修复 + if (self.mpType === 'page' || self.$mpType === 'page' || (self.$mp && self.$mp.mpType === 'page') || self + .$options.mpType === 'page') { + return 'page'; + } + if (self.mpType === 'app' || self.$mpType === 'app' || (self.$mp && self.$mp.mpType === 'app') || self.$options + .mpType === 'app') { + return 'app' + } + return null; +}; + +/** + * 处理上报参数 + * @param {Object} 需要处理的数据 + */ +const handle_data = (statData) => { + let firstArr = []; + let contentArr = []; + let lastArr = []; + for (let i in statData) { + const rd = statData[i]; + rd.forEach((elm) => { + const newData = get_splicing(elm); + if (i === 0) { + firstArr.push(newData); + } else if (i === 3) { + lastArr.push(newData); + } else { + contentArr.push(newData); + } + }); + } + + firstArr.push(...contentArr, ...lastArr); + // 参数需要处理成字符串,方便上传 + return JSON.stringify(firstArr) +}; + + +/** + * 自定义事件参数校验 + */ +const calibration = (eventName, options) => { + // login 、 share 、pay_success 、pay_fail 、register 、title + if (!eventName) { + console.error(`uni.report Missing [eventName] parameter`); + return true + } + if (typeof eventName !== 'string') { + console.error(`uni.report [eventName] Parameter type error, it can only be of type String`); + return true + } + if (eventName.length > 255) { + console.error(`uni.report [eventName] Parameter length cannot be greater than 255`); + return true + } + + if (typeof options !== 'string' && typeof options !== 'object') { + console.error('uni.report [options] Parameter type error, Only supports String or Object type'); + return true + } + + if (typeof options === 'string' && options.length > 255) { + console.error(`uni.report [options] Parameter length cannot be greater than 255`); + return true + } + + if (eventName === 'title' && typeof options !== 'string') { + console.error( + `uni.report [eventName] When the parameter is title, the [options] parameter can only be of type String` + ); + return true + } +}; + +const get_page_name = (routepath) => { + return (titleJsons && titleJsons[routepath]) || '' +}; + +const dbSet = (name, value) => { + let data = uni.getStorageSync('$$STAT__DBDATA') || {}; + if (!data) { + data = {}; + } + data[name] = value; + uni.setStorageSync('$$STAT__DBDATA', data); +}; + +const dbGet = (name) => { + let data = uni.getStorageSync('$$STAT__DBDATA') || {}; + if (!data) { + data = {}; + } + if (!data[name]) { + return undefined + } + return data[name] +}; + +const dbRemove = (name) => { + let data = uni.getStorageSync('$$STAT__DBDATA') || {}; + if (data[name]) { + delete data[name]; + uni.setStorageSync('$$STAT__DBDATA', data); + } else { + data = uni.getStorageSync('$$STAT__DBDATA'); + if (data[name]) { + delete data[name]; + uni.setStorageSync('$$STAT__DBDATA', data); + } + } +}; + +// 首次访问时间 +const FIRST_VISIT_TIME_KEY = '__first__visit__time'; +// 最后访问时间 +const LAST_VISIT_TIME_KEY = '__last__visit__time'; +/** + * 获取当前时间 + */ +const get_time = () => { + return parseInt(new Date().getTime() / 1000) +}; + +/** + * 获取首次访问时间 + */ +const get_first_visit_time = () => { + const timeStorge = dbGet(FIRST_VISIT_TIME_KEY); + let time = 0; + if (timeStorge) { + time = timeStorge; + } else { + time = get_time(); + dbSet(FIRST_VISIT_TIME_KEY, time); + // 首次访问需要 将最后访问时间置 0 + dbRemove(LAST_VISIT_TIME_KEY); + } + return time +}; + +/** + * 最后访问时间 + */ +const get_last_visit_time = () => { + const timeStorge = dbGet(LAST_VISIT_TIME_KEY); + let time = 0; + if (timeStorge) { + time = timeStorge; + } + dbSet(LAST_VISIT_TIME_KEY, get_time()); + return time +}; + +// 页面停留时间记录key +const PAGE_RESIDENCE_TIME = '__page__residence__time'; + +/** + * 设置页面停留时间 + */ +const set_page_residence_time = () => { + let First_Page_Residence_Time = get_time(); + dbSet(PAGE_RESIDENCE_TIME, First_Page_Residence_Time); + return First_Page_Residence_Time +}; + +/** + * 获取页面停留时间 + */ +const get_page_residence_time = () => { + let Last_Page_Residence_Time = get_time(); + let First_Page_Residence_Time = dbGet(PAGE_RESIDENCE_TIME); + return Last_Page_Residence_Time - First_Page_Residence_Time +}; + +/** + * 获取总访问次数 + */ +const TOTAL_VISIT_COUNT = '__total__visit__count'; +const get_total_visit_count = () => { + const timeStorge = dbGet(TOTAL_VISIT_COUNT); + let count = 1; + if (timeStorge) { + count = timeStorge; + count++; + } + dbSet(TOTAL_VISIT_COUNT, count); + return count +}; + +let Set__First__Time = 0; +let Set__Last__Time = 0; + +/** + * 获取第一次时间 + */ +const get_first_time = () => { + let time = new Date().getTime(); + Set__First__Time = time; + Set__Last__Time = 0; + return time +}; + +/** + * 获取最后一次时间 + */ +const get_last_time = () => { + let time = new Date().getTime(); + Set__Last__Time = time; + return time +}; + +/** + * 获取页面 \ 应用停留时间 + */ +const get_residence_time = (type) => { + let residenceTime = 0; + if (Set__First__Time !== 0) { + residenceTime = Set__Last__Time - Set__First__Time; + } + + residenceTime = parseInt(residenceTime / 1000); + residenceTime = residenceTime < 1 ? 1 : residenceTime; + if (type === 'app') { + let overtime = residenceTime > APP_PVER_TIME ? true : false; + return { + residenceTime, + overtime, + } + } + if (type === 'page') { + let overtime = residenceTime > PAGE_PVER_TIME ? true : false; + return { + residenceTime, + overtime, + } + } + return { + residenceTime, + } +}; + +let statConfig = require('uni-stat-config').default || require('uni-stat-config'); + +// 统计数据默认值 +let statData = { + uuid: get_uuid(), // 设备标识 + ut: get_platform_name(), // 平台类型 + mpn: get_pack_name(), // 原生平台包名、小程序 appid + ak: statConfig.appid, // uni-app 应用 Appid + usv: STAT_VERSION, // 统计 sdk 版本 + v: get_version(), // 应用版本,仅app + ch: get_channel(), // 渠道信息 + cn: '', // 国家 + pn: '', // 省份 + ct: '', // 城市 + t: get_time(), // 上报数据时的时间戳 + tt: '', + p: sys.platform === 'android' ? 'a' : 'i', // 手机系统 + brand: sys.brand || '', // 手机品牌 + md: sys.model, // 手机型号 + sv: sys.system.replace(/(Android|iOS)\s/, ''), // 手机系统版本 + mpsdk: sys.SDKVersion || '', // x程序 sdk version + mpv: sys.version || '', // 小程序平台版本 ,如微信、支付宝 + lang: sys.language, // 语言 + pr: sys.pixelRatio, // pixelRatio 设备像素比 + ww: sys.windowWidth, // windowWidth 可使用窗口宽度 + wh: sys.windowHeight, // windowHeight 可使用窗口高度 + sw: sys.screenWidth, // screenWidth 屏幕宽度 + sh: sys.screenHeight, // screenHeight 屏幕高度 +}; +class Report { + constructor() { + // 页面实例 + this.self = ''; + // 进入应用标识 + this.__licationShow = false; + // 离开应用标识 + this.__licationHide = false; + // 统计默认值 + this.statData = statData; + // 标题默认值 + this._navigationBarTitle = { + config: '', + page: '', + report: '', + lt: '', + }; + + // 页面参数 + this._query = {}; + // 页面最后停留页面的 url + // this._lastPageRoute = '' + + // 注册拦截器 + let registerInterceptor = typeof uni.addInterceptor === 'function'; + if (registerInterceptor) { + this.addInterceptorInit(); + this.interceptLogin(); + this.interceptShare(true); + this.interceptRequestPayment(); + } + } + + addInterceptorInit() { + let self = this; + uni.addInterceptor('setNavigationBarTitle', { + invoke(args) { + self._navigationBarTitle.page = args.title; + }, + }); + } + + interceptLogin() { + let self = this; + uni.addInterceptor('login', { + complete() { + self._login(); + }, + }); + } + + interceptShare(type) { + let self = this; + if (!type) { + self._share(); + return + } + uni.addInterceptor('share', { + success() { + self._share(); + }, + fail() { + self._share(); + }, + }); + } + + interceptRequestPayment() { + let self = this; + uni.addInterceptor('requestPayment', { + success() { + self._payment('pay_success'); + }, + fail() { + self._payment('pay_fail'); + }, + }); + } + + _login() { + this.sendEventRequest({ + key: 'login', + }, + 0 + ); + } + + _share() { + this.sendEventRequest({ + key: 'share', + }, 0); + } + _payment(key) { + this.sendEventRequest({ + key, + }, 0); + } + + /** + * 进入应用触发 + */ + applicationShow() { + // 通过 __licationHide 判断保证是进入后台后在次进入应用,避免重复上报数据 + if (this.__licationHide) { + get_last_time(); + const time = get_residence_time('app'); + // 需要判断进入后台是否超过时限 ,默认是 30min ,是的话需要执行进入应用的上报 + if (time.overtime) { + let lastPageRoute = uni.getStorageSync('_STAT_LAST_PAGE_ROUTE'); + let options = { + path: lastPageRoute, + scene: this.statData.sc, + }; + this.sendReportRequest(options); + } + // 状态重置 + this.__licationHide = false; + } + } + + /** + * 离开应用触发 + * @param {Object} self + * @param {Object} type + */ + applicationHide(self, type) { + // 进入应用后台保存状态,方便进入前台后判断是否上报应用数据 + this.__licationHide = true; + get_last_time(); + const time = get_residence_time(); + const route = get_page_route(self); + // this._lastPageRoute = route + uni.setStorageSync('_STAT_LAST_PAGE_ROUTE', route); + this.sendHideRequest({ + urlref: route, + urlref_ts: time.residenceTime, + }, + type + ); + // 重置时间 + get_first_time(); + } + + /** + * 进入页面触发 + */ + pageShow(self) { + // 清空值 ,初始化 ,避免污染后面的上报数据 + this._navigationBarTitle = { + config: '', + page: '', + report: '', + lt: '', + }; + + const route = get_page_route(self); + const routepath = get_route(self); + + this._navigationBarTitle.config = get_page_name(routepath); + // 表示应用触发 ,页面切换不触发之后的逻辑 + if (this.__licationShow) { + get_first_time(); + // this._lastPageRoute = route + uni.setStorageSync('_STAT_LAST_PAGE_ROUTE', route); + this.__licationShow = false; + return + } + + get_last_time(); + + const time = get_residence_time('page'); + // 停留时间 + if (time.overtime) { + let options = { + path: route, + scene: this.statData.sc, + }; + this.sendReportRequest(options); + } + // 重置时间 + get_first_time(); + } + + /** + * 离开页面触发 + */ + pageHide(self) { + if (!this.__licationHide) { + get_last_time(); + const time = get_residence_time('page'); + let route = get_page_route(self); + let lastPageRoute = uni.getStorageSync('_STAT_LAST_PAGE_ROUTE'); + if (!lastPageRoute) { + lastPageRoute = route; + } + uni.setStorageSync('_STAT_LAST_PAGE_ROUTE', route); + this.sendPageRequest({ + url: route, + urlref: lastPageRoute, + urlref_ts: time.residenceTime, + }); + // this._lastPageRoute = route + return + } + } + + + /** + * 发送请求,应用维度上报 + * @param {Object} options 页面信息 + */ + sendReportRequest(options) { + this._navigationBarTitle.lt = '1'; + this._navigationBarTitle.config = get_page_name(options.path); + let is_opt = options.query && JSON.stringify(options.query) !== '{}'; + let query = is_opt ? '?' + JSON.stringify(options.query) : ''; + Object.assign(this.statData, { + lt: '1', + url: (options.path + query) || '', + t: get_time(), + sc: get_scene(options.scene), + fvts: get_first_visit_time(), + lvts: get_last_visit_time(), + tvc: get_total_visit_count() + }); + if (get_platform_name() === 'n') { + this.getProperty(); + } else { + this.getNetworkInfo(); + } + } + + /** + * 发送请求,页面维度上报 + * @param {Object} opt + */ + sendPageRequest(opt) { + let { + url, + urlref, + urlref_ts + } = opt; + this._navigationBarTitle.lt = '11'; + let options = { + ak: this.statData.ak, + uuid: this.statData.uuid, + lt: '11', + ut: this.statData.ut, + url, + tt: this.statData.tt, + urlref, + urlref_ts, + ch: this.statData.ch, + usv: this.statData.usv, + t: get_time(), + p: this.statData.p, + }; + this.request(options); + } + + /** + * 进入后台上报数据 + * @param {Object} opt + * @param {Object} type + */ + sendHideRequest(opt, type) { + let { + urlref, + urlref_ts + } = opt; + let options = { + ak: this.statData.ak, + uuid: this.statData.uuid, + lt: '3', + ut: this.statData.ut, + urlref, + urlref_ts, + ch: this.statData.ch, + usv: this.statData.usv, + t: get_time(), + p: this.statData.p, + }; + this.request(options, type); + } + + /** + * 自定义事件上报 + */ + sendEventRequest({ + key = '', + value = '' + } = {}) { + // const route = this._lastPageRoute + const routepath = get_route(); + this._navigationBarTitle.config = get_page_name(routepath); + this._navigationBarTitle.lt = '21'; + let options = { + ak: this.statData.ak, + uuid: this.statData.uuid, + lt: '21', + ut: this.statData.ut, + url: routepath, + ch: this.statData.ch, + e_n: key, + e_v: typeof value === 'object' ? JSON.stringify(value) : value.toString(), + usv: this.statData.usv, + t: get_time(), + p: this.statData.p, + }; + this.request(options); + } + + /** + * 获取wgt资源版本 + */ + getProperty() { + plus.runtime.getProperty(plus.runtime.appid, (wgtinfo) => { + this.statData.v = wgtinfo.version || ''; + this.getNetworkInfo(); + }); + } + + /** + * 获取网络信息 + */ + getNetworkInfo() { + uni.getNetworkType({ + success: (result) => { + this.statData.net = result.networkType; + this.getLocation(); + }, + }); + } + + /** + * 获取位置信息 + */ + getLocation() { + if (statConfig.getLocation) { + uni.getLocation({ + type: 'wgs84', + geocode: true, + success: (result) => { + if (result.address) { + this.statData.cn = result.address.country; + this.statData.pn = result.address.province; + this.statData.ct = result.address.city; + } + + this.statData.lat = result.latitude; + this.statData.lng = result.longitude; + this.request(this.statData); + }, + }); + } else { + this.statData.lat = 0; + this.statData.lng = 0; + this.request(this.statData); + } + } + + /** + * 发送请求 + * @param {Object} data 上报数据 + * @param {Object} type 类型 + */ + request(data, type) { + let time = get_time(); + const title = this._navigationBarTitle; + Object.assign(data, { + ttn: title.page, + ttpj: title.config, + ttc: title.report + }); + + let uniStatData = dbGet('__UNI__STAT__DATA') || {}; + if (!uniStatData[data.lt]) { + uniStatData[data.lt] = []; + } + // 加入队列 + uniStatData[data.lt].push(data); + dbSet('__UNI__STAT__DATA', uniStatData); + + let page_residence_time = get_page_residence_time(); + // 判断时候到达上报时间 ,默认 10 秒上报 + if (page_residence_time < OPERATING_TIME && !type) return + + // 时间超过,重新获取时间戳 + set_page_residence_time(); + const stat_data = handle_data(uniStatData); + let optionsData = { + usv: STAT_VERSION, //统计 SDK 版本号 + t: time, //发送请求时的时间戮 + requests: stat_data, + }; + + // 重置队列 + dbRemove('__UNI__STAT__DATA'); + + // XXX 安卓需要延迟上报 ,否则会有未知错误,需要验证处理 + if (get_platform_name() === 'n' && this.statData.p === 'a') { + setTimeout(() => { + this.sendRequest(optionsData); + }, 200); + return + } + + this.sendRequest(optionsData); + } + + + /** + * 数据上报 + * @param {Object} optionsData 需要上报的数据 + */ + sendRequest(optionsData) { + + if (!uniCloud.config) { + console.error('当前尚未绑定服务空间.'); + return + } + uniCloud.callFunction({ + name: 'uni-stat-report', + data: optionsData, + success: (res) => {}, + fail: (err) => { + console.log(err); + } + }); + } + + sendEvent(key, value) { + // 校验 type 参数 + if (calibration(key, value)) return + + if (key === 'title') { + this._navigationBarTitle.report = value; + return + } + this.sendEventRequest({ + key, + value: typeof value === 'object' ? JSON.stringify(value) : value, + }, + 1 + ); + } +} + +let vue = (Vue.default || Vue); + +class Stat extends Report { + static getInstance() { + if (!vue.instance) { + vue.instance = new Stat(); + } + return vue.instance + } + constructor() { + super(); + this.instance = null; + } + + /** + * 进入应用 + * @param {Object} options 页面参数 + * @param {Object} self 当前页面实例 + */ + launch(options, self) { + // 初始化页面停留时间 start + let residence_time = set_page_residence_time(); + this.__licationShow = true; + this.sendReportRequest(options, true); + } + load(options, self) { + this.self = self; + this._query = options; + } + + appHide(self){ + this.applicationHide(self, true); + } + + appShow(self){ + this.applicationShow(self); + } + + show(self) { + this.self = self; + if (get_page_types(self) === 'page') { + this.pageShow(self); + } + if (get_page_types(self) === 'app') { + this.appShow(self); + } + } + + hide(self) { + this.self = self; + if (get_page_types(self) === 'page') { + this.pageHide(self); + } + if (get_page_types(self) === 'app') { + this.appHide(self); + } + } + + error(em) { + // 开发工具内不上报错误 + if (this._platform === 'devtools') { + if (process.env.NODE_ENV === 'development') { + console.info('当前运行环境为开发者工具,不上报数据。'); + return; + } + } + let emVal = ''; + if (!em.message) { + emVal = JSON.stringify(em); + } else { + emVal = em.stack; + } + let options = { + ak: this.statData.ak, + uuid: this.statData.uuid, + lt: '31', + ut: this.statData.ut, + ch: this.statData.ch, + mpsdk: this.statData.mpsdk, + mpv: this.statData.mpv, + v: this.statData.v, + em: emVal, + usv: this.statData.usv, + t: parseInt(new Date().getTime() / 1000), + p: this.statData.p, + }; + this.request(options); + } +} + +const stat = Stat.getInstance(); + +// 用于判断是隐藏页面还是卸载页面 +let isHide = false; + +const lifecycle = { + onLaunch(options) { + // 进入应用上报数据 + stat.launch(options, this); + }, + onLoad(options) { + stat.load(options, this); + // 重写分享,获取分享上报事件 + if (this.$scope && this.$scope.onShareAppMessage) { + let oldShareAppMessage = this.$scope.onShareAppMessage; + this.$scope.onShareAppMessage = function(options) { + stat.interceptShare(false); + return oldShareAppMessage.call(this, options) + }; + } + }, + onShow() { + isHide = false; + stat.show(this); + }, + onHide() { + isHide = true; + stat.hide(this); + }, + onUnload() { + if (isHide) { + isHide = false; + return + } + stat.hide(this); + }, + onError(e) { + stat.error(e); + } +}; + + +function main() { + if (process.env.NODE_ENV === 'development') { + uni.report = function(type, options) {}; + } else { + console.log('uniCloud统计已开启'); + const Vue = require('vue'); + (Vue.default || Vue).mixin(lifecycle); + uni.report = function(type, options) { + stat.sendEvent(type, options); + }; + } +} + +main(); diff --git a/packages/uni-cloud-stat/package.json b/packages/uni-cloud-stat/package.json new file mode 100644 index 0000000000000000000000000000000000000000..8a128915d5aced120398a3a0c9fbb2b0d32f6231 --- /dev/null +++ b/packages/uni-cloud-stat/package.json @@ -0,0 +1,38 @@ +{ + "name": "@dcloudio/uni-cloud-stat", + "version": "2.0.0-32920211029001", + "description": "", + "main": "dist/index.js", + "repository": { + "type": "git", + "url": "git+https://github.com/dcloudio/uni-app.git", + "directory": "packages/uni-cloud-stat" + }, + "scripts": { + "dev": "NODE_ENV=development rollup -w -c rollup.config.js", + "build": "NODE_ENV=production rollup -c rollup.config.js" + }, + "files": [ + "dist", + "package.json", + "LICENSE" + ], + "author": "", + "license": "Apache-2.0", + "devDependencies": { + "@babel/core": "^7.5.5", + "@babel/preset-env": "^7.5.5", + "eslint": "^6.1.0", + "rollup": "^1.19.3", + "rollup-plugin-babel": "^4.3.3", + "rollup-plugin-clear": "^2.0.7", + "rollup-plugin-commonjs": "^10.0.2", + "rollup-plugin-copy": "^3.1.0", + "rollup-plugin-eslint": "^7.0.0", + "rollup-plugin-json": "^4.0.0", + "rollup-plugin-node-resolve": "^5.2.0", + "rollup-plugin-replace": "^2.2.0", + "rollup-plugin-uglify": "^6.0.2" + }, + "gitHead": "9e2d0f8e244724fcd64880316c57d837d1778cf8" +} diff --git a/packages/uni-cloud-stat/src/config.js b/packages/uni-cloud-stat/src/config.js new file mode 100644 index 0000000000000000000000000000000000000000..22ddb49ee80f1ea05f900e97fb478466098fe121 --- /dev/null +++ b/packages/uni-cloud-stat/src/config.js @@ -0,0 +1,11 @@ +// 访问开始即启动小程序,访问结束结分为:进入后台超过5min、在前台无任何操作超过30min、在新的来源打开小程序; +export const STAT_VERSION = '0.0.1' +export const STAT_URL = 'https://tongji.dcloud.io/uni/stat' +export const STAT_H5_URL = 'https://tongji.dcloud.io/uni/stat.gif' +export const STAT_KEY = 'qkTHEIegZGcL5iy3' +export const PAGE_PVER_TIME = 1800 // 页面在前台无操作结束访问时间 单位s +export const APP_PVER_TIME = 300 // 应用在后台结束访问时间 单位s +export const OPERATING_TIME = 10 // 数据上报时间 单位s +export const DIFF_TIME = 60 * 1000 * 60 * 24 + +export const DEBUG = true diff --git a/packages/uni-cloud-stat/src/core/report.js b/packages/uni-cloud-stat/src/core/report.js new file mode 100644 index 0000000000000000000000000000000000000000..705750a7e6345f59b4a06b4a1c2ded30c08ac84d --- /dev/null +++ b/packages/uni-cloud-stat/src/core/report.js @@ -0,0 +1,523 @@ +let statConfig = require('uni-stat-config').default || require('uni-stat-config'); + +import { + get_time, + set_page_residence_time, + get_first_visit_time, + get_last_visit_time, + get_total_visit_count, + get_page_residence_time, + get_first_time, + get_last_time, + get_residence_time +} from '../utils/pageTime.js' + +import { + get_uuid, + get_platform_name, + get_pack_name, + get_scene, + get_version, + get_channel, + get_splicing, + get_page_route, + get_route, + handle_data, + calibration, + get_page_name, + get_sgin, + get_encodeURIComponent_options +} from '../utils/pageInfo.js' + +import { + sys +} from '../utils/util.js' + +import { + STAT_VERSION, + OPERATING_TIME, + STAT_URL, + STAT_H5_URL, + DEBUG +} from '../config.js'; + +import { + dbSet, + dbGet, + dbRemove +} from '../utils/db.js' + +// 统计数据默认值 +let statData = { + uuid: get_uuid(), // 设备标识 + ut: get_platform_name(), // 平台类型 + mpn: get_pack_name(), // 原生平台包名、小程序 appid + ak: statConfig.appid, // uni-app 应用 Appid + usv: STAT_VERSION, // 统计 sdk 版本 + v: get_version(), // 应用版本,仅app + ch: get_channel(), // 渠道信息 + cn: '', // 国家 + pn: '', // 省份 + ct: '', // 城市 + t: get_time(), // 上报数据时的时间戳 + tt: '', + p: sys.platform === 'android' ? 'a' : 'i', // 手机系统 + brand: sys.brand || '', // 手机品牌 + md: sys.model, // 手机型号 + sv: sys.system.replace(/(Android|iOS)\s/, ''), // 手机系统版本 + mpsdk: sys.SDKVersion || '', // x程序 sdk version + mpv: sys.version || '', // 小程序平台版本 ,如微信、支付宝 + lang: sys.language, // 语言 + pr: sys.pixelRatio, // pixelRatio 设备像素比 + ww: sys.windowWidth, // windowWidth 可使用窗口宽度 + wh: sys.windowHeight, // windowHeight 可使用窗口高度 + sw: sys.screenWidth, // screenWidth 屏幕宽度 + sh: sys.screenHeight, // screenHeight 屏幕高度 +} +export default class Report { + constructor() { + // 页面实例 + this.self = '' + // 进入应用标识 + this.__licationShow = false + // 离开应用标识 + this.__licationHide = false + // 统计默认值 + this.statData = statData + // 标题默认值 + this._navigationBarTitle = { + config: '', + page: '', + report: '', + lt: '', + } + + // 页面参数 + this._query = {} + // 页面最后停留页面的 url + // this._lastPageRoute = '' + + // 注册拦截器 + let registerInterceptor = typeof uni.addInterceptor === 'function' + if (registerInterceptor) { + this.addInterceptorInit() + this.interceptLogin() + this.interceptShare(true) + this.interceptRequestPayment() + } + } + + addInterceptorInit() { + let self = this + uni.addInterceptor('setNavigationBarTitle', { + invoke(args) { + self._navigationBarTitle.page = args.title + }, + }) + } + + interceptLogin() { + let self = this + uni.addInterceptor('login', { + complete() { + self._login() + }, + }) + } + + interceptShare(type) { + let self = this + if (!type) { + self._share() + return + } + uni.addInterceptor('share', { + success() { + self._share() + }, + fail() { + self._share() + }, + }) + } + + interceptRequestPayment() { + let self = this + uni.addInterceptor('requestPayment', { + success() { + self._payment('pay_success') + }, + fail() { + self._payment('pay_fail') + }, + }) + } + + _login() { + this.sendEventRequest({ + key: 'login', + }, + 0 + ) + } + + _share() { + this.sendEventRequest({ + key: 'share', + }, 0) + } + _payment(key) { + this.sendEventRequest({ + key, + }, 0) + } + + /** + * 进入应用触发 + */ + applicationShow() { + // 通过 __licationHide 判断保证是进入后台后在次进入应用,避免重复上报数据 + if (this.__licationHide) { + get_last_time() + const time = get_residence_time('app') + // 需要判断进入后台是否超过时限 ,默认是 30min ,是的话需要执行进入应用的上报 + if (time.overtime) { + let lastPageRoute = uni.getStorageSync('_STAT_LAST_PAGE_ROUTE') + let options = { + path: lastPageRoute, + scene: this.statData.sc, + } + this.sendReportRequest(options) + } + // 状态重置 + this.__licationHide = false + } + } + + /** + * 离开应用触发 + * @param {Object} self + * @param {Object} type + */ + applicationHide(self, type) { + // 进入应用后台保存状态,方便进入前台后判断是否上报应用数据 + this.__licationHide = true + get_last_time() + const time = get_residence_time() + const route = get_page_route(self) + // this._lastPageRoute = route + uni.setStorageSync('_STAT_LAST_PAGE_ROUTE', route) + this.sendHideRequest({ + urlref: route, + urlref_ts: time.residenceTime, + }, + type + ) + // 重置时间 + get_first_time() + } + + /** + * 进入页面触发 + */ + pageShow(self) { + // 清空值 ,初始化 ,避免污染后面的上报数据 + this._navigationBarTitle = { + config: '', + page: '', + report: '', + lt: '', + } + + const route = get_page_route(self) + const routepath = get_route(self) + + this._navigationBarTitle.config = get_page_name(routepath) + // 表示应用触发 ,页面切换不触发之后的逻辑 + if (this.__licationShow) { + get_first_time() + // this._lastPageRoute = route + uni.setStorageSync('_STAT_LAST_PAGE_ROUTE', route) + this.__licationShow = false + return + } + + get_last_time() + + const time = get_residence_time('page') + // 停留时间 + if (time.overtime) { + let options = { + path: route, + scene: this.statData.sc, + } + this.sendReportRequest(options) + } + // 重置时间 + get_first_time() + } + + /** + * 离开页面触发 + */ + pageHide(self) { + if (!this.__licationHide) { + get_last_time() + const time = get_residence_time('page') + let route = get_page_route(self) + let lastPageRoute = uni.getStorageSync('_STAT_LAST_PAGE_ROUTE') + if (!lastPageRoute) { + lastPageRoute = route + } + uni.setStorageSync('_STAT_LAST_PAGE_ROUTE', route) + this.sendPageRequest({ + url: route, + urlref: lastPageRoute, + urlref_ts: time.residenceTime, + }) + // this._lastPageRoute = route + return + } + } + + + /** + * 发送请求,应用维度上报 + * @param {Object} options 页面信息 + */ + sendReportRequest(options) { + this._navigationBarTitle.lt = '1' + this._navigationBarTitle.config = get_page_name(options.path) + let is_opt = options.query && JSON.stringify(options.query) !== '{}' + let query = is_opt ? '?' + JSON.stringify(options.query) : '' + Object.assign(this.statData, { + lt: '1', + url: (options.path + query) || '', + t: get_time(), + sc: get_scene(options.scene), + fvts: get_first_visit_time(), + lvts: get_last_visit_time(), + tvc: get_total_visit_count() + }) + if (get_platform_name() === 'n') { + this.getProperty() + } else { + this.getNetworkInfo() + } + } + + /** + * 发送请求,页面维度上报 + * @param {Object} opt + */ + sendPageRequest(opt) { + let { + url, + urlref, + urlref_ts + } = opt + this._navigationBarTitle.lt = '11' + let options = { + ak: this.statData.ak, + uuid: this.statData.uuid, + lt: '11', + ut: this.statData.ut, + url, + tt: this.statData.tt, + urlref, + urlref_ts, + ch: this.statData.ch, + usv: this.statData.usv, + t: get_time(), + p: this.statData.p, + } + this.request(options) + } + + /** + * 进入后台上报数据 + * @param {Object} opt + * @param {Object} type + */ + sendHideRequest(opt, type) { + let { + urlref, + urlref_ts + } = opt + let options = { + ak: this.statData.ak, + uuid: this.statData.uuid, + lt: '3', + ut: this.statData.ut, + urlref, + urlref_ts, + ch: this.statData.ch, + usv: this.statData.usv, + t: get_time(), + p: this.statData.p, + } + this.request(options, type) + } + + /** + * 自定义事件上报 + */ + sendEventRequest({ + key = '', + value = '' + } = {}) { + // const route = this._lastPageRoute + const routepath = get_route() + this._navigationBarTitle.config = get_page_name(routepath) + this._navigationBarTitle.lt = '21' + let options = { + ak: this.statData.ak, + uuid: this.statData.uuid, + lt: '21', + ut: this.statData.ut, + url: routepath, + ch: this.statData.ch, + e_n: key, + e_v: typeof value === 'object' ? JSON.stringify(value) : value.toString(), + usv: this.statData.usv, + t: get_time(), + p: this.statData.p, + } + this.request(options) + } + + /** + * 获取wgt资源版本 + */ + getProperty() { + plus.runtime.getProperty(plus.runtime.appid, (wgtinfo) => { + this.statData.v = wgtinfo.version || '' + this.getNetworkInfo() + }) + } + + /** + * 获取网络信息 + */ + getNetworkInfo() { + uni.getNetworkType({ + success: (result) => { + this.statData.net = result.networkType + this.getLocation() + }, + }) + } + + /** + * 获取位置信息 + */ + getLocation() { + if (statConfig.getLocation) { + uni.getLocation({ + type: 'wgs84', + geocode: true, + success: (result) => { + if (result.address) { + this.statData.cn = result.address.country + this.statData.pn = result.address.province + this.statData.ct = result.address.city + } + + this.statData.lat = result.latitude + this.statData.lng = result.longitude + this.request(this.statData) + }, + }) + } else { + this.statData.lat = 0 + this.statData.lng = 0 + this.request(this.statData) + } + } + + /** + * 发送请求 + * @param {Object} data 上报数据 + * @param {Object} type 类型 + */ + request(data, type) { + let time = get_time() + const title = this._navigationBarTitle + Object.assign(data, { + ttn: title.page, + ttpj: title.config, + ttc: title.report + }) + + let uniStatData = dbGet('__UNI__STAT__DATA') || {} + if (!uniStatData[data.lt]) { + uniStatData[data.lt] = [] + } + // 加入队列 + uniStatData[data.lt].push(data) + dbSet('__UNI__STAT__DATA', uniStatData) + + let page_residence_time = get_page_residence_time() + // 判断时候到达上报时间 ,默认 10 秒上报 + if (page_residence_time < OPERATING_TIME && !type) return + + // 时间超过,重新获取时间戳 + set_page_residence_time() + const stat_data = handle_data(uniStatData) + let optionsData = { + usv: STAT_VERSION, //统计 SDK 版本号 + t: time, //发送请求时的时间戮 + requests: stat_data, + } + + // 重置队列 + dbRemove('__UNI__STAT__DATA') + + // XXX 安卓需要延迟上报 ,否则会有未知错误,需要验证处理 + if (get_platform_name() === 'n' && this.statData.p === 'a') { + setTimeout(() => { + this.sendRequest(optionsData) + }, 200) + return + } + + this.sendRequest(optionsData) + } + + + /** + * 数据上报 + * @param {Object} optionsData 需要上报的数据 + */ + sendRequest(optionsData) { + + if (!uniCloud.config) { + console.error('当前尚未绑定服务空间.') + return + } + uniCloud.callFunction({ + name: 'uni-stat-report', + data: optionsData, + success: (res) => {}, + fail: (err) => { + console.log(err); + } + }) + } + + sendEvent(key, value) { + // 校验 type 参数 + if (calibration(key, value)) return + + if (key === 'title') { + this._navigationBarTitle.report = value + return + } + this.sendEventRequest({ + key, + value: typeof value === 'object' ? JSON.stringify(value) : value, + }, + 1 + ) + } +} diff --git a/packages/uni-cloud-stat/src/core/stat.js b/packages/uni-cloud-stat/src/core/stat.js new file mode 100644 index 0000000000000000000000000000000000000000..f7bd623893bfaf80185758bc60938894e5f6720a --- /dev/null +++ b/packages/uni-cloud-stat/src/core/stat.js @@ -0,0 +1,99 @@ +import Report from './report.js' +import Vue from 'vue' +let vue = (Vue.default || Vue) +import { + set_page_residence_time +} from '../utils/pageTime.js' +import { + get_page_types, + get_platform_name +} from '../utils/pageInfo.js' + +class Stat extends Report { + static getInstance() { + if (!vue.instance) { + vue.instance = new Stat() + } + return vue.instance + } + constructor() { + super() + this.instance = null + } + + /** + * 进入应用 + * @param {Object} options 页面参数 + * @param {Object} self 当前页面实例 + */ + launch(options, self) { + // 初始化页面停留时间 start + let residence_time = set_page_residence_time() + this.__licationShow = true + this.sendReportRequest(options, true) + } + load(options, self) { + this.self = self + this._query = options + } + + appHide(self){ + this.applicationHide(self, true) + } + + appShow(self){ + this.applicationShow(self) + } + + show(self) { + this.self = self + if (get_page_types(self) === 'page') { + this.pageShow(self) + } + if (get_page_types(self) === 'app') { + this.appShow(self) + } + } + + hide(self) { + this.self = self + if (get_page_types(self) === 'page') { + this.pageHide(self) + } + if (get_page_types(self) === 'app') { + this.appHide(self) + } + } + + error(em) { + // 开发工具内不上报错误 + if (this._platform === 'devtools') { + if (process.env.NODE_ENV === 'development') { + console.info('当前运行环境为开发者工具,不上报数据。') + return; + } + } + let emVal = '' + if (!em.message) { + emVal = JSON.stringify(em) + } else { + emVal = em.stack + } + let options = { + ak: this.statData.ak, + uuid: this.statData.uuid, + lt: '31', + ut: this.statData.ut, + ch: this.statData.ch, + mpsdk: this.statData.mpsdk, + mpv: this.statData.mpv, + v: this.statData.v, + em: emVal, + usv: this.statData.usv, + t: parseInt(new Date().getTime() / 1000), + p: this.statData.p, + } + this.request(options) + } +} +export default Stat diff --git a/packages/uni-cloud-stat/src/index.js b/packages/uni-cloud-stat/src/index.js new file mode 100644 index 0000000000000000000000000000000000000000..a6485a953893070311201ef74685c6b89ffc4f65 --- /dev/null +++ b/packages/uni-cloud-stat/src/index.js @@ -0,0 +1,61 @@ +import { + get_platform_name, + get_page_vm +} from './utils/pageInfo.js' +import Stat from './core/stat.js' +const stat = Stat.getInstance() + +// 用于判断是隐藏页面还是卸载页面 +let isHide = false + +const lifecycle = { + onLaunch(options) { + // 进入应用上报数据 + stat.launch(options, this); + }, + onLoad(options) { + stat.load(options, this); + // 重写分享,获取分享上报事件 + if (this.$scope && this.$scope.onShareAppMessage) { + let oldShareAppMessage = this.$scope.onShareAppMessage; + this.$scope.onShareAppMessage = function(options) { + stat.interceptShare(false); + return oldShareAppMessage.call(this, options) + } + } + }, + onShow() { + isHide = false + stat.show(this); + }, + onHide() { + isHide = true + stat.hide(this); + }, + onUnload() { + if (isHide) { + isHide = false + return + } + stat.hide(this); + }, + onError(e) { + stat.error(e) + } +} + + +function main() { + if (process.env.NODE_ENV === 'development') { + uni.report = function(type, options) {}; + } else { + console.log('uniCloud统计已开启'); + const Vue = require('vue'); + (Vue.default || Vue).mixin(lifecycle); + uni.report = function(type, options) { + stat.sendEvent(type, options); + }; + } +} + +main() diff --git a/packages/uni-cloud-stat/src/utils/config.js b/packages/uni-cloud-stat/src/utils/config.js new file mode 100644 index 0000000000000000000000000000000000000000..24c21c6bb405c6b2028704f54b4ee8d2d0b95612 --- /dev/null +++ b/packages/uni-cloud-stat/src/utils/config.js @@ -0,0 +1,11 @@ +// 访问开始即启动小程序,访问结束结分为:进入后台超过5min、在前台无任何操作超过30min、在新的来源打开小程序; +export const STAT_VERSION = '0.0.1' + +export const PAGE_PVER_TIME = 1800 // 页面在前台无操作结束访问时间 单位s +// export const PAGE_PVER_TIME = 0 +export const APP_PVER_TIME = 300 // 应用在后台结束访问时间 单位s +// export const APP_PVER_TIME = 0 +// export const OPERATING_TIME = 10 // 数据上报时间 单位s +export const OPERATING_TIME = 10 // 数据上报时间 单位s + +export const DEBUG = true diff --git a/packages/uni-cloud-stat/src/utils/db.js b/packages/uni-cloud-stat/src/utils/db.js new file mode 100755 index 0000000000000000000000000000000000000000..434f7114b95ddfe3ceaf31aad34f91c1c1a42ef1 --- /dev/null +++ b/packages/uni-cloud-stat/src/utils/db.js @@ -0,0 +1,33 @@ +export const dbSet = (name, value) => { + let data = uni.getStorageSync('$$STAT__DBDATA') || {} + if (!data) { + data = {} + } + data[name] = value + uni.setStorageSync('$$STAT__DBDATA', data) +} + +export const dbGet = (name) => { + let data = uni.getStorageSync('$$STAT__DBDATA') || {} + if (!data) { + data = {} + } + if (!data[name]) { + return undefined + } + return data[name] +} + +export const dbRemove = (name) => { + let data = uni.getStorageSync('$$STAT__DBDATA') || {} + if (data[name]) { + delete data[name] + uni.setStorageSync('$$STAT__DBDATA', data) + } else { + data = uni.getStorageSync('$$STAT__DBDATA') + if (data[name]) { + delete data[name] + uni.setStorageSync('$$STAT__DBDATA', data) + } + } +} diff --git a/packages/uni-cloud-stat/src/utils/deepClone.js b/packages/uni-cloud-stat/src/utils/deepClone.js new file mode 100755 index 0000000000000000000000000000000000000000..d356c5491442764272a0076d98765c0720bcecfe --- /dev/null +++ b/packages/uni-cloud-stat/src/utils/deepClone.js @@ -0,0 +1,22 @@ +// 判断arr是否为一个数组,返回一个bool值 +function isArray (arr) { + return Object.prototype.toString.call(arr) === '[object Array]'; +} + +// 深度克隆 +export const deepClone = (obj)=> { + // 对常见的“非”值,直接返回原来值 + if([null, undefined, NaN, false].includes(obj)) return obj; + if(typeof obj !== "object" && typeof obj !== 'function') { + //原始类型直接返回 + return obj; + } + var o = isArray(obj) ? [] : {}; + for(let i in obj) { + if(obj.hasOwnProperty(i)){ + o[i] = typeof obj[i] === "object" ? deepClone(obj[i]) : obj[i]; + } + } + return o; +} + diff --git a/packages/uni-cloud-stat/src/utils/deepMerge.js b/packages/uni-cloud-stat/src/utils/deepMerge.js new file mode 100755 index 0000000000000000000000000000000000000000..23d664cac58823849c27761bdae700f58e5491ea --- /dev/null +++ b/packages/uni-cloud-stat/src/utils/deepMerge.js @@ -0,0 +1,27 @@ +import {deepClone} from './deepClone.js' +// JS对象深度合并 +export const deepMerge = (target = {}, source = {})=> { + target = deepClone(target); + if (typeof target !== 'object' || typeof source !== 'object') return false; + for (var prop in source) { + if (!source.hasOwnProperty(prop)) continue; + if (prop in target) { + if (typeof target[prop] !== 'object') { + target[prop] = source[prop]; + } else { + if (typeof source[prop] !== 'object') { + target[prop] = source[prop]; + } else { + if (target[prop].concat && source[prop].concat) { + target[prop] = target[prop].concat(source[prop]); + } else { + target[prop] = deepMerge(target[prop], source[prop]); + } + } + } + } else { + target[prop] = source[prop]; + } + } + return target; +} diff --git a/packages/uni-cloud-stat/src/utils/index.js b/packages/uni-cloud-stat/src/utils/index.js new file mode 100755 index 0000000000000000000000000000000000000000..656d30279b07eef4f6be47f85a8d3112da83caa5 --- /dev/null +++ b/packages/uni-cloud-stat/src/utils/index.js @@ -0,0 +1,10 @@ +// 批量导出文件 +const requireApi = require.context('.', false, /.js$/) +let module = {} +let noAllowPaht = ['./index.js'] +requireApi.keys().forEach((key, index) => { + if ( noAllowPaht.indexOf(key) !== -1 ) return + Object.assign(module, requireApi(key)) +}) +export default module + diff --git a/packages/uni-cloud-stat/src/utils/pageInfo.js b/packages/uni-cloud-stat/src/utils/pageInfo.js new file mode 100644 index 0000000000000000000000000000000000000000..2e7c0ef014c4aee917c16188683976f23e8198c1 --- /dev/null +++ b/packages/uni-cloud-stat/src/utils/pageInfo.js @@ -0,0 +1,307 @@ +import pagesTitle from 'uni-pages?{"type":"style"}' +let pagesData = pagesTitle.pages +let titleJsons = {} +for (let i in pagesData) { + titleJsons[i] = pagesData[i].navigationBarTitleText || '' +} + +import { + sys +} from './util.js' + + +import { + STAT_URL, + STAT_VERSION, + DIFF_TIME +} from '../config.js'; + +const UUID_KEY = '__DC_STAT_UUID' +const UUID_VALUE = '__DC_UUID_VALUE' + +function getUuid() { + let uuid = '' + if (get_platform_name() === 'n') { + try { + uuid = plus.runtime.getDCloudId() + } catch (e) { + uuid = '' + } + return uuid + } + + try { + uuid = uni.getStorageSync(UUID_KEY) + } catch (e) { + uuid = UUID_VALUE + } + + if (!uuid) { + uuid = Date.now() + '' + Math.floor(Math.random() * 1e7) + try { + uni.setStorageSync(UUID_KEY, uuid) + } catch (e) { + uni.setStorageSync(UUID_KEY, UUID_VALUE) + } + } + return uuid +} + +export const get_uuid = (statData) => { + // 有可能不存在 deviceId(一般不存在就是出bug了),就自己生成一个 + return sys.deviceId || getUuid() +} + +export const get_sgin = (statData) => { + let arr = Object.keys(statData) + let sortArr = arr.sort() + let sgin = {} + let sginStr = '' + for (var i in sortArr) { + sgin[sortArr[i]] = statData[sortArr[i]] + sginStr += sortArr[i] + '=' + statData[sortArr[i]] + '&' + } + + return { + sign: '', + options: sginStr.substr(0, sginStr.length - 1), + } +} + +export const get_encodeURIComponent_options = (statData) => { + let data = {} + for (let prop in statData) { + data[prop] = encodeURIComponent(statData[prop]) + } + return data +} + +/** + * 获取当前平台 + * 移动端 : 'n', + * h5 : 'h5', + * 微信 : 'wx', + * 阿里 : 'ali', + * 百度 : 'bd', + * 头条 : 'tt', + * qq : 'qq', + * 快应用 : 'qn', + * 快手 : 'ks', + * 飞书 : 'lark', + * 快应用 : 'qw', + * 钉钉 : 'dt' + */ +export const get_platform_name = () => { + // 苹果审核代码中禁止出现 alipay 字样 ,需要特殊处理一下 + const aliArr = ['y', 'a', 'p', 'mp-ali'] + const platformList = { + 'app': 'n', + 'app-plus': 'n', + h5: 'h5', + 'mp-weixin': 'wx', + [aliArr.reverse().join('')]: 'ali', + 'mp-baidu': 'bd', + 'mp-toutiao': 'tt', + 'mp-qq': 'qq', + 'quickapp-native': 'qn', + 'mp-kuaishou': 'ks', + 'mp-lark': 'lark', + 'quickapp-webview': 'qw' + } + if (platformList[process.env.VUE_APP_PLATFORM] === 'ali') { + if (my && my.env) { + const clientName = my.env.clientName + if (clientName === 'ap') return 'ali' + if (clientName === 'dingtalk') return 'dt' + // TODO 缺少 ali 下的其他平台 + } + } + return platformList[process.env.VUE_APP_PLATFORM] +} + +/** + * 获取小程序 appid + */ +export const get_pack_name = () => { + let packName = '' + if (get_platform_name() === 'wx' || get_platform_name() === 'qq') { + // 兼容微信小程序低版本基础库 + if (uni.canIUse('getAccountInfoSync')) { + packName = uni.getAccountInfoSync().miniProgram.appId || '' + } + } + if (get_platform_name() === 'n') { + // TODO APP 获取包名 + } + return packName +} + +/** + * 应用版本 + */ +export const get_version = () => { + return get_platform_name() === 'n' ? plus.runtime.version : '' +} + +/** + * 获取渠道 + */ +export const get_channel = () => { + const platformName = get_platform_name() + let channel = '' + if (platformName === 'n') { + channel = plus.runtime.channel + } + if (platformName === 'wx') { + // TODO 需要调研小程序二维码渠道如何获取; + } + return channel +} + +/** + * 获取小程序场景值 + * @param {Object} options 页面信息 + */ +export const get_scene = (options) => { + const platformName = get_platform_name() + let scene = '' + if (options) { + return options + } + if (platformName === 'wx') { + scene = uni.getLaunchOptionsSync().scene + } + return scene +} + +/** + * 获取拼接参数 + */ +export const get_splicing = (data) => { + let str = '' + for (var i in data) { + str += i + '=' + data[i] + '&' + } + return str.substr(0, str.length - 1) +} + +/** + * 获取页面url,不包含参数 + */ +export const get_route = (pageVm) => { + let _self = pageVm || get_page_vm(); + if (get_platform_name() === 'bd') { + let mp_route = _self.$mp && _self.$mp.page && _self.$mp.page.is; + let scope_route = _self.$scope && _self.$scope.is; + return mp_route || scope_route || '' + } else { + return _self.route || (_self.$scope && _self.$scope.route) || (_self.$mp && _self.$mp.page.route) + } +}; + +/** + * 获取页面url, 包含参数 + */ +export const get_page_route = (pageVm) => { + // 从 app 进入应用 ,没有 $page ,获取不到路由 ,需要获取页面 尝试从 getCurrentPages 获取也页面实例 + // FIXME 尽量不使用 getCurrentPages ,大部分获取路由是从 onHide 获取 ,这时可以获取到,如果是 onload ,则可能获取不到,比如 百度 + + let page = pageVm.$page || (pageVm.$scope && pageVm.$scope.$page) + let lastPageRoute = uni.getStorageSync('_STAT_LAST_PAGE_ROUTE'); + if (!page) return lastPageRoute || '' + return page.fullPath === '/' ? page.route : page.fullPath +}; + +/** + * 获取页面实例 + */ +export const get_page_vm = () => { + let pages = getCurrentPages() + let $page = pages[pages.length - 1] + if (!$page) return null + return $page.$vm +} + +/** + * 获取页面类型 + */ +export const get_page_types = (self) => { + // XXX 百度有问题 ,获取的都是 componet ,等待修复 + if (self.mpType === 'page' || self.$mpType === 'page' || (self.$mp && self.$mp.mpType === 'page') || self + .$options.mpType === 'page') { + return 'page'; + } + if (self.mpType === 'app' || self.$mpType === 'app' || (self.$mp && self.$mp.mpType === 'app') || self.$options + .mpType === 'app') { + return 'app' + } + return null; +} + +/** + * 处理上报参数 + * @param {Object} 需要处理的数据 + */ +export const handle_data = (statData) => { + let firstArr = [] + let contentArr = [] + let lastArr = [] + for (let i in statData) { + const rd = statData[i] + rd.forEach((elm) => { + const newData = get_splicing(elm) + if (i === 0) { + firstArr.push(newData) + } else if (i === 3) { + lastArr.push(newData) + } else { + contentArr.push(newData) + } + }) + } + + firstArr.push(...contentArr, ...lastArr) + // 参数需要处理成字符串,方便上传 + return JSON.stringify(firstArr) +} + + +/** + * 自定义事件参数校验 + */ +export const calibration = (eventName, options) => { + // login 、 share 、pay_success 、pay_fail 、register 、title + if (!eventName) { + console.error(`uni.report Missing [eventName] parameter`); + return true + } + if (typeof eventName !== 'string') { + console.error(`uni.report [eventName] Parameter type error, it can only be of type String`); + return true + } + if (eventName.length > 255) { + console.error(`uni.report [eventName] Parameter length cannot be greater than 255`); + return true + } + + if (typeof options !== 'string' && typeof options !== 'object') { + console.error('uni.report [options] Parameter type error, Only supports String or Object type'); + return true + } + + if (typeof options === 'string' && options.length > 255) { + console.error(`uni.report [options] Parameter length cannot be greater than 255`); + return true + } + + if (eventName === 'title' && typeof options !== 'string') { + console.error( + `uni.report [eventName] When the parameter is title, the [options] parameter can only be of type String` + ); + return true + } +} + +export const get_page_name = (routepath) => { + return (titleJsons && titleJsons[routepath]) || '' +} diff --git a/packages/uni-cloud-stat/src/utils/pageTime.js b/packages/uni-cloud-stat/src/utils/pageTime.js new file mode 100644 index 0000000000000000000000000000000000000000..f4183eb8eb6bf4a9eaa56b45cbbdabceae4a4181 --- /dev/null +++ b/packages/uni-cloud-stat/src/utils/pageTime.js @@ -0,0 +1,148 @@ +import { + get_platform_name +} from './pageInfo.js' +import { + dbSet, + dbGet, + dbRemove +} from './db.js' +import { + PAGE_PVER_TIME, + APP_PVER_TIME +} from '../config.js'; +// 首次访问时间 +const FIRST_VISIT_TIME_KEY = '__first__visit__time' +// 最后访问时间 +const LAST_VISIT_TIME_KEY = '__last__visit__time' +/** + * 获取当前时间 + */ +export const get_time = () => { + return parseInt(new Date().getTime() / 1000) +} + +/** + * 获取首次访问时间 + */ +export const get_first_visit_time = () => { + const timeStorge = dbGet(FIRST_VISIT_TIME_KEY) + let time = 0 + if (timeStorge) { + time = timeStorge + } else { + time = get_time() + dbSet(FIRST_VISIT_TIME_KEY, time) + // 首次访问需要 将最后访问时间置 0 + dbRemove(LAST_VISIT_TIME_KEY) + } + return time +} + +/** + * 最后访问时间 + */ +export const get_last_visit_time = () => { + const timeStorge = dbGet(LAST_VISIT_TIME_KEY) + let time = 0 + if (timeStorge) { + time = timeStorge + } + dbSet(LAST_VISIT_TIME_KEY, get_time()) + return time +} + +// 页面停留时间记录key +const PAGE_RESIDENCE_TIME = '__page__residence__time' + +/** + * 设置页面停留时间 + */ +export const set_page_residence_time = () => { + let First_Page_Residence_Time = get_time() + dbSet(PAGE_RESIDENCE_TIME, First_Page_Residence_Time) + return First_Page_Residence_Time +} + +/** + * 获取页面停留时间 + */ +export const get_page_residence_time = () => { + let Last_Page_Residence_Time = get_time() + let First_Page_Residence_Time = dbGet(PAGE_RESIDENCE_TIME) + return Last_Page_Residence_Time - First_Page_Residence_Time +} + +/** + * 获取总访问次数 + */ +const TOTAL_VISIT_COUNT = '__total__visit__count' +export const get_total_visit_count = () => { + const timeStorge = dbGet(TOTAL_VISIT_COUNT) + let count = 1 + if (timeStorge) { + count = timeStorge + count++ + } + dbSet(TOTAL_VISIT_COUNT, count) + return count +} + +export const GetEncodeURIComponentOptions = (statData) => { + let data = {} + for (let prop in statData) { + data[prop] = encodeURIComponent(statData[prop]) + } + return data +} + +let Set__First__Time = 0 +let Set__Last__Time = 0 + +/** + * 获取第一次时间 + */ +export const get_first_time = () => { + let time = new Date().getTime() + Set__First__Time = time + Set__Last__Time = 0 + return time +} + +/** + * 获取最后一次时间 + */ +export const get_last_time = () => { + let time = new Date().getTime() + Set__Last__Time = time + return time +} + +/** + * 获取页面 \ 应用停留时间 + */ +export const get_residence_time = (type) => { + let residenceTime = 0 + if (Set__First__Time !== 0) { + residenceTime = Set__Last__Time - Set__First__Time + } + + residenceTime = parseInt(residenceTime / 1000) + residenceTime = residenceTime < 1 ? 1 : residenceTime + if (type === 'app') { + let overtime = residenceTime > APP_PVER_TIME ? true : false + return { + residenceTime, + overtime, + } + } + if (type === 'page') { + let overtime = residenceTime > PAGE_PVER_TIME ? true : false + return { + residenceTime, + overtime, + } + } + return { + residenceTime, + } +} diff --git a/packages/uni-cloud-stat/src/utils/util.js b/packages/uni-cloud-stat/src/utils/util.js new file mode 100755 index 0000000000000000000000000000000000000000..9e0254efee4fb3d5122e5e333d1562d933bbc6b4 --- /dev/null +++ b/packages/uni-cloud-stat/src/utils/util.js @@ -0,0 +1,5 @@ +/** + * 获取系统信息 + */ +export const sys = uni.getSystemInfoSync() + diff --git a/packages/uni-stat/dist/index.js b/packages/uni-stat/dist/index.js index 82271d8ece65afba22194af4c0575fd6d0a3a055..d29080c26d67a0f70265f26692ea3ed38a9b6973 100644 --- a/packages/uni-stat/dist/index.js +++ b/packages/uni-stat/dist/index.js @@ -1,20 +1,32 @@ -import { version } from '../package.json'; - -const STAT_VERSION = version; -const STAT_URL = 'https://tongji.dcloud.io/uni/stat'; -const STAT_H5_URL = 'https://tongji.dcloud.io/uni/stat.gif'; -const PAGE_PVER_TIME = 1800; -const APP_PVER_TIME = 300; -const OPERATING_TIME = 10; -const DIFF_TIME = 60 * 1000 * 60 * 24; +import pagesTitle from 'uni-pages?{"type":"style"}'; +import Vue from 'vue'; + +/** + * 获取系统信息 + */ +const sys = uni.getSystemInfoSync(); + +// 访问开始即启动小程序,访问结束结分为:进入后台超过5min、在前台无任何操作超过30min、在新的来源打开小程序; +const STAT_VERSION = '0.0.1'; +const STAT_URL = 'https://tongji.dcloud.io/uni/stat'; +const STAT_H5_URL = 'https://tongji.dcloud.io/uni/stat.gif'; +const PAGE_PVER_TIME = 1800; // 页面在前台无操作结束访问时间 单位s +const APP_PVER_TIME = 300; // 应用在后台结束访问时间 单位s +const OPERATING_TIME = 10; // 数据上报时间 单位s +const DIFF_TIME = 60 * 1000 * 60 * 24; + +let pagesData = pagesTitle.pages; +let titleJsons = {}; +for (let i in pagesData) { + titleJsons[i] = pagesData[i].navigationBarTitleText || ''; +} -const statConfig = require('uni-stat-config').default || require('uni-stat-config'); const UUID_KEY = '__DC_STAT_UUID'; const UUID_VALUE = '__DC_UUID_VALUE'; function getUuid() { let uuid = ''; - if (getPlatformName() === 'n') { + if (get_platform_name() === 'n') { try { uuid = plus.runtime.getDCloudId(); } catch (e) { @@ -37,10 +49,15 @@ function getUuid() { uni.setStorageSync(UUID_KEY, UUID_VALUE); } } - return uuid; + return uuid } -const getSgin = (statData) => { +const get_uuid = (statData) => { + // 有可能不存在 deviceId(一般不存在就是出bug了),就自己生成一个 + return sys.deviceId || getUuid() +}; + +const get_sgin = (statData) => { let arr = Object.keys(statData); let sortArr = arr.sort(); let sgin = {}; @@ -49,524 +66,760 @@ const getSgin = (statData) => { sgin[sortArr[i]] = statData[sortArr[i]]; sginStr += sortArr[i] + '=' + statData[sortArr[i]] + '&'; } - // const options = sginStr.substr(0, sginStr.length - 1) - // sginStr = sginStr.substr(0, sginStr.length - 1) + '&key=' + STAT_KEY; - // const si = crypto.createHash('md5').update(sginStr).digest('hex'); + return { sign: '', - options: sginStr.substr(0, sginStr.length - 1) - }; -}; - -const getSplicing = (data) => { - let str = ''; - for (var i in data) { - str += i + '=' + data[i] + '&'; + options: sginStr.substr(0, sginStr.length - 1), } - return str.substr(0, str.length - 1) }; -const getTime = () => { - return parseInt(new Date().getTime() / 1000); +const get_encodeURIComponent_options = (statData) => { + let data = {}; + for (let prop in statData) { + data[prop] = encodeURIComponent(statData[prop]); + } + return data }; -const getPlatformName = () => { +/** + * 获取当前平台 + * 移动端 : 'n', + * h5 : 'h5', + * 微信 : 'wx', + * 阿里 : 'ali', + * 百度 : 'bd', + * 头条 : 'tt', + * qq : 'qq', + * 快应用 : 'qn', + * 快手 : 'ks', + * 飞书 : 'lark', + * 快应用 : 'qw', + * 钉钉 : 'dt' + */ +const get_platform_name = () => { + // 苹果审核代码中禁止出现 alipay 字样 ,需要特殊处理一下 const aliArr = ['y', 'a', 'p', 'mp-ali']; const platformList = { + 'app': 'n', 'app-plus': 'n', - 'h5': 'h5', + h5: 'h5', 'mp-weixin': 'wx', [aliArr.reverse().join('')]: 'ali', 'mp-baidu': 'bd', 'mp-toutiao': 'tt', 'mp-qq': 'qq', - 'mp-jd': 'jd', 'quickapp-native': 'qn', - 'mp-kuaishou': 'ks' + 'mp-kuaishou': 'ks', + 'mp-lark': 'lark', + 'quickapp-webview': 'qw' }; - return platformList[process.env.VUE_APP_PLATFORM]; + if (platformList[process.env.VUE_APP_PLATFORM] === 'ali') { + if (my && my.env) { + const clientName = my.env.clientName; + if (clientName === 'ap') return 'ali' + if (clientName === 'dingtalk') return 'dt' + // TODO 缺少 ali 下的其他平台 + } + } + return platformList[process.env.VUE_APP_PLATFORM] }; -const getPackName = () => { +/** + * 获取小程序 appid + */ +const get_pack_name = () => { let packName = ''; - if (getPlatformName() === 'wx' || getPlatformName() === 'qq') { + if (get_platform_name() === 'wx' || get_platform_name() === 'qq') { // 兼容微信小程序低版本基础库 if (uni.canIUse('getAccountInfoSync')) { packName = uni.getAccountInfoSync().miniProgram.appId || ''; } } + if (get_platform_name() === 'n') ; return packName }; -const getVersion = () => { - return getPlatformName() === 'n' ? plus.runtime.version : ''; +/** + * 应用版本 + */ +const get_version = () => { + return get_platform_name() === 'n' ? plus.runtime.version : '' }; -const getChannel = () => { - const platformName = getPlatformName(); +/** + * 获取渠道 + */ +const get_channel = () => { + const platformName = get_platform_name(); let channel = ''; if (platformName === 'n') { channel = plus.runtime.channel; } - return channel; + return channel }; -const getScene = (options) => { - const platformName = getPlatformName(); +/** + * 获取小程序场景值 + * @param {Object} options 页面信息 + */ +const get_scene = (options) => { + const platformName = get_platform_name(); let scene = ''; if (options) { - return options; + return options } if (platformName === 'wx') { scene = uni.getLaunchOptionsSync().scene; } - return scene; + return scene }; -const First__Visit__Time__KEY = 'First__Visit__Time'; -const Last__Visit__Time__KEY = 'Last__Visit__Time'; - -const getFirstVisitTime = () => { - const timeStorge = uni.getStorageSync(First__Visit__Time__KEY); - let time = 0; - if (timeStorge) { - time = timeStorge; - } else { - time = getTime(); - uni.setStorageSync(First__Visit__Time__KEY, time); - uni.removeStorageSync(Last__Visit__Time__KEY); + +/** + * 获取拼接参数 + */ +const get_splicing = (data) => { + let str = ''; + for (var i in data) { + str += i + '=' + data[i] + '&'; } - return time; + return str.substr(0, str.length - 1) }; -const getLastVisitTime = () => { - const timeStorge = uni.getStorageSync(Last__Visit__Time__KEY); - let time = 0; - if (timeStorge) { - time = timeStorge; +/** + * 获取页面url,不包含参数 + */ +const get_route = (pageVm) => { + let _self = pageVm || get_page_vm(); + if (get_platform_name() === 'bd') { + let mp_route = _self.$mp && _self.$mp.page && _self.$mp.page.is; + let scope_route = _self.$scope && _self.$scope.is; + return mp_route || scope_route || '' } else { - time = ''; + return _self.route || (_self.$scope && _self.$scope.route) || (_self.$mp && _self.$mp.page.route) } - uni.setStorageSync(Last__Visit__Time__KEY, getTime()); - return time; }; - -const PAGE_RESIDENCE_TIME = '__page__residence__time'; -let First_Page_residence_time = 0; -let Last_Page_residence_time = 0; - - -const setPageResidenceTime = () => { - First_Page_residence_time = getTime(); - if (getPlatformName() === 'n') { - uni.setStorageSync(PAGE_RESIDENCE_TIME, getTime()); - } - return First_Page_residence_time +/** + * 获取页面url, 包含参数 + */ +const get_page_route = (pageVm) => { + // 从 app 进入应用 ,没有 $page ,获取不到路由 ,需要获取页面 尝试从 getCurrentPages 获取也页面实例 + // FIXME 尽量不使用 getCurrentPages ,大部分获取路由是从 onHide 获取 ,这时可以获取到,如果是 onload ,则可能获取不到,比如 百度 + + let page = pageVm.$page || (pageVm.$scope && pageVm.$scope.$page); + let lastPageRoute = uni.getStorageSync('_STAT_LAST_PAGE_ROUTE'); + if (!page) return lastPageRoute || '' + return page.fullPath === '/' ? page.route : page.fullPath }; -const getPageResidenceTime = () => { - Last_Page_residence_time = getTime(); - if (getPlatformName() === 'n') { - First_Page_residence_time = uni.getStorageSync(PAGE_RESIDENCE_TIME); - } - return Last_Page_residence_time - First_Page_residence_time -}; -const TOTAL__VISIT__COUNT = 'Total__Visit__Count'; -const getTotalVisitCount = () => { - const timeStorge = uni.getStorageSync(TOTAL__VISIT__COUNT); - let count = 1; - if (timeStorge) { - count = timeStorge; - count++; - } - uni.setStorageSync(TOTAL__VISIT__COUNT, count); - return count; +/** + * 获取页面实例 + */ +const get_page_vm = () => { + let pages = getCurrentPages(); + let $page = pages[pages.length - 1]; + if (!$page) return null + return $page.$vm }; -const GetEncodeURIComponentOptions = (statData) => { - let data = {}; - for (let prop in statData) { - data[prop] = encodeURIComponent(statData[prop]); +/** + * 获取页面类型 + */ +const get_page_types = (self) => { + // XXX 百度有问题 ,获取的都是 componet ,等待修复 + if (self.mpType === 'page' || self.$mpType === 'page' || (self.$mp && self.$mp.mpType === 'page') || self + .$options.mpType === 'page') { + return 'page'; } - return data; -}; - -let Set__First__Time = 0; -let Set__Last__Time = 0; - -const getFirstTime = () => { - let time = new Date().getTime(); - Set__First__Time = time; - Set__Last__Time = 0; - return time; -}; - - -const getLastTime = () => { - let time = new Date().getTime(); - Set__Last__Time = time; - return time; -}; - - -const getResidenceTime = (type) => { - let residenceTime = 0; - if (Set__First__Time !== 0) { - residenceTime = Set__Last__Time - Set__First__Time; - } - - residenceTime = parseInt(residenceTime / 1000); - residenceTime = residenceTime < 1 ? 1 : residenceTime; - if (type === 'app') { - let overtime = residenceTime > APP_PVER_TIME ? true : false; - return { - residenceTime, - overtime - }; - } - if (type === 'page') { - let overtime = residenceTime > PAGE_PVER_TIME ? true : false; - return { - residenceTime, - overtime - }; + if (self.mpType === 'app' || self.$mpType === 'app' || (self.$mp && self.$mp.mpType === 'app') || self.$options + .mpType === 'app') { + return 'app' } - - return { - residenceTime - }; - + return null; }; -const getRoute = () => { - var pages = getCurrentPages(); - var page = pages[pages.length - 1]; - if (!page) return '' - let _self = page.$vm; - - if (getPlatformName() === 'bd') { - return _self.$mp && _self.$mp.page.is; - } else { - return (_self.$scope && _self.$scope.route) || (_self.$mp && _self.$mp.page.route); +/** + * 处理上报参数 + * @param {Object} 需要处理的数据 + */ +const handle_data = (statData) => { + let firstArr = []; + let contentArr = []; + let lastArr = []; + for (let i in statData) { + const rd = statData[i]; + rd.forEach((elm) => { + const newData = get_splicing(elm); + if (i === 0) { + firstArr.push(newData); + } else if (i === 3) { + lastArr.push(newData); + } else { + contentArr.push(newData); + } + }); } -}; -const getPageRoute = (self) => { - var pages = getCurrentPages(); - var page = pages[pages.length - 1]; - if (!page) return '' - let _self = page.$vm; - let query = self._query; - let str = query && JSON.stringify(query) !== '{}' ? '?' + JSON.stringify(query) : ''; - // clear - self._query = ''; - if (getPlatformName() === 'bd') { - return _self.$mp && _self.$mp.page.is + str; - } else { - return (_self.$scope && _self.$scope.route + str) || (_self.$mp && _self.$mp.page.route + str); - } + firstArr.push(...contentArr, ...lastArr); + // 参数需要处理成字符串,方便上传 + return JSON.stringify(firstArr) }; -const getPageTypes = (self) => { - if (self.mpType === 'page' || (self.$mp && self.$mp.mpType === 'page') || self.$options.mpType === 'page') { - return true; - } - return false; -}; +/** + * 自定义事件参数校验 + */ const calibration = (eventName, options) => { // login 、 share 、pay_success 、pay_fail 、register 、title if (!eventName) { - console.error(`uni.report 缺少 [eventName] 参数`); + console.error(`uni.report Missing [eventName] parameter`); return true } if (typeof eventName !== 'string') { - console.error(`uni.report [eventName] 参数类型错误,只能为 String 类型`); + console.error(`uni.report [eventName] Parameter type error, it can only be of type String`); return true } if (eventName.length > 255) { - console.error(`uni.report [eventName] 参数长度不能大于 255`); + console.error(`uni.report [eventName] Parameter length cannot be greater than 255`); return true } if (typeof options !== 'string' && typeof options !== 'object') { - console.error(`uni.report [options] 参数类型错误,只能为 String 或 Object 类型`); + console.error('uni.report [options] Parameter type error, Only supports String or Object type'); return true } if (typeof options === 'string' && options.length > 255) { - console.error(`uni.report [options] 参数长度不能大于 255`); + console.error(`uni.report [options] Parameter length cannot be greater than 255`); return true } if (eventName === 'title' && typeof options !== 'string') { - console.error('uni.report [eventName] 参数为 title 时,[options] 参数只能为 String 类型'); + console.error( + `uni.report [eventName] When the parameter is title, the [options] parameter can only be of type String` + ); return true } }; -const Report_Data_Time = 'Report_Data_Time'; -const Report_Status = 'Report_Status'; -const isReportData = () => { - return new Promise((resolve, reject) => { - let start_time = ''; - let end_time = new Date().getTime(); - let diff_time = DIFF_TIME; - let report_status = 1; - try { - start_time = uni.getStorageSync(Report_Data_Time); - report_status = uni.getStorageSync(Report_Status); - } catch (e) { - start_time = ''; - report_status = 1; - } +const get_page_name = (routepath) => { + return (titleJsons && titleJsons[routepath]) || '' +}; + + +const Report_Data_Time = 'Report_Data_Time'; +const Report_Status = 'Report_Status'; +const is_report_data = () => { + return new Promise((resolve, reject) => { + let start_time = ''; + let end_time = new Date().getTime(); + let diff_time = DIFF_TIME; + let report_status = 1; + try { + start_time = uni.getStorageSync(Report_Data_Time); + report_status = uni.getStorageSync(Report_Status); + } catch (e) { + start_time = ''; + report_status = 1; + } + + if (report_status === '') { + requestData(({ enable }) => { + uni.setStorageSync(Report_Data_Time, end_time); + uni.setStorageSync(Report_Status, enable); + if (enable === 1) { + resolve(); + } + }); + return + } + + if (report_status === 1) { + resolve(); + } + + if (!start_time) { + uni.setStorageSync(Report_Data_Time, end_time); + start_time = end_time; + } + + if (end_time - start_time > diff_time) { + requestData(({ enable }) => { + uni.setStorageSync(Report_Data_Time, end_time); + uni.setStorageSync(Report_Status, enable); + }); + } + }) +}; + +const requestData = (done) => { + const appid = process.env.UNI_APP_ID; + let formData = { + usv: STAT_VERSION, + conf: JSON.stringify({ + ak: appid, + }), + }; + uni.request({ + url: STAT_URL, + method: 'GET', + data: formData, + success: (res) => { + const { data } = res; + if (data.ret === 0) { + typeof done === 'function' && + done({ + enable: data.enable, + }); + } + }, + fail: (e) => { + let report_status_code = 1; + try { + report_status_code = uni.getStorageSync(Report_Status); + } catch (e) { + report_status_code = 1; + } + if (report_status_code === '') { + report_status_code = 1; + } + typeof done === 'function' && + done({ + enable: report_status_code, + }); + }, + }); +}; + +const dbSet = (name, value) => { + let data = uni.getStorageSync('$$STAT__DBDATA') || {}; + if (!data) { + data = {}; + } + data[name] = value; + uni.setStorageSync('$$STAT__DBDATA', data); +}; - if (report_status === '') { - requestData(({ - enable - }) => { - uni.setStorageSync(Report_Data_Time, end_time); - uni.setStorageSync(Report_Status, enable); - if (enable === 1) { - resolve(); - } - }); - return - } +const dbGet = (name) => { + let data = uni.getStorageSync('$$STAT__DBDATA') || {}; + if (!data) { + data = {}; + } + if (!data[name]) { + return undefined + } + return data[name] +}; - if (report_status === 1) { - resolve(); - } +const dbRemove = (name) => { + let data = uni.getStorageSync('$$STAT__DBDATA') || {}; + if (data[name]) { + delete data[name]; + uni.setStorageSync('$$STAT__DBDATA', data); + } else { + data = uni.getStorageSync('$$STAT__DBDATA'); + if (data[name]) { + delete data[name]; + uni.setStorageSync('$$STAT__DBDATA', data); + } + } +}; + +// 首次访问时间 +const FIRST_VISIT_TIME_KEY = '__first__visit__time'; +// 最后访问时间 +const LAST_VISIT_TIME_KEY = '__last__visit__time'; +/** + * 获取当前时间 + */ +const get_time = () => { + return parseInt(new Date().getTime() / 1000) +}; - if (!start_time) { - uni.setStorageSync(Report_Data_Time, end_time); - start_time = end_time; - } +/** + * 获取首次访问时间 + */ +const get_first_visit_time = () => { + const timeStorge = dbGet(FIRST_VISIT_TIME_KEY); + let time = 0; + if (timeStorge) { + time = timeStorge; + } else { + time = get_time(); + dbSet(FIRST_VISIT_TIME_KEY, time); + // 首次访问需要 将最后访问时间置 0 + dbRemove(LAST_VISIT_TIME_KEY); + } + return time +}; + +/** + * 最后访问时间 + */ +const get_last_visit_time = () => { + const timeStorge = dbGet(LAST_VISIT_TIME_KEY); + let time = 0; + if (timeStorge) { + time = timeStorge; + } + dbSet(LAST_VISIT_TIME_KEY, get_time()); + return time +}; - if ((end_time - start_time) > diff_time) { - requestData(({ - enable - }) => { - uni.setStorageSync(Report_Data_Time, end_time); - uni.setStorageSync(Report_Status, enable); - }); - } +// 页面停留时间记录key +const PAGE_RESIDENCE_TIME = '__page__residence__time'; - }) +/** + * 设置页面停留时间 + */ +const set_page_residence_time = () => { + let First_Page_Residence_Time = get_time(); + dbSet(PAGE_RESIDENCE_TIME, First_Page_Residence_Time); + return First_Page_Residence_Time }; -const requestData = (done) => { - let formData = { - usv: STAT_VERSION, - conf: JSON.stringify({ - ak: statConfig.appid - }) - }; - uni.request({ - url: STAT_URL, - method: 'GET', - data: formData, - success: (res) => { - const { - data - } = res; - if (data.ret === 0) { - typeof done === 'function' && done({ - enable: data.enable - }); - } - }, - fail: (e) => { - let report_status_code = 1; - try { - report_status_code = uni.getStorageSync(Report_Status); - } catch (e) { - report_status_code = 1; - } - if (report_status_code === '') { - report_status_code = 1; - } - typeof done === 'function' && done({ - enable: report_status_code - }); - } - }); +/** + * 获取页面停留时间 + */ +const get_page_residence_time = () => { + let Last_Page_Residence_Time = get_time(); + let First_Page_Residence_Time = dbGet(PAGE_RESIDENCE_TIME); + return Last_Page_Residence_Time - First_Page_Residence_Time +}; + +/** + * 获取总访问次数 + */ +const TOTAL_VISIT_COUNT = '__total__visit__count'; +const get_total_visit_count = () => { + const timeStorge = dbGet(TOTAL_VISIT_COUNT); + let count = 1; + if (timeStorge) { + count = timeStorge; + count++; + } + dbSet(TOTAL_VISIT_COUNT, count); + return count }; -const PagesJson = require('uni-pages?{"type":"style"}').default; -const statConfig$1 = require('uni-stat-config').default || require('uni-stat-config'); +let Set__First__Time = 0; +let Set__Last__Time = 0; + +/** + * 获取第一次时间 + */ +const get_first_time = () => { + let time = new Date().getTime(); + Set__First__Time = time; + Set__Last__Time = 0; + return time +}; -const resultOptions = uni.getSystemInfoSync(); +/** + * 获取最后一次时间 + */ +const get_last_time = () => { + let time = new Date().getTime(); + Set__Last__Time = time; + return time +}; -class Util { +/** + * 获取页面 \ 应用停留时间 + */ +const get_residence_time = (type) => { + let residenceTime = 0; + if (Set__First__Time !== 0) { + residenceTime = Set__Last__Time - Set__First__Time; + } + + residenceTime = parseInt(residenceTime / 1000); + residenceTime = residenceTime < 1 ? 1 : residenceTime; + if (type === 'app') { + let overtime = residenceTime > APP_PVER_TIME ? true : false; + return { + residenceTime, + overtime, + } + } + if (type === 'page') { + let overtime = residenceTime > PAGE_PVER_TIME ? true : false; + return { + residenceTime, + overtime, + } + } + return { + residenceTime, + } +}; + +let statConfig = require('uni-stat-config').default || require('uni-stat-config'); + +// 统计数据默认值 +let statData = { + uuid: get_uuid(), // 设备标识 + ut: get_platform_name(), // 平台类型 + mpn: get_pack_name(), // 原生平台包名、小程序 appid + ak: statConfig.appid, // uni-app 应用 Appid + usv: STAT_VERSION, // 统计 sdk 版本 + v: get_version(), // 应用版本,仅app + ch: get_channel(), // 渠道信息 + cn: '', // 国家 + pn: '', // 省份 + ct: '', // 城市 + t: get_time(), // 上报数据时的时间戳 + tt: '', + p: sys.platform === 'android' ? 'a' : 'i', // 手机系统 + brand: sys.brand || '', // 手机品牌 + md: sys.model, // 手机型号 + sv: sys.system.replace(/(Android|iOS)\s/, ''), // 手机系统版本 + mpsdk: sys.SDKVersion || '', // x程序 sdk version + mpv: sys.version || '', // 小程序平台版本 ,如微信、支付宝 + lang: sys.language, // 语言 + pr: sys.pixelRatio, // pixelRatio 设备像素比 + ww: sys.windowWidth, // windowWidth 可使用窗口宽度 + wh: sys.windowHeight, // windowHeight 可使用窗口高度 + sw: sys.screenWidth, // screenWidth 屏幕宽度 + sh: sys.screenHeight, // screenHeight 屏幕高度 +}; +class Report { constructor() { + // 页面实例 this.self = ''; - this._retry = 0; - this._platform = ''; - this._query = {}; + // 进入应用标识 + this.__licationShow = false; + // 离开应用标识 + this.__licationHide = false; + // 统计默认值 + this.statData = statData; + // 标题默认值 this._navigationBarTitle = { config: '', page: '', report: '', - lt: '' - }; - this._operatingTime = 0; - this._reportingRequestData = { - '1': [], - '11': [] + lt: '', }; - this.__prevent_triggering = false; - this.__licationHide = false; - this.__licationShow = false; - this._lastPageRoute = ''; - this.statData = { - uuid: getUuid(), - ut: getPlatformName(), - mpn: getPackName(), - ak: statConfig$1.appid, - usv: STAT_VERSION, - v: getVersion(), - ch: getChannel(), - cn: '', - pn: '', - ct: '', - t: getTime(), - tt: '', - p: resultOptions.platform === 'android' ? 'a' : 'i', - brand: resultOptions.brand || '', - md: resultOptions.model, - sv: resultOptions.system.replace(/(Android|iOS)\s/, ''), - mpsdk: resultOptions.SDKVersion || '', - mpv: resultOptions.version || '', - lang: resultOptions.language, - pr: resultOptions.pixelRatio, - ww: resultOptions.windowWidth, - wh: resultOptions.windowHeight, - sw: resultOptions.screenWidth, - sh: resultOptions.screenHeight - }; + // 页面参数 + this._query = {}; + // 页面最后停留页面的 url + // this._lastPageRoute = '' + // 注册拦截器 + let registerInterceptor = typeof uni.addInterceptor === 'function'; + if (registerInterceptor) { + this.addInterceptorInit(); + this.interceptLogin(); + this.interceptShare(true); + this.interceptRequestPayment(); + } + } + + addInterceptorInit() { + let self = this; + uni.addInterceptor('setNavigationBarTitle', { + invoke(args) { + self._navigationBarTitle.page = args.title; + }, + }); } - getIsReportData() { - return isReportData() + interceptLogin() { + let self = this; + uni.addInterceptor('login', { + complete() { + self._login(); + }, + }); } - _applicationShow() { + interceptShare(type) { + let self = this; + if (!type) { + self._share(); + return + } + uni.addInterceptor('share', { + success() { + self._share(); + }, + fail() { + self._share(); + }, + }); + } + + interceptRequestPayment() { + let self = this; + uni.addInterceptor('requestPayment', { + success() { + self._payment('pay_success'); + }, + fail() { + self._payment('pay_fail'); + }, + }); + } + + _login() { + this.sendEventRequest({ + key: 'login', + }, + 0 + ); + } + + _share() { + this.sendEventRequest({ + key: 'share', + }, 0); + } + _payment(key) { + this.sendEventRequest({ + key, + }, 0); + } + + /** + * 进入应用触发 + */ + applicationShow() { + // 通过 __licationHide 判断保证是进入后台后在次进入应用,避免重复上报数据 if (this.__licationHide) { - getLastTime(); - const time = getResidenceTime('app'); + get_last_time(); + const time = get_residence_time('app'); + // 需要判断进入后台是否超过时限 ,默认是 30min ,是的话需要执行进入应用的上报 if (time.overtime) { + let lastPageRoute = uni.getStorageSync('_STAT_LAST_PAGE_ROUTE'); let options = { - path: this._lastPageRoute, - scene: this.statData.sc + path: lastPageRoute, + scene: this.statData.sc, }; - this._sendReportRequest(options); + this.sendReportRequest(options); } + // 状态重置 this.__licationHide = false; } } - _applicationHide(self, type) { - + /** + * 离开应用触发 + * @param {Object} self + * @param {Object} type + */ + applicationHide(self, type) { + // 进入应用后台保存状态,方便进入前台后判断是否上报应用数据 this.__licationHide = true; - getLastTime(); - const time = getResidenceTime(); - getFirstTime(); - const route = getPageRoute(this); - this._sendHideRequest({ - urlref: route, - urlref_ts: time.residenceTime - }, type); - } - - _pageShow() { - const route = getPageRoute(this); - const routepath = getRoute(); - this._navigationBarTitle.config = PagesJson && - PagesJson.pages[routepath] && - PagesJson.pages[routepath].titleNView && - PagesJson.pages[routepath].titleNView.titleText || - PagesJson && - PagesJson.pages[routepath] && - PagesJson.pages[routepath].navigationBarTitleText || ''; + get_last_time(); + const time = get_residence_time(); + const route = get_page_route(self); + // this._lastPageRoute = route + uni.setStorageSync('_STAT_LAST_PAGE_ROUTE', route); + this.sendHideRequest({ + urlref: route, + urlref_ts: time.residenceTime, + }, + type + ); + // 重置时间 + get_first_time(); + } + + /** + * 进入页面触发 + */ + pageShow(self) { + // 清空值 ,初始化 ,避免污染后面的上报数据 + this._navigationBarTitle = { + config: '', + page: '', + report: '', + lt: '', + }; + const route = get_page_route(self); + const routepath = get_route(self); + + this._navigationBarTitle.config = get_page_name(routepath); + // 表示应用触发 ,页面切换不触发之后的逻辑 if (this.__licationShow) { - getFirstTime(); + get_first_time(); + // this._lastPageRoute = route + uni.setStorageSync('_STAT_LAST_PAGE_ROUTE', route); this.__licationShow = false; - // console.log('这是 onLauch 之后执行的第一次 pageShow ,为下次记录时间做准备'); - this._lastPageRoute = route; - return; + return } - getLastTime(); - this._lastPageRoute = route; - const time = getResidenceTime('page'); + get_last_time(); + + const time = get_residence_time('page'); + // 停留时间 if (time.overtime) { let options = { - path: this._lastPageRoute, - scene: this.statData.sc + path: route, + scene: this.statData.sc, }; - this._sendReportRequest(options); + this.sendReportRequest(options); } - getFirstTime(); + // 重置时间 + get_first_time(); } - _pageHide() { + /** + * 离开页面触发 + */ + pageHide(self) { if (!this.__licationHide) { - getLastTime(); - const time = getResidenceTime('page'); - this._sendPageRequest({ - url: this._lastPageRoute, - urlref: this._lastPageRoute, - urlref_ts: time.residenceTime + get_last_time(); + const time = get_residence_time('page'); + let route = get_page_route(self); + let lastPageRoute = uni.getStorageSync('_STAT_LAST_PAGE_ROUTE'); + if (!lastPageRoute) { + lastPageRoute = route; + } + uni.setStorageSync('_STAT_LAST_PAGE_ROUTE', route); + this.sendPageRequest({ + url: route, + urlref: lastPageRoute, + urlref_ts: time.residenceTime, }); - this._navigationBarTitle = { - config: '', - page: '', - report: '', - lt: '' - }; - return; + // this._lastPageRoute = route + return } } - _login() { - this._sendEventRequest({ - key: 'login' - }, 0); - } - - _share() { - this._sendEventRequest({ - key: 'share' - }, 0); - } - _payment(key) { - this._sendEventRequest({ - key - }, 0); - } - _sendReportRequest(options) { + /** + * 发送请求,应用维度上报 + * @param {Object} options 页面信息 + */ + sendReportRequest(options) { this._navigationBarTitle.lt = '1'; - let query = options.query && JSON.stringify(options.query) !== '{}' ? '?' + JSON.stringify(options.query) : ''; - this.statData.lt = '1'; - this.statData.url = (options.path + query) || ''; - this.statData.t = getTime(); - this.statData.sc = getScene(options.scene); - this.statData.fvts = getFirstVisitTime(); - this.statData.lvts = getLastVisitTime(); - this.statData.tvc = getTotalVisitCount(); - if (getPlatformName() === 'n') { + this._navigationBarTitle.config = get_page_name(options.path); + let is_opt = options.query && JSON.stringify(options.query) !== '{}'; + let query = is_opt ? '?' + JSON.stringify(options.query) : ''; + Object.assign(this.statData, { + lt: '1', + url: (options.path + query) || '', + t: get_time(), + sc: get_scene(options.scene), + fvts: get_first_visit_time(), + lvts: get_last_visit_time(), + tvc: get_total_visit_count() + }); + if (get_platform_name() === 'n') { this.getProperty(); } else { this.getNetworkInfo(); } } - _sendPageRequest(opt) { + /** + * 发送请求,页面维度上报 + * @param {Object} opt + */ + sendPageRequest(opt) { let { url, urlref, @@ -584,13 +837,18 @@ class Util { urlref_ts, ch: this.statData.ch, usv: this.statData.usv, - t: getTime(), - p: this.statData.p + t: get_time(), + p: this.statData.p, }; this.request(options); } - _sendHideRequest(opt, type) { + /** + * 进入后台上报数据 + * @param {Object} opt + * @param {Object} type + */ + sendHideRequest(opt, type) { let { urlref, urlref_ts @@ -604,50 +862,66 @@ class Util { urlref_ts, ch: this.statData.ch, usv: this.statData.usv, - t: getTime(), - p: this.statData.p + t: get_time(), + p: this.statData.p, }; this.request(options, type); } - _sendEventRequest({ + + /** + * 自定义事件上报 + */ + sendEventRequest({ key = '', - value = "" + value = '' } = {}) { - const route = this._lastPageRoute; + // const route = this._lastPageRoute + const routepath = get_route(); + this._navigationBarTitle.config = get_page_name(routepath); + this._navigationBarTitle.lt = '21'; let options = { ak: this.statData.ak, uuid: this.statData.uuid, lt: '21', ut: this.statData.ut, - url: route, + url: routepath, ch: this.statData.ch, e_n: key, - e_v: typeof(value) === 'object' ? JSON.stringify(value) : value.toString(), + e_v: typeof value === 'object' ? JSON.stringify(value) : value.toString(), usv: this.statData.usv, - t: getTime(), - p: this.statData.p + t: get_time(), + p: this.statData.p, }; this.request(options); } + /** + * 获取wgt资源版本 + */ + getProperty() { + plus.runtime.getProperty(plus.runtime.appid, (wgtinfo) => { + this.statData.v = wgtinfo.version || ''; + this.getNetworkInfo(); + }); + } + + /** + * 获取网络信息 + */ getNetworkInfo() { uni.getNetworkType({ success: (result) => { this.statData.net = result.networkType; this.getLocation(); - } - }); - } - - getProperty() { - plus.runtime.getProperty(plus.runtime.appid, (wgtinfo) => { - this.statData.v = wgtinfo.version || ''; - this.getNetworkInfo(); + }, }); } + /** + * 获取位置信息 + */ getLocation() { - if (statConfig$1.getLocation) { + if (statConfig.getLocation) { uni.getLocation({ type: 'wgs84', geocode: true, @@ -661,7 +935,7 @@ class Util { this.statData.lat = result.latitude; this.statData.lng = result.longitude; this.request(this.statData); - } + }, }); } else { this.statData.lat = 0; @@ -670,110 +944,97 @@ class Util { } } + /** + * 发送请求 + * @param {Object} data 上报数据 + * @param {Object} type 类型 + */ request(data, type) { - let time = getTime(); + let time = get_time(); const title = this._navigationBarTitle; - data.ttn = title.page; - data.ttpj = title.config; - data.ttc = title.report; + Object.assign(data, { + ttn: title.page, + ttpj: title.config, + ttc: title.report + }); - let requestData = this._reportingRequestData; - if (getPlatformName() === 'n') { - requestData = uni.getStorageSync('__UNI__STAT__DATA') || {}; - } - if (!requestData[data.lt]) { - requestData[data.lt] = []; + let uniStatData = dbGet('__UNI__STAT__DATA') || {}; + if (!uniStatData[data.lt]) { + uniStatData[data.lt] = []; } - requestData[data.lt].push(data); + // 加入队列 + uniStatData[data.lt].push(data); + dbSet('__UNI__STAT__DATA', uniStatData); - if (getPlatformName() === 'n') { - uni.setStorageSync('__UNI__STAT__DATA', requestData); - } - if (getPageResidenceTime() < OPERATING_TIME && !type) { - return - } - let uniStatData = this._reportingRequestData; - if (getPlatformName() === 'n') { - uniStatData = uni.getStorageSync('__UNI__STAT__DATA'); - } - // 时间超过,重新获取时间戳 - setPageResidenceTime(); - let firstArr = []; - let contentArr = []; - let lastArr = []; - - for (let i in uniStatData) { - const rd = uniStatData[i]; - rd.forEach((elm) => { - const newData = getSplicing(elm); - if (i === 0) { - firstArr.push(newData); - } else if (i === 3) { - lastArr.push(newData); - } else { - contentArr.push(newData); - } - }); - } + let page_residence_time = get_page_residence_time(); + // 判断时候到达上报时间 ,默认 10 秒上报 + if (page_residence_time < OPERATING_TIME && !type) return - firstArr.push(...contentArr, ...lastArr); + // 时间超过,重新获取时间戳 + set_page_residence_time(); + const stat_data = handle_data(uniStatData); let optionsData = { usv: STAT_VERSION, //统计 SDK 版本号 t: time, //发送请求时的时间戮 - requests: JSON.stringify(firstArr), + requests: stat_data, }; - this._reportingRequestData = {}; - if (getPlatformName() === 'n') { - uni.removeStorageSync('__UNI__STAT__DATA'); + // 重置队列 + dbRemove('__UNI__STAT__DATA'); + + if (data.ut === 'h5') { + this.imageRequest(optionsData); + return } - if (data.ut === 'h5') { - this.imageRequest(optionsData); - return - } - - if (getPlatformName() === 'n' && this.statData.p === 'a') { + // XXX 安卓需要延迟上报 ,否则会有未知错误,需要验证处理 + if (get_platform_name() === 'n' && this.statData.p === 'a') { setTimeout(() => { - this._sendRequest(optionsData); + this.sendRequest(optionsData); }, 200); return } - this._sendRequest(optionsData); - } - _sendRequest(optionsData) { - this.getIsReportData().then(() => { - uni.request({ - url: STAT_URL, - method: 'POST', - // header: { - // 'content-type': 'application/json' // 默认值 - // }, - data: optionsData, - success: () => { - // if (process.env.NODE_ENV === 'development') { - // console.log('stat request success'); - // } - }, - fail: (e) => { - if (++this._retry < 3) { - setTimeout(() => { - this._sendRequest(optionsData); - }, 1000); - } - } - }); - }); + + this.sendRequest(optionsData); } + getIsReportData(){ + return is_report_data() + } + /** - * h5 请求 + * 数据上报 + * @param {Object} optionsData 需要上报的数据 */ - imageRequest(data) { - this.getIsReportData().then(() => { - let image = new Image(); - let options = getSgin(GetEncodeURIComponentOptions(data)).options; - image.src = STAT_H5_URL + '?' + options; + sendRequest(optionsData) { + this.getIsReportData().then(() => { + uni.request({ + url: STAT_URL, + method: 'POST', + // header: { + // 'content-type': 'application/json' // 默认值 + // }, + data: optionsData, + success: () => {}, + fail: (e) => { + if (++this._retry < 3) { + setTimeout(() => { + this.sendRequest(optionsData); + }, 1000); + } + }, + }); }); + } + + /** + * h5 请求 + */ + imageRequest(data) { + this.getIsReportData().then(() => { + let image = new Image(); + let options = get_sgin(get_encodeURIComponent_options(data)).options; + image.src = STAT_H5_URL + '?' + options; + }); } sendEvent(key, value) { @@ -784,202 +1045,158 @@ class Util { this._navigationBarTitle.report = value; return } - this._sendEventRequest({ - key, - value: typeof(value) === 'object' ? JSON.stringify(value) : value - }, 1); - } -} - - -class Stat extends Util { - static getInstance() { - if (!this.instance) { - this.instance = new Stat(); - } - return this.instance; - } - constructor() { - super(); - this.instance = null; - // 注册拦截器 - if (typeof uni.addInterceptor === 'function' && process.env.NODE_ENV !== 'development') { - this.addInterceptorInit(); - this.interceptLogin(); - this.interceptShare(true); - this.interceptRequestPayment(); - } - } - - addInterceptorInit() { - let self = this; - uni.addInterceptor('setNavigationBarTitle', { - invoke(args) { - self._navigationBarTitle.page = args.title; - } - }); - } - - interceptLogin() { - let self = this; - uni.addInterceptor('login', { - complete() { - self._login(); - } - }); - } - - interceptShare(type) { - let self = this; - if (!type) { - self._share(); - return - } - uni.addInterceptor('share', { - success() { - self._share(); + this.sendEventRequest({ + key, + value: typeof value === 'object' ? JSON.stringify(value) : value, }, - fail() { - self._share(); - } - }); + 1 + ); } - - interceptRequestPayment() { - let self = this; - uni.addInterceptor('requestPayment', { - success() { - self._payment('pay_success'); - }, - fail() { - self._payment('pay_fail'); - } - }); - } - - report(options, self) { - this.self = self; - // if (process.env.NODE_ENV === 'development') { - // console.log('report init'); - // } - setPageResidenceTime(); - this.__licationShow = true; - this._sendReportRequest(options, true); - } - - load(options, self) { - if (!self.$scope && !self.$mp) { - const page = getCurrentPages(); - self.$scope = page[page.length - 1]; - } - this.self = self; - this._query = options; - } - - show(self) { - this.self = self; - if (getPageTypes(self)) { - this._pageShow(self); - } else { - this._applicationShow(self); - } - } - - ready(self) { - // this.self = self; - // if (getPageTypes(self)) { - // this._pageShow(self); - // } - } - hide(self) { - this.self = self; - if (getPageTypes(self)) { - this._pageHide(self); - } else { - this._applicationHide(self, true); - } - } - error(em) { - if (this._platform === 'devtools') { - if (process.env.NODE_ENV === 'development') { - console.info('当前运行环境为开发者工具,不上报数据。'); - } - // return; - } - let emVal = ''; - if (!em.message) { - emVal = JSON.stringify(em); - } else { - emVal = em.stack; - } - let options = { - ak: this.statData.ak, - uuid: this.statData.uuid, - lt: '31', - ut: this.statData.ut, - ch: this.statData.ch, - mpsdk: this.statData.mpsdk, - mpv: this.statData.mpv, - v: this.statData.v, - em: emVal, - usv: this.statData.usv, - t: getTime(), - p: this.statData.p - }; - this.request(options); - } -} - +} + +let vue = (Vue.default || Vue); + +class Stat extends Report { + static getInstance() { + if (!vue.instance) { + vue.instance = new Stat(); + } + return vue.instance + } + constructor() { + super(); + this.instance = null; + } + + /** + * 进入应用 + * @param {Object} options 页面参数 + * @param {Object} self 当前页面实例 + */ + launch(options, self) { + // 初始化页面停留时间 start + let residence_time = set_page_residence_time(); + this.__licationShow = true; + this.sendReportRequest(options, true); + } + load(options, self) { + this.self = self; + this._query = options; + } + + appHide(self){ + this.applicationHide(self, true); + } + + appShow(self){ + this.applicationShow(self); + } + + show(self) { + this.self = self; + if (get_page_types(self) === 'page') { + this.pageShow(self); + } + if (get_page_types(self) === 'app') { + this.appShow(self); + } + } + + hide(self) { + this.self = self; + if (get_page_types(self) === 'page') { + this.pageHide(self); + } + if (get_page_types(self) === 'app') { + this.appHide(self); + } + } + + error(em) { + // 开发工具内不上报错误 + if (this._platform === 'devtools') { + if (process.env.NODE_ENV === 'development') { + console.info('当前运行环境为开发者工具,不上报数据。'); + return; + } + } + let emVal = ''; + if (!em.message) { + emVal = JSON.stringify(em); + } else { + emVal = em.stack; + } + let options = { + ak: this.statData.ak, + uuid: this.statData.uuid, + lt: '31', + ut: this.statData.ut, + ch: this.statData.ch, + mpsdk: this.statData.mpsdk, + mpv: this.statData.mpv, + v: this.statData.v, + em: emVal, + usv: this.statData.usv, + t: parseInt(new Date().getTime() / 1000), + p: this.statData.p, + }; + this.request(options); + } +} + const stat = Stat.getInstance(); + +// 用于判断是隐藏页面还是卸载页面 let isHide = false; + const lifecycle = { - onLaunch(options) { - stat.report(options, this); - }, - onReady() { - stat.ready(this); - }, - onLoad(options) { - stat.load(options, this); - // 重写分享,获取分享上报事件 - if (this.$scope && this.$scope.onShareAppMessage) { - let oldShareAppMessage = this.$scope.onShareAppMessage; - this.$scope.onShareAppMessage = function(options) { - stat.interceptShare(false); - return oldShareAppMessage.call(this, options) - }; - } - }, - onShow() { - isHide = false; - stat.show(this); - }, - onHide() { - isHide = true; - stat.hide(this); - }, - onUnload() { - if (isHide) { - isHide = false; - return - } - stat.hide(this); - }, - onError(e) { - stat.error(e); - } + onLaunch(options) { + // 进入应用上报数据 + stat.launch(options, this); + }, + onLoad(options) { + stat.load(options, this); + // 重写分享,获取分享上报事件 + if (this.$scope && this.$scope.onShareAppMessage) { + let oldShareAppMessage = this.$scope.onShareAppMessage; + this.$scope.onShareAppMessage = function(options) { + stat.interceptShare(false); + return oldShareAppMessage.call(this, options) + }; + } + }, + onShow() { + isHide = false; + stat.show(this); + }, + onHide() { + isHide = true; + stat.hide(this); + }, + onUnload() { + if (isHide) { + isHide = false; + return + } + stat.hide(this); + }, + onError(e) { + stat.error(e); + } }; -function main() { - if (process.env.NODE_ENV === 'development') { - uni.report = function(type, options) {}; - } else { - const Vue = require('vue'); - (Vue.default || Vue).mixin(lifecycle); - uni.report = function(type, options) { - stat.sendEvent(type, options); - }; - } + +function main() { + if (process.env.NODE_ENV === 'development') { + uni.report = function(type, options) {}; + } else { + console.log('统计已开启'); + const Vue = require('vue'); + (Vue.default || Vue).mixin(lifecycle); + uni.report = function(type, options) { + stat.sendEvent(type, options); + }; + } } -main(); +main(); diff --git a/packages/uni-stat/src/config.js b/packages/uni-stat/src/config.js index 4b2e79fb2143f4abce32ff253301c18edba88294..22ddb49ee80f1ea05f900e97fb478466098fe121 100644 --- a/packages/uni-stat/src/config.js +++ b/packages/uni-stat/src/config.js @@ -1,11 +1,11 @@ -import { - version -} from '../package.json' -export const STAT_VERSION = version -export const STAT_URL = 'https://tongji.dcloud.io/uni/stat' -export const STAT_H5_URL = 'https://tongji.dcloud.io/uni/stat.gif' +// 访问开始即启动小程序,访问结束结分为:进入后台超过5min、在前台无任何操作超过30min、在新的来源打开小程序; +export const STAT_VERSION = '0.0.1' +export const STAT_URL = 'https://tongji.dcloud.io/uni/stat' +export const STAT_H5_URL = 'https://tongji.dcloud.io/uni/stat.gif' export const STAT_KEY = 'qkTHEIegZGcL5iy3' -export const PAGE_PVER_TIME = 1800 -export const APP_PVER_TIME = 300 -export const OPERATING_TIME = 10 +export const PAGE_PVER_TIME = 1800 // 页面在前台无操作结束访问时间 单位s +export const APP_PVER_TIME = 300 // 应用在后台结束访问时间 单位s +export const OPERATING_TIME = 10 // 数据上报时间 单位s export const DIFF_TIME = 60 * 1000 * 60 * 24 + +export const DEBUG = true diff --git a/packages/uni-stat/src/core/report.js b/packages/uni-stat/src/core/report.js new file mode 100644 index 0000000000000000000000000000000000000000..ce23bb839f4bf36215f72785e8936108682fdd00 --- /dev/null +++ b/packages/uni-stat/src/core/report.js @@ -0,0 +1,547 @@ +let statConfig = require('uni-stat-config').default || require('uni-stat-config'); + +import { + get_time, + set_page_residence_time, + get_first_visit_time, + get_last_visit_time, + get_total_visit_count, + get_page_residence_time, + get_first_time, + get_last_time, + get_residence_time +} from '../utils/pageTime.js' + +import { + get_uuid, + get_platform_name, + get_pack_name, + get_scene, + get_version, + get_channel, + get_splicing, + get_page_route, + get_route, + handle_data, + calibration, + get_page_name, + get_sgin, + get_encodeURIComponent_options, + is_report_data, +} from '../utils/pageInfo.js' + +import { + sys +} from '../utils/util.js' + +import { + STAT_VERSION, + OPERATING_TIME, + STAT_URL, + STAT_H5_URL, + DEBUG +} from '../config.js'; + +import { + dbSet, + dbGet, + dbRemove +} from '../utils/db.js' + +// 统计数据默认值 +let statData = { + uuid: get_uuid(), // 设备标识 + ut: get_platform_name(), // 平台类型 + mpn: get_pack_name(), // 原生平台包名、小程序 appid + ak: statConfig.appid, // uni-app 应用 Appid + usv: STAT_VERSION, // 统计 sdk 版本 + v: get_version(), // 应用版本,仅app + ch: get_channel(), // 渠道信息 + cn: '', // 国家 + pn: '', // 省份 + ct: '', // 城市 + t: get_time(), // 上报数据时的时间戳 + tt: '', + p: sys.platform === 'android' ? 'a' : 'i', // 手机系统 + brand: sys.brand || '', // 手机品牌 + md: sys.model, // 手机型号 + sv: sys.system.replace(/(Android|iOS)\s/, ''), // 手机系统版本 + mpsdk: sys.SDKVersion || '', // x程序 sdk version + mpv: sys.version || '', // 小程序平台版本 ,如微信、支付宝 + lang: sys.language, // 语言 + pr: sys.pixelRatio, // pixelRatio 设备像素比 + ww: sys.windowWidth, // windowWidth 可使用窗口宽度 + wh: sys.windowHeight, // windowHeight 可使用窗口高度 + sw: sys.screenWidth, // screenWidth 屏幕宽度 + sh: sys.screenHeight, // screenHeight 屏幕高度 +} +export default class Report { + constructor() { + // 页面实例 + this.self = '' + // 进入应用标识 + this.__licationShow = false + // 离开应用标识 + this.__licationHide = false + // 统计默认值 + this.statData = statData + // 标题默认值 + this._navigationBarTitle = { + config: '', + page: '', + report: '', + lt: '', + } + + // 页面参数 + this._query = {} + // 页面最后停留页面的 url + // this._lastPageRoute = '' + + // 注册拦截器 + let registerInterceptor = typeof uni.addInterceptor === 'function' + if (registerInterceptor) { + this.addInterceptorInit() + this.interceptLogin() + this.interceptShare(true) + this.interceptRequestPayment() + } + } + + addInterceptorInit() { + let self = this + uni.addInterceptor('setNavigationBarTitle', { + invoke(args) { + self._navigationBarTitle.page = args.title + }, + }) + } + + interceptLogin() { + let self = this + uni.addInterceptor('login', { + complete() { + self._login() + }, + }) + } + + interceptShare(type) { + let self = this + if (!type) { + self._share() + return + } + uni.addInterceptor('share', { + success() { + self._share() + }, + fail() { + self._share() + }, + }) + } + + interceptRequestPayment() { + let self = this + uni.addInterceptor('requestPayment', { + success() { + self._payment('pay_success') + }, + fail() { + self._payment('pay_fail') + }, + }) + } + + _login() { + this.sendEventRequest({ + key: 'login', + }, + 0 + ) + } + + _share() { + this.sendEventRequest({ + key: 'share', + }, 0) + } + _payment(key) { + this.sendEventRequest({ + key, + }, 0) + } + + /** + * 进入应用触发 + */ + applicationShow() { + // 通过 __licationHide 判断保证是进入后台后在次进入应用,避免重复上报数据 + if (this.__licationHide) { + get_last_time() + const time = get_residence_time('app') + // 需要判断进入后台是否超过时限 ,默认是 30min ,是的话需要执行进入应用的上报 + if (time.overtime) { + let lastPageRoute = uni.getStorageSync('_STAT_LAST_PAGE_ROUTE') + let options = { + path: lastPageRoute, + scene: this.statData.sc, + } + this.sendReportRequest(options) + } + // 状态重置 + this.__licationHide = false + } + } + + /** + * 离开应用触发 + * @param {Object} self + * @param {Object} type + */ + applicationHide(self, type) { + // 进入应用后台保存状态,方便进入前台后判断是否上报应用数据 + this.__licationHide = true + get_last_time() + const time = get_residence_time() + const route = get_page_route(self) + // this._lastPageRoute = route + uni.setStorageSync('_STAT_LAST_PAGE_ROUTE', route) + this.sendHideRequest({ + urlref: route, + urlref_ts: time.residenceTime, + }, + type + ) + // 重置时间 + get_first_time() + } + + /** + * 进入页面触发 + */ + pageShow(self) { + // 清空值 ,初始化 ,避免污染后面的上报数据 + this._navigationBarTitle = { + config: '', + page: '', + report: '', + lt: '', + } + + const route = get_page_route(self) + const routepath = get_route(self) + + this._navigationBarTitle.config = get_page_name(routepath) + // 表示应用触发 ,页面切换不触发之后的逻辑 + if (this.__licationShow) { + get_first_time() + // this._lastPageRoute = route + uni.setStorageSync('_STAT_LAST_PAGE_ROUTE', route) + this.__licationShow = false + return + } + + get_last_time() + + const time = get_residence_time('page') + // 停留时间 + if (time.overtime) { + let options = { + path: route, + scene: this.statData.sc, + } + this.sendReportRequest(options) + } + // 重置时间 + get_first_time() + } + + /** + * 离开页面触发 + */ + pageHide(self) { + if (!this.__licationHide) { + get_last_time() + const time = get_residence_time('page') + let route = get_page_route(self) + let lastPageRoute = uni.getStorageSync('_STAT_LAST_PAGE_ROUTE') + if (!lastPageRoute) { + lastPageRoute = route + } + uni.setStorageSync('_STAT_LAST_PAGE_ROUTE', route) + this.sendPageRequest({ + url: route, + urlref: lastPageRoute, + urlref_ts: time.residenceTime, + }) + // this._lastPageRoute = route + return + } + } + + + /** + * 发送请求,应用维度上报 + * @param {Object} options 页面信息 + */ + sendReportRequest(options) { + this._navigationBarTitle.lt = '1' + this._navigationBarTitle.config = get_page_name(options.path) + let is_opt = options.query && JSON.stringify(options.query) !== '{}' + let query = is_opt ? '?' + JSON.stringify(options.query) : '' + Object.assign(this.statData, { + lt: '1', + url: (options.path + query) || '', + t: get_time(), + sc: get_scene(options.scene), + fvts: get_first_visit_time(), + lvts: get_last_visit_time(), + tvc: get_total_visit_count() + }) + if (get_platform_name() === 'n') { + this.getProperty() + } else { + this.getNetworkInfo() + } + } + + /** + * 发送请求,页面维度上报 + * @param {Object} opt + */ + sendPageRequest(opt) { + let { + url, + urlref, + urlref_ts + } = opt + this._navigationBarTitle.lt = '11' + let options = { + ak: this.statData.ak, + uuid: this.statData.uuid, + lt: '11', + ut: this.statData.ut, + url, + tt: this.statData.tt, + urlref, + urlref_ts, + ch: this.statData.ch, + usv: this.statData.usv, + t: get_time(), + p: this.statData.p, + } + this.request(options) + } + + /** + * 进入后台上报数据 + * @param {Object} opt + * @param {Object} type + */ + sendHideRequest(opt, type) { + let { + urlref, + urlref_ts + } = opt + let options = { + ak: this.statData.ak, + uuid: this.statData.uuid, + lt: '3', + ut: this.statData.ut, + urlref, + urlref_ts, + ch: this.statData.ch, + usv: this.statData.usv, + t: get_time(), + p: this.statData.p, + } + this.request(options, type) + } + + /** + * 自定义事件上报 + */ + sendEventRequest({ + key = '', + value = '' + } = {}) { + // const route = this._lastPageRoute + const routepath = get_route() + this._navigationBarTitle.config = get_page_name(routepath) + this._navigationBarTitle.lt = '21' + let options = { + ak: this.statData.ak, + uuid: this.statData.uuid, + lt: '21', + ut: this.statData.ut, + url: routepath, + ch: this.statData.ch, + e_n: key, + e_v: typeof value === 'object' ? JSON.stringify(value) : value.toString(), + usv: this.statData.usv, + t: get_time(), + p: this.statData.p, + } + this.request(options) + } + + /** + * 获取wgt资源版本 + */ + getProperty() { + plus.runtime.getProperty(plus.runtime.appid, (wgtinfo) => { + this.statData.v = wgtinfo.version || '' + this.getNetworkInfo() + }) + } + + /** + * 获取网络信息 + */ + getNetworkInfo() { + uni.getNetworkType({ + success: (result) => { + this.statData.net = result.networkType + this.getLocation() + }, + }) + } + + /** + * 获取位置信息 + */ + getLocation() { + if (statConfig.getLocation) { + uni.getLocation({ + type: 'wgs84', + geocode: true, + success: (result) => { + if (result.address) { + this.statData.cn = result.address.country + this.statData.pn = result.address.province + this.statData.ct = result.address.city + } + + this.statData.lat = result.latitude + this.statData.lng = result.longitude + this.request(this.statData) + }, + }) + } else { + this.statData.lat = 0 + this.statData.lng = 0 + this.request(this.statData) + } + } + + /** + * 发送请求 + * @param {Object} data 上报数据 + * @param {Object} type 类型 + */ + request(data, type) { + let time = get_time() + const title = this._navigationBarTitle + Object.assign(data, { + ttn: title.page, + ttpj: title.config, + ttc: title.report + }) + + let uniStatData = dbGet('__UNI__STAT__DATA') || {} + if (!uniStatData[data.lt]) { + uniStatData[data.lt] = [] + } + // 加入队列 + uniStatData[data.lt].push(data) + dbSet('__UNI__STAT__DATA', uniStatData) + + let page_residence_time = get_page_residence_time() + // 判断时候到达上报时间 ,默认 10 秒上报 + if (page_residence_time < OPERATING_TIME && !type) return + + // 时间超过,重新获取时间戳 + set_page_residence_time() + const stat_data = handle_data(uniStatData) + let optionsData = { + usv: STAT_VERSION, //统计 SDK 版本号 + t: time, //发送请求时的时间戮 + requests: stat_data, + } + + // 重置队列 + dbRemove('__UNI__STAT__DATA') + + if (data.ut === 'h5') { + this.imageRequest(optionsData) + return + } + + // XXX 安卓需要延迟上报 ,否则会有未知错误,需要验证处理 + if (get_platform_name() === 'n' && this.statData.p === 'a') { + setTimeout(() => { + this.sendRequest(optionsData) + }, 200) + return + } + + this.sendRequest(optionsData) + } + getIsReportData(){ + return is_report_data() + } + + /** + * 数据上报 + * @param {Object} optionsData 需要上报的数据 + */ + sendRequest(optionsData) { + this.getIsReportData().then(() => { + uni.request({ + url: STAT_URL, + method: 'POST', + // header: { + // 'content-type': 'application/json' // 默认值 + // }, + data: optionsData, + success: () => {}, + fail: (e) => { + if (++this._retry < 3) { + setTimeout(() => { + this.sendRequest(optionsData) + }, 1000) + } + }, + }) + }) + } + + /** + * h5 请求 + */ + imageRequest(data) { + this.getIsReportData().then(() => { + let image = new Image() + let options = get_sgin(get_encodeURIComponent_options(data)).options + image.src = STAT_H5_URL + '?' + options + }) + } + + sendEvent(key, value) { + // 校验 type 参数 + if (calibration(key, value)) return + + if (key === 'title') { + this._navigationBarTitle.report = value + return + } + this.sendEventRequest({ + key, + value: typeof value === 'object' ? JSON.stringify(value) : value, + }, + 1 + ) + } +} diff --git a/packages/uni-stat/src/core/stat.js b/packages/uni-stat/src/core/stat.js new file mode 100644 index 0000000000000000000000000000000000000000..f7bd623893bfaf80185758bc60938894e5f6720a --- /dev/null +++ b/packages/uni-stat/src/core/stat.js @@ -0,0 +1,99 @@ +import Report from './report.js' +import Vue from 'vue' +let vue = (Vue.default || Vue) +import { + set_page_residence_time +} from '../utils/pageTime.js' +import { + get_page_types, + get_platform_name +} from '../utils/pageInfo.js' + +class Stat extends Report { + static getInstance() { + if (!vue.instance) { + vue.instance = new Stat() + } + return vue.instance + } + constructor() { + super() + this.instance = null + } + + /** + * 进入应用 + * @param {Object} options 页面参数 + * @param {Object} self 当前页面实例 + */ + launch(options, self) { + // 初始化页面停留时间 start + let residence_time = set_page_residence_time() + this.__licationShow = true + this.sendReportRequest(options, true) + } + load(options, self) { + this.self = self + this._query = options + } + + appHide(self){ + this.applicationHide(self, true) + } + + appShow(self){ + this.applicationShow(self) + } + + show(self) { + this.self = self + if (get_page_types(self) === 'page') { + this.pageShow(self) + } + if (get_page_types(self) === 'app') { + this.appShow(self) + } + } + + hide(self) { + this.self = self + if (get_page_types(self) === 'page') { + this.pageHide(self) + } + if (get_page_types(self) === 'app') { + this.appHide(self) + } + } + + error(em) { + // 开发工具内不上报错误 + if (this._platform === 'devtools') { + if (process.env.NODE_ENV === 'development') { + console.info('当前运行环境为开发者工具,不上报数据。') + return; + } + } + let emVal = '' + if (!em.message) { + emVal = JSON.stringify(em) + } else { + emVal = em.stack + } + let options = { + ak: this.statData.ak, + uuid: this.statData.uuid, + lt: '31', + ut: this.statData.ut, + ch: this.statData.ch, + mpsdk: this.statData.mpsdk, + mpv: this.statData.mpv, + v: this.statData.v, + em: emVal, + usv: this.statData.usv, + t: parseInt(new Date().getTime() / 1000), + p: this.statData.p, + } + this.request(options) + } +} +export default Stat diff --git a/packages/uni-stat/src/index.js b/packages/uni-stat/src/index.js index d2556c13b933b0e9b9c582a18466c8e00e574f6f..ba62a88db2b0ebb56ba1bf7850becab65124b4ae 100644 --- a/packages/uni-stat/src/index.js +++ b/packages/uni-stat/src/index.js @@ -1,54 +1,61 @@ -import Stat from './stat.js'; -const stat = Stat.getInstance(); +import { + get_platform_name, + get_page_vm +} from './utils/pageInfo.js' +import Stat from './core/stat.js' +const stat = Stat.getInstance() + +// 用于判断是隐藏页面还是卸载页面 let isHide = false + const lifecycle = { - onLaunch(options) { - stat.report(options, this); - }, - onReady() { - stat.ready(this); - }, - onLoad(options) { - stat.load(options, this); - // 重写分享,获取分享上报事件 - if (this.$scope && this.$scope.onShareAppMessage) { - let oldShareAppMessage = this.$scope.onShareAppMessage; - this.$scope.onShareAppMessage = function(options) { - stat.interceptShare(false); - return oldShareAppMessage.call(this, options) - } - } - }, - onShow() { - isHide = false - stat.show(this); - }, - onHide() { - isHide = true - stat.hide(this); - }, - onUnload() { - if (isHide) { - isHide = false - return - } - stat.hide(this); - }, - onError(e) { - stat.error(e) - } + onLaunch(options) { + // 进入应用上报数据 + stat.launch(options, this); + }, + onLoad(options) { + stat.load(options, this); + // 重写分享,获取分享上报事件 + if (this.$scope && this.$scope.onShareAppMessage) { + let oldShareAppMessage = this.$scope.onShareAppMessage; + this.$scope.onShareAppMessage = function(options) { + stat.interceptShare(false); + return oldShareAppMessage.call(this, options) + } + } + }, + onShow() { + isHide = false + stat.show(this); + }, + onHide() { + isHide = true + stat.hide(this); + }, + onUnload() { + if (isHide) { + isHide = false + return + } + stat.hide(this); + }, + onError(e) { + stat.error(e) + } } -function main() { - if (process.env.NODE_ENV === 'development') { - uni.report = function(type, options) {}; - } else { - const Vue = require('vue'); - (Vue.default || Vue).mixin(lifecycle); - uni.report = function(type, options) { - stat.sendEvent(type, options); - }; - } + +function main() { + if (process.env.NODE_ENV === 'development') { + uni.report = function(type, options) {}; + } else { + console.log('统计已开启'); + const Vue = require('vue'); + (Vue.default || Vue).mixin(lifecycle); + uni.report = function(type, options) { + stat.sendEvent(type, options); + }; + } } -main(); +main() diff --git a/packages/uni-stat/src/parameter.js b/packages/uni-stat/src/parameter.js deleted file mode 100644 index 838454e9ea4a1c4fe86c3f92d79a53fd1314818d..0000000000000000000000000000000000000000 --- a/packages/uni-stat/src/parameter.js +++ /dev/null @@ -1,396 +0,0 @@ -import { - PAGE_PVER_TIME, - APP_PVER_TIME, - STAT_URL, - STAT_VERSION, - DIFF_TIME -} from './config'; -const uniI18n = require('@dcloudio/uni-cli-i18n') -const statConfig = require('uni-stat-config').default || require('uni-stat-config'); -const UUID_KEY = '__DC_STAT_UUID'; -const UUID_VALUE = '__DC_UUID_VALUE'; - -export function getUuid() { - let uuid = ''; - if (getPlatformName() === 'n') { - try { - uuid = plus.runtime.getDCloudId() - } catch (e) { - uuid = ''; - } - return uuid - } - - try { - uuid = uni.getStorageSync(UUID_KEY); - } catch (e) { - uuid = UUID_VALUE; - } - - if (!uuid) { - uuid = Date.now() + '' + Math.floor(Math.random() * 1e7); - try { - uni.setStorageSync(UUID_KEY, uuid); - } catch (e) { - uni.setStorageSync(UUID_KEY, UUID_VALUE); - } - } - return uuid; -} - -export const getSgin = (statData) => { - let arr = Object.keys(statData) - let sortArr = arr.sort(); - let sgin = {}; - let sginStr = '' - for (var i in sortArr) { - sgin[sortArr[i]] = statData[sortArr[i]]; - sginStr += sortArr[i] + '=' + statData[sortArr[i]] + '&' - } - // const options = sginStr.substr(0, sginStr.length - 1) - // sginStr = sginStr.substr(0, sginStr.length - 1) + '&key=' + STAT_KEY; - // const si = crypto.createHash('md5').update(sginStr).digest('hex'); - return { - sign: '', - options: sginStr.substr(0, sginStr.length - 1) - }; -} - -export const getSplicing = (data) => { - let str = '' - for (var i in data) { - str += i + '=' + data[i] + '&' - } - return str.substr(0, str.length - 1) -} - -export const getTime = () => { - return parseInt(new Date().getTime() / 1000); -} - -export const getPlatformName = () => { - const aliArr = ['y', 'a', 'p', 'mp-ali'] - const platformList = { - 'app-plus': 'n', - 'h5': 'h5', - 'mp-weixin': 'wx', - [aliArr.reverse().join('')]: 'ali', - 'mp-baidu': 'bd', - 'mp-toutiao': 'tt', - 'mp-qq': 'qq', - 'mp-jd': 'jd', - 'quickapp-native': 'qn', - 'mp-kuaishou': 'ks', - 'mp-lark': 'lark' - } - return platformList[process.env.VUE_APP_PLATFORM]; -} - -export const getPackName = () => { - let packName = '' - if (getPlatformName() === 'wx' || getPlatformName() === 'qq') { - // 兼容微信小程序低版本基础库 - if (uni.canIUse('getAccountInfoSync')) { - packName = uni.getAccountInfoSync().miniProgram.appId || ''; - } - } - return packName -} - -export const getVersion = () => { - return getPlatformName() === 'n' ? plus.runtime.version : ''; -} - -export const getChannel = () => { - const platformName = getPlatformName(); - let channel = ''; - if (platformName === 'n') { - channel = plus.runtime.channel; - } - if (platformName === 'wx') { - // TODO; - } - return channel; -} - -export const getScene = (options) => { - const platformName = getPlatformName(); - let scene = ''; - if (options) { - return options; - } - if (platformName === 'wx') { - scene = uni.getLaunchOptionsSync().scene; - } - return scene; -} -const First__Visit__Time__KEY = 'First__Visit__Time' -const Last__Visit__Time__KEY = 'Last__Visit__Time' - -export const getFirstVisitTime = () => { - const timeStorge = uni.getStorageSync(First__Visit__Time__KEY); - let time = 0; - if (timeStorge) { - time = timeStorge; - } else { - time = getTime(); - uni.setStorageSync(First__Visit__Time__KEY, time); - uni.removeStorageSync(Last__Visit__Time__KEY); - } - return time; -} - -export const getLastVisitTime = () => { - const timeStorge = uni.getStorageSync(Last__Visit__Time__KEY); - let time = 0; - if (timeStorge) { - time = timeStorge; - } else { - time = ''; - } - uni.setStorageSync(Last__Visit__Time__KEY, getTime()); - return time; -} - - -const PAGE_RESIDENCE_TIME = '__page__residence__time' -let First_Page_residence_time = 0; -let Last_Page_residence_time = 0; - - -export const setPageResidenceTime = () => { - First_Page_residence_time = getTime() - if (getPlatformName() === 'n') { - uni.setStorageSync(PAGE_RESIDENCE_TIME, getTime()); - } - return First_Page_residence_time -} - -export const getPageResidenceTime = () => { - Last_Page_residence_time = getTime() - if (getPlatformName() === 'n') { - First_Page_residence_time = uni.getStorageSync(PAGE_RESIDENCE_TIME); - } - return Last_Page_residence_time - First_Page_residence_time -} -const TOTAL__VISIT__COUNT = 'Total__Visit__Count' -export const getTotalVisitCount = () => { - const timeStorge = uni.getStorageSync(TOTAL__VISIT__COUNT); - let count = 1; - if (timeStorge) { - count = timeStorge; - count++ - } - uni.setStorageSync(TOTAL__VISIT__COUNT, count); - return count; -} - -export const GetEncodeURIComponentOptions = (statData) => { - let data = {}; - for (let prop in statData) { - data[prop] = encodeURIComponent(statData[prop]); - } - return data; -} - -let Set__First__Time = 0; -let Set__Last__Time = 0; - -export const getFirstTime = () => { - let time = new Date().getTime(); - Set__First__Time = time; - Set__Last__Time = 0; - return time; -} - - -export const getLastTime = () => { - let time = new Date().getTime(); - Set__Last__Time = time; - return time; -} - - -export const getResidenceTime = (type) => { - let residenceTime = 0; - if (Set__First__Time !== 0) { - residenceTime = Set__Last__Time - Set__First__Time - } - - residenceTime = parseInt(residenceTime / 1000); - residenceTime = residenceTime < 1 ? 1 : residenceTime; - if (type === 'app') { - let overtime = residenceTime > APP_PVER_TIME ? true : false - return { - residenceTime, - overtime - }; - } - if (type === 'page') { - let overtime = residenceTime > PAGE_PVER_TIME ? true : false - return { - residenceTime, - overtime - }; - } - - return { - residenceTime - }; - -} - -export const getRoute = () => { - var pages = getCurrentPages(); - var page = pages[pages.length - 1]; - if (!page) return '' - let _self = page.$vm - - if (getPlatformName() === 'bd') { - return _self.$mp && _self.$mp.page.is; - } else { - return (_self.$scope && _self.$scope.route) || (_self.$mp && _self.$mp.page.route); - } -}; - -export const getPageRoute = (self) => { - var pages = getCurrentPages(); - var page = pages[pages.length - 1]; - if (!page) return '' - let _self = page.$vm - let query = self._query; - let str = query && JSON.stringify(query) !== '{}' ? '?' + JSON.stringify(query) : ''; - // clear - self._query = ''; - if (getPlatformName() === 'bd') { - return _self.$mp && _self.$mp.page.is + str; - } else { - return (_self.$scope && _self.$scope.route + str) || (_self.$mp && _self.$mp.page.route + str); - } -}; - -export const getPageTypes = (self) => { - if (self.mpType === 'page' || (self.$mp && self.$mp.mpType === 'page') || self.$options.mpType === 'page') { - return true; - } - return false; -} - -export const calibration = (eventName, options) => { - // login 、 share 、pay_success 、pay_fail 、register 、title - if (!eventName) { - console.error(`uni.report ${uniI18n.__('uniStat.missingParameter')}`); - return true - } - if (typeof eventName !== 'string') { - console.error(`uni.report [eventName] ${uniI18n.__('uniStat.parameterTypeErrrorString')}`); - return true - } - if (eventName.length > 255) { - console.error(`uni.report [eventName] ${uniI18n.__('uniStat.parameterLengthLess')} 255`); - return true - } - - if (typeof options !== 'string' && typeof options !== 'object') { - console.error('uni.report [options] ' + uniI18n.__('uniStat.parameterTypeErrrorStringOrObject')); - return true - } - - if (typeof options === 'string' && options.length > 255) { - console.error(`uni.report [options] ${uniI18n.__('uniStat.parameterLengthLess')} 255`); - return true - } - - if (eventName === 'title' && typeof options !== 'string') { - console.error(`uni.report [eventName] ${uniI18n.__('uniStat.hasTitleOptionString')}`); - return true - } -} - -const Report_Data_Time = 'Report_Data_Time' -const Report_Status = 'Report_Status' -export const isReportData = () => { - return new Promise((resolve, reject) => { - let start_time = '' - let end_time = new Date().getTime() - let diff_time = DIFF_TIME - let report_status = 1 - try { - start_time = uni.getStorageSync(Report_Data_Time) - report_status = uni.getStorageSync(Report_Status) - } catch (e) { - start_time = '' - report_status = 1 - } - - if (report_status === '') { - requestData(({ - enable - }) => { - uni.setStorageSync(Report_Data_Time, end_time); - uni.setStorageSync(Report_Status, enable); - if (enable === 1) { - resolve(); - } - }); - return - } - - if (report_status === 1) { - resolve(); - } - - if (!start_time) { - uni.setStorageSync(Report_Data_Time, end_time) - start_time = end_time - } - - if ((end_time - start_time) > diff_time) { - requestData(({ - enable - }) => { - uni.setStorageSync(Report_Data_Time, end_time) - uni.setStorageSync(Report_Status, enable) - }); - } - - }) -} - -const requestData = (done) => { - let formData = { - usv: STAT_VERSION, - conf: JSON.stringify({ - ak: statConfig.appid - }) - } - uni.request({ - url: STAT_URL, - method: 'GET', - data: formData, - success: (res) => { - const { - data - } = res - if (data.ret === 0) { - typeof done === 'function' && done({ - enable: data.enable - }) - } - }, - fail: (e) => { - let report_status_code = 1 - try { - report_status_code = uni.getStorageSync(Report_Status) - } catch (e) { - report_status_code = 1 - } - if (report_status_code === '') { - report_status_code = 1 - } - typeof done === 'function' && done({ - enable: report_status_code - }) - } - }); -} diff --git a/packages/uni-stat/src/stat.js b/packages/uni-stat/src/stat.js deleted file mode 100644 index a10cee47eebcc2f6563dedb40da6b3f862a67f1c..0000000000000000000000000000000000000000 --- a/packages/uni-stat/src/stat.js +++ /dev/null @@ -1,569 +0,0 @@ -const PagesJson = require('uni-pages?{"type":"style"}').default -const statConfig = require('uni-stat-config').default || require('uni-stat-config'); -import { - getUuid, - getSgin, - getSplicing, - getPackName, - getPlatformName, - getVersion, - getChannel, - getScene, - getTime, - getFirstVisitTime, - getLastVisitTime, - setPageResidenceTime, - getPageResidenceTime, - getTotalVisitCount, - GetEncodeURIComponentOptions, - getFirstTime, - getLastTime, - getResidenceTime, - getPageRoute, - getRoute, - getPageTypes, - calibration, - isReportData -} from './parameter'; - -import { - STAT_URL, - STAT_VERSION, - STAT_H5_URL, - OPERATING_TIME -} from './config'; - -const resultOptions = uni.getSystemInfoSync(); - -class Util { - constructor() { - this.self = ''; - this._retry = 0; - this._platform = ''; - this._query = {}; - this._navigationBarTitle = { - config: '', - page: '', - report: '', - lt: '' - } - this._operatingTime = 0; - this._reportingRequestData = { - '1': [], - '11': [] - }; - this.__prevent_triggering = false - - this.__licationHide = false; - this.__licationShow = false; - this._lastPageRoute = ''; - this.statData = { - uuid: getUuid(), - ut: getPlatformName(), - mpn: getPackName(), - ak: statConfig.appid, - usv: STAT_VERSION, - v: getVersion(), - ch: getChannel(), - cn: '', - pn: '', - ct: '', - t: getTime(), - tt: '', - p: resultOptions.platform === 'android' ? 'a' : 'i', - brand: resultOptions.brand || '', - md: resultOptions.model, - sv: resultOptions.system.replace(/(Android|iOS)\s/, ''), - mpsdk: resultOptions.SDKVersion || '', - mpv: resultOptions.version || '', - lang: resultOptions.language, - pr: resultOptions.pixelRatio, - ww: resultOptions.windowWidth, - wh: resultOptions.windowHeight, - sw: resultOptions.screenWidth, - sh: resultOptions.screenHeight - } - - } - - getIsReportData() { - return isReportData() - } - - _applicationShow() { - if (this.__licationHide) { - getLastTime(); - const time = getResidenceTime('app'); - if (time.overtime) { - let options = { - path: this._lastPageRoute, - scene: this.statData.sc - } - this._sendReportRequest(options); - } - this.__licationHide = false; - } - } - - _applicationHide(self, type) { - - this.__licationHide = true; - getLastTime(); - const time = getResidenceTime(); - getFirstTime(); - const route = getPageRoute(this); - this._sendHideRequest({ - urlref: route, - urlref_ts: time.residenceTime - }, type) - } - - _pageShow() { - const route = getPageRoute(this); - const routepath = getRoute(this); - this._navigationBarTitle.config = PagesJson && - PagesJson.pages[routepath] && - PagesJson.pages[routepath].titleNView && - PagesJson.pages[routepath].titleNView.titleText || - PagesJson && - PagesJson.pages[routepath] && - PagesJson.pages[routepath].navigationBarTitleText || ''; - - if (this.__licationShow) { - getFirstTime(); - this.__licationShow = false; - // console.log('这是 onLauch 之后执行的第一次 pageShow ,为下次记录时间做准备'); - this._lastPageRoute = route; - return; - } - - getLastTime(); - this._lastPageRoute = route - const time = getResidenceTime('page'); - if (time.overtime) { - let options = { - path: this._lastPageRoute, - scene: this.statData.sc - }; - this._sendReportRequest(options); - } - getFirstTime(); - } - - _pageHide() { - if (!this.__licationHide) { - getLastTime(); - const time = getResidenceTime('page'); - this._sendPageRequest({ - url: this._lastPageRoute, - urlref: this._lastPageRoute, - urlref_ts: time.residenceTime - }); - this._navigationBarTitle = { - config: '', - page: '', - report: '', - lt: '' - }; - return; - } - } - - _login() { - this._sendEventRequest({ - key: 'login' - }, 0) - } - - _share() { - this._sendEventRequest({ - key: 'share' - }, 0) - } - _payment(key) { - this._sendEventRequest({ - key - }, 0) - } - _sendReportRequest(options) { - - this._navigationBarTitle.lt = '1'; - let query = options.query && JSON.stringify(options.query) !== '{}' ? '?' + JSON.stringify(options.query) : ''; - this.statData.lt = '1'; - this.statData.url = (options.path + query) || ''; - this.statData.t = getTime(); - this.statData.sc = getScene(options.scene); - this.statData.fvts = getFirstVisitTime(); - this.statData.lvts = getLastVisitTime(); - this.statData.tvc = getTotalVisitCount(); - if (getPlatformName() === 'n') { - this.getProperty(); - } else { - this.getNetworkInfo(); - } - } - - _sendPageRequest(opt) { - let { - url, - urlref, - urlref_ts - } = opt; - this._navigationBarTitle.lt = '11'; - let options = { - ak: this.statData.ak, - uuid: this.statData.uuid, - lt: '11', - ut: this.statData.ut, - url, - tt: this.statData.tt, - urlref, - urlref_ts, - ch: this.statData.ch, - usv: this.statData.usv, - t: getTime(), - p: this.statData.p - } - this.request(options); - } - - _sendHideRequest(opt, type) { - let { - urlref, - urlref_ts - } = opt; - let options = { - ak: this.statData.ak, - uuid: this.statData.uuid, - lt: '3', - ut: this.statData.ut, - urlref, - urlref_ts, - ch: this.statData.ch, - usv: this.statData.usv, - t: getTime(), - p: this.statData.p - } - this.request(options, type) - } - _sendEventRequest({ - key = '', - value = "" - } = {}) { - const route = this._lastPageRoute; - let options = { - ak: this.statData.ak, - uuid: this.statData.uuid, - lt: '21', - ut: this.statData.ut, - url: route, - ch: this.statData.ch, - e_n: key, - e_v: typeof(value) === 'object' ? JSON.stringify(value) : value.toString(), - usv: this.statData.usv, - t: getTime(), - p: this.statData.p - } - this.request(options); - } - - getNetworkInfo() { - uni.getNetworkType({ - success: (result) => { - this.statData.net = result.networkType; - this.getLocation(); - } - }); - } - - getProperty() { - plus.runtime.getProperty(plus.runtime.appid, (wgtinfo) => { - this.statData.v = wgtinfo.version || ''; - this.getNetworkInfo(); - }); - } - - getLocation() { - if (statConfig.getLocation) { - uni.getLocation({ - type: 'wgs84', - geocode: true, - success: (result) => { - if (result.address) { - this.statData.cn = result.address.country; - this.statData.pn = result.address.province; - this.statData.ct = result.address.city; - } - - this.statData.lat = result.latitude; - this.statData.lng = result.longitude; - this.request(this.statData); - } - }); - } else { - this.statData.lat = 0; - this.statData.lng = 0; - this.request(this.statData); - } - } - - request(data, type) { - let time = getTime(); - const title = this._navigationBarTitle; - data.ttn = title.page; - data.ttpj = title.config; - data.ttc = title.report; - - let requestData = this._reportingRequestData; - if (getPlatformName() === 'n') { - requestData = uni.getStorageSync('__UNI__STAT__DATA') || {} - } - if (!requestData[data.lt]) { - requestData[data.lt] = []; - } - requestData[data.lt].push(data); - - if (getPlatformName() === 'n') { - uni.setStorageSync('__UNI__STAT__DATA', requestData) - } - if (getPageResidenceTime() < OPERATING_TIME && !type) { - return - } - let uniStatData = this._reportingRequestData - if (getPlatformName() === 'n') { - uniStatData = uni.getStorageSync('__UNI__STAT__DATA') - } - // 时间超过,重新获取时间戳 - setPageResidenceTime(); - let firstArr = []; - let contentArr = []; - let lastArr = []; - - for (let i in uniStatData) { - const rd = uniStatData[i]; - rd.forEach((elm) => { - const newData = getSplicing(elm); - if (i === 0) { - firstArr.push(newData); - } else if (i === 3) { - lastArr.push(newData); - } else { - contentArr.push(newData); - } - }); - } - - firstArr.push(...contentArr, ...lastArr); - let optionsData = { - usv: STAT_VERSION, //统计 SDK 版本号 - t: time, //发送请求时的时间戮 - requests: JSON.stringify(firstArr), - }; - - this._reportingRequestData = {}; - if (getPlatformName() === 'n') { - uni.removeStorageSync('__UNI__STAT__DATA') - } - - if (data.ut === 'h5') { - this.imageRequest(optionsData) - return - } - - if (getPlatformName() === 'n' && this.statData.p === 'a') { - setTimeout(() => { - this._sendRequest(optionsData); - }, 200) - return - } - this._sendRequest(optionsData) - } - _sendRequest(optionsData) { - this.getIsReportData().then(() => { - uni.request({ - url: STAT_URL, - method: 'POST', - // header: { - // 'content-type': 'application/json' // 默认值 - // }, - data: optionsData, - success: () => { - // if (process.env.NODE_ENV === 'development') { - // console.log('stat request success'); - // } - }, - fail: (e) => { - if (++this._retry < 3) { - setTimeout(() => { - this._sendRequest(optionsData); - }, 1000); - } - } - }); - }) - } - /** - * h5 请求 - */ - imageRequest(data) { - this.getIsReportData().then(() => { - let image = new Image(); - let options = getSgin(GetEncodeURIComponentOptions(data)).options; - image.src = STAT_H5_URL + '?' + options - }) - } - - sendEvent(key, value) { - // 校验 type 参数 - if (calibration(key, value)) return - - if (key === 'title') { - this._navigationBarTitle.report = value; - return - } - this._sendEventRequest({ - key, - value: typeof(value) === 'object' ? JSON.stringify(value) : value - }, 1); - } -} - - -class Stat extends Util { - static getInstance() { - if (!this.instance) { - this.instance = new Stat(); - } - return this.instance; - } - constructor() { - super() - this.instance = null; - // 注册拦截器 - if (typeof uni.addInterceptor === 'function' && process.env.NODE_ENV !== 'development') { - this.addInterceptorInit(); - this.interceptLogin(); - this.interceptShare(true); - this.interceptRequestPayment(); - } - } - - addInterceptorInit() { - let self = this; - uni.addInterceptor('setNavigationBarTitle', { - invoke(args) { - self._navigationBarTitle.page = args.title - } - }) - } - - interceptLogin() { - let self = this; - uni.addInterceptor('login', { - complete() { - self._login(); - } - }) - } - - interceptShare(type) { - let self = this; - if (!type) { - self._share(); - return - } - uni.addInterceptor('share', { - success() { - self._share(); - }, - fail() { - self._share(); - } - }) - } - - interceptRequestPayment() { - let self = this; - uni.addInterceptor('requestPayment', { - success() { - self._payment('pay_success'); - }, - fail() { - self._payment('pay_fail'); - } - }) - } - - report(options, self) { - this.self = self; - // if (process.env.NODE_ENV === 'development') { - // console.log('report init'); - // } - setPageResidenceTime() - this.__licationShow = true; - this._sendReportRequest(options, true); - } - - load(options, self) { - if (!self.$scope && !self.$mp) { - const page = getCurrentPages() - self.$scope = page[page.length - 1] - } - this.self = self; - this._query = options; - } - - show(self) { - this.self = self; - if (getPageTypes(self)) { - this._pageShow(self); - } else { - this._applicationShow(self); - } - } - - ready(self) { - // this.self = self; - // if (getPageTypes(self)) { - // this._pageShow(self); - // } - } - hide(self) { - this.self = self; - if (getPageTypes(self)) { - this._pageHide(self); - } else { - this._applicationHide(self, true); - } - } - error(em) { - if (this._platform === 'devtools') { - if (process.env.NODE_ENV === 'development') { - console.info('当前运行环境为开发者工具,不上报数据。'); - } - // return; - } - let emVal = '' - if (!em.message) { - emVal = JSON.stringify(em) - } else { - emVal = em.stack - } - let options = { - ak: this.statData.ak, - uuid: this.statData.uuid, - lt: '31', - ut: this.statData.ut, - ch: this.statData.ch, - mpsdk: this.statData.mpsdk, - mpv: this.statData.mpv, - v: this.statData.v, - em: emVal, - usv: this.statData.usv, - t: getTime(), - p: this.statData.p - } - this.request(options); - } -} -export default Stat diff --git a/packages/uni-stat/src/utils/config.js b/packages/uni-stat/src/utils/config.js new file mode 100644 index 0000000000000000000000000000000000000000..24c21c6bb405c6b2028704f54b4ee8d2d0b95612 --- /dev/null +++ b/packages/uni-stat/src/utils/config.js @@ -0,0 +1,11 @@ +// 访问开始即启动小程序,访问结束结分为:进入后台超过5min、在前台无任何操作超过30min、在新的来源打开小程序; +export const STAT_VERSION = '0.0.1' + +export const PAGE_PVER_TIME = 1800 // 页面在前台无操作结束访问时间 单位s +// export const PAGE_PVER_TIME = 0 +export const APP_PVER_TIME = 300 // 应用在后台结束访问时间 单位s +// export const APP_PVER_TIME = 0 +// export const OPERATING_TIME = 10 // 数据上报时间 单位s +export const OPERATING_TIME = 10 // 数据上报时间 单位s + +export const DEBUG = true diff --git a/packages/uni-stat/src/utils/db.js b/packages/uni-stat/src/utils/db.js new file mode 100755 index 0000000000000000000000000000000000000000..434f7114b95ddfe3ceaf31aad34f91c1c1a42ef1 --- /dev/null +++ b/packages/uni-stat/src/utils/db.js @@ -0,0 +1,33 @@ +export const dbSet = (name, value) => { + let data = uni.getStorageSync('$$STAT__DBDATA') || {} + if (!data) { + data = {} + } + data[name] = value + uni.setStorageSync('$$STAT__DBDATA', data) +} + +export const dbGet = (name) => { + let data = uni.getStorageSync('$$STAT__DBDATA') || {} + if (!data) { + data = {} + } + if (!data[name]) { + return undefined + } + return data[name] +} + +export const dbRemove = (name) => { + let data = uni.getStorageSync('$$STAT__DBDATA') || {} + if (data[name]) { + delete data[name] + uni.setStorageSync('$$STAT__DBDATA', data) + } else { + data = uni.getStorageSync('$$STAT__DBDATA') + if (data[name]) { + delete data[name] + uni.setStorageSync('$$STAT__DBDATA', data) + } + } +} diff --git a/packages/uni-stat/src/utils/deepClone.js b/packages/uni-stat/src/utils/deepClone.js new file mode 100755 index 0000000000000000000000000000000000000000..d356c5491442764272a0076d98765c0720bcecfe --- /dev/null +++ b/packages/uni-stat/src/utils/deepClone.js @@ -0,0 +1,22 @@ +// 判断arr是否为一个数组,返回一个bool值 +function isArray (arr) { + return Object.prototype.toString.call(arr) === '[object Array]'; +} + +// 深度克隆 +export const deepClone = (obj)=> { + // 对常见的“非”值,直接返回原来值 + if([null, undefined, NaN, false].includes(obj)) return obj; + if(typeof obj !== "object" && typeof obj !== 'function') { + //原始类型直接返回 + return obj; + } + var o = isArray(obj) ? [] : {}; + for(let i in obj) { + if(obj.hasOwnProperty(i)){ + o[i] = typeof obj[i] === "object" ? deepClone(obj[i]) : obj[i]; + } + } + return o; +} + diff --git a/packages/uni-stat/src/utils/deepMerge.js b/packages/uni-stat/src/utils/deepMerge.js new file mode 100755 index 0000000000000000000000000000000000000000..23d664cac58823849c27761bdae700f58e5491ea --- /dev/null +++ b/packages/uni-stat/src/utils/deepMerge.js @@ -0,0 +1,27 @@ +import {deepClone} from './deepClone.js' +// JS对象深度合并 +export const deepMerge = (target = {}, source = {})=> { + target = deepClone(target); + if (typeof target !== 'object' || typeof source !== 'object') return false; + for (var prop in source) { + if (!source.hasOwnProperty(prop)) continue; + if (prop in target) { + if (typeof target[prop] !== 'object') { + target[prop] = source[prop]; + } else { + if (typeof source[prop] !== 'object') { + target[prop] = source[prop]; + } else { + if (target[prop].concat && source[prop].concat) { + target[prop] = target[prop].concat(source[prop]); + } else { + target[prop] = deepMerge(target[prop], source[prop]); + } + } + } + } else { + target[prop] = source[prop]; + } + } + return target; +} diff --git a/packages/uni-stat/src/utils/index.js b/packages/uni-stat/src/utils/index.js new file mode 100755 index 0000000000000000000000000000000000000000..656d30279b07eef4f6be47f85a8d3112da83caa5 --- /dev/null +++ b/packages/uni-stat/src/utils/index.js @@ -0,0 +1,10 @@ +// 批量导出文件 +const requireApi = require.context('.', false, /.js$/) +let module = {} +let noAllowPaht = ['./index.js'] +requireApi.keys().forEach((key, index) => { + if ( noAllowPaht.indexOf(key) !== -1 ) return + Object.assign(module, requireApi(key)) +}) +export default module + diff --git a/packages/uni-stat/src/utils/pageInfo.js b/packages/uni-stat/src/utils/pageInfo.js new file mode 100644 index 0000000000000000000000000000000000000000..48df531a8e4252b61553371daf0f1041183a1435 --- /dev/null +++ b/packages/uni-stat/src/utils/pageInfo.js @@ -0,0 +1,392 @@ +import pagesTitle from 'uni-pages?{"type":"style"}' +let pagesData = pagesTitle.pages +let titleJsons = {} +for (let i in pagesData) { + titleJsons[i] = pagesData[i].navigationBarTitleText || '' +} + +import { + sys +} from './util.js' + + +import { + STAT_URL, + STAT_VERSION, + DIFF_TIME +} from '../config.js'; + +const UUID_KEY = '__DC_STAT_UUID' +const UUID_VALUE = '__DC_UUID_VALUE' + +function getUuid() { + let uuid = '' + if (get_platform_name() === 'n') { + try { + uuid = plus.runtime.getDCloudId() + } catch (e) { + uuid = '' + } + return uuid + } + + try { + uuid = uni.getStorageSync(UUID_KEY) + } catch (e) { + uuid = UUID_VALUE + } + + if (!uuid) { + uuid = Date.now() + '' + Math.floor(Math.random() * 1e7) + try { + uni.setStorageSync(UUID_KEY, uuid) + } catch (e) { + uni.setStorageSync(UUID_KEY, UUID_VALUE) + } + } + return uuid +} + +export const get_uuid = (statData) => { + // 有可能不存在 deviceId(一般不存在就是出bug了),就自己生成一个 + return sys.deviceId || getUuid() +} + +export const get_sgin = (statData) => { + let arr = Object.keys(statData) + let sortArr = arr.sort() + let sgin = {} + let sginStr = '' + for (var i in sortArr) { + sgin[sortArr[i]] = statData[sortArr[i]] + sginStr += sortArr[i] + '=' + statData[sortArr[i]] + '&' + } + + return { + sign: '', + options: sginStr.substr(0, sginStr.length - 1), + } +} + +export const get_encodeURIComponent_options = (statData) => { + let data = {} + for (let prop in statData) { + data[prop] = encodeURIComponent(statData[prop]) + } + return data +} + +/** + * 获取当前平台 + * 移动端 : 'n', + * h5 : 'h5', + * 微信 : 'wx', + * 阿里 : 'ali', + * 百度 : 'bd', + * 头条 : 'tt', + * qq : 'qq', + * 快应用 : 'qn', + * 快手 : 'ks', + * 飞书 : 'lark', + * 快应用 : 'qw', + * 钉钉 : 'dt' + */ +export const get_platform_name = () => { + // 苹果审核代码中禁止出现 alipay 字样 ,需要特殊处理一下 + const aliArr = ['y', 'a', 'p', 'mp-ali'] + const platformList = { + 'app': 'n', + 'app-plus': 'n', + h5: 'h5', + 'mp-weixin': 'wx', + [aliArr.reverse().join('')]: 'ali', + 'mp-baidu': 'bd', + 'mp-toutiao': 'tt', + 'mp-qq': 'qq', + 'quickapp-native': 'qn', + 'mp-kuaishou': 'ks', + 'mp-lark': 'lark', + 'quickapp-webview': 'qw' + } + if (platformList[process.env.VUE_APP_PLATFORM] === 'ali') { + if (my && my.env) { + const clientName = my.env.clientName + if (clientName === 'ap') return 'ali' + if (clientName === 'dingtalk') return 'dt' + // TODO 缺少 ali 下的其他平台 + } + } + return platformList[process.env.VUE_APP_PLATFORM] +} + +/** + * 获取小程序 appid + */ +export const get_pack_name = () => { + let packName = '' + if (get_platform_name() === 'wx' || get_platform_name() === 'qq') { + // 兼容微信小程序低版本基础库 + if (uni.canIUse('getAccountInfoSync')) { + packName = uni.getAccountInfoSync().miniProgram.appId || '' + } + } + if (get_platform_name() === 'n') { + // TODO APP 获取包名 + } + return packName +} + +/** + * 应用版本 + */ +export const get_version = () => { + return get_platform_name() === 'n' ? plus.runtime.version : '' +} + +/** + * 获取渠道 + */ +export const get_channel = () => { + const platformName = get_platform_name() + let channel = '' + if (platformName === 'n') { + channel = plus.runtime.channel + } + if (platformName === 'wx') { + // TODO 需要调研小程序二维码渠道如何获取; + } + return channel +} + +/** + * 获取小程序场景值 + * @param {Object} options 页面信息 + */ +export const get_scene = (options) => { + const platformName = get_platform_name() + let scene = '' + if (options) { + return options + } + if (platformName === 'wx') { + scene = uni.getLaunchOptionsSync().scene + } + return scene +} + +/** + * 获取拼接参数 + */ +export const get_splicing = (data) => { + let str = '' + for (var i in data) { + str += i + '=' + data[i] + '&' + } + return str.substr(0, str.length - 1) +} + +/** + * 获取页面url,不包含参数 + */ +export const get_route = (pageVm) => { + let _self = pageVm || get_page_vm(); + if (get_platform_name() === 'bd') { + let mp_route = _self.$mp && _self.$mp.page && _self.$mp.page.is; + let scope_route = _self.$scope && _self.$scope.is; + return mp_route || scope_route || '' + } else { + return _self.route || (_self.$scope && _self.$scope.route) || (_self.$mp && _self.$mp.page.route) + } +}; + +/** + * 获取页面url, 包含参数 + */ +export const get_page_route = (pageVm) => { + // 从 app 进入应用 ,没有 $page ,获取不到路由 ,需要获取页面 尝试从 getCurrentPages 获取也页面实例 + // FIXME 尽量不使用 getCurrentPages ,大部分获取路由是从 onHide 获取 ,这时可以获取到,如果是 onload ,则可能获取不到,比如 百度 + + let page = pageVm.$page || (pageVm.$scope && pageVm.$scope.$page) + let lastPageRoute = uni.getStorageSync('_STAT_LAST_PAGE_ROUTE'); + if (!page) return lastPageRoute || '' + return page.fullPath === '/' ? page.route : page.fullPath +}; + +/** + * 获取页面实例 + */ +export const get_page_vm = () => { + let pages = getCurrentPages() + let $page = pages[pages.length - 1] + if (!$page) return null + return $page.$vm +} + +/** + * 获取页面类型 + */ +export const get_page_types = (self) => { + // XXX 百度有问题 ,获取的都是 componet ,等待修复 + if (self.mpType === 'page' || self.$mpType === 'page' || (self.$mp && self.$mp.mpType === 'page') || self + .$options.mpType === 'page') { + return 'page'; + } + if (self.mpType === 'app' || self.$mpType === 'app' || (self.$mp && self.$mp.mpType === 'app') || self.$options + .mpType === 'app') { + return 'app' + } + return null; +} + +/** + * 处理上报参数 + * @param {Object} 需要处理的数据 + */ +export const handle_data = (statData) => { + let firstArr = [] + let contentArr = [] + let lastArr = [] + for (let i in statData) { + const rd = statData[i] + rd.forEach((elm) => { + const newData = get_splicing(elm) + if (i === 0) { + firstArr.push(newData) + } else if (i === 3) { + lastArr.push(newData) + } else { + contentArr.push(newData) + } + }) + } + + firstArr.push(...contentArr, ...lastArr) + // 参数需要处理成字符串,方便上传 + return JSON.stringify(firstArr) +} + + +/** + * 自定义事件参数校验 + */ +export const calibration = (eventName, options) => { + // login 、 share 、pay_success 、pay_fail 、register 、title + if (!eventName) { + console.error(`uni.report Missing [eventName] parameter`); + return true + } + if (typeof eventName !== 'string') { + console.error(`uni.report [eventName] Parameter type error, it can only be of type String`); + return true + } + if (eventName.length > 255) { + console.error(`uni.report [eventName] Parameter length cannot be greater than 255`); + return true + } + + if (typeof options !== 'string' && typeof options !== 'object') { + console.error('uni.report [options] Parameter type error, Only supports String or Object type'); + return true + } + + if (typeof options === 'string' && options.length > 255) { + console.error(`uni.report [options] Parameter length cannot be greater than 255`); + return true + } + + if (eventName === 'title' && typeof options !== 'string') { + console.error( + `uni.report [eventName] When the parameter is title, the [options] parameter can only be of type String` + ); + return true + } +} + +export const get_page_name = (routepath) => { + return (titleJsons && titleJsons[routepath]) || '' +} + + +const Report_Data_Time = 'Report_Data_Time' +const Report_Status = 'Report_Status' +export const is_report_data = () => { + return new Promise((resolve, reject) => { + let start_time = '' + let end_time = new Date().getTime() + let diff_time = DIFF_TIME + let report_status = 1 + try { + start_time = uni.getStorageSync(Report_Data_Time) + report_status = uni.getStorageSync(Report_Status) + } catch (e) { + start_time = '' + report_status = 1 + } + + if (report_status === '') { + requestData(({ enable }) => { + uni.setStorageSync(Report_Data_Time, end_time) + uni.setStorageSync(Report_Status, enable) + if (enable === 1) { + resolve() + } + }) + return + } + + if (report_status === 1) { + resolve() + } + + if (!start_time) { + uni.setStorageSync(Report_Data_Time, end_time) + start_time = end_time + } + + if (end_time - start_time > diff_time) { + requestData(({ enable }) => { + uni.setStorageSync(Report_Data_Time, end_time) + uni.setStorageSync(Report_Status, enable) + }) + } + }) +} + +const requestData = (done) => { + const appid = process.env.UNI_APP_ID + let formData = { + usv: STAT_VERSION, + conf: JSON.stringify({ + ak: appid, + }), + } + uni.request({ + url: STAT_URL, + method: 'GET', + data: formData, + success: (res) => { + const { data } = res + if (data.ret === 0) { + typeof done === 'function' && + done({ + enable: data.enable, + }) + } + }, + fail: (e) => { + let report_status_code = 1 + try { + report_status_code = uni.getStorageSync(Report_Status) + } catch (e) { + report_status_code = 1 + } + if (report_status_code === '') { + report_status_code = 1 + } + typeof done === 'function' && + done({ + enable: report_status_code, + }) + }, + }) +} diff --git a/packages/uni-stat/src/utils/pageTime.js b/packages/uni-stat/src/utils/pageTime.js new file mode 100644 index 0000000000000000000000000000000000000000..f4183eb8eb6bf4a9eaa56b45cbbdabceae4a4181 --- /dev/null +++ b/packages/uni-stat/src/utils/pageTime.js @@ -0,0 +1,148 @@ +import { + get_platform_name +} from './pageInfo.js' +import { + dbSet, + dbGet, + dbRemove +} from './db.js' +import { + PAGE_PVER_TIME, + APP_PVER_TIME +} from '../config.js'; +// 首次访问时间 +const FIRST_VISIT_TIME_KEY = '__first__visit__time' +// 最后访问时间 +const LAST_VISIT_TIME_KEY = '__last__visit__time' +/** + * 获取当前时间 + */ +export const get_time = () => { + return parseInt(new Date().getTime() / 1000) +} + +/** + * 获取首次访问时间 + */ +export const get_first_visit_time = () => { + const timeStorge = dbGet(FIRST_VISIT_TIME_KEY) + let time = 0 + if (timeStorge) { + time = timeStorge + } else { + time = get_time() + dbSet(FIRST_VISIT_TIME_KEY, time) + // 首次访问需要 将最后访问时间置 0 + dbRemove(LAST_VISIT_TIME_KEY) + } + return time +} + +/** + * 最后访问时间 + */ +export const get_last_visit_time = () => { + const timeStorge = dbGet(LAST_VISIT_TIME_KEY) + let time = 0 + if (timeStorge) { + time = timeStorge + } + dbSet(LAST_VISIT_TIME_KEY, get_time()) + return time +} + +// 页面停留时间记录key +const PAGE_RESIDENCE_TIME = '__page__residence__time' + +/** + * 设置页面停留时间 + */ +export const set_page_residence_time = () => { + let First_Page_Residence_Time = get_time() + dbSet(PAGE_RESIDENCE_TIME, First_Page_Residence_Time) + return First_Page_Residence_Time +} + +/** + * 获取页面停留时间 + */ +export const get_page_residence_time = () => { + let Last_Page_Residence_Time = get_time() + let First_Page_Residence_Time = dbGet(PAGE_RESIDENCE_TIME) + return Last_Page_Residence_Time - First_Page_Residence_Time +} + +/** + * 获取总访问次数 + */ +const TOTAL_VISIT_COUNT = '__total__visit__count' +export const get_total_visit_count = () => { + const timeStorge = dbGet(TOTAL_VISIT_COUNT) + let count = 1 + if (timeStorge) { + count = timeStorge + count++ + } + dbSet(TOTAL_VISIT_COUNT, count) + return count +} + +export const GetEncodeURIComponentOptions = (statData) => { + let data = {} + for (let prop in statData) { + data[prop] = encodeURIComponent(statData[prop]) + } + return data +} + +let Set__First__Time = 0 +let Set__Last__Time = 0 + +/** + * 获取第一次时间 + */ +export const get_first_time = () => { + let time = new Date().getTime() + Set__First__Time = time + Set__Last__Time = 0 + return time +} + +/** + * 获取最后一次时间 + */ +export const get_last_time = () => { + let time = new Date().getTime() + Set__Last__Time = time + return time +} + +/** + * 获取页面 \ 应用停留时间 + */ +export const get_residence_time = (type) => { + let residenceTime = 0 + if (Set__First__Time !== 0) { + residenceTime = Set__Last__Time - Set__First__Time + } + + residenceTime = parseInt(residenceTime / 1000) + residenceTime = residenceTime < 1 ? 1 : residenceTime + if (type === 'app') { + let overtime = residenceTime > APP_PVER_TIME ? true : false + return { + residenceTime, + overtime, + } + } + if (type === 'page') { + let overtime = residenceTime > PAGE_PVER_TIME ? true : false + return { + residenceTime, + overtime, + } + } + return { + residenceTime, + } +} diff --git a/packages/uni-stat/src/utils/util.js b/packages/uni-stat/src/utils/util.js new file mode 100755 index 0000000000000000000000000000000000000000..9e0254efee4fb3d5122e5e333d1562d933bbc6b4 --- /dev/null +++ b/packages/uni-stat/src/utils/util.js @@ -0,0 +1,5 @@ +/** + * 获取系统信息 + */ +export const sys = uni.getSystemInfoSync() +