@atlaskit/react-ufo 3.4.1 → 3.4.2

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/CHANGELOG.md +8 -0
  2. package/dist/cjs/additional-payload/utils/lighthouse-metrics/utils/observer/index.js +8 -2
  3. package/dist/cjs/create-payload/index.js +136 -76
  4. package/dist/cjs/segment/segment.js +29 -4
  5. package/dist/cjs/vc/vc-observer/index.js +71 -46
  6. package/dist/cjs/vc/vc-observer/observers/ssr-placeholders/index.js +18 -11
  7. package/dist/cjs/vc/vc-observer-new/get-element-name.js +52 -64
  8. package/dist/cjs/vc/vc-observer-new/get-unique-element-name.js +80 -0
  9. package/dist/cjs/vc/vc-observer-new/metric-calculator/fy25_03/index.js +7 -2
  10. package/dist/cjs/vc/vc-observer-new/metric-calculator/percentile-calc/canvas-heatmap/canvas-pixel.js +6 -4
  11. package/dist/cjs/vc/vc-observer-new/viewport-observer/index.js +17 -9
  12. package/dist/es2019/additional-payload/utils/lighthouse-metrics/utils/observer/index.js +8 -2
  13. package/dist/es2019/create-payload/index.js +39 -3
  14. package/dist/es2019/segment/segment.js +29 -3
  15. package/dist/es2019/vc/vc-observer/index.js +39 -12
  16. package/dist/es2019/vc/vc-observer/observers/ssr-placeholders/index.js +14 -7
  17. package/dist/es2019/vc/vc-observer-new/get-element-name.js +51 -64
  18. package/dist/es2019/vc/vc-observer-new/get-unique-element-name.js +74 -0
  19. package/dist/es2019/vc/vc-observer-new/metric-calculator/fy25_03/index.js +6 -1
  20. package/dist/es2019/vc/vc-observer-new/metric-calculator/percentile-calc/canvas-heatmap/canvas-pixel.js +6 -4
  21. package/dist/es2019/vc/vc-observer-new/viewport-observer/index.js +17 -9
  22. package/dist/esm/additional-payload/utils/lighthouse-metrics/utils/observer/index.js +8 -2
  23. package/dist/esm/create-payload/index.js +136 -76
  24. package/dist/esm/segment/segment.js +29 -4
  25. package/dist/esm/vc/vc-observer/index.js +71 -46
  26. package/dist/esm/vc/vc-observer/observers/ssr-placeholders/index.js +18 -11
  27. package/dist/esm/vc/vc-observer-new/get-element-name.js +52 -64
  28. package/dist/esm/vc/vc-observer-new/get-unique-element-name.js +74 -0
  29. package/dist/esm/vc/vc-observer-new/metric-calculator/fy25_03/index.js +6 -1
  30. package/dist/esm/vc/vc-observer-new/metric-calculator/percentile-calc/canvas-heatmap/canvas-pixel.js +6 -4
  31. package/dist/esm/vc/vc-observer-new/viewport-observer/index.js +17 -9
  32. package/dist/types/interaction-context/index.d.ts +2 -0
  33. package/dist/types/segment/segment.d.ts +2 -1
  34. package/dist/types/vc/vc-observer-new/get-unique-element-name.d.ts +8 -0
  35. package/dist/types/vc/vc-observer-new/metric-calculator/fy25_03/index.d.ts +1 -0
  36. package/dist/types-ts4.5/interaction-context/index.d.ts +2 -0
  37. package/dist/types-ts4.5/segment/segment.d.ts +2 -1
  38. package/dist/types-ts4.5/vc/vc-observer-new/get-unique-element-name.d.ts +8 -0
  39. package/dist/types-ts4.5/vc/vc-observer-new/metric-calculator/fy25_03/index.d.ts +1 -0
  40. package/package.json +7 -1
