@tinkoff/perfume 0.0.0-stage → 1.0.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.
- package/LICENSE +21 -0
- package/README.md +523 -2
- package/dist/index.d.ts +256 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +708 -0
- package/dist/index.js.map +1 -0
- package/package.json +76 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,708 @@
|
|
|
1
|
+
import { onCLS, onFCP, onFID, onINP, onLCP, onTTFB } from "web-vitals/attribution";
|
|
2
|
+
//#region src/config.ts
|
|
3
|
+
const config = {
|
|
4
|
+
isResourceTiming: false,
|
|
5
|
+
isElementTiming: false,
|
|
6
|
+
maxTime: 3e4,
|
|
7
|
+
reportOptions: {}
|
|
8
|
+
};
|
|
9
|
+
//#endregion
|
|
10
|
+
//#region src/constants.ts
|
|
11
|
+
const W = typeof window !== "undefined" ? window : {};
|
|
12
|
+
const C = W.console;
|
|
13
|
+
const WN = W.navigator;
|
|
14
|
+
const WP = W.performance;
|
|
15
|
+
const getDM = () => WN.deviceMemory;
|
|
16
|
+
const getHC = () => WN.hardwareConcurrency;
|
|
17
|
+
const M = "mark.";
|
|
18
|
+
const S = "step.";
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/isSupported.ts
|
|
21
|
+
/**
|
|
22
|
+
* True if the browser supports the Navigation Timing API,
|
|
23
|
+
* User Timing API and the PerformanceObserver Interface.
|
|
24
|
+
* In Safari, the User Timing API (performance.mark()) is not available,
|
|
25
|
+
* so the DevTools timeline will not be annotated with marks.
|
|
26
|
+
* Support: developer.mozilla.org/en-US/docs/Web/API/Performance/mark
|
|
27
|
+
* Support: developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver
|
|
28
|
+
* Support: developer.mozilla.org/en-US/docs/Web/API/Performance/getEntriesByType
|
|
29
|
+
*/
|
|
30
|
+
const isPerformanceSupported = () => WP && !!WP.getEntriesByType && !!WP.now && !!WP.mark;
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region src/getNavigationTiming.ts
|
|
33
|
+
/**
|
|
34
|
+
* Navigation Timing API provides performance metrics for HTML documents.
|
|
35
|
+
* w3c.github.io/navigation-timing/
|
|
36
|
+
* developers.google.com/web/fundamentals/performance/navigation-and-resource-timing
|
|
37
|
+
*/
|
|
38
|
+
const getNavigationTiming = () => {
|
|
39
|
+
if (!isPerformanceSupported()) return {};
|
|
40
|
+
const n = WP.getEntriesByType("navigation")[0];
|
|
41
|
+
if (!n) return {};
|
|
42
|
+
const responseStart = n.responseStart;
|
|
43
|
+
const responseEnd = n.responseEnd;
|
|
44
|
+
return {
|
|
45
|
+
fetchTime: responseEnd - n.fetchStart,
|
|
46
|
+
workerTime: n.workerStart > 0 ? responseEnd - n.workerStart : 0,
|
|
47
|
+
totalTime: responseEnd - n.requestStart,
|
|
48
|
+
downloadTime: responseEnd - responseStart,
|
|
49
|
+
timeToFirstByte: responseStart - n.requestStart,
|
|
50
|
+
headerSize: n.transferSize - n.encodedBodySize || 0,
|
|
51
|
+
dnsLookupTime: n.domainLookupEnd - n.domainLookupStart,
|
|
52
|
+
redirectTime: n.redirectEnd - n.redirectStart
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
//#endregion
|
|
56
|
+
//#region src/getNetworkInformation.ts
|
|
57
|
+
let et = "4g";
|
|
58
|
+
let sd = false;
|
|
59
|
+
const getNetworkInformation = () => {
|
|
60
|
+
if ("connection" in WN) {
|
|
61
|
+
const dataConnection = WN.connection;
|
|
62
|
+
if (typeof dataConnection !== "object") return {};
|
|
63
|
+
et = dataConnection.effectiveType;
|
|
64
|
+
sd = !!dataConnection.saveData;
|
|
65
|
+
return {
|
|
66
|
+
downlink: dataConnection.downlink,
|
|
67
|
+
effectiveType: dataConnection.effectiveType,
|
|
68
|
+
rtt: dataConnection.rtt,
|
|
69
|
+
saveData: !!dataConnection.saveData
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return {};
|
|
73
|
+
};
|
|
74
|
+
//#endregion
|
|
75
|
+
//#region src/metrics.ts
|
|
76
|
+
const metrics = {};
|
|
77
|
+
const fcp = { value: 0 };
|
|
78
|
+
const rt = { value: {
|
|
79
|
+
beacon: 0,
|
|
80
|
+
css: 0,
|
|
81
|
+
fetch: 0,
|
|
82
|
+
img: 0,
|
|
83
|
+
other: 0,
|
|
84
|
+
script: 0,
|
|
85
|
+
total: 0,
|
|
86
|
+
xmlhttprequest: 0
|
|
87
|
+
} };
|
|
88
|
+
const tbt = { value: 0 };
|
|
89
|
+
const ntbt = { value: 0 };
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/observeInstances.ts
|
|
92
|
+
const perfObservers = {};
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region src/onVisibilityChange.ts
|
|
95
|
+
const visibility = {
|
|
96
|
+
isHidden: false,
|
|
97
|
+
didChange: false
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* From visibilitychange listener it saves only when
|
|
101
|
+
* the page gets hidden, because it's important to not
|
|
102
|
+
* use the wrong "hidden" value when send timing or logging.
|
|
103
|
+
*/
|
|
104
|
+
const didVisibilityChange = function() {
|
|
105
|
+
visibility.isHidden = false;
|
|
106
|
+
if (document.hidden) {
|
|
107
|
+
visibility.isHidden = document.hidden;
|
|
108
|
+
visibility.didChange = true;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
//#endregion
|
|
112
|
+
//#region src/performanceObserver.ts
|
|
113
|
+
/**
|
|
114
|
+
* PerformanceObserver subscribes to performance events as they happen
|
|
115
|
+
* and respond to them asynchronously.
|
|
116
|
+
*/
|
|
117
|
+
const po = (eventType, cb) => {
|
|
118
|
+
try {
|
|
119
|
+
const perfObserver = new PerformanceObserver((entryList) => {
|
|
120
|
+
cb(entryList.getEntries());
|
|
121
|
+
});
|
|
122
|
+
perfObserver.observe({
|
|
123
|
+
type: eventType,
|
|
124
|
+
buffered: true
|
|
125
|
+
});
|
|
126
|
+
return perfObserver;
|
|
127
|
+
} catch (e) {
|
|
128
|
+
C.warn("Perfume.js:", e);
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
};
|
|
132
|
+
//#endregion
|
|
133
|
+
//#region src/isLowEnd.ts
|
|
134
|
+
const getIsLowEndDevice = () => {
|
|
135
|
+
if (getHC() && getHC() <= 4) return true;
|
|
136
|
+
if (getDM() && getDM() <= 4) return true;
|
|
137
|
+
return false;
|
|
138
|
+
};
|
|
139
|
+
const getIsLowEndExperience = (et, sd) => {
|
|
140
|
+
switch (et) {
|
|
141
|
+
case "slow-2g": return true;
|
|
142
|
+
case "2g": return true;
|
|
143
|
+
case "3g": return true;
|
|
144
|
+
default: return getIsLowEndDevice() || sd;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
//#endregion
|
|
148
|
+
//#region src/getNavigatorInfo.ts
|
|
149
|
+
/**
|
|
150
|
+
* Information coming from window.navigator:
|
|
151
|
+
* 1. Device Memory
|
|
152
|
+
* 2. Hardware Concurency
|
|
153
|
+
* 3. Status of the service worker:
|
|
154
|
+
* - controlled: a service worker is controlling the page
|
|
155
|
+
* - supported: the browser supports service worker
|
|
156
|
+
* - unsupported: the user's browser does not support service worker
|
|
157
|
+
*/
|
|
158
|
+
const getNavigatorInfo = () => {
|
|
159
|
+
if (WN) return {
|
|
160
|
+
deviceMemory: getDM() || 0,
|
|
161
|
+
hardwareConcurrency: getHC() || 0,
|
|
162
|
+
serviceWorkerStatus: "serviceWorker" in WN ? WN.serviceWorker.controller ? "controlled" : "supported" : "unsupported",
|
|
163
|
+
isLowEndDevice: getIsLowEndDevice(),
|
|
164
|
+
isLowEndExperience: getIsLowEndExperience(et, sd)
|
|
165
|
+
};
|
|
166
|
+
return {};
|
|
167
|
+
};
|
|
168
|
+
//#endregion
|
|
169
|
+
//#region src/utils.ts
|
|
170
|
+
const roundByFour = (num) => parseFloat(num.toFixed(4));
|
|
171
|
+
const convertToKB = (bytes) => {
|
|
172
|
+
if (typeof bytes !== "number") return null;
|
|
173
|
+
return roundByFour(bytes / Math.pow(1024, 2));
|
|
174
|
+
};
|
|
175
|
+
/**
|
|
176
|
+
* PushTask to requestIdleCallback
|
|
177
|
+
*/
|
|
178
|
+
const pushTask = (cb) => {
|
|
179
|
+
if ("requestIdleCallback" in W) W.requestIdleCallback(cb, { timeout: 3e3 });
|
|
180
|
+
else cb();
|
|
181
|
+
};
|
|
182
|
+
//#endregion
|
|
183
|
+
//#region src/reportPerf.ts
|
|
184
|
+
/**
|
|
185
|
+
* Sends the User timing measure to analyticsTracker
|
|
186
|
+
*/
|
|
187
|
+
const reportPerf = (measureName, data, rating, attribution, navigationType) => {
|
|
188
|
+
const reportTask = () => {
|
|
189
|
+
if (!config.analyticsTracker) return;
|
|
190
|
+
if (visibility.isHidden && !["CLS", "INP"].includes(measureName)) return;
|
|
191
|
+
config.analyticsTracker({
|
|
192
|
+
attribution,
|
|
193
|
+
metricName: measureName,
|
|
194
|
+
data,
|
|
195
|
+
navigatorInformation: getNavigatorInfo(),
|
|
196
|
+
rating,
|
|
197
|
+
navigationType
|
|
198
|
+
});
|
|
199
|
+
};
|
|
200
|
+
if (["CLS", "INP"].includes(measureName)) reportTask();
|
|
201
|
+
else pushTask(reportTask);
|
|
202
|
+
};
|
|
203
|
+
//#endregion
|
|
204
|
+
//#region src/totalBlockingTime.ts
|
|
205
|
+
const initTotalBlockingTime = (performanceEntries) => {
|
|
206
|
+
performanceEntries.forEach((entry) => {
|
|
207
|
+
if (entry.name !== "self" || entry.startTime < fcp.value) return;
|
|
208
|
+
const blockingTime = entry.duration - 50;
|
|
209
|
+
if (blockingTime > 0) {
|
|
210
|
+
tbt.value += blockingTime;
|
|
211
|
+
ntbt.value += blockingTime;
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
};
|
|
215
|
+
//#endregion
|
|
216
|
+
//#region src/types.ts
|
|
217
|
+
let IThresholdTier = /* @__PURE__ */ function(IThresholdTier) {
|
|
218
|
+
IThresholdTier["instant"] = "instant";
|
|
219
|
+
IThresholdTier["quick"] = "quick";
|
|
220
|
+
IThresholdTier["moderate"] = "moderate";
|
|
221
|
+
IThresholdTier["slow"] = "slow";
|
|
222
|
+
IThresholdTier["unavoidable"] = "unavoidable";
|
|
223
|
+
return IThresholdTier;
|
|
224
|
+
}({});
|
|
225
|
+
//#endregion
|
|
226
|
+
//#region src/vitalsScore.ts
|
|
227
|
+
const rtScore = [100, 200];
|
|
228
|
+
const tbtScore = [200, 600];
|
|
229
|
+
const ntbtScore = [200, 600];
|
|
230
|
+
const STEP_THRESHOLDS = {
|
|
231
|
+
["instant"]: {
|
|
232
|
+
vitalsThresholds: [100, 200],
|
|
233
|
+
maxOutlierThreshold: 1e4
|
|
234
|
+
},
|
|
235
|
+
["quick"]: {
|
|
236
|
+
vitalsThresholds: [200, 500],
|
|
237
|
+
maxOutlierThreshold: 1e4
|
|
238
|
+
},
|
|
239
|
+
["moderate"]: {
|
|
240
|
+
vitalsThresholds: [500, 1e3],
|
|
241
|
+
maxOutlierThreshold: 1e4
|
|
242
|
+
},
|
|
243
|
+
["slow"]: {
|
|
244
|
+
vitalsThresholds: [1e3, 2e3],
|
|
245
|
+
maxOutlierThreshold: 1e4
|
|
246
|
+
},
|
|
247
|
+
["unavoidable"]: {
|
|
248
|
+
vitalsThresholds: [2e3, 5e3],
|
|
249
|
+
maxOutlierThreshold: 2e4
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
const webVitalsScore = {
|
|
253
|
+
RT: rtScore,
|
|
254
|
+
TBT: tbtScore,
|
|
255
|
+
NTBT: ntbtScore
|
|
256
|
+
};
|
|
257
|
+
const getRating = (value, vitalsThresholds) => {
|
|
258
|
+
if (value <= vitalsThresholds[0]) return "good";
|
|
259
|
+
return value <= vitalsThresholds[1] ? "needsImprovement" : "poor";
|
|
260
|
+
};
|
|
261
|
+
const getVitalsScore = (measureName, value) => {
|
|
262
|
+
if (!webVitalsScore[measureName]) return null;
|
|
263
|
+
if (value <= webVitalsScore[measureName][0]) return "good";
|
|
264
|
+
return value <= webVitalsScore[measureName][1] ? "needsImprovement" : "poor";
|
|
265
|
+
};
|
|
266
|
+
//#endregion
|
|
267
|
+
//#region src/log.ts
|
|
268
|
+
const logData = (measureName, metric, attribution) => {
|
|
269
|
+
Object.keys(metric).forEach((key) => {
|
|
270
|
+
if (typeof metric[key] === "number") metric[key] = roundByFour(metric[key]);
|
|
271
|
+
});
|
|
272
|
+
reportPerf(measureName, metric, null, attribution || {});
|
|
273
|
+
};
|
|
274
|
+
/**
|
|
275
|
+
* Dispatches the metric duration into internal logs
|
|
276
|
+
* and the external time tracking service.
|
|
277
|
+
*/
|
|
278
|
+
const logMetric = ({ attribution, name, rating, value, navigationType }) => {
|
|
279
|
+
if (name === "FCP") fcp.value = value;
|
|
280
|
+
if (["FCP", "LCP"].includes(name) && !perfObservers[0]) perfObservers[0] = po("longtask", initTotalBlockingTime);
|
|
281
|
+
if ("FID" === name) setTimeout(() => {
|
|
282
|
+
if (visibility.didChange) return;
|
|
283
|
+
logMetric({
|
|
284
|
+
attribution,
|
|
285
|
+
name: "TBT",
|
|
286
|
+
rating: getVitalsScore("TBT", tbt.value),
|
|
287
|
+
value: tbt.value,
|
|
288
|
+
navigationType
|
|
289
|
+
});
|
|
290
|
+
logData("dataConsumption", rt.value);
|
|
291
|
+
}, 1e4);
|
|
292
|
+
const duration2Decimal = roundByFour(value);
|
|
293
|
+
if (duration2Decimal <= config.maxTime && duration2Decimal >= 0) reportPerf(name, duration2Decimal, rating, attribution, navigationType);
|
|
294
|
+
};
|
|
295
|
+
//#endregion
|
|
296
|
+
//#region src/element-timing.ts
|
|
297
|
+
const initElementTiming = (performanceEntries) => {
|
|
298
|
+
performanceEntries.forEach((entry) => {
|
|
299
|
+
if (entry.identifier) logMetric({
|
|
300
|
+
attribution: { identifier: entry.identifier },
|
|
301
|
+
name: "ET",
|
|
302
|
+
rating: null,
|
|
303
|
+
value: entry.startTime
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
};
|
|
307
|
+
//#endregion
|
|
308
|
+
//#region src/resourceTiming.ts
|
|
309
|
+
const initResourceTiming = (performanceEntries) => {
|
|
310
|
+
performanceEntries.forEach((entry) => {
|
|
311
|
+
if (config.isResourceTiming) logData("resourceTiming", entry);
|
|
312
|
+
if (entry.decodedBodySize && entry.initiatorType) {
|
|
313
|
+
const bodySize = entry.decodedBodySize / 1e3;
|
|
314
|
+
rt.value[entry.initiatorType] += bodySize;
|
|
315
|
+
rt.value.total += bodySize;
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
};
|
|
319
|
+
//#endregion
|
|
320
|
+
//#region src/observe.ts
|
|
321
|
+
const initPerformanceObserver = () => {
|
|
322
|
+
onTTFB((report) => {
|
|
323
|
+
if (report.value > 0) logMetric(report);
|
|
324
|
+
}, config.reportOptions.ttfb);
|
|
325
|
+
onCLS((report) => logMetric(report), config.reportOptions.cls);
|
|
326
|
+
onFCP((report) => logMetric(report), config.reportOptions.fcp);
|
|
327
|
+
onFID((report) => logMetric(report), config.reportOptions.fid);
|
|
328
|
+
onLCP((report) => logMetric(report), config.reportOptions.lcp);
|
|
329
|
+
onINP((report) => logMetric(report), config.reportOptions.inp);
|
|
330
|
+
if (config.isResourceTiming) po("resource", initResourceTiming);
|
|
331
|
+
if (config.isElementTiming) po("element", initElementTiming);
|
|
332
|
+
};
|
|
333
|
+
//#endregion
|
|
334
|
+
//#region src/storageEstimate.ts
|
|
335
|
+
/**
|
|
336
|
+
* The estimate() method of the StorageManager interface asks the Storage Manager
|
|
337
|
+
* for how much storage the app takes up (usage),
|
|
338
|
+
* and how much space is available (quota).
|
|
339
|
+
*/
|
|
340
|
+
const reportStorageEstimate = (storageInfo) => {
|
|
341
|
+
const estimateUsageDetails = "usageDetails" in storageInfo ? storageInfo.usageDetails : {};
|
|
342
|
+
logData("storageEstimate", {
|
|
343
|
+
quota: convertToKB(storageInfo.quota),
|
|
344
|
+
usage: convertToKB(storageInfo.usage),
|
|
345
|
+
caches: convertToKB(estimateUsageDetails.caches),
|
|
346
|
+
indexedDB: convertToKB(estimateUsageDetails.indexedDB),
|
|
347
|
+
serviceWorker: convertToKB(estimateUsageDetails.serviceWorkerRegistrations)
|
|
348
|
+
});
|
|
349
|
+
};
|
|
350
|
+
//#endregion
|
|
351
|
+
//#region src/steps/steps.ts
|
|
352
|
+
const steps = {
|
|
353
|
+
finalMarkToStepsMap: {},
|
|
354
|
+
startMarkToStepsMap: {},
|
|
355
|
+
active: {},
|
|
356
|
+
/**
|
|
357
|
+
An internal data structure to represent the state of active steps.
|
|
358
|
+
In this data structure, the keys represent the navgiation index, starting at 0 -
|
|
359
|
+
i.e - the initial app launch it's index 0, the first navigation is index 1 and so one.
|
|
360
|
+
The value of each navigation index is a Set of all the first marks for the currently active steps.
|
|
361
|
+
When a start mark is hit - it will be added to this state.
|
|
362
|
+
When a corresponding end mark is hit, the start mark previously added will be removed from this navigation state
|
|
363
|
+
Examples:
|
|
364
|
+
Example of the state when home is loading:
|
|
365
|
+
{
|
|
366
|
+
[0]: ['start_navigate_to_home_screen'] // load home - empty array since home has loaded
|
|
367
|
+
}
|
|
368
|
+
Example of the state during the navigation from home to assets
|
|
369
|
+
{
|
|
370
|
+
[0]: [] // load home - empty array since home has loaded
|
|
371
|
+
[1]: ['start_navigate_to_trade_screen']
|
|
372
|
+
}
|
|
373
|
+
*/
|
|
374
|
+
navigationSteps: {}
|
|
375
|
+
};
|
|
376
|
+
/**
|
|
377
|
+
* This method allows to add new steps by passing the start mark
|
|
378
|
+
*/
|
|
379
|
+
const addActiveSteps = (startMark) => {
|
|
380
|
+
var _steps$startMarkToSte;
|
|
381
|
+
const newSteps = (_steps$startMarkToSte = steps.startMarkToStepsMap[startMark]) !== null && _steps$startMarkToSte !== void 0 ? _steps$startMarkToSte : [];
|
|
382
|
+
Object.keys(newSteps).forEach((step) => {
|
|
383
|
+
if (steps.active[step]) return;
|
|
384
|
+
steps.active[step] = true;
|
|
385
|
+
});
|
|
386
|
+
};
|
|
387
|
+
const resetNavigationSteps = () => {
|
|
388
|
+
steps.navigationSteps = {};
|
|
389
|
+
};
|
|
390
|
+
/**
|
|
391
|
+
* Removes one step from active steps
|
|
392
|
+
*/
|
|
393
|
+
const removeActiveStep = (step) => {
|
|
394
|
+
delete steps.active[step];
|
|
395
|
+
};
|
|
396
|
+
const resetActiveSteps = () => {
|
|
397
|
+
steps.active = {};
|
|
398
|
+
};
|
|
399
|
+
const resetSteps = () => {
|
|
400
|
+
steps.startMarkToStepsMap = {};
|
|
401
|
+
steps.finalMarkToStepsMap = {};
|
|
402
|
+
resetActiveSteps();
|
|
403
|
+
resetNavigationSteps();
|
|
404
|
+
};
|
|
405
|
+
const getNavigationState = () => steps.navigationSteps;
|
|
406
|
+
const getStepsFromNavigation = (navIndex) => {
|
|
407
|
+
var _navigationSteps$navI;
|
|
408
|
+
return (_navigationSteps$navI = getNavigationState()[navIndex]) !== null && _navigationSteps$navI !== void 0 ? _navigationSteps$navI : {};
|
|
409
|
+
};
|
|
410
|
+
//#endregion
|
|
411
|
+
//#region src/steps/setStepsMap.ts
|
|
412
|
+
const setStepsMap = () => {
|
|
413
|
+
if (!config.steps) return;
|
|
414
|
+
resetSteps();
|
|
415
|
+
Object.entries(config.steps).forEach(([step, { marks }]) => {
|
|
416
|
+
var _steps$startMarkToSte;
|
|
417
|
+
const startMark = marks[0];
|
|
418
|
+
const endMark = marks[1];
|
|
419
|
+
const currentStartMarks = (_steps$startMarkToSte = steps.startMarkToStepsMap[startMark]) !== null && _steps$startMarkToSte !== void 0 ? _steps$startMarkToSte : {};
|
|
420
|
+
currentStartMarks[step] = true;
|
|
421
|
+
steps.startMarkToStepsMap[startMark] = currentStartMarks;
|
|
422
|
+
if (!steps.finalMarkToStepsMap[endMark]) steps.finalMarkToStepsMap[endMark] = { [startMark]: [step] };
|
|
423
|
+
else {
|
|
424
|
+
const currentSteps = steps.finalMarkToStepsMap[endMark][startMark] || [];
|
|
425
|
+
currentSteps.push(step);
|
|
426
|
+
steps.finalMarkToStepsMap[endMark][startMark] = currentSteps;
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
};
|
|
430
|
+
//#endregion
|
|
431
|
+
//#region src/initPerfume.ts
|
|
432
|
+
/**
|
|
433
|
+
* Perfume.js v9.4.1 (http://zizzamia.github.io/perfume)
|
|
434
|
+
* Copyright 2022 Leonardo Zizzamia (https://github.com/Zizzamia/perfume.js/graphs/contributors)
|
|
435
|
+
* Licensed under MIT (https://github.com/Zizzamia/perfume.js/blob/master/LICENSE)
|
|
436
|
+
*
|
|
437
|
+
* @license
|
|
438
|
+
*/
|
|
439
|
+
const initPerfume = (options = {}) => {
|
|
440
|
+
config.analyticsTracker = options.analyticsTracker;
|
|
441
|
+
config.isResourceTiming = !!options.resourceTiming;
|
|
442
|
+
config.isElementTiming = !!options.elementTiming;
|
|
443
|
+
config.maxTime = options.maxMeasureTime || config.maxTime;
|
|
444
|
+
config.reportOptions = options.reportOptions || config.reportOptions;
|
|
445
|
+
config.steps = options.steps;
|
|
446
|
+
config.onMarkStep = options.onMarkStep;
|
|
447
|
+
if (!isPerformanceSupported()) return;
|
|
448
|
+
if ("PerformanceObserver" in W) initPerformanceObserver();
|
|
449
|
+
if (typeof document.hidden !== "undefined") document.addEventListener("visibilitychange", didVisibilityChange);
|
|
450
|
+
const navigationTiming = getNavigationTiming();
|
|
451
|
+
logData("navigationTiming", navigationTiming);
|
|
452
|
+
if (navigationTiming.redirectTime) logMetric({
|
|
453
|
+
attribution: {},
|
|
454
|
+
name: `RT`,
|
|
455
|
+
rating: getVitalsScore("RT", navigationTiming.redirectTime),
|
|
456
|
+
value: navigationTiming.redirectTime
|
|
457
|
+
});
|
|
458
|
+
logData("networkInformation", getNetworkInformation());
|
|
459
|
+
if (WN && WN.storage && typeof WN.storage.estimate === "function") WN.storage.estimate().then(reportStorageEstimate).catch(() => {});
|
|
460
|
+
if (config.steps) setStepsMap();
|
|
461
|
+
};
|
|
462
|
+
//#endregion
|
|
463
|
+
//#region src/measure.ts
|
|
464
|
+
/**
|
|
465
|
+
* Get the duration of the timing metric or -1
|
|
466
|
+
* if there a measurement has not been made by the User Timing API
|
|
467
|
+
*/
|
|
468
|
+
const performanceMeasure = (measureName) => {
|
|
469
|
+
WP.measure(measureName, `mark_${measureName}_start`, `mark_${measureName}_end`);
|
|
470
|
+
const entry = WP.getEntriesByName(measureName).pop();
|
|
471
|
+
if (entry && entry.entryType === "measure") return entry.duration;
|
|
472
|
+
return -1;
|
|
473
|
+
};
|
|
474
|
+
//#endregion
|
|
475
|
+
//#region src/steps/measureStep.ts
|
|
476
|
+
const measureStep = (step, startMark, endMark) => {
|
|
477
|
+
const stepMetricName = S + step;
|
|
478
|
+
const startMarkExists = WP.getEntriesByName(M + startMark).length > 0;
|
|
479
|
+
if (!(WP.getEntriesByName("mark." + endMark).length > 0) || !startMarkExists || !config.steps || !config.steps[step]) return;
|
|
480
|
+
const { maxOutlierThreshold, vitalsThresholds } = STEP_THRESHOLDS[config.steps[step].threshold];
|
|
481
|
+
const stepMeasure = WP.measure(stepMetricName, M + startMark, M + endMark);
|
|
482
|
+
if (!stepMeasure) return;
|
|
483
|
+
const { duration } = stepMeasure;
|
|
484
|
+
if (duration <= maxOutlierThreshold) {
|
|
485
|
+
const score = getRating(duration, vitalsThresholds);
|
|
486
|
+
if (duration >= 0) {
|
|
487
|
+
reportPerf("userJourneyStep", duration, score, { stepName: step }, void 0);
|
|
488
|
+
WP.measure(`step.${step}_vitals_${score}`, {
|
|
489
|
+
start: stepMeasure.startTime + stepMeasure.duration,
|
|
490
|
+
end: stepMeasure.startTime + stepMeasure.duration,
|
|
491
|
+
detail: {
|
|
492
|
+
type: "stepVital",
|
|
493
|
+
duration
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
//#endregion
|
|
500
|
+
//#region src/steps/navigationSteps.ts
|
|
501
|
+
/**
|
|
502
|
+
* We only report the active steps in the last 2 navigation step to prevent
|
|
503
|
+
* Convert the steps.navigationSteps to an array of active steps.
|
|
504
|
+
* "stale steps" from being reported.
|
|
505
|
+
*
|
|
506
|
+
* "stale steps" are defined as steps where the start mark was executed but the
|
|
507
|
+
* corresponding end mark was never executed - thus leaving it as a "stale step"
|
|
508
|
+
*/
|
|
509
|
+
const getActiveStepsFromNavigationSteps = () => {
|
|
510
|
+
const navigationSteps = getNavigationState();
|
|
511
|
+
const { startMarkToStepsMap } = steps;
|
|
512
|
+
const numberOfSteps = Object.keys(navigationSteps).length;
|
|
513
|
+
if (numberOfSteps === 0) return {};
|
|
514
|
+
const activeSteps = {};
|
|
515
|
+
const currentNavIndex = numberOfSteps - 1;
|
|
516
|
+
const currentNavStep = getStepsFromNavigation(currentNavIndex);
|
|
517
|
+
Object.keys(currentNavStep).forEach((startMark) => {
|
|
518
|
+
var _startMarkToStepsMap$;
|
|
519
|
+
const ongoingSteps = (_startMarkToStepsMap$ = startMarkToStepsMap[startMark]) !== null && _startMarkToStepsMap$ !== void 0 ? _startMarkToStepsMap$ : [];
|
|
520
|
+
Object.keys(ongoingSteps).forEach((step) => {
|
|
521
|
+
activeSteps[step] = true;
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
if (numberOfSteps > 1) {
|
|
525
|
+
const prevNavStep = getStepsFromNavigation(currentNavIndex - 1);
|
|
526
|
+
Object.keys(prevNavStep).forEach((startMark) => {
|
|
527
|
+
var _startMarkToStepsMap$2;
|
|
528
|
+
const prevNavSteps = (_startMarkToStepsMap$2 = startMarkToStepsMap[startMark]) !== null && _startMarkToStepsMap$2 !== void 0 ? _startMarkToStepsMap$2 : [];
|
|
529
|
+
Object.keys(prevNavSteps).forEach((step) => {
|
|
530
|
+
activeSteps[step] = true;
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
return activeSteps;
|
|
535
|
+
};
|
|
536
|
+
const recordStartMark = (startMark) => {
|
|
537
|
+
const { navigationSteps } = steps;
|
|
538
|
+
const numberOfSteps = Object.keys(navigationSteps).length;
|
|
539
|
+
const lastNavIndex = (numberOfSteps > 0 ? numberOfSteps : 1) - 1;
|
|
540
|
+
const newCurrentMark = getStepsFromNavigation(lastNavIndex) || [];
|
|
541
|
+
newCurrentMark[startMark] = true;
|
|
542
|
+
navigationSteps[lastNavIndex] = newCurrentMark;
|
|
543
|
+
};
|
|
544
|
+
const recordEndMark = (endMark) => {
|
|
545
|
+
const { navigationSteps, finalMarkToStepsMap } = steps;
|
|
546
|
+
const navigationLength = Object.keys(navigationSteps).length;
|
|
547
|
+
if (navigationLength === 0) return;
|
|
548
|
+
const currentNavIndex = navigationLength - 1;
|
|
549
|
+
const ongoingNavStep = getStepsFromNavigation(currentNavIndex);
|
|
550
|
+
if (!ongoingNavStep || !finalMarkToStepsMap[endMark]) return;
|
|
551
|
+
const endMarkMap = finalMarkToStepsMap[endMark];
|
|
552
|
+
if (!endMarkMap) return;
|
|
553
|
+
/**
|
|
554
|
+
* loop through all steps where end mark is present and check if their start mark is present
|
|
555
|
+
* in the navigation state. If so, remove it to signal that the step has been completed
|
|
556
|
+
*/
|
|
557
|
+
Object.keys(endMarkMap).forEach((startMark) => {
|
|
558
|
+
if (ongoingNavStep[startMark]) {
|
|
559
|
+
const currentNavStep = getStepsFromNavigation(currentNavIndex) || {};
|
|
560
|
+
currentNavStep[startMark] = false;
|
|
561
|
+
navigationSteps[currentNavIndex] = currentNavStep;
|
|
562
|
+
}
|
|
563
|
+
if (navigationLength > 1) {
|
|
564
|
+
const lastNavIndex = currentNavIndex - 1;
|
|
565
|
+
const lastNavStep = getStepsFromNavigation(lastNavIndex);
|
|
566
|
+
if (lastNavStep[startMark]) {
|
|
567
|
+
lastNavStep[startMark] = false;
|
|
568
|
+
navigationSteps[lastNavIndex] = lastNavStep;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
};
|
|
573
|
+
/**
|
|
574
|
+
* An API consumers must use to update internal state based on page/screen navigations.
|
|
575
|
+
* Internal state must be updated when the following are true
|
|
576
|
+
* 1. Journey marks are hit
|
|
577
|
+
* 2. Page navigations occur
|
|
578
|
+
* This function ensures the 2nd case is covered to help prevent "stale"
|
|
579
|
+
* steps that don't hit their final mark because the users navigates
|
|
580
|
+
* backwards in journey.
|
|
581
|
+
*/
|
|
582
|
+
const trackUJNavigation = () => {
|
|
583
|
+
var _config$onMarkStep;
|
|
584
|
+
const navigationLength = Object.keys(steps.navigationSteps).length;
|
|
585
|
+
steps.navigationSteps[navigationLength] = {};
|
|
586
|
+
const activeSteps = getActiveStepsFromNavigationSteps();
|
|
587
|
+
(_config$onMarkStep = config.onMarkStep) === null || _config$onMarkStep === void 0 || _config$onMarkStep.call(config, "", Object.keys(activeSteps));
|
|
588
|
+
};
|
|
589
|
+
//#endregion
|
|
590
|
+
//#region src/steps/measureSteps.ts
|
|
591
|
+
const measureSteps = (mark) => {
|
|
592
|
+
var _config$onMarkStep;
|
|
593
|
+
if (steps.finalMarkToStepsMap[mark]) {
|
|
594
|
+
recordEndMark(mark);
|
|
595
|
+
const finalSteps = steps.finalMarkToStepsMap[mark];
|
|
596
|
+
Object.keys(finalSteps).forEach((startMark) => {
|
|
597
|
+
const possibleSteps = finalSteps[startMark];
|
|
598
|
+
possibleSteps.forEach(removeActiveStep);
|
|
599
|
+
Promise.all(possibleSteps.map(async (step) => {
|
|
600
|
+
await measureStep(step, startMark, mark);
|
|
601
|
+
})).catch(() => {});
|
|
602
|
+
});
|
|
603
|
+
} else {
|
|
604
|
+
recordStartMark(mark);
|
|
605
|
+
addActiveSteps(mark);
|
|
606
|
+
}
|
|
607
|
+
const navigationBasedActiveSteps = getActiveStepsFromNavigationSteps();
|
|
608
|
+
(_config$onMarkStep = config.onMarkStep) === null || _config$onMarkStep === void 0 || _config$onMarkStep.call(config, mark, Object.keys(navigationBasedActiveSteps));
|
|
609
|
+
};
|
|
610
|
+
//#endregion
|
|
611
|
+
//#region src/steps/markStep.ts
|
|
612
|
+
/**
|
|
613
|
+
* Function which creates a step mark with a name generated
|
|
614
|
+
* from the provided mark when called.
|
|
615
|
+
*
|
|
616
|
+
* The generated mark name has the following format:
|
|
617
|
+
* `mark.${mark}`
|
|
618
|
+
*
|
|
619
|
+
*/
|
|
620
|
+
const markStep = (mark) => {
|
|
621
|
+
if (!isPerformanceSupported()) return;
|
|
622
|
+
WP.mark(M + mark);
|
|
623
|
+
measureSteps(mark);
|
|
624
|
+
};
|
|
625
|
+
/**
|
|
626
|
+
* Start performance measurement
|
|
627
|
+
*/
|
|
628
|
+
const start = (markName) => {
|
|
629
|
+
if (!isPerformanceSupported() || metrics[markName]) return;
|
|
630
|
+
metrics[markName] = true;
|
|
631
|
+
WP.mark(`mark_${markName}_start`);
|
|
632
|
+
};
|
|
633
|
+
/**
|
|
634
|
+
* End performance measurement
|
|
635
|
+
*/
|
|
636
|
+
const end = (markName, customProperties = {}, doLogData = true) => {
|
|
637
|
+
if (!isPerformanceSupported() || !metrics[markName]) return;
|
|
638
|
+
WP.mark(`mark_${markName}_end`);
|
|
639
|
+
delete metrics[markName];
|
|
640
|
+
const measure = performanceMeasure(markName);
|
|
641
|
+
if (doLogData) logData(markName, roundByFour(measure), customProperties);
|
|
642
|
+
};
|
|
643
|
+
/**
|
|
644
|
+
* Removes the named mark from the browser's performance entry buffer.
|
|
645
|
+
*/
|
|
646
|
+
const clear = (markName) => {
|
|
647
|
+
delete metrics[markName];
|
|
648
|
+
if (!WP.clearMarks) return;
|
|
649
|
+
WP.clearMarks(`mark_${markName}_start`);
|
|
650
|
+
WP.clearMarks(`mark_${markName}_end`);
|
|
651
|
+
};
|
|
652
|
+
//#endregion
|
|
653
|
+
//#region src/steps/markStepOnce.ts
|
|
654
|
+
/**
|
|
655
|
+
* Function which creates a step mark with a name generated
|
|
656
|
+
* from the provided mark if a mark with the same name does not
|
|
657
|
+
* already exist when called.
|
|
658
|
+
*
|
|
659
|
+
* The generated mark name has the following format:
|
|
660
|
+
* `mark.${mark}`
|
|
661
|
+
*
|
|
662
|
+
*/
|
|
663
|
+
const markStepOnce = (mark) => {
|
|
664
|
+
if (WP.getEntriesByName("mark." + mark).length === 0) {
|
|
665
|
+
WP.mark(M + mark);
|
|
666
|
+
measureSteps(mark);
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
//#endregion
|
|
670
|
+
//#region src/markNTBT.ts
|
|
671
|
+
let ntbtTimeoutID = 0;
|
|
672
|
+
/**
|
|
673
|
+
* NTBT = Navigation Total Blocking Time
|
|
674
|
+
*
|
|
675
|
+
* This metric measures the amount of time the application may be blocked
|
|
676
|
+
* from processing code during the 2s window after a user navigates
|
|
677
|
+
* from page A to page B.
|
|
678
|
+
*
|
|
679
|
+
* Because this library is navigation agnostic, we have this method
|
|
680
|
+
* to mark when the navigation starts.
|
|
681
|
+
*
|
|
682
|
+
* The NTBT metric is the summation of the blocking time of all long tasks
|
|
683
|
+
* in the 2s window after this method is invoked.
|
|
684
|
+
*
|
|
685
|
+
* If this method is called before the 2s window ends; it will trigger a new
|
|
686
|
+
* NTBT measurement and interrupt the previous one.
|
|
687
|
+
*
|
|
688
|
+
* Credit: Thank you Steven Lam for helping with this!
|
|
689
|
+
*/
|
|
690
|
+
const markNTBT = () => {
|
|
691
|
+
start("ntbt");
|
|
692
|
+
ntbt.value = 0;
|
|
693
|
+
clearTimeout(ntbtTimeoutID);
|
|
694
|
+
ntbtTimeoutID = setTimeout(() => {
|
|
695
|
+
end("ntbt", {}, false);
|
|
696
|
+
logMetric({
|
|
697
|
+
attribution: {},
|
|
698
|
+
name: `NTBT`,
|
|
699
|
+
rating: getVitalsScore("NTBT", ntbt.value),
|
|
700
|
+
value: ntbt.value
|
|
701
|
+
});
|
|
702
|
+
ntbt.value = 0;
|
|
703
|
+
}, 2e3);
|
|
704
|
+
};
|
|
705
|
+
//#endregion
|
|
706
|
+
export { IThresholdTier, clear, end, initPerfume, markNTBT, markStep, markStepOnce, start, trackUJNavigation };
|
|
707
|
+
|
|
708
|
+
//# sourceMappingURL=index.js.map
|