Skip to content

Commit

Permalink
Expose the target element in INP attribution (#479)
Browse files Browse the repository at this point in the history
Revert changes to LCP and CLS

Revert to just using interactionTarget
  • Loading branch information
philipwalton authored May 12, 2024
1 parent 14d3e1d commit a12b628
Show file tree
Hide file tree
Showing 7 changed files with 59 additions and 46 deletions.
22 changes: 10 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -946,21 +948,18 @@ 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
* interaction. This time captures the delay before event processing can
* 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
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/attribution/onINP.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
20 changes: 9 additions & 11 deletions src/types/inp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -82,21 +84,18 @@ 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
* interaction. This time captures the delay before event processing can
* 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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/onCLS-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
44 changes: 31 additions & 13 deletions test/e2e/onINP-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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/);
});
Expand All @@ -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/);
});
Expand Down Expand Up @@ -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/);
});
Expand All @@ -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/);
});
Expand All @@ -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/);
});
Expand All @@ -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/);
});
Expand Down Expand Up @@ -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/);

Expand All @@ -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');
Expand All @@ -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');
Expand Down Expand Up @@ -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');
});
Expand All @@ -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');
});
Expand All @@ -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/);

Expand Down Expand Up @@ -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');
Expand All @@ -542,7 +552,7 @@ describe('onINP()', async function () {
containsEntry(
inp2.attribution.processedEventEntries,
'keydown',
'#textarea',
'[object HTMLTextAreaElement]',
),
);

Expand Down Expand Up @@ -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 () {
Expand All @@ -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 () {
Expand Down
10 changes: 5 additions & 5 deletions test/e2e/onLCP-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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/);
});

Expand Down Expand Up @@ -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/);
});

Expand All @@ -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();
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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.
Expand Down
6 changes: 2 additions & 4 deletions test/views/layout.njk
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down

0 comments on commit a12b628

Please sign in to comment.