From 491debaabfe255b3c046c79db34defee146e5eec Mon Sep 17 00:00:00 2001
From: Connor Clark <cjamcl@gmail.com>
Date: Fri, 10 Oct 2025 09:46:44 -0700
Subject: [PATCH] Add onEachInteraction to onINP options

This adds an "onEachInteraction" option to onINP, which exposes to us
each interaction. It also calls attributeINP on each value given to the
callback.

---
 dist/modules/attribution/onINP.js | 13 +++++++++++--
 src/attribution/onINP.ts          | 47 +++++++++++++++++++++++++++++++++++++++-
 src/types/inp.ts                  |  1 +
 3 files changed, 56 insertions(+), 5 deletions(-)

diff --git a/dist/modules/attribution/onINP.js b/dist/modules/attribution/onINP.js
index 479a00f..d295263 100644
--- a/dist/modules/attribution/onINP.js
+++ b/dist/modules/attribution/onINP.js
@@ -121,7 +121,11 @@
                 group.startTime = Math.min(entry.startTime, group.startTime);
                 group.processingStart = Math.min(entry.processingStart, group.processingStart);
                 group.processingEnd = Math.max(entry.processingEnd, group.processingEnd);
-                group.entries.push(entry);
+                // Entries are not needed in DevTools since we're only displaying the
+                // summary information, and also emitting events as they come in. Stop
+                // holding a reference to avoid memory issues.
+                // See https://crbug.com/484342204
+                // group.entries.push(entry);
                 break;
             }
         }
@@ -135,7 +139,12 @@
             group = {
                 startTime: entry.startTime,
                 processingStart: entry.processingStart,
                 processingEnd: entry.processingEnd,
                 renderTime,
-                entries: [entry],
+                // Entries are not needed in DevTools since we're only displaying the
+                // summary information, and also emiting events as they come in. Stop
+                // holding a reference to avoid memory issues.
+                // See https://crbug.com/484342204
+                // entries: [entry],
+                entries: [],
             };
             pendingEntriesGroups.push(group);
         }
diff --git a/src/attribution/onINP.ts b/src/attribution/onINP.ts
index 479a00f..d295263 100644
--- a/src/attribution/onINP.ts
+++ b/src/attribution/onINP.ts
@@ -165,7 +165,11 @@
         group.processingEnd = Math.max(
           entry.processingEnd,
           group.processingEnd,
         );
-        group.entries.push(entry);
+        // Entries are not needed in DevTools since we're only displaying the
+        // summary information, and also emitting events as they come in. Stop
+        // holding a reference to avoid memory issues.
+        // See https://crbug.com/484342204
+        // group.entries.push(entry);
 
         break;
       }
@@ -186,7 +190,12 @@
         processingStart: entry.processingStart,
         processingEnd: entry.processingEnd,
         renderTime,
-        entries: [entry],
+        // Entries are not needed in DevTools since we're only displaying the
+        // summary information, and also emiting events as they come in. Stop
+        // holding a reference to avoid memory issues.
+        // See https://crbug.com/484342204
+        // entries: [entry],
+        entries: [],
       };
 
       pendingEntriesGroups.push(group);
@@ -240,7 +240,40 @@ export const onINP = (
     cleanupPending = false;
   };
 
-  interactionManager._onBeforeProcessingEntry = groupEntriesByRenderTime;
+  async function handleOnEachInteractionCallback(
+    entry: PerformanceEventTiming,
+  ) {
+    if (!opts.onEachInteraction) {
+      return;
+    }
+
+    // Wait a microtask so this "pre" processing callback actually
+    // becomes a "post" processing callback.
+    void (await Promise.resolve());
+    if (!entry.interactionId) {
+      return;
+    }
+
+    const interaction = attributeINP({
+      entries: [entry],
+      // The only value we really need for `attributeINP` is `entries`
+      // Everything else is included to fill out the type.
+      name: 'INP',
+      rating: 'good',
+      value: entry.duration,
+      delta: entry.duration,
+      navigationType: 'navigate',
+      id: 'N/A',
+    });
+    opts.onEachInteraction(interaction);
+  }
+
+  interactionManager._onBeforeProcessingEntry = (
+    entry: PerformanceEventTiming,
+  ) => {
+    void handleOnEachInteractionCallback(entry);
+    groupEntriesByRenderTime(entry);
+  };
   interactionManager._onAfterProcessingINPCandidate = saveInteractionTarget;
 
   const getIntersectingLoAFs = (
diff --git a/src/types/inp.ts b/src/types/inp.ts
index 7dfa9bb..f87a2c5 100644
--- a/src/types/inp.ts
+++ b/src/types/inp.ts
@@ -27,6 +27,7 @@ export interface INPReportOpts extends ReportOpts {
 
 export interface INPAttributionReportOpts extends AttributionReportOpts {
   durationThreshold?: number;
+  onEachInteraction?: (interaction: INPMetricWithAttribution) => void;
 }
 
 /**
-- 
2.51.0.760.g7b8bcc2412-goog
