Skip to content

Commit 79e1ba3

Browse files
Integrate seth.fowler/PANA-3969-try-to-collect-some-initial-view-metrics-even-when-page-unloads-early (#3808) into staging-35
Integrated commit sha: 5c5dfc2 Co-authored-by: sethfowler-datadog <[email protected]>
2 parents 0c37d1d + 5c5dfc2 commit 79e1ba3

File tree

2 files changed

+127
-25
lines changed

2 files changed

+127
-25
lines changed

packages/rum-core/src/domain/view/viewMetrics/startInitialViewMetricsTelemetry.spec.ts

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import type { Telemetry, RelativeTime, Duration, RawTelemetryEvent } from '@datadog/browser-core'
1+
import type { Telemetry, RelativeTime, Duration, RawTelemetryEvent, PageMayExitEvent } from '@datadog/browser-core'
2+
import { PageExitReason } from '@datadog/browser-core'
23
import type { MockTelemetry } from '@datadog/browser-core/test'
34
import { registerCleanupTask, startMockTelemetry } from '@datadog/browser-core/test'
45
import type { RumConfiguration } from '@datadog/browser-rum-core'
@@ -39,6 +40,18 @@ const TELEMETRY_FOR_VIEW_METRICS: RawTelemetryEvent = {
3940
},
4041
}
4142

