@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/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