gio-miniprogram-sdk 4.3.1 → 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.
Files changed (52) hide show
  1. package/.github/workflows/publish.yml +3 -3
  2. package/dist/alipay.js +1 -1
  3. package/dist/baidu.js +1 -1
  4. package/dist/bytedance.js +1 -1
  5. package/dist/jingdong.js +1 -1
  6. package/dist/kuaishou.js +1 -1
  7. package/dist/plugins/gioABTest.js +1 -1
  8. package/dist/plugins/gioCompress.js +1 -1
  9. package/dist/plugins/gioEventAutoTracking.js +1 -1
  10. package/dist/plugins/gioImpressionTracking.js +1 -1
  11. package/dist/plugins/gioMultipleInstances.js +1 -1
  12. package/dist/plugins/gioPerformance.js +1 -0
  13. package/dist/qq.js +1 -1
  14. package/dist/quickapp.js +1 -1
  15. package/dist/taobao.js +1 -1
  16. package/dist/taro.js +1 -1
  17. package/dist/uniapp.js +1 -1
  18. package/dist/wechat.js +1 -1
  19. package/dist/xiaohongshu.js +1 -0
  20. package/package.json +12 -6
  21. package/rollup.config.js +9 -6
  22. package/rollup.plugins.config.js +1 -1
  23. package/src/constants/emitMsg.ts +1 -0
  24. package/src/core/dataStore/eventHooks/appEffects.ts +3 -0
  25. package/src/core/dataStore/eventHooks/index.ts +4 -2
  26. package/src/core/dataStore/eventHooks/pageEffects.ts +1 -0
  27. package/src/core/growingIO.ts +27 -3
  28. package/src/core/uploader.ts +12 -3
  29. package/src/core/userStore.ts +1 -2
  30. package/src/minipInstance/base.ts +62 -25
  31. package/src/minipInstance/framework.ts +3 -1
  32. package/src/minipInstance/my.ts +40 -0
  33. package/src/minipInstance/quickapp.ts +23 -6
  34. package/src/minipInstance/wx.ts +15 -0
  35. package/src/minipInstance/xhs.ts +20 -0
  36. package/src/platformConfig/base.ts +1 -0
  37. package/src/platformConfig/xhs.ts +20 -0
  38. package/src/plugins/gioABTest.ts +32 -3
  39. package/src/plugins/gioEventAutoTracking.ts +99 -1
  40. package/src/plugins/gioImpressionTracking.ts +3 -1
  41. package/src/plugins/gioPerformance/exception.ts +152 -0
  42. package/src/plugins/gioPerformance/monitor.ts +284 -0
  43. package/src/plugins/gioPerformance/network.ts +197 -0
  44. package/src/plugins/gioPerformance.ts +131 -0
  45. package/src/types/base.ts +5 -1
  46. package/src/types/minipInstance.ts +3 -1
  47. package/src/types/platforms.ts +3 -0
  48. package/src/utils/tools.ts +46 -3
  49. package/src/utils/verifiers.ts +4 -3
  50. package/tsconfig.json +1 -1
  51. package/typings/global.d.ts +8 -0
  52. package/bin/build.sh +0 -38
@@ -5,17 +5,21 @@
5
5
  import { ID_REG, SWAN_XID_REG, TARO_XID_REG } from '@@/constants/regex';
6
6
  import { EventTarget } from '@@/types/eventHooks';
7
7
  import { GrowingIOType } from '@@/types/growingIO';
8
- import { consoleText, isTaro3 } from '@@/utils/tools';
8
+ import { consoleText, getExtraData, isTaro3 } from '@@/utils/tools';
9
9
  import EMIT_MSG from '@@/constants/emitMsg';
10
+ import { isEmpty } from '@@/utils/glodash';
10
11
 
11
12
  let ut;
