@atlaskit/react-ufo 5.1.3 → 5.2.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 (40) hide show
  1. package/AGENTS.md +426 -0
  2. package/CHANGELOG.md +22 -0
  3. package/dist/cjs/create-payload/index.js +3 -3
  4. package/dist/cjs/hidden-timing/index.js +135 -0
  5. package/dist/cjs/interaction-metrics/index.js +11 -3
  6. package/dist/cjs/interaction-metrics-init/index.js +3 -0
  7. package/dist/cjs/set-terminal-error/index.js +11 -3
  8. package/dist/cjs/vc/vc-observer-new/metric-calculator/abstract-base-vc-calculator.js +39 -27
  9. package/dist/cjs/vc/vc-observer-new/metric-calculator/percentile-calc/canvas-heatmap/index.js +22 -7
  10. package/dist/es2019/create-payload/index.js +3 -3
  11. package/dist/es2019/hidden-timing/index.js +128 -0
  12. package/dist/es2019/interaction-metrics/index.js +10 -2
  13. package/dist/es2019/interaction-metrics-init/index.js +4 -1
  14. package/dist/es2019/set-terminal-error/index.js +12 -4
  15. package/dist/es2019/vc/vc-observer-new/metric-calculator/abstract-base-vc-calculator.js +17 -4
  16. package/dist/es2019/vc/vc-observer-new/metric-calculator/percentile-calc/canvas-heatmap/index.js +22 -7
  17. package/dist/esm/create-payload/index.js +4 -4
  18. package/dist/esm/hidden-timing/index.js +130 -0
  19. package/dist/esm/interaction-metrics/index.js +10 -2
  20. package/dist/esm/interaction-metrics-init/index.js +4 -1
  21. package/dist/esm/set-terminal-error/index.js +12 -4
  22. package/dist/esm/vc/vc-observer-new/metric-calculator/abstract-base-vc-calculator.js +39 -27
  23. package/dist/esm/vc/vc-observer-new/metric-calculator/percentile-calc/canvas-heatmap/index.js +22 -7
  24. package/dist/types/common/react-ufo-payload-schema.d.ts +1 -0
  25. package/dist/types/common/vc/types.d.ts +2 -0
  26. package/dist/types/create-terminal-error-payload/index.d.ts +4 -0
  27. package/dist/types/hidden-timing/index.d.ts +42 -0
  28. package/dist/types/interaction-metrics/index.d.ts +8 -0
  29. package/dist/types/set-terminal-error/index.d.ts +4 -0
  30. package/dist/types/vc/vc-observer-new/metric-calculator/percentile-calc/canvas-heatmap/index.d.ts +3 -3
  31. package/dist/types/vc/vc-observer-new/metric-calculator/percentile-calc/types.d.ts +12 -0
  32. package/dist/types-ts4.5/common/react-ufo-payload-schema.d.ts +1 -0
  33. package/dist/types-ts4.5/common/vc/types.d.ts +2 -0
  34. package/dist/types-ts4.5/create-terminal-error-payload/index.d.ts +4 -0
  35. package/dist/types-ts4.5/hidden-timing/index.d.ts +42 -0
  36. package/dist/types-ts4.5/interaction-metrics/index.d.ts +8 -0
  37. package/dist/types-ts4.5/set-terminal-error/index.d.ts +4 -0
  38. package/dist/types-ts4.5/vc/vc-observer-new/metric-calculator/percentile-calc/canvas-heatmap/index.d.ts +3 -3
  39. package/dist/types-ts4.5/vc/vc-observer-new/metric-calculator/percentile-calc/types.d.ts +12 -0
  40. package/package.json +10 -1
@@ -4,7 +4,7 @@ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefau
4
4
  Object.defineProperty(exports, "__esModule", {
5
5
  value: true
6
6
  });
7
- exports.ModuleLoadingProfiler = void 0;
7
+ exports.PreviousInteractionLog = exports.ModuleLoadingProfiler = void 0;
8
8
  exports.abort = abort;
9
9
  exports.abortAll = abortAll;
10
10
  exports.abortByNewInteraction = abortByNewInteraction;
@@ -71,9 +71,12 @@ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t =
71
71
  function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
72
72
  function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
73
73
  function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } // eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead
74
- var PreviousInteractionLog = {
74
+ var PreviousInteractionLog = exports.PreviousInteractionLog = {
75
+ id: undefined,
75
76
  name: undefined,
76
- isAborted: undefined
77
+ type: undefined,
78
+ isAborted: undefined,
79
+ timestamp: undefined
77
80
  };
78
81
  var postInteractionLog = exports.postInteractionLog = new _postInteractionLog.default();
79
82
  var interactionExtraMetrics = exports.interactionExtraMetrics = new _interactionExtraMetrics.default();
@@ -751,6 +754,11 @@ function finishInteraction(id, data) {
751
754
  remove(id);
752
755
  }
753
756
  }
757
+ if ((0, _platformFeatureFlags.fg)('platform_ufo_enable_terminal_errors')) {
758
+ PreviousInteractionLog.id = data.id;
759
+ PreviousInteractionLog.type = data.type;
760
+ PreviousInteractionLog.timestamp = data.end;
761
+ }
754
762
  PreviousInteractionLog.name = data.ufoName || 'unknown';
755
763
  PreviousInteractionLog.isAborted = data.abortReason != null;
