Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions packages/rum-core/src/browser/performanceObservable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,19 @@ export interface RumPerformancePaintTiming {
toJSON(): Omit<RumPerformancePaintTiming, 'toJSON'>
}

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<RumPerformanceResourceTiming, 'entryType'> {
entryType: RumPerformanceEntryType.NAVIGATION
initiatorType: 'navigation'
Expand All @@ -84,6 +97,7 @@ export interface RumPerformanceNavigationTiming extends Omit<RumPerformanceResou
domContentLoadedEventEnd: RelativeTime
domInteractive: RelativeTime
loadEventEnd: RelativeTime
notRestoredReasons?: RumNotRestoredReasons | null

toJSON(): Omit<RumPerformanceNavigationTiming, 'toJSON'>
}
Expand Down
1 change: 1 addition & 0 deletions packages/rum-core/src/domain/view/viewCollection.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ describe('viewCollection', () => {
long_task: {
count: 10,
},
not_restored_reasons: undefined,
performance: {
cls: {
score: 1,
Expand Down
22 changes: 22 additions & 0 deletions packages/rum-core/src/domain/view/viewCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -129,6 +130,7 @@ function processViewUpdate(
count: view.eventCounts.longTaskCount,
},
performance: computeViewPerformanceData(view.commonViewMetrics, view.initialViewMetrics),
not_restored_reasons: mapNotRestoredReasons(view.initialViewMetrics.navigationTimings?.notRestoredReasons),
resource: {
count: view.eventCounts.resourceCount,
},
Expand Down Expand Up @@ -171,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
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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
Expand All @@ -46,6 +56,7 @@ describe('trackNavigationTimings', () => {
domContentLoaded: 345 as Duration,
domInteractive: 234 as Duration,
loadEvent: 567 as Duration,
notRestoredReasons: undefined,
})
})

Expand Down Expand Up @@ -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)
})
})
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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(
Expand Down Expand Up @@ -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,
}
}

Expand Down
2 changes: 2 additions & 0 deletions packages/rum-core/src/rawRumEvent.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -127,6 +128,7 @@ export interface RawRumViewEvent {
resource: Count
frustration: Count
performance?: ViewPerformanceData
not_restored_reasons?: RumNotRestoredReasons | null
}
display?: ViewDisplay
privacy?: {
Expand Down