@atlaskit/react-ufo 5.2.9 → 5.3.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 (61) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/cjs/create-payload/index.js +6 -5
  3. package/dist/cjs/hidden-timing/index.js +52 -0
  4. package/dist/cjs/interaction-metrics/index.js +1 -0
  5. package/dist/cjs/interaction-metrics-init/index.js +2 -1
  6. package/dist/cjs/vc/index.js +4 -2
  7. package/dist/cjs/vc/vc-observer-new/index.js +10 -4
  8. package/dist/cjs/vc/vc-observer-new/metric-calculator/abstract-base-vc-calculator.js +84 -29
  9. package/dist/cjs/vc/vc-observer-new/metric-calculator/utils/detect-layout-shift-cause.js +164 -0
  10. package/dist/cjs/vc/vc-observer-new/viewport-observer/index.js +173 -34
  11. package/dist/cjs/vc/vc-observer-new/viewport-observer/intersection-observer/index.js +1 -1
  12. package/dist/cjs/vc/vc-observer-new/viewport-observer/mutation-observer/index.js +2 -1
  13. package/dist/es2019/create-payload/index.js +5 -3
  14. package/dist/es2019/hidden-timing/index.js +51 -0
  15. package/dist/es2019/interaction-metrics/index.js +1 -0
  16. package/dist/es2019/interaction-metrics-init/index.js +2 -1
  17. package/dist/es2019/vc/index.js +4 -2
  18. package/dist/es2019/vc/vc-observer-new/index.js +10 -5
  19. package/dist/es2019/vc/vc-observer-new/metric-calculator/abstract-base-vc-calculator.js +61 -7
  20. package/dist/es2019/vc/vc-observer-new/metric-calculator/utils/detect-layout-shift-cause.js +138 -0
  21. package/dist/es2019/vc/vc-observer-new/viewport-observer/index.js +145 -10
  22. package/dist/es2019/vc/vc-observer-new/viewport-observer/intersection-observer/index.js +1 -1
  23. package/dist/es2019/vc/vc-observer-new/viewport-observer/mutation-observer/index.js +2 -1
  24. package/dist/esm/create-payload/index.js +7 -6
  25. package/dist/esm/hidden-timing/index.js +51 -0
  26. package/dist/esm/interaction-metrics/index.js +1 -0
  27. package/dist/esm/interaction-metrics-init/index.js +2 -1
  28. package/dist/esm/vc/index.js +4 -2
  29. package/dist/esm/vc/vc-observer-new/index.js +10 -4
  30. package/dist/esm/vc/vc-observer-new/metric-calculator/abstract-base-vc-calculator.js +84 -29
  31. package/dist/esm/vc/vc-observer-new/metric-calculator/utils/detect-layout-shift-cause.js +158 -0
  32. package/dist/esm/vc/vc-observer-new/viewport-observer/index.js +173 -34
  33. package/dist/esm/vc/vc-observer-new/viewport-observer/intersection-observer/index.js +1 -1
  34. package/dist/esm/vc/vc-observer-new/viewport-observer/mutation-observer/index.js +2 -1
  35. package/dist/types/common/react-ufo-payload-schema.d.ts +2 -0
  36. package/dist/types/common/vc/types.d.ts +55 -0
  37. package/dist/types/config/index.d.ts +1 -0
  38. package/dist/types/hidden-timing/index.d.ts +9 -0
  39. package/dist/types/vc/types.d.ts +2 -0
  40. package/dist/types/vc/vc-observer-new/index.d.ts +2 -0
  41. package/dist/types/vc/vc-observer-new/metric-calculator/abstract-base-vc-calculator.d.ts +1 -1
  42. package/dist/types/vc/vc-observer-new/metric-calculator/types.d.ts +1 -0
  43. package/dist/types/vc/vc-observer-new/metric-calculator/utils/detect-layout-shift-cause.d.ts +31 -0
  44. package/dist/types/vc/vc-observer-new/types.d.ts +2 -0
  45. package/dist/types/vc/vc-observer-new/viewport-observer/index.d.ts +3 -1
  46. package/dist/types/vc/vc-observer-new/viewport-observer/mutation-observer/index.d.ts +1 -0
  47. package/dist/types/vc/vc-observer-new/viewport-observer/types.d.ts +5 -2
  48. package/dist/types-ts4.5/common/react-ufo-payload-schema.d.ts +2 -0
  49. package/dist/types-ts4.5/common/vc/types.d.ts +55 -0
  50. package/dist/types-ts4.5/config/index.d.ts +1 -0
  51. package/dist/types-ts4.5/hidden-timing/index.d.ts +9 -0
  52. package/dist/types-ts4.5/vc/types.d.ts +2 -0
  53. package/dist/types-ts4.5/vc/vc-observer-new/index.d.ts +2 -0
  54. package/dist/types-ts4.5/vc/vc-observer-new/metric-calculator/abstract-base-vc-calculator.d.ts +1 -1
  55. package/dist/types-ts4.5/vc/vc-observer-new/metric-calculator/types.d.ts +1 -0
  56. package/dist/types-ts4.5/vc/vc-observer-new/metric-calculator/utils/detect-layout-shift-cause.d.ts +31 -0
  57. package/dist/types-ts4.5/vc/vc-observer-new/types.d.ts +2 -0
  58. package/dist/types-ts4.5/vc/vc-observer-new/viewport-observer/index.d.ts +3 -1
  59. package/dist/types-ts4.5/vc/vc-observer-new/viewport-observer/mutation-observer/index.d.ts +1 -0
  60. package/dist/types-ts4.5/vc/vc-observer-new/viewport-observer/types.d.ts +5 -2
  61. package/package.json +4 -1