756
764
  if (data.ufoName) {
@@ -187,6 +187,9 @@ function init(analyticsWebClientAsync, config) {
187
187
  }
188
188
  (0, _hiddenTiming.setupHiddenTimingCapture)();
189
189
  (0, _additionalPayload.startLighthouseObserver)();
190
+ if ((0, _platformFeatureFlags.fg)('platform_ufo_is_tab_throttled')) {
191
+ (0, _hiddenTiming.setupThrottleDetection)();
192
+ }
190
193
  initialized = true;
191
194
  if (typeof PerformanceObserver !== 'undefined') {
192
195
  var observer = (0, _interactionsPerformanceObserver.getPerformanceObserver)();
@@ -18,18 +18,26 @@ function sinkTerminalErrorHandler(fn) {
18
18
  sinkHandlerFn = fn;
19
19
  }
20
20
  function setTerminalError(error, additionalAttributes, labelStack) {
21
- var _activeInteraction$uf, _activeInteraction$id, _activeInteraction$ty;
21
+ var _activeInteraction$uf, _activeInteraction$id, _activeInteraction$ty, _PreviousInteractionL, _PreviousInteractionL2, _PreviousInteractionL3;
22
22
  var activeInteraction = (0, _interactionMetrics.getActiveInteraction)();
23
+ var currentTime = performance.now();
23
24
  var errorData = _objectSpread({
24
25
  errorType: error.name || 'Error',
25
26
  errorMessage: error.message.slice(0, 100),
26
- timestamp: performance.now()
27
+ timestamp: currentTime
27
28
  }, additionalAttributes);
29
+
30
+ // Calculate time since previous interaction
31
+ var timeSincePreviousInteraction = _interactionMetrics.PreviousInteractionLog.timestamp != null ? currentTime - _interactionMetrics.PreviousInteractionLog.timestamp : null;
28
32
  var context = {
29
33
  labelStack: labelStack !== null && labelStack !== void 0 ? labelStack : null,
30
34
  activeInteractionName: (_activeInteraction$uf = activeInteraction === null || activeInteraction === void 0 ? void 0 : activeInteraction.ufoName) !== null && _activeInteraction$uf !== void 0 ? _activeInteraction$uf : null,
31
35
  activeInteractionId: (_activeInteraction$id = activeInteraction === null || activeInteraction === void 0 ? void 0 : activeInteraction.id) !== null && _activeInteraction$id !== void 0 ? _activeInteraction$id : null,
32
- activeInteractionType: (_activeInteraction$ty = activeInteraction === null || activeInteraction === void 0 ? void 0 : activeInteraction.type) !== null && _activeInteraction$ty !== void 0 ? _activeInteraction$ty : null
36
+ activeInteractionType: (_activeInteraction$ty = activeInteraction === null || activeInteraction === void 0 ? void 0 : activeInteraction.type) !== null && _activeInteraction$ty !== void 0 ? _activeInteraction$ty : null,
37
+ previousInteractionId: (_PreviousInteractionL = _interactionMetrics.PreviousInteractionLog.id) !== null && _PreviousInteractionL !== void 0 ? _PreviousInteractionL : null,
38
+ previousInteractionName: (_PreviousInteractionL2 = _interactionMetrics.PreviousInteractionLog.name) !== null && _PreviousInteractionL2 !== void 0 ? _PreviousInteractionL2 : null,
39
+ previousInteractionType: (_PreviousInteractionL3 = _interactionMetrics.PreviousInteractionLog.type) !== null && _PreviousInteractionL3 !== void 0 ? _PreviousInteractionL3 : null,
40
+ timeSincePreviousInteraction: timeSincePreviousInteraction
33
41
  };
34
42
  sinkHandlerFn(errorData, context);
35
43
  }
@@ -120,13 +120,14 @@ var AbstractVCCalculatorBase = exports.default = /*#__PURE__*/function () {
120
120
  value: function () {
121
121
  var _calculateWithDebugInfo = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee(filteredEntries, startTime, stopTime, isPostInteraction, isVCClean, interactionType, isPageVisible, interactionId, dirtyReason, allEntries, include3p, excludeSmartAnswersInSearch, interactionAbortReason, includeSSRRatio) {
122
122
  var _window, _window2, _window3, _window4, _window6;
123
- var percentiles, viewportEntries, vcLogs, vcDetails, percentileIndex, entryDataBuffer, ssrRatio, _iterator4, _step4, _entry3, time, viewportPercentage, entries, elementNames, previousResult, i, percentile, enhancedVcLogs, shouldCalculate3p, shouldCalculateDebugDetails, sortedVcLogs, maxViewportPercentageAtTime, maxSoFar, _iterator5, _step5, log, getBiggestPreviousViewportPercentage, ignoredEntriesByTime, _iterator6, _step6, _entry4, _ignoredEntriesByTime, _viewportData$rect, _viewportData$previou, viewportData, timestamp, additionalVcLogs, _iterator7, _step7, _step7$value, _timestamp, ignoredEntries, _viewportPercentage, v3RevisionDebugDetails, _window5, _window5$__ufo_devtoo, _window7, _window7$__on_ufo_vc_;
123
+ var percentiles, viewportEntries, shouldCalculateSpeedIndex, _yield$calculateTTVCP, vcLogs, speedIndex, vcDetails, percentileIndex, entryDataBuffer, ssrRatio, _iterator4, _step4, _entry3, time, viewportPercentage, entries, elementNames, previousResult, i, percentile, enhancedVcLogs, shouldCalculate3p, shouldCalculateDebugDetails, sortedVcLogs, maxViewportPercentageAtTime, maxSoFar, _iterator5, _step5, log, getBiggestPreviousViewportPercentage, ignoredEntriesByTime, _iterator6, _step6, _entry4, _ignoredEntriesByTime, _viewportData$rect, _viewportData$previou, viewportData, timestamp, additionalVcLogs, _iterator7, _step7, _step7$value, _timestamp, ignoredEntries, _viewportPercentage, v3RevisionDebugDetails, _window5, _window5$__ufo_devtoo, _window7, _window7$__on_ufo_vc_;
124
124
  return _regenerator.default.wrap(function _callee$(_context) {
125
125
  while (1) switch (_context.prev = _context.next) {
126
126
  case 0:
127
127
  percentiles = [25, 50, 75, 80, 85, 90, 95, 98, 99, 100];
128
128
  viewportEntries = this.filterViewportEntries(filteredEntries);
129
- _context.next = 4;
129
+ shouldCalculateSpeedIndex = (0, _platformFeatureFlags.fg)('platform_ufo_ttvc_v4_speed_index');
130
+ _context.next = 5;
130
131
  return (0, _percentileCalc.calculateTTVCPercentilesWithDebugInfo)({
131
132
  viewport: {
132
133
  width: (0, _getViewportWidth.default)(),
@@ -134,24 +135,27 @@ var AbstractVCCalculatorBase = exports.default = /*#__PURE__*/function () {
134
135
  },
135
136
  startTime: startTime,
136
137
  stopTime: stopTime,
137
- orderedEntries: viewportEntries
138
+ orderedEntries: viewportEntries,
139
+ calculateSpeedIndex: shouldCalculateSpeedIndex
138
140
  });
139
- case 4:
140
- vcLogs = _context.sent;
141
+ case 5:
142
+ _yield$calculateTTVCP = _context.sent;
143
+ vcLogs = _yield$calculateTTVCP.entries;
144
+ speedIndex = _yield$calculateTTVCP.speedIndex;
141
145
  vcDetails = {};
142
146
  percentileIndex = 0;
143
147
  entryDataBuffer = new Set();
144
148
  ssrRatio = -1;
145
149
  if (!vcLogs) {
146
- _context.next = 30;
150
+ _context.next = 33;
147
151
  break;
148
152
  }
149
153
  _iterator4 = _createForOfIteratorHelper(vcLogs);
150
- _context.prev = 11;
154
+ _context.prev = 14;
151
155
  _iterator4.s();
152
- case 13:
156
+ case 16:
153
157
  if ((_step4 = _iterator4.n()).done) {
154
- _context.next = 22;
158
+ _context.next = 25;
155
159
  break;
156
160
  }
157
161
  _entry3 = _step4.value;
@@ -164,11 +168,11 @@ var AbstractVCCalculatorBase = exports.default = /*#__PURE__*/function () {
164
168
 
165
169
  // Only process entries if we haven't reached all percentiles
166
170
  if (!(percentileIndex >= percentiles.length)) {
167
- _context.next = 19;
171
+ _context.next = 22;
168
172
  break;
169
173
  }
170
- return _context.abrupt("break", 22);
171
- case 19:
174
+ return _context.abrupt("break", 25);
175
+ case 22:
172
176
  // Check if this entry matches any checkpoint percentiles
173
177
  if (viewportPercentage >= percentiles[percentileIndex]) {
174
178
  elementNames = [];
@@ -199,21 +203,21 @@ var AbstractVCCalculatorBase = exports.default = /*#__PURE__*/function () {
199
203
  return entryDataBuffer.add(e);
200
204
  });
201
205
  }
202
- case 20:
203
- _context.next = 13;
206
+ case 23:
207
+ _context.next = 16;
204
208
  break;
205
- case 22:
206
- _context.next = 27;
209
+ case 25:
210
+ _context.next = 30;
207
211
  break;
208
- case 24:
209
- _context.prev = 24;
210
- _context.t0 = _context["catch"](11);
211
- _iterator4.e(_context.t0);
212
212
  case 27:
213
213
  _context.prev = 27;
214
- _iterator4.f();
215
- return _context.finish(27);
214
+ _context.t0 = _context["catch"](14);
215
+ _iterator4.e(_context.t0);
216
216
  case 30:
217
+ _context.prev = 30;
218
+ _iterator4.f();
219
+ return _context.finish(30);
220
+ case 33:
217
221
  // Fill in any missing percentiles with the last known values
218
222
  previousResult = {
219
223
  t: 0,
@@ -373,13 +377,14 @@ var AbstractVCCalculatorBase = exports.default = /*#__PURE__*/function () {
373
377
  }
374
378
  return _context.abrupt("return", {
375
379
  vcDetails: vcDetails,
376
- ssrRatio: ssrRatio
380
+ ssrRatio: ssrRatio,
381
+ speedIndex: speedIndex
377
382
  });
378
- case 42:
383
+ case 45:
379
384
  case "end":
380
385
  return _context.stop();
381
386
  }
382
- }, _callee, this, [[11, 24, 27, 30]]);
387
+ }, _callee, this, [[14, 27, 30, 33]]);
383
388
  }));
384
389
  function calculateWithDebugInfo(_x, _x2, _x3, _x4, _x5, _x6, _x7, _x8, _x9, _x0, _x1, _x10, _x11, _x12) {
385
390
  return _calculateWithDebugInfo.apply(this, arguments);
@@ -393,7 +398,7 @@ var AbstractVCCalculatorBase = exports.default = /*#__PURE__*/function () {
393
398
  var _this = this,
394
399
  _vcDetails$90$t,
395
400
  _vcDetails$;
396
- var startTime, stopTime, orderedEntries, interactionId, isPostInteraction, include3p, excludeSmartAnswersInSearch, includeSSRRatio, interactionType, isPageVisible, interactionAbortReason, filteredEntries, isVCClean, dirtyReason, getVCCleanStatusResult, _yield$this$calculate, vcDetails, ssrRatio, result;
401
+ var startTime, stopTime, orderedEntries, interactionId, isPostInteraction, include3p, excludeSmartAnswersInSearch, includeSSRRatio, interactionType, isPageVisible, interactionAbortReason, filteredEntries, isVCClean, dirtyReason, getVCCleanStatusResult, _yield$this$calculate, vcDetails, ssrRatio, speedIndex, result;
397
402
  return _regenerator.default.wrap(function _callee2$(_context2) {
398
403
  while (1) switch (_context2.prev = _context2.next) {
399
404
  case 0:
@@ -422,6 +427,7 @@ var AbstractVCCalculatorBase = exports.default = /*#__PURE__*/function () {
422
427
  _yield$this$calculate = _context2.sent;
423
428
  vcDetails = _yield$this$calculate.vcDetails;
424
429
  ssrRatio = _yield$this$calculate.ssrRatio;
430
+ speedIndex = _yield$this$calculate.speedIndex;
425
431
  result = {
426
432
  revision: this.revisionNo,
427
433
  clean: true,
@@ -432,9 +438,15 @@ var AbstractVCCalculatorBase = exports.default = /*#__PURE__*/function () {
432
438
  if (ssrRatio !== -1) {
433
439
  result.ssrRatio = ssrRatio;
434
440
  }
441
+
442
+ // speedIndex is only calculated when platform_ufo_ttvc_v4_speed_index is enabled,
443
+ // so we only include it in the result when it has a meaningful value (> 0)
444
+ if (speedIndex > 0) {
445
+ result.speedIndex = speedIndex;
446
+ }
435
447
  result.labelStacks = this.getLabelStacks(filteredEntries, isPostInteraction);
436
448
  return _context2.abrupt("return", result);
437
- case 17:
449
+ case 19:
438
450
  case "end":
439
451
  return _context2.stop();
440
452
  }
@@ -85,11 +85,11 @@ function calculateTTVCPercentilesWithDebugInfo(_x2) {
85
85
  }
86
86
  function _calculateTTVCPercentilesWithDebugInfo() {
87
87
  _calculateTTVCPercentilesWithDebugInfo = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee2(_ref2) {
88
- var viewport, orderedEntries, startTime, canvas, elementMap, _iterator3, _step3, entry, rect, timePixelCounts, canvasDimensions, totalPixels;
88
+ var viewport, orderedEntries, startTime, _ref2$calculateSpeedI, calculateSpeedIndex, canvas, elementMap, _iterator3, _step3, entry, rect, timePixelCounts, canvasDimensions, totalPixels;
89
89
  return _regenerator.default.wrap(function _callee2$(_context2) {
90
90
  while (1) switch (_context2.prev = _context2.next) {
91
91
  case 0:
92
- viewport = _ref2.viewport, orderedEntries = _ref2.orderedEntries, startTime = _ref2.startTime;
92
+ viewport = _ref2.viewport, orderedEntries = _ref2.orderedEntries, startTime = _ref2.startTime, _ref2$calculateSpeedI = _ref2.calculateSpeedIndex, calculateSpeedIndex = _ref2$calculateSpeedI === void 0 ? false : _ref2$calculateSpeedI;
93
93
  canvas = new _canvasPixel.ViewportCanvas(viewport, (0, _platformFeatureFlags.fg)('platform_ufo_canvas_heatmap_full_precision') ? 1 : 0.25);
94
94
  elementMap = new Map();
95
95
  _iterator3 = _createForOfIteratorHelper(orderedEntries);
@@ -134,7 +134,7 @@ function _calculateTTVCPercentilesWithDebugInfo() {
134
134
  timePixelCounts = _context2.sent;
135
135
  canvasDimensions = canvas.getScaledDimensions();
136
136
  totalPixels = canvasDimensions.width * canvasDimensions.height;
137
- return _context2.abrupt("return", calculatePercentilesWithDebugInfo(timePixelCounts, elementMap, totalPixels, startTime));
137
+ return _context2.abrupt("return", calculatePercentilesWithDebugInfo(timePixelCounts, elementMap, totalPixels, startTime, calculateSpeedIndex));
138
138
  case 30:
139
139
  case "end":
140
140
  return _context2.stop();
@@ -209,8 +209,11 @@ function calculatePercentiles(timePixelCounts, elementMap, unorderedPercentiles,
209
209
  return results;
210
210
  }
211
211
  function calculatePercentilesWithDebugInfo(timePixelCounts, elementMap, totalPixels, startTime) {
212
- var results = new Array(elementMap.size);
212
+ var calculateSpeedIndex = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;
213
+ var entries = new Array(elementMap.size);
213
214
  var cumulativePixels = 0;
215
+ var speedIndex = 0;
216
+ var previousPercentCovered = 0;
214
217
  var sortedEntries = Array.from(timePixelCounts.entries()).sort(function (_ref7, _ref8) {
215
218
  var _ref9 = (0, _slicedToArray2.default)(_ref7, 1),
216
219
  timeA = _ref9[0];
@@ -225,11 +228,23 @@ function calculatePercentilesWithDebugInfo(timePixelCounts, elementMap, totalPix
225
228
  cumulativePixels += pixelCount;
226
229
  var percentCovered = cumulativePixels / totalPixels * 100;
227
230
  var entryDatas = elementMap.get(time) || [];
228
- results[i] = {
229
- time: Math.round(Number(time - startTime)),
231
+ var relativeTime = Math.round(Number(time - startTime));
232
+ entries[i] = {
233
+ time: relativeTime,
230
234
  viewportPercentage: percentCovered,
231
235
  entries: Array.from(entryDatas)
232
236
  };
237
+
238
+ // Speed index calculation: sum of (time × incremental viewport percentage)
239
+ // Only calculate when feature flag is enabled
240
+ if (calculateSpeedIndex) {
241
+ var ratioDelta = (percentCovered - previousPercentCovered) / 100;
242
+ speedIndex += relativeTime * ratioDelta;
243
+ previousPercentCovered = percentCovered;
244
+ }
233
245
  }
234
- return results;
246
+ return {
247
+ entries: entries,
248
+ speedIndex: Math.round(speedIndex)
249
+ };
235
250
  }
@@ -1,5 +1,5 @@
1
1
  import{getDocument}from'@atlaskit/browser-apis';import{fg}from'@atlaskit/platform-feature-flags';// Import common utilities
2
- import{getLighthouseMetrics}from'../additional-payload';import{CHRReporter}from'../assets';import*as bundleEvalTiming from'../bundle-eval-timing';import coinflip from'../coinflip';import{getConfig,getExperimentalInteractionRate,getUfoNameOverrides,shouldUseRawDataThirdPartyBehavior}from'../config';import{getExperimentalVCMetrics}from'../create-experimental-interaction-metrics-payload';import{getBm3Timings}from'../custom-timings';import{getGlobalErrorCount}from'../global-error-handler';import{getEarliestHiddenTiming,getHasHiddenTimingBeforeSetup,getPageVisibilityState,isOpenedInBackground}from'../hidden-timing';import*as initialPageLoadExtraTiming from'../initial-page-load-extra-timing';import{interactionSpans as atlaskitInteractionSpans}from'../interaction-metrics';import{createMemoryStateReport,createPressureStateReport}from'../machine-utilisation';import*as resourceTiming from'../resource-timing';import{filterResourceTimings}from'../resource-timing/common/utils/resource-timing-buffer';import{roundEpsilon}from'../round-number';import*as ssr from'../ssr';import{buildSegmentTree,getOldSegmentsLabelStack,labelStackStartWith,optimizeLabelStack,sanitizeUfoName,stringifyLabelStackFully}from'./common/utils';import{createCriticalMetricsPayloads}from'./critical-metrics-payload';import{addPerformanceMeasures}from'./utils/add-performance-measures';import{getBatteryInfoToLegacyFormat}from'./utils/get-battery-info';import{getBrowserMetadataToLegacyFormat}from'./utils/get-browser-metadata';import getInteractionStatus from'./utils/get-interaction-status';import{getMoreAccuratePageVisibilityUpToTTAI}from'./utils/get-more-accurate-page-visibility-up-to-ttai';import{getNavigationMetricsToLegacyFormat}from'./utils/get-navigation-metrics';import getPageVisibilityUpToTTAI from'./utils/get-page-visibility-up-to-ttai';import{getPaintMetricsToLegacyFormat}from'./utils/get-paint-metrics';import getPayloadSize from'./utils/get-payload-size';import{getReactUFOPayloadVersion}from'./utils/get-react-ufo-payload-version';import getSSRDoneTimeValue from'./utils/get-ssr-done-time-value';import getSSRSuccessUtil from'./utils/get-ssr-success';import getTTAI from'./utils/get-ttai';import getVCMetrics from'./utils/get-vc-metrics';import{getVisibilityStateFromPerformance}from'./utils/get-visibility-state-from-performance';import{optimizeApdex}from'./utils/optimize-apdex';import{optimizeCustomTimings}from'./utils/optimize-custom-timings';import{optimizeHoldInfo}from'./utils/optimize-hold-info';import{optimizeMarks}from'./utils/optimize-marks';import{optimizeReactProfilerTimings}from'./utils/optimize-react-profiler-timings';import{optimizeRequestInfo}from'./utils/optimize-request-info';import{optimizeSpans}from'./utils/optimize-spans';const MAX_PAYLOAD_SIZE=230;function getUfoNameOverride(interaction){const{ufoName,apdex}=interaction;try{const ufoNameOverrides=getUfoNameOverrides();if(ufoNameOverrides!=null){const metricKey=apdex.length>0?apdex[0].key:'';if(ufoNameOverrides[ufoName][metricKey]){return ufoNameOverrides[ufoName][metricKey];}}return ufoName;}catch{return ufoName;}}function getEarliestLegacyStopTime(interaction,labelStack){let earliestLegacyStopTime=null;interaction.apdex.forEach(a=>{var _a$labelStack,_earliestLegacyStopTi;if(!(a!==null&&a!==void 0&&a.stopTime)){return;}if(!labelStackStartWith((_a$labelStack=a.labelStack)!==null&&_a$labelStack!==void 0?_a$labelStack:[],labelStack)){return;}if(a.stopTime>interaction.start&&((_earliestLegacyStopTi=earliestLegacyStopTime)!==null&&_earliestLegacyStopTi!==void 0?_earliestLegacyStopTi:a.stopTime)>=a.stopTime){earliestLegacyStopTime=a.stopTime;}});return earliestLegacyStopTime;}function getBm3EndTimeOrFallbackValue(interaction,labelStack=[],fallbackValue=interaction.end){var _getEarliestLegacySto;if(interaction.type==='press'){return fallbackValue;}return(_getEarliestLegacySto=getEarliestLegacyStopTime(interaction,labelStack))!==null&&_getEarliestLegacySto!==void 0?_getEarliestLegacySto:fallbackValue;}function getPageVisibilityUpToTTI(interaction){const{start}=interaction;const bm3EndTimeOrInteractionEndTime=getBm3EndTimeOrFallbackValue(interaction);return getPageVisibilityState(start,bm3EndTimeOrInteractionEndTime);}function getMoreAccuratePageVisibilityUpToTTI(interaction){const old=getPageVisibilityUpToTTI(interaction);const tti=getEarliestLegacyStopTime(interaction,[]);if(!tti){return old;}const buffered=getVisibilityStateFromPerformance(tti);if(!buffered){return old;}if(buffered!==old){return'mixed';}return old;}function getResourceTimings(start,end){var _resourceTiming$getRe;return(_resourceTiming$getRe=resourceTiming.getResourceTimings(start,end))!==null&&_resourceTiming$getRe!==void 0?_resourceTiming$getRe:undefined;}function getBundleEvalTimings(start){return bundleEvalTiming.getBundleEvalTimings(start);}function getSSRPhaseSuccess(type){return type==='page_load'?ssr.getSSRPhaseSuccess():undefined;}function getSSRFeatureFlags(type){return type==='page_load'?ssr.getSSRFeatureFlags():undefined;}function getPPSMetrics(interaction){var _interaction$apdex,_interaction$apdex$;const{start,end}=interaction;const config=getConfig();const interactionStatus=getInteractionStatus(interaction);const pageVisibilityUpToTTAI=getPageVisibilityUpToTTAI(interaction);const tti=(_interaction$apdex=interaction.apdex)===null||_interaction$apdex===void 0?void 0:(_interaction$apdex$=_interaction$apdex[0])===null||_interaction$apdex$===void 0?void 0:_interaction$apdex$.stopTime;const ttai=interactionStatus.originalInteractionStatus==='SUCCEEDED'&&pageVisibilityUpToTTAI==='visible'?Math.round(end-start):undefined;const PPSMetricsAtTTI=tti!==undefined?getLighthouseMetrics({start,stop:tti}):null;const PPSMetricsAtTTAI=ttai!==undefined?getLighthouseMetrics({start,stop:interaction.end}):null;if(fg('platform_ufo_remove_deprecated_config_fields')){if(PPSMetricsAtTTAI!==null){return PPSMetricsAtTTAI;}}else{if(config!==null&&config!==void 0&&config.shouldCalculateLighthouseMetricsFromTTAI&&PPSMetricsAtTTAI!==null){return PPSMetricsAtTTAI;}if(PPSMetricsAtTTI!==null){return{...PPSMetricsAtTTI,'metrics@ttai':PPSMetricsAtTTAI};}}return{};}function getSSRProperties(type){const ssrPhases=getSSRPhaseSuccess(type);return{'ssr:success':(ssrPhases===null||ssrPhases===void 0?void 0:ssrPhases.done)!=null?ssrPhases.done:getSSRSuccessUtil(type),'ssr:featureFlags':getSSRFeatureFlags(type),...((ssrPhases===null||ssrPhases===void 0?void 0:ssrPhases.earlyFlush)!=null?{'ssr:earlyflush:success':ssrPhases.earlyFlush}:null),...((ssrPhases===null||ssrPhases===void 0?void 0:ssrPhases.prefetch)!=null?{'ssr:prefetch:success':ssrPhases.prefetch}:null)};}function getAssetsMetrics(interaction,SSRDoneTime){try{const config=getConfig();const{type}=interaction;const allowedTypes=['page_load'];const assetsConfig=config===null||config===void 0?void 0:config.assetsConfig;if(!allowedTypes.includes(type)||!assetsConfig){// Skip if: type not allowed or assetsClassification isn't configured
2
+ import{getLighthouseMetrics}from'../additional-payload';import{CHRReporter}from'../assets';import*as bundleEvalTiming from'../bundle-eval-timing';import coinflip from'../coinflip';import{getConfig,getExperimentalInteractionRate,getUfoNameOverrides,shouldUseRawDataThirdPartyBehavior}from'../config';import{getExperimentalVCMetrics}from'../create-experimental-interaction-metrics-payload';import{getBm3Timings}from'../custom-timings';import{getGlobalErrorCount}from'../global-error-handler';import{getEarliestHiddenTiming,getHasHiddenTimingBeforeSetup,getPageVisibilityState,isOpenedInBackground,isTabThrottled}from'../hidden-timing';import*as initialPageLoadExtraTiming from'../initial-page-load-extra-timing';import{interactionSpans as atlaskitInteractionSpans}from'../interaction-metrics';import{createMemoryStateReport,createPressureStateReport}from'../machine-utilisation';import*as resourceTiming from'../resource-timing';import{filterResourceTimings}from'../resource-timing/common/utils/resource-timing-buffer';import{roundEpsilon}from'../round-number';import*as ssr from'../ssr';import{buildSegmentTree,getOldSegmentsLabelStack,labelStackStartWith,optimizeLabelStack,sanitizeUfoName,stringifyLabelStackFully}from'./common/utils';import{createCriticalMetricsPayloads}from'./critical-metrics-payload';import{addPerformanceMeasures}from'./utils/add-performance-measures';import{getBatteryInfoToLegacyFormat}from'./utils/get-battery-info';import{getBrowserMetadataToLegacyFormat}from'./utils/get-browser-metadata';import getInteractionStatus from'./utils/get-interaction-status';import{getMoreAccuratePageVisibilityUpToTTAI}from'./utils/get-more-accurate-page-visibility-up-to-ttai';import{getNavigationMetricsToLegacyFormat}from'./utils/get-navigation-metrics';import getPageVisibilityUpToTTAI from'./utils/get-page-visibility-up-to-ttai';import{getPaintMetricsToLegacyFormat}from'./utils/get-paint-metrics';import getPayloadSize from'./utils/get-payload-size';import{getReactUFOPayloadVersion}from'./utils/get-react-ufo-payload-version';import getSSRDoneTimeValue from'./utils/get-ssr-done-time-value';import getSSRSuccessUtil from'./utils/get-ssr-success';import getTTAI from'./utils/get-ttai';import getVCMetrics from'./utils/get-vc-metrics';import{getVisibilityStateFromPerformance}from'./utils/get-visibility-state-from-performance';import{optimizeApdex}from'./utils/optimize-apdex';import{optimizeCustomTimings}from'./utils/optimize-custom-timings';import{optimizeHoldInfo}from'./utils/optimize-hold-info';import{optimizeMarks}from'./utils/optimize-marks';import{optimizeReactProfilerTimings}from'./utils/optimize-react-profiler-timings';import{optimizeRequestInfo}from'./utils/optimize-request-info';import{optimizeSpans}from'./utils/optimize-spans';const MAX_PAYLOAD_SIZE=230;function getUfoNameOverride(interaction){const{ufoName,apdex}=interaction;try{const ufoNameOverrides=getUfoNameOverrides();if(ufoNameOverrides!=null){const metricKey=apdex.length>0?apdex[0].key:'';if(ufoNameOverrides[ufoName][metricKey]){return ufoNameOverrides[ufoName][metricKey];}}return ufoName;}catch{return ufoName;}}function getEarliestLegacyStopTime(interaction,labelStack){let earliestLegacyStopTime=null;interaction.apdex.forEach(a=>{var _a$labelStack,_earliestLegacyStopTi;if(!(a!==null&&a!==void 0&&a.stopTime)){return;}if(!labelStackStartWith((_a$labelStack=a.labelStack)!==null&&_a$labelStack!==void 0?_a$labelStack:[],labelStack)){return;}if(a.stopTime>interaction.start&&((_earliestLegacyStopTi=earliestLegacyStopTime)!==null&&_earliestLegacyStopTi!==void 0?_earliestLegacyStopTi:a.stopTime)>=a.stopTime){earliestLegacyStopTime=a.stopTime;}});return earliestLegacyStopTime;}function getBm3EndTimeOrFallbackValue(interaction,labelStack=[],fallbackValue=interaction.end){var _getEarliestLegacySto;if(interaction.type==='press'){return fallbackValue;}return(_getEarliestLegacySto=getEarliestLegacyStopTime(interaction,labelStack))!==null&&_getEarliestLegacySto!==void 0?_getEarliestLegacySto:fallbackValue;}function getPageVisibilityUpToTTI(interaction){const{start}=interaction;const bm3EndTimeOrInteractionEndTime=getBm3EndTimeOrFallbackValue(interaction);return getPageVisibilityState(start,bm3EndTimeOrInteractionEndTime);}function getMoreAccuratePageVisibilityUpToTTI(interaction){const old=getPageVisibilityUpToTTI(interaction);const tti=getEarliestLegacyStopTime(interaction,[]);if(!tti){return old;}const buffered=getVisibilityStateFromPerformance(tti);if(!buffered){return old;}if(buffered!==old){return'mixed';}return old;}function getResourceTimings(start,end){var _resourceTiming$getRe;return(_resourceTiming$getRe=resourceTiming.getResourceTimings(start,end))!==null&&_resourceTiming$getRe!==void 0?_resourceTiming$getRe:undefined;}function getBundleEvalTimings(start){return bundleEvalTiming.getBundleEvalTimings(start);}function getSSRPhaseSuccess(type){return type==='page_load'?ssr.getSSRPhaseSuccess():undefined;}function getSSRFeatureFlags(type){return type==='page_load'?ssr.getSSRFeatureFlags():undefined;}function getPPSMetrics(interaction){var _interaction$apdex,_interaction$apdex$;const{start,end}=interaction;const config=getConfig();const interactionStatus=getInteractionStatus(interaction);const pageVisibilityUpToTTAI=getPageVisibilityUpToTTAI(interaction);const tti=(_interaction$apdex=interaction.apdex)===null||_interaction$apdex===void 0?void 0:(_interaction$apdex$=_interaction$apdex[0])===null||_interaction$apdex$===void 0?void 0:_interaction$apdex$.stopTime;const ttai=interactionStatus.originalInteractionStatus==='SUCCEEDED'&&pageVisibilityUpToTTAI==='visible'?Math.round(end-start):undefined;const PPSMetricsAtTTI=tti!==undefined?getLighthouseMetrics({start,stop:tti}):null;const PPSMetricsAtTTAI=ttai!==undefined?getLighthouseMetrics({start,stop:interaction.end}):null;if(fg('platform_ufo_remove_deprecated_config_fields')){if(PPSMetricsAtTTAI!==null){return PPSMetricsAtTTAI;}}else{if(config!==null&&config!==void 0&&config.shouldCalculateLighthouseMetricsFromTTAI&&PPSMetricsAtTTAI!==null){return PPSMetricsAtTTAI;}if(PPSMetricsAtTTI!==null){return{...PPSMetricsAtTTI,'metrics@ttai':PPSMetricsAtTTAI};}}return{};}function getSSRProperties(type){const ssrPhases=getSSRPhaseSuccess(type);return{'ssr:success':(ssrPhases===null||ssrPhases===void 0?void 0:ssrPhases.done)!=null?ssrPhases.done:getSSRSuccessUtil(type),'ssr:featureFlags':getSSRFeatureFlags(type),...((ssrPhases===null||ssrPhases===void 0?void 0:ssrPhases.earlyFlush)!=null?{'ssr:earlyflush:success':ssrPhases.earlyFlush}:null),...((ssrPhases===null||ssrPhases===void 0?void 0:ssrPhases.prefetch)!=null?{'ssr:prefetch:success':ssrPhases.prefetch}:null)};}function getAssetsMetrics(interaction,SSRDoneTime){try{const config=getConfig();const{type}=interaction;const allowedTypes=['page_load'];const assetsConfig=config===null||config===void 0?void 0:config.assetsConfig;if(!allowedTypes.includes(type)||!assetsConfig){// Skip if: type not allowed or assetsClassification isn't configured
3
3
  return{};}const reporter=new CHRReporter();const resourceTimings=filterResourceTimings(interaction.start,interaction.end);const assets=reporter.get(resourceTimings,assetsConfig,SSRDoneTime);if(assets){// Only add assets in case it exists
4
4
  return{'event:assets':assets};}return{};}catch{// Skip CHR in case of error
5
5
  return{};}}function getTracingContextData(interaction){const{trace,start}=interaction;let tracingContextData={};if(trace){tracingContextData={'ufo:tracingContext':{'X-B3-TraceId':trace.traceId,'X-B3-SpanId':trace.spanId,// eslint-disable-next-line compat/compat
@@ -11,12 +11,12 @@ const getDetailedInteractionMetrics=resourceTimings=>{if(experimental||window.__
11
11
  if(shouldInclude3pHolds){var _interaction$hold3pIn;return{...basePayload,hold3pActive:interaction.hold3pActive?[...interaction.hold3pActive.values()]:[],hold3pInfo:optimizeHoldInfo((_interaction$hold3pIn=interaction.hold3pInfo)!==null&&_interaction$hold3pIn!==void 0?_interaction$hold3pIn:[],start,getReactUFOPayloadVersion(interaction.type))};}return basePayload;};// Page load & detailed payload
12
12
  const getPageLoadDetailedInteractionMetrics=()=>{var _config$ssr2,_config$ssr2$getSSRTi;if(!isPageLoad||!isDetailedPayload){return{};}const initialPageLoadExtraTimings=objectToArray(initialPageLoadExtraTiming.getTimings());const config=getConfig();const defaultSSRTimings=objectToArray(ssr.getSSRTimings());const ssrTimingsFromConfig=config===null||config===void 0?void 0:(_config$ssr2=config.ssr)===null||_config$ssr2===void 0?void 0:(_config$ssr2$getSSRTi=_config$ssr2.getSSRTimings)===null||_config$ssr2$getSSRTi===void 0?void 0:_config$ssr2$getSSRTi.call(_config$ssr2);return{initialPageLoadExtraTimings,SSRTimings:ssrTimingsFromConfig?[...ssrTimingsFromConfig,...defaultSSRTimings]:defaultSSRTimings};};if(experimental){expTTAI=getTTAI(interaction);}else{regularTTAI=getTTAI(interaction);}const newUFOName=sanitizeUfoName(ufoName);const resourceTimings=getResourceTimings(start,end);const[finalVCMetrics,experimentalMetrics,paintMetrics,batteryInfo]=await Promise.all([vcMetrics||(await getVCMetrics(interaction)),experimental?getExperimentalVCMetrics(interaction):Promise.resolve(undefined),getPaintMetricsToLegacyFormat(type,end),getBatteryInfoToLegacyFormat()]);if(!experimental){addPerformanceMeasures(interaction.start,[...((finalVCMetrics===null||finalVCMetrics===void 0?void 0:finalVCMetrics['ufo:vc:rev'])||[])]);}const getReactHydrationStats=()=>{if(!hydration){return{};}return{hydration};};const payload={actionSubject:'experience',action:'measured',eventType:'operational',source:'measured',tags:['observability'],attributes:{properties:{// basic
13
13
  'event:hostname':((_window$location=window.location)===null||_window$location===void 0?void 0:_window$location.hostname)||'unknown','event:product':config.product,'event:population':config.population,'event:schema':'1.0.0','event:sizeInKb':0,'event:source':{name:'react-ufo/web',version:getReactUFOPayloadVersion(interaction.type)},'event:region':config.region||'unknown','experience:key':experimental?'custom.experimental-interaction-metrics':'custom.interaction-metrics','experience:name':newUFOName,// Include CPU usage monitoring data
14
- 'event:cpu:usage':createPressureStateReport(interaction.start,interaction.end),'event:memory:usage':createMemoryStateReport(interaction.start,interaction.end),...(criticalPayloadCount!==undefined?{'ufo:multipayload':true,'ufo:criticalPayloadCount':criticalPayloadCount}:{}),...(fg('platform_ufo_browser_backgrounded_abort_timestamp')?{'ufo:pageVisibilityHiddenTimestamp':getEarliestHiddenTiming(interaction.start,interaction.end)}:{}),'ufo:wasPageHiddenBeforeInit':getHasHiddenTimingBeforeSetup(),'ufo:isOpenedInBackground':isOpenedInBackground(interaction.type),// root
14
+ 'event:cpu:usage':createPressureStateReport(interaction.start,interaction.end),'event:memory:usage':createMemoryStateReport(interaction.start,interaction.end),...(criticalPayloadCount!==undefined?{'ufo:multipayload':true,'ufo:criticalPayloadCount':criticalPayloadCount}:{}),...(fg('platform_ufo_browser_backgrounded_abort_timestamp')?{'ufo:pageVisibilityHiddenTimestamp':getEarliestHiddenTiming(interaction.start,interaction.end)}:{}),'ufo:wasPageHiddenBeforeInit':getHasHiddenTimingBeforeSetup(),'ufo:isOpenedInBackground':isOpenedInBackground(interaction.type),...(fg('platform_ufo_is_tab_throttled')?{'ufo:isTabThrottled':isTabThrottled(start,end)}:{}),// root
15
15
  ...getBrowserMetadataToLegacyFormat(),...batteryInfo,...getSSRProperties(type),...getAssetsMetrics(interaction,pageLoadInteractionMetrics===null||pageLoadInteractionMetrics===void 0?void 0:pageLoadInteractionMetrics.SSRDoneTime),...getPPSMetrics(interaction),...paintMetrics,...getNavigationMetricsToLegacyFormat(type),...finalVCMetrics,...experimentalMetrics,...((_config$additionalPay=config.additionalPayloadData)===null||_config$additionalPay===void 0?void 0:_config$additionalPay.call(config,interaction)),...getTracingContextData(interaction),...getStylesheetMetrics(),...getErrorCounts(interaction),...getReactHydrationStats(),interactionMetrics:{namePrefix:config.namePrefix||'',segmentPrefix:config.segmentPrefix||'',interactionId,pageVisibilityAtTTI,pageVisibilityAtTTAI,experimental__pageVisibilityAtTTI:moreAccuratePageVisibilityAtTTI,experimental__pageVisibilityAtTTAI:moreAccuratePageVisibilityAtTTAI,// raw interaction metrics
16
16
  rate,routeName,type,abortReason,featureFlags,previousInteractionName,isPreviousInteractionAborted,abortedByInteractionName,// performance
17
17
  apdex:optimizeApdex(interaction.apdex,getReactUFOPayloadVersion(interaction.type)),end:Math.round(end),...(interaction.end3p?{end3p:Math.round(interaction.end3p)}:{}),start:Math.round(start),segments:getReactUFOPayloadVersion(interaction.type)==='2.0.0'?segmentTree:getOldSegmentsLabelStack(segments,interaction.type),marks:optimizeMarks(interaction.marks,getReactUFOPayloadVersion(interaction.type)),customData:optimizeCustomData(interaction),reactProfilerTimings:optimizeReactProfilerTimings(interaction.reactProfilerTimings,start,getReactUFOPayloadVersion(interaction.type)),minorInteractions:interaction.minorInteractions,...(responsiveness?{responsiveness}:{}),...labelStack,...pageLoadInteractionMetrics,...getDetailedInteractionMetrics(resourceTimings),...getPageLoadDetailedInteractionMetrics(),...getBm3TrackerTimings(interaction),'metric:ttai':experimental?regularTTAI||expTTAI:undefined,'metric:experimental:ttai':expTTAI,...(unknownElementName?{unknownElementName}:{}),...(unknownElementHierarchy?{unknownElementHierarchy}:{})},'ufo:payloadTime':roundEpsilon(performance.now()-interactionPayloadStart)}}};if(experimental){regularTTAI=undefined;expTTAI=undefined;}if(fg('platform_ufo_enable_vc_raw_data')){const size=getPayloadSize(payload.attributes.properties);const vcRev=payload.attributes.properties['ufo:vc:rev'];const rawData=vcRev.find(item=>item.revision==='raw-handler');if(rawData){const rawDataSize=getPayloadSize(rawData);payload.attributes.properties['ufo:vc:raw:size']=rawDataSize;if(size>MAX_PAYLOAD_SIZE&&Array.isArray(vcRev)&&vcRev.length>0){payload.attributes.properties['ufo:vc:rev']=vcRev.filter(item=>item.revision!=='raw-handler');payload.attributes.properties['ufo:vc:raw:removed']=true;}}payload.attributes.properties['event:sizeInKb']=getPayloadSize(payload.attributes.properties);}else{payload.attributes.properties['event:sizeInKb']=getPayloadSize(payload.attributes.properties);}// in order of importance, first one being least important
18
18
  // we can add more fields as necessary
19
- const interactionMetricsFieldsToTrim=['requestInfo','featureFlags','resourceTimings'];const properties=payload.attributes.properties;const interactionMetrics=properties.interactionMetrics;if(interactionMetrics){for(const field of interactionMetricsFieldsToTrim){if(getPayloadSize(properties)<=MAX_PAYLOAD_SIZE){continue;}interactionMetrics[field]=undefined;properties['event:isTrimmed']=true;let trimmedFields=properties['event:trimmedFields'];if(!Array.isArray(trimmedFields)){trimmedFields=[];}trimmedFields.push(`interactionMetrics.${field}`);properties['event:trimmedFields']=trimmedFields;}}return payload;}export async function createPayloads(interactionId,interaction){const ufoNameOverride=getUfoNameOverride(interaction);const modifiedInteraction={...interaction,ufoName:ufoNameOverride};const payloads=[];const isCriticalMetricsEnabled=fg('platform_ufo_critical_metrics_payload');// Calculate VC metrics once to avoid duplicate expensive calculations
19
+ const interactionMetricsFieldsToTrim=fg('ufo_remove_featureflags_from_trimmed_fields')?['requestInfo','resourceTimings']:['requestInfo','featureFlags','resourceTimings'];const properties=payload.attributes.properties;const interactionMetrics=properties.interactionMetrics;if(interactionMetrics){for(const field of interactionMetricsFieldsToTrim){if(getPayloadSize(properties)<=MAX_PAYLOAD_SIZE){continue;}interactionMetrics[field]=undefined;properties['event:isTrimmed']=true;let trimmedFields=properties['event:trimmedFields'];if(!Array.isArray(trimmedFields)){trimmedFields=[];}trimmedFields.push(`interactionMetrics.${field}`);properties['event:trimmedFields']=trimmedFields;}}return payload;}export async function createPayloads(interactionId,interaction){const ufoNameOverride=getUfoNameOverride(interaction);const modifiedInteraction={...interaction,ufoName:ufoNameOverride};const payloads=[];const isCriticalMetricsEnabled=fg('platform_ufo_critical_metrics_payload');// Calculate VC metrics once to avoid duplicate expensive calculations
20
20
  const vcMetrics=await getVCMetrics(interaction);// typeof Promise<CriticalMetricsPayload[]>
21
21
  const criticalMetricsPayloads=isCriticalMetricsEnabled?await createCriticalMetricsPayloads(interactionId,interaction,vcMetrics):[];payloads.push(...criticalMetricsPayloads);const criticalPayloadCount=isCriticalMetricsEnabled?criticalMetricsPayloads.length:undefined;const interactionMetricsPayload=await createInteractionMetricsPayload(modifiedInteraction,interactionId,undefined,criticalPayloadCount,vcMetrics);payloads.push(interactionMetricsPayload);return payloads.filter(Boolean);}export async function createExperimentalMetricsPayload(interactionId,interaction){const config=getConfig();if(!config){throw Error('UFO Configuration not provided');}const ufoName=sanitizeUfoName(interaction.ufoName);const rate=getExperimentalInteractionRate(ufoName,interaction.type);if(!coinflip(rate)){return null;}const pageVisibilityState=getPageVisibilityState(interaction.start,interaction.end);if(pageVisibilityState!=='visible'){return null;}const result=await createInteractionMetricsPayload(interaction,interactionId,true);return result;}export async function createExtraSearchPageInteractionPayload(interactionId,interaction){var _newEnd;const SAIN_HOLD_NAMES=['search-ai-dialog-visible-text-loading','search-ai-dialog-all-text-loading'];const NAME_OVERRIDE='search-page-ignoring-smart-answers';const SEARCH_PAGE_SMART_ANSWERS_SEGMENT_LABEL='search-page-smart-answers';const newInteractionId=`${interactionId}-ignoring-smart-answers`;// Calculate a new end time which excludes SAIN holds
22
22
  let newEnd;const{holdInfo,reactProfilerTimings}=interaction;const lastHold=holdInfo.at(-1);const isLastHoldSAIN=Boolean(lastHold&&SAIN_HOLD_NAMES.includes(lastHold.name));// A new end time is only calculated if the last hold is a SAIN hold
@@ -148,4 +148,132 @@ export function getPageVisibilityState(start, end) {
148
148
  hiddenState = timings[startIdx].hidden ? 'hidden' : 'visible';
149
149
  }
150
150
  return hiddenState;
151
+ }
152
+
153
+ // Throttle detection configuration
154
+ // Expected interval for timer checks (in milliseconds)
155
+ const THROTTLE_CHECK_INTERVAL_MS = 1000;
156
+ // Threshold for considering a timer as throttled (50% drift tolerance)
157
+ const THROTTLE_DRIFT_THRESHOLD = 1.5;
158
+ // Maximum number of throttle measurements to store (circular buffer)
159
+ const THROTTLE_BUFFER_SIZE = 120;
160
+ // Circular buffer to store throttle measurements
161
+ const throttleMeasurements = [];
162
+ let throttleInsertIndex = 0;
163
+ let throttleIntervalId = null;
164
+ let lastThrottleCheckTime = null;
165
+ let throttleSetupDone = false;
166
+ function recordThrottleMeasurement(expectedElapsed, actualElapsed) {
167
+ const isThrottled = actualElapsed > expectedElapsed * THROTTLE_DRIFT_THRESHOLD;
168
+ throttleMeasurements[throttleInsertIndex] = {
169
+ time: performance.now(),
170
+ expectedElapsed,
171
+ actualElapsed,
172
+ isThrottled
173
+ };
174
+ throttleInsertIndex = (throttleInsertIndex + 1) % THROTTLE_BUFFER_SIZE;
175
+ }
176
+ function throttleCheckCallback() {
177
+ const currentTime = performance.now();
178
+ if (lastThrottleCheckTime !== null) {
179
+ const actualElapsed = currentTime - lastThrottleCheckTime;
180
+ recordThrottleMeasurement(THROTTLE_CHECK_INTERVAL_MS, actualElapsed);
181
+ }
182
+ lastThrottleCheckTime = currentTime;
183
+ }
184
+
185
+ /**
186
+ * Sets up the throttle detection mechanism.
187
+ * This should be called early in the page lifecycle.
188
+ * Uses a periodic timer to detect browser throttling by measuring timer drift.
189
+ */
190
+ export function setupThrottleDetection() {
191
+ if (throttleSetupDone) {
192
+ return;
193
+ }
194
+ throttleSetupDone = true;
195
+
196
+ // Record the initial timestamp
197
+ lastThrottleCheckTime = performance.now();
198
+
199
+ // Start the periodic timer for throttle detection
200
+ throttleIntervalId = setInterval(throttleCheckCallback, THROTTLE_CHECK_INTERVAL_MS);
201
+ }
202
+
203
+ /**
204
+ * Stops the throttle detection mechanism.
205
+ * Useful for cleanup in tests or when the feature is no longer needed.
206
+ */
207
+ export function stopThrottleDetection() {
208
+ if (throttleIntervalId !== null) {
209
+ clearInterval(throttleIntervalId);
210
+ throttleIntervalId = null;
211
+ }
212
+ lastThrottleCheckTime = null;
213
+ throttleSetupDone = false;
214
+ throttleMeasurements.length = 0;
215
+ throttleInsertIndex = 0;
216
+ }
217
+
218
+ /**
219
+ * Checks if the tab was throttled at any point during the specified time window.
220
+ * Returns true if any timer measurement showed significant drift (throttling).
221
+ *
222
+ * @param startTime - The start timestamp of the window to check (DOMHighResTimeStamp)
223
+ * @param endTime - The end timestamp of the window to check (DOMHighResTimeStamp)
224
+ * @returns boolean - true if throttling was detected during the time window, false otherwise
225
+ */
226
+ export function isTabThrottled(startTime, endTime) {
227
+ // Input validation
228
+ if (!Number.isFinite(startTime) || !Number.isFinite(endTime) || startTime >= endTime) {
229
+ return false;
230
+ }
231
+
232
+ // No measurements available
233
+ if (throttleMeasurements.length === 0) {
234
+ return false;
235
+ }
236
+
237
+ // Check if any measurement within the time window indicates throttling
238
+ for (let i = 0; i < throttleMeasurements.length; i++) {
239
+ const measurement = throttleMeasurements[i];
240
+ if (measurement && measurement.time >= startTime && measurement.time <= endTime && measurement.isThrottled) {
241
+ return true;
242
+ }
243
+ }
244
+ return false;
245
+ }
246
+
247
+ /**
248
+ * Gets detailed throttle information for debugging purposes.
249
+ * Returns all throttle measurements within the specified time window.
250
+ *
251
+ * @param startTime - The start timestamp of the window to check
252
+ * @param endTime - The end timestamp of the window to check
253
+ * @returns Array of throttle measurements within the time window
254
+ */
255
+ export function getThrottleMeasurements(startTime, endTime) {
256
+ // Input validation
257
+ if (!Number.isFinite(startTime) || !Number.isFinite(endTime) || startTime >= endTime) {
258
+ return [];
259
+ }
260
+ return throttleMeasurements.filter(measurement => measurement && measurement.time >= startTime && measurement.time <= endTime);
261
+ }
262
+
263
+ /**
264
+ * Injects a fake throttle measurement for testing purposes.
265
+ * This allows integration tests to simulate throttling scenarios.
266
+ *
267
+ * @param measurement - The throttle measurement to inject
268
+ */
269
+ export function __injectThrottleMeasurementForTesting(measurement) {
270
+ throttleMeasurements[throttleInsertIndex] = measurement;
271
+ throttleInsertIndex = (throttleInsertIndex + 1) % THROTTLE_BUFFER_SIZE;
272
+ }
273
+
274
+ // Expose testing API on window for integration tests
275
+ if (typeof window !== 'undefined') {
276
+ window.__reactUfoHiddenTiming = {
277
+ __injectThrottleMeasurementForTesting
278
+ };
151
279
  }
@@ -14,9 +14,12 @@ import { newVCObserver } from '../vc';
14
14
  import { interactions } from './common/constants';
15
15
  import InteractionExtraMetrics from './interaction-extra-metrics';
16
16
  import PostInteractionLog from './post-interaction-log';
17
- const PreviousInteractionLog = {
17
+ export const PreviousInteractionLog = {
18
+ id: undefined,
18
19
  name: undefined,
19
- isAborted: undefined
20
+ type: undefined,
21
+ isAborted: undefined,
22
+ timestamp: undefined
20
23
  };
21
24
  export const postInteractionLog = new PostInteractionLog();
22
25
  export const interactionExtraMetrics = new InteractionExtraMetrics();
@@ -667,6 +670,11 @@ function finishInteraction(id, data, endTime = performance.now()) {
667
670
  remove(id);
668
671
  }
669
672
  }
673
+ if (fg('platform_ufo_enable_terminal_errors')) {
674
+ PreviousInteractionLog.id = data.id;
675
+ PreviousInteractionLog.type = data.type;
676
+ PreviousInteractionLog.timestamp = data.end;
677
+ }
670
678
  PreviousInteractionLog.name = data.ufoName || 'unknown';
671
679
  PreviousInteractionLog.isAborted = data.abortReason != null;
672
680
  if (data.ufoName) {
@@ -5,7 +5,7 @@ import { setUFOConfig } from '../config';
5
5
  import { experimentalVC, sinkExperimentalHandler } from '../create-experimental-interaction-metrics-payload';
6
6
  import { sinkExtraSearchPageInteractionHandler } from '../create-extra-search-page-interaction-payload';
7
7
  import { setContextManager, UFOContextManager } from '../experience-trace-id-context/context-manager';
8
- import { setupHiddenTimingCapture } from '../hidden-timing';
8
+ import { setupHiddenTimingCapture, setupThrottleDetection } from '../hidden-timing';
9
9
  import { interactionExtraMetrics, postInteractionLog, sinkInteractionHandler, sinkPostInteractionLogHandler } from '../interaction-metrics';
10
10
  import { getPerformanceObserver } from '../interactions-performance-observer';
11
11
  import { initialiseMemoryObserver, initialisePressureObserver } from '../machine-utilisation';
@@ -163,6 +163,9 @@ export function init(analyticsWebClientAsync, config) {
163
163
  }
164
164
  setupHiddenTimingCapture();
165
165
  startLighthouseObserver();
166
+ if (fg('platform_ufo_is_tab_throttled')) {
167
+ setupThrottleDetection();
168
+ }
166
169
  initialized = true;
167
170
  if (typeof PerformanceObserver !== 'undefined') {
168
171
  const observer = getPerformanceObserver();
@@ -1,24 +1,32 @@
1
1
  import { useEffect, useRef } from 'react';
2
2
  import { useInteractionContext } from '../interaction-context';
3
- import { getActiveInteraction } from '../interaction-metrics';
3
+ import { getActiveInteraction, PreviousInteractionLog } from '../interaction-metrics';
4
4
  let sinkHandlerFn = () => {};
5
5
  export function sinkTerminalErrorHandler(fn) {
6
6
  sinkHandlerFn = fn;
7
7
  }
8
8
  export function setTerminalError(error, additionalAttributes, labelStack) {
9
- var _activeInteraction$uf, _activeInteraction$id, _activeInteraction$ty;
9
+ var _activeInteraction$uf, _activeInteraction$id, _activeInteraction$ty, _PreviousInteractionL, _PreviousInteractionL2, _PreviousInteractionL3;
10
10
  const activeInteraction = getActiveInteraction();
11
+ const currentTime = performance.now();
11
12
  const errorData = {
12
13
  errorType: error.name || 'Error',
13
14
  errorMessage: error.message.slice(0, 100),
14
- timestamp: performance.now(),
15
+ timestamp: currentTime,
15
16
  ...additionalAttributes
16
17
  };
18
+
19
+ // Calculate time since previous interaction
20
+ const timeSincePreviousInteraction = PreviousInteractionLog.timestamp != null ? currentTime - PreviousInteractionLog.timestamp : null;
17
21
  const context = {
18
22
  labelStack: labelStack !== null && labelStack !== void 0 ? labelStack : null,
19
23
  activeInteractionName: (_activeInteraction$uf = activeInteraction === null || activeInteraction === void 0 ? void 0 : activeInteraction.ufoName) !== null && _activeInteraction$uf !== void 0 ? _activeInteraction$uf : null,
20
24
  activeInteractionId: (_activeInteraction$id = activeInteraction === null || activeInteraction === void 0 ? void 0 : activeInteraction.id) !== null && _activeInteraction$id !== void 0 ? _activeInteraction$id : null,
21
- activeInteractionType: (_activeInteraction$ty = activeInteraction === null || activeInteraction === void 0 ? void 0 : activeInteraction.type) !== null && _activeInteraction$ty !== void 0 ? _activeInteraction$ty : null
25
+ activeInteractionType: (_activeInteraction$ty = activeInteraction === null || activeInteraction === void 0 ? void 0 : activeInteraction.type) !== null && _activeInteraction$ty !== void 0 ? _activeInteraction$ty : null,
26
+ previousInteractionId: (_PreviousInteractionL = PreviousInteractionLog.id) !== null && _PreviousInteractionL !== void 0 ? _PreviousInteractionL : null,
27
+ previousInteractionName: (_PreviousInteractionL2 = PreviousInteractionLog.name) !== null && _PreviousInteractionL2 !== void 0 ? _PreviousInteractionL2 : null,
28
+ previousInteractionType: (_PreviousInteractionL3 = PreviousInteractionLog.type) !== null && _PreviousInteractionL3 !== void 0 ? _PreviousInteractionL3 : null,
29
+ timeSincePreviousInteraction
22
30
  };
23
31
  sinkHandlerFn(errorData, context);
24
32
  }
@@ -64,14 +64,19 @@ export default class AbstractVCCalculatorBase {
64
64
  var _window, _window2, _window3, _window4, _window6;
65
65
  const percentiles = [25, 50, 75, 80, 85, 90, 95, 98, 99, 100];
66
66
  const viewportEntries = this.filterViewportEntries(filteredEntries);
67
- const vcLogs = await calculateTTVCPercentilesWithDebugInfo({
67
+ const shouldCalculateSpeedIndex = fg('platform_ufo_ttvc_v4_speed_index');
68
+ const {
69
+ entries: vcLogs,
70
+ speedIndex
71
+ } = await calculateTTVCPercentilesWithDebugInfo({
68
72
  viewport: {
69
73
  width: getViewportWidth(),
70
74
  height: getViewportHeight()
71
75
  },
72
76
  startTime,
73
77
  stopTime,
74
- orderedEntries: viewportEntries
78
+ orderedEntries: viewportEntries,
79
+ calculateSpeedIndex: shouldCalculateSpeedIndex
75
80
  });
76
81
  const vcDetails = {};
77
82
  let percentileIndex = 0;
@@ -263,7 +268,8 @@ export default class AbstractVCCalculatorBase {
263
268
  }
264
269
  return {
265
270
  vcDetails,
266
- ssrRatio
271
+ ssrRatio,
272
+ speedIndex
267
273
  };
268
274
  }
269
275
  async calculate({
@@ -299,7 +305,8 @@ export default class AbstractVCCalculatorBase {
299
305
  }
300
306
  const {
301
307
  vcDetails,
302
- ssrRatio
308
+ ssrRatio,
309
+ speedIndex
303
310
  } = await this.calculateWithDebugInfo(filteredEntries, startTime, stopTime, isPostInteraction, isVCClean, interactionType, isPageVisible, interactionId, dirtyReason, orderedEntries, include3p, excludeSmartAnswersInSearch, interactionAbortReason, includeSSRRatio);
304
311
  const result = {
305
312
  revision: this.revisionNo,
@@ -311,6 +318,12 @@ export default class AbstractVCCalculatorBase {
311
318
  if (ssrRatio !== -1) {
312
319
  result.ssrRatio = ssrRatio;
313
320
  }
321
+
322
+ // speedIndex is only calculated when platform_ufo_ttvc_v4_speed_index is enabled,
323
+ // so we only include it in the result when it has a meaningful value (> 0)
324
+ if (speedIndex > 0) {
325
+ result.speedIndex = speedIndex;
326
+ }
314
327
  result.labelStacks = this.getLabelStacks(filteredEntries, isPostInteraction);
315
328
  return result;
316
329
  }