diff --git a/packages/rum-core/src/browser/performanceObservable.ts b/packages/rum-core/src/browser/performanceObservable.ts index 2c9493ba19..7de14c5d5d 100644 --- a/packages/rum-core/src/browser/performanceObservable.ts +++ b/packages/rum-core/src/browser/performanceObservable.ts @@ -75,6 +75,20 @@ 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 + toJSON?(): RumNotRestoredReasons +} + export interface RumPerformanceNavigationTiming extends Omit { entryType: RumPerformanceEntryType.NAVIGATION initiatorType: 'navigation' @@ -84,6 +98,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..1c705a60a5 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?.toJSON?.(), 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?: {