@@ -16,7 +16,7 @@ export class VCObserverWrapper {
16
16
  enablePageLayoutPlaceholder: (_opts$ssrEnablePageLa = opts.ssrEnablePageLayoutPlaceholder) !== null && _opts$ssrEnablePageLa !== void 0 ? _opts$ssrEnablePageLa : false
17
17
  });
18
18
  if (isVCRevisionEnabled('fy25.03') || isVCRevisionEnabled('fy26.04')) {
19
- var _opts$ssrEnablePageLa2;
19
+ var _opts$ssrEnablePageLa2, _opts$trackLayoutShif;
20
20
  this.newVCObserver = new VCObserverNew({
21
21
  selectorConfig: opts.selectorConfig,
22
22
  isPostInteraction: opts.isPostInteraction,
@@ -24,6 +24,7 @@ export class VCObserverWrapper {
24
24
  enablePageLayoutPlaceholder: (_opts$ssrEnablePageLa2 = opts.ssrEnablePageLayoutPlaceholder) !== null && _opts$ssrEnablePageLa2 !== void 0 ? _opts$ssrEnablePageLa2 : false
25
25
  },
26
26
  ssrPlaceholderHandler: this.ssrPlaceholderHandler,
27
+ trackLayoutShiftOffenders: (_opts$trackLayoutShif = opts.trackLayoutShiftOffenders) !== null && _opts$trackLayoutShif !== void 0 ? _opts$trackLayoutShif : false,
27
28
  searchPageConfig: opts.searchPageConfig
28
29
  });
29
30
  }
@@ -115,7 +116,8 @@ export class VCObserverWrapper {
115
116
  interactionAbortReason: param.interactionAbortReason,
116
117
  includeRawData,
117
118
  includeSSRInV3: param.includeSSRInV3,
118
- rawDataStopTime: param.rawDataStopTime
119
+ rawDataStopTime: param.rawDataStopTime,
120
+ reportLayoutShiftOffenders: param.reportLayoutShiftOffenders
119
121
  })) : [];