43+
const TELEMETRY_FOR_EARLY_PAGE_UNLOAD: RawTelemetryEvent = {
44+
type: 'log',
45+
status: 'debug',
46+
message: 'Initial view metrics',
47+
metrics: {
48+
earlyPageUnload: {
49+
domContentLoaded: jasmine.anything(),
50+
timestamp: jasmine.anything(),
51+
},
52+
},
53+
}
54+
4255
describe('startInitialViewMetricsTelemetry', () => {
4356
const lifeCycle = new LifeCycle()
4457
let telemetry: MockTelemetry
@@ -49,6 +62,10 @@ describe('startInitialViewMetricsTelemetry', () => {
4962
telemetrySampleRate: 100,
5063
}
5164

65+
function generatePageMayExit(reason: PageExitReason) {
66+
lifeCycle.notify(LifeCycleEventType.PAGE_MAY_EXIT, { reason } as PageMayExitEvent)
67+
}
68+
5269
function generateViewUpdateWithInitialViewMetrics(initialViewMetrics: Partial<InitialViewMetrics>) {
5370
lifeCycle.notify(LifeCycleEventType.VIEW_UPDATED, { initialViewMetrics } as ViewEvent)
5471
}
@@ -72,6 +89,12 @@ describe('startInitialViewMetricsTelemetry', () => {
7289
expect(await telemetry.getEvents()).toEqual([jasmine.objectContaining(TELEMETRY_FOR_VIEW_METRICS)])
7390
})
7491

92+
it('should collect minimal initial view metrics telemetry if page unloads early', async () => {
93+
startInitialViewMetricsTelemetryCollection()
94+
generatePageMayExit(PageExitReason.UNLOADING)
95+
expect(await telemetry.getEvents()).toEqual([jasmine.objectContaining(TELEMETRY_FOR_EARLY_PAGE_UNLOAD)])
96+
})
97+
7598
it('should not collect initial view metrics telemetry twice', async () => {
7699
startInitialViewMetricsTelemetryCollection()
77100

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

114+
it('should not collect early page unload telemetry if page is not unloading', async () => {
115+
startInitialViewMetricsTelemetryCollection()
116+
generatePageMayExit(PageExitReason.FROZEN)
117+
generatePageMayExit(PageExitReason.HIDDEN)
118+
generatePageMayExit(PageExitReason.PAGEHIDE)
119+
expect(await telemetry.hasEvents()).toBe(false)
120+
})
121+
122+
it('should not collect early page unload telemetry if initial view metrics were already collected', async () => {
123+
startInitialViewMetricsTelemetryCollection()
124+
125+
generateViewUpdateWithInitialViewMetrics(VIEW_METRICS)
126+
expect(await telemetry.getEvents()).toEqual([jasmine.objectContaining(TELEMETRY_FOR_VIEW_METRICS)])
127+
telemetry.reset()
128+
129+
generatePageMayExit(PageExitReason.UNLOADING)
130+
expect(await telemetry.hasEvents()).toBe(false)
131+
})
132+
133+
it('should collect initial view metrics even if page unload telemetry was already collected', async () => {
134+
startInitialViewMetricsTelemetryCollection()
135+
136+
generatePageMayExit(PageExitReason.UNLOADING)
137+
expect(await telemetry.getEvents()).toEqual([jasmine.objectContaining(TELEMETRY_FOR_EARLY_PAGE_UNLOAD)])
138+
telemetry.reset()
139+
140+
generateViewUpdateWithInitialViewMetrics(VIEW_METRICS)
141+
expect(await telemetry.getEvents()).toEqual([jasmine.objectContaining(TELEMETRY_FOR_VIEW_METRICS)])
142+
})
143+
91144
it('should not collect initial view metrics telemetry until LCP is known', async () => {
92145
startInitialViewMetricsTelemetryCollection()
93146

@@ -123,5 +176,7 @@ describe('startInitialViewMetricsTelemetry', () => {
123176
})
124177
generateViewUpdateWithInitialViewMetrics(VIEW_METRICS)
125178
expect(await telemetry.hasEvents()).toBe(false)
179+
generatePageMayExit(PageExitReason.UNLOADING)
180+
expect(await telemetry.hasEvents()).toBe(false)
126181
})
127182
})
Lines changed: 71 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import type { Context, Telemetry } from '@datadog/browser-core'
2-
import { performDraw, addTelemetryMetrics, noop } from '@datadog/browser-core'
1+
import type { Context, RelativeTime, Telemetry } from '@datadog/browser-core'
2+
import { PageExitReason, performDraw, addTelemetryMetrics, noop, relativeNow } from '@datadog/browser-core'
3+
import { getNavigationEntry } from '../../../browser/performanceUtils'
34
import { LifeCycleEventType } from '../../lifeCycle'
45
import type { LifeCycle } from '../../lifeCycle'
56
import type { RumConfiguration } from '../../configuration'
@@ -8,7 +9,7 @@ import type { NavigationTimings } from './trackNavigationTimings'
89

910
const INITIAL_VIEW_METRICS_TELEMETRY_NAME = 'Initial view metrics'
1011

11-
interface CoreInitialViewMetrics extends Context {
12+
interface AfterPageLoadInitialViewMetrics extends Context {
1213
lcp: {
1314
value: number
1415
}
@@ -21,6 +22,13 @@ interface CoreInitialViewMetrics extends Context {
2122
}
2223
}
2324

25+
interface EarlyPageUnloadInitialViewMetrics extends Context {
26+
earlyPageUnload: {
27+
domContentLoaded: number | undefined
28+
timestamp: number
29+
}
30+
}
31+
2432
export function startInitialViewMetricsTelemetry(
2533
configuration: RumConfiguration,
2634
lifeCycle: LifeCycle,
@@ -32,38 +40,65 @@ export function startInitialViewMetricsTelemetry(
3240
return { stop: noop }
3341
}
3442

35-
const { unsubscribe } = lifeCycle.subscribe(LifeCycleEventType.VIEW_UPDATED, ({ initialViewMetrics }) => {
36-
if (!initialViewMetrics.largestContentfulPaint || !initialViewMetrics.navigationTimings) {
37-
return
43+
const { unsubscribe: unsubscribePageMayExit } = lifeCycle.subscribe(
44+
LifeCycleEventType.PAGE_MAY_EXIT,
45+
({ reason }) => {
46+
if (reason !== PageExitReason.UNLOADING) {
47+
return
48+
}
49+
50+
const navigationEntry = getNavigationEntry()
51+
addTelemetryMetrics(INITIAL_VIEW_METRICS_TELEMETRY_NAME, {
52+
metrics: createEarlyPageUnloadInitialViewMetrics(navigationEntry.domContentLoadedEventEnd, relativeNow()),
53+
})
54+
55+
// Only send metrics in response to PAGE_MAY_EXIT once, but keep the subscription to
56+
// VIEW_UPDATED in case the page doesn't actually exit and we do eventually get
57+
// final numbers.
58+
unsubscribePageMayExit()
3859
}
60+
)
61+
62+
const { unsubscribe: unsubscribeViewUpdated } = lifeCycle.subscribe(
63+
LifeCycleEventType.VIEW_UPDATED,
64+
({ initialViewMetrics }) => {
65+
if (!initialViewMetrics.largestContentfulPaint || !initialViewMetrics.navigationTimings) {
66+
return
67+
}
3968

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

48-
addTelemetryMetrics(INITIAL_VIEW_METRICS_TELEMETRY_NAME, {
49-
metrics: createCoreInitialViewMetrics(
50-
initialViewMetrics.largestContentfulPaint,
51-
initialViewMetrics.navigationTimings
52-
),
53-
})
77+
addTelemetryMetrics(INITIAL_VIEW_METRICS_TELEMETRY_NAME, {
78+
metrics: createAfterPageLoadInitialViewMetrics(
79+
initialViewMetrics.largestContentfulPaint,
80+
initialViewMetrics.navigationTimings
81+
),
82+
})
5483

55-
unsubscribe()
56-
})
84+
// Don't send any further metrics.
85+
unsubscribePageMayExit()
86+
unsubscribeViewUpdated()
87+
}
88+
)
5789

5890
return {
59-
stop: unsubscribe,
91+
stop: () => {
92+
unsubscribePageMayExit()
93+
unsubscribeViewUpdated()
94+
},
6095
}
6196
}
6297

63-
function createCoreInitialViewMetrics(
98+
function createAfterPageLoadInitialViewMetrics(
6499
lcp: LargestContentfulPaint,
65100
navigation: NavigationTimings
66-
): CoreInitialViewMetrics {
101+
): AfterPageLoadInitialViewMetrics {
67102
return {
68103
lcp: {
69104
value: lcp.value,
@@ -77,3 +112,15 @@ function createCoreInitialViewMetrics(
77112
},
78113
}
79114
}
115+
116+
function createEarlyPageUnloadInitialViewMetrics(
117+
domContentLoadedEventEnd: RelativeTime,
118+
timestamp: RelativeTime
119+
): EarlyPageUnloadInitialViewMetrics {
120+
return {
121+
earlyPageUnload: {
122+
domContentLoaded: domContentLoadedEventEnd > 0 ? domContentLoadedEventEnd : undefined,
123+
timestamp,
124+
},
125+
}
126+
}

0 commit comments

Comments
 (0)