Skip to content

Commit

Permalink
⚗️ [RUM-1020] Collect core web vitals target selectors (#2418)
Browse files Browse the repository at this point in the history
  • Loading branch information
amortemousque authored Sep 14, 2023
1 parent e93c5a8 commit bd8e66a
Show file tree
Hide file tree
Showing 40 changed files with 922 additions and 410 deletions.
1 change: 1 addition & 0 deletions packages/core/src/tools/experimentalFeatures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export enum ExperimentalFeature {
NO_RESOURCE_DURATION_FROZEN_STATE = 'no_resource_duration_frozen_state',
SCROLLMAP = 'scrollmap',
INTERACTION_TO_NEXT_PAINT = 'interaction_to_next_paint',
WEB_VITALS_ATTRIBUTION = 'web_vitals_attribution',
DISABLE_REPLAY_INLINE_CSS = 'disable_replay_inline_css',
}

Expand Down
3 changes: 2 additions & 1 deletion packages/rum-core/src/boot/startRum.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
} from '@datadog/browser-core/test'
import type { RumSessionManagerMock, TestSetupBuilder } from '../../test'
import { createRumSessionManagerMock, noopRecorderApi, noopWebVitalTelemetryDebug, setup } from '../../test'
import { RumPerformanceEntryType } from '../browser/performanceCollection'
import type { RumPerformanceNavigationTiming, RumPerformanceEntry } from '../browser/performanceCollection'
import type { LifeCycle } from '../domain/lifeCycle'
import { LifeCycleEventType } from '../domain/lifeCycle'
Expand Down Expand Up @@ -231,7 +232,7 @@ describe('rum events url', () => {
domComplete: 456 as RelativeTime,
domContentLoadedEventEnd: 345 as RelativeTime,
domInteractive: 234 as RelativeTime,
entryType: 'navigation',
entryType: RumPerformanceEntryType.NAVIGATION,
loadEventEnd: 567 as RelativeTime,
}
const VIEW_DURATION = 1000
Expand Down
83 changes: 52 additions & 31 deletions packages/rum-core/src/browser/performanceCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
relativeNow,
runOnReadyState,
addEventListener,
objectHasValue,
} from '@datadog/browser-core'

import type { RumConfiguration } from '../domain/configuration'
Expand All @@ -32,8 +33,22 @@ export interface RumPerformanceObserver extends PerformanceObserver {
observe(options?: PerformanceObserverInit & { durationThreshold: number }): void
}

// We want to use a real enum (i.e. not a const enum) here, to be able to check whether an arbitrary
// string is an expected performance entry
// eslint-disable-next-line no-restricted-syntax
export enum RumPerformanceEntryType {
EVENT = 'event',
FIRST_INPUT = 'first-input',
LARGEST_CONTENTFUL_PAINT = 'largest-contentful-paint',
LAYOUT_SHIFT = 'layout-shift',
LONG_TASK = 'longtask',
NAVIGATION = 'navigation',
PAINT = 'paint',
RESOURCE = 'resource',
}

