Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Telemetry, RelativeTime, Duration, RawTelemetryEvent } from '@datadog/browser-core'
import type { Telemetry, RelativeTime, Duration, RawTelemetryEvent, PageMayExitEvent } from '@datadog/browser-core'
import { PageExitReason } from '@datadog/browser-core'
import type { MockTelemetry } from '@datadog/browser-core/test'
import { registerCleanupTask, startMockTelemetry } from '@datadog/browser-core/test'
import type { RumConfiguration } from '@datadog/browser-rum-core'
Expand Down Expand Up @@ -39,6 +40,18 @@ const TELEMETRY_FOR_VIEW_METRICS: RawTelemetryEvent = {
},
}

const TELEMETRY_FOR_EARLY_PAGE_UNLOAD: RawTelemetryEvent = {
type: 'log',
status: 'debug',
message: 'Initial view metrics',
metrics: {
earlyPageUnload: {
domContentLoaded: jasmine.anything(),
timestamp: jasmine.anything(),
},
},
}

describe('startInitialViewMetricsTelemetry', () => {
const lifeCycle = new LifeCycle()
let telemetry: MockTelemetry
Expand All @@ -49,6 +62,10 @@ describe('startInitialViewMetricsTelemetry', () => {
telemetrySampleRate: 100,
}

function generatePageMayExit(reason: PageExitReason) {
lifeCycle.notify(LifeCycleEventType.PAGE_MAY_EXIT, { reason } as PageMayExitEvent)
}

function generateViewUpdateWithInitialViewMetrics(initialViewMetrics: Partial<InitialViewMetrics>) {
lifeCycle.notify(LifeCycleEventType.VIEW_UPDATED, { initialViewMetrics } as ViewEvent)
}
Expand All @@ -72,6 +89,12 @@ describe('startInitialViewMetricsTelemetry', () => {
expect(await telemetry.getEvents()).toEqual([jasmine.objectContaining(TELEMETRY_FOR_VIEW_METRICS)])
})

it('should collect minimal initial view metrics telemetry if page unloads early', async () => {
startInitialViewMetricsTelemetryCollection()
generatePageMayExit(PageExitReason.UNLOADING)
expect(await telemetry.getEvents()).toEqual([jasmine.objectContaining(TELEMETRY_FOR_EARLY_PAGE_UNLOAD)])
})

it('should not collect initial view metrics telemetry twice', async () => {
startInitialViewMetricsTelemetryCollection()

Expand All @@ -88,6 +111,36 @@ describe('startInitialViewMetricsTelemetry', () => {
expect(await telemetry.hasEvents()).toBe(false)
})

it('should not collect early page unload telemetry if page is not unloading', async () => {
startInitialViewMetricsTelemetryCollection()
generatePageMayExit(PageExitReason.FROZEN)
generatePageMayExit(PageExitReason.HIDDEN)
generatePageMayExit(PageExitReason.PAGEHIDE)
expect(await telemetry.hasEvents()).toBe(false)
})

it('should not collect early page unload telemetry if initial view metrics were already collected', async () => {
startInitialViewMetricsTelemetryCollection()

generateViewUpdateWithInitialViewMetrics(VIEW_METRICS)
expect(await telemetry.getEvents()).toEqual([jasmine.objectContaining(TELEMETRY_FOR_VIEW_METRICS)])
telemetry.reset()

generatePageMayExit(PageExitReason.UNLOADING)
expect(await telemetry.hasEvents()).toBe(false)
})

it('should collect initial view metrics even if page unload telemetry was already collected', async () => {
startInitialViewMetricsTelemetryCollection()

generatePageMayExit(PageExitReason.UNLOADING)
expect(await telemetry.getEvents()).toEqual([jasmine.objectContaining(TELEMETRY_FOR_EARLY_PAGE_UNLOAD)])
telemetry.reset()

generateViewUpdateWithInitialViewMetrics(VIEW_METRICS)
expect(await telemetry.getEvents()).toEqual([jasmine.objectContaining(TELEMETRY_FOR_VIEW_METRICS)])
})

it('should not collect initial view metrics telemetry until LCP is known', async () => {
startInitialViewMetricsTelemetryCollection()

Expand Down Expand Up @@ -123,5 +176,7 @@ describe('startInitialViewMetricsTelemetry', () => {
})
generateViewUpdateWithInitialViewMetrics(VIEW_METRICS)
expect(await telemetry.hasEvents()).toBe(false)
generatePageMayExit(PageExitReason.UNLOADING)
expect(await telemetry.hasEvents()).toBe(false)
})
})
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Context, Telemetry } from '@datadog/browser-core'
import { performDraw, addTelemetryMetrics, noop } from '@datadog/browser-core'
import type { Context, RelativeTime, Telemetry } from '@datadog/browser-core'
import { PageExitReason, performDraw, addTelemetryMetrics, noop, relativeNow } from '@datadog/browser-core'
import { getNavigationEntry } from '../../../browser/performanceUtils'
import { LifeCycleEventType } from '../../lifeCycle'
import type { LifeCycle } from '../../lifeCycle'
import type { RumConfiguration } from '../../configuration'
Expand All @@ -8,7 +9,7 @@ import type { NavigationTimings } from './trackNavigationTimings'

const INITIAL_VIEW_METRICS_TELEMETRY_NAME = 'Initial view metrics'

interface CoreInitialViewMetrics extends Context {
interface AfterPageLoadInitialViewMetrics extends Context {
lcp: {
value: number
}
Expand All @@ -21,6 +22,13 @@ interface CoreInitialViewMetrics extends Context {
}
}

interface EarlyPageUnloadInitialViewMetrics extends Context {
earlyPageUnload: {
domContentLoaded: number | undefined
timestamp: number
}
}

export function startInitialViewMetricsTelemetry(
configuration: RumConfiguration,
lifeCycle: LifeCycle,
Expand All @@ -32,38 +40,65 @@ export function startInitialViewMetricsTelemetry(
return { stop: noop }
}

const { unsubscribe } = lifeCycle.subscribe(LifeCycleEventType.VIEW_UPDATED, ({ initialViewMetrics }) => {
if (!initialViewMetrics.largestContentfulPaint || !initialViewMetrics.navigationTimings) {
return
const { unsubscribe: unsubscribePageMayExit } = lifeCycle.subscribe(
LifeCycleEventType.PAGE_MAY_EXIT,
({ reason }) => {
if (reason !== PageExitReason.UNLOADING) {
return
}

const navigationEntry = getNavigationEntry()
addTelemetryMetrics(INITIAL_VIEW_METRICS_TELEMETRY_NAME, {
metrics: createEarlyPageUnloadInitialViewMetrics(navigationEntry.domContentLoadedEventEnd, relativeNow()),
})

// Only send metrics in response to PAGE_MAY_EXIT once, but keep the subscription to
// VIEW_UPDATED in case the page doesn't actually exit and we do eventually get
// final numbers.
unsubscribePageMayExit()
}
)

const { unsubscribe: unsubscribeViewUpdated } = lifeCycle.subscribe(
LifeCycleEventType.VIEW_UPDATED,
({ initialViewMetrics }) => {
if (!initialViewMetrics.largestContentfulPaint || !initialViewMetrics.navigationTimings) {
return
}

// The navigation timings become available shortly after the load event fires, so
// we're snapshotting the LCP value available at that point. However, more LCP values
// can be emitted until the page is scrolled or interacted with, so it's possible that
// the final LCP value may differ. These metrics are intended to help diagnose
// performance issues early in the page load process, and using LCP-at-page-load is a
// good fit for that use case, but it's important to be aware that this is not
// necessarily equivalent to the normal LCP metric.
// The navigation timings become available shortly after the load event fires, so
// we're snapshotting the LCP value available at that point. However, more LCP values
// can be emitted until the page is scrolled or interacted with, so it's possible that
// the final LCP value may differ. These metrics are intended to help diagnose
// performance issues early in the page load process, and using LCP-at-page-load is a
// good fit for that use case, but it's important to be aware that this is not
// necessarily equivalent to the normal LCP metric.

addTelemetryMetrics(INITIAL_VIEW_METRICS_TELEMETRY_NAME, {
metrics: createCoreInitialViewMetrics(
initialViewMetrics.largestContentfulPaint,
initialViewMetrics.navigationTimings
),
})
addTelemetryMetrics(INITIAL_VIEW_METRICS_TELEMETRY_NAME, {
metrics: createAfterPageLoadInitialViewMetrics(
initialViewMetrics.largestContentfulPaint,
initialViewMetrics.navigationTimings
),
})

unsubscribe()
})
// Don't send any further metrics.
unsubscribePageMayExit()
unsubscribeViewUpdated()
}
)

return {
stop: unsubscribe,
stop: () => {
unsubscribePageMayExit()
unsubscribeViewUpdated()
},
}
}

function createCoreInitialViewMetrics(
function createAfterPageLoadInitialViewMetrics(
lcp: LargestContentfulPaint,
navigation: NavigationTimings
): CoreInitialViewMetrics {
): AfterPageLoadInitialViewMetrics {
return {
lcp: {
value: lcp.value,
Expand All @@ -77,3 +112,15 @@ function createCoreInitialViewMetrics(
},
}
}

function createEarlyPageUnloadInitialViewMetrics(
domContentLoadedEventEnd: RelativeTime,
timestamp: RelativeTime
): EarlyPageUnloadInitialViewMetrics {
return {
earlyPageUnload: {
domContentLoaded: domContentLoadedEventEnd > 0 ? domContentLoadedEventEnd : undefined,
timestamp,
},
}
}