diff --git a/README.md b/README.md index 2119358c..2da544b9 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.interactionTarget; 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; - + /** + * 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 @@ -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/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/types/inp.ts b/src/types/inp.ts index d469be47..c3acbd93 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 `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 @@ -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/test/e2e/onCLS-test.js b/test/e2e/onCLS-test.js index e56190f0..bf74bedb 100644 --- a/test/e2e/onCLS-test.js +++ b/test/e2e/onCLS-test.js @@ -866,7 +866,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..e2e77aaf 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,10 @@ describe('onINP()', async function () { assert.match(inp2.navigationType, /navigate|reload/); 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 +552,7 @@ describe('onINP()', async function () { containsEntry( inp2.attribution.processedEventEntries, 'keydown', - '#textarea', + '[object HTMLTextAreaElement]', ), ); @@ -649,6 +659,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 +687,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..a501d17f 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(() => { @@ -628,7 +628,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. 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) {