spy-client 2.0.7 → 2.1.1

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 (41) hide show
  1. package/README.md +119 -10
  2. package/dist/head/base.d.ts +1 -1
  3. package/dist/lib/huffman.d.ts +9 -0
  4. package/dist/lib/interface.d.ts +59 -1
  5. package/dist/lib/util.d.ts +22 -0
  6. package/dist/module/longtask.d.ts +1 -1
  7. package/dist/module/resource.d.ts +19 -9
  8. package/dist/module/tti.d.ts +1 -1
  9. package/dist/spy-client-basic.esm.js +233 -1167
  10. package/dist/spy-client-basic.iife.js +237 -1171
  11. package/dist/spy-client-basic.iife.min.js +1 -1
  12. package/dist/spy-client-basic.js +238 -1172
  13. package/dist/spy-client-basic.min.js +1 -1
  14. package/dist/spy-client-basic.mjs +82 -54
  15. package/dist/spy-client.d.ts +5 -4
  16. package/dist/spy-client.esm.js +1393 -5662
  17. package/dist/spy-client.iife.js +1401 -5670
  18. package/dist/spy-client.iife.min.js +1 -1
  19. package/dist/spy-client.js +1402 -5671
  20. package/dist/spy-client.min.js +1 -1
  21. package/dist/spy-client.mjs +704 -455
  22. package/dist/spy-head.js +330 -276
  23. package/dist/spy-head.min.js +1 -1
  24. package/dist/spy-local-cache.d.ts +18 -0
  25. package/dist/spy-local-cache.js +690 -0
  26. package/dist/spy-local-cache.min.js +1 -0
  27. package/example/index.html +8 -2
  28. package/example/local-cache.html +97 -0
  29. package/example/saveCache.js +41 -0
  30. package/karma.conf.js +111 -0
  31. package/package.json +21 -20
  32. package/src/head/error.ts +14 -15
  33. package/src/head/whitescreen.ts +8 -5
  34. package/src/lib/huffman.ts +326 -0
  35. package/src/lib/interface.ts +70 -1
  36. package/src/lib/util.ts +101 -0
  37. package/src/module/resource.ts +226 -129
  38. package/src/spy-client-basic.ts +8 -7
  39. package/src/spy-client.ts +15 -6
  40. package/src/spy-local-cache.ts +385 -0
  41. package/src/types/globals.d.ts +1 -1
@@ -1,22 +1,13 @@
1
- // import Debug from 'debug';
2
- // import { PageApi } from "./pageApi";
3
- // import WebbInstance from 'webb/instance';
4
- // import _ from '@searchfe/underscore';
5
-
6
- // const WEBB_PID = '1_1';
7
- // const WEBB_TYPE = 'pf_comm';
8
- // const WEBB_GROUP = 'resource';
9
- // const debug = Debug('perfcollect:resource');
10
-
11
- import {Module, ResourceMetric, ResourceCB, ResourceErrorCB, ResType} from '../lib/interface';
12
-
13
-
14
- // URL拓展
15
- // 增加后缀名
16
- interface UrlObj extends URL {
17
- ext?: string;
18
- };
19
1
 
2
+ import {
3
+ Module, ResourceMetric, ResourceCB, ResourceErrorCB, ResType,
4
+ ResOption,
5
+ BigImgOption,
6
+ HttpResOption,
7
+ SlowOption,
8
+ ResourceHostMetric,
9
+ } from '../lib/interface';
10
+ import {getUrlInfo, URLINFO, assign, getResTiming, getxpath} from '../lib/util';
20
11
 
21
12
  // 字体
22
13
  // svg使用场景比较灵活,可能作为一种字体格式,也可能当做图片使用
