diff --git a/packages/core/src/tools/experimentalFeatures.ts b/packages/core/src/tools/experimentalFeatures.ts index bc9cafb4d4..a29a3c1ead 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() diff --git a/packages/rum-core/src/domain/view/viewCollection.spec.ts b/packages/rum-core/src/domain/view/viewCollection.spec.ts index 56e4925f0b..b4f2b9ec5a 100644 --- a/packages/rum-core/src/domain/view/viewCollection.spec.ts +++ b/packages/rum-core/src/domain/view/viewCollection.spec.ts @@ -179,6 +179,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, diff --git a/packages/rum-core/src/domain/view/viewCollection.ts b/packages/rum-core/src/domain/view/viewCollection.ts index 465b5e46c3..bda7502a28 100644 --- a/packages/rum-core/src/domain/view/viewCollection.ts +++ b/packages/rum-core/src/domain/view/viewCollection.ts @@ -192,6 +192,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), 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 83ad0e925f..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,5 +1,5 @@ import type { Duration, RelativeTime } from '@datadog/browser-core' -import { elapsed, relativeNow } from '@datadog/browser-core' +import { elapsed, relativeNow, ExperimentalFeature, addExperimentalFeatures } from '@datadog/browser-core' import { registerCleanupTask } from '@datadog/browser-core/test' import { appendElement, @@ -106,6 +106,7 @@ describe('trackInteractionToNextPaint', () => { value: 100 as Duration, targetSelector: undefined, time: 1 as RelativeTime, + subParts: undefined, }) }) @@ -121,6 +122,7 @@ describe('trackInteractionToNextPaint', () => { value: MAX_INP_VALUE, targetSelector: undefined, time: 1 as RelativeTime, + subParts: undefined, }) }) @@ -137,6 +139,7 @@ describe('trackInteractionToNextPaint', () => { value: 98 as Duration, targetSelector: undefined, time: 98 as RelativeTime, + subParts: undefined, }) }) @@ -158,6 +161,7 @@ describe('trackInteractionToNextPaint', () => { value: 40 as Duration, targetSelector: undefined, time: 1 as RelativeTime, + subParts: undefined, }) }) @@ -175,6 +179,7 @@ describe('trackInteractionToNextPaint', () => { value: 100 as Duration, targetSelector: undefined, time: 100 as RelativeTime, + subParts: undefined, }) }) @@ -265,6 +270,227 @@ 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 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() + + 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', () => { diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts index 10e019bef3..8d05ee293d 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 } from '@datadog/browser-core' import type { Duration, RelativeTime } from '@datadog/browser-core' +import { elapsed, ExperimentalFeature, isExperimentalFeatureEnabled, 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,11 +18,27 @@ 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 time?: Duration + subParts?: { + inputDelay: Duration + processingDuration: Duration + 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. @@ -42,14 +58,32 @@ export function trackInteractionToNextPaint( } } - const { getViewInteractionCount, stopViewInteractionCount } = trackViewInteractionCount(viewLoadingType) - let viewEnd = Infinity as RelativeTime + let currentInp: + | { + duration: Duration + startTime: Duration + targetSelector?: string + subParts?: InteractionToNextPaint['subParts'] + } + | undefined + const { getViewInteractionCount, stopViewInteractionCount } = trackViewInteractionCount(viewLoadingType) const longestInteractions = trackLongestInteractions(getViewInteractionCount) - let interactionToNextPaint = -1 as Duration - let interactionToNextPaintTargetSelector: string | undefined - let interactionToNextPaintStartTime: Duration | undefined + const subPartsTracker = isExperimentalFeatureEnabled(ExperimentalFeature.INP_SUBPARTS) + ? createSubPartsTracker(longestInteractions) + : null + 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) { @@ -60,45 +94,48 @@ export function trackInteractionToNextPaint( entry.startTime <= viewEnd ) { longestInteractions.process(entry) + subPartsTracker?.process(entry) } } + subPartsTracker?.pruneUntracked() + const candidate = longestInteractions.estimateP98Interaction() + if (candidate) { + updateCurrentInp(candidate) + } + } - const newInteraction = longestInteractions.estimateP98Interaction() - if (newInteraction && 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 - ) + 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) } + currentInp = { + duration: candidate.duration, + startTime: newStartTime, + 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)) } } - 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: Math.min(interactionToNextPaint, MAX_INP_VALUE) as Duration, - targetSelector: interactionToNextPaintTargetSelector, - time: interactionToNextPaintStartTime, + value: sanitizeInpValue(currentInp.duration), + targetSelector: currentInp.targetSelector, + time: currentInp.startTime, + subParts: currentInp.subParts, } } else if (getViewInteractionCount()) { return { @@ -113,10 +150,15 @@ export function trackInteractionToNextPaint( stop: () => { eventSubscription.unsubscribe() firstInputSubscription.unsubscribe() + subPartsTracker?.stop() }, } } +/** + * 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 = [] @@ -158,9 +200,17 @@ 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) + }, } } +/** + * 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() @@ -184,6 +234,103 @@ 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() + + 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) && @@ -191,3 +338,7 @@ export function isInteractionToNextPaintSupported() { 'interactionId' in PerformanceEventTiming.prototype ) } + +function sanitizeInpValue(inpValue: Duration) { + return Math.min(inpValue, MAX_INP_VALUE) as Duration +} diff --git a/packages/rum-core/src/rawRumEvent.types.ts b/packages/rum-core/src/rawRumEvent.types.ts index 766cc33e94..ccb4aae485 100644 --- a/packages/rum-core/src/rawRumEvent.types.ts +++ b/packages/rum-core/src/rawRumEvent.types.ts @@ -200,6 +200,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 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