提交 75371dca 编写于 作者: M mehaotian

feat(stat): 新增统计插件

上级 a14bddd2
......@@ -45,6 +45,17 @@
// observer1.observe({
// entryTypes: ['render', 'navigation'],
// } as PerformanceObserverOptions)
// 统计上报 - 应用启动
uni.report({
name: 'uni-app-launch',
options: res,
success(res_data) {
console.log(res_data);
}, fail(err_data) {
console.log(err_data);
}
})
},
onShow: function (res : OnShowOptions) {
this.globalData.onShowOption = res
......@@ -60,11 +71,31 @@
// 自动化测试
setLifeCycleNum(state.lifeCycleNum + 100)
console.log('App Show')
// 统计上报 - 应用显示
uni.report({
name: 'uni-app-show',
success(res_data) {
console.log(res_data);
}, fail(err_data) {
console.log(err_data);
}
})
},
onHide: function () {
// 自动化测试
setLifeCycleNum(state.lifeCycleNum - 100)
console.log('App Hide')
// 统计上报 - 应用进入后台
uni.report({
name: 'uni-app-hide',
success(res) {
console.log(res);
}, fail(err) {
console.log(err);
}
})
},
// #ifdef APP-ANDROID
onLastPageBackPress: function () {
......@@ -88,6 +119,18 @@
onExit() {
console.log('App Exit')
},
onError(err : any) {
// 统计上报 - 应用发生错误
uni.report({
name: 'uni-app-error',
options: err,
success(res) {
console.log(res);
}, fail(err) {
console.log(err);
}
})
},
// #endif
methods: {
increasetLifeCycleNum() {
......
// 仅测试 console.log 时机问题
import './test-main-console.uts'
import { uniStat } from '@/uni_modules/uni-stat/plugin.uts'
import App from './App.uvue'
import { createSSRApp } from 'vue'
// 统计配置
const collectItems = {
uniStatPageLog: true
}
const statOptions = {
debug: false,
collectItems: collectItems,
}
export function createApp() {
const app = createSSRApp(App)
app.use(uniStat, statOptions)
// app.mixin({
// onReady() {
// setTimeout(() => {
......
{
"id": "uni-stat",
"displayName": "uni-stat",
"version": "1.0.0",
"description": "uni-stat",
"keywords": [
"uni-stat"
],
"repository": "",
"engines": {
"HBuilderX": "^3.6.8"
},
"dcloudext": {
"type": "uts",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "",
"data": "",
"permissions": ""
},
"npmurl": ""
},
"uni_modules": {
"uni-ext-api": {
"uni": {
"report": {
"name": "report",
"app": {
"js": true,
"kotlin": true,
"swift": false
}
}
}
},
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "u",
"aliyun": "u",
"alipay": "u"
},
"client": {
"Vue": {
"vue2": "u",
"vue3": "u"
},
"App": {
"app-android": "u",
"app-ios": "u"
},
"H5-mobile": {
"Safari": "u",
"Android Browser": "u",
"微信浏览器(Android)": "u",
"QQ浏览器(Android)": "u"
},
"H5-pc": {
"Chrome": "u",
"IE": "u",
"Edge": "u",
"Firefox": "u",
"Safari": "u"
},
"小程序": {
"微信": "u",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "u",
"钉钉": "u",
"快手": "u",
"飞书": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}
\ No newline at end of file
import { Stat } from '@/uni_modules/uni-stat'
// // UniStatOptions, UniStatCollectItemsOptions
// 实例统计sdk
const stat_instance = Stat.getInstance()
const lifecycle = defineMixin({
// onLaunch(options : OnLaunchOptions) { stat_instance.onLaunch(options, this) },
// @ts-ignore
onLoad() { stat_instance.onLoad(this) },
// @ts-ignore
onShow() { stat_instance.onShow(this) },
// @ts-ignore
onHide() { stat_instance.onHide(this) },
// @ts-ignore
onUnload() { stat_instance.onUnload(this) },
// onError(error : string) { stat_instance.onError(error) }
})
export const uniStat = definePlugin({
install(app : VueApp, options : UTSJSONObject) {
stat_instance.init(options)
app.mixin(lifecycle)
}
})
# uni-app x 专用的 uni 统计插件
> 注意:当前版本 uni 统计仅支持 uni-app x ,与 uni统计1.0和uni统计2.0 数据不兼容。
## 配置 uni统计后台
与uni统计2.0后台配置一样,只是数据不互通,详情参考 [创建 admin 项目](https://uniapp.dcloud.net.cn/uni-stat-v2.html#%E5%90%8E%E5%8F%B0%E6%8A%A5%E8%A1%A8%E9%85%8D%E7%BD%AE)
## 前端配置
因为当前版本 uni统计是单独的插件,需要在插件市场中下载使用
[下载uni统计](https://ext.dcloud.net.cn/plugin?name=uni-stat)
### 配置说明
**uniStatistics说明**
|字段 | 类型 | 默认值 | 可选值 | 说明 |
|:-: |:-: |:-: |:-: |:-: |
|debug | Boolean |false | true/ false |开启统计调试模式 ,会产生大量日志,且会在开发阶段上报数据,应用发布请关闭此项 |
|reportInterval | Number |10 | - |前端数据上报周期 |
|collectItems |Object |- | - |采集项配置 |
**collectItems 采集项配置说明**
|字段 |类型| 默认值 |可选值 |说明|
|:-:|:-: |:-: |:-: |:-:|
|uniPushClientID| Boolean |false| true/false |是否开启推送PushClientID的采集|
|uniStatPageLog| Boolean |true| true/false |是否开启页面数据采集|
### 如何引用
在 main.uts 中 ,通过vue插件的方式加载 uni统计
```js
import App from './App.uvue'
import { createSSRApp } from 'vue'
// 引入uni统计插件
import { uniStat } from '@/uni_modules/uni-stat/plugin.uts'
// uni统计采集项配置
const collectItems = {
uniStatPageLog: true
}
// uni统计基础配置
const statOptions = {
debug: true,
collectItems: collectItems
}
export function createApp() {
const app = createSSRApp(App)
// 载入uni统计插件 ,use 第二个参数不能为空,如需使用默认配置,请传入 {}
app.use(uniStat, statOptions)
return {
app
}
}
```
### 如何使用
因版本限制,uni统计无法自动采集应用相关的数据,如应用启动、进入后台、进入前台等信息,需要用户手动调用 uni.report() 来实现对应的数据采集。
**使用示例**
```js
<script lang="uts">
export default {
onLaunch: function (options) {
console.log('App Launch')
uni.report({
name: 'uni-app-launch',
options: options,
success(res) {
console.log(res);
}, fail(err) {
console.log(err);
}
})
},
onShow: function () {
console.log('App Show')
uni.report({
name: 'uni-app-show',
success(res) {
console.log(res);
}, fail(err) {
console.log(err);
}
})
},
onHide: function () {
console.log('App Hide')
uni.report({
name: 'uni-app-hide',
success(res) {
console.log(res);
}, fail(err) {
console.log(err);
}
})
},
onError(err : any) {
console.log('错误');
uni.report({
name: 'uni-app-error',
options: err,
success(res) {
console.log(res);
}, fail(err) {
console.log(err);
}
})
}
}
</script>
```
### 影响范围
`uni-app-launch` 是整个统计的入口,会采集应用启动的相关数据会影响的统计数据为:
- 日活
- 新增
- 总设备数
`uni-app-show` 是应用从后台进入前台调用,会影响:
- 日活
`uni-app-hide` 是应用从前台进入后台调用,会影响:
- 页面访问次数
### uni.report(OBJECT) 说明
**OBJECT参数说明**
|参数名 |类型 |必填 |说明 |
|:-: |:-: |:-: |:-: |
|name | String | 是 |事件名称 ,内置名称见下方 **name 内置事件说明** |
|options | Any | 否 |事件自定义参数 |
|success | Function| 否 |接口调用成功的回调函数 |
|fail |Function | 否 |接口调用失败的回调函数 |
|complete | Function| 否 |接口调用结束的回调函数(调用成功、失败都会执行) |
**name 内置事件说明**
|事件名 |调用声明周期 |事件说明 |
|:-: |:-: |:-: |
|uni-app-launch | App.onLaunch| 应用启动 |
|uni-app-show | App.onShow | 应用进入前台 |
|uni-app-hide | App.onHide | 应用进入后台 |
|uni-app-error | App.onError | 应用发生错误 |
|title | any | 内容统计采集标题 |
**如何使用自定义上报**
```js
// 参数支持字符串
uni.report({
name:'购买',
options:'购买成功'
})
// 参数支持对象
uni.report({
name:'购买',
options:{
id:'1000',
name:'上衣',
price:'998',
msg:'购买成功'
// ...
}
})
```
export * from '../index.uts'
\ No newline at end of file
import { UniStatOptions, UniStatCollectItemsOptions } from '../interface.uts'
// 访问开始即启动小程序,访问结束结分为:进入后台超过5min、在前台无任何操作超过30min、在新的来源打开小程序;
export const sys = uni.getSystemInfoSync()
export const sysAppBase = uni.getAppBaseInfo();
export const STAT_VERSION = sys.uniCompilerVersion
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
class ConfigData {
static __config_instance : ConfigData | null = null;
public static getInstance() : ConfigData {
if (ConfigData.__config_instance == null) {
ConfigData.__config_instance = new ConfigData()
}
return ConfigData.__config_instance as ConfigData
}
private options : UniStatOptions | null = null
private constructor() {
// 私有构造函数,防止通过 new Singleton() 创建新实例
}
setOptions(options : UniStatOptions | null) {
// 处理参数默认值
// 判断是否存在 options
if (options == null) {
options = {} as UniStatOptions
}
// 是否开启debug,默认false
if (options.debug == null) {
options.debug = false
}
// 上报周期,默认 10s
if (options.reportInterval == null) {
options.reportInterval = 10
}
// 采集项配置
if (options.collectItems == null) {
options.collectItems = {} as UniStatCollectItemsOptions
}
let collectItems = options.collectItems as UniStatCollectItemsOptions
// 是否开启推送,默认为false
if (collectItems.uniPushClientID === null) {
collectItems.uniPushClientID = false
}
// 是否开启页面采集,默认为 true
if (collectItems.uniStatPageLog === null) {
collectItems.uniStatPageLog = true
}
options.collectItems = collectItems
this.options = options
}
getOptions() : UniStatOptions {
// 如果没有参数,获取参数时设置为默认值
if (this.options == null) {
this.setOptions(null)
}
return this.options as UniStatOptions
}
}
export const Config = ConfigData.getInstance()
\ No newline at end of file
import { OnLaunchOptionsWithCst, StatDefault, PageParams, TitleConfigParams, RequestData, EventParams } from '../../interface.uts'
import { get_residence_time, get_time, set_first_time, get_last_visit_time, get_first_visit_time, get_total_visit_count, get_page_residence_time, set_page_residence_time } from '../utils/pageTime.uts'
import { get_default_data, get_route, get_page_name, get_odid, is_handle_device, get_scene, get_report_Interval } from '../utils/pageInfo.uts'
import { ReportType, ReprotCstType } from './stat-type.uts'
import { dbGet, dbSet, dbRemove } from '../utils/db.uts'
import { Log, Map2String, Map2Json, handle_data } from '../utils/index.uts'
import { OPERATING_TIME, STAT_VERSION, Config } from '../config.uts'
// import Config from '../config'
// #ifdef APP-ANDROID
class CloudObjectToDo extends InternalUniCloudCloudObjectCaller {
constructor(obj : InternalUniCloudCloudObject) {
super(obj)
}
report(...do_not_transform_spread : Array<any | null>) : Promise<UTSJSONObject> {
return this._obj.callMethod('report', this._getArgs(...do_not_transform_spread))
}
}
// #endif
export class Report {
/* uniCloud 实例 */
// @ts-ignore
static uniCloudInstance : UniCloud | null = null
/**进入应用标识*/
licationShow : boolean = false
/**离开应用标识*/
licationHide : boolean = false
/** 上报数据 */
statData : StatDefault
// 上报间隔时长,默认值 是10
eportInterval = get_report_Interval(OPERATING_TIME)
_navigationBarTitle : TitleConfigParams = {
config: '',
page: '',
report: '',
lt: ''
} as TitleConfigParams
constructor() {
this.statData = get_default_data()
// 注册拦截器
// @ts-ignore
let registerInterceptor = typeof uni.addInterceptor === 'function'
if (registerInterceptor) {
this.interceptSetNavBar()
this.interceptLogin()
this.interceptShare(true)
this.interceptRequestPayment()
}
}
/**
* 拦截 setNavigationBarTitle 事件
*/
interceptSetNavBar() {
let self = this
// @ts-ignore
uni.addInterceptor('setNavigationBarTitle', {
// @ts-ignore
invoke(args : SetNavigationBarTitleOptions) {
self._navigationBarTitle.page = args.title
},
// @ts-ignore
} as Interceptor)
}
/**
* 拦截 login 事件
*/
interceptLogin() {
let self = this
// @ts-ignore
uni.addInterceptor('login', {
complete() {
self._login()
},
// @ts-ignore
} as Interceptor)
}
interceptShare(type : boolean = false) {
let self = this
if (!type) {
self._share()
return
}
// @ts-ignore
uni.addInterceptor('share', {
complete() {
self._share()
}
// @ts-ignore
} as Interceptor)
}
interceptRequestPayment() {
let self = this
// @ts-ignore
uni.addInterceptor('requestPayment', {
success() {
self._payment('pay_success')
},
fail() {
self._payment('pay_fail')
},
// @ts-ignore
} as Interceptor)
}
/**
* 实现登录拦截逻辑
*/
private _login() {
this.sendEventRequest(
{
key: 'login',
} as EventParams
)
}
private _share() {
this.sendEventRequest(
{
key: 'share',
} as EventParams
)
}
private _payment(key : string) {
this.sendEventRequest(
{
key,
} as EventParams
)
}
/**
* 进入应用
* @param {any} options 应用参数
*/
launch(options : any) {
// 重置页面停留时长
set_page_residence_time()
// 告诉report是应用进来的
this.licationShow = true
// 存储应用传入值
dbSet('__launch_options', options)
// 应用初始上报参数为1
// TODO 这里应该是 把 options 都结构到下面对象中,目前unix只有path ,所以这段代码后续需要改一下
// let request_option : OnLaunchOptionsWithCst = {
// ...options,
// cst: ReprotCstType.AppNormal
// }
let request_option : OnLaunchOptionsWithCst = {
// @ts-ignore
path: (options as OnLaunchOptions).path,
cst: ReprotCstType.AppNormal
}
// 上报数据
this.sendAppRequest(request_option, true)
}
/**
* 进入应用
* @description 上报主要数据采集是从onShow
*/
load() {
// 记录当前页面实例
// this.appInstance = appInstance
}
/**
* 进入应用或应用从后台进入前台
*/
appShow() {
// 通过 licationHide 判断,保证是进入后台后在次进入应用,避免重复上报数据
// 当前方法在应用声明周期内,只会触发一次,即首次进入应用触发
if (!this.licationHide) return
const time = get_residence_time('app')
// 需要判断进入后台是否超过时限 ,默认是 5min ,是的话需要执行进入应用的上报
if (time.overtime) {
let lastPageRoute = uni.getStorageSync('_STAT_LAST_PAGE_ROUTE') as string
let options : OnLaunchOptionsWithCst = {
path: lastPageRoute,
scene: this.statData.sc,
cst: ReprotCstType.AppAwakenTimeout,
}
this.sendAppRequest(options)
}
// 状态重置
this.licationHide = false
}
/**
* 引用进入后台触发
* @param {Boolean} type 是否立即触发上报
*/
appHide(type : boolean = false) {
// 进入应用后台保存状态,方便进入前台后判断是否上报应用数据
this.licationHide = true
const time = get_residence_time('outher')
const route = get_route().fullpath
uni.setStorageSync('_STAT_LAST_PAGE_ROUTE', route)
const formdata : PageParams = {
url: route,
urlref: route,
urlref_ts: time.residenceTime,
// urlref_tt: ''
}
this.sendHideRequest(formdata, type)
// 更新页面首次访问时间
set_first_time()
}
/**
* 进入页面触发
* @param {Page } self 当前页面实例
*/
// @ts-ignore
pageShow(self : Page) {
// 初始化,标题数据。清空值,避免污染后面的上报数据
this._navigationBarTitle = { config: '', page: '', report: '', lt: '' } as TitleConfigParams
// 获取页面路径
const { path, fullpath } = get_route(self)
// 获取页面标题
const pageTitle = get_page_name(path)
this._navigationBarTitle.config = pageTitle
// 表示应用触发 ,页面切换不触发之后的逻辑
if (this.licationShow) {
// 更新页面首次访问时间
set_first_time()
uni.setStorageSync('_STAT_LAST_PAGE_ROUTE', fullpath)
this.licationShow = false
return
}
// 页面进入
const time = get_residence_time('page')
// 停留时间,判断是否长时间停留在当前页面,如果超过30min重新上报数据
if (time.overtime) {
let options : OnLaunchOptionsWithCst = {
path: fullpath,
scene: this.statData.sc,
cst: ReprotCstType.PageStayTimeout,
}
this.sendAppRequest(options)
}
// 更新页面首次访问时间
set_first_time()
}
/**
* 离开页面触发
* @param {ComponentPublicInstance } self 当前页面实例
*/
// @ts-ignore
pageHide(self : Page) {
// 如果进入后台则不触发后续 页面隐藏的逻辑
if (this.licationHide) return
const time = get_residence_time('page')
let fullpath = get_route(self).fullpath
let lastPageRoute = uni.getStorageSync('_STAT_LAST_PAGE_ROUTE')
// 如果不存在的话,复制当前的路径,说明之前没有页面
if (lastPageRoute == null) {
lastPageRoute = fullpath
}
uni.setStorageSync('_STAT_LAST_PAGE_ROUTE', fullpath)
const options : PageParams = {
url: fullpath,
urlref: lastPageRoute as string,
urlref_ts: time.residenceTime,
urlref_tt: ''
}
this.sendPageRequest(options)
}
appError(em : any) {
console.log('发生了错误123');
console.log(em);
let errmsg : string
// #ifdef APP-ANDROID
// @ts-ignore
if (em instanceof Exception) {
errmsg = em.message ?? ''
} else if (em instanceof Error) {
errmsg = em.message
} else {
errmsg = JSON.stringify(em)
}
// #endif
// #ifndef APP-ANDROID
// 处理h5
errmsg = em.message ?? ''
// #endif
let route = get_route().path
let options : StatDefault = {
ak: this.statData.ak,
uuid: this.statData.uuid,
p: this.statData.p,
lt: '31',
url: route,
ut: this.statData.ut,
ch: this.statData.ch,
mpsdk: this.statData.mpsdk,
mpv: this.statData.mpv,
v: this.statData.v,
em: errmsg,
usv: this.statData.usv,
t: get_time(),
}
this.request(options, false)
}
/**
* 发送请求,应用维度上报
* @param {Object} options 页面信息
* @param {Boolean} type 是否立即上报
*/
sendAppRequest(options : OnLaunchOptionsWithCst, type : boolean = false) {
let is_opt = options.query != null && JSON.stringify(options.query) != '{}'
// TODO ,此处处理的值有问题,
// 如果页面有参数 需要上传如:/xxx/xxx?{xx:xx,xxx:xxx} 格式的值
let query = is_opt ? '?' + JSON.stringify(options.query) : ''
// 获取应用最后访问时间
const last_time = get_last_visit_time()
// 非老用户
if (last_time != 0) {
const odid = get_odid()
// TODO 老版本的odid兼容问题,看看是否需要保留
const have_device = is_handle_device()
// 如果没有上报过设备信息 ,则需要上报设备信息
if (!have_device) {
this.statData.odid = odid
}
}
this.statData.lt = ReportType.AppStart
this.statData.url = options.path + query
this.statData.t = get_time()
this.statData.sc = get_scene(options)
this.statData.fvts = get_first_visit_time()
this.statData.lvts = last_time
this.statData.tvc = get_total_visit_count()
this.statData.cst = options.cst ?? ReprotCstType.AppNormal
// TODO 获取应用版本,网络信息和地址信息
// if (get_platform_name() === 'n') {
// this.getProperty(type)
// } else {
// this.getNetworkInfo(type)
// }
this.request(this.statData, type)
}
/**
* 发送请求,页面维度上报
* @param {Object} opt
*/
sendPageRequest(opt : PageParams) {
let { url, urlref, urlref_ts } = opt
this._navigationBarTitle.lt = ReportType.PageShow
let options : StatDefault = {
ak: this.statData.ak,
uuid: this.statData.uuid,
p: this.statData.p,
lt: ReportType.PageShow,
ut: this.statData.ut,
url,
// TODO 好像是没有这个参数,看一下具体描述
tt: this.statData.tt,
urlref,
urlref_ts,
ch: this.statData.ch,
usv: this.statData.usv,
t: get_time(),
}
this.request(options)
}
/**
* 进入后台上报数据
* @param {Object} opt 页面进入应用参数
* @param {Boolean} type 是否立即上报数据
*/
sendHideRequest(opt : PageParams, type : boolean) {
let { urlref, urlref_ts } = opt
let options : StatDefault = {
ak: this.statData.ak,
uuid: this.statData.uuid,
p: this.statData.p,
lt: ReportType.AppHide,
ut: this.statData.ut,
urlref: urlref,
urlref_ts: urlref_ts,
ch: this.statData.ch,
usv: this.statData.usv,
t: get_time(),
}
this.request(options, type)
}
/**
* 推送数据上报
* @param {any} options
* @param {string} cid
*/
sendPushRequest(options : any, cid : string) {
let time = get_time()
const statData : StatDefault = {
uuid: this.statData.uuid,
p: this.statData.p,
ak: this.statData.ak,
lt: ReportType.Push,
cid: cid,
t: time,
ut: this.statData.ut,
}
const dataStr = JSON.stringify(statData)
// #ifdef WEB
// @ts-ignore
let uniStatData = JSON.parse(dataStr) as Map<string, StatDefault[]>
// @ts-ignore
uniStatData = new Map(Object.entries(uniStatData))
// #endif
// #ifndef WEB
// @ts-ignore
let uniStatData = JSON.parse<Map<string, StatDefault[]>>(dataStr)
if (uniStatData == null) {
uniStatData = new Map([])
}
// #endif
const hasKey = uniStatData.has(ReportType.Push)
if (!hasKey) {
uniStatData.set(ReportType.Push, [])
}
// 将消息加入对列
uniStatData.get(ReportType.Push)!.push(statData)
const stat_data = handle_data(uniStatData)
let optionsData : RequestData = {
usv: STAT_VERSION, //统计 SDK 版本号
t: time, //发送请求时的时间戮
requests: stat_data,
}
this.sendRequest(optionsData)
}
/**
* 自定义事件上报
* @param {EventParams} data 事件参数
*/
sendEventRequest(data : EventParams) {
const key = data.key
const value = data.value ?? ''
let routepath = get_route().fullpath
const lt = ReportType.Event
this._navigationBarTitle.config = get_page_name(routepath)
this._navigationBarTitle.lt = lt
let options : StatDefault = {
ak: this.statData.ak,
uuid: this.statData.uuid,
p: this.statData.p,
lt: lt,
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(),
}
this.request(options)
}
/**
* 发送请求
* @param {StatDefault} data 上报数据
* @param {Object} type 类型
*/
request(data : StatDefault, type : boolean = false) {
const statConfig = Config.getOptions()
let time = get_time()
// 需要把标题插入到数据中
const title = this._navigationBarTitle
data.ttn = title.page
data.ttpj = title.config
data.ttc = title.report
if (statConfig.debug!) {
// 打印输出日志
Log(data)
}
const lt = data.lt!
// 获取消息对列
let statData = dbGet<string>('__UNI__STAT__DATA')
if (statData == null) {
statData = '{}'
}
let uniStatData = JSON.parse<Map<string, StatDefault[]>>(statData)
if (uniStatData == null) {
uniStatData = new Map([])
}
const hasKey = uniStatData.has(lt)
if (!hasKey) {
uniStatData.set(lt, [])
}
// 将消息加入对列
uniStatData.get(lt)!.push(data)
// 将 map 转换为 str
const statDataStr = Map2String(uniStatData)
// 存储
dbSet('__UNI__STAT__DATA', statDataStr)
let page_residence_time = get_page_residence_time()
// 判断时候到达上报时间 ,默认 10 秒上报
const is_timeout = page_residence_time < this.eportInterval && !type
if (is_timeout) return
// 时间超过,重新获取时间戳
set_page_residence_time()
if (statConfig.debug!) {
// 待上传消息对列
console.log(`=== 统计待上传队列数据 ===`)
const statJSON = Map2Json(uniStatData)
console.log(statJSON)
console.log(`=== 采集结束 ===`)
}
const stat_data = handle_data(uniStatData)
let optionsData : RequestData = {
usv: STAT_VERSION, //统计 SDK 版本号
t: time, //发送请求时的时间戮
requests: stat_data,
}
// 重置队列
dbRemove('__UNI__STAT__DATA')
this.sendRequest(optionsData)
}
sendRequest(options : RequestData) {
const statConfig = Config.getOptions()
if (Report.uniCloudInstance != null) {
// 数据上报
const app = Report.uniCloudInstance!
// #ifdef APP-ANDROID
// @ts-ignore
const uniCloudObj = app.importObject('uni-stat-receiver', {
customUI: true,
// @ts-ignore
} as UniCloudImportObjectOptions, UTSAndroid.getJavaClass(CloudObjectToDo))
// #endif
// #ifndef APP-ANDROID
// @ts-ignore
const uniCloudObj = app.importObject(
'uni-stat-receiver',
{
customUI: true,
} as UniCloudImportObjectOptions
)
// #endif
uniCloudObj
.report(options)
.then(() => {
if (statConfig.debug!) {
console.log(`=== 统计队列数据上报 ===`)
console.log(options)
console.log(`=== 上报结束 ===`)
}
})
.catch((err : any | null) => {
console.log('=== 统计上报错误 ===')
console.error(err)
console.log(`=== 上报结束 ===`)
})
}
}
}
/**
* lifecycle 生命周期类型
*/
export class StatType {
/**应用onLauch*/
static LifeCycleLaunch : number = 1
/**页面onLoad*/
static LifeCycleLoad : number = 2
/**应用 onShow */
static LifeCycleAppShow : number = 3
/**应用 onHide */
static LifeCycleAppHide : number = 4
/**页面 onShow*/
static LifeCyclePageShow : number = 5
/**页面 onHide */
static LifeCyclePageHide : number = 6
/**页面卸载*/
static LifeCyclePageUnLoad : number = 7
/**错误*/
static LifeCycleError : number = 8
}
/**
* lt 统计数据类型
*/
export class ReportType {
/** 1: 应用启动,对应 `onLaunch` 事件 */
static AppStart : string = "1"
/** 2: 应用显示,对应 `onShow` 事件 */
static AppShow : string = "2"
/** 3: 应用进入后台,对应 `onHide` 事件 */
static AppHide : string = "3"
/** 4: 应用退出 */
static AppExit : string = "4"
/** 11: 页面跳转,对应 `onShow` 事件 */
static PageShow : string = "11"
/** 12: 页面关闭,对应 `onHide` 事件 */
static PageHide : string = "12"
/** 21: 事件 */
static Event : string = "21"
/** 31: 错误信息 */
static Error : string = "31"
/** 101: 推送 */
static Push : string = "101"
}
/**
* 平台类型
*/
export class PlatformType {
// TODO 平台需要整理在这里
}
/**
* cst 上报时机类型
*/
export class ReprotCstType {
/** 1: 正常进入上报 */
static AppNormal : number = 1
/** 2: 后台进前台超时上报 */
static AppAwakenTimeout : number = 2
/** 3: 页面停留超时上报 */
static PageStayTimeout : number = 3
}
// 应用首次启动需要保留的keys
export const AppShowParamsKeys = ["uuid", "ak", "lt", "ut", "mpsdk", "mpv", "mpn", "v", "p", "sv", "net", "brand", "md", "lang", "lat", "lng", "pr", "ww", "wh", "sw", "sh", "url", "tt", "ch", "fvts", "lvts", "cn", "pn", "ct", "sc", "tvc", "usv", "t", "odid", "cst"]
// 应用进入后台需要保留的keys
export const AppHideParamsKeys = ['ak', 'uuid','ttn','ttpj','ttc', 'lt', 'ut', 'p', 'urlref', 'urlref_ts', 'ch', 'usv', 't']
// 页面切换需要保留的keyss
export const PageShowParamsKeys = ['ak', 'uuid', 'lt', 'ut', 'p', 'url', 'ttpj', 'ttn', 'ttc', 'ttct', 'urlref', 'urlref_ts', 'ch', 'usv', 't']
// push 需要保留的keys
export const PushParamsKeys = ['lt', 'cid', 't', 'ut']
// 事件需要保留的keys
export const EventParamsKeys = ['ak', 'uuid', 'p', 'lt', 'ut', 'url', 'ch', 'e_n', 'e_v', 'usv', 't']
// 错误事件需要保留的keys
export const ErrorParamsKeys = ["ak", "uuid", "p", "lt", "url", "ut", "ch", "mpsdk", "mpv", "v", "em", "usv", "t"]
\ No newline at end of file
import { Report } from "./report.uts";
import { StatType } from "./stat-type";
import { EventParams, UniStatOptions, ErrorCallback } from '../../interface.uts'
import { is_page, is_page_report, get_space, is_push_clientid, calibration } from '../utils/pageInfo.uts'
import { Config } from "../config";
export class Stat {
static __stat_instance : Stat | null = null;
static is_register : boolean = false
// 上报逻辑实例
report : Report;
// 使用单例,只初始化一次
static getInstance() : Stat {
// 获取服务空间配置信息
let space = get_space(uniCloud.config)
if (Report.uniCloudInstance == null) {
// 判断不为空对象
if (space != null) {
// 重新构造 uniCloud
let spaceData : UniCloudInitOptions = {
provider: space.provider,
spaceId: space.spaceId,
clientSecret: space.clientSecret,
}
const endpoint = space.endpoint
if (endpoint != null) {
spaceData.endpoint = space.endpoint
}
// 支付宝单独处理一些参数
if (space.provider == 'alipay') {
spaceData.secretKey = space.secretKey
spaceData.accessKey = space.accessKey
spaceData.spaceAppId = space.spaceAppId
}
// 初始化 uniCloud
// @ts-ignore
Report.uniCloudInstance = uniCloud.init(spaceData)
} else {
console.error('应用已集成uni统计,但未关联服务空间,请在uniCloud目录右键关联服务空间')
}
}
// 实例化统计sdk ,要在 实例 unicloud 之后进行,避免 Report 无法拿到 uniCloud 实例
if (this.__stat_instance == null) {
this.__stat_instance = new Stat()
}
return this.__stat_instance as Stat
}
// 当前生命周期内的页面或应用实例
appInstance ?: Page | null = null
private isHide : boolean = false
constructor() {
this.report = new Report()
}
/**
* 初始化插件参数
* @param {Object} options
*/
init(options : UTSJSONObject) {
// 插件挂载玩成,可以进行后续操作
Stat.is_register = true
// 参数处理
Config.setOptions({ ...options } as UniStatOptions)
const uniStatConfig = Config.getOptions()
// 设置上报周期时间
this.report.eportInterval = uniStatConfig.reportInterval ?? 10
}
/**
* 应用启动
* @param {OnLaunchOptions} options 应用参数
* @param {ComponentPublicInstance} appInstance 应用实例
*/
// options : OnLaunchOptions, appInstance : ComponentPublicInstance
onLaunch(options : OnLaunchOptions, appInstance : ComponentPublicInstance) {
// 注册事件 onLaunch ,需要手动触发
// this.registerEvent(StatType.LifeCycleLaunch, appInstance, options as any)
}
/**
* 页面加载
* @param {ComponentPublicInstance} appInstance 应用实例
*/
onLoad(appInstance : Page) {
this.registerEvent(StatType.LifeCycleLoad, appInstance)
}
/**
* 显示页面或应用进入前台
* @param {ComponentPublicInstance} appInstance 应用实例
*/
onShow(appInstance : Page) {
this.isHide = false
// @ts-ignore
const mptype = is_page(appInstance)
// 页面执行,应用需要手动调用
if (mptype) {
this.registerEvent(StatType.LifeCyclePageShow, appInstance, null)
}
// const life_type = mptype == 'app' ? StatType.LifeCycleAppShow : StatType.LifeCyclePageShow
// this.registerEvent(life_type, appInstance, null)
}
/**
* 页面隐藏或应用进入后台
* @param {ComponentPublicInstance} appInstance 应用实例
*/
onHide(appInstance : Page) {
this.isHide = true
// @ts-ignore
const mptype = is_page(appInstance)
// 页面执行,应用需要手动调用
if (mptype) {
this.registerEvent(StatType.LifeCyclePageHide, appInstance, null)
}
// const life_type = mptype == 'app' ? StatType.LifeCycleAppHide : StatType.LifeCyclePageHide
// this.registerEvent(life_type, appInstance, null)
}
/**
* 卸载页面
* @param {ComponentPublicInstance} appInstance 应用实例
*/
onUnload(appInstance : Page) {
// 如果 isHide 为true 说明页面隐藏了,不走卸载逻辑,如果走卸载逻辑,isHide 必不可能是true
if (this.isHide) {
this.isHide = false
return
}
this.registerEvent(StatType.LifeCyclePageUnLoad, appInstance, null)
}
/**
* 错误
* @param {String} error 应用实例
*/
onError(error : string) {
// 单独处理错误上报
this.error(error)
}
/**
* 获取推送ID
*/
pushEvent(options : any) {
// TODO 具体实现
const ClientID = is_push_clientid()
if (ClientID) {
uni.getPushClientId({
success: (res) => {
const cid = res.cid
// 只有获取到才会上传
// if (cid != null) {
this.report.sendPushRequest(options, cid)
// }
},
} as GetPushClientIdOptions)
}
}
/**
* 注册事件
* @param {number} EventType 事件类型
* @param {Page} appInstance 当前页面实例
* @param {UTSJSONObject} options 应用参数
*/
registerEvent(EventType : number, appInstance : Page | null, options : any | null = null, error : any | null = '') {
this.appInstance = appInstance
// 是否要上报页面数据
const isPageReport = is_page_report()
switch (EventType) {
case StatType.LifeCycleLaunch:
// 使用非空断言,options在这里肯定非空
this.report.launch(options!)
this.pushEvent(options)
break
case StatType.LifeCycleAppShow:
// TODO 目前只兼容 web 和 app ,小程序等平台需要调用 api onAppHide
this.report.appShow()
break
case StatType.LifeCycleAppHide:
this.report.appHide(true)
break
case StatType.LifeCycleLoad:
break
case StatType.LifeCyclePageShow:
if (isPageReport) {
this.report.pageShow(appInstance!)
}
break
case StatType.LifeCyclePageHide:
if (isPageReport) {
this.report.pageHide(appInstance!)
}
break
case StatType.LifeCyclePageUnLoad:
if (isPageReport) {
this.report.pageHide(appInstance!)
}
break
case StatType.LifeCycleError:
if (error != null) {
this.report.appError(error)
}
break
}
}
error(em : string) {
// 生命周期监听,暂时无用,需要手动调用api
}
// 自定义参数上报
// fn : ErrorCallback
appEvent(name : string, options : any | null = null, fn : ErrorCallback) {
if (!Stat.is_register) {
fn(false, '统计服务尚未初始化,请在main.uts中引入统计插件。')
return
}
// const names = ['uni-app-launch', 'uni-app-show', 'uni-app-hide', 'uni-app-error']
// if (names.indexOf(name) <= -1) {
// // console.error('uniStatReport 事件名不存在,请检查!');
// fn(false, 'uniStatReport 事件名不存在,请检查!')
// return
// }
if (name == 'uni-app-launch' && options == null) {
fn(false, 'uniStatReport options参数错误,请检查!')
return
}
fn(true, 'report:ok')
if (name == 'uni-app-launch') {
// StatType.LifeCycleLaunch, null, options as OnLaunchOptions
this.registerEvent(StatType.LifeCycleLaunch, null, options)
return
}
if (name == 'uni-app-show') {
this.registerEvent(StatType.LifeCycleAppShow, null, null)
return
}
if (name == 'uni-app-hide') {
this.registerEvent(StatType.LifeCycleAppHide, null, null)
return
}
if (name == 'uni-app-error') {
this.registerEvent(StatType.LifeCycleError, null, null, options)
return
}
// 校验 type 参数
const is_calibration = calibration(name, options)
if (is_calibration) {
return
}
if (name === 'title') {
this.report._navigationBarTitle.report = (options as string)
return
}
const value = (typeof options === 'object' ? JSON.stringify(options) : options) as string
const data : EventParams = {
key: name,
value: value as string,
}
this.report.sendEventRequest(data)
}
}
// export { Stat } from './core/stat.uts'
const APPID = uni.getSystemInfoSync().appId
export function dbSet<T>(name : string, value : T) {
let data = uni.getStorageSync('$$STAT__DBDATA:' + APPID)
let newData = {} as UTSJSONObject
if (data != null && typeof data == 'object') {
newData = data as UTSJSONObject
}
newData[name] = value
uni.setStorageSync('$$STAT__DBDATA:' + APPID, newData)
}
export function dbGet<T>(name : string) : T | null {
let data = uni.getStorageSync('$$STAT__DBDATA:' + APPID)
if (data == null) return null
if (typeof data != 'object') return null
const newData = data as UTSJSONObject
return newData[name] as T
}
export function dbRemove(name : string) {
let data = uni.getStorageSync('$$STAT__DBDATA:' + APPID)
if(data == '') {
data = {}
}
if (data != null) {
let newData = data as UTSJSONObject
if (newData[name] != null) {
newData[name] = null
uni.setStorageSync('$$STAT__DBDATA:' + APPID, newData)
}
}
}
\ No newline at end of file
// AppShowReportParams, AppHideReportParams, PageReportParams,
import { StatDefault } from '../../interface.uts'
import { AppShowParamsKeys, AppHideParamsKeys, PageShowParamsKeys, PushParamsKeys, EventParamsKeys,ErrorParamsKeys } from '../core/stat-type.uts'
/**
* 序列化url参数
* @param {Object} obj
* @example
*/
export function Serialize(obj : UTSJSONObject) : string {
let str : string[] = [];
for (let p in obj) {
if (obj.hasOwnProperty(p)) {
let key = obj[p]
if (obj[p] == null) {
key = ''
}
const text = p + "=" + key
str.push(text);
}
}
return "?" + str.join("&");
}
export function IsNumber(value : any | null) : boolean {
return typeof value === 'number';
// if (value.trim() === "") {
// return false;
// }
// const num = parseInt(value);
// if (isNaN(num)) {
// return false;
// }
// return true;
}
/**
* 上报参数过滤
*/
export function FilterParam<T>(keys : string[] = [], data : StatDefault) : T {
let result = {};
keys.forEach(key => {
result[key] = data[key];
});
let formdata : T = result as T
return formdata;
}
/**
* 日志输出
* @param {StatDefault} data 统计数据
* @param {Boolean} type 打印类型,如果是type=true ,则是最终上报数据
*/
export function Log(data : StatDefault, type : Boolean = false) {
let logData = getEventData(data.lt!, data)
let msg_type : string = ''
switch (data.lt) {
case '1':
msg_type = '应用启动'
break
case '3':
msg_type = '应用进入后台'
break
case '11':
msg_type = '页面切换'
break
case '21':
msg_type = '事件触发'
break
case '31':
msg_type = '应用错误'
break
case '101':
msg_type = 'PUSH'
break
}
if (type) {
console.log(`=== 统计队列数据上报 ===`)
console.log(logData)
console.log(`=== 上报结束 ===`)
return
}
if (msg_type != '') {
console.log(`=== 统计数据采集:${msg_type} ===`)
console.log(logData)
console.log(`=== 采集结束 ===`)
}
}
/**
* map 转 string
*/
export function Map2String(statData : Map<string, StatDefault[]>) : string {
let statDataJson = {}
statData.forEach((rd : StatDefault[], key : string) => {
statDataJson[key] = rd
});
return JSON.stringify(statDataJson)
}
export function Map2Json(statData : Map<string, StatDefault[]>) : UTSJSONObject {
let statDataJson = {}
statData.forEach((rd : StatDefault[], key : string) => {
let arr : object[] = [];
rd.forEach((elm : StatDefault) => {
let data = getEventData(key, elm)
arr.push(data)
})
statDataJson[key] = arr
});
return statDataJson
}
/**
* 处理上报参数
* @param {Object} statData 需要处理的数据
*/
export const handle_data = (statData : Map<string, StatDefault[]>) : string => {
let firstArr : any[] = []
let contentArr : any[] = []
let lastArr : any[] = []
statData.forEach((rd : StatDefault[], key : string) => {
rd.forEach((elm : StatDefault) => {
let data = getEventData(key, elm)
if (key == '1') {
firstArr.push(data)
} else if (key == '4') {
lastArr.push(data)
} else {
contentArr.push(data)
}
})
});
firstArr.push(...contentArr, ...lastArr)
return JSON.stringify(firstArr)
}
/**
* 获取具体的上报参数对象
*/
function getEventData(lt : string, elm : StatDefault) : any {
let data : any = {}
switch (lt) {
case '1'://应用启动
data = FilterParam(AppShowParamsKeys, elm)
break
case '3': // 应用进入后台
data = FilterParam(AppHideParamsKeys, elm)
break
case '11': //页面切换
data = FilterParam(PageShowParamsKeys, elm)
break
case '21': // 事件触发
data = FilterParam(EventParamsKeys, elm)
break
case '31': // 应用错误
data = FilterParam(ErrorParamsKeys, elm)
break
case '101': // PUSH
data = FilterParam(PushParamsKeys, elm)
break
}
return data
}
\ No newline at end of file
import { OnLaunchOptionsWithCst, RouteParams, StatDefault } from '../../interface.uts'
import { STAT_VERSION, sys, sysAppBase, Config } from '../config.uts'
// import Config from '../config.uts'
import { get_time } from './pageTime.uts'
import { dbGet, dbSet } from './db.uts'
import { Serialize, IsNumber } from './index.uts'
const APPID = sys.appId
const RUNTIME_VERSION = sys.appVersion
// const UUID_KEY = '__DC_STAT_UUID'
// const UUID_VALUE = '__DC_UUID_VALUE'
/**
* 生成uuid,一般用不到
*/
// function getUuid() : string {
// let uuid : string
// if (get_platform_name() === 'n') {
// try {
// uuid = sys.deviceId
// } catch (e) {
// uuid = ''
// }
// return uuid
// }
// try {
// uuid = uni.getStorageSync(UUID_KEY) as string
// } 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
// }
/**
* 获取uuid
*/
export const get_uuid = () : string => {
// 有可能不存在 deviceId(一般不存在就是出bug了),就自己生成一个
// 目前 deviceId 肯定存在所以不用生成
// const uuid : string = sys.deviceId ?? getUuid()
return sys.deviceId
}
/**
* 获取老版的 deviceid ,兼容以前的错误 deviceid
*/
export const get_odid = () : string => {
// let odid : string
// if (get_platform_name() === 'n') {
// try {
// odid = plus.device.uuid
// } catch (e) {
// odid = ''
// }
// return odid
// }
// odid = sys.deviceId ?? getUuid()
// TODO 需要获取老版本的uuid ,目前无法获取,只返回 deviceId
return sys.deviceId
}
/**
* 获取当前平台
* 移动端 : 'n',
* h5 : 'h5',
* 微信 : 'wx',
* 阿里 : 'ali',
* 百度 : 'bd',
* 头条 : 'tt',
* qq : 'qq',
* 快应用 : 'qn',
* 快手 : 'ks',
* 飞书 : 'lark',
* 快应用 : 'qw',
* 钉钉 : 'dt'
*/
export const get_platform_name = () : string => {
let platformList = {
'app': 'n',
'app-plus': 'n',
'h5': 'h5',
'web': 'web',
'mp-weixin': 'wx',
'mp-baidu': 'bd',
'mp-toutiao': 'tt',
'mp-qq': 'qq',
'quickapp-native': 'qn',
'mp-kuaishou': 'ks',
'mp-lark': 'lark',
'quickapp-webview': 'qw',
}
// 苹果审核代码中禁止出现 alipay 字样 ,需要特殊处理一下
const aliArr = ['y', 'a', 'p', 'mp-ali']
const aliKey = aliArr.reverse().join('')
platformList[aliKey] = 'ali'
const platform = sys.uniPlatform
// TODO 兼容子平台
// 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[platform] as string
}
/**
* 获取原生包名,或小程序 appid
*/
export const get_pack_name = () : string => {
let packName = ''
if (get_platform_name() === 'n') {
if (sys.osName == 'android') {
packName = sysAppBase.packageName ?? ''
}
if (sys.osName == 'ios') {
packName = sysAppBase.bundleId ?? ''
}
}
return packName
}
/**
* 应用版本
*/
export const get_version = () : string => {
return RUNTIME_VERSION
}
/**
* 获取渠道
*/
export const get_channel = () : string => {
// const platformName = get_platform_name()
let channel = ''
// if (platformName === 'n') {
// channel = plus.runtime.channel
// }
// if (platformName === 'wx') {
// // TODO 需要调研小程序二维码渠道如何获取;
// }
return channel
}
/**
* 获取小程序场景值
*/
export const get_scene = (_ : OnLaunchOptionsWithCst) : number => {
// TODO 场景值获取有问题 ,暂时硬编码,需要修改
// options : OnLaunchOptionsWithCst
// const platformName = get_platform_name()
// let scene = ''
// if (options) {
// return options
// }
// if (platformName === 'wx') {
// scene = uni.getLaunchOptionsSync().scene
// }
// return scene
// return options.scene as number
return 1001
}
/**
* 获取页面类型
* @param {VueComponent} appInstance 页面实例
*/
// @ts-ignore
export const get_page_types = (appInstance : ComponentPublicInstance) : string => {
// TODO 暂时无法获取是否应用还是页面,写的硬编码,只支持页面调用
// #ifdef WEB || APP-IOS
return appInstance?.$mpType ?? 'page'
// #endif
// #ifndef WEB
return 'page'
// #endif
}
/**
* 是否获取 page 页面
* @param {VueComponent} appInstance 页面实例
*/
// @ts-ignore
export const is_page = (appInstance : ComponentPublicInstance) : Boolean => {
// #ifdef WEB ||APP-IOS
const type = appInstance?.$mpType ?? 'page'
return type == 'page' ? true : false
// #endif
// #ifndef WEB
// 其他平台没有 $mpType ,只有页面触发
return true
// #endif
}
/**
* 获取页面标题
* @param {String} routepath 页面路由
*/
export const get_page_name = (routepath : string) : string => {
let page = get_page_vm()
if (page == null) return ''
if (page.route != routepath) {
const pages = getCurrentPages()
// 如果传入路由与当前页面不同,则从页面栈找一个,如果找不到返回空
let page_now = pages.find((v) : boolean => {
if (v.route == routepath) {
return true;
}
return false
})
if (page_now == null) {
return ''
}
page = page_now
}
const pageStyle = page.$getPageStyle()
const titleText = pageStyle['navigationBarTitleText'] ?? ''
return titleText as string
}
/**
* 获取页面实例
*/
export const get_page_vm = () : Page | null => {
let pages = getCurrentPages()
if (pages.length == 0) {
return null
}
let page = pages[pages.length - 1]
// TODO 正常来说,调用当前方法的地方只在 生命周期内,数组内最少会有一个页面,所以理论上是不存在获取不到的情况的
return page
}
/**
* 获取页面url,不包含参数
* @param {ComponentPublicInstance} page 页面实例
*/
export function get_route(page : Page | null = null) : RouteParams {
let _self = page ?? get_page_vm()
if (_self == null) {
const data : RouteParams = {
path: '',
fullpath: ''
}
return data
}
// TODO 条件编译处理参数问题,安卓上 options返回的是map,需要处理成 utsobject
// #ifdef APP-ANDROID
const opts = new UTSJSONObject(_self.options)
// TODO 上报页面参数需要处理
let url_params = Serialize(opts)
// #endif
// #ifndef APP-ANDROID
// @ts-ignore
let url_params = Serialize(_self.options)
// #endif
let params = ''
// 如果参数只有 ?则说明没有参数
if (url_params != '?') {
params = url_params
}
let route = _self.route
const data : RouteParams = {
path: route,
fullpath: route + params
}
return data
}
/**
* 获取页面url, 包含参数 ,暂时不用,合并到 get_route
* @param {ComponentPublicInstance} page 页面实例
*/
export function get_page_route(page : Page | null = null) : string {
let _self = page ?? get_page_vm()
let lastPageRoute = uni.getStorageSync('_STAT_LAST_PAGE_ROUTE')
if (_self == null) {
return lastPageRoute ?? ''
}
// 如果找不到 fullPath 就取 route 的值
// TODO 取完整路径,目前最新为 optiosn 为携带参数,需要手动序列化
// return page.fullPath === '/' ? page.route : page.fullPath || page.route
return _self.route
}
/**
* 是否上报页面数据
* @returns
*/
export const is_page_report = () : boolean => {
const uniStatConfig = Config.getOptions();
const collectItems = uniStatConfig.collectItems;
if (collectItems != null) {
const statPageLog = collectItems.uniStatPageLog
if (statPageLog == null) return true
return typeof statPageLog == 'boolean' ? statPageLog : true
}
return true
}
/**
* 是否已处理设备 DeviceId
* 如果值为 1 则表示已处理
*/
const IS_HANDLE_DEVECE_ID = 'is_handle_device_id'
export const is_handle_device = () : boolean => {
let isHandleDevice = dbGet(IS_HANDLE_DEVECE_ID) ?? ''
dbSet(IS_HANDLE_DEVECE_ID, '1')
return isHandleDevice === '1'
}
/**
* 获取上报数据默认值
*/
export const get_default_data = () : StatDefault => {
let statData : StatDefault = {
uuid: get_uuid(),
ak: APPID,
p: sys.osName == 'android' ? 'a' : 'i',
ut: get_platform_name(),
mpn: get_pack_name(),
usv: STAT_VERSION,
v: RUNTIME_VERSION,
ch: get_channel(),
cn: '',
pn: '',
ct: '',
t: get_time(),
tt: '',
brand: sys.deviceBrand,
md: sys.deviceModel,
sv: sys.osVersion.replace(/(Android|iOS)\s/, ''),
mpsdk: sys.SDKVersion,
mpv: sys.uniCompilerVersionCode.toString(),
// mpv: '',
lang: sys.osLanguage,
pr: sys.devicePixelRatio,
ww: sys.windowWidth,
wh: sys.windowHeight,
sw: sys.screenWidth,
sh: sys.screenHeight,
lat: '',
lng: '',
net: '',
odid: ''
}
return statData
}
/**
* 获取上报时间间隔
* @param {*} defaultTime 默认上报间隔时间 单位s
*/
export const get_report_Interval = (defaultTime : number) : number => {
const uniStatConfig = Config.getOptions()
let time = uniStatConfig.reportInterval
// let reg = /(^[1-9]\d*$)/
// 如果不是整数,则默认为上报间隔时间
// if (!reg.test(time)) return defaultTime
if (!IsNumber(time)) return defaultTime
// 如果上报时间配置为0 相当于立即上报
if (time == 0) return 0
// time = time ?? defaultTime
return time as number
}
/**
* 获取uniCloud服务空间配置
* @returns {Object}
*/
export const uni_cloud_config = () : UniCloudInitOptions | null => {
// return process.env.UNI_STAT_UNI_CLOUD || {}
// const custemUnicloudConfig: = {}
return null
}
/**
* 获取服务空间
* @param {*} config
* @returns
*/
export const get_space = (config : UniCloudInitOptions) : UniCloudInitOptions | null => {
const uniCloudConfig = uni_cloud_config()
if (uniCloudConfig == null) {
console.log(config);
// #ifdef APP-ANDROID
if (config.spaceId != '') {
return config
}
// #endif
// #ifndef APP-ANDROID
if (config?.spaceId != '') {
return config
}
// #endif
return null
}
let spaceId = uniCloudConfig.spaceId
let provider = uniCloudConfig.provider
let clientSecret = uniCloudConfig.clientSecret
let secretKey = uniCloudConfig.secretKey
let accessKey = uniCloudConfig.accessKey
const space_type = ['tcb', 'tencent', 'aliyun', 'alipay']
// @ts-ignore
const is_space_id = spaceId != ''
const is_provider = space_type.indexOf(provider) != -1
const is_aliyun = provider == 'aliyun' && is_space_id && clientSecret != null
const is_tcb = (provider == 'tcb' || provider === 'tencent') && is_space_id
const is_alipay = provider == 'alipay' && is_space_id && secretKey != null && accessKey != null
if (is_provider && (is_aliyun || is_tcb || is_alipay)) {
return uniCloudConfig
}
return null
}
/**
* 获取隐私协议配置
*/
export const is_push_clientid = () : boolean => {
const uniStatConfig = Config.getOptions()
const collectItems = uniStatConfig.collectItems
if (collectItems != null) {
const ClientID = collectItems.uniPushClientID ?? false
return typeof ClientID == 'boolean' ? ClientID : false
}
return false
}
/**
* 自定义事件参数校验
*/
export const calibration = (eventName : string, options : any | null) : boolean => {
// 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 as string).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
}
return false
}
import { dbGet } from "./db"
import { PAGE_PVER_TIME, APP_PVER_TIME } from '../config.uts';
import { ResidenceTimeReturn } from '../../interface.uts'
import { dbSet, dbRemove } from "./db";
// 首次访问时间
const FIRST_VISIT_TIME_KEY = '__first__visit__time'
// 最后访问时间
const LAST_VISIT_TIME_KEY = '__last__visit__time'
// 访问总数
const TOTAL_VISIT_COUNT = '__total__visit__count'
const FIRST_TIME = '__first_time'
// 页面停留时间记录key
const PAGE_RESIDENCE_TIME = '__page__residence__time'
let First_Page_Residence_Time = 0
let Last_Page_Residence_Time = 0
/**
* 获取当前时间
*/
export const get_time = () : number => {
return Math.floor(new Date().getTime() / 1000)
}
/**
* 设置页面首次访问时间,用户获取页面/应用停留时常
*/
export const set_first_time = () : number => {
// 获取当前时间 ,以下代码获取到是毫秒级时间戳 ,实际上用到是秒级时间戳,所以需要除以1000
// const time = new Date().getTime()
let time = get_time()
dbSet(FIRST_TIME, time)
return time
}
/**
* 获取首次访问时间
*/
export const get_first_visit_time = () : number => {
const timeStorge = dbGet<number>(FIRST_VISIT_TIME_KEY)
let time : number
if (timeStorge != null && timeStorge != 0) {
time = timeStorge as number
} else {
time = get_time()
dbSet(FIRST_VISIT_TIME_KEY, time)
// 首次访问需要 将最后访问时间置 0
dbRemove(LAST_VISIT_TIME_KEY)
}
return time
}
/**
* 最后访问时间
*/
export const get_last_visit_time = () : number => {
const timeStorge = dbGet<number>(LAST_VISIT_TIME_KEY)
let time = 0
if (timeStorge != null && timeStorge != 0) {
time = timeStorge as number
}
dbSet(LAST_VISIT_TIME_KEY, get_time())
return time
}
/**
* 获取总访问次数
*/
export const get_total_visit_count = () : number => {
const timeStorge = dbGet<number>(TOTAL_VISIT_COUNT)
let count = 1
if (timeStorge != null) {
count = timeStorge as number
count++
}
dbSet(TOTAL_VISIT_COUNT, count)
return count
}
/**
* 获取页面 \ 应用停留时间
*/
export const get_residence_time = (type : string) : ResidenceTimeReturn => {
let residenceTime = 0
const last_time = get_time()
const first_time = (dbGet(FIRST_TIME) ?? last_time) as number
if (first_time != 0) {
residenceTime = last_time - first_time
}
// 将毫秒级时间戳转换为秒级时间戳,因为直接获取的是秒级时间戳,所以不需要转换
// residenceTime = parseInt(residenceTime / 1000)
residenceTime = residenceTime < 1 ? 1 : residenceTime
let timeData : ResidenceTimeReturn = {
residenceTime: residenceTime,
overtime: false,
}
if (type === 'app') {
let overtime = residenceTime > APP_PVER_TIME ? true : false
timeData.overtime = overtime
return timeData
}
if (type === 'page') {
let overtime = residenceTime > PAGE_PVER_TIME ? true : false
timeData.overtime = overtime
return timeData
}
return timeData
}
/**
* 设置页面停留时间
*/
export const set_page_residence_time = () : number => {
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 = () : number => {
Last_Page_Residence_Time = get_time()
First_Page_Residence_Time = dbGet<number>(PAGE_RESIDENCE_TIME) ?? 0
const diff : number = Last_Page_Residence_Time - First_Page_Residence_Time
return diff
}
import { Report, ReportOptions, ReportSuccess, ReportFail } from './interface.uts'
import { Stat } from './common/core/stat.uts'
const stat = Stat.getInstance()
export const report : Report = function (options : ReportOptions) {
const name = options.name
const option = options.options
stat.appEvent(name, option, (type : boolean, msg : string) => {
if (type) {
const res : ReportSuccess = {
errMsg: 'report:ok',
}
options.success?.(res)
options.complete?.(res)
} else {
const err : ReportFail = {
errMsg: 'report fail:' + msg,
}
options.fail?.(err)
options.complete?.(err)
}
})
}
export { Stat } from './common/core/stat.uts'
// // --- 导出统计类型 ---
export { UniStatOptions, UniStatCollectItemsOptions } from './interface.uts'
\ No newline at end of file
export type ReportSuccess = {
errMsg : string,
}
export type ReportFail = {
/**
* 错误的详细信息
*/
errMsg : string,
}
export type ReportOptions = {
/**
* 自定义事件名称
* 内置名称(不允许覆盖):
* uni-app-launch : 应用启动
* uni-app-show : 应用进入前台
* uni-app-hide : 应用进入后台
* uni-app-error : 应用发生错误
*/
name : string
/**
* 额外参数
*/
options ?: any
/**
* 接口调用结束的回调函数(调用成功、失败都会执行)
*/
success ?: (res : ReportSuccess) => void
/**
* 接口调用失败的回调函数
*/
fail ?: (res : ReportFail) => void
/**
* 接口调用成功的回调
*/
complete ?: (res : any) => void
}
export type ReportResult = {
}
/**
* 自定义事件信息
* @param {ReportOptions} options
*
*
* @tutorial https://uniapp.dcloud.net.cn/xxxx
* @platforms APP-IOS = ^9.0,APP-ANDROID = ^22
* @since 4.25
*/
export type Report = (options : ReportOptions) => void
interface Uni {
/**
* 统计自定义事件
* @description 统计自定义事件上报
* @param {ReportOptions} options
* @example
* ```typescript
* uni.report({
* name:'uni-app-launch',
* success(res) {
* console.log(res);
* }
* })
* ```
* @remark
* - 该接口需要同步调用
* @uniPlatform {
* "app": {
* "android": {
* "osVer": "4.4.4",
* "uniVer": "3.6.11",
* "unixVer": "3.9.0"
* },
* "ios": {
* "osVer": "9.0",
* "uniVer": "3.6.11",
* "unixVer": "3.9.0"
* }
* }
* }
* @uniVueVersion 2,3 //支持的vue版本
*
*/
report(options : ReportOptions) : void,
}
// 统计插件参数类型
export type UniStatOptions = {
/* 是否开启debug模式,默认 false */
debug ?: boolean
/* 前端数据上报周期 ,默认 10s ,单位s */
reportInterval ?: number
/* 多服务空间配置 */
uniCloud ?: UniCloudInitOptions
/* 采集项配置 */
collectItems ?: UniStatCollectItemsOptions
}
export type UniStatCollectItemsOptions = {
/* 是否开启推送PushClientID的采集,默认false */
uniPushClientID ?: boolean
/* 是否开启页面数据采集,默认true */
uniStatPageLog ?: boolean
}
/**
* 应用参数扩展
*/
export type OnLaunchOptionsWithCst = {
/** 页面路径 */
path ?: string
/** 上报时机,参看 ReprotCstType */
cst ?: number
/** 场景值 */
scene ?: number
/** 参数 */
query ?: string
};
/**
* 停留时长
*/
export type ResidenceTimeReturn = {
/** 停留时长 */
residenceTime : number
/** 是否超时 */
overtime : boolean
}
/**
* 路由
*/
export type RouteParams = {
path : string
fullpath : string
}
/**
* 页面标题
*/
export type TitleConfigParams = {
/** pages.json 中的标题 */
config : string
/** setNavigationBarTitle 获取的标题*/
page : string
/** 自定义上报的标题 */
report : string
/** 统计数据类型 */
lt : string
}
/**
* 页面参数
*/
export type PageParams = {
/** 当前页面的完整 url,包含参数在内。最多255字符 */
url ?: string
/** pages.json 中定义的页面的 title,获取不到时,可以不传 */
ttpj ?: string
/** 通过接口 uni.setnavigationbartitle 设置的 title,获取不到时,可以不传 */
ttn ?: string
/** 通过 uni.report 上报的页面的 title,获取不到时,可以不传 */
ttc ?: string
/** title 组件中设置的 title,获取不到时,可以不传 */
ttct ?: string
/** 上个页面的完整 url */
urlref : string
/** 上个页面停留时间,单位为秒,不足1秒按1秒计。 url */
urlref_ts : number
/** 上个页面的标题 */
urlref_tt ?: string
}
/**
* 上传 unicloud 参数
*/
export type RequestData = {
/** 统计 SDK 版本号 */
usv : string
/** 发送请求时的时间戮 */
t : number
/** 组合数据 */
requests : string
}
/**
* 用户自定义服务空间配置信息
*/
export type CustomUnicloudConfig = {
/** 服务空间id */
spaceId : string,
/** 云厂商 */
provider : string,
/** clientSecret */
clientSecret ?: string
/** secretKey */
secretKey ?: string
/** secretId */
secretId ?: string
}
export type EventParams = {
/** 事件名字*/
key : string
/** 事件内容 */
value ?: string
}
/**
* 统计默认值
*/
export type StatDefault = {
/** 设备标识 */
uuid : string
/** uni-app 应用 Appid */
ak : string
/** 手机系统 */
p : string
/** 平台类型 */
ut : string
/** 原生平台包名、小程序 appid */
mpn ?: string
/** 统计 sdk 版本 */
usv ?: string
/** 应用版本,仅app */
v ?: string
/** 渠道信息 */
ch ?: string
/** 国家 */
cn ?: string
/** 省份 */
pn ?: string
/** 城市 */
ct ?: string
/** 上报数据时的时间戳 */
t : number
/** 页面标题 */
tt ?: string
/** 手机品牌 */
brand ?: string
/** 手机型号 */
md ?: string
/** 手机系统版本 */
sv ?: string
/** x程序 sdk version */
mpsdk ?: string
/** 小程序平台版本 ,如微信、支付宝 */
mpv ?: string
/** 语言 */
lang ?: string
/** pixelRatio 设备像素比 */
pr ?: number
/** windowWidth 可使用窗口宽度 */
ww ?: number
/** windowHeight 可使用窗口高度 */
wh ?: number
/** screenWidth 屏幕宽度 */
sw ?: number
/** screenHeight 屏幕高度 */
sh ?: number
/** 场景值 */
sc ?: number
/** 统计数据类型 */
lt ?: string
/** 老用户错误的的设备id */
odid ?: string
url ?: string
/** 首次访问时间戳。需要保存在 storage 中。用户首次访问时,取当前时间。*/
fvts ?: number
/** 上次访问时间戳。需要保存在 storage 中。用户首次访问时,设置为0。*/
lvts ?: number
/** total visit count 用户到本次访问为止总共的访问次数。*/
tvc ?: number
/** 上报时机,参看 ReprotCstType */
cst ?: number
urlref_ts ?: number
urlref ?: string
// 标题相关
/** pages.json 中定义的页面的 title,获取不到时,可以不传 */
ttpj ?: string
/** 通过接口 uni.setnavigationbartitle 设置的 title,获取不到时,可以不传 */
ttn ?: string
/** 通过 uni.report 上报的页面的 title,获取不到时,可以不传 */
ttc ?: string
/** title 组件中设置的 title,获取不到时,可以不传 */
ttct ?: string
/** push id */
cid ?: string
/** 自定义事件key */
e_n ?: string
/** 自定义事件value */
e_v ?: string
/** 纬度 */
lat ?: string
/** 经度 */
lng ?: string
/** 网络 */
net ?: string
/** 错误信息 */
em ?:string
}
/**
* 应用首次启动上报参数
*/
export type AppShowReportParams = {
/** uni-app 应用 Appid */
uuid : string
/** 设备标识 */
ak : string
/** 统计数据类型 */
lt : string
/** 平台类型 */
ut : string
/** x程序 sdk version */
mpsdk : string
/** 小程序平台版本 ,如微信、支付宝 */
mpv : string
/** 原生平台包名、小程序 appid */
mpn : string
/** 应用版本,仅app */
v : string
/** 手机系统 */
p : string
/** 手机系统版本 */
sv : string
/** 设备网络 */
net : string
/** 手机品牌 */
brand : string
/** 手机型号 */
md : string
/** 语言 */
lang : string
/** 纬度 */
lat : string
/** 经度 */
lng : string
/** pixelRatio 设备像素比 */
pr : number
/** windowWidth 可使用窗口宽度 */
ww : number
/** windowHeight 可使用窗口高度 */
wh : number
/** screenWidth 屏幕宽度 */
sw : number
/** screenHeight 屏幕高度 */
sh : number
/** 页面启动的url */
url : string
/** 页面标题 */
tt : string
/** 渠道信息 */
ch : string
/** 首次访问时间戳。需要保存在 storage 中。用户首次访问时,取当前时间。*/
fvts : number
/** 上次访问时间戳。需要保存在 storage 中。用户首次访问时,设置为0。*/
lvts : number
/** 国家 */
cn : string
/** 省份 */
pn : string
/** 城市 */
ct : string
/** 场景值 */
sc : number
/** total visit count 用户到本次访问为止总共的访问次数。*/
tvc : number
/** 统计 sdk 版本 */
usv : string
/** 上报数据时的时间戳 */
t : number
/** 老用户错误的的设备id */
odid : string
/** 上报时机,参看 ReprotCstType */
cst : number
}
/**
* 应用进入后台上报参数
*/
export type AppHideReportParams = {
/** uni-app 应用 Appid */
ak : string
/** 设备标识 */
uuid : string
/** 统计数据类型 */
lt : string
/** 平台类型 */
ut : string
/** 手机系统 */
p : string
/** 上个页面的完整 url */
urlref : string
/** 上个页面停留时间,单位为秒,不足1秒按1秒计。 */
urlref_ts : number
/** 渠道信息 */
ch : string
/** 统计 sdk 版本 */
usv : string
/** 上报数据时的时间戳。 */
t : number
}
/**
* 页面切换上报参数
*/
export type PageReportParams = {
/** uni-app 应用 Appid */
ak : string
/** 设备标识 */
uuid : string
/** 统计数据类型 */
lt : string
/** 平台类型 */
ut : string
/** 手机系统 */
p : string
/** 当前页面 url */
url : string
// 标题相关
/** pages.json 中定义的页面的 title,获取不到时,可以不传 */
ttpj : string
/** 通过接口 uni.setnavigationbartitle 设置的 title,获取不到时,可以不传 */
ttn : string
/** 通过 uni.report 上报的页面的 title,获取不到时,可以不传 */
ttc : string
/** title 组件中设置的 title,获取不到时,可以不传 */
ttct : string
/** 上个页面的完整 url */
urlref : string
/** 上个页面停留时间,单位为秒,不足1秒按1秒计。 */
urlref_ts : number
/** 渠道信息 */
ch : string
/** 统计 sdk 版本 */
usv : string
/** 上报数据时的时间戳。 */
t : number
/** 上报时机,参看 ReprotCstType */
cst ?: number
}
export type ErrorCallback = (is_err : boolean, errMsg : string) => void
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册