Skip to content

Commit 084b7ac

Browse files
✨[PANA-3817] Add telemetry for initial view metrics (#3788)
1 parent bc401aa commit 084b7ac

File tree

4 files changed

+216
-0
lines changed

4 files changed

+216
-0
lines changed

packages/rum-core/src/boot/startRum.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import { startTrackingConsentContext } from '../domain/contexts/trackingConsentC
5656
import type { Hooks } from '../domain/hooks'
5757
import { createHooks } from '../domain/hooks'
5858
import { startEventCollection } from '../domain/event/eventCollection'
59+
import { startInitialViewMetricsTelemetry } from '../domain/view/viewMetrics/startInitialViewMetricsTelemetry'
5960
import type { RecorderApi, ProfilerApi } from './rumPublicApi'
6061

6162
export type StartRum = typeof startRum
@@ -180,6 +181,13 @@ export function startRum(
180181

181182
cleanupTasks.push(stopViewCollection)
182183

184+
const { stop: stopInitialViewMetricsTelemetry } = startInitialViewMetricsTelemetry(
185+
configuration,
186+
lifeCycle,
187+
telemetry
188+
)
189+
cleanupTasks.push(stopInitialViewMetricsTelemetry)
190+
183191
const { stop: stopResourceCollection } = startResourceCollection(lifeCycle, configuration, pageStateHistory)
184192
cleanupTasks.push(stopResourceCollection)
185193

packages/rum-core/src/domain/configuration/configuration.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ export interface RumConfiguration extends Configuration {
247247
trackBfcacheViews: boolean
248248
subdomain?: string
249249
customerDataTelemetrySampleRate: number
250+
initialViewMetricsTelemetrySampleRate: number
250251
segmentTelemetrySampleRate: number
251252
traceContextInjection: TraceContextInjection
252253
plugins: RumPlugin[]
@@ -319,6 +320,7 @@ export function validateAndBuildRumConfiguration(
319320
: DefaultPrivacyLevel.MASK,
320321
enablePrivacyForActionName: !!initConfiguration.enablePrivacyForActionName,
321322
customerDataTelemetrySampleRate: 1,
323+
initialViewMetricsTelemetrySampleRate: 1,
322324
segmentTelemetrySampleRate: 1,
323325
traceContextInjection: objectHasValue(TraceContextInjection, initConfiguration.traceContextInjection)
324326
? initConfiguration.traceContextInjection
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import type { Telemetry, RelativeTime, Duration, RawTelemetryEvent } from '@datadog/browser-core'
2+
import type { MockTelemetry } from '@datadog/browser-core/test'
3+
import { registerCleanupTask, startMockTelemetry } from '@datadog/browser-core/test'
4+
import type { RumConfiguration } from '@datadog/browser-rum-core'
5+
import { LifeCycle, LifeCycleEventType } from '../../lifeCycle'
6+
import { mockRumConfiguration } from '../../../../test'
7+
import type { ViewEvent } from '../trackViews'
8+
import { startInitialViewMetricsTelemetry } from './startInitialViewMetricsTelemetry'
9+
import type { InitialViewMetrics } from './trackInitialViewMetrics'
10+
11+
const VIEW_METRICS: Partial<InitialViewMetrics> = {
12+
largestContentfulPaint: {
13+
value: 100 as RelativeTime,
14+
},
15+
navigationTimings: {
16+
domComplete: 10 as Duration,
17+
domContentLoaded: 20 as Duration,
18+
domInteractive: 30 as Duration,
19+
firstByte: 40 as Duration,
20+
loadEvent: 50 as Duration,
21+
},
22+
}
23+
24+
const TELEMETRY_FOR_VIEW_METRICS: RawTelemetryEvent = {
25+
type: 'log',
26+
status: 'debug',
27+
message: 'Initial view metrics',
28+
metrics: {
29+
lcp: {
30+
value: 100,
31+
},
32+
navigation: {
33+
domComplete: 10,
34+
domContentLoaded: 20,
35+
domInteractive: 30,
36+
firstByte: 40,
37+
loadEvent: 50,
38+
},
39+
},
40+
}
41+
42+
describe('startInitialViewMetricsTelemetry', () => {
43+
const lifeCycle = new LifeCycle()
44+
let telemetry: MockTelemetry
45+
46+
const config: Partial<RumConfiguration> = {
47+
maxTelemetryEventsPerPage: 2,
48+
initialViewMetricsTelemetrySampleRate: 100,
49+
telemetrySampleRate: 100,
50+
}
51+
52+
function generateViewUpdateWithInitialViewMetrics(initialViewMetrics: Partial<InitialViewMetrics>) {
53+
lifeCycle.notify(LifeCycleEventType.VIEW_UPDATED, { initialViewMetrics } as ViewEvent)
54+
}
55+
56+
function startInitialViewMetricsTelemetryCollection(partialConfig: Partial<RumConfiguration> = config) {
57+
const configuration = mockRumConfiguration(partialConfig)
58+
telemetry = startMockTelemetry()
59+
const { stop: stopInitialViewMetricsTelemetryCollection } = startInitialViewMetricsTelemetry(
60+
configuration,
61+
lifeCycle,
62+
{
63+
enabled: true,
64+
} as Telemetry
65+
)
66+
registerCleanupTask(stopInitialViewMetricsTelemetryCollection)
67+
}
68+
69+
it('should collect initial view metrics telemetry', async () => {
70+
startInitialViewMetricsTelemetryCollection()
71+
generateViewUpdateWithInitialViewMetrics(VIEW_METRICS)
72+
expect(await telemetry.getEvents()).toEqual([jasmine.objectContaining(TELEMETRY_FOR_VIEW_METRICS)])
73+
})
74+
75+
it('should not collect initial view metrics telemetry twice', async () => {
76+
startInitialViewMetricsTelemetryCollection()
77+
78+
generateViewUpdateWithInitialViewMetrics(VIEW_METRICS)
79+
expect(await telemetry.getEvents()).toEqual([jasmine.objectContaining(TELEMETRY_FOR_VIEW_METRICS)])
80+
telemetry.reset()
81+
82+
generateViewUpdateWithInitialViewMetrics({
83+
...VIEW_METRICS,
84+
largestContentfulPaint: {
85+
value: 1000 as RelativeTime,
86+
},
87+
})
88+
expect(await telemetry.hasEvents()).toBe(false)
89+
})
90+
91+
it('should not collect initial view metrics telemetry until LCP is known', async () => {
92+
startInitialViewMetricsTelemetryCollection()
93+
94+
generateViewUpdateWithInitialViewMetrics({
95+
...VIEW_METRICS,
96+
largestContentfulPaint: undefined,
97+
})
98+
expect(await telemetry.hasEvents()).toBe(false)
99+
telemetry.reset()
100+
101+
generateViewUpdateWithInitialViewMetrics(VIEW_METRICS)
102+
expect(await telemetry.getEvents()).toEqual([jasmine.objectContaining(TELEMETRY_FOR_VIEW_METRICS)])
103+
})
104+
105+
it('should not collect initial view metrics telemetry until navigation timings are known', async () => {
106+
startInitialViewMetricsTelemetryCollection()
107+
108+
generateViewUpdateWithInitialViewMetrics({
109+
...VIEW_METRICS,
110+
navigationTimings: undefined,
111+
})
112+
expect(await telemetry.hasEvents()).toBe(false)
113+
telemetry.reset()
114+
115+
generateViewUpdateWithInitialViewMetrics(VIEW_METRICS)
116+
expect(await telemetry.getEvents()).toEqual([jasmine.objectContaining(TELEMETRY_FOR_VIEW_METRICS)])
117+
})
118+
119+
it('should not collect initial view metrics telemetry when telemetry is disabled', async () => {
120+
startInitialViewMetricsTelemetryCollection({
121+
telemetrySampleRate: 100,
122+
initialViewMetricsTelemetrySampleRate: 0,
123+
})
124+
generateViewUpdateWithInitialViewMetrics(VIEW_METRICS)
125+
expect(await telemetry.hasEvents()).toBe(false)
126+
})
127+
})
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import type { Context, Telemetry } from '@datadog/browser-core'
2+
import { performDraw, addTelemetryMetrics, noop } from '@datadog/browser-core'
3+
import { LifeCycleEventType } from '../../lifeCycle'
4+
import type { LifeCycle } from '../../lifeCycle'
5+
import type { RumConfiguration } from '../../configuration'
6+
import type { LargestContentfulPaint } from './trackLargestContentfulPaint'
7+
import type { NavigationTimings } from './trackNavigationTimings'
8+
9+
const INITIAL_VIEW_METRICS_TELEMETRY_NAME = 'Initial view metrics'
10+
11+
interface CoreInitialViewMetrics extends Context {
12+
lcp: {
13+
value: number
14+
}
15+
navigation: {
16+
domComplete: number
17+
domContentLoaded: number
18+
domInteractive: number
19+
firstByte: number | undefined
20+
loadEvent: number
21+
}
22+
}
23+
24+
export function startInitialViewMetricsTelemetry(
25+
configuration: RumConfiguration,
26+
lifeCycle: LifeCycle,
27+
telemetry: Telemetry
28+
) {
29+
const initialViewMetricsTelemetryEnabled =
30+
telemetry.enabled && performDraw(configuration.initialViewMetricsTelemetrySampleRate)
31+
if (!initialViewMetricsTelemetryEnabled) {
32+
return { stop: noop }
33+
}
34+
35+
const { unsubscribe } = lifeCycle.subscribe(LifeCycleEventType.VIEW_UPDATED, ({ initialViewMetrics }) => {
36+
if (!initialViewMetrics.largestContentfulPaint || !initialViewMetrics.navigationTimings) {
37+
return
38+
}
39+
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.
47+
48+
addTelemetryMetrics(INITIAL_VIEW_METRICS_TELEMETRY_NAME, {
49+
metrics: createCoreInitialViewMetrics(
50+
initialViewMetrics.largestContentfulPaint,
51+
initialViewMetrics.navigationTimings
52+
),
53+
})
54+
55+
unsubscribe()
56+
})
57+
58+
return {
59+
stop: unsubscribe,
60+
}
61+
}
62+
63+
function createCoreInitialViewMetrics(
64+
lcp: LargestContentfulPaint,
65+
navigation: NavigationTimings
66+
): CoreInitialViewMetrics {
67+
return {
68+
lcp: {
69+
value: lcp.value,
70+
},
71+
navigation: {
72+
domComplete: navigation.domComplete,
73+
domContentLoaded: navigation.domContentLoaded,
74+
domInteractive: navigation.domInteractive,
75+
firstByte: navigation.firstByte,
76+
loadEvent: navigation.loadEvent,
77+
},
78+
}
79+
}

0 commit comments

Comments
 (0)