@tmsfe/tms-core 0.0.63 → 0.0.64
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/report/README.md +201 -0
- package/src/report/proxy/component.ts +6 -12
- package/src/report/proxy/helper.ts +1 -1
- package/src/report/proxy/page.ts +5 -6
- package/src/stringUtils.js +14 -9
- package/src/timeUtils.js +3 -0
- package/src/timeUtils.test.js +3 -1
package/package.json
CHANGED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# 埋点
|
|
2
|
+
|
|
3
|
+
### 目前支持的数据能力
|
|
4
|
+
<img width="700" src="https://tai-static-1251316161.cos.ap-chengdu.myqcloud.com/reportcos/tms-data.png"/>
|
|
5
|
+
|
|
6
|
+
### 埋点展示网站:https://tmsgo.testsite.woa.com/reportManage/list
|
|
7
|
+
|
|
8
|
+
### 目录结构
|
|
9
|
+
```
|
|
10
|
+
┌─ proxy # 全埋点逻辑
|
|
11
|
+
│ ├─ clone # 负责对象的克隆
|
|
12
|
+
│ ├─ component # 负责组件的全埋点
|
|
13
|
+
│ ├─ helper # 全埋点辅助类
|
|
14
|
+
│ ├─ index # 负责页面和组件的全埋点初始化
|
|
15
|
+
│ ├─ page # 负责页面的全埋点
|
|
16
|
+
│ └─ types # 全埋点ts类型定义
|
|
17
|
+
│
|
|
18
|
+
├─ formatV1 # 格式化旧埋点
|
|
19
|
+
├─ formatV2 # 格式化新埋点
|
|
20
|
+
├─ helper # 埋点辅助函数
|
|
21
|
+
├─ index # 埋点入口
|
|
22
|
+
├─ sender # 负责发送埋点
|
|
23
|
+
└─ types # ts类型定义
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### 埋点的基础字段有哪些?
|
|
27
|
+
不管是全埋点还是手动埋点,都会携带相同的基础字段,具体请查看```formatV2.ts```代码
|
|
28
|
+
|
|
29
|
+
### 手动添加埋点
|
|
30
|
+
```coffeescript
|
|
31
|
+
1. const reporter = getApp().tms.getReporter();
|
|
32
|
+
// 点击登录按钮|用户名
|
|
33
|
+
2. reporter.report2('login_btn_click', this.data.username);
|
|
34
|
+
```
|
|
35
|
+
#### 说明:
|
|
36
|
+
1. 新埋点函数名为```report2()```,旧埋点函数名为```report()```,不建议再用旧埋点
|
|
37
|
+
2. 埋点上方一定要写注释说明埋点每个参数的定义,用"|"隔开
|
|
38
|
+
3. 构建平台在每次构建时会调用工具分析代码提取出埋点列表更新到展示网站数据库
|
|
39
|
+
4. 埋点函数第一个参数为事件名,必须为string,其他参数为该事件附带数据,可以是基础类型,也可以是json对象
|
|
40
|
+
|
|
41
|
+
#### 问题
|
|
42
|
+
1. 代码中已有的旧埋点怎么办?可以保留,但是不建议再新增旧埋点
|
|
43
|
+
2. 埋点发送机制是怎么样的?普通埋点采用合并上报的方式,通常3s上报一次,如果小程序关闭则会立即上报
|
|
44
|
+
3. 如果有埋点需要立即上报该怎么办?可以把```report2()```改为调用```fastReport2()```
|
|
45
|
+
4. ```report2()```与```fastReport2()```的区别是什么?```fastReport2()```上报时携带的"省"、"市"等需要异步请求的基础字段会从缓存中读取,如果无缓存则为空,```report2()```则一定会携带这些字段
|
|
46
|
+
|
|
47
|
+
### 全埋点
|
|
48
|
+
#### 页面自动上报事件
|
|
49
|
+
##### 一、必须上报的生命周期事件:
|
|
50
|
+
1. onLoad: 页面加载
|
|
51
|
+
2. onShow: 页面显示
|
|
52
|
+
3. onReady: 页面初次渲染完成
|
|
53
|
+
4. onHide: 页面隐藏
|
|
54
|
+
5. onUnload: 页面卸载
|
|
55
|
+
|
|
56
|
+
##### 二、可能会上报的生命周期事件(取决于页面是否实现该函数):
|
|
57
|
+
1. onPullDownRefresh: 用户下拉动作
|
|
58
|
+
2. onReachBottom: 页面上拉触底
|
|
59
|
+
3. onShareAppMessage: 用户点击右上角转发
|
|
60
|
+
4. onShareTimeline: 用户点击右上角转发到朋友圈
|
|
61
|
+
5. onAddToFavorites: 用户点击右上角收藏
|
|
62
|
+
6. onReportPageTouch: 用户触摸页面 - 如果页面wxml有view节点则会注入该事件
|
|
63
|
+
|
|
64
|
+
##### 三、页面的其他bind事件,通过分析wxml得出
|
|
65
|
+
#### 组件自动上报事件
|
|
66
|
+
1. ready: 组件加载(会携带组件data)
|
|
67
|
+
2. 组件的其他bind事件,通过分析wxml得出(会携带组件data)
|
|
68
|
+
|
|
69
|
+
#### 其他自动上报事件
|
|
70
|
+
1. App_onLaunch: 小程序启动
|
|
71
|
+
2. wx_navigateTo_before: 页面即将跳转
|
|
72
|
+
3. wx_redirectTo_before: 页面即将重定向
|
|
73
|
+
4. wx_reLaunch_before: 小程序即将重启
|
|
74
|
+
|
|
75
|
+
#### 注意事项
|
|
76
|
+
1. bind事件函数上方要写注释,工具提取该事件时会把注释也提取出来,用作该事件说明
|
|
77
|
+
2. Page或者Component的data要写注释,工具会提取data代码附到埋点说明中
|
|
78
|
+
3. 某个事件触发很频繁觉得上报没意义,在函数注释中加上```ignoreReport```这个单词就不会上报该函数
|
|
79
|
+
```coffeescript
|
|
80
|
+
/**
|
|
81
|
+
* 拖动组件
|
|
82
|
+
* ignoreReport
|
|
83
|
+
*/
|
|
84
|
+
onTouchMove(e) {
|
|
85
|
+
...
|
|
86
|
+
}
|
|
87
|
+
or
|
|
88
|
+
// ignoreReport
|
|
89
|
+
onTouchMove(e) {
|
|
90
|
+
...
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
#### 哪种bind事件的写法会使全埋点上报时把最新的data也带上?
|
|
95
|
+
ps:tms-core会先执行事件函数,然后判断返回值是否promise,如果时则会等promise.then或catch之后再上报,非promise则直接上报
|
|
96
|
+
1. 函数中直接setData
|
|
97
|
+
```coffeescript
|
|
98
|
+
/**
|
|
99
|
+
* 点击关闭按钮
|
|
100
|
+
*/
|
|
101
|
+
onCloseBtnTap() {
|
|
102
|
+
this.setData({ show: false });
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
2. 函数返回promise的
|
|
106
|
+
```coffeescript
|
|
107
|
+
/**
|
|
108
|
+
* 点击登录按钮
|
|
109
|
+
*/
|
|
110
|
+
onLoginBtnTap() {
|
|
111
|
+
return tms.login().then((res) => {
|
|
112
|
+
this.setData({ userName: res.userName });
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
or
|
|
116
|
+
/**
|
|
117
|
+
* 点击登录按钮
|
|
118
|
+
*/
|
|
119
|
+
async onLoginBtnTap() {
|
|
120
|
+
const res = await tms.login();
|
|
121
|
+
this.setData({ userName: res.userName });
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
#### 哪种bind事件的写法会使全埋点上报时不把最新的data也带上?
|
|
126
|
+
1. 函数中异步setData但是没有返回promise的
|
|
127
|
+
```coffeescript
|
|
128
|
+
/**
|
|
129
|
+
* 点击登录按钮
|
|
130
|
+
*/
|
|
131
|
+
onLoginBtnTap() {
|
|
132
|
+
tms.login().then((res) => {
|
|
133
|
+
this.setData({ userName: res.userName });
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
#### 如何在开发过程中开启全埋点?
|
|
139
|
+
1. 切到新脚手架的小程序目录下
|
|
140
|
+
```
|
|
141
|
+
cd miniprogram/programs/sinan/dist
|
|
142
|
+
```
|
|
143
|
+
2. 运行工具
|
|
144
|
+
```
|
|
145
|
+
node ../../../../tools/tms-prebuild/main.js
|
|
146
|
+
```
|
|
147
|
+
3. 等待工具跑完
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
#### 如何判断全埋点是否满足产品需求?
|
|
151
|
+
1. 目标事件是页面生命周期函数触发时上报,且不需要带data
|
|
152
|
+
2. 目标事件是组件挂载(ready),并且需要携带的参数在组件data中
|
|
153
|
+
3. 目标事件是页面或者组件的bind事件,并且需要携带的参数在data或者函数的首个参数中(event的dataset、mark、detail)
|
|
154
|
+
|
|
155
|
+
#### 埋点提取工具AST匹配规则
|
|
156
|
+
```coffeescript
|
|
157
|
+
/**
|
|
158
|
+
* 提取旧埋点
|
|
159
|
+
* @param content {string}
|
|
160
|
+
* @param rootAst {GoGoAST}
|
|
161
|
+
* @returns {DataInfo[]}
|
|
162
|
+
*/
|
|
163
|
+
function extractV1(content, rootAst) {
|
|
164
|
+
const paramsKey = '$$$0';
|
|
165
|
+
const selector = [
|
|
166
|
+
`report({ ${paramsKey} })`,
|
|
167
|
+
`Report({ ${paramsKey} })`,
|
|
168
|
+
`reportLog({ ${paramsKey} })`,
|
|
169
|
+
`fastReport({ ${paramsKey} })`,
|
|
170
|
+
`$_$1.report({ ${paramsKey} })`,
|
|
171
|
+
`$_$1.Report({ ${paramsKey} })`,
|
|
172
|
+
`$_$1.reportLog({ ${paramsKey} })`,
|
|
173
|
+
`$_$1.fastReport({ ${paramsKey} })`,
|
|
174
|
+
];
|
|
175
|
+
const ast = rootAst.find(selector);
|
|
176
|
+
const result = [];
|
|
177
|
+
ast.each((node) => { 处理逻辑... });
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* 提取新埋点
|
|
184
|
+
* @param content
|
|
185
|
+
* @param rootAst {GoGoAST}
|
|
186
|
+
* @returns {DataInfo[]}
|
|
187
|
+
*/
|
|
188
|
+
function extractV2(content, rootAst) {
|
|
189
|
+
const paramsKey = '$$$0';
|
|
190
|
+
const selector = [
|
|
191
|
+
`report2(${paramsKey})`,
|
|
192
|
+
`fastReport2(${paramsKey})`,
|
|
193
|
+
`$_$1.report2(${paramsKey})`,
|
|
194
|
+
`$_$1.fastReport2(${paramsKey})`,
|
|
195
|
+
];
|
|
196
|
+
const ast = rootAst.find(selector);
|
|
197
|
+
const result = [];
|
|
198
|
+
ast.each((node) => { 处理逻辑... });
|
|
199
|
+
return result;
|
|
200
|
+
}
|
|
201
|
+
```
|
|
@@ -12,9 +12,12 @@ let originalComponent: any = null;
|
|
|
12
12
|
// 劫持Component的生命周期
|
|
13
13
|
function proxyLifeMethod(componentName: string, componentOptions: any, methodName: string): void {
|
|
14
14
|
/* eslint-disable */
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
let obj = componentOptions;
|
|
16
|
+
if (componentOptions.lifetimes?.hasOwnProperty(methodName)) {
|
|
17
|
+
obj = componentOptions.lifetimes;
|
|
18
|
+
}
|
|
19
|
+
let original = obj[methodName] || helper.emptyFunc;
|
|
20
|
+
obj[methodName] = function (...args: any[]): any {
|
|
18
21
|
// 生命周期函数先发埋点,避免次过程用户退出而丢失埋点
|
|
19
22
|
const data = clone.deepClone(this.data);
|
|
20
23
|
const eventName = `Component_${componentName}_${methodName}`;
|
|
@@ -22,15 +25,6 @@ function proxyLifeMethod(componentName: string, componentOptions: any, methodNam
|
|
|
22
25
|
// 执行原函数
|
|
23
26
|
return helper.executeFunc(this, original, args, helper.emptyFunc);
|
|
24
27
|
}
|
|
25
|
-
if (componentOptions.lifetimes[methodName]) {
|
|
26
|
-
original = componentOptions.lifetimes[methodName];
|
|
27
|
-
componentOptions.lifetimes[methodName] = proxyMethod;
|
|
28
|
-
} else if (componentOptions[methodName]) {
|
|
29
|
-
original = componentOptions[methodName];
|
|
30
|
-
componentOptions[methodName] = proxyMethod;
|
|
31
|
-
} else {
|
|
32
|
-
componentOptions.lifetimes[methodName] = proxyMethod;
|
|
33
|
-
}
|
|
34
28
|
/* eslint-enable */
|
|
35
29
|
}
|
|
36
30
|
|
package/src/report/proxy/page.ts
CHANGED
|
@@ -29,9 +29,8 @@ function proxyLifeMethod(pageOptions: any, methodName: string): void {
|
|
|
29
29
|
// eslint-disable-next-line no-param-reassign
|
|
30
30
|
pageOptions[methodName] = function (...args: any[]): any {
|
|
31
31
|
// 生命周期函数先发埋点,避免次过程用户退出而丢失埋点
|
|
32
|
-
const options = methodName === 'onLoad' ? clone.deepClone(args[0], 0, 1) : null;
|
|
33
32
|
const extra = getPageLifeExtra(this, methodName, args);
|
|
34
|
-
helper.reportData(`Page_${methodName}`, extra
|
|
33
|
+
helper.reportData(`Page_${methodName}`, extra);
|
|
35
34
|
|
|
36
35
|
// 执行原函数
|
|
37
36
|
return helper.executeFunc(this, original, args, helper.emptyFunc);
|
|
@@ -126,11 +125,11 @@ function proxyNavigateApi(api: string): void {
|
|
|
126
125
|
enumerable: true,
|
|
127
126
|
configurable: true,
|
|
128
127
|
value(...args: any) {
|
|
129
|
-
originalApi.apply(this, args);
|
|
130
|
-
|
|
131
128
|
const { url = '' } = args[0] || {};
|
|
132
129
|
const [path, params = ''] = url.split('?');
|
|
133
|
-
helper.reportData(
|
|
130
|
+
helper.reportData(`wx_${api}_before`, path, params);
|
|
131
|
+
|
|
132
|
+
originalApi.apply(this, args);
|
|
134
133
|
},
|
|
135
134
|
});
|
|
136
135
|
}
|
|
@@ -139,8 +138,8 @@ function proxyNavigateApi(api: string): void {
|
|
|
139
138
|
function init(): void {
|
|
140
139
|
proxyPage();
|
|
141
140
|
proxyNavigateApi('navigateTo');
|
|
142
|
-
proxyNavigateApi('reLaunch');
|
|
143
141
|
proxyNavigateApi('redirectTo');
|
|
142
|
+
proxyNavigateApi('reLaunch');
|
|
144
143
|
}
|
|
145
144
|
|
|
146
145
|
export default {
|
package/src/stringUtils.js
CHANGED
|
@@ -125,19 +125,21 @@ const isValidPlate = (plate) => {
|
|
|
125
125
|
* @returns {string} 返回四舍五入后的字符串,异常情况下返回空字符串''
|
|
126
126
|
*/
|
|
127
127
|
const roundStr = (x, n = 2, removeTrailingZero = false) => {
|
|
128
|
-
let xNum = Number(x); // x转换为数字
|
|
129
128
|
const nNum = Math.floor(Number(n)); // n转换为数字,且只保留整数部分
|
|
129
|
+
let xNum = Number(x); // x转换为数字
|
|
130
130
|
|
|
131
131
|
// 异常情况,返回''
|
|
132
|
-
if (isNaN(xNum) || isNaN(nNum) || nNum < 0)
|
|
132
|
+
if (isNaN(xNum) || isNaN(nNum) || nNum < 0) {
|
|
133
|
+
return '';
|
|
134
|
+
}
|
|
133
135
|
|
|
134
136
|
// 仅保留整数的情况
|
|
135
|
-
if (nNum === 0)
|
|
137
|
+
if (nNum === 0) {
|
|
138
|
+
return Math.round(xNum).toString();
|
|
139
|
+
}
|
|
136
140
|
|
|
137
|
-
// 保留n位小数的情况
|
|
138
|
-
const xStr = xNum.toString();
|
|
139
141
|
const rexExp = new RegExp(`\\.\\d{${nNum}}5`);
|
|
140
|
-
|
|
142
|
+
// 保留n位小数的情况
|
|
141
143
|
// 1. 大部分情况下,四舍五入使用Number.toFixed即可
|
|
142
144
|
// 2. 然而,Number.toFixed方法在某些情况下对第n+1位是5的四舍五入存在问题,如1.325保留2小数时结果为1.32(期望为1.33)
|
|
143
145
|
// 对此种情况下,有两种处理方式:
|
|
@@ -146,18 +148,21 @@ const roundStr = (x, n = 2, removeTrailingZero = false) => {
|
|
|
146
148
|
// 2.2 Number.toFixed是四舍6入,对于第n+1位是5的情况,增加2*10^(-n-1),保证满足第n+1位>6
|
|
147
149
|
// 增加2*10^(-n-1)而不是增加1*10^(-n-1)是因为后者不能保证第n+1位>=6,例如1.325+0.001=1.32599999...第n+1位仍然为5
|
|
148
150
|
// 此处,采用2.2方式,解决Number.toFixed的问题,又能避免2.1方式中数字超过Infinity的问题
|
|
149
|
-
|
|
151
|
+
// 情况2,处理方式2.1:如果小数部分第n+1位是5,增加2*10^(-n-1)
|
|
152
|
+
if (rexExp.test(xNum.toString())) {
|
|
150
153
|
xNum += 2 * (10 ** (-nNum - 1));
|
|
151
154
|
}
|
|
152
155
|
|
|
153
156
|
const str = xNum.toFixed(nNum);
|
|
154
|
-
if (!removeTrailingZero)
|
|
157
|
+
if (!removeTrailingZero) {
|
|
158
|
+
return str;
|
|
159
|
+
}
|
|
155
160
|
|
|
156
161
|
// 去除末尾的0
|
|
157
162
|
if (/^\d+\.0*$/.test(str)) { // 小数部分全是0
|
|
158
163
|
return str.replace(/^(\d+)(\.0*)$/, (_m, s1) => s1);
|
|
159
164
|
}
|
|
160
|
-
return str.replace(/^(\d+\.\d*[1-9]
|
|
165
|
+
return str.replace(/^(\d+\.\d*[1-9])(0*)$/, (_m, s1) => s1);
|
|
161
166
|
};
|
|
162
167
|
|
|
163
168
|
export {
|
package/src/timeUtils.js
CHANGED
|
@@ -233,6 +233,9 @@ const parseDateTime = (str) => {
|
|
|
233
233
|
|
|
234
234
|
const date = new Date();
|
|
235
235
|
date.setFullYear(year);
|
|
236
|
+
// 解决js setMonth bug,在setMonth前先将day设置为1号,保证setMonth结果正确
|
|
237
|
+
// https://www.google.com/search?q=js+setmonth+bug&oq=js+setMonth&aqs=chrome.1.69i57j0i512j0i8i30j0i5i30j69i61.4323j0j4&sourceid=chrome&ie=UTF-8
|
|
238
|
+
date.setDate(1);
|
|
236
239
|
date.setMonth(month - 1);
|
|
237
240
|
date.setDate(day);
|
|
238
241
|
date.setHours(hour);
|
package/src/timeUtils.test.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { formatTimeDuration, formatTime, formatTimeWithDetails, parseDateTime } from './timeUtils';
|
|
2
2
|
|
|
3
3
|
describe('timeUtils', () => {
|
|
4
|
-
describe
|
|
4
|
+
describe('formatTimeDuration test cases', () => {
|
|
5
5
|
test.each([
|
|
6
6
|
[0.001, undefined, undefined, undefined, '1秒内'],
|
|
7
7
|
[0.01, 'year', 'second', 2, '0.01秒'],
|
|
@@ -120,6 +120,8 @@ describe('timeUtils', () => {
|
|
|
120
120
|
['2022/03/24', 1648051200000],
|
|
121
121
|
['2022/03/24 20:2', 1648123320000],
|
|
122
122
|
['2022年03月24日20时02分', 1648123320000],
|
|
123
|
+
['2022年4月1日20时02分', 1648814520000],
|
|
124
|
+
['2022年6月1日', 1654012800000],
|
|
123
125
|
])('parseDateTime(%s) => Date(%d)', (str, timeStamp) => {
|
|
124
126
|
const date = parseDateTime(str);
|
|
125
127
|
expect(date).not.toBe(null);
|