From 333181a41c5072ca19a27c1f565a5c71197b2e11 Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Sat, 11 May 2024 12:59:33 -0700 Subject: [PATCH 1/2] Expose the target element object in attribution --- src/attribution/onCLS.ts | 1 + src/attribution/onINP.ts | 1 + src/attribution/onLCP.ts | 3 ++- src/types/cls.ts | 7 +++++++ src/types/inp.ts | 20 ++++++++---------- src/types/lcp.ts | 12 +++++++++-- test/e2e/onCLS-test.js | 10 ++++++++- test/e2e/onINP-test.js | 45 ++++++++++++++++++++++++++++------------ test/e2e/onLCP-test.js | 28 ++++++++++++++++--------- test/views/layout.njk | 6 ++---- 10 files changed, 91 insertions(+), 42 deletions(-) diff --git a/src/attribution/onCLS.ts b/src/attribution/onCLS.ts index 52e2bee4..9895ba2f 100644 --- a/src/attribution/onCLS.ts +++ b/src/attribution/onCLS.ts @@ -43,6 +43,7 @@ const attributeCLS = (metric: CLSMetric): CLSMetricWithAttribution => { if (largestSource) { attribution = { largestShiftTarget: getSelector(largestSource.node), + largestShiftTargetElement: largestSource.node, largestShiftTime: largestEntry.startTime, largestShiftValue: largestEntry.value, largestShiftSource: largestSource, diff --git a/src/attribution/onINP.ts b/src/attribution/onINP.ts index e23f738d..361fa4f2 100644 --- a/src/attribution/onINP.ts +++ b/src/attribution/onINP.ts @@ -264,6 +264,7 @@ const attributeINP = (metric: INPMetric): INPMetricWithAttribution => { const attribution: INPAttribution = { interactionTarget: getSelector(interactionTargetElement), + interactionTargetElement: interactionTargetElement, interactionType: firstEntry.name.startsWith('key') ? 'keyboard' : 'pointer', interactionTime: firstEntry.startTime, nextPaintTime: nextPaintTime, diff --git a/src/attribution/onLCP.ts b/src/attribution/onLCP.ts index 285f319a..cf975366 100644 --- a/src/attribution/onLCP.ts +++ b/src/attribution/onLCP.ts @@ -64,7 +64,8 @@ const attributeLCP = (metric: LCPMetric): LCPMetricWithAttribution => { ); attribution = { - element: getSelector(lcpEntry.element), + target: getSelector(lcpEntry.element), + targetElement: lcpEntry.element, timeToFirstByte: ttfb, resourceLoadDelay: lcpRequestStart - ttfb, resourceLoadDuration: lcpResponseEnd - lcpRequestStart, diff --git a/src/types/cls.ts b/src/types/cls.ts index c79ce4c0..cea02548 100644 --- a/src/types/cls.ts +++ b/src/types/cls.ts @@ -36,6 +36,13 @@ export interface CLSAttribution { * CLS score occurred. */ largestShiftTarget?: string; + /** + * A reference to the HTML element identified by `largestShiftTarget`. + * NOTE: for attribution purpose, a selector identifying the element is + * typically more useful than the element itself. However, the element is + * also made available in case additional context is needed. + */ + largestShiftTargetElement?: Node; /** * The time when the single largest layout shift contributing to the page's * CLS score occurred. diff --git a/src/types/inp.ts b/src/types/inp.ts index d469be47..c4a62837 100644 --- a/src/types/inp.ts +++ b/src/types/inp.ts @@ -33,18 +33,23 @@ export interface INPAttribution { /** * A selector identifying the element that the user first interacted with * as part of the frame where the INP candidate interaction occurred. - * If `interactionTarget` is an empty string, that generally means the - * element was removed from the DOM after the interaction. + * If this value is an empty string, that generally means the element was + * removed from the DOM after the interaction. */ interactionTarget: string; - + /** + * A reference to the HTML element identified by `interactionTarget`. + * NOTE: for attribution purpose, a selector identifying the element is + * typically more useful than the element itself. However, the element is + * also made available in case additional context is needed. + */ + interactionTargetElement: Node | undefined; /** * The time when the user first interacted during the frame where the INP * candidate interaction occurred (if more than one interaction occurred * within the frame, only the first time is reported). */ interactionTime: DOMHighResTimeStamp; - /** * The best-guess timestamp of the next paint after the interaction. * In general, this timestamp is the same as the `startTime + duration` of @@ -57,7 +62,6 @@ export interface INPAttribution { * animation frame, which should be closer to the "real" value. */ nextPaintTime: DOMHighResTimeStamp; - /** * The type of interaction, based on the event type of the `event` entry * that corresponds to the interaction (i.e. the first `event` entry @@ -66,13 +70,11 @@ export interface INPAttribution { * and for "keydown" or "keyup" events this will be "keyboard". */ interactionType: 'pointer' | 'keyboard'; - /** * An array of Event Timing entries that were processed within the same * animation frame as the INP candidate interaction. */ processedEventEntries: PerformanceEventTiming[]; - /** * If the browser supports the Long Animation Frame API, this array will * include any `long-animation-frame` entries that intersect with the INP @@ -82,7 +84,6 @@ export interface INPAttribution { * are detect, this array will be empty. */ longAnimationFrameEntries: PerformanceLongAnimationFrameTiming[]; - /** * The time from when the user interacted with the page until when the * browser was first able to start processing event listeners for that @@ -90,13 +91,11 @@ export interface INPAttribution { * begin due to the main thread being busy with other work. */ inputDelay: number; - /** * The time from when the first event listener started running in response to * the user interaction until when all event listener processing has finished. */ processingDuration: number; - /** * The time from when the browser finished processing all event listeners for * the user interaction until the next frame is presented on the screen and @@ -106,7 +105,6 @@ export interface INPAttribution { * as off-main-thread work (such as compositor, GPU, and raster work). */ presentationDelay: number; - /** * The loading state of the document at the time when the interaction * corresponding to INP occurred (see `LoadState` for details). If the diff --git a/src/types/lcp.ts b/src/types/lcp.ts index 4761fdd1..ffeaf9b1 100644 --- a/src/types/lcp.ts +++ b/src/types/lcp.ts @@ -31,9 +31,17 @@ export interface LCPMetric extends Metric { */ export interface LCPAttribution { /** - * The element corresponding to the largest contentful paint for the page. + * A selector identifying the element corresponding to the largest contentful + * paint for the page. */ - element?: string; + target?: string; + /** + * A reference to the HTML element identified by `target`. + * NOTE: for attribution purpose, a selector identifying the element is + * typically more useful than the element itself. However, the element is + * also made available in case additional context is needed. + */ + targetElement?: Node; /** * The URL (if applicable) of the LCP image resource. If the LCP element * is a text node, this value will not be set. diff --git a/test/e2e/onCLS-test.js b/test/e2e/onCLS-test.js index e56190f0..576c5eff 100644 --- a/test/e2e/onCLS-test.js +++ b/test/e2e/onCLS-test.js @@ -756,6 +756,10 @@ describe('onCLS()', async function () { assert.equal(cls.attribution.largestShiftValue, largestShiftEntry.value); assert.equal(cls.attribution.largestShiftTarget, '#p3'); + assert.equal( + cls.attribution.largestShiftTargetElement, + '[object HTMLParagraphElement]', + ); assert.equal( cls.attribution.largestShiftTime, largestShiftEntry.startTime, @@ -802,6 +806,10 @@ describe('onCLS()', async function () { assert.equal(cls.attribution.largestShiftValue, largestShiftEntry.value); assert.equal(cls.attribution.largestShiftTarget, 'html>body>main>h1'); + assert.equal( + cls.attribution.largestShiftTargetElement, + '[object HTMLHeadingElement]', + ); assert.equal( cls.attribution.largestShiftTime, largestShiftEntry.startTime, @@ -866,7 +874,7 @@ function getAttribution(entries) { } const largestShiftSource = largestShiftEntry.sources.find((source) => { - return source.node !== '#text'; + return source.node !== '[object Text]'; }); return {largestShiftEntry, largestShiftSource}; diff --git a/test/e2e/onINP-test.js b/test/e2e/onINP-test.js index 4e9f8755..9a09e9df 100644 --- a/test/e2e/onINP-test.js +++ b/test/e2e/onINP-test.js @@ -58,7 +58,7 @@ describe('onINP()', async function () { assert.strictEqual(inp.name, 'INP'); assert.strictEqual(inp.value, inp.delta); assert.strictEqual(inp.rating, 'good'); - assert(containsEntry(inp.entries, 'click', 'h1')); + assert(containsEntry(inp.entries, 'click', '[object HTMLHeadingElement]')); assert(allEntriesPresentTogether(inp.entries)); assert.match(inp.navigationType, /navigate|reload/); }); @@ -81,7 +81,7 @@ describe('onINP()', async function () { assert.strictEqual(inp.name, 'INP'); assert.strictEqual(inp.value, inp.delta); assert.strictEqual(inp.rating, 'good'); - assert(containsEntry(inp.entries, 'click', 'h1')); + assert(containsEntry(inp.entries, 'click', '[object HTMLHeadingElement]')); assert(allEntriesPresentTogether(inp.entries)); assert.match(inp.navigationType, /navigate|reload/); }); @@ -109,7 +109,7 @@ describe('onINP()', async function () { assert.strictEqual(inp.name, 'INP'); assert.strictEqual(inp.value, inp.delta); assert.strictEqual(inp.rating, 'good'); - assert(containsEntry(inp.entries, 'click', 'h1')); + assert(containsEntry(inp.entries, 'click', '[object HTMLHeadingElement]')); assert(allEntriesPresentTogether(inp.entries)); assert.match(inp.navigationType, /navigate|reload/); }); @@ -135,7 +135,7 @@ describe('onINP()', async function () { assert.strictEqual(inp.name, 'INP'); assert.strictEqual(inp.value, inp.delta); assert.strictEqual(inp.rating, 'good'); - assert(containsEntry(inp.entries, 'click', 'h1')); + assert(containsEntry(inp.entries, 'click', '[object HTMLHeadingElement]')); assert(allEntriesPresentTogether(inp.entries)); assert.match(inp.navigationType, /navigate|reload/); }); @@ -158,7 +158,7 @@ describe('onINP()', async function () { assert.strictEqual(inp.name, 'INP'); assert.strictEqual(inp.value, inp.delta); assert.strictEqual(inp.rating, 'good'); - assert(containsEntry(inp.entries, 'click', 'h1')); + assert(containsEntry(inp.entries, 'click', '[object HTMLHeadingElement]')); assert(allEntriesPresentTogether(inp.entries)); assert.match(inp.navigationType, /navigate|reload/); }); @@ -183,7 +183,7 @@ describe('onINP()', async function () { assert.strictEqual(inp.name, 'INP'); assert.strictEqual(inp.value, inp.delta); assert.strictEqual(inp.rating, 'good'); - assert(containsEntry(inp.entries, 'click', 'h1')); + assert(containsEntry(inp.entries, 'click', '[object HTMLHeadingElement]')); assert(allEntriesPresentTogether(inp.entries)); assert.match(inp.navigationType, /navigate|reload/); }); @@ -312,7 +312,7 @@ describe('onINP()', async function () { assert.strictEqual(inp1.name, 'INP'); assert.strictEqual(inp1.value, inp1.delta); assert.strictEqual(inp1.rating, 'good'); - assert(containsEntry(inp1.entries, 'click', 'h1')); + assert(containsEntry(inp1.entries, 'click', '[object HTMLHeadingElement]')); assert(allEntriesPresentTogether(inp1.entries)); assert.match(inp1.navigationType, /navigate|reload/); @@ -337,7 +337,9 @@ describe('onINP()', async function () { assert.strictEqual(inp2.name, 'INP'); assert.strictEqual(inp2.value, inp2.delta); assert.strictEqual(inp2.rating, 'good'); - assert(containsEntry(inp2.entries, 'keydown', '#textarea')); + assert( + containsEntry(inp2.entries, 'keydown', '[object HTMLTextAreaElement]'), + ); assert(allEntriesPresentTogether(inp1.entries)); assert(inp2.entries[0].startTime > inp1.entries[0].startTime); assert.strictEqual(inp2.navigationType, 'back-forward-cache'); @@ -363,7 +365,9 @@ describe('onINP()', async function () { assert.strictEqual(inp3.name, 'INP'); assert.strictEqual(inp3.value, inp3.delta); assert.strictEqual(inp3.rating, 'needs-improvement'); - assert(containsEntry(inp3.entries, 'pointerdown', '#reset')); + assert( + containsEntry(inp3.entries, 'pointerdown', '[object HTMLButtonElement]'), + ); assert(allEntriesPresentTogether(inp3.entries)); assert(inp3.entries[0].startTime > inp2.entries[0].startTime); assert.strictEqual(inp3.navigationType, 'back-forward-cache'); @@ -403,7 +407,7 @@ describe('onINP()', async function () { assert.strictEqual(inp.name, 'INP'); assert.strictEqual(inp.value, inp.delta); assert.strictEqual(inp.rating, 'good'); - assert(containsEntry(inp.entries, 'click', 'h1')); + assert(containsEntry(inp.entries, 'click', '[object HTMLHeadingElement]')); assert(allEntriesPresentTogether(inp.entries)); assert.strictEqual(inp.navigationType, 'prerender'); }); @@ -428,7 +432,7 @@ describe('onINP()', async function () { assert.strictEqual(inp.name, 'INP'); assert.strictEqual(inp.value, inp.delta); assert.strictEqual(inp.rating, 'good'); - assert(containsEntry(inp.entries, 'click', 'h1')); + assert(containsEntry(inp.entries, 'click', '[object HTMLHeadingElement]')); assert(allEntriesPresentTogether(inp.entries)); assert.strictEqual(inp.navigationType, 'restore'); }); @@ -455,7 +459,9 @@ describe('onINP()', async function () { assert.strictEqual(inp1.name, 'INP'); assert.strictEqual(inp1.value, inp1.delta); assert.strictEqual(inp1.rating, 'good'); - assert(containsEntry(inp1.entries, 'click', 'h1')); + assert( + containsEntry(inp1.entries, 'click', '[object HTMLHeadingElement]'), + ); assert(allEntriesPresentTogether(inp1.entries)); assert.match(inp1.navigationType, /navigate|reload/); @@ -534,6 +540,11 @@ describe('onINP()', async function () { assert.match(inp2.navigationType, /navigate|reload/); assert.equal(inp2.attribution.interactionTarget, '#textarea'); + assert.equal(inp2.attribution.interactionTarget, '#textarea'); + assert.equal( + inp2.attribution.interactionTargetElement, + '[object HTMLTextAreaElement]', + ); assert.equal(inp2.attribution.interactionType, 'keyboard'); assert.equal(inp2.attribution.interactionTime, inp2.entries[0].startTime); assert.equal(inp2.attribution.loadState, 'complete'); @@ -542,7 +553,7 @@ describe('onINP()', async function () { containsEntry( inp2.attribution.processedEventEntries, 'keydown', - '#textarea', + '[object HTMLTextAreaElement]', ), ); @@ -649,6 +660,10 @@ describe('onINP()', async function () { // entry doesn't contain a target. // See: https://bugs.chromium.org/p/chromium/issues/detail?id=1367329 assert.equal(inp1.attribution.interactionTarget, 'html>body>main>h1'); + assert.equal( + inp1.attribution.interactionTargetElement, + '[object HTMLHeadingElement]', + ); }); it('reports the interaction target when target is removed from the DOM', async function () { @@ -673,6 +688,10 @@ describe('onINP()', async function () { assert.equal(inp.attribution.interactionType, 'pointer'); assert.equal(inp.attribution.interactionTarget, '#reset'); + assert.equal( + inp.attribution.interactionTargetElement, + '[object HTMLButtonElement]', + ); }); it('includes LoAF entries if the browser supports it', async function () { diff --git a/test/e2e/onLCP-test.js b/test/e2e/onLCP-test.js index 707ac7ba..50a62b9a 100644 --- a/test/e2e/onLCP-test.js +++ b/test/e2e/onLCP-test.js @@ -269,7 +269,7 @@ describe('onLCP()', async function () { assert.strictEqual(lcp1.value, lcp1.delta); assert.strictEqual(lcp1.rating, 'needs-improvement'); assert.strictEqual(lcp1.entries.length, 1); - assert.strictEqual(lcp1.entries[0].element, 'img'); + assert.strictEqual(lcp1.entries[0].element, '[object HTMLImageElement]'); assert.match(lcp1.navigationType, /navigate|reload/); }); @@ -304,7 +304,7 @@ describe('onLCP()', async function () { assert.strictEqual(lcp1.value, lcp1.delta); assert.strictEqual(lcp1.rating, 'good'); assert.strictEqual(lcp1.entries.length, 1); - assert.strictEqual(lcp1.entries[0].element, 'h1'); + assert.strictEqual(lcp1.entries[0].element, '[object HTMLHeadingElement]'); assert.match(lcp1.navigationType, /navigate|reload/); }); @@ -321,7 +321,7 @@ describe('onLCP()', async function () { assert.strictEqual(lcp.value, lcp.delta); assert.strictEqual(lcp.rating, 'good'); assert.strictEqual(lcp.entries.length, 1); - assert.strictEqual(lcp.entries[0].element, 'h1'); + assert.strictEqual(lcp.entries[0].element, '[object HTMLHeadingElement]'); assert.match(lcp.navigationType, /navigate|reload/); await clearBeacons(); @@ -461,7 +461,7 @@ describe('onLCP()', async function () { await imagesPainted(); const navEntry = await browser.execute(() => { - return performance.getEntriesByType('navigation')[0].toJSON(); + return __toSafeObject(performance.getEntriesByType('navigation')[0]); }); const lcpResEntry = await browser.execute(() => { @@ -480,7 +480,8 @@ describe('onLCP()', async function () { assertStandardReportsAreCorrect([lcp]); assert(lcp.attribution.url.endsWith('/test/img/square.png?delay=500')); - assert.equal(lcp.attribution.element, 'html>body>main>p>img'); + assert.equal(lcp.attribution.target, 'html>body>main>p>img'); + assert.equal(lcp.attribution.targetElement, '[object HTMLImageElement]'); assert.equal( lcp.attribution.timeToFirstByte + lcp.attribution.resourceLoadDelay + @@ -530,7 +531,8 @@ describe('onLCP()', async function () { assertStandardReportsAreCorrect([lcp]); assert(lcp.attribution.url.endsWith('/test/img/square.png?delay=500')); - assert.equal(lcp.attribution.element, 'html>body>main>p>img'); + assert.equal(lcp.attribution.target, 'html>body>main>p>img'); + assert.equal(lcp.attribution.targetElement, '[object HTMLImageElement]'); // Specifically check that resourceLoadDelay falls back to `startTime`. assert.equal( @@ -580,7 +582,8 @@ describe('onLCP()', async function () { assert(lcp.attribution.url.endsWith('/test/img/square.png?delay=500')); assert.equal(lcp.navigationType, 'prerender'); - assert.equal(lcp.attribution.element, 'html>body>main>p>img'); + assert.equal(lcp.attribution.target, 'html>body>main>p>img'); + assert.equal(lcp.attribution.targetElement, '[object HTMLImageElement]'); // Assert each individual LCP sub-part accounts for `activationStart` assert.equal( @@ -628,7 +631,7 @@ describe('onLCP()', async function () { }); const navEntry = await browser.execute(() => { - return performance.getEntriesByType('navigation')[0].toJSON(); + return __toSafeObject(performance.getEntriesByType('navigation')[0]); }); // Load a new page to trigger the hidden state. @@ -639,7 +642,11 @@ describe('onLCP()', async function () { const [lcp] = await getBeacons(); assert.equal(lcp.attribution.url, undefined); - assert.equal(lcp.attribution.element, 'html>body>main>h1'); + assert.equal(lcp.attribution.target, 'html>body>main>h1'); + assert.equal( + lcp.attribution.targetElement, + '[object HTMLHeadingElement]', + ); assert.equal(lcp.attribution.resourceLoadDelay, 0); assert.equal(lcp.attribution.resourceLoadDuration, 0); assert.equal( @@ -688,7 +695,8 @@ describe('onLCP()', async function () { assert.strictEqual(lcp2.entries.length, 0); assert.strictEqual(lcp2.navigationType, 'back-forward-cache'); - assert.equal(lcp2.attribution.element, undefined); + assert.equal(lcp2.attribution.target, undefined); + assert.equal(lcp2.attribution.targetElement, undefined); assert.equal(lcp2.attribution.timeToFirstByte, 0); assert.equal(lcp2.attribution.resourceLoadDelay, 0); assert.equal(lcp2.attribution.resourceLoadDuration, 0); diff --git a/test/views/layout.njk b/test/views/layout.njk index 93076b8c..65e71033 100644 --- a/test/views/layout.njk +++ b/test/views/layout.njk @@ -234,10 +234,8 @@ self.__toSafeObject = (oldObj) => { if (typeof oldObj !== 'object') { return oldObj; - } else if (oldObj instanceof Node) { - return oldObj?.id ? `#${oldObj.id}` : oldObj?.nodeName?.toLowerCase(); - } else if (oldObj instanceof Window) { - return '#window'; + } else if (oldObj instanceof EventTarget) { + return oldObj.toString(); } const newObj = {}; for (let key in oldObj) { From e97e0e8b7300c03e179a92f302a30a9d88e9f847 Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Sat, 11 May 2024 21:20:55 -0700 Subject: [PATCH 2/2] Revert changes to LCP and CLS --- README.md | 24 +++++++++++------------- src/attribution/onCLS.ts | 1 - src/attribution/onINP.ts | 2 +- src/attribution/onLCP.ts | 3 +-- src/types/cls.ts | 7 ------- src/types/inp.ts | 4 ++-- src/types/lcp.ts | 12 ++---------- test/e2e/onCLS-test.js | 8 -------- test/e2e/onINP-test.js | 15 ++++++++++----- test/e2e/onLCP-test.js | 18 +++++------------- 10 files changed, 32 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index 2119358c..c419e883 100644 --- a/README.md +++ b/README.md @@ -344,7 +344,7 @@ function sendToGoogleAnalytics({name, delta, value, id, attribution}) { eventParams.debug_target = attribution.largestShiftTarget; break; case 'INP': - eventParams.debug_target = attribution.eventTarget; + eventParams.debug_target = attribution.interactionTargetSelector; break; case 'LCP': eventParams.debug_target = attribution.element; @@ -897,18 +897,23 @@ interface INPAttribution { /** * A selector identifying the element that the user first interacted with * as part of the frame where the INP candidate interaction occurred. - * If `interactionTarget` is an empty string, that generally means the - * element was removed from the DOM after the interaction. + * If this value is an empty string, that generally means the element was + * removed from the DOM after the interaction. */ - interactionTarget: string; - + interactionTargetSelector: string; + /** + * A reference to the HTML element identified by `interactionTargetSelector`. + * NOTE: for attribution purpose, a selector identifying the element is + * typically more useful than the element itself. However, the element is + * also made available in case additional context is needed. + */ + interactionTargetElement: Node | undefined; /** * The time when the user first interacted during the frame where the INP * candidate interaction occurred (if more than one interaction occurred * within the frame, only the first time is reported). */ interactionTime: DOMHighResTimeStamp; - /** * The best-guess timestamp of the next paint after the interaction. * In general, this timestamp is the same as the `startTime + duration` of @@ -921,7 +926,6 @@ interface INPAttribution { * animation frame, which should be closer to the "real" value. */ nextPaintTime: DOMHighResTimeStamp; - /** * The type of interaction, based on the event type of the `event` entry * that corresponds to the interaction (i.e. the first `event` entry @@ -930,13 +934,11 @@ interface INPAttribution { * and for "keydown" or "keyup" events this will be "keyboard". */ interactionType: 'pointer' | 'keyboard'; - /** * An array of Event Timing entries that were processed within the same * animation frame as the INP candidate interaction. */ processedEventEntries: PerformanceEventTiming[]; - /** * If the browser supports the Long Animation Frame API, this array will * include any `long-animation-frame` entries that intersect with the INP @@ -946,7 +948,6 @@ interface INPAttribution { * are detect, this array will be empty. */ longAnimationFrameEntries: PerformanceLongAnimationFrameTiming[]; - /** * The time from when the user interacted with the page until when the * browser was first able to start processing event listeners for that @@ -954,13 +955,11 @@ interface INPAttribution { * begin due to the main thread being busy with other work. */ inputDelay: number; - /** * The time from when the first event listener started running in response to * the user interaction until when all event listener processing has finished. */ processingDuration: number; - /** * The time from when the browser finished processing all event listeners for * the user interaction until the next frame is presented on the screen and @@ -970,7 +969,6 @@ interface INPAttribution { * as off-main-thread work (such as compositor, GPU, and raster work). */ presentationDelay: number; - /** * The loading state of the document at the time when the interaction * corresponding to INP occurred (see `LoadState` for details). If the diff --git a/src/attribution/onCLS.ts b/src/attribution/onCLS.ts index 9895ba2f..52e2bee4 100644 --- a/src/attribution/onCLS.ts +++ b/src/attribution/onCLS.ts @@ -43,7 +43,6 @@ const attributeCLS = (metric: CLSMetric): CLSMetricWithAttribution => { if (largestSource) { attribution = { largestShiftTarget: getSelector(largestSource.node), - largestShiftTargetElement: largestSource.node, largestShiftTime: largestEntry.startTime, largestShiftValue: largestEntry.value, largestShiftSource: largestSource, diff --git a/src/attribution/onINP.ts b/src/attribution/onINP.ts index 361fa4f2..1f5ade04 100644 --- a/src/attribution/onINP.ts +++ b/src/attribution/onINP.ts @@ -263,7 +263,7 @@ const attributeINP = (metric: INPMetric): INPMetricWithAttribution => { const nextPaintTime = Math.max.apply(Math, nextPaintTimeCandidates); const attribution: INPAttribution = { - interactionTarget: getSelector(interactionTargetElement), + interactionTargetSelector: getSelector(interactionTargetElement), interactionTargetElement: interactionTargetElement, interactionType: firstEntry.name.startsWith('key') ? 'keyboard' : 'pointer', interactionTime: firstEntry.startTime, diff --git a/src/attribution/onLCP.ts b/src/attribution/onLCP.ts index cf975366..285f319a 100644 --- a/src/attribution/onLCP.ts +++ b/src/attribution/onLCP.ts @@ -64,8 +64,7 @@ const attributeLCP = (metric: LCPMetric): LCPMetricWithAttribution => { ); attribution = { - target: getSelector(lcpEntry.element), - targetElement: lcpEntry.element, + element: getSelector(lcpEntry.element), timeToFirstByte: ttfb, resourceLoadDelay: lcpRequestStart - ttfb, resourceLoadDuration: lcpResponseEnd - lcpRequestStart, diff --git a/src/types/cls.ts b/src/types/cls.ts index cea02548..c79ce4c0 100644 --- a/src/types/cls.ts +++ b/src/types/cls.ts @@ -36,13 +36,6 @@ export interface CLSAttribution { * CLS score occurred. */ largestShiftTarget?: string; - /** - * A reference to the HTML element identified by `largestShiftTarget`. - * NOTE: for attribution purpose, a selector identifying the element is - * typically more useful than the element itself. However, the element is - * also made available in case additional context is needed. - */ - largestShiftTargetElement?: Node; /** * The time when the single largest layout shift contributing to the page's * CLS score occurred. diff --git a/src/types/inp.ts b/src/types/inp.ts index c4a62837..7078f13e 100644 --- a/src/types/inp.ts +++ b/src/types/inp.ts @@ -36,9 +36,9 @@ export interface INPAttribution { * If this value is an empty string, that generally means the element was * removed from the DOM after the interaction. */ - interactionTarget: string; + interactionTargetSelector: string; /** - * A reference to the HTML element identified by `interactionTarget`. + * A reference to the HTML element identified by `interactionTargetSelector`. * NOTE: for attribution purpose, a selector identifying the element is * typically more useful than the element itself. However, the element is * also made available in case additional context is needed. diff --git a/src/types/lcp.ts b/src/types/lcp.ts index ffeaf9b1..4761fdd1 100644 --- a/src/types/lcp.ts +++ b/src/types/lcp.ts @@ -31,17 +31,9 @@ export interface LCPMetric extends Metric { */ export interface LCPAttribution { /** - * A selector identifying the element corresponding to the largest contentful - * paint for the page. + * The element corresponding to the largest contentful paint for the page. */ - target?: string; - /** - * A reference to the HTML element identified by `target`. - * NOTE: for attribution purpose, a selector identifying the element is - * typically more useful than the element itself. However, the element is - * also made available in case additional context is needed. - */ - targetElement?: Node; + element?: string; /** * The URL (if applicable) of the LCP image resource. If the LCP element * is a text node, this value will not be set. diff --git a/test/e2e/onCLS-test.js b/test/e2e/onCLS-test.js index 576c5eff..bf74bedb 100644 --- a/test/e2e/onCLS-test.js +++ b/test/e2e/onCLS-test.js @@ -756,10 +756,6 @@ describe('onCLS()', async function () { assert.equal(cls.attribution.largestShiftValue, largestShiftEntry.value); assert.equal(cls.attribution.largestShiftTarget, '#p3'); - assert.equal( - cls.attribution.largestShiftTargetElement, - '[object HTMLParagraphElement]', - ); assert.equal( cls.attribution.largestShiftTime, largestShiftEntry.startTime, @@ -806,10 +802,6 @@ describe('onCLS()', async function () { assert.equal(cls.attribution.largestShiftValue, largestShiftEntry.value); assert.equal(cls.attribution.largestShiftTarget, 'html>body>main>h1'); - assert.equal( - cls.attribution.largestShiftTargetElement, - '[object HTMLHeadingElement]', - ); assert.equal( cls.attribution.largestShiftTime, largestShiftEntry.startTime, diff --git a/test/e2e/onINP-test.js b/test/e2e/onINP-test.js index 9a09e9df..12ab49c9 100644 --- a/test/e2e/onINP-test.js +++ b/test/e2e/onINP-test.js @@ -465,7 +465,10 @@ describe('onINP()', async function () { assert(allEntriesPresentTogether(inp1.entries)); assert.match(inp1.navigationType, /navigate|reload/); - assert.equal(inp1.attribution.interactionTarget, 'html>body>main>h1'); + assert.equal( + inp1.attribution.interactionTargetSelector, + 'html>body>main>h1', + ); assert.equal(inp1.attribution.interactionType, 'pointer'); assert.equal(inp1.attribution.interactionTime, inp1.entries[0].startTime); assert.equal(inp1.attribution.loadState, 'complete'); @@ -539,8 +542,7 @@ describe('onINP()', async function () { assert(allEntriesPresentTogether(inp2.entries)); assert.match(inp2.navigationType, /navigate|reload/); - assert.equal(inp2.attribution.interactionTarget, '#textarea'); - assert.equal(inp2.attribution.interactionTarget, '#textarea'); + assert.equal(inp2.attribution.interactionTargetSelector, '#textarea'); assert.equal( inp2.attribution.interactionTargetElement, '[object HTMLTextAreaElement]', @@ -659,7 +661,10 @@ describe('onINP()', async function () { // The event target should match the h1, even if the `pointerup` // entry doesn't contain a target. // See: https://bugs.chromium.org/p/chromium/issues/detail?id=1367329 - assert.equal(inp1.attribution.interactionTarget, 'html>body>main>h1'); + assert.equal( + inp1.attribution.interactionTargetSelector, + 'html>body>main>h1', + ); assert.equal( inp1.attribution.interactionTargetElement, '[object HTMLHeadingElement]', @@ -687,7 +692,7 @@ describe('onINP()', async function () { const [inp] = await getBeacons(); assert.equal(inp.attribution.interactionType, 'pointer'); - assert.equal(inp.attribution.interactionTarget, '#reset'); + assert.equal(inp.attribution.interactionTargetSelector, '#reset'); assert.equal( inp.attribution.interactionTargetElement, '[object HTMLButtonElement]', diff --git a/test/e2e/onLCP-test.js b/test/e2e/onLCP-test.js index 50a62b9a..a501d17f 100644 --- a/test/e2e/onLCP-test.js +++ b/test/e2e/onLCP-test.js @@ -480,8 +480,7 @@ describe('onLCP()', async function () { assertStandardReportsAreCorrect([lcp]); assert(lcp.attribution.url.endsWith('/test/img/square.png?delay=500')); - assert.equal(lcp.attribution.target, 'html>body>main>p>img'); - assert.equal(lcp.attribution.targetElement, '[object HTMLImageElement]'); + assert.equal(lcp.attribution.element, 'html>body>main>p>img'); assert.equal( lcp.attribution.timeToFirstByte + lcp.attribution.resourceLoadDelay + @@ -531,8 +530,7 @@ describe('onLCP()', async function () { assertStandardReportsAreCorrect([lcp]); assert(lcp.attribution.url.endsWith('/test/img/square.png?delay=500')); - assert.equal(lcp.attribution.target, 'html>body>main>p>img'); - assert.equal(lcp.attribution.targetElement, '[object HTMLImageElement]'); + assert.equal(lcp.attribution.element, 'html>body>main>p>img'); // Specifically check that resourceLoadDelay falls back to `startTime`. assert.equal( @@ -582,8 +580,7 @@ describe('onLCP()', async function () { assert(lcp.attribution.url.endsWith('/test/img/square.png?delay=500')); assert.equal(lcp.navigationType, 'prerender'); - assert.equal(lcp.attribution.target, 'html>body>main>p>img'); - assert.equal(lcp.attribution.targetElement, '[object HTMLImageElement]'); + assert.equal(lcp.attribution.element, 'html>body>main>p>img'); // Assert each individual LCP sub-part accounts for `activationStart` assert.equal( @@ -642,11 +639,7 @@ describe('onLCP()', async function () { const [lcp] = await getBeacons(); assert.equal(lcp.attribution.url, undefined); - assert.equal(lcp.attribution.target, 'html>body>main>h1'); - assert.equal( - lcp.attribution.targetElement, - '[object HTMLHeadingElement]', - ); + assert.equal(lcp.attribution.element, 'html>body>main>h1'); assert.equal(lcp.attribution.resourceLoadDelay, 0); assert.equal(lcp.attribution.resourceLoadDuration, 0); assert.equal( @@ -695,8 +688,7 @@ describe('onLCP()', async function () { assert.strictEqual(lcp2.entries.length, 0); assert.strictEqual(lcp2.navigationType, 'back-forward-cache'); - assert.equal(lcp2.attribution.target, undefined); - assert.equal(lcp2.attribution.targetElement, undefined); + assert.equal(lcp2.attribution.element, undefined); assert.equal(lcp2.attribution.timeToFirstByte, 0); assert.equal(lcp2.attribution.resourceLoadDelay, 0); assert.equal(lcp2.attribution.resourceLoadDuration, 0);