@@ -32,46 +23,52 @@ enum FontsTypes {
32
23
 
33
24
  // 忽略图片
34
25
  // 下述图片路径往往是用来进行日志统计,不是正常图片
35
- enum IgnoreImgPath {
26
+ const ignoreDefaultPaths = [
36
27
  '/mwb2.gif',
37
- };
38
-
39
- function getxpath(el: Element | null) {
40
- if (!el) {
41
- return {xpath: ''};
42
- }
43
- const xpath = [];
44
- while (el && el.nodeType === 1 && el !== el.parentNode) {
45
- xpath.push(el.tagName.toLowerCase());
28
+ ];
46
29
 
47
- if (el === document.body) {
48
- break;
30
+ function ignorePath(url: string, paths: string[]) {
31
+ for (const path of paths) {
32
+ if (url.indexOf(path) > -1) {
33
+ return true;
49
34
  }
50
- el = el.parentNode as Element; // 修复缺陷检查
51
35
  }
52
- return {
53
- xpath: xpath.join('<'),
54
- };
36
+ return false;
55
37
  }
56
38
 
57
39
  function f(n: number): number {
58
40
  return +n.toFixed(1);
59
41
  }
60
42
 
43
+ interface ListItem {
44
+ timing: PerformanceResourceTiming;
45
+ type: string;
46
+ }
47
+
48
+ interface ListWithHost {
49
+ [host: string]: ListItem[];
50
+ }
51
+
61
52
  export default class Resource implements Module {
62
53
  private cb: ResourceCB;
63
54
  private bigImgCB: ResourceErrorCB;
64
55
  private httpResCB: ResourceErrorCB;
65
- private maxImgSize: number;
56
+ private slowResCB: ResourceErrorCB;
57
+ private resOption: ResOption;
58
+ private bigImgOption: BigImgOption;
59
+ private httpResOption: HttpResOption;
60
+ private slowOption: SlowOption;
61
+ private trigger: string = 'load';
66
62
 
67
63
  private readonly jsList: PerformanceResourceTiming[];
68
64
  private readonly cssList: PerformanceResourceTiming[];
69
65
  private readonly imgList: PerformanceResourceTiming[];
70
66
  private readonly fontList: PerformanceResourceTiming[];
71
- private readonly imgTnList: PerformanceResourceTiming[];
72
67
 
73
- private readonly bigImgList: PerformanceResourceTiming[];
74
- private readonly httpResList: PerformanceResourceTiming[];
68
+ private readonly hostList: ListWithHost;
69
+ private readonly bigImgList: ListWithHost;
70
+ private readonly httpResList: ListWithHost;
71
+ private readonly slowList: ListWithHost;
75
72
 
76
73
  constructor() {
77
74
  this.jsList = [];
@@ -79,113 +76,210 @@ export default class Resource implements Module {
79
76
  this.imgList = [];
80
77
  this.fontList = [];
81
78
 
82
- this.bigImgList = [];
83
- this.httpResList = [];
79
+ this.hostList = {};
80
+ this.bigImgList = {};
81
+ this.httpResList = {};
82
+ this.slowList = {};
83
+
84
+ this.resOption = {ignorePaths: [], trigger: 'load'};
85
+ this.bigImgOption = {ignorePaths: [], maxSize: 150 * 1024, trigger: 'load'};
86
+ this.httpResOption = {ignorePaths: [], trigger: 'load'};
87
+ this.slowOption = {ignorePaths: [], trigger: 'load', threshold: 1000};
84
88
  }
85
89
 
86
90
  check() {
87
91
  return performance && performance.getEntriesByType;
88
92
  }
89
93
 
90
- listenResource(cb: ResourceCB) {
94
+ listenResource(cb: ResourceCB, option: ResOption = {}) {
91
95
  if (!this.check()) {
92
96
  return;
93
97
  }
98
+ this.resOption = assign(this.resOption, option);
99
+ this.trigger = this.resOption.trigger as string;
94
100
  this.cb = cb;
95
101
  }
96
102
 
97
- listenBigImg(cb: ResourceErrorCB, maxSize = 150) {
103
+ listenBigImg(cb: ResourceErrorCB, option: BigImgOption = {}) {
98
104
  if (!this.check()) {
99
105
  return;
100
106
  }
101
- this.maxImgSize = maxSize * 1024;
107
+ this.bigImgOption = assign(this.bigImgOption, option);
108
+ this.trigger = this.bigImgOption.trigger as string;
102
109
  this.bigImgCB = cb;
103
110
  }
104
111
 
105
- listenHttpResource(cb: ResourceErrorCB) {
112
+ listenHttpResource(cb: ResourceErrorCB, option: HttpResOption = {}) {
106
113
  if (!this.check()) {
107
114
  return;
108
115
  }
109
-
116
+ this.httpResOption = assign(this.httpResOption, option);
117
+ this.trigger = this.httpResOption.trigger as string;
110
118
  this.httpResCB = cb;
111
119
  }
112
120
 
113
- load() {
121
+ listenSlowResource(cb: ResourceErrorCB, option: SlowOption = {}) {
114
122
  if (!this.check()) {
115
123
  return;
116
124
  }
125
+ this.slowOption = assign(this.slowOption, option);
126
+ this.trigger = this.slowOption.trigger as string;
127
+ this.slowResCB = cb;
128
+ }
117
129
 
118
- setTimeout(() => {
119
- const metric = this.getMetric();
120
- if (metric) {
121
- this.cb && this.cb(metric);
122
- }
130
+ report() {
131
+ const metricRes = this.getMetric();
132
+ if (metricRes) {
133
+ const {metric, hostMetric} = metricRes;
134
+ this.cb && this.cb(metric, hostMetric);
135
+ }
123
136
 
137
+ if (this.bigImgCB) {
124
138
  // 发送大图监控数据
125
- if (window.requestIdleCallback && this.bigImgList.length && this.bigImgCB) {
139
+ if (typeof window.requestIdleCallback === 'function' && Object.keys(this.bigImgList).length) {
126
140
  window.requestIdleCallback(() => {
127
- for (const entry of this.bigImgList) {
128
- try {
129
- const img = document.body.querySelector('img[src="' + entry.name + '"]');
130
- this.bigImgCB({
131
- msg: entry.name,
132
- xpath: getxpath(img).xpath,
133
- });
134
- }
135
- catch (e) {
136
- console.error(e);
141
+ for (const host of Object.keys(this.bigImgList)) {
142
+ for (const entry of this.bigImgList[host]) {
143
+ const timing = entry.timing;
144
+ const type = entry.type;
145
+ try {
146
+ const img = document.body.querySelector('img[src="' + timing.name + '"]');
147
+ this.bigImgCB({
148
+ msg: timing.name,
149
+ dur: timing.duration,
150
+ xpath: getxpath(img).xpath,
151
+ host,
152
+ type,
153
+ });
154
+ }
155
+ catch (e) {
156
+ console.error(e);
157
+ }
137
158
  }
138
159
  }
139
160
  });
140
161
  }
162
+ }
141
163
 
164
+ if (this.httpResCB) {
142
165
  // 发送http资源监控数据
143
- if (window.requestIdleCallback && this.httpResList.length && this.httpResCB) {
166
+ if (typeof window.requestIdleCallback === 'function' && Object.keys(this.httpResList).length) {
144
167
  window.requestIdleCallback(() => {
145
- for (const entry of this.httpResList) {
146
- try {
147
- const img = document.body.querySelector('[src="' + entry.name + '"]');
148
- this.httpResCB({
149
- msg: entry.name,
150
- xpath: getxpath(img).xpath,
151
- });
168
+ for (const host of Object.keys(this.httpResList)) {
169
+ for (const entry of this.httpResList[host]) {
170
+ const timing = entry.timing;
171
+ const type = entry.type;
172
+ try {
173
+ const img = document.body.querySelector('[src="' + timing.name + '"]');
174
+ this.httpResCB({
175
+ msg: timing.name,
176
+ dur: timing.duration,
177
+ xpath: getxpath(img).xpath,
178
+ host,
179
+ type,
180
+ });
181
+ }
182
+ catch (e) {
183
+ console.error(e);
184
+ }
152
185
  }
153
- catch (e) {
154
- console.error(e);
186
+ }
187
+ });
188
+ }
189
+ }
190
+
191
+ if (this.slowResCB) {
192
+ // 发送慢资源监控数据
193
+ if (typeof window.requestIdleCallback === 'function' && Object.keys(this.slowList).length) {
194
+ window.requestIdleCallback(() => {
195
+ for (const host of Object.keys(this.slowList)) {
196
+ for (const entry of this.slowList[host]) {
197
+ const timing = entry.timing;
198
+ const type = entry.type;
199
+ try {
200
+ const img = document.body.querySelector('[src="' + timing.name + '"]');
201
+
202
+ const info = getResTiming(timing);
203
+
204
+ this.slowResCB({
205
+ ...info,
206
+ dur: timing.duration,
207
+ msg: timing.name,
208
+ xpath: getxpath(img).xpath,
209
+ host,
210
+ type,
211
+ });
212
+ }
213
+ catch (e) {
214
+ console.error(e);
215
+ }
155
216
  }
156
217
  }
157
218
  });
158
219
  }
220
+ }
221
+ }
222
+
223
+ load() {
224
+ if (this.trigger !== 'load') {
225
+ return;
226
+ }
227
+ if (!this.check()) {
228
+ return;
229
+ }
230
+
231
+ setTimeout(() => {
232
+ this.report();
159
233
  }, 500);
160
234
  }
161
235
 
162
- private getUrlObj(url: string): UrlObj {
163
- try {
164
- const urlObj: UrlObj = new URL(url);
165
- const split = urlObj.pathname.split('.');
166
- urlObj.ext = split[split.length - 1];
167
- return urlObj;
236
+ leave() {
237
+ if (this.trigger !== 'leave') {
238
+ return;
168
239
  }
169
- catch (error) {
170
- return ({} as UrlObj);
240
+ this.report();
241
+ }
242
+
243
+ private push(type: string, list: any, timing: PerformanceResourceTiming, urlInfo: URLINFO) {
244
+ if (!ignorePath(urlInfo.pathname, this.resOption.ignorePaths || [])) {
245
+ list.push(timing);
246
+ this.pushWithHost(type, this.hostList, timing, urlInfo);
247
+ }
248
+
249
+ if (!ignorePath(urlInfo.pathname, this.slowOption.ignorePaths || [])) {
250
+ if (timing.duration > (this.slowOption.threshold as number)) {
251
+ this.pushWithHost(type, this.slowList, timing, urlInfo);
252
+ }
171
253
  }
254
+
172
255
  }
173
256
 
174
- private collectHttpResInHttps(timing: PerformanceResourceTiming) {
175
- if (location.protocol === 'https:' && timing.name.indexOf('http://') === 0) {
176
- this.httpResList.push(timing);
257
+ private pushWithHost(type: string, list: any, timing: PerformanceResourceTiming, urlInfo: URLINFO) {
258
+ const host = urlInfo.host;
259
+ if (!list[host]) {
260
+ list[host] = [] as ListItem[];
261
+ }
262
+ list[host].push({
263
+ timing,
264
+ type,
265
+ });
266
+ }
267
+
268
+ private collectHttpResInHttps(type: string, timing: PerformanceResourceTiming, urlInfo: URLINFO) {
269
+ if (location.protocol === 'https:'
270
+ && timing.name.indexOf('http://') === 0
271
+ && !ignorePath(urlInfo.pathname, this.httpResOption.ignorePaths || [])
272
+ ) {
273
+ this.pushWithHost(type, this.httpResList, timing, urlInfo);
177
274
  }
178
275
  }
179
276
 
180
277
  // 有些jsonp也属于script,这里只统计js后缀的文件
181
- private addScript(timing: PerformanceResourceTiming) {
182
- const {ext} = this.getUrlObj(timing.name);
183
- if (ext === 'js') {
278
+ private addScript(timing: PerformanceResourceTiming, urlInfo: URLINFO) {
279
+ if (urlInfo.ext === 'js') {
184
280
  if (timing.decodedBodySize !== 0) {
185
- this.jsList.push(timing);
281
+ this.push('js', this.jsList, timing, urlInfo);
186
282
  }
187
-
188
- this.collectHttpResInHttps(timing);
189
283
  }
190
284
  }
191
285
 
@@ -194,70 +288,67 @@ export default class Resource implements Module {
194
288
  // 2、加载背景图(图片不容易区分,有的没有明确后缀名)
195
289
  // 3、光标文件(后缀为.cur,这里也划分为图片)
196
290
  // (svg当做图片,前述已说明)
197
- private addCss(timing: PerformanceResourceTiming) {
198
- const {ext} = this.getUrlObj(timing.name);
199
- if (ext && FontsTypes.hasOwnProperty(ext)) {
200
- this.fontList.push(timing);
291
+ private addResFromCss(timing: PerformanceResourceTiming, urlInfo: URLINFO) {
292
+ if (urlInfo.ext && FontsTypes.hasOwnProperty(urlInfo.ext)) {
293
+ this.push('font', this.fontList, timing, urlInfo);
201
294
  }
202
295
  else {
203
- this.imgList.push(timing);
296
+ this.addImg(timing, urlInfo);
204
297
  }
205
298
  }
206
299
 
207
300
  // link一般加载css资源
208
301
  // 也可以通过preload可以预下载一些资源
209
302
  // 这里只统计js类型的preload
210
- private addLink(timing: PerformanceResourceTiming) {
211
- const {ext} = this.getUrlObj(timing.name);
212
- if (ext === 'css') {
213
- this.cssList.push(timing);
303
+ private addLink(timing: PerformanceResourceTiming, urlInfo: URLINFO) {
304
+ if (urlInfo.ext === 'css') {
305
+ this.push('css', this.cssList, timing, urlInfo);
214
306
  }
215
307
  // preload as script
216
- else if (ext === 'js') {
217
- this.jsList.push(timing);
308
+ else if (urlInfo.ext === 'js') {
309
+ this.push('js', this.jsList, timing, urlInfo);
218
310
  }
219
311
  }
220
312
 
221
- private addImg(timing: PerformanceResourceTiming) {
222
- const {pathname} = this.getUrlObj(timing.name);
223
- if (!IgnoreImgPath.hasOwnProperty(pathname)) {
224
- this.imgList.push(timing);
225
-
226
- // 识别出处理webp的url
227
- if (timing.name.indexOf('fm=') > -1 && timing.name.indexOf('webpstat=1') > -1) {
228
- this.imgTnList.push(timing);
229
- }
230
-
231
- // 大于102400=100K的采集
232
- if (timing.decodedBodySize > this.maxImgSize) {
233
- this.bigImgList.push(timing);
234
- }
313
+ private addImg(timing: PerformanceResourceTiming, urlInfo: URLINFO) {
314
+ this.push('img', this.imgList, timing, urlInfo);
235
315
 
236
- this.collectHttpResInHttps(timing);
316
+ // 大于指定size的图片采集
317
+ if (
318
+ timing.decodedBodySize > (this.bigImgOption.maxSize || 0)
319
+ && !ignorePath(urlInfo.pathname, this.bigImgOption.ignorePaths || [])
320
+ ) {
321
+ this.pushWithHost('img', this.bigImgList, timing, urlInfo);
237
322
  }
238
323
  }
239
324
 
240
325
  private handleTimings(list: PerformanceEntry[]) {
241
326
  const len = list.length;
242
327
  for (let i = 0; i < len; i++) {
328
+ const timing = list[i] as PerformanceResourceTiming;
329
+ const urlInfo = getUrlInfo(timing.name);
330
+
331
+ if (ignorePath(urlInfo.pathname, ignoreDefaultPaths)) {
332
+ continue;
333
+ }
243
334
  switch ((list[i] as any).initiatorType) {
244
335
  case 'script':
245
- this.addScript(list[i] as PerformanceResourceTiming);
336
+ this.addScript(timing, urlInfo);
246
337
  break;
247
338
  case 'css':
248
- this.addCss(list[i] as PerformanceResourceTiming);
339
+ this.addResFromCss(timing, urlInfo);
249
340
  break;
250
341
  case 'img':
251
- this.addImg(list[i] as PerformanceResourceTiming);
342
+ this.addImg(timing, urlInfo);
252
343
  break;
253
344
  case 'link':
254
- this.addLink(list[i] as PerformanceResourceTiming);
345
+ this.addLink(timing, urlInfo);
255
346
  break;
256
347
  case 'audio':
257
- this.collectHttpResInHttps(list[i] as PerformanceResourceTiming);
348
+ this.collectHttpResInHttps('audio', timing, urlInfo);
258
349
  break;
259
350
  case 'video':
260
- this.collectHttpResInHttps(list[i] as PerformanceResourceTiming);
351
+ this.collectHttpResInHttps('video', timing, urlInfo);
261
352
  break;
262
353
  default:
263
354
  break;
@@ -265,7 +356,7 @@ export default class Resource implements Module {
265
356
  }
266
357
  }
267
358
 
268
- private getNumAndSize(type: ResType) {
359
+ private getNumAndSize(type: string, list: PerformanceResourceTiming[]) {
269
360
  const obj: any = {};
270
361
  const num = type + 'Num';
271
362
  const size = type + 'Size';
@@ -276,7 +367,7 @@ export default class Resource implements Module {
276
367
  obj[size] = 0;
277
368
  obj[transferSize] = 0;
278
369
  let totalDurationTime = 0;
279
- (this as any)[type + 'List'].forEach((timing: PerformanceResourceTiming) => {
370
+ list.forEach((timing: PerformanceResourceTiming) => {
280
371
  obj[num]++;
281
372
  obj[size] += (timing.decodedBodySize / 1024);
282
373
  obj[transferSize] += (timing.transferSize / 1024);
@@ -295,7 +386,7 @@ export default class Resource implements Module {
295
386
  return obj;
296
387
  }
297
388
 
298
- private getMetric(): ResourceMetric | undefined {
389
+ private getMetric(): {metric: ResourceMetric, hostMetric: ResourceHostMetric} | undefined {
299
390
  // 原来代码
300
391
  const {0: mainPageTiming} = performance.getEntriesByType('navigation');
301
392
  const resourceTimings = performance.getEntriesByType('resource');
@@ -304,10 +395,10 @@ export default class Resource implements Module {
304
395
  this.handleTimings(resourceTimings);
305
396
 
306
397
  let metric: ResourceMetric = {
307
- ...this.getNumAndSize(ResType.JS),
308
- ...this.getNumAndSize(ResType.CSS),
309
- ...this.getNumAndSize(ResType.IMG),
310
- ...this.getNumAndSize(ResType.FONT),
398
+ ...this.getNumAndSize(ResType.JS, this.jsList),
399
+ ...this.getNumAndSize(ResType.CSS, this.cssList),
400
+ ...this.getNumAndSize(ResType.IMG, this.imgList),
401
+ ...this.getNumAndSize(ResType.FONT, this.fontList),
311
402
  };
312
403
 
313
404
  // 主文档大小
@@ -329,7 +420,13 @@ export default class Resource implements Module {
329
420
  + metric.imgTransferSize
330
421
  + metric.fontTransferSize);
331
422
 
332
- return metric;
423
+ const hostMetric: ResourceHostMetric = {};
424
+ for (const host of Object.keys(this.hostList)) {
425
+ const timings = this.hostList[host].map(item => item.timing);
426
+ hostMetric[host] = this.getNumAndSize('host', timings);
427
+ }
428
+
429
+ return {metric, hostMetric};
333
430
  }
334
431
  return;
335
432
  }