Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

⚗️ [RUM-1020] Collect core web vitals target selectors #2418

Merged
merged 21 commits into from
Sep 14, 2023
Merged
Show file tree
Hide file tree
Changes from 13 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
4 changes: 4 additions & 0 deletions packages/core/src/domain/telemetry/telemetryEvent.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,10 @@ export type TelemetryConfigurationEvent = CommonTelemetryProperties & {
* Whether it is allowed to use LocalStorage when cookies are not available
*/
allow_fallback_to_local_storage?: boolean
/**
* Whether contexts are stored in local storage
*/
store_contexts_across_pages?: boolean
/**
* Whether untrusted events are allowed
*/
Expand Down
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
77 changes: 49 additions & 28 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 Down Expand Up @@ -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 @@ -199,12 +225,15 @@ export function retrieveInitialDocumentResourceTiming(
let timing: RumPerformanceResourceTiming

const forcedAttributes = {
entryType: 'resource' as const,
entryType: RumPerformanceEntryType.RESOURCE as const,
BenoitZugmeyer marked this conversation as resolved.
Show resolved Hide resolved
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)
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💭 thought: ‏I wonder if we really need to keep filtering entries like this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting 🤔. Do you remember why it was done in the first place?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FMU, I did this in 15da132 , where I changed entries.getEntriesByType('xxx') to entries.filter(entry => entry.entryType === 'xxx') and then we iterated over this, but I don't see any good reason to do it anymore.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FMU, we still have this case to filter

if (supportPerformanceObject()) {
const performanceEntries = performance.getEntries()

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah right that's why :D Ok nevermind then. We can always change things if/when we work on my suggestion here: #2355 (comment)


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
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
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
32 changes: 18 additions & 14 deletions packages/rum-core/src/domain/view/trackViews.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ describe('initial view', () => {
clock.tick(THROTTLE_VIEW_UPDATE_PERIOD)

expect(getViewUpdateCount()).toEqual(2)
expect(getViewUpdate(1).initialViewMetrics).toEqual({
expect(getViewUpdate(1).initialViewMetrics.navigationTimings).toEqual({
firstByte: 123 as Duration,
domComplete: 456 as Duration,
domContentLoaded: 345 as Duration,
Expand Down Expand Up @@ -161,13 +161,15 @@ describe('initial view', () => {
expect(getViewUpdateCount()).toEqual(3)
expect(getViewUpdate(1).initialViewMetrics).toEqual(
jasmine.objectContaining({
firstByte: 123 as Duration,
domComplete: 456 as Duration,
domContentLoaded: 345 as Duration,
domInteractive: 234 as Duration,
firstContentfulPaint: 123 as Duration,
largestContentfulPaint: 789 as Duration,
loadEvent: 567 as Duration,
navigationTimings: {
firstByte: 123 as Duration,
domComplete: 456 as Duration,
domContentLoaded: 345 as Duration,
domInteractive: 234 as Duration,
loadEvent: 567 as Duration,
},
largestContentfulPaint: { value: 789 as Duration, targetSelector: undefined },
})
)
expect(getViewUpdate(2).initialViewMetrics).toEqual({})
Expand Down Expand Up @@ -220,13 +222,15 @@ describe('initial view', () => {
it('should set initial view metrics only on the initial view', () => {
expect(initialView.last.initialViewMetrics).toEqual(
jasmine.objectContaining({
firstByte: 123 as Duration,
domComplete: 456 as Duration,
domContentLoaded: 345 as Duration,
domInteractive: 234 as Duration,
firstContentfulPaint: 123 as Duration,
largestContentfulPaint: 789 as Duration,
loadEvent: 567 as Duration,
navigationTimings: {
firstByte: 123 as Duration,
domComplete: 456 as Duration,
domContentLoaded: 345 as Duration,
domInteractive: 234 as Duration,
loadEvent: 567 as Duration,
},
largestContentfulPaint: { value: 789 as Duration, targetSelector: undefined },
})
)
})
Expand Down Expand Up @@ -258,7 +262,7 @@ describe('initial view', () => {
const latestUpdate = getViewUpdate(getViewUpdateCount() - 1)
const firstView = getViewUpdate(0)
expect(latestUpdate.id).toBe(firstView.id)
expect(latestUpdate.initialViewMetrics.largestContentfulPaint).toEqual(
expect(latestUpdate.initialViewMetrics.largestContentfulPaint?.value).toEqual(
FAKE_LARGEST_CONTENTFUL_PAINT_ENTRY.startTime
)
})
Expand Down
Loading