@@ -130,7 +130,37 @@ const getResourceTimings = (start, end) => {
130
130
  const getBundleEvalTimings = start => bundleEvalTiming.getBundleEvalTimings(start);
131
131
  const getSSRSuccess = type => type === 'page_load' ? ssr.getSSRSuccess() : undefined;
132
132
  const getSSRFeatureFlags = type => type === 'page_load' ? ssr.getSSRFeatureFlags() : undefined;
133
- const getPaintMetrics = type => {
133
+ const getLCP = end => {
134
+ return new Promise(resolve => {
135
+ let observer;
136
+ const timeout = setTimeout(() => {
137
+ var _observer;
138
+ (_observer = observer) === null || _observer === void 0 ? void 0 : _observer.disconnect();
139
+ resolve(null);
140
+ }, 200);
141
+ observer = new PerformanceObserver(list => {
142
+ const entries = Array.from(list.getEntries());
143
+ const lastEntry = entries.reduce((agg, entry) => {
144
+ // Use the latest LCP candidate before TTAI
145
+ if (entry.startTime <= end && (agg === null || agg.startTime < entry.startTime)) {
146
+ return entry;
147
+ }
148
+ return agg;
149
+ }, null);
150
+ clearTimeout(timeout);
151
+ if (!lastEntry || lastEntry === null) {
152
+ resolve(null);
153
+ } else {
154
+ resolve(lastEntry.startTime);
155
+ }
156
+ });
157
+ observer.observe({
158
+ type: "largest-contentful-paint",
159
+ buffered: true
160
+ });
161
+ });
162
+ };
163
+ const getPaintMetrics = async (type, end) => {
134
164
  if (type !== 'page_load') {
135
165
  return {};
136
166
  }
@@ -143,6 +173,12 @@ const getPaintMetrics = type => {
143
173
  metrics['metric:fcp'] = Math.round(entry.startTime);
144
174
  }
145
175
  });
176
+ if (fg('ufo_lcp')) {
177
+ const lcp = await getLCP(end);
178
+ if (lcp) {
179
+ metrics['metric:lcp'] = Math.round(lcp);
180
+ }
181
+ }
146
182
  return metrics;
147
183
  };