export interface RumPerformanceResourceTiming {
entryType: 'resource'
entryType: RumPerformanceEntryType.RESOURCE
initiatorType: string
name: string
startTime: RelativeTime
Expand All @@ -54,20 +69,20 @@ export interface RumPerformanceResourceTiming {
}

export interface RumPerformanceLongTaskTiming {
entryType: 'longtask'
entryType: RumPerformanceEntryType.LONG_TASK
startTime: RelativeTime
duration: Duration
toJSON(): PerformanceEntryRepresentation
}

export interface RumPerformancePaintTiming {
entryType: 'paint'
entryType: RumPerformanceEntryType.PAINT
name: 'first-paint' | 'first-contentful-paint'
startTime: RelativeTime
}

export interface RumPerformanceNavigationTiming {
entryType: 'navigation'
entryType: RumPerformanceEntryType.NAVIGATION
domComplete: RelativeTime
domContentLoadedEventEnd: RelativeTime
domInteractive: RelativeTime
Expand All @@ -76,14 +91,14 @@ export interface RumPerformanceNavigationTiming {
}

export interface RumLargestContentfulPaintTiming {
entryType: 'largest-contentful-paint'
entryType: RumPerformanceEntryType.LARGEST_CONTENTFUL_PAINT
startTime: RelativeTime
size: number
element?: Element
}

export interface RumFirstInputTiming {
entryType: 'first-input'
entryType: RumPerformanceEntryType.FIRST_INPUT
startTime: RelativeTime
processingStart: RelativeTime
duration: Duration
Expand All @@ -92,14 +107,15 @@ export interface RumFirstInputTiming {
}

export interface RumPerformanceEventTiming {
entryType: 'event'
entryType: RumPerformanceEntryType.EVENT
startTime: RelativeTime
duration: Duration
interactionId?: number
target?: Node
}

export interface RumLayoutShiftTiming {
entryType: 'layout-shift'
entryType: RumPerformanceEntryType.LAYOUT_SHIFT
startTime: RelativeTime
value: number
hadRecentInput: boolean
Expand All @@ -122,7 +138,7 @@ function supportPerformanceObject() {
return window.performance !== undefined && 'getEntries' in performance
}

export function supportPerformanceTimingEvent(entryType: string) {
export function supportPerformanceTimingEvent(entryType: RumPerformanceEntryType) {
return (
window.PerformanceObserver &&
PerformanceObserver.supportedEntryTypes !== undefined &&
Expand All @@ -146,8 +162,18 @@ export function startPerformanceCollection(lifeCycle: LifeCycle, configuration:
const handlePerformanceEntryList = monitor((entries: PerformanceObserverEntryList) =>
handleRumPerformanceEntries(lifeCycle, configuration, entries.getEntries())
)
const mainEntries = ['resource', 'navigation', 'longtask', 'paint']
const experimentalEntries = ['largest-contentful-paint', 'first-input', 'layout-shift', 'event']
const mainEntries = [
RumPerformanceEntryType.RESOURCE,
RumPerformanceEntryType.NAVIGATION,
RumPerformanceEntryType.LONG_TASK,
RumPerformanceEntryType.PAINT,
]
const experimentalEntries = [
RumPerformanceEntryType.LARGEST_CONTENTFUL_PAINT,
RumPerformanceEntryType.FIRST_INPUT,
RumPerformanceEntryType.LAYOUT_SHIFT,
RumPerformanceEntryType.EVENT,
]

try {
// Experimental entries are not retrieved by performance.getEntries()
Expand Down Expand Up @@ -179,12 +205,12 @@ export function startPerformanceCollection(lifeCycle: LifeCycle, configuration:
})
}
}
if (!supportPerformanceTimingEvent('navigation')) {
if (!supportPerformanceTimingEvent(RumPerformanceEntryType.NAVIGATION)) {
retrieveNavigationTiming(configuration, (timing) => {
handleRumPerformanceEntries(lifeCycle, configuration, [timing])
})
}
if (!supportPerformanceTimingEvent('first-input')) {
if (!supportPerformanceTimingEvent(RumPerformanceEntryType.FIRST_INPUT)) {
retrieveFirstInputTiming(configuration, (timing) => {
handleRumPerformanceEntries(lifeCycle, configuration, [timing])
})
Expand All @@ -199,12 +225,15 @@ export function retrieveInitialDocumentResourceTiming(
let timing: RumPerformanceResourceTiming

const forcedAttributes = {
entryType: 'resource' as const,
entryType: RumPerformanceEntryType.RESOURCE as const,
initiatorType: FAKE_INITIAL_DOCUMENT,
traceId: getDocumentTraceId(document),
}
if (supportPerformanceTimingEvent('navigation') && performance.getEntriesByType('navigation').length > 0) {
const navigationEntry = performance.getEntriesByType('navigation')[0]
if (
supportPerformanceTimingEvent(RumPerformanceEntryType.NAVIGATION) &&
performance.getEntriesByType(RumPerformanceEntryType.NAVIGATION).length > 0
) {
const navigationEntry = performance.getEntriesByType(RumPerformanceEntryType.NAVIGATION)[0]
timing = assign(navigationEntry.toJSON(), forcedAttributes)
} else {
const relativePerformanceTiming = computeRelativePerformanceTiming()
Expand All @@ -230,7 +259,7 @@ function retrieveNavigationTiming(
function sendFakeTiming() {
callback(
assign(computeRelativePerformanceTiming(), {
entryType: 'navigation' as const,
entryType: RumPerformanceEntryType.NAVIGATION as const,
})
)
}
Expand Down Expand Up @@ -263,7 +292,7 @@ function retrieveFirstInputTiming(configuration: RumConfiguration, callback: (ti
// when the system received the event (e.g. evt.timeStamp) and when it could run the callback
// (e.g. performance.now()).
const timing: RumFirstInputTiming = {
entryType: 'first-input',
entryType: RumPerformanceEntryType.FIRST_INPUT,
processingStart: relativeNow(),
startTime: evt.timeStamp as RelativeTime,
duration: 0 as Duration, // arbitrary value to avoid nullable duration and simplify INP logic
Expand Down Expand Up @@ -337,17 +366,9 @@ function handleRumPerformanceEntries(
configuration: RumConfiguration,
entries: Array<PerformanceEntry | RumPerformanceEntry>
) {
const rumPerformanceEntries = entries.filter(
(entry) =>
entry.entryType === 'resource' ||
entry.entryType === 'navigation' ||
entry.entryType === 'paint' ||
entry.entryType === 'longtask' ||
entry.entryType === 'largest-contentful-paint' ||
entry.entryType === 'first-input' ||
entry.entryType === 'layout-shift' ||
entry.entryType === 'event'
) as RumPerformanceEntry[]
const rumPerformanceEntries = entries.filter((entry): entry is RumPerformanceEntry =>
objectHasValue(RumPerformanceEntryType, entry.entryType)
)

const rumAllowedPerformanceEntries = rumPerformanceEntries.filter(
(entry) => !isIncompleteNavigation(entry) && !isForbiddenResource(configuration, entry)
Expand All @@ -359,9 +380,9 @@ function handleRumPerformanceEntries(
}

function isIncompleteNavigation(entry: RumPerformanceEntry) {
return entry.entryType === 'navigation' && entry.loadEventEnd <= 0
return entry.entryType === RumPerformanceEntryType.NAVIGATION && entry.loadEventEnd <= 0
}

function isForbiddenResource(configuration: RumConfiguration, entry: RumPerformanceEntry) {
return entry.entryType === 'resource' && !isAllowedRequestUrl(configuration, entry.name)
return entry.entryType === RumPerformanceEntryType.RESOURCE && !isAllowedRequestUrl(configuration, entry.name)
}
2 changes: 1 addition & 1 deletion packages/rum-core/src/domain/action/trackClickActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ import type { LifeCycle } from '../lifeCycle'
import { LifeCycleEventType } from '../lifeCycle'
import { trackEventCounts } from '../trackEventCounts'
import { PAGE_ACTIVITY_VALIDATION_DELAY, waitPageActivityEnd } from '../waitPageActivityEnd'
import { getSelectorFromElement } from '../getSelectorFromElement'
import type { ClickChain } from './clickChain'
import { createClickChain } from './clickChain'
import { getActionNameFromElement } from './getActionNameFromElement'
import { getSelectorFromElement } from './getSelectorFromElement'
import type { MouseEventOnElement, UserActivity } from './listenActionEvents'
import { listenActionEvents } from './listenActionEvents'
import { computeFrustration } from './computeFrustration'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { IsolatedDom } from '../../../test'
import { createIsolatedDom } from '../../../test'
import type { IsolatedDom } from '../../test'
import { createIsolatedDom } from '../../test'
import { getSelectorFromElement, supportScopeSelector } from './getSelectorFromElement'

describe('getSelectorFromElement', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { cssEscape } from '@datadog/browser-core'
import { DEFAULT_PROGRAMMATIC_ACTION_NAME_ATTRIBUTE } from './getActionNameFromElement'
import { DEFAULT_PROGRAMMATIC_ACTION_NAME_ATTRIBUTE } from './action/getActionNameFromElement'

/**
* Stable attributes are attributes that are commonly used to identify parts of a UI (ex:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import type { Duration, RelativeTime, ServerDuration } from '@datadog/browser-core'
import type { RumSessionManagerMock, TestSetupBuilder } from '../../../test'
import { createRumSessionManagerMock, setup } from '../../../test'
import type { RumPerformanceEntry, RumPerformanceLongTaskTiming } from '../../browser/performanceCollection'
import {
RumPerformanceEntryType,
type RumPerformanceEntry,
type RumPerformanceLongTaskTiming,
} from '../../browser/performanceCollection'
import { RumEventType } from '../../rawRumEvent.types'
import { LifeCycleEventType } from '../lifeCycle'
import { startLongTaskCollection } from './longTaskCollection'

const LONG_TASK: RumPerformanceLongTaskTiming = {
duration: 100 as Duration,
entryType: 'longtask',
entryType: RumPerformanceEntryType.LONG_TASK,
startTime: 1234 as RelativeTime,
toJSON() {
return { name: 'self', duration: 100, entryType: 'longtask', startTime: 1234 }
Expand Down
3 changes: 2 additions & 1 deletion packages/rum-core/src/domain/longTask/longTaskCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import { RumEventType } from '../../rawRumEvent.types'
import type { LifeCycle } from '../lifeCycle'
import { LifeCycleEventType } from '../lifeCycle'
import type { RumSessionManager } from '../rumSessionManager'
import { RumPerformanceEntryType } from '../../browser/performanceCollection'

export function startLongTaskCollection(lifeCycle: LifeCycle, sessionManager: RumSessionManager) {
lifeCycle.subscribe(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, (entries) => {
for (const entry of entries) {
if (entry.entryType !== 'longtask') {
if (entry.entryType !== RumPerformanceEntryType.LONG_TASK) {
break
}
const session = sessionManager.findTrackedSession(entry.startTime)
Expand Down
3 changes: 2 additions & 1 deletion packages/rum-core/src/domain/resource/resourceCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
import type { ClocksState, Duration } from '@datadog/browser-core'
import type { RumConfiguration } from '../configuration'
import type { RumPerformanceEntry, RumPerformanceResourceTiming } from '../../browser/performanceCollection'
import { RumPerformanceEntryType } from '../../browser/performanceCollection'
import type {
PerformanceEntryRepresentation,
RumXhrResourceEventDomainContext,
Expand Down Expand Up @@ -50,7 +51,7 @@ export function startResourceCollection(

lifeCycle.subscribe(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, (entries) => {
for (const entry of entries) {
if (entry.entryType === 'resource' && !isRequestKind(entry)) {
if (entry.entryType === RumPerformanceEntryType.RESOURCE && !isRequestKind(entry)) {
lifeCycle.notify(
LifeCycleEventType.RAW_RUM_EVENT_COLLECTED,
processResourceEntry(entry, configuration, sessionManager, pageStateHistory)
Expand Down
4 changes: 2 additions & 2 deletions packages/rum-core/src/domain/resource/resourceUtils.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Duration, RelativeTime, ServerDuration } from '@datadog/browser-core'
import { SPEC_ENDPOINTS } from '@datadog/browser-core/test'
import type { RumPerformanceResourceTiming } from '../../browser/performanceCollection'
import { RumPerformanceEntryType, type RumPerformanceResourceTiming } from '../../browser/performanceCollection'
import type { RumConfiguration } from '../configuration'
import { validateAndBuildRumConfiguration } from '../configuration'
import {
Expand All @@ -17,7 +17,7 @@ function generateResourceWith(overrides: Partial<RumPerformanceResourceTiming>)
domainLookupEnd: 14 as RelativeTime,
domainLookupStart: 13 as RelativeTime,
duration: 50 as Duration,
entryType: 'resource',
entryType: RumPerformanceEntryType.RESOURCE,
fetchStart: 12 as RelativeTime,
name: 'entry',
redirectEnd: 11 as RelativeTime,
Expand Down
9 changes: 5 additions & 4 deletions packages/rum-core/src/domain/view/setupViewTest.specHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Duration, RelativeTime } from '@datadog/browser-core'
import { noopWebVitalTelemetryDebug } from '../../../test'
import type { BuildContext } from '../../../test'
import { LifeCycleEventType } from '../lifeCycle'
import { RumPerformanceEntryType } from '../../browser/performanceCollection'
import type {
RumFirstInputTiming,
RumLargestContentfulPaintTiming,
Expand Down Expand Up @@ -75,12 +76,12 @@ function spyOnViews(name?: string) {
}

export const FAKE_PAINT_ENTRY: RumPerformancePaintTiming = {
entryType: 'paint',
entryType: RumPerformanceEntryType.PAINT,
name: 'first-contentful-paint',
startTime: 123 as RelativeTime,
}
export const FAKE_LARGEST_CONTENTFUL_PAINT_ENTRY: RumLargestContentfulPaintTiming = {
entryType: 'largest-contentful-paint',
entryType: RumPerformanceEntryType.LARGEST_CONTENTFUL_PAINT,
startTime: 789 as RelativeTime,
size: 10,
}
Expand All @@ -90,12 +91,12 @@ export const FAKE_NAVIGATION_ENTRY: RumPerformanceNavigationTiming = {
domComplete: 456 as RelativeTime,
domContentLoadedEventEnd: 345 as RelativeTime,
domInteractive: 234 as RelativeTime,
entryType: 'navigation',
entryType: RumPerformanceEntryType.NAVIGATION,
loadEventEnd: 567 as RelativeTime,
}

export const FAKE_FIRST_INPUT_ENTRY: RumFirstInputTiming = {
entryType: 'first-input',
entryType: RumPerformanceEntryType.FIRST_INPUT,
processingStart: 1100 as RelativeTime,
startTime: 1000 as RelativeTime,
duration: 0 as Duration,
Expand Down
Loading

0 comments on commit bd8e66a

Please sign in to comment.