From a86fd07f156bf4396d4c3ac621ef559d2c1dcb4e Mon Sep 17 00:00:00 2001 From: "roman.gaignault" Date: Wed, 3 Sep 2025 12:21:25 +0200 Subject: [PATCH 1/4] =?UTF-8?q?=E2=9C=A8=20New=20notRestoredReasons=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/browser/performanceObservable.ts | 14 ++++ .../src/domain/view/viewCollection.spec.ts | 1 + .../src/domain/view/viewCollection.ts | 1 + .../trackNavigationTimings.spec.ts | 84 +++++++++++++++++++ .../viewMetrics/trackNavigationTimings.ts | 11 ++- packages/rum-core/src/rawRumEvent.types.ts | 2 + 6 files changed, 111 insertions(+), 2 deletions(-) diff --git a/packages/rum-core/src/browser/performanceObservable.ts b/packages/rum-core/src/browser/performanceObservable.ts index 2c9493ba19..188920bbd9 100644 --- a/packages/rum-core/src/browser/performanceObservable.ts +++ b/packages/rum-core/src/browser/performanceObservable.ts @@ -75,6 +75,19 @@ export interface RumPerformancePaintTiming { toJSON(): Omit } +export interface RumNotRestoredReasonDetails { + reason: string +} + +export interface RumNotRestoredReasons { + children: RumNotRestoredReasons[] + id: string | null + name: string | null + reasons: RumNotRestoredReasonDetails[] | null + src: string | null + url: string | null +} + export interface RumPerformanceNavigationTiming extends Omit { entryType: RumPerformanceEntryType.NAVIGATION initiatorType: 'navigation' @@ -84,6 +97,7 @@ export interface RumPerformanceNavigationTiming extends Omit } diff --git a/packages/rum-core/src/domain/view/viewCollection.spec.ts b/packages/rum-core/src/domain/view/viewCollection.spec.ts index e279e711c1..3dd05bb534 100644 --- a/packages/rum-core/src/domain/view/viewCollection.spec.ts +++ b/packages/rum-core/src/domain/view/viewCollection.spec.ts @@ -160,6 +160,7 @@ describe('viewCollection', () => { long_task: { count: 10, }, + not_restored_reasons: undefined, performance: { cls: { score: 1, diff --git a/packages/rum-core/src/domain/view/viewCollection.ts b/packages/rum-core/src/domain/view/viewCollection.ts index 3d46c4aa1d..1beb70d3a7 100644 --- a/packages/rum-core/src/domain/view/viewCollection.ts +++ b/packages/rum-core/src/domain/view/viewCollection.ts @@ -129,6 +129,7 @@ function processViewUpdate( count: view.eventCounts.longTaskCount, }, performance: computeViewPerformanceData(view.commonViewMetrics, view.initialViewMetrics), + not_restored_reasons: view.initialViewMetrics.navigationTimings?.notRestoredReasons, resource: { count: view.eventCounts.resourceCount, }, diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackNavigationTimings.spec.ts b/packages/rum-core/src/domain/view/viewMetrics/trackNavigationTimings.spec.ts index bc62d32423..a42ed72e72 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackNavigationTimings.spec.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackNavigationTimings.spec.ts @@ -1,6 +1,7 @@ import { relativeNow, type Duration, type RelativeTime } from '@datadog/browser-core' import type { Clock } from '@datadog/browser-core/test' import { mockClock, registerCleanupTask } from '@datadog/browser-core/test' +import type { RumNotRestoredReasons } from '../../../browser/performanceObservable' import { mockDocumentReadyState, mockRumConfiguration } from '../../../../test' import type { NavigationTimings, RelevantNavigationTiming } from './trackNavigationTimings' import { trackNavigationTimings } from './trackNavigationTimings' @@ -21,6 +22,15 @@ const FAKE_INCOMPLETE_NAVIGATION_ENTRY: RelevantNavigationTiming = { responseStart: 0 as RelativeTime, } +const FAKE_NOT_RESTORED_REASONS: RumNotRestoredReasons = { + children: [], + id: null, + name: null, + reasons: [{ reason: 'unload-listener' }], + src: null, + url: 'https://example.com/', +} + describe('trackNavigationTimings', () => { let navigationTimingsCallback: jasmine.Spy<(timings: NavigationTimings) => void> let stop: () => void @@ -46,6 +56,7 @@ describe('trackNavigationTimings', () => { domContentLoaded: 345 as Duration, domInteractive: 234 as Duration, loadEvent: 567 as Duration, + notRestoredReasons: undefined, }) }) @@ -91,4 +102,77 @@ describe('trackNavigationTimings', () => { expect(navigationTimingsCallback).not.toHaveBeenCalled() }) + + it('includes notRestoredReasons when present', () => { + ;({ stop } = trackNavigationTimings(mockRumConfiguration(), navigationTimingsCallback, () => ({ + ...FAKE_NAVIGATION_ENTRY, + notRestoredReasons: FAKE_NOT_RESTORED_REASONS, + }))) + + clock.tick(0) + + expect(navigationTimingsCallback).toHaveBeenCalledOnceWith({ + firstByte: 123 as Duration, + domComplete: 456 as Duration, + domContentLoaded: 345 as Duration, + domInteractive: 234 as Duration, + loadEvent: 567 as Duration, + notRestoredReasons: FAKE_NOT_RESTORED_REASONS, + }) + }) + + it('handles null notRestoredReasons', () => { + ;({ stop } = trackNavigationTimings(mockRumConfiguration(), navigationTimingsCallback, () => ({ + ...FAKE_NAVIGATION_ENTRY, + notRestoredReasons: null, + }))) + + clock.tick(0) + + expect(navigationTimingsCallback).toHaveBeenCalledOnceWith({ + firstByte: 123 as Duration, + domComplete: 456 as Duration, + domContentLoaded: 345 as Duration, + domInteractive: 234 as Duration, + loadEvent: 567 as Duration, + notRestoredReasons: null, + }) + }) + + it('handles notRestoredReasons with nested iframes', () => { + const complexNotRestoredReasons: RumNotRestoredReasons = { + children: [ + { + children: [], + id: 'iframe-1', + name: 'myFrame', + reasons: null, + src: './frame.html', + url: 'https://example.com/frame.html', + }, + { + children: [], + id: 'iframe-2', + name: 'anotherFrame', + reasons: [{ reason: 'response-cache-control-no-store' }], + src: './another.html', + url: 'https://example.com/another.html', + }, + ], + id: null, + name: null, + reasons: [], + src: null, + url: 'https://example.com/', + } + + ;({ stop } = trackNavigationTimings(mockRumConfiguration(), navigationTimingsCallback, () => ({ + ...FAKE_NAVIGATION_ENTRY, + notRestoredReasons: complexNotRestoredReasons, + }))) + + clock.tick(0) + + expect(navigationTimingsCallback.calls.mostRecent().args[0].notRestoredReasons).toEqual(complexNotRestoredReasons) + }) }) diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackNavigationTimings.ts b/packages/rum-core/src/domain/view/viewMetrics/trackNavigationTimings.ts index b70585f267..8236669c35 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackNavigationTimings.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackNavigationTimings.ts @@ -1,6 +1,6 @@ import type { Duration, TimeoutId } from '@datadog/browser-core' import { setTimeout, relativeNow, runOnReadyState, clearTimeout } from '@datadog/browser-core' -import type { RumPerformanceNavigationTiming } from '../../../browser/performanceObservable' +import type { RumPerformanceNavigationTiming, RumNotRestoredReasons } from '../../../browser/performanceObservable' import type { RumConfiguration } from '../../configuration' import { getNavigationEntry } from '../../../browser/performanceUtils' @@ -10,13 +10,19 @@ export interface NavigationTimings { domInteractive: Duration loadEvent: Duration firstByte: Duration | undefined + notRestoredReasons?: RumNotRestoredReasons | null } // This is a subset of "RumPerformanceNavigationTiming" that only contains the relevant fields for // computing navigation timings. This is useful to mock the navigation entry in tests. export type RelevantNavigationTiming = Pick< RumPerformanceNavigationTiming, - 'domComplete' | 'domContentLoadedEventEnd' | 'domInteractive' | 'loadEventEnd' | 'responseStart' + | 'domComplete' + | 'domContentLoadedEventEnd' + | 'domInteractive' + | 'loadEventEnd' + | 'responseStart' + | 'notRestoredReasons' > export function trackNavigationTimings( @@ -44,6 +50,7 @@ function processNavigationEntry(entry: RelevantNavigationTiming): NavigationTimi // https://github.com/GoogleChrome/web-vitals/issues/137 // https://github.com/GoogleChrome/web-vitals/issues/162 firstByte: entry.responseStart >= 0 && entry.responseStart <= relativeNow() ? entry.responseStart : undefined, + notRestoredReasons: entry.notRestoredReasons, } } diff --git a/packages/rum-core/src/rawRumEvent.types.ts b/packages/rum-core/src/rawRumEvent.types.ts index 206f213b0d..041192000f 100644 --- a/packages/rum-core/src/rawRumEvent.types.ts +++ b/packages/rum-core/src/rawRumEvent.types.ts @@ -11,6 +11,7 @@ import type { Context, } from '@datadog/browser-core' import type { PageState } from './domain/contexts/pageStateHistory' +import type { RumNotRestoredReasons } from './browser/performanceObservable' export const RumEventType = { ACTION: 'action', @@ -127,6 +128,7 @@ export interface RawRumViewEvent { resource: Count frustration: Count performance?: ViewPerformanceData + not_restored_reasons?: RumNotRestoredReasons | null } display?: ViewDisplay privacy?: { From 53da2a1487aba6c521ce11e0e1ffe1e6b76b4b76 Mon Sep 17 00:00:00 2001 From: "roman.gaignault" Date: Wed, 3 Sep 2025 13:41:53 +0200 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=90=9B=20object=20mapping?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/domain/view/viewCollection.ts | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/rum-core/src/domain/view/viewCollection.ts b/packages/rum-core/src/domain/view/viewCollection.ts index 1beb70d3a7..d2d2b53659 100644 --- a/packages/rum-core/src/domain/view/viewCollection.ts +++ b/packages/rum-core/src/domain/view/viewCollection.ts @@ -4,6 +4,7 @@ import { discardNegativeDuration } from '../discardNegativeDuration' import type { RecorderApi } from '../../boot/rumPublicApi' import type { RawRumViewEvent, ViewPerformanceData } from '../../rawRumEvent.types' import { RumEventType } from '../../rawRumEvent.types' +import type { RumNotRestoredReasons } from '../../browser/performanceObservable' import type { LifeCycle, RawRumEventCollectedData } from '../lifeCycle' import { LifeCycleEventType } from '../lifeCycle' import type { LocationChange } from '../../browser/locationChangeObservable' @@ -129,7 +130,7 @@ function processViewUpdate( count: view.eventCounts.longTaskCount, }, performance: computeViewPerformanceData(view.commonViewMetrics, view.initialViewMetrics), - not_restored_reasons: view.initialViewMetrics.navigationTimings?.notRestoredReasons, + not_restored_reasons: mapNotRestoredReasons(view.initialViewMetrics.navigationTimings?.notRestoredReasons), resource: { count: view.eventCounts.resourceCount, }, @@ -172,6 +173,26 @@ function processViewUpdate( } } +function mapNotRestoredReasons( + notRestoredReasons: RumNotRestoredReasons | null | undefined +): RumNotRestoredReasons | undefined { + if (!notRestoredReasons) { + return undefined + } + + function mapObject(reasonObject: RumNotRestoredReasons): RumNotRestoredReasons { + return { + src: reasonObject.src, + id: reasonObject.id, + url: reasonObject.url, + name: reasonObject.name, + reasons: reasonObject.reasons ? reasonObject.reasons.map((r) => ({ reason: r.reason })) : null, + children: reasonObject.children ? reasonObject.children.map(mapObject) : [], + } + } + return mapObject(notRestoredReasons) +} + function computeViewPerformanceData( { cumulativeLayoutShift, interactionToNextPaint }: CommonViewMetrics, { firstContentfulPaint, firstInput, largestContentfulPaint }: InitialViewMetrics From 6957401920b0de9ce7f62075412b4b4a9a0bb306 Mon Sep 17 00:00:00 2001 From: "roman.gaignault" Date: Tue, 30 Sep 2025 16:51:49 +0200 Subject: [PATCH 3/4] use toJson --- .../src/browser/performanceObservable.ts | 1 + .../src/domain/view/viewCollection.ts | 23 ++----------------- 2 files changed, 3 insertions(+), 21 deletions(-) diff --git a/packages/rum-core/src/browser/performanceObservable.ts b/packages/rum-core/src/browser/performanceObservable.ts index 188920bbd9..abef8de6b3 100644 --- a/packages/rum-core/src/browser/performanceObservable.ts +++ b/packages/rum-core/src/browser/performanceObservable.ts @@ -86,6 +86,7 @@ export interface RumNotRestoredReasons { reasons: RumNotRestoredReasonDetails[] | null src: string | null url: string | null + toJSON(): Omit } export interface RumPerformanceNavigationTiming extends Omit { diff --git a/packages/rum-core/src/domain/view/viewCollection.ts b/packages/rum-core/src/domain/view/viewCollection.ts index d2d2b53659..6fd21fe194 100644 --- a/packages/rum-core/src/domain/view/viewCollection.ts +++ b/packages/rum-core/src/domain/view/viewCollection.ts @@ -130,7 +130,8 @@ function processViewUpdate( count: view.eventCounts.longTaskCount, }, performance: computeViewPerformanceData(view.commonViewMetrics, view.initialViewMetrics), - not_restored_reasons: mapNotRestoredReasons(view.initialViewMetrics.navigationTimings?.notRestoredReasons), + not_restored_reasons: + view.initialViewMetrics.navigationTimings?.notRestoredReasons?.toJSON() as RumNotRestoredReasons, resource: { count: view.eventCounts.resourceCount, }, @@ -173,26 +174,6 @@ function processViewUpdate( } } -function mapNotRestoredReasons( - notRestoredReasons: RumNotRestoredReasons | null | undefined -): RumNotRestoredReasons | undefined { - if (!notRestoredReasons) { - return undefined - } - - function mapObject(reasonObject: RumNotRestoredReasons): RumNotRestoredReasons { - return { - src: reasonObject.src, - id: reasonObject.id, - url: reasonObject.url, - name: reasonObject.name, - reasons: reasonObject.reasons ? reasonObject.reasons.map((r) => ({ reason: r.reason })) : null, - children: reasonObject.children ? reasonObject.children.map(mapObject) : [], - } - } - return mapObject(notRestoredReasons) -} - function computeViewPerformanceData( { cumulativeLayoutShift, interactionToNextPaint }: CommonViewMetrics, { firstContentfulPaint, firstInput, largestContentfulPaint }: InitialViewMetrics From 32ab48a3f53d7b03e9dfeb4d4b446fed66a32ca2 Mon Sep 17 00:00:00 2001 From: "roman.gaignault" Date: Tue, 30 Sep 2025 17:27:31 +0200 Subject: [PATCH 4/4] typecheck --- packages/rum-core/src/browser/performanceObservable.ts | 2 +- packages/rum-core/src/domain/view/viewCollection.ts | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/rum-core/src/browser/performanceObservable.ts b/packages/rum-core/src/browser/performanceObservable.ts index abef8de6b3..7de14c5d5d 100644 --- a/packages/rum-core/src/browser/performanceObservable.ts +++ b/packages/rum-core/src/browser/performanceObservable.ts @@ -86,7 +86,7 @@ export interface RumNotRestoredReasons { reasons: RumNotRestoredReasonDetails[] | null src: string | null url: string | null - toJSON(): Omit + toJSON?(): RumNotRestoredReasons } export interface RumPerformanceNavigationTiming extends Omit { diff --git a/packages/rum-core/src/domain/view/viewCollection.ts b/packages/rum-core/src/domain/view/viewCollection.ts index 6fd21fe194..1c705a60a5 100644 --- a/packages/rum-core/src/domain/view/viewCollection.ts +++ b/packages/rum-core/src/domain/view/viewCollection.ts @@ -4,7 +4,6 @@ import { discardNegativeDuration } from '../discardNegativeDuration' import type { RecorderApi } from '../../boot/rumPublicApi' import type { RawRumViewEvent, ViewPerformanceData } from '../../rawRumEvent.types' import { RumEventType } from '../../rawRumEvent.types' -import type { RumNotRestoredReasons } from '../../browser/performanceObservable' import type { LifeCycle, RawRumEventCollectedData } from '../lifeCycle' import { LifeCycleEventType } from '../lifeCycle' import type { LocationChange } from '../../browser/locationChangeObservable' @@ -130,8 +129,7 @@ function processViewUpdate( count: view.eventCounts.longTaskCount, }, performance: computeViewPerformanceData(view.commonViewMetrics, view.initialViewMetrics), - not_restored_reasons: - view.initialViewMetrics.navigationTimings?.notRestoredReasons?.toJSON() as RumNotRestoredReasons, + not_restored_reasons: view.initialViewMetrics.navigationTimings?.notRestoredReasons?.toJSON?.(), resource: { count: view.eventCounts.resourceCount, },