148
184
  const getTTAI = interaction => {
@@ -728,7 +764,7 @@ async function createInteractionMetricsPayload(interaction, interactionId, exper
728
764
  }
729
765
  const newUFOName = sanitizeUfoName(ufoName);
730
766
  const resourceTimings = getResourceTimings(start, end);
731
- const [vcMetrics, experimentalMetrics] = await Promise.all([getVCMetrics(interaction), experimental ? getExperimentalVCMetrics(interaction) : Promise.resolve(undefined)]);
767
+ const [vcMetrics, experimentalMetrics, paintMetrics] = await Promise.all([getVCMetrics(interaction), experimental ? getExperimentalVCMetrics(interaction) : Promise.resolve(undefined), getPaintMetrics(type, end)]);
732
768
  const payload = {
733
769
  actionSubject: 'experience',
734
770
  action: 'measured',
@@ -754,7 +790,7 @@ async function createInteractionMetricsPayload(interaction, interactionId, exper
754
790
  ...getSSRProperties(type),
755
791
  ...getAssetsMetrics(interaction, pageLoadInteractionMetrics === null || pageLoadInteractionMetrics === void 0 ? void 0 : pageLoadInteractionMetrics.SSRDoneTime),
756
792
  ...getPPSMetrics(interaction),
757
- ...getPaintMetrics(type),
793
+ ...paintMetrics,
758
794
  ...getNavigationMetrics(type),
759
795
  ...vcMetrics,
760
796
  ...experimentalMetrics,
@@ -15,15 +15,40 @@ let tryCompleteHandle;
15
15
  const AsyncSegmentHighlight = /*#__PURE__*/lazy(() => import( /* webpackChunkName: "@atlaskit-internal_ufo-segment-highlight" */'./segment-highlight').then(module => ({
16
16
  default: module.SegmentHighlight
17
17
  })));
18
+ const noopIdMap = new Map();
18
19
 
19
20
  /** A portion of the page we apply measurement to */
20
21
  export default function UFOSegment({
21
22
  name: segmentName,
22
- children
23
+ children,
24
+ mode = 'single'
23
25
  }) {
24
26
  var _getConfig2;
25
27
  const parentContext = useContext(UFOInteractionContext);
26
- const segmentId = useMemo(() => generateId(), []);
28
+ const segmentIdMap = useMemo(() => {
29
+ if (!fg('platform_ufo_segment_list_mode')) {
30
+ // just in case we cause rerender issues, use noop map
31
+ return noopIdMap;
32
+ }
33
+ if (!(parentContext !== null && parentContext !== void 0 && parentContext.segmentIdMap)) {
34
+ return new Map();
35
+ }
36
+ return parentContext.segmentIdMap;
37
+ }, [parentContext]);
38
+ const segmentId = useMemo(() => {
39
+ if (!fg('platform_ufo_segment_list_mode')) {
40
+ return generateId();
41
+ }
42
+ if (mode === 'single') {
43
+ return generateId();
44
+ }
45
+ if (segmentIdMap.has(segmentName)) {
46
+ return segmentIdMap.get(segmentName);
47
+ }
48
+ const newSegmentId = generateId();
49
+ segmentIdMap.set(segmentName, newSegmentId);
50
+ return newSegmentId;
51
+ }, [mode, segmentName, segmentIdMap]);
27
52
  const labelStack = useMemo(() => parentContext !== null && parentContext !== void 0 && parentContext.labelStack ? [...parentContext.labelStack, {
28
53
  name: segmentName,
29
54
  segmentId
@@ -108,6 +133,7 @@ export default function UFOSegment({
108
133
  }
109
134
  return {
110
135
  labelStack,
136
+ segmentIdMap: segmentIdMap,
111
137
  hold(name = 'unknown') {
112
138
  return this._internalHold(this.labelStack, name);
113
139
  },
@@ -204,7 +230,7 @@ export default function UFOSegment({
204
230
  _internalHoldByID,
205
231
  complete
206
232
  };
207
- }, [parentContext, labelStack, interactionId]);
233
+ }, [parentContext, labelStack, segmentIdMap, interactionId]);
208
234
  const hasMounted = useRef(false);
209
235
  const onRender = useCallback((_id, phase, actualDuration, baseDuration, startTime, commitTime) => {
210
236
  var _getConfig;
@@ -250,8 +250,33 @@ export class VCObserver {
250
250
  } catch (e) {
251
251
  /* do nothing */
252
252
  }
253
+ const isVCClean = !abortReasonInfo;
253
254
  const isMultiHeatmapEnabled = !fg('platform_ufo_multiheatmap_killswitch');
254
- const revisionsData = isMultiHeatmapEnabled && multiHeatmap !== null ? {
255
+ const revisionsData = isMultiHeatmapEnabled ? fg('platform_ufo_vc_observer_new') ? {
256
+ [`${fullPrefix}vc:rev`]: [{
257
+ revision: 'fy25.01',
258
+ clean: isVCClean,
259
+ 'metric:vc90': VC['90'],
260
+ vcDetails: Object.fromEntries(VCObserver.VCParts.map(key => {
261
+ var _VCBox$key;
262
+ return [key, {
263
+ t: VC[key],
264
+ e: (_VCBox$key = VCBox[key]) !== null && _VCBox$key !== void 0 ? _VCBox$key : []
265
+ }];
266
+ }))
267
+ }, {
268
+ revision: 'fy25.02',
269
+ clean: isVCClean,
270
+ 'metric:vc90': vcNext.VC['90'],
271
+ vcDetails: Object.fromEntries(VCObserver.VCParts.map(key => {
272
+ var _vcNext$VCBox$key;
273
+ return [key, {
274
+ t: vcNext.VC[key],
275
+ e: (_vcNext$VCBox$key = vcNext.VCBox[key]) !== null && _vcNext$VCBox$key !== void 0 ? _vcNext$VCBox$key : []
276
+ }];
277
+ }))
278
+ }]
279
+ } : multiHeatmap !== null ? {
255
280
  [`${fullPrefix}vc:rev`]: multiHeatmap === null || multiHeatmap === void 0 ? void 0 : multiHeatmap.getPayloadShapedData({
256
281
  VCParts: VCObserver.VCParts.map(v => parseInt(v)),
257
282
  VCCalculationMethods: getRevisions().map(({
@@ -264,9 +289,9 @@ export class VCObserver {
264
289
  interactionStart: start,
265
290
  ttai: stop,
266
291
  ssr,
267
- clean: !abortReasonInfo
292
+ clean: isVCClean
268
293
  })
269
- } : null;
294
+ } : null : null;
270
295
  // eslint-disable-next-line @atlaskit/platform/ensure-feature-flag-prefix
271
296
  const isCalcSpeedIndexEnabled = fg('ufo-calc-speed-index');
272
297
  const speedIndex = {
@@ -276,7 +301,7 @@ export class VCObserver {
276
301
  return {
277
302
  'metrics:vc': VC,
278
303
  [`${fullPrefix}vc:state`]: true,
279
- [`${fullPrefix}vc:clean`]: !abortReasonInfo,
304
+ [`${fullPrefix}vc:clean`]: isVCClean,
280
305
  [`${fullPrefix}vc:dom`]: VCBox,
281
306
  [`${fullPrefix}vc:updates`]: VCEntries.rel.slice(0, 50),
282
307
  // max 50
@@ -297,14 +322,16 @@ export class VCObserver {
297
322
  _defineProperty(this, "handleUpdate", (rawTime, intersectionRect, targetName, element, type, ignoreReason) => {
298
323
  this.measureStart();
299
324
  this.legacyHandleUpdate(rawTime, intersectionRect, targetName, element, type, ignoreReason);
300
- this.onViewportChangeDetected({
301
- timestamp: rawTime,
302
- intersectionRect,
303
- targetName,
304
- element,
305
- type,
306
- ignoreReason
307
- });
325
+ if (!fg('platform_ufo_vc_observer_new')) {
326
+ this.onViewportChangeDetected({
327
+ timestamp: rawTime,
328
+ intersectionRect,
329
+ targetName,
330
+ element,
331
+ type,
332
+ ignoreReason
333
+ });
334
+ }
308
335
  this.measureStop();
309
336
  });
310
337
  _defineProperty(this, "legacyHandleUpdate", (rawTime, intersectionRect, targetName, element, type, ignoreReason) => {
@@ -11,7 +11,8 @@ export class SSRPlaceholderHandlers {
11
11
  target,
12
12
  boundingClientRect
13
13
  }) => {
14
- this.intersectionObserver.unobserve(target);
14
+ var _this$intersectionObs;
15
+ (_this$intersectionObs = this.intersectionObserver) === null || _this$intersectionObs === void 0 ? void 0 : _this$intersectionObs.unobserve(target);
15
16
  if (!(target instanceof HTMLElement)) {
16
17
  // impossible case - keep typescript healthy
17
18
  return;
@@ -58,14 +59,17 @@ export class SSRPlaceholderHandlers {
58
59
  this.reactValidateCallbacks.delete(staticKey);
59
60
  }
60
61
  });
61
- this.intersectionObserver = new IntersectionObserver(entries => entries.filter(entry => entry.intersectionRatio > 0).forEach(this.intersectionObserverCallback));
62
+ if (typeof IntersectionObserver === 'function') {
63
+ // Only instantiate the IntersectionObserver if it's supported
64
+ this.intersectionObserver = new IntersectionObserver(entries => entries.filter(entry => entry.intersectionRatio > 0).forEach(this.intersectionObserverCallback));
65
+ }
62
66
  if (window.document) {
63
67
  try {
64
68
  const existingElements = document.querySelectorAll('[data-ssr-placeholder]');
65
69
  existingElements.forEach(el => {
66
70
  var _el$dataset;
67
71
  if (el instanceof HTMLElement && el !== null && el !== void 0 && (_el$dataset = el.dataset) !== null && _el$dataset !== void 0 && _el$dataset.ssrPlaceholder) {
68
- var _window$__SSR_PLACEHO;
72
+ var _window$__SSR_PLACEHO, _this$intersectionObs2;
69
73
  let width = -1;
70
74
  let height = -1;
71
75
  let x = -1;
@@ -83,7 +87,7 @@ export class SSRPlaceholderHandlers {
83
87
  x,
84
88
  y
85
89
  });
86
- this.intersectionObserver.observe(el);
90
+ (_this$intersectionObs2 = this.intersectionObserver) === null || _this$intersectionObs2 === void 0 ? void 0 : _this$intersectionObs2.observe(el);
87
91
  }
88
92
  });
89
93
  } catch (e) {} finally {
@@ -130,15 +134,17 @@ export class SSRPlaceholderHandlers {
130
134
  resolve(false);
131
135
  return;
132
136
  } else {
137
+ var _this$intersectionObs3;
133
138
  this.callbacks.set(id, resolve);
134
- this.intersectionObserver.observe(el);
139
+ (_this$intersectionObs3 = this.intersectionObserver) === null || _this$intersectionObs3 === void 0 ? void 0 : _this$intersectionObs3.observe(el);
135
140
  }
136
141
  });
137
142
  }
138
143
  getSize(el) {
139
144
  return new Promise(resolve => {
145
+ var _this$intersectionObs4;
140
146
  this.getSizeCallbacks.set(el.dataset.ssrPlaceholder || '', resolve);
141
- this.intersectionObserver.observe(el);
147
+ (_this$intersectionObs4 = this.intersectionObserver) === null || _this$intersectionObs4 === void 0 ? void 0 : _this$intersectionObs4.observe(el);
142
148
  });
143
149
  }
144
150
  validateReactComponentMatchToPlaceholder(el) {
@@ -149,8 +155,9 @@ export class SSRPlaceholderHandlers {
149
155
  resolve(false);
150
156
  return;
151
157
  } else {
158
+ var _this$intersectionObs5;
152
159
  this.reactValidateCallbacks.set(id, resolve);
153
- this.intersectionObserver.observe(el);
160
+ (_this$intersectionObs5 = this.intersectionObserver) === null || _this$intersectionObs5 === void 0 ? void 0 : _this$intersectionObs5.observe(el);
154
161
  }
155
162
  });
156
163
  }
@@ -1,65 +1,4 @@
1
1
  const nameCache = new WeakMap();
2
- function getAttributeSelector(element, attributeName) {
3
- const attrValue = element.getAttribute(attributeName);
4
- if (!attrValue) {
5
- return '';
6
- }
7
- return `[${attributeName}="${attrValue}"]`;
8
- }
9
- function isValidSelector(selector) {
10
- try {
11
- document.querySelector(selector);
12
- return true;
13
- } catch (err) {
14
- return false;
15
- }
16
- }
17
- function isSelectorUnique(selector) {
18
- return document.querySelectorAll(selector).length === 1;
19
- }
20
- function getUniqueSelector(selectorConfig, element) {
21
- let currentElement = element;
22
- const parts = [];
23
- const MAX_DEPTH = 3;
24
- let currentDepth = 0;
25
- while (currentElement && currentElement.localName !== 'body' && currentDepth <= MAX_DEPTH) {
26
- const tagName = currentElement.localName;
27
- let selectorPart = tagName;
28
- if (selectorConfig.id && currentElement.id && isValidSelector(`#${currentElement.id}`)) {
29
- selectorPart += `#${currentElement.id}`;
30
- } else if (selectorConfig.dataVC) {
31
- selectorPart += getAttributeSelector(currentElement, 'data-vc');
32
- } else if (selectorConfig.testId) {
33
- selectorPart += getAttributeSelector(currentElement, 'data-testid') || getAttributeSelector(currentElement, 'data-test-id');
34
- } else if (selectorConfig.role) {
35
- selectorPart += getAttributeSelector(currentElement, 'role');
36
- } else if (selectorConfig.className && currentElement.className) {
37
- const classNames = Array.from(currentElement.classList).join('.');
38
- if (classNames) {
39
- if (isValidSelector(`.${classNames}`)) {
40
- selectorPart += `.${classNames}`;
41
- }
42
- }
43
- }
44
- parts.unshift(selectorPart);
45
- const potentialSelector = parts.join(' > ').trim();
46
- if (potentialSelector && isSelectorUnique(potentialSelector)) {
47
- return potentialSelector;
48
- }
49
- currentElement = currentElement.parentElement;
50
- currentDepth++;
51
- }
52
- const potentialSelector = parts.join(' > ').trim();
53
- if (!potentialSelector) {
54
- return 'unknown';
55
- } else if (!isSelectorUnique(potentialSelector)) {
56
- const parentElement = element.parentElement;
57
- if (parentElement) {
58
- return `${potentialSelector}:nth-child`; // NOTE: invalid DOM selector, but enough information for VC
59
- }
60
- }
61
- return potentialSelector;
62
- }
63
2
  export default function getElementName(selectorConfig, element) {
64
3
  if (!(element instanceof HTMLElement)) {
65
4
  return 'error';
@@ -68,7 +7,55 @@ export default function getElementName(selectorConfig, element) {
68
7
  if (cachedName) {
69
8
  return cachedName;
70
9
  }
71
- const uniqueSelector = getUniqueSelector(selectorConfig, element);
72
- nameCache.set(element, uniqueSelector);
73
- return uniqueSelector;
10
+ // Get the tag name of the element.
11
+ const tagName = element.localName;
12
+ const encodeValue = s => {
13
+ try {
14
+ return encodeURIComponent(s);
15
+ } catch (e) {
16
+ return 'malformed_value';
17
+ }
18
+ };
19
+
20
+ // Helper function to construct attribute selectors.
21
+ const getAttributeSelector = (attributeName, prefix = '') => {
22
+ const attrValue = element.getAttribute(attributeName);
23
+ if (!attrValue) {
24
+ return '';
25
+ }
26
+ const encondedAttrValue = encodeValue(attrValue);
27
+ return `${prefix}[${attributeName}="${encondedAttrValue}"]`;
28
+ };
29
+
30
+ // Construct the data-vc attribute selector if specified in the config.
31
+ const dataVC = selectorConfig.dataVC ? getAttributeSelector('data-vc') : '';
32
+
33
+ // Construct the ID selector if specified in the config and the element has an ID.
34
+ const id = selectorConfig.id && element.id ? `#${encodeValue(element.id)}` : '';
35
+
36
+ // Construct the test ID selector if specified in the config.
37
+ const testId = selectorConfig.testId ? getAttributeSelector('data-testid') || getAttributeSelector('data-test-id') : '';
38
+
39
+ // Construct the role selector if specified in the config.
40
+ const role = selectorConfig.role ? getAttributeSelector('role') : '';
41
+ const classNames = Array.from(element.classList).map(encodeValue).join('.');
42
+ // Construct the class list selector if specified in the config.
43
+ const classList = selectorConfig.className && classNames ? `.${classNames}` : '';
44
+
45
+ // Combine primary attribute selectors (id, testId, role) into a single string.
46
+ const primaryAttributes = [id, testId, role].filter(Boolean).join('');
47
+
48
+ // Use dataVC if available, otherwise use the primary attributes.
49
+ const attributes = dataVC || primaryAttributes;
50
+
51
+ // If no attributes or class list, recursively get the parent's name.
52
+ if (!attributes && !classList) {
53
+ const parentName = element.parentElement ? getElementName(selectorConfig, element.parentElement) : 'unknown';
54
+ return `${parentName} > ${tagName}`;
55
+ }
56
+
57
+ // Return the final constructed name: tagName + attributes or classList.
58
+ const name = `${tagName}${attributes || classList}`;
59
+ nameCache.set(element, name);
60
+ return name;
74
61
  }
@@ -0,0 +1,74 @@
1
+ const nameCache = new WeakMap();
2
+ function getAttributeSelector(element, attributeName) {
3
+ const attrValue = element.getAttribute(attributeName);
4
+ if (!attrValue) {
5
+ return '';
6
+ }
7
+ return `[${attributeName}="${attrValue}"]`;
8
+ }
9
+ function isValidSelector(selector) {
10
+ try {
11
+ document.querySelector(selector);
12
+ return true;
13
+ } catch (err) {
14
+ return false;
15
+ }
16
+ }
17
+ function isSelectorUnique(selector) {
18
+ return document.querySelectorAll(selector).length === 1;
19
+ }
20
+ function getUniqueSelector(selectorConfig, element) {
21
+ let currentElement = element;
22
+ const parts = [];
23
+ const MAX_DEPTH = 3;
24
+ let currentDepth = 0;
25
+ while (currentElement && currentElement.localName !== 'body' && currentDepth <= MAX_DEPTH) {
26
+ const tagName = currentElement.localName;
27
+ let selectorPart = tagName;
28
+ if (selectorConfig.id && currentElement.id && isValidSelector(`#${currentElement.id}`)) {
29
+ selectorPart += `#${currentElement.id}`;
30
+ } else if (selectorConfig.dataVC) {
31
+ selectorPart += getAttributeSelector(currentElement, 'data-vc');
32
+ } else if (selectorConfig.testId) {
33
+ selectorPart += getAttributeSelector(currentElement, 'data-testid') || getAttributeSelector(currentElement, 'data-test-id');
34
+ } else if (selectorConfig.role) {
35
+ selectorPart += getAttributeSelector(currentElement, 'role');
36
+ } else if (selectorConfig.className && currentElement.className) {
37
+ const classNames = Array.from(currentElement.classList).join('.');
38
+ if (classNames) {
39
+ if (isValidSelector(`.${classNames}`)) {
40
+ selectorPart += `.${classNames}`;
41
+ }
42
+ }
43
+ }
44
+ parts.unshift(selectorPart);
45
+ const potentialSelector = parts.join(' > ').trim();
46
+ if (potentialSelector && isSelectorUnique(potentialSelector)) {
47
+ return potentialSelector;
48
+ }
49
+ currentElement = currentElement.parentElement;
50
+ currentDepth++;
51
+ }
52
+ const potentialSelector = parts.join(' > ').trim();
53
+ if (!potentialSelector) {
54
+ return 'unknown';
55
+ } else if (!isSelectorUnique(potentialSelector)) {
56
+ const parentElement = element.parentElement;
57
+ if (parentElement) {
58
+ return `${potentialSelector}:nth-child`; // NOTE: invalid DOM selector, but enough information for VC
59
+ }
60
+ }
61
+ return potentialSelector;
62
+ }
63
+ export default function getElementName(selectorConfig, element) {
64
+ if (!(element instanceof HTMLElement)) {
65
+ return 'error';
66
+ }
67
+ const cachedName = nameCache.get(element);
68
+ if (cachedName) {
69
+ return cachedName;
70
+ }
71
+ const uniqueSelector = getUniqueSelector(selectorConfig, element);
72
+ nameCache.set(element, uniqueSelector);
73
+ return uniqueSelector;
74
+ }
@@ -3,6 +3,11 @@ import isViewportEntryData from '../utils/is-viewport-entry-data';
3
3
  const ABORTING_WINDOW_EVENT = ['wheel', 'scroll', 'keydown', 'resize'];
4
4
  const REVISION_NO = 'fy25.03';
5
5
  const CONSIDERED_ENTRY_TYPE = ['mutation:child-element', 'mutation:element', 'mutation:attribute', 'layout-shift', 'window:event'];
6
+
7
+ // TODO: AFO-3523
8
+ // Those are the attributes we have found when testing the 'fy25.03' manually.
9
+ // We still need to replace this hardcoded list with a proper automation
10
+ export const KNOWN_ATTRIBUTES_THAT_DOES_NOT_CAUSE_LAYOUT_SHIFTS = ['data-drop-target-for-element', 'draggable'];
6
11
  export default class VCCalculator_FY25_03 extends AbstractVCCalculatorBase {
7
12
  constructor() {
8
13
  super(REVISION_NO);
@@ -14,7 +19,7 @@ export default class VCCalculator_FY25_03 extends AbstractVCCalculatorBase {
14
19
  if (entry.type === 'mutation:attribute') {
15
20
  const entryData = entry.data;
16
21
  const attributeName = entryData.attributeName;
17
- if (!attributeName) {
22
+ if (!attributeName || KNOWN_ATTRIBUTES_THAT_DOES_NOT_CAUSE_LAYOUT_SHIFTS.includes(attributeName)) {
18
23
  return false;
19
24
  }
20
25
  return true;
@@ -28,12 +28,14 @@ export class ViewportCanvas {
28
28
  this.scaleFactor = scaleFactor;
29
29
  this.colorCounter = 1;
30
30
  this.colorTimeMap = new Map();
31
+ const safeViewportWidth = Math.max(viewport.width, 1);
32
+ const safeViewportHeight = Math.max(viewport.height, 1);
31
33
 
32
34
  // Calculate scaled dimensions
33
- this.scaledWidth = Math.ceil(viewport.width * scaleFactor);
34
- this.scaledHeight = Math.ceil(viewport.height * scaleFactor);
35
- this.scaleX = this.scaledWidth / viewport.width;
36
- this.scaleY = this.scaledHeight / viewport.height;
35
+ this.scaledWidth = Math.max(Math.ceil(safeViewportWidth * scaleFactor), 1);
36
+ this.scaledHeight = Math.max(Math.ceil(safeViewportHeight * scaleFactor), 1);
37
+ this.scaleX = this.scaledWidth / safeViewportWidth;
38
+ this.scaleY = this.scaledHeight / safeViewportHeight;
37
39
 
38
40
  // Initialize OffscreenCanvas with scaled dimensions
39
41
  this.canvas = document.createElement('canvas');
@@ -9,7 +9,9 @@ function isElementVisible(element) {
9
9
  try {
10
10
  const visible = element.checkVisibility({
11
11
  // @ts-expect-error
12
- visibilityProperty: true
12
+ visibilityProperty: true,
13
+ contentVisibilityAuto: true,
14
+ opacityProperty: true
13
15
  });
14
16
  return visible;
15
17
  } catch (e) {
@@ -42,6 +44,9 @@ export default class ViewportObserver {
42
44
  type,
43
45
  mutationData
44
46
  }) => {
47
+ if (!target) {
48
+ return;
49
+ }
45
50
  const visible = isElementVisible(target);
46
51
  const lastElementRect = this.mapVisibleNodeRects.get(target);
47
52
  this.mapVisibleNodeRects.set(target, rect);
@@ -132,14 +137,17 @@ export default class ViewportObserver {
132
137
  changedRects
133
138
  }) => {
134
139
  for (const changedRect of changedRects) {
135
- onChange({
136
- time,
137
- elementRef: new WeakRef(changedRect.node),
138
- visible: true,
139
- rect: changedRect.rect,
140
- previousRect: changedRect.previousRect,
141
- type: 'layout-shift'
142
- });
140
+ const target = changedRect.node;
141
+ if (target) {
142
+ onChange({
143
+ time,
144
+ elementRef: new WeakRef(target),
145
+ visible: true,
146
+ rect: changedRect.rect,
147
+ previousRect: changedRect.previousRect,
148
+ type: 'layout-shift'
149
+ });
150
+ }
143
151
  }
144
152
  }
145
153
  });
@@ -2,6 +2,10 @@ import { PerformanceObserverEntryTypes } from '../../const';
2
2
  import { EntriesBuffer } from '../buffer';
3
3
  var pe = null;
4
4
  var getObserver = function getObserver() {
5
+ if (typeof PerformanceObserver !== 'function') {
6
+ // Only instantiate the IntersectionObserver if it's supported
7
+ return null;
8
+ }
5
9
  if (pe !== null) {
6
10
  return pe;
7
11
  }
@@ -18,13 +22,15 @@ var getObserver = function getObserver() {
18
22
  return pe;
19
23
  };
20
24
  export var startLSObserver = function startLSObserver() {
21
- getObserver().observe({
25
+ var _getObserver;
26
+ (_getObserver = getObserver()) === null || _getObserver === void 0 || _getObserver.observe({
22
27
  type: PerformanceObserverEntryTypes.LayoutShift,
23
28
  buffered: true
24
29
  });
25
30
  };
26
31
  export var startLTObserver = function startLTObserver() {
27
- getObserver().observe({
32
+ var _getObserver2;
33
+ (_getObserver2 = getObserver()) === null || _getObserver2 === void 0 || _getObserver2.observe({
28
34
  type: PerformanceObserverEntryTypes.LongTask,
29
35
  buffered: true
30
36
  });