12
13
  class GioEventAutoTracking {
13
14
  public pluginVersion: string;
14
15
  private prevEvent: any;
16
+ public circleServerUrl: string;
17
+ public circleOpen = false;
15
18
  constructor(public growingIO: GrowingIOType) {
16
19
  this.pluginVersion = '__PLUGIN_VERSION__';
17
20
  ut = this.growingIO.utils;
18
21
  this.prevEvent = {};
22
+ this.listenForLunchEvent();
19
23
  }
20
24
 
21
25
  main = (e: EventTarget, eventName: string) => {
@@ -183,6 +187,100 @@ class GioEventAutoTracking {
183
187
  eventInterceptor(event);
184
188
  }
185
189
  };
190
+
191
+ // --------------------- 以下内容是对新版小程序圈选的支持 ---------------------
192
+
193
+ // 监听小程序的onLunch事件
194
+ listenForLunchEvent = () => {
195
+ const { emitter } = this.growingIO;
196
+ emitter.on(EMIT_MSG.MINIP_LIFECYCLE, ({ event, params }) => {
197
+ if (['App onLaunch', 'App onShow'].includes(event)) {
198
+ this.circleInit(params);
199
+ }
200
+ });
201
+ };
202
+
203
+ // 初始化圈选
204
+ circleInit = (params: any) => {
205
+ const { emitter } = this.growingIO;
206
+ const extraData = getExtraData(params.referrerInfo?.extraData);
207
+ // 额外参数中存在gdpCircleRoomCollectUrl,则表示是圈选的跳转,进入圈选状态
208
+ if (extraData?.gdpCircleRoomCollectUrl) {
209
+ // 圈选初始化
210
+ if (this.circleServerUrl !== extraData?.gdpCircleRoomCollectUrl) {
211
+ this.circleServerUrl = extraData?.gdpCircleRoomCollectUrl;
212
+ // 提前移除一次事件发送监听,防止重复监听
213
+ emitter.off(EMIT_MSG.ON_SEND_AFTER, this.collectorSendFn);
214
+ // 添加事件发送监听,用于发送圈选事件
215
+ emitter.on(EMIT_MSG.ON_SEND_AFTER, this.collectorSendFn);
216
+ this.showPromptModal('enter');
217
+ }
218
+ this.circleOpen = true;
219
+ }
220
+ };
221
+
222
+ // 数据采集发送监听回调
223
+ collectorSendFn = ({ requestData, trackingId }) => {
224
+ const { uploader } = this.growingIO;
225
+ if (this.circleOpen) {
226
+ // 过滤仅主实例的非CUSTOM事件可以圈选
227
+ const filteredEvents = requestData.filter(
228
+ (e) =>
229
+ e.eventType !== 'CUSTOM' && trackingId === this.growingIO.trackingId
230
+ );
231
+ if (!isEmpty(filteredEvents)) {
232
+ // 同时请求数在3个以下直接发起请求
233
+ if (uploader.requestingNum < uploader.requestLimit) {
234
+ this.circleRequest(filteredEvents);
235
+ } else {
236
+ // 同时有3个请求在发送时,设轮询延时直到请求数在3个以下
237
+ let t = setInterval(() => {
238
+ if (uploader.requestingNum < uploader.requestLimit) {
239
+ this.circleRequest(filteredEvents);
240
+ clearInterval(t);
241
+ t = undefined;
242
+ }
243
+ }, 500);
244
+ }
245
+ }
246
+ }
247
+ };
248
+
249
+ // 圈选结束
250
+ circleClose = () => {
251
+ this.circleOpen = false;
252
+ this.circleServerUrl = '';
253
+ this.growingIO.emitter.off(EMIT_MSG.ON_SEND_AFTER, this.collectorSendFn);
254
+ };
255
+
256
+ // 展示圈选提示
257
+ showPromptModal = (type: 'enter' | 'error', msg?: string) => {
258
+ this.growingIO.minipInstance.minip.showModal({
259
+ title: 'GrowingIO提示',
260
+ content: type === 'error' ? msg : '您已进入圈选模式',
261
+ showCancel: false
262
+ });
263
+ };
264
+
265
+ // 发起圈选请求
266
+ circleRequest = (requestData: any) => {
267
+ const { minipInstance } = this.growingIO;
268
+ minipInstance.request({
269
+ url: this.circleServerUrl,
270
+ header: { 'content-type': 'application/json;charset=UTF-8' },
271
+ method: 'POST',
272
+ data: requestData,
273
+ complete: ({ statusCode, data, errMsg }: any) => {
274
+ if (![200, 204].includes(statusCode) || !data?.success) {
275
+ this.showPromptModal(
276
+ 'error',
277
+ data?.message || data || errMsg || '圈选请求失败,请重试!'
278
+ );
279
+ this.circleClose();
280
+ }
281
+ }
282
+ });
283
+ };
186
284
  }
187
285
 
188
286
  export default { name: 'gioEventAutoTracking', method: GioEventAutoTracking };
@@ -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
+ }