120
122
  if (!v3v4Result || v3v4Result.length === 0) {
121
123
  return v1v2Result !== null && v1v2Result !== void 0 ? v1v2Result : {};
@@ -31,7 +31,7 @@ export function getHasAbortingEventDuringSSR() {
31
31
  }
32
32
  export default class VCObserverNew {
33
33
  constructor(config) {
34
- var _config$isPostInterac, _config$selectorConfi;
34
+ var _config$isPostInterac, _config$selectorConfi, _config$trackLayoutSh;
35
35
  _defineProperty(this, "viewportObserver", null);
36
36
  _defineProperty(this, "windowEventObserver", null);
37
37
  // SSR related properties
@@ -55,6 +55,7 @@ export default class VCObserverNew {
55
55
  enablePageLayoutPlaceholder: (_config$SSRConfig$ena = (_config$SSRConfig = config.SSRConfig) === null || _config$SSRConfig === void 0 ? void 0 : _config$SSRConfig.enablePageLayoutPlaceholder) !== null && _config$SSRConfig$ena !== void 0 ? _config$SSRConfig$ena : false
56
56
  });
57
57
  }
58
+ this.trackLayoutShiftOffenders = (_config$trackLayoutSh = config.trackLayoutShiftOffenders) !== null && _config$trackLayoutSh !== void 0 ? _config$trackLayoutSh : false;
58
59
  this.viewportObserver = new ViewportObserver({
59
60
  onChange: onChangeArg => {
60
61
  const {
@@ -79,7 +80,8 @@ export default class VCObserverNew {
79
80
  visible,
80
81
  attributeName: mutationData === null || mutationData === void 0 ? void 0 : mutationData.attributeName,
81
82
  oldValue: mutationData === null || mutationData === void 0 ? void 0 : mutationData.oldValue,
82
- newValue: mutationData === null || mutationData === void 0 ? void 0 : mutationData.newValue
83
+ newValue: mutationData === null || mutationData === void 0 ? void 0 : mutationData.newValue,
84
+ originalMutationTimestamp: mutationData === null || mutationData === void 0 ? void 0 : mutationData.timestamp
83
85
  };
84
86
  if (element) {
85
87
  const labelStacks = getLabelStacks(element);
@@ -97,7 +99,8 @@ export default class VCObserverNew {
97
99
  // Pass SSR context to ViewportObserver
98
100
  getSSRState: () => this.getSSRState(),
99
101
  getSSRPlaceholderHandler: () => this.getSSRPlaceholderHandler(),
100
- searchPageConfig: config.searchPageConfig
102
+ searchPageConfig: config.searchPageConfig,
103
+ trackLayoutShiftOffenders: this.trackLayoutShiftOffenders
101
104
  });
102
105
  this.windowEventObserver = new WindowEventObserver({
103
106
  onEvent: ({
@@ -271,7 +274,8 @@ export default class VCObserverNew {
271
274
  excludeSmartAnswersInSearch,
272
275
  includeSSRRatio,
273
276
  isPageVisible,
274
- interactionAbortReason
277
+ interactionAbortReason,
278
+ reportLayoutShiftOffenders: this.trackLayoutShiftOffenders
275
279
  }) : null;
276
280
  if (fy25_03) {
277
281
  results.push(fy25_03);
@@ -294,7 +298,8 @@ export default class VCObserverNew {
294
298
  excludeSmartAnswersInSearch,
295
299
  includeSSRRatio,
296
300
  isPageVisible,
297
- interactionAbortReason
301
+ interactionAbortReason,
302
+ reportLayoutShiftOffenders: this.trackLayoutShiftOffenders
298
303
  };
299
304
  const [fy26_04, vcNext] = await Promise.all([isVCRevisionEnabled('fy26.04') ? calculator_fy26_04.calculate(calculatorParams) : null, isVCRevisionEnabled('next') ? calculator_next.calculate(calculatorParams) : null]);
300
305
  if (fy26_04) {
@@ -1,5 +1,6 @@
1
1
  import { fg } from '@atlaskit/platform-feature-flags';
2
2
  import { calculateTTVCPercentilesWithDebugInfo } from './percentile-calc';
3
+ import { detectLayoutShiftCause } from './utils/detect-layout-shift-cause';
3
4
  import getViewportHeight from './utils/get-viewport-height';
4
5
  import getViewportWidth from './utils/get-viewport-width';
5
6
 
@@ -60,7 +61,7 @@ export default class AbstractVCCalculatorBase {
60
61
  }
61
62
  return labelStacks;
62
63
  }
63
- async calculateWithDebugInfo(filteredEntries, startTime, stopTime, isPostInteraction, isVCClean, interactionType, isPageVisible, interactionId, dirtyReason, allEntries, include3p, excludeSmartAnswersInSearch, interactionAbortReason, includeSSRRatio) {
64
+ async calculateWithDebugInfo(filteredEntries, startTime, stopTime, isPostInteraction, isVCClean, interactionType, isPageVisible, interactionId, dirtyReason, allEntries, include3p, excludeSmartAnswersInSearch, interactionAbortReason, includeSSRRatio, reportLayoutShiftOffenders) {
64
65
  var _window, _window2, _window3, _window4, _window6;
65
66
  const percentiles = [25, 50, 75, 80, 85, 90, 95, 98, 99, 100];
66
67
  const viewportEntries = this.filterViewportEntries(filteredEntries);
@@ -80,6 +81,8 @@ export default class AbstractVCCalculatorBase {
80
81
  let percentileIndex = 0;
81
82
  const entryDataBuffer = new Set();
82
83
  let ssrRatio = -1;
84
+ let layoutShiftInsights = null;
85
+ let previousViewportPercentage = 0;
83
86
  if (vcLogs) {
84
87
  for (const entry of vcLogs) {
85
88
  const {
@@ -106,6 +109,19 @@ export default class AbstractVCCalculatorBase {
106
109
  t: Math.round(time),
107
110
  e: elementNames
108
111
  };
112
+ if (reportLayoutShiftOffenders && percentiles[percentileIndex] === 90 && entries.some(e => e.type === 'layout-shift')) {
113
+ const layoutShiftEntries = entries.filter(e => e.type === 'layout-shift');
114
+ layoutShiftInsights = {
115
+ layoutShiftOffendersResult: detectLayoutShiftCause({
116
+ viewportEntries: viewportEntries,
117
+ layoutShiftEntries,
118
+ time,
119
+ startTime
120
+ }),
121
+ layoutShiftEntriesCount: layoutShiftEntries.length,
122
+ layoutShiftImpact: viewportPercentage - previousViewportPercentage
123
+ };
124
+ }
109
125
  percentileIndex++;
110
126
  }
111
127
 
@@ -115,6 +131,7 @@ export default class AbstractVCCalculatorBase {
115
131
  // Only add to buffer if we haven't reached all percentiles
116
132
  entries.forEach(e => entryDataBuffer.add(e));
117
133
  }
134
+ previousViewportPercentage = viewportPercentage;
118
135
  }
119
136
  }
120
137
 
@@ -262,7 +279,8 @@ export default class AbstractVCCalculatorBase {
262
279
  return {
263
280
  vcDetails,
264
281
  ssrRatio,
265
- speedIndex
282
+ speedIndex,
283
+ VC90layoutShiftInsights: layoutShiftInsights
266
284
  };
267
285
  }
268
286
  async calculate({
@@ -276,9 +294,10 @@ export default class AbstractVCCalculatorBase {
276
294
  includeSSRRatio,
277
295
  interactionType,
278
296
  isPageVisible,
279
- interactionAbortReason
297
+ interactionAbortReason,
298
+ reportLayoutShiftOffenders
280
299
  }) {
281
- var _vcDetails$90$t, _vcDetails$;
300
+ var _vcDetails$90$t, _vcDetails$, _layoutShiftInsightsP;
282
301
  const filteredEntries = orderedEntries.filter(entry => {
283
302
  return this.isEntryIncluded(entry, include3p, excludeSmartAnswersInSearch);
284
303
  });
@@ -299,13 +318,48 @@ export default class AbstractVCCalculatorBase {
299
318
  const {
300
319
  vcDetails,
301
320
  ssrRatio,
302
- speedIndex
303
- } = await this.calculateWithDebugInfo(filteredEntries, startTime, stopTime, isPostInteraction, isVCClean, interactionType, isPageVisible, interactionId, dirtyReason, orderedEntries, include3p, excludeSmartAnswersInSearch, interactionAbortReason, includeSSRRatio);
321
+ speedIndex,
322
+ VC90layoutShiftInsights
323
+ } = await this.calculateWithDebugInfo(filteredEntries, startTime, stopTime, isPostInteraction, isVCClean, interactionType, isPageVisible, interactionId, dirtyReason, orderedEntries, include3p, excludeSmartAnswersInSearch, interactionAbortReason, includeSSRRatio, reportLayoutShiftOffenders);
324
+ let layoutShiftInsightsPayload;
325
+ if (VC90layoutShiftInsights !== null && reportLayoutShiftOffenders) {
326
+ var _layoutShiftOffenders, _layoutShiftOffenders2, _layoutShiftOffenders3;
327
+ const {
328
+ layoutShiftOffendersResult,
329
+ layoutShiftEntriesCount,
330
+ layoutShiftImpact
331
+ } = VC90layoutShiftInsights;
332
+ layoutShiftInsightsPayload = {
333
+ impact: layoutShiftImpact,
334
+ sources: layoutShiftEntriesCount !== null && layoutShiftEntriesCount !== void 0 ? layoutShiftEntriesCount : 0,
335
+ same: {
336
+ dir: (_layoutShiftOffenders = layoutShiftOffendersResult === null || layoutShiftOffendersResult === void 0 ? void 0 : layoutShiftOffendersResult.layoutShiftVariables.allMovedSameWay) !== null && _layoutShiftOffenders !== void 0 ? _layoutShiftOffenders : false,
337
+ dist: (_layoutShiftOffenders2 = layoutShiftOffendersResult === null || layoutShiftOffendersResult === void 0 ? void 0 : layoutShiftOffendersResult.layoutShiftVariables.allMovedSameAmount) !== null && _layoutShiftOffenders2 !== void 0 ? _layoutShiftOffenders2 : false
338
+ },
339
+ total_mut: (_layoutShiftOffenders3 = layoutShiftOffendersResult === null || layoutShiftOffendersResult === void 0 ? void 0 : layoutShiftOffendersResult.layoutShiftOffenders.length) !== null && _layoutShiftOffenders3 !== void 0 ? _layoutShiftOffenders3 : 0,
340
+ mut: layoutShiftOffendersResult === null || layoutShiftOffendersResult === void 0 ? void 0 : layoutShiftOffendersResult.layoutShiftOffenders.sort((a, b) => Math.abs(a.distanceToLS) - Math.abs(b.distanceToLS)).slice(0, 5).map(offender => ({
341
+ e: offender.offender,
342
+ size: -1,
343
+ // @todo: calculate size
344
+ attr: {
345
+ t_before: offender.happenedBefore,
346
+ t_distance: offender.distanceToLS,
347
+ p_above: offender.isAbove,
348
+ p_left: offender.isLeft,
349
+ p_right: offender.isRight,
350
+ p_h_overlap: offender.hasHorizontalOverlap,
351
+ p_v_overlap: offender.hasVerticalOverlap,
352
+ p_same_offset: offender.matchesLayoutShiftDelta ? 'all' : 'none'
353
+ }
354
+ }))
355
+ };
356
+ }
304
357
  const result = {
305
358
  revision: this.revisionNo,
306
359
  clean: true,
307
360
  'metric:vc90': (_vcDetails$90$t = vcDetails === null || vcDetails === void 0 ? void 0 : (_vcDetails$ = vcDetails['90']) === null || _vcDetails$ === void 0 ? void 0 : _vcDetails$.t) !== null && _vcDetails$90$t !== void 0 ? _vcDetails$90$t : null,
308
- vcDetails: vcDetails !== null && vcDetails !== void 0 ? vcDetails : undefined
361
+ vcDetails: vcDetails !== null && vcDetails !== void 0 ? vcDetails : undefined,
362
+ 'vc90:ls': (_layoutShiftInsightsP = layoutShiftInsightsPayload) !== null && _layoutShiftInsightsP !== void 0 ? _layoutShiftInsightsP : undefined
309
363
  };
310
364
  result.ratios = this.calculateRatios(filteredEntries);
311
365
  if (ssrRatio !== -1) {
@@ -0,0 +1,138 @@
1
+ const calculateOffenderState = (current, matchesCurrentEntry) => {
2
+ // Once we know it's mixed across entries, we can short-circuit.
3
+ if (current === 'some') {
4
+ return 'some';
5
+ }
6
+ const entryState = matchesCurrentEntry ? 'all' : 'none';
7
+ if (current == null) {
8
+ return entryState;
9
+ }
10
+
11
+ // If the state flips between entries (all <-> none), it's "some".
12
+ if (current !== entryState) {
13
+ return 'some';
14
+ }
15
+ return current;
16
+ };
17
+ const calculateLayoutShiftMovement = layoutShiftEntries => {
18
+ const movements = layoutShiftEntries.map(lsEntry => {
19
+ const rect = lsEntry.rect;
20
+ const previousRect = lsEntry.previousRect;
21
+ if (!rect || !previousRect) {
22
+ return null;
23
+ }
24
+ const dx = rect.x - previousRect.x;
25
+ const dy = rect.y - previousRect.y;
26
+ return {
27
+ dx,
28
+ dy,
29
+ movedX: dx !== 0,
30
+ movedY: dy !== 0,
31
+ dirX: Math.sign(dx),
32
+ dirY: Math.sign(dy),
33
+ absDx: Math.abs(dx),
34
+ absDy: Math.abs(dy)
35
+ };
36
+ });
37
+ const hasMovement = m => m !== null;
38
+ const validMovements = movements.filter(hasMovement);
39
+ const allHaveRects = layoutShiftEntries.length > 0 && validMovements.length === layoutShiftEntries.length;
40
+ if (!allHaveRects) {
41
+ return {
42
+ allHaveRects,
43
+ allMovedSameWay: false,
44
+ allMovedSameAmount: false
45
+ };
46
+ }
47
+ const first = validMovements[0];
48
+ const allMovedSameWay = validMovements.every(m => m.movedX === first.movedX && m.movedY === first.movedY && m.dirX === first.dirX && m.dirY === first.dirY);
49
+ const allMovedSameAmount = allMovedSameWay && validMovements.every(m => m.absDx === first.absDx && m.absDy === first.absDy);
50
+ return {
51
+ allHaveRects,
52
+ allMovedSameWay,
53
+ allMovedSameAmount,
54
+ deltaX: first.dx,
55
+ deltaY: first.dy
56
+ };
57
+ };
58
+ export const detectLayoutShiftCause = ({
59
+ viewportEntries,
60
+ layoutShiftEntries,
61
+ time,
62
+ startTime,
63
+ offenderWindowMs = 250
64
+ }) => {
65
+ const checkpointTimestamp = startTime + time;
66
+ const filteredLSPotentialOffenders = viewportEntries.filter(viewportEntry => (viewportEntry.time < checkpointTimestamp && viewportEntry.time > checkpointTimestamp - offenderWindowMs || viewportEntry.time > checkpointTimestamp && viewportEntry.time < checkpointTimestamp + offenderWindowMs) && viewportEntry.data.type !== 'layout-shift');
67
+
68
+ // Summarize whether all layout-shift entries moved in a consistent direction and by the same amount.
69
+ const layoutShiftVariables = calculateLayoutShiftMovement(layoutShiftEntries);
70
+
71
+ // Classify each offender against *all* layout-shift entries when the layout shift is consistently moving in a
72
+ // single axis (pure X or pure Y) so we can reason about whether an offender could have caused the movement.
73
+ const layoutShiftOffenders = filteredLSPotentialOffenders.reduce((acc, offender) => {
74
+ var _offenderData$origina, _isAbove, _isLeft, _isRight, _hasHorizontalOverlap, _hasVerticalOverlap;
75
+ const offenderData = offender.data;
76
+ const offenderRect = offenderData.rect;
77
+ if (!offenderRect) {
78
+ return acc;
79
+ }
80
+ const offenderTimestamp = (_offenderData$origina = offenderData.originalMutationTimestamp) !== null && _offenderData$origina !== void 0 ? _offenderData$origina : offender.time;
81
+ const happenedBefore = offenderTimestamp <= checkpointTimestamp;
82
+ const offenderLeft = offenderRect.x;
83
+ const offenderRight = offenderRect.x + offenderRect.width;
84
+ const offenderTop = offenderRect.y;
85
+ const offenderBottom = offenderRect.y + offenderRect.height;
86
+ const shouldClassifyAgainstAllEntries = layoutShiftVariables.allMovedSameWay && 'deltaX' in layoutShiftVariables && 'deltaY' in layoutShiftVariables && layoutShiftVariables.deltaX === 0 !== (layoutShiftVariables.deltaY === 0);
87
+ let isAbove = null;
88
+ let isLeft = null;
89
+ let isRight = null;
90
+ let hasHorizontalOverlap = null;
91
+ let hasVerticalOverlap = null;
92
+ if (shouldClassifyAgainstAllEntries) {
93
+ for (const lsEntry of layoutShiftEntries) {
94
+ const lsRect = lsEntry.rect;
95
+ if (!lsRect) {
96
+ continue;
97
+ }
98
+ const lsLeft = lsRect.x;
99
+ const lsRight = lsRect.x + lsRect.width;
100
+ const lsTop = lsRect.y;
101
+ const lsBottom = lsRect.y + lsRect.height;
102
+ isAbove = calculateOffenderState(isAbove, offenderBottom <= lsTop);
103
+ isLeft = calculateOffenderState(isLeft, offenderRight <= lsLeft);
104
+ isRight = calculateOffenderState(isRight, offenderLeft >= lsRight);
105
+ hasHorizontalOverlap = calculateOffenderState(hasHorizontalOverlap, offenderLeft < lsRight && offenderRight > lsLeft);
106
+ hasVerticalOverlap = calculateOffenderState(hasVerticalOverlap, offenderTop < lsBottom && offenderBottom > lsTop);
107
+ }
108
+ }
109
+ let matchesLayoutShiftDelta = false;
110
+ if (shouldClassifyAgainstAllEntries && layoutShiftVariables.allMovedSameAmount && offenderData.previousRect) {
111
+ const dx = offenderRect.x - offenderData.previousRect.x;
112
+ const dy = offenderRect.y - offenderData.previousRect.y;
113
+ const isPureX = layoutShiftVariables.deltaX !== 0 && layoutShiftVariables.deltaY === 0;
114
+ const isPureY = layoutShiftVariables.deltaY !== 0 && layoutShiftVariables.deltaX === 0;
115
+ if (isPureX) {
116
+ matchesLayoutShiftDelta = dx === layoutShiftVariables.deltaX && dy === 0;
117
+ } else if (isPureY) {
118
+ matchesLayoutShiftDelta = dy === layoutShiftVariables.deltaY && dx === 0;
119
+ }
120
+ }
121
+ acc.push({
122
+ offender: offenderData.elementName,
123
+ happenedBefore,
124
+ distanceToLS: Math.round(offenderTimestamp - checkpointTimestamp),
125
+ isAbove: (_isAbove = isAbove) !== null && _isAbove !== void 0 ? _isAbove : 'none',
126
+ isLeft: (_isLeft = isLeft) !== null && _isLeft !== void 0 ? _isLeft : 'none',
127
+ isRight: (_isRight = isRight) !== null && _isRight !== void 0 ? _isRight : 'none',
128
+ hasHorizontalOverlap: (_hasHorizontalOverlap = hasHorizontalOverlap) !== null && _hasHorizontalOverlap !== void 0 ? _hasHorizontalOverlap : 'none',
129
+ hasVerticalOverlap: (_hasVerticalOverlap = hasVerticalOverlap) !== null && _hasVerticalOverlap !== void 0 ? _hasVerticalOverlap : 'none',
130
+ matchesLayoutShiftDelta
131
+ });
132
+ return acc;
133
+ }, []);
134
+ return {
135
+ layoutShiftVariables,
136
+ layoutShiftOffenders
137
+ };
138
+ };
@@ -78,6 +78,130 @@ const createElementMutationsWatcher = (removedNodeRects, isWithinThirdPartySegme
78
78
  }
79
79
  return 'mutation:element';
80
80
  };
81
+ const createElementMutationsWatcherNew = (removedNodeRects, isWithinThirdPartySegment, isWithinSmartAnswersSegment, hasSameDeletedNode, timestamp, isTargetReactRoot, getSSRState, getSSRPlaceholderHandler) => ({
82
+ target,
83
+ rect
84
+ }) => {
85
+ if (getSSRState) {
86
+ const ssrState = getSSRState();
87
+ const SSRStateEnum = {
88
+ normal: 1,
89
+ waitingForFirstRender: 2,
90
+ ignoring: 3
91
+ };
92
+ if (ssrState.state === SSRStateEnum.waitingForFirstRender && timestamp > ssrState.renderStart && isTargetReactRoot) {
93
+ ssrState.state = SSRStateEnum.ignoring;
94
+ if (ssrState.renderStop === -1) {
95
+ // arbitrary 500ms DOM update window
96
+ ssrState.renderStop = timestamp + 500;
97
+ }
98
+ return {
99
+ type: 'ssr-hydration',
100
+ mutationData: {
101
+ timestamp
102
+ }
103
+ };
104
+ }
105
+ if (ssrState.state === SSRStateEnum.ignoring && timestamp > ssrState.renderStart && isTargetReactRoot) {
106
+ if (timestamp <= ssrState.renderStop) {
107
+ return {
108
+ type: 'ssr-hydration',
109
+ mutationData: {
110
+ timestamp
111
+ }
112
+ };
113
+ } else {
114
+ ssrState.state = SSRStateEnum.normal;
115
+ }
116
+ }
117
+ }
118
+ if (getSSRPlaceholderHandler) {
119
+ const ssrPlaceholderHandler = getSSRPlaceholderHandler();
120
+ if (ssrPlaceholderHandler) {
121
+ if ((ssrPlaceholderHandler.isPlaceholderV4(target) || ssrPlaceholderHandler.isPlaceholderIgnored(target)) && ssrPlaceholderHandler.checkIfExistedAndSizeMatchingV3(target)) {
122
+ return {
123
+ type: 'mutation:ssr-placeholder',
124
+ mutationData: {
125
+ timestamp
126
+ }
127
+ };
128
+ }
129
+ if ((ssrPlaceholderHandler.isPlaceholderReplacementV4(target) || ssrPlaceholderHandler.isPlaceholderIgnored(target)) && ssrPlaceholderHandler.validateReactComponentMatchToPlaceholderV4(target)) {
130
+ return {
131
+ type: 'mutation:ssr-placeholder',
132
+ mutationData: {
133
+ timestamp
134
+ }
135
+ };
136
+ }
137
+ }
138
+ }
139
+ if (hasSameDeletedNode && isInVCIgnoreIfNoLayoutShiftMarker(target)) {
140
+ return {
141
+ type: 'mutation:remount',
142
+ mutationData: {
143
+ timestamp
144
+ }
145
+ };
146
+ }
147
+ if (isContainedWithinMediaWrapper(target)) {
148
+ return {
149
+ type: 'mutation:media',
150
+ mutationData: {
151
+ timestamp
152
+ }
153
+ };
154
+ }
155
+ if (isWithinThirdPartySegment) {
156
+ return {
157
+ type: 'mutation:third-party-element',
158
+ mutationData: {
159
+ timestamp
160
+ }
161
+ };
162
+ }
163
+ if (isWithinSmartAnswersSegment) {
164
+ return {
165
+ type: 'mutation:smart-answers-element',
166
+ mutationData: {
167
+ timestamp
168
+ }
169
+ };
170
+ }
171
+ const isInIgnoreLsMarker = isInVCIgnoreIfNoLayoutShiftMarker(target);
172
+ if (!isInIgnoreLsMarker) {
173
+ return {
174
+ type: 'mutation:element',
175
+ mutationData: {
176
+ timestamp
177
+ }
178
+ };
179
+ }
180
+ const isRLLPlaceholder = RLLPlaceholderHandlers.getInstance().isRLLPlaceholderHydration(rect);
181
+ if (isRLLPlaceholder && isInIgnoreLsMarker) {
182
+ return {
183
+ type: 'mutation:rll-placeholder',
184
+ mutationData: {
185
+ timestamp
186
+ }
187
+ };
188
+ }
189
+ const wasDeleted = removedNodeRects.some(nr => isSameRectDimensions(nr, rect));
190
+ if (wasDeleted && isInIgnoreLsMarker) {
191
+ return {
192
+ type: 'mutation:element-replacement',
193
+ mutationData: {
194
+ timestamp
195
+ }
196
+ };
197
+ }
198
+ return {
199
+ type: 'mutation:element',
200
+ mutationData: {
201
+ timestamp
202
+ }
203
+ };
204
+ };
81
205
  export default class ViewportObserver {
82
206
  // SSR context functions
83
207
 
@@ -85,6 +209,7 @@ export default class ViewportObserver {
85
209
  onChange,
86
210
  getSSRState,
87
211
  getSSRPlaceholderHandler,
212
+ trackLayoutShiftOffenders = false,
88
213
  searchPageConfig
89
214
  }) {
90
215
  _defineProperty(this, "handleIntersectionEntry", ({
@@ -142,14 +267,15 @@ export default class ViewportObserver {
142
267
  } = checkWithinComponent(addedNode, 'UFOThirdPartySegment', this.mapIs3pResult);
143
268
  const isWithinSmartAnswersSegment = Boolean(this.shouldCheckSmartAnswersMutations() && isContainedWithinSmartAnswers(addedNode));
144
269
  const isTargetReactRoot = targetNode === ((_this$getSSRState = this.getSSRState) === null || _this$getSSRState === void 0 ? void 0 : (_this$getSSRState$cal = _this$getSSRState.call(this)) === null || _this$getSSRState$cal === void 0 ? void 0 : _this$getSSRState$cal.reactRootElement);
145
- (_this$intersectionObs = this.intersectionObserver) === null || _this$intersectionObs === void 0 ? void 0 : _this$intersectionObs.watchAndTag(addedNode, createElementMutationsWatcher(removedNodeRects, isWithinThirdPartySegment, isWithinSmartAnswersSegment, !!hasSameDeletedNode, timestamp, isTargetReactRoot, this.getSSRState, this.getSSRPlaceholderHandler));
270
+ (_this$intersectionObs = this.intersectionObserver) === null || _this$intersectionObs === void 0 ? void 0 : _this$intersectionObs.watchAndTag(addedNode, (this.trackLayoutShiftOffenders ? createElementMutationsWatcherNew : createElementMutationsWatcher)(removedNodeRects, isWithinThirdPartySegment, isWithinSmartAnswersSegment, !!hasSameDeletedNode, timestamp, isTargetReactRoot, this.getSSRState, this.getSSRPlaceholderHandler));
146
271
  }
147
272
  });
148
273
  _defineProperty(this, "handleAttributeMutation", ({
149
274
  target,
150
275
  attributeName,
151
276
  oldValue,
152
- newValue
277
+ newValue,
278
+ timestamp
153
279
  }) => {
154
280
  var _this$intersectionObs2;
155
281
  (_this$intersectionObs2 = this.intersectionObserver) === null || _this$intersectionObs2 === void 0 ? void 0 : _this$intersectionObs2.watchAndTag(target, ({
@@ -162,7 +288,8 @@ export default class ViewportObserver {
162
288
  mutationData: {
163
289
  attributeName,
164
290
  oldValue,
165
- newValue
291
+ newValue,
292
+ timestamp
166
293
  }
167
294
  };
168
295
  }
@@ -176,7 +303,8 @@ export default class ViewportObserver {
176
303
  mutationData: {
177
304
  attributeName,
178
305
  oldValue,
179
- newValue
306
+ newValue,
307
+ timestamp
180
308
  }
181
309
  };
182
310
  }
@@ -202,7 +330,8 @@ export default class ViewportObserver {
202
330
  mutationData: {
203
331
  attributeName,
204
332
  oldValue,
205
- newValue
333
+ newValue,
334
+ timestamp
206
335
  }
207
336
  };
208
337
  }
@@ -216,7 +345,8 @@ export default class ViewportObserver {
216
345
  mutationData: {
217
346
  attributeName,
218
347
  oldValue,
219
- newValue
348
+ newValue,
349
+ timestamp
220
350
  }
221
351
  };
222
352
  }
@@ -231,7 +361,8 @@ export default class ViewportObserver {
231
361
  mutationData: {
232
362
  attributeName,
233
363
  oldValue,
234
- newValue
364
+ newValue,
365
+ timestamp
235
366
  }
236
367
  };
237
368
  }
@@ -242,7 +373,8 @@ export default class ViewportObserver {
242
373
  mutationData: {
243
374
  attributeName,
244
375
  oldValue,
245
- newValue
376
+ newValue,
377
+ timestamp
246
378
  }
247
379
  };
248
380
  }
@@ -253,7 +385,8 @@ export default class ViewportObserver {
253
385
  mutationData: {
254
386
  attributeName,
255
387
  oldValue,
256
- newValue
388
+ newValue,
389
+ timestamp
257
390
  }
258
391
  };
259
392
  }
@@ -262,7 +395,8 @@ export default class ViewportObserver {
262
395
  mutationData: {
263
396
  attributeName,
264
397
  oldValue,
265
- newValue
398
+ newValue,
399
+ timestamp
266
400
  }
267
401
  };
268
402
  });
@@ -297,6 +431,7 @@ export default class ViewportObserver {
297
431
  this.intersectionObserver = null;
298
432
  this.mutationObserver = null;
299
433
  this.performanceObserver = null;
434
+ this.trackLayoutShiftOffenders = trackLayoutShiftOffenders;
300
435
 
301
436
  // Initialize SSR context functions
302
437
  this.getSSRState = getSSRState;
@@ -29,7 +29,7 @@ export function createIntersectionObserver({
29
29
  }
30
30
 
31
31
  // override as display-contents mutation
32
- if (tagOrCallbackResult && typeof tagOrCallbackResult !== 'string' && tagOrCallbackResult.type === 'mutation:attribute') {
32
+ if (tagOrCallbackResult && typeof tagOrCallbackResult !== 'string' && tagOrCallbackResult.type === 'mutation:attribute' && 'oldValue' in tagOrCallbackResult.mutationData) {
33
33
  const {
34
34
  attributeName,
35
35
  oldValue,
@@ -45,7 +45,8 @@ function createMutationObserver({
45
45
  target: mut.target,
46
46
  attributeName: (_mut$attributeName = mut.attributeName) !== null && _mut$attributeName !== void 0 ? _mut$attributeName : 'unknown',
47
47
  oldValue,
48
- newValue
48
+ newValue,
49
+ timestamp: mut.timestamp || performance.now()
49
50
  });
50
51
  }
51
52
  continue;