gio-miniprogram-sdk 4.3.2 → 4.4.0
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/.github/workflows/publish.yml +3 -3
- package/dist/alipay.js +1 -1
- package/dist/baidu.js +1 -1
- package/dist/bytedance.js +1 -1
- package/dist/jingdong.js +1 -1
- package/dist/kuaishou.js +1 -1
- package/dist/plugins/gioABTest.js +1 -1
- package/dist/plugins/gioCompress.js +1 -1
- package/dist/plugins/gioEventAutoTracking.js +1 -1
- package/dist/plugins/gioImpressionTracking.js +1 -1
- package/dist/plugins/gioMultipleInstances.js +1 -1
- package/dist/plugins/gioPerformance.js +1 -0
- package/dist/qq.js +1 -1
- package/dist/quickapp.js +1 -1
- package/dist/taobao.js +1 -1
- package/dist/taro.js +1 -1
- package/dist/uniapp.js +1 -1
- package/dist/wechat.js +1 -1
- package/dist/xiaohongshu.js +1 -0
- package/package.json +12 -6
- package/rollup.config.js +9 -6
- package/rollup.plugins.config.js +1 -1
- package/src/constants/emitMsg.ts +1 -0
- package/src/core/dataStore/eventHooks/appEffects.ts +3 -0
- package/src/core/dataStore/eventHooks/index.ts +4 -2
- package/src/core/dataStore/eventHooks/pageEffects.ts +1 -0
- package/src/core/growingIO.ts +27 -3
- package/src/core/uploader.ts +12 -3
- package/src/core/userStore.ts +1 -2
- package/src/minipInstance/base.ts +62 -23
- package/src/minipInstance/framework.ts +3 -1
- package/src/minipInstance/quickapp.ts +11 -0
- package/src/minipInstance/wx.ts +15 -0
- package/src/minipInstance/xhs.ts +20 -0
- package/src/platformConfig/base.ts +1 -0
- package/src/platformConfig/xhs.ts +20 -0
- package/src/plugins/gioABTest.ts +32 -3
- package/src/plugins/gioEventAutoTracking.ts +99 -1
- package/src/plugins/gioImpressionTracking.ts +3 -1
- package/src/plugins/gioPerformance/exception.ts +152 -0
- package/src/plugins/gioPerformance/monitor.ts +284 -0
- package/src/plugins/gioPerformance/network.ts +197 -0
- package/src/plugins/gioPerformance.ts +131 -0
- package/src/types/base.ts +5 -1
- package/src/types/minipInstance.ts +3 -1
- package/src/types/platforms.ts +3 -0
- package/src/utils/tools.ts +46 -3
- package/src/utils/verifiers.ts +4 -3
- package/tsconfig.json +1 -1
- package/typings/global.d.ts +8 -0
- package/bin/build.sh +0 -38
|
@@ -107,6 +107,8 @@ class GioImpressionTracking {
|
|
|
107
107
|
return collectTarget.route
|
|
108
108
|
? toString(hashCode(collectTarget.route)) // swan page
|
|
109
109
|
: collectTarget.__nodeId__; // swan component
|
|
110
|
+
case 'xhs':
|
|
111
|
+
return collectTarget.pageId;
|
|
110
112
|
default:
|
|
111
113
|
break;
|
|
112
114
|
}
|
|
@@ -216,7 +218,7 @@ class GioImpressionTracking {
|
|
|
216
218
|
originObserver.disconnect();
|
|
217
219
|
}
|
|
218
220
|
// 创建一个新的监听
|
|
219
|
-
let observer = ['swan', 'jd'].includes(gioPlatform)
|
|
221
|
+
let observer = ['swan', 'jd', 'xhs'].includes(gioPlatform)
|
|
220
222
|
? minipInstance.minip?.createIntersectionObserver(
|
|
221
223
|
collectTarget,
|
|
222
224
|
observerOption
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { GrowingIOType } from '@@/types/growingIO';
|
|
2
|
+
import { isArray, isEmpty, isFunction, toString } from '@@/utils/glodash';
|
|
3
|
+
import EMIT_MSG from '@@/constants/emitMsg';
|
|
4
|
+
|
|
5
|
+
export default class Exception {
|
|
6
|
+
private minipInst: any;
|
|
7
|
+
// 监听标记,防止重复监听或者内存泄露
|
|
8
|
+
private listenerSet = false;
|
|
9
|
+
// 缓存队列,防止SDK初始化之前的报错丢掉或发不出去
|
|
10
|
+
private cacheQueue: any[];
|
|
11
|
+
private buildErrorEvent: (attributes: any) => void;
|
|
12
|
+
constructor(public growingIO: GrowingIOType) {
|
|
13
|
+
this.minipInst = this.growingIO.minipInstance.minip;
|
|
14
|
+
this.cacheQueue = [];
|
|
15
|
+
// 上报方法
|
|
16
|
+
this.buildErrorEvent = (attributes: any) =>
|
|
17
|
+
this.growingIO.plugins.gioPerformance?.buildPerfEvent(
|
|
18
|
+
'apm_system_error',
|
|
19
|
+
attributes
|
|
20
|
+
);
|
|
21
|
+
// 进入页面时初始化各种错误的监听
|
|
22
|
+
this.setListeners();
|
|
23
|
+
this.hookNavigate();
|
|
24
|
+
// app从后台拉起时开始监听(第一次见进入时会根据listenerSet判断不会重复监听)
|
|
25
|
+
if (isFunction(this.minipInst?.onAppShow)) {
|
|
26
|
+
this.minipInst?.onAppShow(this.setListeners);
|
|
27
|
+
}
|
|
28
|
+
// app切后台或关闭小程序时把监听移除,防止内存泄露
|
|
29
|
+
if (isFunction(this.minipInst?.onAppHide)) {
|
|
30
|
+
this.minipInst?.onAppHide(() => {
|
|
31
|
+
if (this.minipInst.canIUse('offError')) {
|
|
32
|
+
this.minipInst.offError();
|
|
33
|
+
}
|
|
34
|
+
if (this.minipInst.canIUse('offUnhandledRejection')) {
|
|
35
|
+
this.minipInst.offUnhandledRejection();
|
|
36
|
+
}
|
|
37
|
+
if (this.minipInst.canIUse('offLazyLoadError')) {
|
|
38
|
+
this.minipInst.offLazyLoadError();
|
|
39
|
+
}
|
|
40
|
+
this.listenerSet = false;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
this.growingIO.emitter.on(EMIT_MSG.SDK_INITIALIZED, () => {
|
|
44
|
+
if (!isEmpty(this.cacheQueue)) {
|
|
45
|
+
this.cacheQueue.forEach((c: any) => {
|
|
46
|
+
this.buildErrorEvent(c);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 设置错误监听
|
|
53
|
+
setListeners = () => {
|
|
54
|
+
if (!this.listenerSet) {
|
|
55
|
+
// 监听小程序错误事件。如脚本错误或 API 调用报错等。
|
|
56
|
+
if (this.minipInst.canIUse('onError')) {
|
|
57
|
+
this.minipInst?.onError((errorString: string) => {
|
|
58
|
+
this.errorParse(errorString);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
// 监听未处理的 Promise 拒绝事件。
|
|
62
|
+
if (this.minipInst.canIUse('onUnhandledRejection')) {
|
|
63
|
+
this.minipInst?.onUnhandledRejection((rejectError: any) => {
|
|
64
|
+
// 普通的promise reject
|
|
65
|
+
if (rejectError.reason.stack) {
|
|
66
|
+
this.errorParse(
|
|
67
|
+
rejectError.reason.stack,
|
|
68
|
+
rejectError.reason.message
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
// 监听异步组件加载失败
|
|
74
|
+
if (this.minipInst.canIUse('onLazyLoadError')) {
|
|
75
|
+
this.minipInst?.onLazyLoadError((error: any) => {
|
|
76
|
+
this.buildErrorEvent({
|
|
77
|
+
error_type: error.errMsg,
|
|
78
|
+
error_content: toString(error.subpackage)
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
this.listenerSet = true;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// hook跳转方法获取跳转错误
|
|
87
|
+
hookNavigate = () => {
|
|
88
|
+
const self = this;
|
|
89
|
+
[
|
|
90
|
+
'switchTab',
|
|
91
|
+
'reLaunch',
|
|
92
|
+
'redirectTo',
|
|
93
|
+
'navigateTo',
|
|
94
|
+
'navigateBack'
|
|
95
|
+
].forEach((reqKey: string) => {
|
|
96
|
+
const originNavigate = this.minipInst[reqKey];
|
|
97
|
+
Object.defineProperty(this.minipInst, reqKey, {
|
|
98
|
+
writable: true,
|
|
99
|
+
enumerable: true,
|
|
100
|
+
configurable: true,
|
|
101
|
+
value: function (...args: any[]) {
|
|
102
|
+
const options: any = args[0] ?? {};
|
|
103
|
+
const originFail = options?.fail;
|
|
104
|
+
const fail = function (result: any) {
|
|
105
|
+
self.growingIO.plugins.gioPerformance?.buildPerfEvent('Error', {
|
|
106
|
+
error_type: `${reqKey} Error`,
|
|
107
|
+
error_content: result.errMsg
|
|
108
|
+
});
|
|
109
|
+
if (typeof originFail === 'function') {
|
|
110
|
+
return originFail(result);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
return originNavigate.call(this, {
|
|
114
|
+
...options,
|
|
115
|
+
fail
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// 解析普通错误堆栈
|
|
123
|
+
errorParse = (errorString: string, msg?: string) => {
|
|
124
|
+
// 按分行截取为堆栈数组
|
|
125
|
+
const stacks = errorString.split('\n');
|
|
126
|
+
let title: string, // 错误标题
|
|
127
|
+
content: string; // 错误的第一行堆栈
|
|
128
|
+
if (stacks.length) {
|
|
129
|
+
stacks.find((v, i) => {
|
|
130
|
+
const matcher = v.match(/at (.*?) \((.*):(\d{1,}):(\d{1,})\)/);
|
|
131
|
+
if (isArray(matcher)) {
|
|
132
|
+
title = stacks[i - 1];
|
|
133
|
+
content = v.trim();
|
|
134
|
+
return matcher.length;
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
if (!title) {
|
|
138
|
+
title = stacks[0];
|
|
139
|
+
}
|
|
140
|
+
if (!content) {
|
|
141
|
+
content = stacks[1];
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
title = msg || 'Error';
|
|
145
|
+
content = errorString.substring(0, 100);
|
|
146
|
+
}
|
|
147
|
+
this.buildErrorEvent({
|
|
148
|
+
error_type: msg || title,
|
|
149
|
+
error_content: content
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { PERF_MONITOR_VLUES } from '@@/types/gioPerformance';
|
|
2
|
+
import { GrowingIOType } from '@@/types/growingIO';
|
|
3
|
+
import { forEach, has, head, isEmpty, keys, unset } from '@@/utils/glodash';
|
|
4
|
+
import EMIT_MSG from '@@/constants/emitMsg';
|
|
5
|
+
|
|
6
|
+
export default class Monitor {
|
|
7
|
+
private minipInst: any;
|
|
8
|
+
private appPerf: any;
|
|
9
|
+
private pagePerf: any;
|
|
10
|
+
private bootType: 'cold' | 'warm';
|
|
11
|
+
private lastWarm = false;
|
|
12
|
+
public referrerPath = ''; // 基础库低于2.23.1或者taro等一些框架中会没有referrerPageId,因此需要靠自己记录上一个页面
|
|
13
|
+
private lastUnloadPath = ''; // 打开小程序后再点击分享的连接进入小程序时,页面会被销毁,会触发页面重新加载,从而变成两个热启动,根据上一个被销毁的页面地址来判断是否要发热启动
|
|
14
|
+
private buildMonitorEvent: (attributes: any, extra: any) => void;
|
|
15
|
+
constructor(public growingIO: GrowingIOType) {
|
|
16
|
+
this.minipInst = this.growingIO.minipInstance.minip;
|
|
17
|
+
this.appPerf = {};
|
|
18
|
+
this.pagePerf = {};
|
|
19
|
+
this.bootType = 'cold';
|
|
20
|
+
this.lastWarm = false;
|
|
21
|
+
this.buildMonitorEvent = (attributes: any, extra: any) =>
|
|
22
|
+
this.growingIO.plugins.gioPerformance?.buildPerfEvent(
|
|
23
|
+
'apm_app_launch',
|
|
24
|
+
attributes,
|
|
25
|
+
extra
|
|
26
|
+
);
|
|
27
|
+
// 目前仅微信和字节小程序支持小程序启动性能的获取
|
|
28
|
+
if (['MinP', 'bytedance'].includes(this.growingIO.minipInstance.platform)) {
|
|
29
|
+
this.initObserver();
|
|
30
|
+
}
|
|
31
|
+
this.growingIO.emitter.on(
|
|
32
|
+
EMIT_MSG.MINIP_LIFECYCLE,
|
|
33
|
+
({ event, timestamp, params }) => {
|
|
34
|
+
const [actionType, lifeCycleName] = event.split(' ');
|
|
35
|
+
switch (actionType) {
|
|
36
|
+
case 'App': {
|
|
37
|
+
this.appPerf = {
|
|
38
|
+
...this.appPerf,
|
|
39
|
+
[lifeCycleName]: timestamp
|
|
40
|
+
};
|
|
41
|
+
if (lifeCycleName === 'onShowEnd') {
|
|
42
|
+
// 只有靠appOnShow才能判断是不是热启动,所以放在App的生命周期中做标记
|
|
43
|
+
// 也因为App的生命周期先执行,在后面Page和Component中判断的时候是没问题的
|
|
44
|
+
if (this.bootType === 'warm' && !this.lastWarm) {
|
|
45
|
+
this.lastWarm = true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
case 'Page': {
|
|
51
|
+
const path = (params.page || params.instance)?.route;
|
|
52
|
+
this.pagePerf[path] = {
|
|
53
|
+
...this.pagePerf[path],
|
|
54
|
+
[lifeCycleName]: timestamp
|
|
55
|
+
};
|
|
56
|
+
if (lifeCycleName === 'onShowEnd') {
|
|
57
|
+
// 标记当前页面是热启动,则发送App热启动性能
|
|
58
|
+
if (
|
|
59
|
+
this.lastWarm &&
|
|
60
|
+
this.bootType === 'warm' &&
|
|
61
|
+
this.referrerPath === path &&
|
|
62
|
+
this.lastUnloadPath !== path
|
|
63
|
+
) {
|
|
64
|
+
this.handleEvent(path);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (['onHide', 'onUnload'].includes(lifeCycleName)) {
|
|
68
|
+
this.referrerPath = path;
|
|
69
|
+
}
|
|
70
|
+
if (lifeCycleName === 'onUnload') {
|
|
71
|
+
this.lastUnloadPath = path;
|
|
72
|
+
}
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
default:
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 初始化性能监听
|
|
83
|
+
initObserver = () => {
|
|
84
|
+
const performance = this.minipInst.canIUse('getPerformance')
|
|
85
|
+
? this.minipInst.getPerformance()
|
|
86
|
+
: {};
|
|
87
|
+
const observer = performance?.createObserver((entryList) => {
|
|
88
|
+
this.handleLaunch(head(entryList.getEntriesByName('appLaunch')));
|
|
89
|
+
// this.handleScript(head(entryList.getEntriesByName('evaluateScript')));
|
|
90
|
+
// this.handleFR(head(entryList.getEntriesByName('firstRender')));
|
|
91
|
+
// this.handleFP(head(entryList.getEntriesByName('firstPaint')));
|
|
92
|
+
this.handleFCP(head(entryList.getEntriesByName('firstContentfulPaint')));
|
|
93
|
+
this.handleLCP(
|
|
94
|
+
head(entryList.getEntriesByName('largestContentfulPaint'))
|
|
95
|
+
);
|
|
96
|
+
this.handleRoute(head(entryList.getEntriesByName('route')));
|
|
97
|
+
});
|
|
98
|
+
observer?.observe({ entryTypes: ['render', 'script', 'navigation'] });
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// 小程序初始化耗时
|
|
102
|
+
handleLaunch = (entry: any) => {
|
|
103
|
+
if (!isEmpty(entry)) {
|
|
104
|
+
this.appPerf = {
|
|
105
|
+
...this.appPerf,
|
|
106
|
+
appStartTime: entry.startTime,
|
|
107
|
+
appDuration: entry.duration,
|
|
108
|
+
firstPath: entry.path
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// 注入代码耗时
|
|
114
|
+
// handleScript = (entry: any) => {
|
|
115
|
+
// if (!isEmpty(entry)) {
|
|
116
|
+
// this.appPerf = {
|
|
117
|
+
// ...this.appPerf,
|
|
118
|
+
// scriptStartTime: entry.startTime,
|
|
119
|
+
// scriptDuration: entry.duration
|
|
120
|
+
// };
|
|
121
|
+
// }
|
|
122
|
+
// };
|
|
123
|
+
|
|
124
|
+
// firstRender耗时
|
|
125
|
+
// handleFR = (entry: any) => {
|
|
126
|
+
// if (!isEmpty(entry)) {
|
|
127
|
+
// this.pagePerf[entry.path] = {
|
|
128
|
+
// ...this.pagePerf[entry.path],
|
|
129
|
+
// FRStartTime: entry.startTime,
|
|
130
|
+
// FRDuration: entry.duration,
|
|
131
|
+
// };
|
|
132
|
+
// }
|
|
133
|
+
// };
|
|
134
|
+
|
|
135
|
+
// firstPaint耗时
|
|
136
|
+
// handleFP = (entry: any) => {
|
|
137
|
+
// if (!isEmpty(entry)) {
|
|
138
|
+
// this.pagePerf[entry.path] = {
|
|
139
|
+
// ...this.pagePerf[entry.path],
|
|
140
|
+
// FPStartTime: entry.startTime,
|
|
141
|
+
// };
|
|
142
|
+
// }
|
|
143
|
+
// };
|
|
144
|
+
|
|
145
|
+
// firstContentfulPaint耗时
|
|
146
|
+
handleFCP = (entry: any) => {
|
|
147
|
+
if (!isEmpty(entry)) {
|
|
148
|
+
this.pagePerf[entry.path] = {
|
|
149
|
+
...this.pagePerf[entry.path],
|
|
150
|
+
FCPStartTime: entry.startTime
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
this.entriesValidate(entry ?? { name: 'firstContentfulPaint' });
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// largestContentfulPaint耗时
|
|
157
|
+
handleLCP = (entry: any) => {
|
|
158
|
+
if (!isEmpty(entry)) {
|
|
159
|
+
this.pagePerf[entry.path] = {
|
|
160
|
+
...this.pagePerf[entry.path],
|
|
161
|
+
LCPStartTime: entry.startTime
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
this.entriesValidate(entry ?? { name: 'largestContentfulPaint' });
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// 路由跳转耗时
|
|
168
|
+
handleRoute = (entry: any) => {
|
|
169
|
+
if (!isEmpty(entry)) {
|
|
170
|
+
this.pagePerf[entry.path] = {
|
|
171
|
+
...this.pagePerf[entry.path],
|
|
172
|
+
navigationStart: entry.startTime,
|
|
173
|
+
routeDuration: entry.duration
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
this.entriesValidate(entry ?? { name: 'route' });
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// 校验指标是否都获取完毕
|
|
180
|
+
entriesValidate = (entry: any) => {
|
|
181
|
+
const path =
|
|
182
|
+
entry.path || this.growingIO.minipInstance.getCurrentPage().route;
|
|
183
|
+
if (!this.pagePerf[path]) {
|
|
184
|
+
this.pagePerf[path] = {
|
|
185
|
+
path
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
if (!this.pagePerf[path].entries) {
|
|
189
|
+
this.pagePerf[path].entries = [];
|
|
190
|
+
}
|
|
191
|
+
this.pagePerf[path].entries.push(entry.name);
|
|
192
|
+
// 所有的entry都执行过('firstContentfulPaint', 'largestContentfulPaint', 'route')
|
|
193
|
+
const matcher = ['firstContentfulPaint', 'largestContentfulPaint', 'route'];
|
|
194
|
+
let eve = true;
|
|
195
|
+
if (this.pagePerf[path].entries.length < matcher.length) {
|
|
196
|
+
eve = false;
|
|
197
|
+
}
|
|
198
|
+
matcher.forEach((o) => {
|
|
199
|
+
if (!this.pagePerf[path].entries.includes(o)) {
|
|
200
|
+
eve = false;
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
if (eve) {
|
|
204
|
+
this.handleEvent(path);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// 组装监控事件
|
|
209
|
+
handleEvent = (path: string) => {
|
|
210
|
+
const { currentPage } = this.growingIO.dataStore.eventHooks;
|
|
211
|
+
const perfData: PERF_MONITOR_VLUES = {
|
|
212
|
+
// 页面标题
|
|
213
|
+
title: currentPage.getPageTitle()
|
|
214
|
+
};
|
|
215
|
+
const { appStartTime } = this.appPerf;
|
|
216
|
+
const appLaunch = this.appPerf.onLaunch;
|
|
217
|
+
const appShow = this.appPerf.onShow;
|
|
218
|
+
const {
|
|
219
|
+
onLoad, // page生命周期
|
|
220
|
+
onShowEnd, // page生命周期
|
|
221
|
+
onReadyEnd, // page生命周期
|
|
222
|
+
navigationStart,
|
|
223
|
+
FCPStartTime,
|
|
224
|
+
LCPStartTime
|
|
225
|
+
} = this.pagePerf[path];
|
|
226
|
+
// 用于计算的页面开始时间
|
|
227
|
+
const pageStart = onLoad;
|
|
228
|
+
// 用于计算的页面结束时间
|
|
229
|
+
const pageEnd = onReadyEnd || onShowEnd;
|
|
230
|
+
// FCP(白屏时长)从created(相当于页面onLoad)开始算
|
|
231
|
+
if (FCPStartTime) {
|
|
232
|
+
perfData.first_contentful_paint_duration =
|
|
233
|
+
FCPStartTime > pageStart ? FCPStartTime - pageStart : 0;
|
|
234
|
+
}
|
|
235
|
+
// LCP(最大内容加载时长)从created(相当于页面onLoad)开始算
|
|
236
|
+
if (LCPStartTime) {
|
|
237
|
+
perfData.largest_contentful_paint_duration =
|
|
238
|
+
LCPStartTime > pageStart ? LCPStartTime - pageStart : 0;
|
|
239
|
+
}
|
|
240
|
+
// 页面加载时长
|
|
241
|
+
if (this.referrerPath) {
|
|
242
|
+
// 如果上次的页面和本次进入的页面是同一个说明是热启动
|
|
243
|
+
if (path === this.referrerPath && appShow) {
|
|
244
|
+
// 热启动
|
|
245
|
+
perfData.page_launch_duration = pageEnd - appShow;
|
|
246
|
+
perfData.reboot_duration = pageEnd - appShow;
|
|
247
|
+
perfData.reboot_mode = 'warm';
|
|
248
|
+
this.lastWarm = false;
|
|
249
|
+
} else {
|
|
250
|
+
// 如果上次的页面和本次进入的页面不是同一个说明是跳转页面
|
|
251
|
+
perfData.page_launch_duration =
|
|
252
|
+
pageEnd - (navigationStart ?? pageStart);
|
|
253
|
+
}
|
|
254
|
+
} else if (this.bootType === 'cold') {
|
|
255
|
+
// 没有上次页面说明是冷启动
|
|
256
|
+
perfData.page_launch_duration = pageEnd - appLaunch;
|
|
257
|
+
// 如果标记是app首屏页面加载的冷启动,添加app的冷启动标识和冷启动整体加载时长
|
|
258
|
+
perfData.reboot_duration = pageEnd - appStartTime;
|
|
259
|
+
perfData.reboot_mode = 'cold';
|
|
260
|
+
// 如果本次标记了冷启动耗时,则标记后续的小程序启动都是热启动
|
|
261
|
+
this.bootType = 'warm';
|
|
262
|
+
}
|
|
263
|
+
forEach(perfData, (v, k) => {
|
|
264
|
+
if ([null, 'null', NaN, 'NaN'].includes(v)) {
|
|
265
|
+
unset(perfData, k);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
if (perfData.reboot_mode && !has(perfData, 'reboot_duration')) {
|
|
269
|
+
unset(perfData, 'reboot_mode');
|
|
270
|
+
}
|
|
271
|
+
this.appPerf = {};
|
|
272
|
+
this.pagePerf[path] = {};
|
|
273
|
+
// 过滤没有性能信息和没有页面加载时长的性能数据
|
|
274
|
+
if (keys(perfData).length <= 2 || !has(perfData, 'page_launch_duration')) {
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
this.buildMonitorEvent(perfData, {
|
|
278
|
+
timestamp: +Date.now(),
|
|
279
|
+
path: currentPage.getPagePath(),
|
|
280
|
+
query: currentPage.getPageQuery(),
|
|
281
|
+
title: perfData.title
|
|
282
|
+
});
|
|
283
|
+
};
|
|
284
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { GrowingIOType } from '@@/types/growingIO';
|
|
2
|
+
import {
|
|
3
|
+
compact,
|
|
4
|
+
forEach,
|
|
5
|
+
isArray,
|
|
6
|
+
isEmpty,
|
|
7
|
+
isNil,
|
|
8
|
+
isRegExp,
|
|
9
|
+
isString,
|
|
10
|
+
unset
|
|
11
|
+
} from '@@/utils/glodash';
|
|
12
|
+
|
|
13
|
+
export default class Network {
|
|
14
|
+
private minipInst: any;
|
|
15
|
+
private excludeRegExp: RegExp | string | any[];
|
|
16
|
+
private gdpRequestURL: string;
|
|
17
|
+
private buildNetworkEvent: (attributes: any) => void;
|
|
18
|
+
constructor(public growingIO: GrowingIOType, public options: any = {}) {
|
|
19
|
+
this.minipInst = this.growingIO.minipInstance.minip;
|
|
20
|
+
this.buildNetworkEvent = (attributes: any) => {
|
|
21
|
+
if (!options.network) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const { duration, url, method, statusCode } = attributes;
|
|
25
|
+
this.growingIO.plugins.gioPerformance?.buildPerfEvent(
|
|
26
|
+
'apm_network_request',
|
|
27
|
+
{
|
|
28
|
+
response_duration: duration,
|
|
29
|
+
request_address: url,
|
|
30
|
+
request_method: method,
|
|
31
|
+
http_code: statusCode
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
if (this.options.network?.exclude) {
|
|
37
|
+
this.excludeRegExp = this.options.network.exclude;
|
|
38
|
+
}
|
|
39
|
+
this.hookRequest();
|
|
40
|
+
// 重写生成地址的方法,sdk在性能分析时需要拿到主实例的请求地址做判断用
|
|
41
|
+
const self = this;
|
|
42
|
+
const originFunc = this.growingIO.uploader.generateURL;
|
|
43
|
+
this.growingIO.uploader.generateURL = function () {
|
|
44
|
+
const result = originFunc.apply(this, arguments);
|
|
45
|
+
const trackingId = arguments[0];
|
|
46
|
+
if (trackingId === self.growingIO.trackingId) {
|
|
47
|
+
self.gdpRequestURL = result;
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 检验url是否符合过滤条件
|
|
54
|
+
verifyUrl = (url: string) => {
|
|
55
|
+
// Gio的请求直接过滤
|
|
56
|
+
if (url.indexOf(this.gdpRequestURL) > -1) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
// 正则或字符串域名数组过滤
|
|
60
|
+
if (isArray(this.excludeRegExp)) {
|
|
61
|
+
const verifyResult = compact(
|
|
62
|
+
(this.excludeRegExp as any[]).map((re: RegExp | string) => {
|
|
63
|
+
if (isRegExp(re)) {
|
|
64
|
+
return (re as RegExp).test(url);
|
|
65
|
+
} else if (isString(re)) {
|
|
66
|
+
return url.indexOf(re as string) > -1;
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
);
|
|
70
|
+
return !isEmpty(verifyResult);
|
|
71
|
+
} else if (isString(this.excludeRegExp)) {
|
|
72
|
+
// 单个字符串过滤校验
|
|
73
|
+
return url.indexOf(this.excludeRegExp as string) > -1;
|
|
74
|
+
} else if (isRegExp(this.excludeRegExp)) {
|
|
75
|
+
// 单个正则过滤校验
|
|
76
|
+
return (this.excludeRegExp as RegExp).test(url);
|
|
77
|
+
} else {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// 重写request
|
|
83
|
+
hookRequest = () => {
|
|
84
|
+
const self = this;
|
|
85
|
+
['request', 'downloadFile', 'uploadFile'].forEach((reqKey: string) => {
|
|
86
|
+
const originRequest = this.minipInst[reqKey];
|
|
87
|
+
Object.defineProperty(this.minipInst, reqKey, {
|
|
88
|
+
writable: true,
|
|
89
|
+
enumerable: true,
|
|
90
|
+
configurable: true,
|
|
91
|
+
value: function (...args: any[]) {
|
|
92
|
+
const options: any = args[0];
|
|
93
|
+
const { url } = options;
|
|
94
|
+
// 如果符合过滤条件就直接发请求不做处理
|
|
95
|
+
if (self.verifyUrl(url)) {
|
|
96
|
+
return originRequest.call(this, options);
|
|
97
|
+
}
|
|
98
|
+
let cstmRequestInfo: any = {
|
|
99
|
+
header: options.header || {},
|
|
100
|
+
url: options.url
|
|
101
|
+
};
|
|
102
|
+
switch (reqKey) {
|
|
103
|
+
case 'request':
|
|
104
|
+
const { method } = options;
|
|
105
|
+
cstmRequestInfo = { ...cstmRequestInfo, method };
|
|
106
|
+
break;
|
|
107
|
+
case 'downloadFile':
|
|
108
|
+
case 'uploadFile':
|
|
109
|
+
const { filePath } = options;
|
|
110
|
+
cstmRequestInfo = {
|
|
111
|
+
...cstmRequestInfo,
|
|
112
|
+
filePath,
|
|
113
|
+
method: reqKey === 'downloadFile' ? 'GET' : 'POST'
|
|
114
|
+
};
|
|
115
|
+
break;
|
|
116
|
+
default:
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const originFail = options.fail;
|
|
121
|
+
const fail = function (result: any) {
|
|
122
|
+
cstmRequestInfo.endTime = Date.now();
|
|
123
|
+
self.growingIO.plugins.gioPerformance?.buildPerfEvent('Error', {
|
|
124
|
+
error_type: result.errMsg,
|
|
125
|
+
error_content: cstmRequestInfo.url
|
|
126
|
+
});
|
|
127
|
+
if (typeof originFail === 'function') {
|
|
128
|
+
return originFail(result);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const originSuccess = options.success;
|
|
133
|
+
const success = function (result) {
|
|
134
|
+
cstmRequestInfo.endTime = Date.now();
|
|
135
|
+
self.networkPerfBuilder(result, cstmRequestInfo);
|
|
136
|
+
if (typeof originSuccess === 'function') {
|
|
137
|
+
return originSuccess(result);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
cstmRequestInfo.startTime = Date.now();
|
|
141
|
+
return originRequest.call(this, {
|
|
142
|
+
...options,
|
|
143
|
+
success,
|
|
144
|
+
fail
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// 请求性能数据组装
|
|
152
|
+
networkPerfBuilder = (requestResult: any, cstmRequestInfo: any) => {
|
|
153
|
+
const { profile = {}, statusCode = 0, errMsg } = requestResult;
|
|
154
|
+
const { url, filePath, method } = cstmRequestInfo;
|
|
155
|
+
const {
|
|
156
|
+
redirectStart,
|
|
157
|
+
redirectEnd,
|
|
158
|
+
fetchStart,
|
|
159
|
+
domainLookUpStart,
|
|
160
|
+
domainLookUpEnd,
|
|
161
|
+
connectStart,
|
|
162
|
+
connectEnd,
|
|
163
|
+
requestStart,
|
|
164
|
+
responseStart,
|
|
165
|
+
responseEnd
|
|
166
|
+
} = profile;
|
|
167
|
+
const p = !isEmpty(profile);
|
|
168
|
+
const networkPerf = {
|
|
169
|
+
url,
|
|
170
|
+
filePath,
|
|
171
|
+
method,
|
|
172
|
+
statusCode,
|
|
173
|
+
message: errMsg,
|
|
174
|
+
// 重定向耗时(重定向没有发生,或者其中一个重定向非同源,则该值为 0)
|
|
175
|
+
redirect: p ? redirectEnd - redirectStart : 0,
|
|
176
|
+
// 缓存耗时
|
|
177
|
+
cache: p ? domainLookUpStart - fetchStart : 0,
|
|
178
|
+
// dns域名解析耗时
|
|
179
|
+
dns: p ? domainLookUpEnd - domainLookUpStart : 0,
|
|
180
|
+
// tcp链接耗时
|
|
181
|
+
tcp: p ? connectEnd - connectStart : 0,
|
|
182
|
+
// 在SSL链接建立好后,从客户端发送至服务端首次响应的耗时
|
|
183
|
+
request: p ? responseStart - requestStart : 0,
|
|
184
|
+
// 从服务端首次响应至数据完全响应完的耗时
|
|
185
|
+
response: p ? responseEnd - responseStart : 0,
|
|
186
|
+
// 请求总耗时
|
|
187
|
+
duration: p
|
|
188
|
+
? responseEnd - fetchStart
|
|
189
|
+
: cstmRequestInfo.endTime - cstmRequestInfo.startTime
|
|
190
|
+
};
|
|
191
|
+
forEach(
|
|
192
|
+
networkPerf,
|
|
193
|
+
(v, k) => (v === '' || isNil(v)) && unset(networkPerf, k)
|
|
194
|
+
);
|
|
195
|
+
this.buildNetworkEvent(networkPerf);
|
|
196
|
+
};
|
|
197
|
+
}
|