From 42c483fe50895140f0305521a6f75df620b83151 Mon Sep 17 00:00:00 2001 From: Hugo Garrido y Saez <9059402+HugoGarrido@users.noreply.github.com> Date: Tue, 10 Feb 2026 14:36:45 +0100 Subject: [PATCH 01/22] =?UTF-8?q?=E2=9A=97=20feat:=20add=20new=20flag=20fo?= =?UTF-8?q?r=20the=20feature?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/src/tools/experimentalFeatures.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/src/tools/experimentalFeatures.ts b/packages/core/src/tools/experimentalFeatures.ts index 019df815e1..d46cc2bb4a 100644 --- a/packages/core/src/tools/experimentalFeatures.ts +++ b/packages/core/src/tools/experimentalFeatures.ts @@ -23,6 +23,7 @@ export enum ExperimentalFeature { USE_CHANGE_RECORDS = 'use_change_records', SOURCE_CODE_CONTEXT = 'source_code_context', LCP_SUBPARTS = 'lcp_subparts', + INP_SUBPARTS = 'inp_subparts', } const enabledExperimentalFeatures: Set = new Set() From 6fc488f791bba2dbc42679ef3dc60f327828d5de Mon Sep 17 00:00:00 2001 From: Hugo Garrido y Saez <9059402+HugoGarrido@users.noreply.github.com> Date: Tue, 10 Feb 2026 14:37:53 +0100 Subject: [PATCH 02/22] =?UTF-8?q?=E2=9A=97=20feat:=20update=20types=20for?= =?UTF-8?q?=20inp=20supbarts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/view/viewMetrics/trackInteractionToNextPaint.ts | 5 +++++ packages/rum-core/src/rawRumEvent.types.ts | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts index 10e019bef3..82ba4b02f6 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts @@ -22,6 +22,11 @@ export interface InteractionToNextPaint { value: Duration targetSelector?: string time?: Duration + subParts?: { + inputDelay: Duration + processingDuration: Duration + presentationDelay: Duration + } } /** * Track the interaction to next paint (INP). diff --git a/packages/rum-core/src/rawRumEvent.types.ts b/packages/rum-core/src/rawRumEvent.types.ts index de4a594878..614f17b894 100644 --- a/packages/rum-core/src/rawRumEvent.types.ts +++ b/packages/rum-core/src/rawRumEvent.types.ts @@ -199,6 +199,11 @@ export interface ViewPerformanceData { duration: ServerDuration timestamp?: ServerDuration target_selector?: string + sub_parts?: { + input_delay: ServerDuration + processing_duration: ServerDuration + presentation_delay: ServerDuration + } } lcp?: { timestamp: ServerDuration From 190734dc7133650b97565e4e090a1f802755b87f Mon Sep 17 00:00:00 2001 From: Hugo Garrido y Saez <9059402+HugoGarrido@users.noreply.github.com> Date: Tue, 10 Feb 2026 15:21:15 +0100 Subject: [PATCH 03/22] =?UTF-8?q?=E2=9A=97=20feat:=20add=20map=20to=20grou?= =?UTF-8?q?p=20entries=20for=20subpart=20computation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../trackInteractionToNextPaint.ts | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts index 82ba4b02f6..10eca527c9 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts @@ -18,6 +18,8 @@ const MAX_INTERACTION_ENTRIES = 10 // Arbitrary value to cap INP outliers export const MAX_INP_VALUE = (1 * ONE_MINUTE) as Duration +const RENDER_TIME_GROUPING_THRESHOLD = 8 as Duration + export interface InteractionToNextPaint { value: Duration targetSelector?: string @@ -28,6 +30,16 @@ export interface InteractionToNextPaint { presentationDelay: Duration } } + +interface EntriesGroup { + startTime: RelativeTime + processingStart: RelativeTime + processingEnd: RelativeTime + // Reference time use for grouping, set once for each group + referenceRenderTime: RelativeTime + entries: Array +} + /** * Track the interaction to next paint (INP). * To avoid outliers, return the p98 worst interaction of the view. @@ -56,6 +68,54 @@ export function trackInteractionToNextPaint( let interactionToNextPaintTargetSelector: string | undefined let interactionToNextPaintStartTime: Duration | undefined + // Entry grouping for subparts calculation + const groupsByInteractionId = new Map() + + function updateGroupWithEntry(group: EntriesGroup, entry: RumPerformanceEventTiming | RumFirstInputTiming) { + group.startTime = Math.min(entry.startTime, group.startTime) as RelativeTime + + // For each group, we keep the biggest interval possible between processingStart and processingEnd + group.processingStart = Math.min(entry.processingStart, group.processingStart) as RelativeTime + group.processingEnd = Math.max(entry.processingEnd, group.processingEnd) as RelativeTime + group.entries.push(entry) + } + + function groupEntriesByRenderTime(entry: RumPerformanceEventTiming | RumFirstInputTiming) { + if (!entry.interactionId || !entry.processingStart || !entry.processingEnd) { + return + } + + const renderTime = (entry.startTime + entry.duration) as RelativeTime + + // Check if this interactionId already has a group + const existingGroup = groupsByInteractionId.get(entry.interactionId) + + if (existingGroup) { + // Update existing group with MIN/MAX values (keep original referenceRenderTime) + updateGroupWithEntry(existingGroup, entry) + return + } + + // Try to find a group within 8ms window to merge with (different interactionId, same frame) + for (const [, group] of groupsByInteractionId.entries()) { + if (Math.abs(renderTime - group.referenceRenderTime) <= RENDER_TIME_GROUPING_THRESHOLD) { + updateGroupWithEntry(group, entry) + // Also store under this entry's interactionId for easy lookup + groupsByInteractionId.set(entry.interactionId, group) + return + } + } + + // Create new group + groupsByInteractionId.set(entry.interactionId, { + startTime: entry.startTime, + processingStart: entry.processingStart, + processingEnd: entry.processingEnd, + referenceRenderTime: renderTime, + entries: [entry], + }) + } + function handleEntries(entries: Array) { for (const entry of entries) { if ( @@ -65,6 +125,7 @@ export function trackInteractionToNextPaint( entry.startTime <= viewEnd ) { longestInteractions.process(entry) + groupEntriesByRenderTime(entry) } } From f03e852d125dead7804b02ec882b8315d6f383e3 Mon Sep 17 00:00:00 2001 From: Hugo Garrido y Saez <9059402+HugoGarrido@users.noreply.github.com> Date: Tue, 10 Feb 2026 15:24:44 +0100 Subject: [PATCH 04/22] =?UTF-8?q?=E2=9A=97=20feat:=20compute=20inp=20subpa?= =?UTF-8?q?rts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../trackInteractionToNextPaint.ts | 44 ++++++++++++++++++- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts index 10eca527c9..40be3d2d97 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts @@ -1,4 +1,4 @@ -import { elapsed, noop, ONE_MINUTE } from '@datadog/browser-core' +import { elapsed, noop, ONE_MINUTE, ExperimentalFeature, isExperimentalFeatureEnabled } from '@datadog/browser-core' import type { Duration, RelativeTime } from '@datadog/browser-core' import { createPerformanceObservable, @@ -67,6 +67,7 @@ export function trackInteractionToNextPaint( let interactionToNextPaint = -1 as Duration let interactionToNextPaintTargetSelector: string | undefined let interactionToNextPaintStartTime: Duration | undefined + let interactionToNextPaintSubParts: InteractionToNextPaint['subParts'] | undefined // Entry grouping for subparts calculation const groupsByInteractionId = new Map() @@ -116,6 +117,36 @@ export function trackInteractionToNextPaint( }) } + function computeInpSubParts( + entry: RumPerformanceEventTiming | RumFirstInputTiming, + inpDuration: Duration + ): InteractionToNextPaint['subParts'] | undefined { + if (!entry.processingStart || !entry.processingEnd) { + return undefined + } + + // Get group timing by interactionId (or use individual entry if no group) + const group = entry.interactionId ? groupsByInteractionId.get(entry.interactionId) : undefined + + const { startTime, processingStart, processingEnd: processingEndRaw } = group || entry; + + // Prevents reported value to happen before processingStart. + // We group values around startTime +/- RENDER_TIME_GROUPING_THRESHOLD duration so some entries can be before processingStart. + const nextPaintTime = Math.max( + (entry.startTime + inpDuration) as RelativeTime, + processingStart + ) as RelativeTime + + // Clamp processingEnd to not exceed nextPaintTime + const processingEnd = Math.min(processingEndRaw, nextPaintTime) as RelativeTime + + return { + inputDelay: elapsed(startTime, processingStart), + processingDuration: elapsed(processingStart, processingEnd), + presentationDelay: elapsed(processingEnd, nextPaintTime), + } + } + function handleEntries(entries: Array) { for (const entry of entries) { if ( @@ -130,15 +161,23 @@ export function trackInteractionToNextPaint( } const newInteraction = longestInteractions.estimateP98Interaction() - if (newInteraction && newInteraction.duration !== interactionToNextPaint) { + + if (newInteraction) { + if (newInteraction.duration !== interactionToNextPaint) { interactionToNextPaint = newInteraction.duration interactionToNextPaintStartTime = elapsed(viewStart, newInteraction.startTime) interactionToNextPaintTargetSelector = getInteractionSelector(newInteraction.startTime) + if (!interactionToNextPaintTargetSelector && newInteraction.target && isElementNode(newInteraction.target)) { interactionToNextPaintTargetSelector = getSelectorFromElement( newInteraction.target, configuration.actionNameAttribute ) + } + } + + if (isExperimentalFeatureEnabled(ExperimentalFeature.INP_SUBPARTS)) { + interactionToNextPaintSubParts = computeInpSubParts(newInteraction, interactionToNextPaint) } } } @@ -165,6 +204,7 @@ export function trackInteractionToNextPaint( value: Math.min(interactionToNextPaint, MAX_INP_VALUE) as Duration, targetSelector: interactionToNextPaintTargetSelector, time: interactionToNextPaintStartTime, + subParts: interactionToNextPaintSubParts, } } else if (getViewInteractionCount()) { return { From 5bb7635868176ef75eac8bc514835082e90b896d Mon Sep 17 00:00:00 2001 From: Hugo Garrido y Saez <9059402+HugoGarrido@users.noreply.github.com> Date: Tue, 10 Feb 2026 15:26:32 +0100 Subject: [PATCH 05/22] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20fix:=20clear=20map?= =?UTF-8?q?=20on=20stop=20callback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../trackInteractionToNextPaint.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts index 40be3d2d97..9bb2aa59f6 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts @@ -164,15 +164,15 @@ export function trackInteractionToNextPaint( if (newInteraction) { if (newInteraction.duration !== interactionToNextPaint) { - interactionToNextPaint = newInteraction.duration - interactionToNextPaintStartTime = elapsed(viewStart, newInteraction.startTime) - interactionToNextPaintTargetSelector = getInteractionSelector(newInteraction.startTime) - - if (!interactionToNextPaintTargetSelector && newInteraction.target && isElementNode(newInteraction.target)) { - interactionToNextPaintTargetSelector = getSelectorFromElement( - newInteraction.target, - configuration.actionNameAttribute - ) + interactionToNextPaint = newInteraction.duration + interactionToNextPaintStartTime = elapsed(viewStart, newInteraction.startTime) + interactionToNextPaintTargetSelector = getInteractionSelector(newInteraction.startTime) + + if (!interactionToNextPaintTargetSelector && newInteraction.target && isElementNode(newInteraction.target)) { + interactionToNextPaintTargetSelector = getSelectorFromElement( + newInteraction.target, + configuration.actionNameAttribute + ) } } @@ -219,6 +219,7 @@ export function trackInteractionToNextPaint( stop: () => { eventSubscription.unsubscribe() firstInputSubscription.unsubscribe() + groupsByInteractionId.clear() }, } } From f65b697956b296e28eaa5fb0d2be2988637c0314 Mon Sep 17 00:00:00 2001 From: Hugo Garrido y Saez <9059402+HugoGarrido@users.noreply.github.com> Date: Tue, 10 Feb 2026 15:27:14 +0100 Subject: [PATCH 06/22] =?UTF-8?q?=E2=9A=97=20feat:=20report=20inp=20subpar?= =?UTF-8?q?ts=20to=20view=20collection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/rum-core/src/domain/view/viewCollection.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/rum-core/src/domain/view/viewCollection.ts b/packages/rum-core/src/domain/view/viewCollection.ts index 4ae2aafa84..76bc979e2d 100644 --- a/packages/rum-core/src/domain/view/viewCollection.ts +++ b/packages/rum-core/src/domain/view/viewCollection.ts @@ -194,6 +194,13 @@ function computeViewPerformanceData( duration: toServerDuration(interactionToNextPaint.value), timestamp: toServerDuration(interactionToNextPaint.time), target_selector: interactionToNextPaint.targetSelector, + sub_parts: interactionToNextPaint.subParts + ? { + input_delay: toServerDuration(interactionToNextPaint.subParts.inputDelay), + processing_duration: toServerDuration(interactionToNextPaint.subParts.processingDuration), + presentation_delay: toServerDuration(interactionToNextPaint.subParts.presentationDelay), + } + : undefined, }, lcp: largestContentfulPaint && { timestamp: toServerDuration(largestContentfulPaint.value), From 41283a47f5d03eb1c121bae3f5e7490e410c7e5c Mon Sep 17 00:00:00 2001 From: Hugo Garrido y Saez <9059402+HugoGarrido@users.noreply.github.com> Date: Tue, 10 Feb 2026 15:35:19 +0100 Subject: [PATCH 07/22] =?UTF-8?q?=E2=9C=85=20feat:=20add=20inp=20subparts?= =?UTF-8?q?=20specific=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../trackInteractionToNextPaint.spec.ts | 212 +++++++++++++++++- 1 file changed, 206 insertions(+), 6 deletions(-) diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.spec.ts b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.spec.ts index bdc4b769d0..224ac97d58 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.spec.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.spec.ts @@ -1,5 +1,11 @@ import type { Duration, RelativeTime } from '@datadog/browser-core' -import { elapsed, relativeNow, resetExperimentalFeatures } from '@datadog/browser-core' +import { + elapsed, + relativeNow, + resetExperimentalFeatures, + ExperimentalFeature, + addExperimentalFeatures, +} from '@datadog/browser-core' import { registerCleanupTask } from '@datadog/browser-core/test' import { appendElement, @@ -106,7 +112,8 @@ describe('trackInteractionToNextPaint', () => { expect(getInteractionToNextPaint()).toEqual({ value: 100 as Duration, targetSelector: undefined, - time: 1 as RelativeTime, + time: 1 as Duration, + subParts: undefined, }) }) @@ -121,7 +128,8 @@ describe('trackInteractionToNextPaint', () => { expect(getInteractionToNextPaint()).toEqual({ value: MAX_INP_VALUE, targetSelector: undefined, - time: 1 as RelativeTime, + time: 1 as Duration, + subParts: undefined, }) }) @@ -137,7 +145,8 @@ describe('trackInteractionToNextPaint', () => { expect(getInteractionToNextPaint()).toEqual({ value: 98 as Duration, targetSelector: undefined, - time: 98 as RelativeTime, + time: 98 as Duration, + subParts: undefined, }) }) @@ -158,7 +167,8 @@ describe('trackInteractionToNextPaint', () => { expect(getInteractionToNextPaint()).toEqual({ value: 40 as Duration, targetSelector: undefined, - time: 1 as RelativeTime, + time: 1 as Duration, + subParts: undefined, }) }) @@ -175,7 +185,8 @@ describe('trackInteractionToNextPaint', () => { expect(getInteractionToNextPaint()).toEqual({ value: 100 as Duration, targetSelector: undefined, - time: 100 as RelativeTime, + time: 100 as Duration, + subParts: undefined, }) }) @@ -266,6 +277,195 @@ describe('trackInteractionToNextPaint', () => { expect(getInteractionSelector(startTime)).toBeUndefined() }) }) + + describe('INP subparts', () => { + beforeEach(() => { + addExperimentalFeatures([ExperimentalFeature.INP_SUBPARTS]) + }) + + it('should not include subparts when INP is 0', () => { + startINPTracking() + interactionCountMock.setInteractionCount(1 as Duration) + + expect(getInteractionToNextPaint()).toEqual({ value: 0 as Duration }) + }) + + ;[ + { + description: 'should calculate INP subparts correctly', + interaction: { + startTime: 1000, + processingStart: 1050, + processingEnd: 1200, + duration: 250, + }, + expectedSubParts: { + inputDelay: 50, // 1050 - 1000 + processingDuration: 150, // 1200 - 1050 + presentationDelay: 50, // 1250 - 1200 + }, + }, + { + description: 'should return undefined subparts when processingStart is missing', + interaction: { + startTime: 1000, + processingStart: undefined, + processingEnd: 1200, + duration: 250, + }, + expectedSubParts: undefined, + }, + { + description: 'should return undefined subparts when processingEnd is missing', + interaction: { + startTime: 1000, + processingStart: 1050, + processingEnd: undefined, + duration: 250, + }, + expectedSubParts: undefined, + }, + { + description: 'should clamp processingEnd to nextPaintTime', + interaction: { + startTime: 1000, + processingStart: 1050, + processingEnd: 1300, // Exceeds nextPaintTime (1000 + 250 = 1250) + duration: 250, + }, + expectedSubParts: { + inputDelay: 50, // 1050 - 1000 + processingDuration: 200, // 1250 - 1050 (clamped) + presentationDelay: 0, // 1250 - 1250 + }, + }, + ].forEach(({ description, interaction, expectedSubParts }) => { + it(description, () => { + startINPTracking() + + newInteraction({ + interactionId: 1, + startTime: interaction.startTime as RelativeTime, + processingStart: interaction.processingStart as RelativeTime | undefined, + processingEnd: interaction.processingEnd as RelativeTime | undefined, + duration: interaction.duration as Duration, + }) + + const inp = getInteractionToNextPaint() + + if (expectedSubParts === undefined) { + expect(inp?.subParts).toBeUndefined() + } else { + expect(inp?.subParts).toEqual({ + inputDelay: expectedSubParts.inputDelay as Duration, + processingDuration: expectedSubParts.processingDuration as Duration, + presentationDelay: expectedSubParts.presentationDelay as Duration, + }) + // Validate: subparts sum equals INP duration + const subPartsSum = + expectedSubParts.inputDelay + expectedSubParts.processingDuration + expectedSubParts.presentationDelay + expect(subPartsSum).toBe(interaction.duration) + } + }) + }) + + it('should handle entry grouping with same interactionId by using group max processingEnd', () => { + startINPTracking() + + newInteraction({ + interactionId: 1, + startTime: 1000 as RelativeTime, + processingStart: 1050 as RelativeTime, + processingEnd: 1100 as RelativeTime, + duration: 250 as Duration, + }) + + newInteraction({ + interactionId: 1, + startTime: 1005 as RelativeTime, + processingStart: 1060 as RelativeTime, + processingEnd: 1200 as RelativeTime, + duration: 245 as Duration, + }) + + const inp = getInteractionToNextPaint() + expect(inp?.value).toBe(250 as Duration) + expect(inp?.subParts).toEqual({ + inputDelay: 50 as Duration, + processingDuration: 150 as Duration, + presentationDelay: 50 as Duration, + }) + }) + + it('should group entries within 8ms renderTime window', () => { + startINPTracking() + + newInteraction({ + interactionId: 1, + startTime: 1000 as RelativeTime, + processingStart: 1050 as RelativeTime, + processingEnd: 1150 as RelativeTime, + duration: 250 as Duration, + }) + + // within 8ms renderTime window + newInteraction({ + interactionId: 2, + startTime: 1010 as RelativeTime, + processingStart: 1055 as RelativeTime, + processingEnd: 1240 as RelativeTime, + duration: 245 as Duration, + }) + + // outside of 8ms renderTime window + newInteraction({ + interactionId: 2, + startTime: 1020 as RelativeTime, + processingStart: 1070 as RelativeTime, + processingEnd: 1200 as RelativeTime, + duration: 190 as Duration, + }) + + const inp = getInteractionToNextPaint() + expect(inp?.value).toBe(250 as Duration) + expect(inp?.subParts).toEqual({ + inputDelay: 50 as Duration, + processingDuration: 190 as Duration, + presentationDelay: 10 as Duration, + }) + }) + + it('should update subparts when INP changes to a different interaction', () => { + startINPTracking() + + newInteraction({ + interactionId: 1, + startTime: 1000 as RelativeTime, + processingStart: 1050 as RelativeTime, + processingEnd: 1150 as RelativeTime, + duration: 200 as Duration, + }) + + let inp = getInteractionToNextPaint() + expect(inp?.subParts?.inputDelay).toBe(50 as Duration) + + newInteraction({ + interactionId: 2, + startTime: 2000 as RelativeTime, + processingStart: 2100 as RelativeTime, + processingEnd: 2350 as RelativeTime, + duration: 400 as Duration, + }) + + inp = getInteractionToNextPaint() + expect(inp?.value).toBe(400 as Duration) + expect(inp?.subParts).toEqual({ + inputDelay: 100 as Duration, + processingDuration: 250 as Duration, + presentationDelay: 50 as Duration, + }) + }) + }) }) describe('trackViewInteractionCount', () => { From 8bdf5dbf80ae7d7a4132311a02f795424371c42b Mon Sep 17 00:00:00 2001 From: Hugo Garrido y Saez <9059402+HugoGarrido@users.noreply.github.com> Date: Tue, 10 Feb 2026 15:55:59 +0100 Subject: [PATCH 08/22] =?UTF-8?q?=E2=9C=85=20fix=20tests=20in=20domain/vie?= =?UTF-8?q?w?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/rum-core/src/domain/view/viewCollection.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/rum-core/src/domain/view/viewCollection.spec.ts b/packages/rum-core/src/domain/view/viewCollection.spec.ts index 2fe87452b5..3d1297b3a0 100644 --- a/packages/rum-core/src/domain/view/viewCollection.spec.ts +++ b/packages/rum-core/src/domain/view/viewCollection.spec.ts @@ -180,6 +180,7 @@ describe('viewCollection', () => { duration: (10 * 1e6) as ServerDuration, timestamp: (100 * 1e6) as ServerDuration, target_selector: undefined, + sub_parts: undefined, }, lcp: { timestamp: (10 * 1e6) as ServerDuration, From 1fbf5a1ee45232edb3832cc0328f72b883bb9ac9 Mon Sep 17 00:00:00 2001 From: Hugo Garrido y Saez <9059402+HugoGarrido@users.noreply.github.com> Date: Tue, 10 Feb 2026 15:56:34 +0100 Subject: [PATCH 09/22] =?UTF-8?q?=F0=9F=8E=A8=20fix:=20format=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../viewMetrics/trackInteractionToNextPaint.spec.ts | 11 +++++------ .../view/viewMetrics/trackInteractionToNextPaint.ts | 7 ++----- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.spec.ts b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.spec.ts index 224ac97d58..5378482009 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.spec.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.spec.ts @@ -112,7 +112,7 @@ describe('trackInteractionToNextPaint', () => { expect(getInteractionToNextPaint()).toEqual({ value: 100 as Duration, targetSelector: undefined, - time: 1 as Duration, + time: 1 as RelativeTime, subParts: undefined, }) }) @@ -128,7 +128,7 @@ describe('trackInteractionToNextPaint', () => { expect(getInteractionToNextPaint()).toEqual({ value: MAX_INP_VALUE, targetSelector: undefined, - time: 1 as Duration, + time: 1 as RelativeTime, subParts: undefined, }) }) @@ -145,7 +145,7 @@ describe('trackInteractionToNextPaint', () => { expect(getInteractionToNextPaint()).toEqual({ value: 98 as Duration, targetSelector: undefined, - time: 98 as Duration, + time: 98 as RelativeTime, subParts: undefined, }) }) @@ -167,7 +167,7 @@ describe('trackInteractionToNextPaint', () => { expect(getInteractionToNextPaint()).toEqual({ value: 40 as Duration, targetSelector: undefined, - time: 1 as Duration, + time: 1 as RelativeTime, subParts: undefined, }) }) @@ -185,7 +185,7 @@ describe('trackInteractionToNextPaint', () => { expect(getInteractionToNextPaint()).toEqual({ value: 100 as Duration, targetSelector: undefined, - time: 100 as Duration, + time: 100 as RelativeTime, subParts: undefined, }) }) @@ -289,7 +289,6 @@ describe('trackInteractionToNextPaint', () => { expect(getInteractionToNextPaint()).toEqual({ value: 0 as Duration }) }) - ;[ { description: 'should calculate INP subparts correctly', diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts index 9bb2aa59f6..29d21188e3 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts @@ -128,14 +128,11 @@ export function trackInteractionToNextPaint( // Get group timing by interactionId (or use individual entry if no group) const group = entry.interactionId ? groupsByInteractionId.get(entry.interactionId) : undefined - const { startTime, processingStart, processingEnd: processingEndRaw } = group || entry; + const { startTime, processingStart, processingEnd: processingEndRaw } = group || entry // Prevents reported value to happen before processingStart. // We group values around startTime +/- RENDER_TIME_GROUPING_THRESHOLD duration so some entries can be before processingStart. - const nextPaintTime = Math.max( - (entry.startTime + inpDuration) as RelativeTime, - processingStart - ) as RelativeTime + const nextPaintTime = Math.max((entry.startTime + inpDuration) as RelativeTime, processingStart) as RelativeTime // Clamp processingEnd to not exceed nextPaintTime const processingEnd = Math.min(processingEndRaw, nextPaintTime) as RelativeTime From 106e82dfd741c24a870cd80c44e2ebe2ce4d40ef Mon Sep 17 00:00:00 2001 From: Hugo Garrido y Saez <9059402+HugoGarrido@users.noreply.github.com> Date: Tue, 10 Feb 2026 18:09:10 +0100 Subject: [PATCH 10/22] =?UTF-8?q?=F0=9F=91=8C=20fix:=20gate=20groupEntries?= =?UTF-8?q?ByRenderTime=20behind=20the=20feature=20flag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/domain/view/viewMetrics/trackInteractionToNextPaint.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts index 29d21188e3..1b2bd58207 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts @@ -153,7 +153,10 @@ export function trackInteractionToNextPaint( entry.startTime <= viewEnd ) { longestInteractions.process(entry) + + if (isExperimentalFeatureEnabled(ExperimentalFeature.INP_SUBPARTS)) { groupEntriesByRenderTime(entry) + } } } From ecc7e42b2f4f582131e3850a5885754a0249f839 Mon Sep 17 00:00:00 2001 From: Hugo Garrido y Saez <9059402+HugoGarrido@users.noreply.github.com> Date: Tue, 10 Feb 2026 18:09:57 +0100 Subject: [PATCH 11/22] =?UTF-8?q?=F0=9F=91=8C=20fix:=20always=20use=20safe?= =?UTF-8?q?=20inp=20value=20for=20subparts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../view/viewMetrics/trackInteractionToNextPaint.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts index 1b2bd58207..86d1404549 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts @@ -155,7 +155,7 @@ export function trackInteractionToNextPaint( longestInteractions.process(entry) if (isExperimentalFeatureEnabled(ExperimentalFeature.INP_SUBPARTS)) { - groupEntriesByRenderTime(entry) + groupEntriesByRenderTime(entry) } } } @@ -177,7 +177,7 @@ export function trackInteractionToNextPaint( } if (isExperimentalFeatureEnabled(ExperimentalFeature.INP_SUBPARTS)) { - interactionToNextPaintSubParts = computeInpSubParts(newInteraction, interactionToNextPaint) + interactionToNextPaintSubParts = computeInpSubParts(newInteraction, sanitizeInpValue(interactionToNextPaint)) } } } @@ -201,7 +201,7 @@ export function trackInteractionToNextPaint( // but the view interaction count > 0 then report 0 if (interactionToNextPaint >= 0) { return { - value: Math.min(interactionToNextPaint, MAX_INP_VALUE) as Duration, + value: sanitizeInpValue(interactionToNextPaint), targetSelector: interactionToNextPaintTargetSelector, time: interactionToNextPaintStartTime, subParts: interactionToNextPaintSubParts, @@ -268,6 +268,10 @@ function trackLongestInteractions(getViewInteractionCount: () => number) { } } +function sanitizeInpValue(inpValue: Duration) { + return Math.min(inpValue, MAX_INP_VALUE) as Duration +} + export function trackViewInteractionCount(viewLoadingType: ViewLoadingType) { initInteractionCountPolyfill() const previousInteractionCount = viewLoadingType === ViewLoadingType.INITIAL_LOAD ? 0 : getInteractionCount() From 4f1ce8ba93e639a3ca70807025a0bf160f2910a7 Mon Sep 17 00:00:00 2001 From: Hugo Garrido y Saez <9059402+HugoGarrido@users.noreply.github.com> Date: Wed, 11 Feb 2026 11:16:09 +0100 Subject: [PATCH 12/22] =?UTF-8?q?=F0=9F=91=8C=20fix:=20only=20rely=20on=20?= =?UTF-8?q?group=20that=20should=20exists=20to=20compute=20subparts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../trackInteractionToNextPaint.ts | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts index 86d1404549..90213c2e02 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts @@ -121,25 +121,30 @@ export function trackInteractionToNextPaint( entry: RumPerformanceEventTiming | RumFirstInputTiming, inpDuration: Duration ): InteractionToNextPaint['subParts'] | undefined { - if (!entry.processingStart || !entry.processingEnd) { + if (!entry.processingStart || !entry.processingEnd || !entry.interactionId) { return undefined } - // Get group timing by interactionId (or use individual entry if no group) - const group = entry.interactionId ? groupsByInteractionId.get(entry.interactionId) : undefined + const group = groupsByInteractionId.get(entry.interactionId) - const { startTime, processingStart, processingEnd: processingEndRaw } = group || entry + // Shouldn't happen since entries are grouped before p98 calculation. + if (!group) { + return undefined + } - // Prevents reported value to happen before processingStart. - // We group values around startTime +/- RENDER_TIME_GROUPING_THRESHOLD duration so some entries can be before processingStart. - const nextPaintTime = Math.max((entry.startTime + inpDuration) as RelativeTime, processingStart) as RelativeTime + // Use group.startTime consistently to ensure subparts sum to inpDuration + // Math.max prevents nextPaintTime from being before processingStart (Chrome implementation) + const nextPaintTime = Math.max( + (group.startTime + inpDuration) as RelativeTime, + group.processingStart + ) as RelativeTime // Clamp processingEnd to not exceed nextPaintTime - const processingEnd = Math.min(processingEndRaw, nextPaintTime) as RelativeTime + const processingEnd = Math.min(group.processingEnd, nextPaintTime) as RelativeTime return { - inputDelay: elapsed(startTime, processingStart), - processingDuration: elapsed(processingStart, processingEnd), + inputDelay: elapsed(group.startTime, group.processingStart), + processingDuration: elapsed(group.processingStart, processingEnd), presentationDelay: elapsed(processingEnd, nextPaintTime), } } From 2c2ad1245c0991d7f2a5e60811b0c39fcd2c0336 Mon Sep 17 00:00:00 2001 From: Hugo Garrido y Saez <9059402+HugoGarrido@users.noreply.github.com> Date: Wed, 11 Feb 2026 11:22:23 +0100 Subject: [PATCH 13/22] =?UTF-8?q?=F0=9F=91=8C=20fix:=20stop=20storing=20en?= =?UTF-8?q?tries=20in=20group=20since=20not=20used?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/domain/view/viewMetrics/trackInteractionToNextPaint.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts index 90213c2e02..b5a42de533 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts @@ -37,7 +37,6 @@ interface EntriesGroup { processingEnd: RelativeTime // Reference time use for grouping, set once for each group referenceRenderTime: RelativeTime - entries: Array } /** @@ -78,7 +77,6 @@ export function trackInteractionToNextPaint( // For each group, we keep the biggest interval possible between processingStart and processingEnd group.processingStart = Math.min(entry.processingStart, group.processingStart) as RelativeTime group.processingEnd = Math.max(entry.processingEnd, group.processingEnd) as RelativeTime - group.entries.push(entry) } function groupEntriesByRenderTime(entry: RumPerformanceEventTiming | RumFirstInputTiming) { @@ -113,7 +111,6 @@ export function trackInteractionToNextPaint( processingStart: entry.processingStart, processingEnd: entry.processingEnd, referenceRenderTime: renderTime, - entries: [entry], }) } From 5189c309deff5a0b4adffabd77c6808eb63ba838 Mon Sep 17 00:00:00 2001 From: Hugo Garrido y Saez <9059402+HugoGarrido@users.noreply.github.com> Date: Wed, 11 Feb 2026 11:37:55 +0100 Subject: [PATCH 14/22] =?UTF-8?q?=F0=9F=91=8C=20fix:=20prevents=200=20valu?= =?UTF-8?q?e=20being=20falsy=20in=20checks=20for=20interactionId?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/view/viewMetrics/trackInteractionToNextPaint.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts index b5a42de533..5e16163b4f 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts @@ -80,7 +80,7 @@ export function trackInteractionToNextPaint( } function groupEntriesByRenderTime(entry: RumPerformanceEventTiming | RumFirstInputTiming) { - if (!entry.interactionId || !entry.processingStart || !entry.processingEnd) { + if (entry.interactionId === undefined || !entry.processingStart || !entry.processingEnd) { return } @@ -118,7 +118,7 @@ export function trackInteractionToNextPaint( entry: RumPerformanceEventTiming | RumFirstInputTiming, inpDuration: Duration ): InteractionToNextPaint['subParts'] | undefined { - if (!entry.processingStart || !entry.processingEnd || !entry.interactionId) { + if (!entry.processingStart || !entry.processingEnd || entry.interactionId === undefined) { return undefined } From fbec2d2ab0f5dfd403e44e40706bbd064fe87376 Mon Sep 17 00:00:00 2001 From: Hugo Garrido y Saez <9059402+HugoGarrido@users.noreply.github.com> Date: Mon, 23 Feb 2026 12:05:21 +0100 Subject: [PATCH 15/22] =?UTF-8?q?=F0=9F=91=8C=20fix:=20prevent=20storing?= =?UTF-8?q?=20too=20much=20none=20needed=20entries=20groups?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../trackInteractionToNextPaint.spec.ts | 33 +++++++++++++++++++ .../trackInteractionToNextPaint.ts | 14 ++++++++ 2 files changed, 47 insertions(+) diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.spec.ts b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.spec.ts index 5378482009..1881384146 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.spec.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.spec.ts @@ -434,6 +434,39 @@ describe('trackInteractionToNextPaint', () => { }) }) + it('should keep correct subparts for p98 after interactions causing pruning', () => { + startINPTracking() + + // The interaction that will remain the p98 throughout + newInteraction({ + interactionId: 1, + startTime: 1000 as RelativeTime, + processingStart: 1050 as RelativeTime, + processingEnd: 1200 as RelativeTime, + duration: 1100 as Duration, + }) + + // Add more than MAX_INTERACTION_ENTRIES (10) shorter interactions to fill longestInteractions + // and trigger eviction and pruning of their groups + for (let i = 2; i <= 12; i++) { + newInteraction({ + interactionId: i, + startTime: (i * 2000) as RelativeTime, + processingStart: (i * 2000 + 10) as RelativeTime, + processingEnd: (i * 2000 + 20) as RelativeTime, + duration: i as Duration, + }) + } + + const inp = getInteractionToNextPaint() + expect(inp?.value).toBe(1100 as Duration) + expect(inp?.subParts).toEqual({ + inputDelay: 50 as Duration, + processingDuration: 150 as Duration, + presentationDelay: 900 as Duration, + }) + }) + it('should update subparts when INP changes to a different interaction', () => { startINPTracking() diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts index 5e16163b4f..bc2a7717fe 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts @@ -162,6 +162,16 @@ export function trackInteractionToNextPaint( } } + if (isExperimentalFeatureEnabled(ExperimentalFeature.INP_SUBPARTS)) { + // Prune after all entries are grouped: groups not in longestInteractions can never affect p98 subparts. + // Keeps groupsByInteractionId capped at MAX_INTERACTION_ENTRIES + for (const [interactionId] of groupsByInteractionId) { + if (!longestInteractions.isTracked(interactionId)) { + groupsByInteractionId.delete(interactionId) + } + } + } + const newInteraction = longestInteractions.estimateP98Interaction() if (newInteraction) { @@ -267,6 +277,10 @@ function trackLongestInteractions(getViewInteractionCount: () => number) { const interactionIndex = Math.min(longestInteractions.length - 1, Math.floor(getViewInteractionCount() / 50)) return longestInteractions[interactionIndex] }, + + isTracked(interactionId: number): boolean { + return longestInteractions.some((i) => i.interactionId === interactionId) + }, } } From 820aca88db05799cb98cdd3e8abddc4f1455a311 Mon Sep 17 00:00:00 2001 From: Hugo Garrido y Saez <9059402+HugoGarrido@users.noreply.github.com> Date: Mon, 23 Feb 2026 13:42:18 +0100 Subject: [PATCH 16/22] =?UTF-8?q?=F0=9F=91=8C=20fix:=20not=20only=20rely?= =?UTF-8?q?=20on=20duration=20to=20compute=20new=20interaction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../view/viewMetrics/trackInteractionToNextPaint.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts index bc2a7717fe..c27b067084 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts @@ -18,6 +18,7 @@ const MAX_INTERACTION_ENTRIES = 10 // Arbitrary value to cap INP outliers export const MAX_INP_VALUE = (1 * ONE_MINUTE) as Duration +// Event Timing API rounds duration values to the nearest 8 ms const RENDER_TIME_GROUPING_THRESHOLD = 8 as Duration export interface InteractionToNextPaint { @@ -175,9 +176,12 @@ export function trackInteractionToNextPaint( const newInteraction = longestInteractions.estimateP98Interaction() if (newInteraction) { - if (newInteraction.duration !== interactionToNextPaint) { + const newStartTime = elapsed(viewStart, newInteraction.startTime) + // startTime catches identity changes when the p98 switches to a different interaction with the same duration, + // ensuring time, targetSelector and subParts always describe the same interaction. + if (newInteraction.duration !== interactionToNextPaint || newStartTime !== interactionToNextPaintStartTime) { interactionToNextPaint = newInteraction.duration - interactionToNextPaintStartTime = elapsed(viewStart, newInteraction.startTime) + interactionToNextPaintStartTime = newStartTime interactionToNextPaintTargetSelector = getInteractionSelector(newInteraction.startTime) if (!interactionToNextPaintTargetSelector && newInteraction.target && isElementNode(newInteraction.target)) { @@ -189,6 +193,8 @@ export function trackInteractionToNextPaint( } if (isExperimentalFeatureEnabled(ExperimentalFeature.INP_SUBPARTS)) { + // Recomputed on every batch: the group for the p98 interaction may have been updated + // with new min/max timing even when the p98 identity (duration, startTime) is unchanged. interactionToNextPaintSubParts = computeInpSubParts(newInteraction, sanitizeInpValue(interactionToNextPaint)) } } From b19ca0ed74de79a1113ddccc424366cf42745ace Mon Sep 17 00:00:00 2001 From: Hugo Garrido y Saez <9059402+HugoGarrido@users.noreply.github.com> Date: Mon, 23 Feb 2026 14:06:21 +0100 Subject: [PATCH 17/22] fix: typecheck --- .../domain/view/viewMetrics/trackInteractionToNextPaint.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.spec.ts b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.spec.ts index a1cb93d7d3..e0ebfaa860 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.spec.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.spec.ts @@ -2,7 +2,6 @@ import type { Duration, RelativeTime } from '@datadog/browser-core' import { elapsed, relativeNow, - resetExperimentalFeatures, ExperimentalFeature, addExperimentalFeatures, } from '@datadog/browser-core' From 7fd8fcd5fc040f01fa08c3f81a68fd68c9036559 Mon Sep 17 00:00:00 2001 From: Hugo Garrido y Saez <9059402+HugoGarrido@users.noreply.github.com> Date: Mon, 23 Feb 2026 14:09:31 +0100 Subject: [PATCH 18/22] fix: format --- .../view/viewMetrics/trackInteractionToNextPaint.spec.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.spec.ts b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.spec.ts index e0ebfaa860..433147832b 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.spec.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.spec.ts @@ -1,10 +1,5 @@ import type { Duration, RelativeTime } from '@datadog/browser-core' -import { - elapsed, - relativeNow, - ExperimentalFeature, - addExperimentalFeatures, -} from '@datadog/browser-core' +import { elapsed, relativeNow, ExperimentalFeature, addExperimentalFeatures } from '@datadog/browser-core' import { registerCleanupTask } from '@datadog/browser-core/test' import { appendElement, From b6fa3305532e5de606ef6125d8978552dcfdb005 Mon Sep 17 00:00:00 2001 From: Bastien Caudan Date: Wed, 25 Feb 2026 13:49:00 +0100 Subject: [PATCH 19/22] =?UTF-8?q?=F0=9F=91=8Cextract=20currentInp=20to=20g?= =?UTF-8?q?roup=20state=20variables=20together?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../trackInteractionToNextPaint.ts | 202 ++++-------------- 1 file changed, 43 insertions(+), 159 deletions(-) diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts index c27b067084..41906550da 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts @@ -1,11 +1,11 @@ -import { elapsed, noop, ONE_MINUTE, ExperimentalFeature, isExperimentalFeatureEnabled } from '@datadog/browser-core' import type { Duration, RelativeTime } from '@datadog/browser-core' +import { elapsed, noop, ONE_MINUTE } from '@datadog/browser-core' +import type { RumFirstInputTiming, RumPerformanceEventTiming } from '../../../browser/performanceObservable' import { createPerformanceObservable, RumPerformanceEntryType, supportPerformanceTimingEvent, } from '../../../browser/performanceObservable' -import type { RumFirstInputTiming, RumPerformanceEventTiming } from '../../../browser/performanceObservable' import { ViewLoadingType } from '../../../rawRumEvent.types' import { getSelectorFromElement } from '../../getSelectorFromElement' import { isElementNode } from '../../../browser/htmlDomUtils' @@ -18,9 +18,6 @@ const MAX_INTERACTION_ENTRIES = 10 // Arbitrary value to cap INP outliers export const MAX_INP_VALUE = (1 * ONE_MINUTE) as Duration -// Event Timing API rounds duration values to the nearest 8 ms -const RENDER_TIME_GROUPING_THRESHOLD = 8 as Duration - export interface InteractionToNextPaint { value: Duration targetSelector?: string @@ -31,15 +28,6 @@ export interface InteractionToNextPaint { presentationDelay: Duration } } - -interface EntriesGroup { - startTime: RelativeTime - processingStart: RelativeTime - processingEnd: RelativeTime - // Reference time use for grouping, set once for each group - referenceRenderTime: RelativeTime -} - /** * Track the interaction to next paint (INP). * To avoid outliers, return the p98 worst interaction of the view. @@ -59,93 +47,29 @@ export function trackInteractionToNextPaint( } } - const { getViewInteractionCount, stopViewInteractionCount } = trackViewInteractionCount(viewLoadingType) - let viewEnd = Infinity as RelativeTime - - const longestInteractions = trackLongestInteractions(getViewInteractionCount) - let interactionToNextPaint = -1 as Duration - let interactionToNextPaintTargetSelector: string | undefined - let interactionToNextPaintStartTime: Duration | undefined - let interactionToNextPaintSubParts: InteractionToNextPaint['subParts'] | undefined - - // Entry grouping for subparts calculation - const groupsByInteractionId = new Map() - - function updateGroupWithEntry(group: EntriesGroup, entry: RumPerformanceEventTiming | RumFirstInputTiming) { - group.startTime = Math.min(entry.startTime, group.startTime) as RelativeTime - - // For each group, we keep the biggest interval possible between processingStart and processingEnd - group.processingStart = Math.min(entry.processingStart, group.processingStart) as RelativeTime - group.processingEnd = Math.max(entry.processingEnd, group.processingEnd) as RelativeTime - } - - function groupEntriesByRenderTime(entry: RumPerformanceEventTiming | RumFirstInputTiming) { - if (entry.interactionId === undefined || !entry.processingStart || !entry.processingEnd) { - return - } - - const renderTime = (entry.startTime + entry.duration) as RelativeTime - - // Check if this interactionId already has a group - const existingGroup = groupsByInteractionId.get(entry.interactionId) - - if (existingGroup) { - // Update existing group with MIN/MAX values (keep original referenceRenderTime) - updateGroupWithEntry(existingGroup, entry) - return - } - - // Try to find a group within 8ms window to merge with (different interactionId, same frame) - for (const [, group] of groupsByInteractionId.entries()) { - if (Math.abs(renderTime - group.referenceRenderTime) <= RENDER_TIME_GROUPING_THRESHOLD) { - updateGroupWithEntry(group, entry) - // Also store under this entry's interactionId for easy lookup - groupsByInteractionId.set(entry.interactionId, group) - return + let currentInp: + | { + duration: Duration + startTime: Duration + targetSelector?: string + subParts?: InteractionToNextPaint['subParts'] } - } - - // Create new group - groupsByInteractionId.set(entry.interactionId, { - startTime: entry.startTime, - processingStart: entry.processingStart, - processingEnd: entry.processingEnd, - referenceRenderTime: renderTime, - }) - } - - function computeInpSubParts( - entry: RumPerformanceEventTiming | RumFirstInputTiming, - inpDuration: Duration - ): InteractionToNextPaint['subParts'] | undefined { - if (!entry.processingStart || !entry.processingEnd || entry.interactionId === undefined) { - return undefined - } - - const group = groupsByInteractionId.get(entry.interactionId) - - // Shouldn't happen since entries are grouped before p98 calculation. - if (!group) { - return undefined - } - - // Use group.startTime consistently to ensure subparts sum to inpDuration - // Math.max prevents nextPaintTime from being before processingStart (Chrome implementation) - const nextPaintTime = Math.max( - (group.startTime + inpDuration) as RelativeTime, - group.processingStart - ) as RelativeTime - - // Clamp processingEnd to not exceed nextPaintTime - const processingEnd = Math.min(group.processingEnd, nextPaintTime) as RelativeTime + | undefined - return { - inputDelay: elapsed(group.startTime, group.processingStart), - processingDuration: elapsed(group.processingStart, processingEnd), - presentationDelay: elapsed(processingEnd, nextPaintTime), - } - } + const { getViewInteractionCount, stopViewInteractionCount } = trackViewInteractionCount(viewLoadingType) + const longestInteractions = trackLongestInteractions(getViewInteractionCount) + const firstInputSubscription = createPerformanceObservable(configuration, { + type: RumPerformanceEntryType.FIRST_INPUT, + buffered: true, + }).subscribe(handleEntries) + const eventSubscription = createPerformanceObservable(configuration, { + type: RumPerformanceEntryType.EVENT, + // durationThreshold only impact PerformanceEventTiming entries used for INP computation which requires a threshold at 40 (default is 104ms) + // cf: https://github.com/GoogleChrome/web-vitals/blob/3806160ffbc93c3c4abf210a167b81228172b31c/src/onINP.ts#L202-L210 + durationThreshold: 40, + buffered: true, + }).subscribe(handleEntries) function handleEntries(entries: Array) { for (const entry of entries) { @@ -156,73 +80,42 @@ export function trackInteractionToNextPaint( entry.startTime <= viewEnd ) { longestInteractions.process(entry) - - if (isExperimentalFeatureEnabled(ExperimentalFeature.INP_SUBPARTS)) { - groupEntriesByRenderTime(entry) - } } } - if (isExperimentalFeatureEnabled(ExperimentalFeature.INP_SUBPARTS)) { - // Prune after all entries are grouped: groups not in longestInteractions can never affect p98 subparts. - // Keeps groupsByInteractionId capped at MAX_INTERACTION_ENTRIES - for (const [interactionId] of groupsByInteractionId) { - if (!longestInteractions.isTracked(interactionId)) { - groupsByInteractionId.delete(interactionId) - } - } + const candidate = longestInteractions.estimateP98Interaction() + if (candidate) { + updateCurrentInp(candidate) } + } - const newInteraction = longestInteractions.estimateP98Interaction() - - if (newInteraction) { - const newStartTime = elapsed(viewStart, newInteraction.startTime) - // startTime catches identity changes when the p98 switches to a different interaction with the same duration, - // ensuring time, targetSelector and subParts always describe the same interaction. - if (newInteraction.duration !== interactionToNextPaint || newStartTime !== interactionToNextPaintStartTime) { - interactionToNextPaint = newInteraction.duration - interactionToNextPaintStartTime = newStartTime - interactionToNextPaintTargetSelector = getInteractionSelector(newInteraction.startTime) - - if (!interactionToNextPaintTargetSelector && newInteraction.target && isElementNode(newInteraction.target)) { - interactionToNextPaintTargetSelector = getSelectorFromElement( - newInteraction.target, - configuration.actionNameAttribute - ) - } + function updateCurrentInp(candidate: RumPerformanceEventTiming | RumFirstInputTiming) { + const newStartTime = elapsed(viewStart, candidate.startTime) + // startTime catches identity changes when the p98 switches to a different interaction with the same duration, + // ensuring targetSelector and subParts always describe the same interaction. + if (!currentInp || candidate.duration !== currentInp.duration || newStartTime !== currentInp.startTime) { + let targetSelector = getInteractionSelector(candidate.startTime) + if (!targetSelector && candidate.target && isElementNode(candidate.target)) { + targetSelector = getSelectorFromElement(candidate.target, configuration.actionNameAttribute) } - - if (isExperimentalFeatureEnabled(ExperimentalFeature.INP_SUBPARTS)) { - // Recomputed on every batch: the group for the p98 interaction may have been updated - // with new min/max timing even when the p98 identity (duration, startTime) is unchanged. - interactionToNextPaintSubParts = computeInpSubParts(newInteraction, sanitizeInpValue(interactionToNextPaint)) + currentInp = { + duration: candidate.duration, + startTime: newStartTime, + targetSelector, } } } - const firstInputSubscription = createPerformanceObservable(configuration, { - type: RumPerformanceEntryType.FIRST_INPUT, - buffered: true, - }).subscribe(handleEntries) - - const eventSubscription = createPerformanceObservable(configuration, { - type: RumPerformanceEntryType.EVENT, - // durationThreshold only impact PerformanceEventTiming entries used for INP computation which requires a threshold at 40 (default is 104ms) - // cf: https://github.com/GoogleChrome/web-vitals/blob/3806160ffbc93c3c4abf210a167b81228172b31c/src/onINP.ts#L202-L210 - durationThreshold: 40, - buffered: true, - }).subscribe(handleEntries) - return { getInteractionToNextPaint: (): InteractionToNextPaint | undefined => { // If no INP duration where captured because of the performanceObserver 40ms threshold // but the view interaction count > 0 then report 0 - if (interactionToNextPaint >= 0) { + if (currentInp) { return { - value: sanitizeInpValue(interactionToNextPaint), - targetSelector: interactionToNextPaintTargetSelector, - time: interactionToNextPaintStartTime, - subParts: interactionToNextPaintSubParts, + value: Math.min(currentInp.duration, MAX_INP_VALUE) as Duration, + targetSelector: currentInp.targetSelector, + time: currentInp.startTime, + subParts: currentInp.subParts, } } else if (getViewInteractionCount()) { return { @@ -237,7 +130,6 @@ export function trackInteractionToNextPaint( stop: () => { eventSubscription.unsubscribe() firstInputSubscription.unsubscribe() - groupsByInteractionId.clear() }, } } @@ -283,17 +175,9 @@ function trackLongestInteractions(getViewInteractionCount: () => number) { const interactionIndex = Math.min(longestInteractions.length - 1, Math.floor(getViewInteractionCount() / 50)) return longestInteractions[interactionIndex] }, - - isTracked(interactionId: number): boolean { - return longestInteractions.some((i) => i.interactionId === interactionId) - }, } } -function sanitizeInpValue(inpValue: Duration) { - return Math.min(inpValue, MAX_INP_VALUE) as Duration -} - export function trackViewInteractionCount(viewLoadingType: ViewLoadingType) { initInteractionCountPolyfill() const previousInteractionCount = viewLoadingType === ViewLoadingType.INITIAL_LOAD ? 0 : getInteractionCount() From 73a585226359235452efd63775b1eca02e55d45b Mon Sep 17 00:00:00 2001 From: Bastien Caudan Date: Wed, 25 Feb 2026 14:03:50 +0100 Subject: [PATCH 20/22] =?UTF-8?q?=F0=9F=91=8Cextract=20subPartsTracker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../trackInteractionToNextPaint.ts | 126 +++++++++++++++++- 1 file changed, 123 insertions(+), 3 deletions(-) diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts index 41906550da..a4e1120803 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts @@ -1,5 +1,5 @@ import type { Duration, RelativeTime } from '@datadog/browser-core' -import { elapsed, noop, ONE_MINUTE } from '@datadog/browser-core' +import { elapsed, ExperimentalFeature, isExperimentalFeatureEnabled, noop, ONE_MINUTE } from '@datadog/browser-core' import type { RumFirstInputTiming, RumPerformanceEventTiming } from '../../../browser/performanceObservable' import { createPerformanceObservable, @@ -18,6 +18,9 @@ const MAX_INTERACTION_ENTRIES = 10 // Arbitrary value to cap INP outliers export const MAX_INP_VALUE = (1 * ONE_MINUTE) as Duration +// Event Timing API rounds duration values to the nearest 8 ms +const RENDER_TIME_GROUPING_THRESHOLD = 8 as Duration + export interface InteractionToNextPaint { value: Duration targetSelector?: string @@ -28,6 +31,14 @@ export interface InteractionToNextPaint { presentationDelay: Duration } } +interface EntriesGroup { + startTime: RelativeTime + processingStart: RelativeTime + processingEnd: RelativeTime + // Reference time used for grouping, set once at group creation — anchors the 8ms merge window + referenceRenderTime: RelativeTime +} + /** * Track the interaction to next paint (INP). * To avoid outliers, return the p98 worst interaction of the view. @@ -59,6 +70,9 @@ export function trackInteractionToNextPaint( const { getViewInteractionCount, stopViewInteractionCount } = trackViewInteractionCount(viewLoadingType) const longestInteractions = trackLongestInteractions(getViewInteractionCount) + const subPartsTracker = isExperimentalFeatureEnabled(ExperimentalFeature.INP_SUBPARTS) + ? createSubPartsTracker(longestInteractions) + : null const firstInputSubscription = createPerformanceObservable(configuration, { type: RumPerformanceEntryType.FIRST_INPUT, buffered: true, @@ -80,9 +94,10 @@ export function trackInteractionToNextPaint( entry.startTime <= viewEnd ) { longestInteractions.process(entry) + subPartsTracker?.process(entry) } } - + subPartsTracker?.pruneUntracked() const candidate = longestInteractions.estimateP98Interaction() if (candidate) { updateCurrentInp(candidate) @@ -104,6 +119,11 @@ export function trackInteractionToNextPaint( targetSelector, } } + // Recomputed on every batch: the group for the p98 interaction may have been updated + // with new min/max timing even when the p98 identity (duration, startTime) is unchanged. + if (subPartsTracker) { + currentInp.subParts = subPartsTracker.computeSubParts(candidate, sanitizeInpValue(currentInp.duration)) + } } return { @@ -112,7 +132,7 @@ export function trackInteractionToNextPaint( // but the view interaction count > 0 then report 0 if (currentInp) { return { - value: Math.min(currentInp.duration, MAX_INP_VALUE) as Duration, + value: sanitizeInpValue(currentInp.duration), targetSelector: currentInp.targetSelector, time: currentInp.startTime, subParts: currentInp.subParts, @@ -130,6 +150,7 @@ export function trackInteractionToNextPaint( stop: () => { eventSubscription.unsubscribe() firstInputSubscription.unsubscribe() + subPartsTracker?.stop() }, } } @@ -175,6 +196,10 @@ function trackLongestInteractions(getViewInteractionCount: () => number) { const interactionIndex = Math.min(longestInteractions.length - 1, Math.floor(getViewInteractionCount() / 50)) return longestInteractions[interactionIndex] }, + + isTracked(interactionId: number): boolean { + return longestInteractions.some((i) => i.interactionId === interactionId) + }, } } @@ -201,6 +226,97 @@ export function trackViewInteractionCount(viewLoadingType: ViewLoadingType) { } } +function createSubPartsTracker(longestInteractions: ReturnType) { + const groupsByInteractionId = new Map() + + function updateGroupWithEntry(group: EntriesGroup, entry: RumPerformanceEventTiming | RumFirstInputTiming) { + group.startTime = Math.min(entry.startTime, group.startTime) as RelativeTime + // For each group, we keep the biggest interval possible between processingStart and processingEnd + group.processingStart = Math.min(entry.processingStart, group.processingStart) as RelativeTime + group.processingEnd = Math.max(entry.processingEnd, group.processingEnd) as RelativeTime + } + + return { + process(entry: RumPerformanceEventTiming | RumFirstInputTiming): void { + if (entry.interactionId === undefined || !entry.processingStart || !entry.processingEnd) { + return + } + + const renderTime = (entry.startTime + entry.duration) as RelativeTime + const existingGroup = groupsByInteractionId.get(entry.interactionId) + + if (existingGroup) { + // Update existing group with MIN/MAX values (keep original referenceRenderTime) + updateGroupWithEntry(existingGroup, entry) + return + } + + // Try to find a group within 8ms window to merge with (different interactionId, same frame) + for (const [, group] of groupsByInteractionId.entries()) { + if (Math.abs(renderTime - group.referenceRenderTime) <= RENDER_TIME_GROUPING_THRESHOLD) { + updateGroupWithEntry(group, entry) + // Also store under this entry's interactionId for easy lookup + groupsByInteractionId.set(entry.interactionId, group) + return + } + } + + // Create new group + groupsByInteractionId.set(entry.interactionId, { + startTime: entry.startTime, + processingStart: entry.processingStart, + processingEnd: entry.processingEnd, + referenceRenderTime: renderTime, + }) + }, + + // Prune after all entries are grouped: groups not in longestInteractions can never affect p98 subparts. + // Keeps groupsByInteractionId capped at MAX_INTERACTION_ENTRIES + pruneUntracked(): void { + for (const [interactionId] of groupsByInteractionId) { + if (!longestInteractions.isTracked(interactionId)) { + groupsByInteractionId.delete(interactionId) + } + } + }, + + computeSubParts( + entry: RumPerformanceEventTiming | RumFirstInputTiming, + inpDuration: Duration + ): InteractionToNextPaint['subParts'] | undefined { + if (!entry.processingStart || !entry.processingEnd || entry.interactionId === undefined) { + return undefined + } + + const group = groupsByInteractionId.get(entry.interactionId) + // Shouldn't happen since entries are grouped before p98 calculation. + if (!group) { + return undefined + } + + // Use group.startTime consistently to ensure subparts sum to inpDuration + // Math.max prevents nextPaintTime from being before processingStart (Chrome implementation) + const nextPaintTime = Math.max( + (group.startTime + inpDuration) as RelativeTime, + group.processingStart + ) as RelativeTime + + // Clamp processingEnd to not exceed nextPaintTime + const processingEnd = Math.min(group.processingEnd, nextPaintTime) as RelativeTime + + return { + inputDelay: elapsed(group.startTime, group.processingStart), + processingDuration: elapsed(group.processingStart, processingEnd), + presentationDelay: elapsed(processingEnd, nextPaintTime), + } + }, + + stop(): void { + groupsByInteractionId.clear() + }, + } +} + export function isInteractionToNextPaintSupported() { return ( supportPerformanceTimingEvent(RumPerformanceEntryType.EVENT) && @@ -208,3 +324,7 @@ export function isInteractionToNextPaintSupported() { 'interactionId' in PerformanceEventTiming.prototype ) } + +function sanitizeInpValue(inpValue: Duration) { + return Math.min(inpValue, MAX_INP_VALUE) as Duration +} From f2100d10fb587973de51ddc24f38b70a34f8a201 Mon Sep 17 00:00:00 2001 From: Bastien Caudan Date: Wed, 25 Feb 2026 14:09:08 +0100 Subject: [PATCH 21/22] =?UTF-8?q?=F0=9F=93=9Dadd=20a=20bit=20of=20doc=20on?= =?UTF-8?q?=20main=20tracking=20methods?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../viewMetrics/trackInteractionToNextPaint.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts index a4e1120803..8d05ee293d 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts @@ -155,6 +155,10 @@ export function trackInteractionToNextPaint( } } +/** + * Maintains a bounded list of the slowest interactions seen so far, used to estimate the p98 + * interaction duration without keeping every entry in memory. + */ function trackLongestInteractions(getViewInteractionCount: () => number) { const longestInteractions: Array = [] @@ -203,6 +207,10 @@ function trackLongestInteractions(getViewInteractionCount: () => number) { } } +/** + * Tracks the number of interactions that occurred during the current view. Freezes the count + * when the view ends so that the p98 estimate remains stable after `setViewEnd` is called. + */ export function trackViewInteractionCount(viewLoadingType: ViewLoadingType) { initInteractionCountPolyfill() const previousInteractionCount = viewLoadingType === ViewLoadingType.INITIAL_LOAD ? 0 : getInteractionCount() @@ -226,6 +234,12 @@ export function trackViewInteractionCount(viewLoadingType: ViewLoadingType) { } } +/** + * Groups performance entries by interaction and render time to compute INP subparts + * (input delay, processing duration, presentation delay). Entries sharing the same + * interactionId, or whose render time falls within the 8 ms Event Timing rounding window, + * are merged into a single group so that subparts always sum to the reported INP duration. + */ function createSubPartsTracker(longestInteractions: ReturnType) { const groupsByInteractionId = new Map() From 5d1dc2efcc4572fde3a2581152875da8a3c9152e Mon Sep 17 00:00:00 2001 From: Bastien Caudan Date: Wed, 25 Feb 2026 14:12:39 +0100 Subject: [PATCH 22/22] =?UTF-8?q?=E2=AC=86=EF=B8=8Fupdate=20rum-events-for?= =?UTF-8?q?mat?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/rum-core/src/rumEvent.types.ts | 64 +++++++++++++++++++--- packages/rum/src/types/profiling.ts | 72 ++++++++++++++++++++++++- rum-events-format | 2 +- 3 files changed, 129 insertions(+), 9 deletions(-) diff --git a/packages/rum-core/src/rumEvent.types.ts b/packages/rum-core/src/rumEvent.types.ts index 8a7f4fe953..10565eee83 100644 --- a/packages/rum-core/src/rumEvent.types.ts +++ b/packages/rum-core/src/rumEvent.types.ts @@ -141,10 +141,6 @@ export type RumActionEvent = CommonProperties & * CSS selector path of the target element */ readonly selector?: string - /** - * Mobile-only: a globally unique and stable identifier for this UI element, computed as the hash of the element's path (32 lowercase hex characters). Used to correlate actions with mobile session replay wireframes. - */ - readonly permanent_id?: string /** * Width of the target element (in pixels) */ @@ -153,6 +149,10 @@ export type RumActionEvent = CommonProperties & * Height of the target element (in pixels) */ readonly height?: number + /** + * Mobile-only: a globally unique and stable identifier for this UI element, computed as the hash of the element's path (32 lowercase hex characters). Used to correlate actions with mobile session replay wireframes. + */ + readonly permanent_id?: string [k: string]: unknown } /** @@ -676,15 +676,15 @@ export type RumResourceEvent = CommonProperties & */ readonly size?: number /** - * Size in octet of the resource before removing any applied content encodings + * Size in octet of the response body before removing any applied content encodings */ readonly encoded_body_size?: number /** - * Size in octet of the resource after removing any applied encoding + * Size in octet of the response body after removing any applied encoding */ readonly decoded_body_size?: number /** - * Size in octet of the fetched resource + * Size in octet of the fetched response resource */ readonly transfer_size?: number /** @@ -829,6 +829,38 @@ export type RumResourceEvent = CommonProperties & | 'video' [k: string]: unknown } + /** + * Request properties + */ + readonly request?: { + /** + * Size in octet of the request body sent over the network (after encoding) + */ + readonly encoded_body_size?: number + /** + * Size in octet of the request body before any encoding + */ + readonly decoded_body_size?: number + /** + * HTTP headers of the resource request + */ + readonly headers?: { + [k: string]: string + } + [k: string]: unknown + } + /** + * Response properties + */ + readonly response?: { + /** + * HTTP headers of the resource response + */ + readonly headers?: { + [k: string]: string + } + [k: string]: unknown + } /** * GraphQL requests parameters */ @@ -2016,6 +2048,24 @@ export interface ViewPerformanceData { * CSS selector path of the interacted element for the INP interaction */ readonly target_selector?: string + /** + * Sub-parts of the INP + */ + sub_parts?: { + /** + * Time from the start of the input event to the start of the processing of the event + */ + readonly input_delay: number + /** + * Event handler execution time + */ + readonly processing_time: number + /** + * Rendering time happening after processing + */ + readonly presentation_delay: number + [k: string]: unknown + } [k: string]: unknown } /** diff --git a/packages/rum/src/types/profiling.ts b/packages/rum/src/types/profiling.ts index bf751ccade..47a052e10d 100644 --- a/packages/rum/src/types/profiling.ts +++ b/packages/rum/src/types/profiling.ts @@ -24,6 +24,32 @@ export type BrowserProfileEvent = ProfileCommonProperties & { */ readonly clock_drift: number } + /** + * Action properties. + */ + readonly action?: { + /** + * Array of action IDs. + */ + readonly id: string[] + /** + * Array of action labels. + */ + readonly label: string[] + } + /** + * Vital properties. + */ + readonly vital?: { + /** + * Array of vital IDs. + */ + readonly id: string[] + /** + * Array of vital labels. + */ + readonly label: string[] + } } /** @@ -130,6 +156,14 @@ export interface BrowserProfilerTrace { * List of detected long tasks. */ readonly longTasks: RumProfilerLongTaskEntry[] + /** + * List of detected vital entries. + */ + readonly vitals?: RumProfilerVitalEntry[] + /** + * List of detected action entries. + */ + readonly actions?: RumProfilerActionEntry[] /** * List of detected navigation entries. */ @@ -204,7 +238,7 @@ export interface RumProfilerLongTaskEntry { */ readonly id?: string /** - * Duration in ns of the long task or long animation frame. + * Duration in ms of the long task or long animation frame. */ readonly duration: number /** @@ -213,6 +247,42 @@ export interface RumProfilerLongTaskEntry { readonly entryType: 'longtask' | 'long-animation-frame' startClocks: ClocksState } +/** + * Schema of a vital entry recorded during profiling. + */ +export interface RumProfilerVitalEntry { + /** + * RUM Vital id. + */ + readonly id: string + /** + * RUM Vital label. + */ + readonly label: string + /** + * Duration in ms of the vital. + */ + readonly duration?: number + startClocks: ClocksState +} +/** + * Schema of a action entry recorded during profiling. + */ +export interface RumProfilerActionEntry { + /** + * RUM Action id. + */ + readonly id: string + /** + * RUM Action label. + */ + readonly label: string + /** + * Duration in ms of the duration vital. + */ + readonly duration?: number + startClocks: ClocksState +} /** * Schema of a RUM view entry recorded during profiling. */ diff --git a/rum-events-format b/rum-events-format index 8dc61166ee..6ded6e4b6e 160000 --- a/rum-events-format +++ b/rum-events-format @@ -1 +1 @@ -Subproject commit 8dc61166ee818608892d13b6565ff04a3f2a7fe9 +Subproject commit 6ded6e4b6e39e9497335f8cc6a2426707fc68a15