Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose the target element in INP attribution #479

Merged
merged 1 commit into from
May 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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