From 02a5942acb809053f8bfb678c02b3bd83afa374a Mon Sep 17 00:00:00 2001 From: Aymeric Date: Tue, 5 Sep 2023 11:23:52 +0200 Subject: [PATCH 01/18] Move CWV telemetry to their dedicated metric module --- .../viewMetrics/trackCommonViewMetrics.ts | 9 ++------- .../viewMetrics/trackCumulativeLayoutShift.ts | 19 +++++++++++++++++-- .../trackFirstInputTimings.spec.ts | 18 ++++++------------ .../viewMetrics/trackFirstInputTimings.ts | 16 ++++++---------- .../viewMetrics/trackInitialViewMetrics.ts | 10 ++++------ .../trackLargestContentfulPaint.spec.ts | 14 ++++++++++---- .../trackLargestContentfulPaint.ts | 8 ++++++-- 7 files changed, 51 insertions(+), 43 deletions(-) diff --git a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackCommonViewMetrics.ts b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackCommonViewMetrics.ts index 5a113f8e2d..902befb29f 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackCommonViewMetrics.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackCommonViewMetrics.ts @@ -61,18 +61,13 @@ export function trackCommonViewMetrics( ) let stopCLSTracking: () => void - let clsAttributionCollected = false if (isLayoutShiftSupported()) { commonViewMetrics.cumulativeLayoutShift = 0 ;({ stop: stopCLSTracking } = trackCumulativeLayoutShift( lifeCycle, - (cumulativeLayoutShift, largestLayoutShiftNode, largestLayoutShiftTime) => { + webVitalTelemetryDebug, + (cumulativeLayoutShift) => { commonViewMetrics.cumulativeLayoutShift = cumulativeLayoutShift - - if (!clsAttributionCollected) { - clsAttributionCollected = true - webVitalTelemetryDebug.addWebVitalTelemetryDebug('CLS', largestLayoutShiftNode, largestLayoutShiftTime) - } scheduleViewUpdate() } )) diff --git a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackCumulativeLayoutShift.ts b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackCumulativeLayoutShift.ts index e45a511012..b9d10a3934 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackCumulativeLayoutShift.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackCumulativeLayoutShift.ts @@ -2,6 +2,7 @@ import { round, type RelativeTime, find, ONE_SECOND } from '@datadog/browser-cor import type { LifeCycle } from '../../../lifeCycle' import { LifeCycleEventType } from '../../../lifeCycle' import { supportPerformanceTimingEvent, type RumLayoutShiftTiming } from '../../../../browser/performanceCollection' +import type { WebVitalTelemetryDebug } from '../startWebVitalTelemetryDebug' /** * Track the cumulative layout shifts (CLS). @@ -22,11 +23,14 @@ import { supportPerformanceTimingEvent, type RumLayoutShiftTiming } from '../../ */ export function trackCumulativeLayoutShift( lifeCycle: LifeCycle, - callback: (layoutShift: number, largestShiftNode: Node | undefined, largestShiftTime: RelativeTime) => void + webVitalTelemetryDebug: WebVitalTelemetryDebug, + callback: (layoutShift: number) => void ) { let maxClsValue = 0 const window = slidingSessionWindow() + let clsAttributionCollected = false + const { unsubscribe: stop } = lifeCycle.subscribe(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, (entries) => { for (const entry of entries) { if (entry.entryType === 'layout-shift' && !entry.hadRecentInput) { @@ -34,7 +38,18 @@ export function trackCumulativeLayoutShift( if (window.value() > maxClsValue) { maxClsValue = window.value() - callback(round(maxClsValue, 4), window.largestLayoutShiftNode(), window.largestLayoutShiftTime()) + const cls = round(maxClsValue, 4) + + if (!clsAttributionCollected) { + clsAttributionCollected = true + webVitalTelemetryDebug.addWebVitalTelemetryDebug( + 'CLS', + window.largestLayoutShiftNode(), + window.largestLayoutShiftTime() + ) + } + + callback(cls) } } } diff --git a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackFirstInputTimings.spec.ts b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackFirstInputTimings.spec.ts index cd487cfa2f..364a91018a 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackFirstInputTimings.spec.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackFirstInputTimings.spec.ts @@ -1,4 +1,4 @@ -import type { Duration, RelativeTime } from '@datadog/browser-core' +import { noop, type Duration, type RelativeTime } from '@datadog/browser-core' import { restorePageVisibility, setPageVisibility } from '@datadog/browser-core/test' import type { TestSetupBuilder } from '../../../../../test' import { setup } from '../../../../../test' @@ -11,21 +11,16 @@ import { trackFirstInputTimings } from './trackFirstInputTimings' describe('firstInputTimings', () => { let setupBuilder: TestSetupBuilder let fitCallback: jasmine.Spy< - ({ - firstInputDelay, - firstInputTime, - }: { - firstInputDelay: number - firstInputTime: number - firstInputTarget: Node | undefined - }) => void + ({ firstInputDelay, firstInputTime }: { firstInputDelay: number; firstInputTime: number }) => void > let configuration: RumConfiguration beforeEach(() => { configuration = {} as RumConfiguration fitCallback = jasmine.createSpy() - setupBuilder = setup().beforeBuild(({ lifeCycle }) => trackFirstInputTimings(lifeCycle, configuration, fitCallback)) + setupBuilder = setup().beforeBuild(({ lifeCycle }) => + trackFirstInputTimings(lifeCycle, configuration, { addWebVitalTelemetryDebug: noop }, fitCallback) + ) resetFirstHidden() }) @@ -45,7 +40,6 @@ describe('firstInputTimings', () => { expect(fitCallback).toHaveBeenCalledWith({ firstInputDelay: 100, firstInputTime: 1000, - firstInputTarget: jasmine.any(Node), }) }) @@ -71,6 +65,6 @@ describe('firstInputTimings', () => { ]) expect(fitCallback).toHaveBeenCalledTimes(1) - expect(fitCallback).toHaveBeenCalledWith({ firstInputDelay: 0, firstInputTime: 1000, firstInputTarget: undefined }) + expect(fitCallback).toHaveBeenCalledWith({ firstInputDelay: 0, firstInputTime: 1000 }) }) }) diff --git a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackFirstInputTimings.ts b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackFirstInputTimings.ts index 72c7544794..d39e575953 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackFirstInputTimings.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackFirstInputTimings.ts @@ -4,6 +4,7 @@ import type { RumConfiguration } from '../../../configuration' import type { LifeCycle } from '../../../lifeCycle' import { LifeCycleEventType } from '../../../lifeCycle' import type { RumFirstInputTiming } from '../../../../browser/performanceCollection' +import type { WebVitalTelemetryDebug } from '../startWebVitalTelemetryDebug' import { trackFirstHidden } from './trackFirstHidden' /** @@ -18,15 +19,8 @@ import { trackFirstHidden } from './trackFirstHidden' export function trackFirstInputTimings( lifeCycle: LifeCycle, configuration: RumConfiguration, - callback: ({ - firstInputDelay, - firstInputTime, - firstInputTarget, - }: { - firstInputDelay: Duration - firstInputTime: RelativeTime - firstInputTarget: Node | undefined - }) => void + webVitalTelemetryDebug: WebVitalTelemetryDebug, + callback: ({ firstInputDelay, firstInputTime }: { firstInputDelay: Duration; firstInputTime: RelativeTime }) => void ) { const firstHidden = trackFirstHidden(configuration) @@ -38,12 +32,14 @@ export function trackFirstInputTimings( ) if (firstInputEntry) { const firstInputDelay = elapsed(firstInputEntry.startTime, firstInputEntry.processingStart) + + webVitalTelemetryDebug.addWebVitalTelemetryDebug('FID', firstInputEntry.target, firstInputEntry.startTime) + callback({ // Ensure firstInputDelay to be positive, see // https://bugs.chromium.org/p/chromium/issues/detail?id=1185815 firstInputDelay: firstInputDelay >= 0 ? firstInputDelay : (0 as Duration), firstInputTime: firstInputEntry.startTime, - firstInputTarget: firstInputEntry.target, }) } }) diff --git a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackInitialViewMetrics.ts b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackInitialViewMetrics.ts index 26be24dacd..3c0fc1f58e 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackInitialViewMetrics.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackInitialViewMetrics.ts @@ -52,10 +52,9 @@ export function trackInitialViewMetrics( const { stop: stopLCPTracking } = trackLargestContentfulPaint( lifeCycle, configuration, + webVitalTelemetryDebug, window, - (largestContentfulPaint, lcpElement) => { - webVitalTelemetryDebug.addWebVitalTelemetryDebug('LCP', lcpElement, largestContentfulPaint) - + (largestContentfulPaint) => { setMetrics({ largestContentfulPaint, }) @@ -65,9 +64,8 @@ export function trackInitialViewMetrics( const { stop: stopFIDTracking } = trackFirstInputTimings( lifeCycle, configuration, - ({ firstInputDelay, firstInputTime, firstInputTarget }) => { - webVitalTelemetryDebug.addWebVitalTelemetryDebug('FID', firstInputTarget, firstInputTime) - + webVitalTelemetryDebug, + ({ firstInputDelay, firstInputTime }) => { setMetrics({ firstInputDelay, firstInputTime, diff --git a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackLargestContentfulPaint.spec.ts b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackLargestContentfulPaint.spec.ts index e2e70274f9..790c20113e 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackLargestContentfulPaint.spec.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackLargestContentfulPaint.spec.ts @@ -1,5 +1,5 @@ import type { RelativeTime } from '@datadog/browser-core' -import { DOM_EVENT } from '@datadog/browser-core' +import { DOM_EVENT, noop } from '@datadog/browser-core' import { restorePageVisibility, setPageVisibility, createNewEvent } from '@datadog/browser-core/test' import type { RumLargestContentfulPaintTiming } from '../../../../browser/performanceCollection' import type { TestSetupBuilder } from '../../../../../test' @@ -11,7 +11,7 @@ import { LCP_MAXIMUM_DELAY, trackLargestContentfulPaint } from './trackLargestCo describe('trackLargestContentfulPaint', () => { let setupBuilder: TestSetupBuilder - let lcpCallback: jasmine.Spy<(value: RelativeTime, lcpElement: Element | undefined) => void> + let lcpCallback: jasmine.Spy<(value: RelativeTime) => void> let eventTarget: Window let configuration: RumConfiguration @@ -20,7 +20,13 @@ describe('trackLargestContentfulPaint', () => { lcpCallback = jasmine.createSpy() eventTarget = document.createElement('div') as unknown as Window setupBuilder = setup().beforeBuild(({ lifeCycle }) => - trackLargestContentfulPaint(lifeCycle, configuration, eventTarget, lcpCallback) + trackLargestContentfulPaint( + lifeCycle, + configuration, + { addWebVitalTelemetryDebug: noop }, + eventTarget, + lcpCallback + ) ) resetFirstHidden() }) @@ -36,7 +42,7 @@ describe('trackLargestContentfulPaint', () => { lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [FAKE_LARGEST_CONTENTFUL_PAINT_ENTRY]) expect(lcpCallback).toHaveBeenCalledTimes(1 as RelativeTime) - expect(lcpCallback).toHaveBeenCalledWith(789 as RelativeTime, jasmine.any(Element)) + expect(lcpCallback).toHaveBeenCalledWith(789 as RelativeTime) }) it('should be discarded if it is reported after a user interaction', () => { diff --git a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackLargestContentfulPaint.ts b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackLargestContentfulPaint.ts index 24d6020556..56a7804ff1 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackLargestContentfulPaint.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackLargestContentfulPaint.ts @@ -3,6 +3,7 @@ import { DOM_EVENT, ONE_MINUTE, addEventListeners, findLast } from '@datadog/bro import { LifeCycleEventType, type LifeCycle } from '../../../lifeCycle' import type { RumConfiguration } from '../../../configuration' import type { RumLargestContentfulPaintTiming } from '../../../../browser/performanceCollection' +import type { WebVitalTelemetryDebug } from '../startWebVitalTelemetryDebug' import { trackFirstHidden } from './trackFirstHidden' // Discard LCP timings above a certain delay to avoid incorrect data @@ -18,8 +19,9 @@ export const LCP_MAXIMUM_DELAY = 10 * ONE_MINUTE export function trackLargestContentfulPaint( lifeCycle: LifeCycle, configuration: RumConfiguration, + webVitalTelemetryDebug: WebVitalTelemetryDebug, eventTarget: Window, - callback: (lcpTiming: RelativeTime, lcpElement?: Element) => void + callback: (lcpTiming: RelativeTime) => void ) { const firstHidden = trackFirstHidden(configuration) @@ -49,7 +51,9 @@ export function trackLargestContentfulPaint( entry.startTime < LCP_MAXIMUM_DELAY ) if (lcpEntry) { - callback(lcpEntry.startTime, lcpEntry.element) + webVitalTelemetryDebug.addWebVitalTelemetryDebug('LCP', lcpEntry.element, lcpEntry.startTime) + + callback(lcpEntry.startTime) } } ) From 4609904ba42a1a300c0d6238783fbcf39ba0aad4 Mon Sep 17 00:00:00 2001 From: Aymeric Date: Tue, 5 Sep 2023 15:32:48 +0200 Subject: [PATCH 02/18] Collect LCP target selector --- .../getSelectorFromElement.spec.ts | 4 +- .../action => }/getSelectorFromElement.ts | 2 +- .../action/trackClickActions.ts | 2 +- .../view/viewCollection.ts | 1 + .../viewMetrics/trackInitialViewMetrics.ts | 4 +- .../trackLargestContentfulPaint.spec.ts | 69 ++++++++++++------- .../trackLargestContentfulPaint.ts | 22 ++++-- packages/rum-core/src/index.ts | 2 +- packages/rum-core/src/rawRumEvent.types.ts | 1 + 9 files changed, 73 insertions(+), 34 deletions(-) rename packages/rum-core/src/domain/{rumEventsCollection/action => }/getSelectorFromElement.spec.ts (98%) rename packages/rum-core/src/domain/{rumEventsCollection/action => }/getSelectorFromElement.ts (98%) diff --git a/packages/rum-core/src/domain/rumEventsCollection/action/getSelectorFromElement.spec.ts b/packages/rum-core/src/domain/getSelectorFromElement.spec.ts similarity index 98% rename from packages/rum-core/src/domain/rumEventsCollection/action/getSelectorFromElement.spec.ts rename to packages/rum-core/src/domain/getSelectorFromElement.spec.ts index c1e2f7d5ba..44f458d0ea 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/action/getSelectorFromElement.spec.ts +++ b/packages/rum-core/src/domain/getSelectorFromElement.spec.ts @@ -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', () => { diff --git a/packages/rum-core/src/domain/rumEventsCollection/action/getSelectorFromElement.ts b/packages/rum-core/src/domain/getSelectorFromElement.ts similarity index 98% rename from packages/rum-core/src/domain/rumEventsCollection/action/getSelectorFromElement.ts rename to packages/rum-core/src/domain/getSelectorFromElement.ts index 653747011e..fbf63c369e 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/action/getSelectorFromElement.ts +++ b/packages/rum-core/src/domain/getSelectorFromElement.ts @@ -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 './rumEventsCollection/action/getActionNameFromElement' /** * Stable attributes are attributes that are commonly used to identify parts of a UI (ex: diff --git a/packages/rum-core/src/domain/rumEventsCollection/action/trackClickActions.ts b/packages/rum-core/src/domain/rumEventsCollection/action/trackClickActions.ts index fab6602f00..ecd47374cd 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/action/trackClickActions.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/action/trackClickActions.ts @@ -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' diff --git a/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.ts b/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.ts index 24c29bae51..a63b9f6625 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.ts @@ -94,6 +94,7 @@ function processViewUpdate( is_active: view.isActive, name: view.name, largest_contentful_paint: toServerDuration(view.initialViewMetrics.largestContentfulPaint), + largest_contentful_paint_target_selector: view.initialViewMetrics.largestContentfulPaintTargetSelector, load_event: toServerDuration(view.initialViewMetrics.loadEvent), loading_time: discardNegativeDuration(toServerDuration(view.commonViewMetrics.loadingTime)), loading_type: view.loadingType, diff --git a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackInitialViewMetrics.ts b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackInitialViewMetrics.ts index 3c0fc1f58e..0b646c1ff4 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackInitialViewMetrics.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackInitialViewMetrics.ts @@ -24,6 +24,7 @@ export interface InitialViewMetrics { domComplete?: Duration loadEvent?: Duration largestContentfulPaint?: Duration + largestContentfulPaintTargetSelector?: string firstInputDelay?: Duration firstInputTime?: Duration } @@ -54,9 +55,10 @@ export function trackInitialViewMetrics( configuration, webVitalTelemetryDebug, window, - (largestContentfulPaint) => { + (largestContentfulPaint, largestContentfulPaintTargetSelector) => { setMetrics({ largestContentfulPaint, + largestContentfulPaintTargetSelector, }) } ) diff --git a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackLargestContentfulPaint.spec.ts b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackLargestContentfulPaint.spec.ts index 790c20113e..05748f4d14 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackLargestContentfulPaint.spec.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackLargestContentfulPaint.spec.ts @@ -1,9 +1,16 @@ import type { RelativeTime } from '@datadog/browser-core' -import { DOM_EVENT, noop } from '@datadog/browser-core' +import { + DOM_EVENT, + ExperimentalFeature, + addExperimentalFeatures, + noop, + resetExperimentalFeatures, +} from '@datadog/browser-core' import { restorePageVisibility, setPageVisibility, createNewEvent } from '@datadog/browser-core/test' import type { RumLargestContentfulPaintTiming } from '../../../../browser/performanceCollection' import type { TestSetupBuilder } from '../../../../../test' import { setup } from '../../../../../test' +import type { LifeCycle } from '../../../lifeCycle' import { LifeCycleEventType } from '../../../lifeCycle' import type { RumConfiguration } from '../../../configuration' import { resetFirstHidden } from './trackFirstHidden' @@ -11,20 +18,36 @@ import { LCP_MAXIMUM_DELAY, trackLargestContentfulPaint } from './trackLargestCo describe('trackLargestContentfulPaint', () => { let setupBuilder: TestSetupBuilder - let lcpCallback: jasmine.Spy<(value: RelativeTime) => void> - let eventTarget: Window + let lcpCallback: jasmine.Spy<(value: RelativeTime, targetSelector?: string) => void> let configuration: RumConfiguration + let target: HTMLImageElement + + function newLargestContentfulPaint(lifeCycle: LifeCycle, overrides?: Partial) { + lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [ + { + entryType: 'largest-contentful-paint', + startTime: 789 as RelativeTime, + size: 10, + element: target, + ...overrides, + }, + ]) + } beforeEach(() => { configuration = {} as RumConfiguration lcpCallback = jasmine.createSpy() - eventTarget = document.createElement('div') as unknown as Window + + target = document.createElement('img') + target.setAttribute('id', 'lcp-target-element') + document.body.appendChild(target) + setupBuilder = setup().beforeBuild(({ lifeCycle }) => trackLargestContentfulPaint( lifeCycle, configuration, { addWebVitalTelemetryDebug: noop }, - eventTarget, + target as unknown as Window, lcpCallback ) ) @@ -35,22 +58,33 @@ describe('trackLargestContentfulPaint', () => { setupBuilder.cleanup() restorePageVisibility() resetFirstHidden() + target.remove() + resetExperimentalFeatures() }) it('should provide the largest contentful paint timing', () => { const { lifeCycle } = setupBuilder.build() - lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [FAKE_LARGEST_CONTENTFUL_PAINT_ENTRY]) + newLargestContentfulPaint(lifeCycle) + expect(lcpCallback).toHaveBeenCalledTimes(1 as RelativeTime) + expect(lcpCallback).toHaveBeenCalledWith(789 as RelativeTime, undefined) + }) + + it('should provide the largest contentful paint target selector if FF enabled', () => { + addExperimentalFeatures([ExperimentalFeature.WEB_VITALS_ATTRIBUTION]) + const { lifeCycle } = setupBuilder.build() + + newLargestContentfulPaint(lifeCycle) expect(lcpCallback).toHaveBeenCalledTimes(1 as RelativeTime) - expect(lcpCallback).toHaveBeenCalledWith(789 as RelativeTime) + expect(lcpCallback).toHaveBeenCalledWith(789 as RelativeTime, '#lcp-target-element') }) it('should be discarded if it is reported after a user interaction', () => { const { lifeCycle } = setupBuilder.build() - eventTarget.dispatchEvent(createNewEvent(DOM_EVENT.KEY_DOWN, { timeStamp: 1 })) + target.dispatchEvent(createNewEvent(DOM_EVENT.KEY_DOWN, { timeStamp: 1 })) - lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [FAKE_LARGEST_CONTENTFUL_PAINT_ENTRY]) + newLargestContentfulPaint(lifeCycle) expect(lcpCallback).not.toHaveBeenCalled() }) @@ -58,27 +92,14 @@ describe('trackLargestContentfulPaint', () => { setPageVisibility('hidden') const { lifeCycle } = setupBuilder.build() - lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [FAKE_LARGEST_CONTENTFUL_PAINT_ENTRY]) - + newLargestContentfulPaint(lifeCycle) expect(lcpCallback).not.toHaveBeenCalled() }) it('should be discarded if it is reported after a long time', () => { const { lifeCycle } = setupBuilder.build() - lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [ - { - ...FAKE_LARGEST_CONTENTFUL_PAINT_ENTRY, - startTime: LCP_MAXIMUM_DELAY as RelativeTime, - }, - ]) + newLargestContentfulPaint(lifeCycle, { startTime: LCP_MAXIMUM_DELAY as RelativeTime }) expect(lcpCallback).not.toHaveBeenCalled() }) }) - -const FAKE_LARGEST_CONTENTFUL_PAINT_ENTRY: RumLargestContentfulPaintTiming = { - entryType: 'largest-contentful-paint', - startTime: 789 as RelativeTime, - size: 10, - element: document.createElement('div'), -} diff --git a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackLargestContentfulPaint.ts b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackLargestContentfulPaint.ts index 56a7804ff1..fad368780b 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackLargestContentfulPaint.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackLargestContentfulPaint.ts @@ -1,9 +1,17 @@ import type { RelativeTime } from '@datadog/browser-core' -import { DOM_EVENT, ONE_MINUTE, addEventListeners, findLast } from '@datadog/browser-core' +import { + DOM_EVENT, + ExperimentalFeature, + ONE_MINUTE, + addEventListeners, + findLast, + isExperimentalFeatureEnabled, +} from '@datadog/browser-core' import { LifeCycleEventType, type LifeCycle } from '../../../lifeCycle' import type { RumConfiguration } from '../../../configuration' import type { RumLargestContentfulPaintTiming } from '../../../../browser/performanceCollection' import type { WebVitalTelemetryDebug } from '../startWebVitalTelemetryDebug' +import { getSelectorFromElement } from '../../../getSelectorFromElement' import { trackFirstHidden } from './trackFirstHidden' // Discard LCP timings above a certain delay to avoid incorrect data @@ -21,7 +29,7 @@ export function trackLargestContentfulPaint( configuration: RumConfiguration, webVitalTelemetryDebug: WebVitalTelemetryDebug, eventTarget: Window, - callback: (lcpTiming: RelativeTime) => void + callback: (lcpTiming: RelativeTime, lcpTargetSelector?: string) => void ) { const firstHidden = trackFirstHidden(configuration) @@ -50,10 +58,16 @@ export function trackLargestContentfulPaint( entry.startTime < firstHidden.timeStamp && entry.startTime < LCP_MAXIMUM_DELAY ) + if (lcpEntry) { - webVitalTelemetryDebug.addWebVitalTelemetryDebug('LCP', lcpEntry.element, lcpEntry.startTime) + let lcpTargetSelector + if (isExperimentalFeatureEnabled(ExperimentalFeature.WEB_VITALS_ATTRIBUTION) && lcpEntry.element) { + lcpTargetSelector = getSelectorFromElement(lcpEntry.element, configuration.actionNameAttribute) + } - callback(lcpEntry.startTime) + callback(lcpEntry.startTime, lcpTargetSelector) + + webVitalTelemetryDebug.addWebVitalTelemetryDebug('LCP', lcpEntry.element, lcpEntry.startTime) } } ) diff --git a/packages/rum-core/src/index.ts b/packages/rum-core/src/index.ts index 2680370340..3cc4c016f0 100644 --- a/packages/rum-core/src/index.ts +++ b/packages/rum-core/src/index.ts @@ -29,6 +29,6 @@ export { initViewportObservable, getViewportDimension } from './browser/viewport export { getScrollX, getScrollY } from './browser/scroll' export { RumInitConfiguration, RumConfiguration } from './domain/configuration' export { DEFAULT_PROGRAMMATIC_ACTION_NAME_ATTRIBUTE } from './domain/rumEventsCollection/action/getActionNameFromElement' -export { STABLE_ATTRIBUTES } from './domain/rumEventsCollection/action/getSelectorFromElement' +export { STABLE_ATTRIBUTES } from './domain/getSelectorFromElement' export * from './browser/htmlDomUtils' export { getSessionReplayUrl } from './domain/getSessionReplayUrl' diff --git a/packages/rum-core/src/rawRumEvent.types.ts b/packages/rum-core/src/rawRumEvent.types.ts index ff6ba3856b..23223cbded 100644 --- a/packages/rum-core/src/rawRumEvent.types.ts +++ b/packages/rum-core/src/rawRumEvent.types.ts @@ -89,6 +89,7 @@ export interface RawRumViewEvent { [key: string]: ServerDuration } largest_contentful_paint?: ServerDuration + largest_contentful_paint_target_selector?: string dom_interactive?: ServerDuration dom_content_loaded?: ServerDuration dom_complete?: ServerDuration From b4b28474a75c1c98296673f994f623b7cf326f0c Mon Sep 17 00:00:00 2001 From: Aymeric Date: Tue, 5 Sep 2023 16:03:05 +0200 Subject: [PATCH 03/18] Collect FID target selector --- .../view/viewCollection.spec.ts | 2 + .../view/viewCollection.ts | 1 + .../trackFirstInputTimings.spec.ts | 86 +++++++++++++++---- .../viewMetrics/trackFirstInputTimings.ts | 26 +++++- .../trackInitialViewMetrics.spec.ts | 1 + .../viewMetrics/trackInitialViewMetrics.ts | 8 +- packages/rum-core/src/rawRumEvent.types.ts | 1 + 7 files changed, 101 insertions(+), 24 deletions(-) diff --git a/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.spec.ts b/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.spec.ts index 3220a62f06..2b97d89609 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.spec.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.spec.ts @@ -135,10 +135,12 @@ describe('viewCollection', () => { first_contentful_paint: (10 * 1e6) as ServerDuration, first_input_delay: (12 * 1e6) as ServerDuration, first_input_time: (10 * 1e6) as ServerDuration, + first_input_target_selector: undefined, interaction_to_next_paint: (10 * 1e6) as ServerDuration, is_active: false, name: undefined, largest_contentful_paint: (10 * 1e6) as ServerDuration, + largest_contentful_paint_target_selector: undefined, load_event: (10 * 1e6) as ServerDuration, loading_time: (20 * 1e6) as ServerDuration, loading_type: ViewLoadingType.INITIAL_LOAD, diff --git a/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.ts b/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.ts index a63b9f6625..5de0c40adf 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.ts @@ -90,6 +90,7 @@ function processViewUpdate( first_contentful_paint: toServerDuration(view.initialViewMetrics.firstContentfulPaint), first_input_delay: toServerDuration(view.initialViewMetrics.firstInputDelay), first_input_time: toServerDuration(view.initialViewMetrics.firstInputTime), + first_input_target_selector: view.initialViewMetrics.firstInputTargetSelector, interaction_to_next_paint: toServerDuration(view.commonViewMetrics.interactionToNextPaint), is_active: view.isActive, name: view.name, diff --git a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackFirstInputTimings.spec.ts b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackFirstInputTimings.spec.ts index 364a91018a..f45ad6cc59 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackFirstInputTimings.spec.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackFirstInputTimings.spec.ts @@ -1,23 +1,58 @@ -import { noop, type Duration, type RelativeTime } from '@datadog/browser-core' +import { + noop, + type Duration, + type RelativeTime, + resetExperimentalFeatures, + ExperimentalFeature, + addExperimentalFeatures, +} from '@datadog/browser-core' import { restorePageVisibility, setPageVisibility } from '@datadog/browser-core/test' +import type { RumFirstInputTiming } from '../../../../browser/performanceCollection' import type { TestSetupBuilder } from '../../../../../test' import { setup } from '../../../../../test' +import type { LifeCycle } from '../../../lifeCycle' import { LifeCycleEventType } from '../../../lifeCycle' import type { RumConfiguration } from '../../../configuration' -import { FAKE_FIRST_INPUT_ENTRY } from '../setupViewTest.specHelper' import { resetFirstHidden } from './trackFirstHidden' import { trackFirstInputTimings } from './trackFirstInputTimings' describe('firstInputTimings', () => { let setupBuilder: TestSetupBuilder let fitCallback: jasmine.Spy< - ({ firstInputDelay, firstInputTime }: { firstInputDelay: number; firstInputTime: number }) => void + ({ + firstInputDelay, + firstInputTime, + firstInputTargetSelector, + }: { + firstInputDelay: number + firstInputTime: number + firstInputTargetSelector?: string + }) => void > let configuration: RumConfiguration + let target: HTMLButtonElement + + function newFirstInput(lifeCycle: LifeCycle, overrides?: Partial) { + lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [ + { + entryType: 'first-input', + processingStart: 1100 as RelativeTime, + startTime: 1000 as RelativeTime, + duration: 0 as Duration, + target, + ...overrides, + }, + ]) + } beforeEach(() => { configuration = {} as RumConfiguration fitCallback = jasmine.createSpy() + + target = document.createElement('button') + target.setAttribute('id', 'fid-target-element') + document.body.appendChild(target) + setupBuilder = setup().beforeBuild(({ lifeCycle }) => trackFirstInputTimings(lifeCycle, configuration, { addWebVitalTelemetryDebug: noop }, fitCallback) ) @@ -26,28 +61,44 @@ describe('firstInputTimings', () => { afterEach(() => { setupBuilder.cleanup() + target.remove() restorePageVisibility() resetFirstHidden() + resetExperimentalFeatures() }) it('should provide the first input timings', () => { const { lifeCycle } = setupBuilder.build() - lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [ - { ...FAKE_FIRST_INPUT_ENTRY, target: document.createElement('button') }, - ]) + newFirstInput(lifeCycle) + expect(fitCallback).toHaveBeenCalledTimes(1) expect(fitCallback).toHaveBeenCalledWith({ firstInputDelay: 100, firstInputTime: 1000, + firstInputTargetSelector: undefined, }) }) + it('should provide the first input target selector if FF enabled', () => { + addExperimentalFeatures([ExperimentalFeature.WEB_VITALS_ATTRIBUTION]) + const { lifeCycle } = setupBuilder.build() + + newFirstInput(lifeCycle) + + expect(fitCallback).toHaveBeenCalledTimes(1) + expect(fitCallback).toHaveBeenCalledWith( + jasmine.objectContaining({ + firstInputTargetSelector: '#fid-target-element', + }) + ) + }) + it('should be discarded if the page is hidden', () => { setPageVisibility('hidden') const { lifeCycle } = setupBuilder.build() - lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [FAKE_FIRST_INPUT_ENTRY]) + newFirstInput(lifeCycle) expect(fitCallback).not.toHaveBeenCalled() }) @@ -55,16 +106,19 @@ describe('firstInputTimings', () => { it('should be adjusted to 0 if the computed value would be negative due to browser timings imprecisions', () => { const { lifeCycle } = setupBuilder.build() - lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [ - { - entryType: 'first-input' as const, - processingStart: 900 as RelativeTime, - startTime: 1000 as RelativeTime, - duration: 0 as Duration, - }, - ]) + newFirstInput(lifeCycle, { + entryType: 'first-input' as const, + processingStart: 900 as RelativeTime, + startTime: 1000 as RelativeTime, + duration: 0 as Duration, + }) expect(fitCallback).toHaveBeenCalledTimes(1) - expect(fitCallback).toHaveBeenCalledWith({ firstInputDelay: 0, firstInputTime: 1000 }) + expect(fitCallback).toHaveBeenCalledWith( + jasmine.objectContaining({ + firstInputDelay: 0, + firstInputTime: 1000, + }) + ) }) }) diff --git a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackFirstInputTimings.ts b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackFirstInputTimings.ts index d39e575953..c2dca0ba98 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackFirstInputTimings.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackFirstInputTimings.ts @@ -1,10 +1,12 @@ import type { Duration, RelativeTime } from '@datadog/browser-core' -import { elapsed, find } from '@datadog/browser-core' +import { elapsed, find, ExperimentalFeature, isExperimentalFeatureEnabled } from '@datadog/browser-core' +import { isElementNode } from '../../../../browser/htmlDomUtils' import type { RumConfiguration } from '../../../configuration' import type { LifeCycle } from '../../../lifeCycle' import { LifeCycleEventType } from '../../../lifeCycle' import type { RumFirstInputTiming } from '../../../../browser/performanceCollection' import type { WebVitalTelemetryDebug } from '../startWebVitalTelemetryDebug' +import { getSelectorFromElement } from '../../../getSelectorFromElement' import { trackFirstHidden } from './trackFirstHidden' /** @@ -20,7 +22,15 @@ export function trackFirstInputTimings( lifeCycle: LifeCycle, configuration: RumConfiguration, webVitalTelemetryDebug: WebVitalTelemetryDebug, - callback: ({ firstInputDelay, firstInputTime }: { firstInputDelay: Duration; firstInputTime: RelativeTime }) => void + callback: ({ + firstInputDelay, + firstInputTime, + firstInputTargetSelector, + }: { + firstInputDelay: Duration + firstInputTime: RelativeTime + firstInputTargetSelector?: string + }) => void ) { const firstHidden = trackFirstHidden(configuration) @@ -32,15 +42,25 @@ export function trackFirstInputTimings( ) if (firstInputEntry) { const firstInputDelay = elapsed(firstInputEntry.startTime, firstInputEntry.processingStart) + let firstInputTargetSelector - webVitalTelemetryDebug.addWebVitalTelemetryDebug('FID', firstInputEntry.target, firstInputEntry.startTime) + if ( + isExperimentalFeatureEnabled(ExperimentalFeature.WEB_VITALS_ATTRIBUTION) && + firstInputEntry.target && + isElementNode(firstInputEntry.target) + ) { + firstInputTargetSelector = getSelectorFromElement(firstInputEntry.target, configuration.actionNameAttribute) + } callback({ // Ensure firstInputDelay to be positive, see // https://bugs.chromium.org/p/chromium/issues/detail?id=1185815 firstInputDelay: firstInputDelay >= 0 ? firstInputDelay : (0 as Duration), firstInputTime: firstInputEntry.startTime, + firstInputTargetSelector, }) + + webVitalTelemetryDebug.addWebVitalTelemetryDebug('FID', firstInputEntry.target, firstInputEntry.startTime) } }) diff --git a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackInitialViewMetrics.spec.ts b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackInitialViewMetrics.spec.ts index 49bfc69456..6261996781 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackInitialViewMetrics.spec.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackInitialViewMetrics.spec.ts @@ -52,6 +52,7 @@ describe('trackInitialViewMetrics', () => { firstContentfulPaint: 123 as Duration, firstInputDelay: 100 as Duration, firstInputTime: 1000 as Duration, + firstInputTargetSelector: undefined, loadEvent: 567 as Duration, }) }) diff --git a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackInitialViewMetrics.ts b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackInitialViewMetrics.ts index 0b646c1ff4..cf73fa5c91 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackInitialViewMetrics.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackInitialViewMetrics.ts @@ -27,6 +27,7 @@ export interface InitialViewMetrics { largestContentfulPaintTargetSelector?: string firstInputDelay?: Duration firstInputTime?: Duration + firstInputTargetSelector?: string } export function trackInitialViewMetrics( @@ -67,11 +68,8 @@ export function trackInitialViewMetrics( lifeCycle, configuration, webVitalTelemetryDebug, - ({ firstInputDelay, firstInputTime }) => { - setMetrics({ - firstInputDelay, - firstInputTime, - }) + (firstInputTimings) => { + setMetrics(firstInputTimings) } ) diff --git a/packages/rum-core/src/rawRumEvent.types.ts b/packages/rum-core/src/rawRumEvent.types.ts index 23223cbded..fca11e4b55 100644 --- a/packages/rum-core/src/rawRumEvent.types.ts +++ b/packages/rum-core/src/rawRumEvent.types.ts @@ -83,6 +83,7 @@ export interface RawRumViewEvent { first_contentful_paint?: ServerDuration first_input_delay?: ServerDuration first_input_time?: ServerDuration + first_input_target_selector?: string interaction_to_next_paint?: ServerDuration cumulative_layout_shift?: number custom_timings?: { From 560255d4df73ae31ed66114e61f4448af84db0a0 Mon Sep 17 00:00:00 2001 From: Aymeric Date: Wed, 6 Sep 2023 09:45:17 +0200 Subject: [PATCH 04/18] Collect INP target selector --- .../core/src/tools/experimentalFeatures.ts | 1 + .../src/browser/performanceCollection.ts | 1 + .../view/viewCollection.spec.ts | 1 + .../view/viewCollection.ts | 1 + .../viewMetrics/trackCommonViewMetrics.ts | 11 +++- .../trackInteractionToNextPaint.spec.ts | 64 +++++++++++++------ .../trackInteractionToNextPaint.ts | 43 ++++++++++--- packages/rum-core/src/rawRumEvent.types.ts | 1 + 8 files changed, 90 insertions(+), 33 deletions(-) diff --git a/packages/core/src/tools/experimentalFeatures.ts b/packages/core/src/tools/experimentalFeatures.ts index 9306d19c9a..eb024a6142 100644 --- a/packages/core/src/tools/experimentalFeatures.ts +++ b/packages/core/src/tools/experimentalFeatures.ts @@ -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', } diff --git a/packages/rum-core/src/browser/performanceCollection.ts b/packages/rum-core/src/browser/performanceCollection.ts index adc4a2ce5d..aedca556e3 100644 --- a/packages/rum-core/src/browser/performanceCollection.ts +++ b/packages/rum-core/src/browser/performanceCollection.ts @@ -96,6 +96,7 @@ export interface RumPerformanceEventTiming { startTime: RelativeTime duration: Duration interactionId?: number + target?: Node } export interface RumLayoutShiftTiming { diff --git a/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.spec.ts b/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.spec.ts index 2b97d89609..f5227a8247 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.spec.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.spec.ts @@ -137,6 +137,7 @@ describe('viewCollection', () => { first_input_time: (10 * 1e6) as ServerDuration, first_input_target_selector: undefined, interaction_to_next_paint: (10 * 1e6) as ServerDuration, + interaction_to_next_paint_target_selector: undefined, is_active: false, name: undefined, largest_contentful_paint: (10 * 1e6) as ServerDuration, diff --git a/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.ts b/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.ts index 5de0c40adf..8e50d53ff1 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.ts @@ -92,6 +92,7 @@ function processViewUpdate( first_input_time: toServerDuration(view.initialViewMetrics.firstInputTime), first_input_target_selector: view.initialViewMetrics.firstInputTargetSelector, interaction_to_next_paint: toServerDuration(view.commonViewMetrics.interactionToNextPaint), + interaction_to_next_paint_target_selector: view.commonViewMetrics.interactionToNextPaintTargetSelector, is_active: view.isActive, name: view.name, largest_contentful_paint: toServerDuration(view.initialViewMetrics.largestContentfulPaint), diff --git a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackCommonViewMetrics.ts b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackCommonViewMetrics.ts index 902befb29f..62788efc78 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackCommonViewMetrics.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackCommonViewMetrics.ts @@ -14,6 +14,7 @@ export interface CommonViewMetrics { loadingTime?: Duration cumulativeLayoutShift?: number interactionToNextPaint?: Duration + interactionToNextPaintTargetSelector?: string scroll?: ScrollMetrics } @@ -75,7 +76,11 @@ export function trackCommonViewMetrics( stopCLSTracking = noop } - const { stop: stopINPTracking, getInteractionToNextPaint } = trackInteractionToNextPaint(loadingType, lifeCycle) + const { stop: stopINPTracking, getInteractionToNextPaint } = trackInteractionToNextPaint( + configuration, + loadingType, + lifeCycle + ) return { stop: () => { @@ -86,7 +91,9 @@ export function trackCommonViewMetrics( }, setLoadEvent, getCommonViewMetrics: () => { - commonViewMetrics.interactionToNextPaint = getInteractionToNextPaint() + const { interactionToNextPaint, interactionToNextPaintTargetSelector } = getInteractionToNextPaint() || {} + commonViewMetrics.interactionToNextPaint = interactionToNextPaint + commonViewMetrics.interactionToNextPaintTargetSelector = interactionToNextPaintTargetSelector return commonViewMetrics }, } diff --git a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackInteractionToNextPaint.spec.ts b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackInteractionToNextPaint.spec.ts index acde12e5d6..07e3852201 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackInteractionToNextPaint.spec.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackInteractionToNextPaint.spec.ts @@ -24,27 +24,22 @@ import { describe('trackInteractionToNextPaint', () => { let setupBuilder: TestSetupBuilder let interactionCountStub: ReturnType - let getInteractionToNextPaint: () => Duration | undefined - - function newInteraction( - lifeCycle: LifeCycle, - { - interactionId, - duration = 40 as Duration, - entryType = 'event', - }: Partial - ) { - if (interactionId) { + let getInteractionToNextPaint: ReturnType['getInteractionToNextPaint'] + let target: HTMLButtonElement + + function newInteraction(lifeCycle: LifeCycle, overrides: Partial) { + if (overrides.interactionId) { interactionCountStub.incrementInteractionCount() } lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [ { - entryType, + entryType: 'event', processingStart: relativeNow(), startTime: relativeNow(), - duration, - interactionId, - }, + duration: 40 as Duration, + target, + ...overrides, + } as RumPerformanceEventTiming, ]) } @@ -54,12 +49,21 @@ describe('trackInteractionToNextPaint', () => { } interactionCountStub = subInteractionCount() - setupBuilder = setup().beforeBuild(({ lifeCycle }) => { - ;({ getInteractionToNextPaint } = trackInteractionToNextPaint(ViewLoadingType.INITIAL_LOAD, lifeCycle)) + target = document.createElement('button') + target.setAttribute('id', 'inp-target-element') + document.body.appendChild(target) + + setupBuilder = setup().beforeBuild(({ lifeCycle, configuration }) => { + ;({ getInteractionToNextPaint } = trackInteractionToNextPaint( + configuration, + ViewLoadingType.INITIAL_LOAD, + lifeCycle + )) }) }) afterEach(() => { + target.remove() interactionCountStub.clear() }) @@ -93,13 +97,16 @@ describe('trackInteractionToNextPaint', () => { interactionId: index, }) } - expect(getInteractionToNextPaint()).toEqual(98 as Duration) + expect(getInteractionToNextPaint()).toEqual({ + interactionToNextPaint: 98 as Duration, + interactionToNextPaintTargetSelector: undefined, + }) }) it('should return 0 when an interaction happened without generating a performance event (interaction duration below 40ms)', () => { setupBuilder.build() interactionCountStub.setInteractionCount(1 as Duration) // assumes an interaction happened but no PERFORMANCE_ENTRIES_COLLECTED have been triggered - expect(getInteractionToNextPaint()).toEqual(0 as Duration) + expect(getInteractionToNextPaint()).toEqual({ interactionToNextPaint: 0 as Duration }) }) it('should take first-input entry into account', () => { @@ -108,7 +115,10 @@ describe('trackInteractionToNextPaint', () => { interactionId: 1, entryType: 'first-input', }) - expect(getInteractionToNextPaint()).toEqual(40 as Duration) + expect(getInteractionToNextPaint()).toEqual({ + interactionToNextPaint: 40 as Duration, + interactionToNextPaintTargetSelector: undefined, + }) }) it('should replace the entry in the list of worst interactions when an entry with the same interactionId exist', () => { @@ -121,7 +131,19 @@ describe('trackInteractionToNextPaint', () => { }) } // the p98 return 100 which shows that the entry has been updated - expect(getInteractionToNextPaint()).toEqual(100 as Duration) + expect(getInteractionToNextPaint()).toEqual({ + interactionToNextPaint: 100 as Duration, + interactionToNextPaintTargetSelector: undefined, + }) + }) + + it('should return the target selector when FF web_vital_attribution is enabled', () => { + addExperimentalFeatures([ExperimentalFeature.WEB_VITALS_ATTRIBUTION]) + const { lifeCycle } = setupBuilder.build() + + newInteraction(lifeCycle, { interactionId: 2 }) + + expect(getInteractionToNextPaint()?.interactionToNextPaintTargetSelector).toEqual('#inp-target-element') }) }) diff --git a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackInteractionToNextPaint.ts b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackInteractionToNextPaint.ts index 4196ba9f99..c2386ee557 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackInteractionToNextPaint.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackInteractionToNextPaint.ts @@ -1,9 +1,12 @@ import { noop, isExperimentalFeatureEnabled, ExperimentalFeature } from '@datadog/browser-core' import type { Duration } from '@datadog/browser-core' +import { isElementNode } from '../../../../browser/htmlDomUtils' import { supportPerformanceTimingEvent } from '../../../../browser/performanceCollection' import type { RumFirstInputTiming, RumPerformanceEventTiming } from '../../../../browser/performanceCollection' import { LifeCycleEventType, type LifeCycle } from '../../../lifeCycle' import { ViewLoadingType } from '../../../../rawRumEvent.types' +import { getSelectorFromElement } from '../../../getSelectorFromElement' +import type { RumConfiguration } from '../../../configuration' import { getInteractionCount, initInteractionCountPolyfill } from './interactionCountPolyfill' // Arbitrary value to prevent unnecessary memory usage on views with lots of interactions. @@ -15,7 +18,11 @@ const MAX_INTERACTION_ENTRIES = 10 * Documentation: https://web.dev/inp/ * Reference implementation: https://github.com/GoogleChrome/web-vitals/blob/main/src/onINP.ts */ -export function trackInteractionToNextPaint(viewLoadingType: ViewLoadingType, lifeCycle: LifeCycle) { +export function trackInteractionToNextPaint( + configuration: RumConfiguration, + viewLoadingType: ViewLoadingType, + lifeCycle: LifeCycle +) { if ( !isInteractionToNextPaintSupported() || !isExperimentalFeatureEnabled(ExperimentalFeature.INTERACTION_TO_NEXT_PAINT) @@ -28,7 +35,8 @@ export function trackInteractionToNextPaint(viewLoadingType: ViewLoadingType, li const { getViewInteractionCount } = trackViewInteractionCount(viewLoadingType) const longestInteractions = trackLongestInteractions(getViewInteractionCount) - let inpDuration = -1 as Duration + let interactionToNextPaint = -1 as Duration + let interactionToNextPaintTargetSelector: string | undefined const { unsubscribe: stop } = lifeCycle.subscribe(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, (entries) => { for (const entry of entries) { @@ -37,9 +45,19 @@ export function trackInteractionToNextPaint(viewLoadingType: ViewLoadingType, li } } - const newInpDuration = longestInteractions.estimateP98Duration() - if (newInpDuration) { - inpDuration = newInpDuration + const newInteraction = longestInteractions.estimateP98Interaction() + if (newInteraction) { + interactionToNextPaint = newInteraction.duration + if ( + isExperimentalFeatureEnabled(ExperimentalFeature.WEB_VITALS_ATTRIBUTION) && + newInteraction.target && + isElementNode(newInteraction.target) + ) { + interactionToNextPaintTargetSelector = getSelectorFromElement( + newInteraction.target, + configuration.actionNameAttribute + ) + } } }) @@ -47,10 +65,15 @@ export function trackInteractionToNextPaint(viewLoadingType: ViewLoadingType, li getInteractionToNextPaint: () => { // If no INP duration where captured because of the performanceObserver 40ms threshold // but the view interaction count > 0 then report 0 - if (inpDuration >= 0) { - return inpDuration + if (interactionToNextPaint >= 0) { + return { + interactionToNextPaint, + interactionToNextPaintTargetSelector, + } } else if (getViewInteractionCount()) { - return 0 as Duration + return { + interactionToNextPaint: 0 as Duration, + } } }, stop, @@ -94,9 +117,9 @@ function trackLongestInteractions(getViewInteractionCount: () => number) { * Compute the p98 longest interaction. * For better performance the computation is based on 10 longest interactions and the interaction count of the current view. */ - estimateP98Duration(): Duration | undefined { + estimateP98Interaction(): RumPerformanceEventTiming | RumFirstInputTiming | undefined { const interactionIndex = Math.min(longestInteractions.length - 1, Math.floor(getViewInteractionCount() / 50)) - return longestInteractions[interactionIndex]?.duration + return longestInteractions[interactionIndex] }, } } diff --git a/packages/rum-core/src/rawRumEvent.types.ts b/packages/rum-core/src/rawRumEvent.types.ts index fca11e4b55..da42687651 100644 --- a/packages/rum-core/src/rawRumEvent.types.ts +++ b/packages/rum-core/src/rawRumEvent.types.ts @@ -85,6 +85,7 @@ export interface RawRumViewEvent { first_input_time?: ServerDuration first_input_target_selector?: string interaction_to_next_paint?: ServerDuration + interaction_to_next_paint_target_selector?: string cumulative_layout_shift?: number custom_timings?: { [key: string]: ServerDuration From eec87021f8408c96df165f48791e409c98ad1661 Mon Sep 17 00:00:00 2001 From: Aymeric Date: Wed, 6 Sep 2023 15:10:15 +0200 Subject: [PATCH 05/18] Collect CLS target selector --- .../view/viewCollection.spec.ts | 1 + .../view/viewCollection.ts | 1 + .../viewMetrics/trackCommonViewMetrics.ts | 5 +- .../trackCumulativeLayoutShift.spec.ts | 58 +++++++++++++++++-- .../viewMetrics/trackCumulativeLayoutShift.ts | 42 ++++++++++---- packages/rum-core/src/rawRumEvent.types.ts | 1 + 6 files changed, 92 insertions(+), 16 deletions(-) diff --git a/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.spec.ts b/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.spec.ts index f5227a8247..7d1a40ac85 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.spec.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.spec.ts @@ -121,6 +121,7 @@ describe('viewCollection', () => { count: 10, }, cumulative_layout_shift: 1, + cumulative_layout_shift_target_selector: undefined, custom_timings: { bar: (20 * 1e6) as ServerDuration, foo: (10 * 1e6) as ServerDuration, diff --git a/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.ts b/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.ts index 8e50d53ff1..14260e1997 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.ts @@ -80,6 +80,7 @@ function processViewUpdate( count: view.eventCounts.frustrationCount, }, cumulative_layout_shift: view.commonViewMetrics.cumulativeLayoutShift, + cumulative_layout_shift_target_selector: view.commonViewMetrics.cumulativeLayoutShiftTargetSelector, first_byte: toServerDuration(view.initialViewMetrics.firstByte), dom_complete: toServerDuration(view.initialViewMetrics.domComplete), dom_content_loaded: toServerDuration(view.initialViewMetrics.domContentLoaded), diff --git a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackCommonViewMetrics.ts b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackCommonViewMetrics.ts index 62788efc78..a7d9caaabd 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackCommonViewMetrics.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackCommonViewMetrics.ts @@ -13,6 +13,7 @@ import { trackInteractionToNextPaint } from './trackInteractionToNextPaint' export interface CommonViewMetrics { loadingTime?: Duration cumulativeLayoutShift?: number + cumulativeLayoutShiftTargetSelector?: string interactionToNextPaint?: Duration interactionToNextPaintTargetSelector?: string scroll?: ScrollMetrics @@ -65,10 +66,12 @@ export function trackCommonViewMetrics( if (isLayoutShiftSupported()) { commonViewMetrics.cumulativeLayoutShift = 0 ;({ stop: stopCLSTracking } = trackCumulativeLayoutShift( + configuration, lifeCycle, webVitalTelemetryDebug, - (cumulativeLayoutShift) => { + (cumulativeLayoutShift, cumulativeLayoutShiftTargetSelector) => { commonViewMetrics.cumulativeLayoutShift = cumulativeLayoutShift + commonViewMetrics.cumulativeLayoutShiftTargetSelector = cumulativeLayoutShiftTargetSelector scheduleViewUpdate() } )) diff --git a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackCumulativeLayoutShift.spec.ts b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackCumulativeLayoutShift.spec.ts index ad85d2a3ef..0f38fc94ca 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackCumulativeLayoutShift.spec.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackCumulativeLayoutShift.spec.ts @@ -1,4 +1,9 @@ -import { relativeNow } from '@datadog/browser-core' +import { + ExperimentalFeature, + addExperimentalFeatures, + relativeNow, + resetExperimentalFeatures, +} from '@datadog/browser-core' import type { TestSetupBuilder } from '../../../../../test' import { setup } from '../../../../../test' import type { LifeCycle } from '../../../lifeCycle' @@ -6,6 +11,7 @@ import { LifeCycleEventType } from '../../../lifeCycle' import { THROTTLE_VIEW_UPDATE_PERIOD } from '../trackViews' import type { ViewTest } from '../setupViewTest.specHelper' import { setupViewTest } from '../setupViewTest.specHelper' +import type { RumLayoutShiftTiming } from '../../../../browser/performanceCollection' describe('trackCumulativeLayoutShift', () => { let setupBuilder: TestSetupBuilder @@ -13,13 +19,14 @@ describe('trackCumulativeLayoutShift', () => { let isLayoutShiftSupported: boolean let originalSupportedEntryTypes: PropertyDescriptor | undefined - function newLayoutShift(lifeCycle: LifeCycle, { value = 0.1, hadRecentInput = false }) { + function newLayoutShift(lifeCycle: LifeCycle, overrides: Partial) { lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [ { entryType: 'layout-shift', startTime: relativeNow(), - hadRecentInput, - value, + hadRecentInput: false, + value: 0.1, + ...overrides, }, ]) } @@ -28,6 +35,7 @@ describe('trackCumulativeLayoutShift', () => { if (!('PerformanceObserver' in window) || !('supportedEntryTypes' in PerformanceObserver)) { pending('No PerformanceObserver support') } + setupBuilder = setup() .withFakeLocation('/foo') .beforeBuild((buildContext) => { @@ -150,4 +158,46 @@ describe('trackCumulativeLayoutShift', () => { expect(getViewUpdateCount()).toEqual(3) expect(getViewUpdate(2).commonViewMetrics.cumulativeLayoutShift).toBe(0.5) }) + + describe('cls target element', () => { + let sandbox: HTMLDivElement + + beforeEach(() => { + sandbox = document.createElement('div') + document.body.appendChild(sandbox) + }) + + afterEach(() => { + resetExperimentalFeatures() + sandbox.remove() + }) + + it('should return the first target element selector amongst all the shifted nodes when FF enabled', () => { + addExperimentalFeatures([ExperimentalFeature.WEB_VITALS_ATTRIBUTION]) + const { lifeCycle } = setupBuilder.build() + const { getViewUpdate, getViewUpdateCount } = viewTest + + const textNode = sandbox.appendChild(document.createTextNode('')) + const divElement = sandbox.appendChild(document.createElement('div')) + divElement.setAttribute('id', 'div-element') + + newLayoutShift(lifeCycle, { sources: [{ node: textNode }, { node: divElement }, { node: textNode }] }) + + expect(getViewUpdateCount()).toEqual(1) + expect(getViewUpdate(0).commonViewMetrics.cumulativeLayoutShiftTargetSelector).toBe('#div-element') + }) + + it('should not return the target element selector when FF disabled', () => { + const { lifeCycle } = setupBuilder.build() + const { getViewUpdate, getViewUpdateCount } = viewTest + + const divElement = sandbox.appendChild(document.createElement('div')) + divElement.setAttribute('id', 'div-element') + + newLayoutShift(lifeCycle, { sources: [{ node: divElement }] }) + + expect(getViewUpdateCount()).toEqual(1) + expect(getViewUpdate(0).commonViewMetrics.cumulativeLayoutShiftTargetSelector).toBe(undefined) + }) + }) }) diff --git a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackCumulativeLayoutShift.ts b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackCumulativeLayoutShift.ts index b9d10a3934..42f2525574 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackCumulativeLayoutShift.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackCumulativeLayoutShift.ts @@ -1,8 +1,18 @@ -import { round, type RelativeTime, find, ONE_SECOND } from '@datadog/browser-core' +import { + round, + type RelativeTime, + find, + ONE_SECOND, + isExperimentalFeatureEnabled, + ExperimentalFeature, +} from '@datadog/browser-core' +import { isElementNode } from '../../../../browser/htmlDomUtils' import type { LifeCycle } from '../../../lifeCycle' import { LifeCycleEventType } from '../../../lifeCycle' import { supportPerformanceTimingEvent, type RumLayoutShiftTiming } from '../../../../browser/performanceCollection' import type { WebVitalTelemetryDebug } from '../startWebVitalTelemetryDebug' +import { getSelectorFromElement } from '../../../getSelectorFromElement' +import type { RumConfiguration } from '../../../configuration' /** * Track the cumulative layout shifts (CLS). @@ -21,10 +31,12 @@ import type { WebVitalTelemetryDebug } from '../startWebVitalTelemetryDebug' * https://web.dev/evolving-cls/ * Reference implementation: https://github.com/GoogleChrome/web-vitals/blob/master/src/getCLS.ts */ + export function trackCumulativeLayoutShift( + configuration: RumConfiguration, lifeCycle: LifeCycle, webVitalTelemetryDebug: WebVitalTelemetryDebug, - callback: (layoutShift: number) => void + callback: (cumulativeLayoutShift: number, cumulativeLayoutShiftTargetSelector?: string) => void ) { let maxClsValue = 0 @@ -39,17 +51,23 @@ export function trackCumulativeLayoutShift( if (window.value() > maxClsValue) { maxClsValue = window.value() const cls = round(maxClsValue, 4) + const clsTarget = window.largestLayoutShiftTarget() + let cslTargetSelector + + if (isExperimentalFeatureEnabled(ExperimentalFeature.WEB_VITALS_ATTRIBUTION) && clsTarget) { + cslTargetSelector = getSelectorFromElement(clsTarget, configuration.actionNameAttribute) + } + + callback(cls, cslTargetSelector) if (!clsAttributionCollected) { clsAttributionCollected = true webVitalTelemetryDebug.addWebVitalTelemetryDebug( 'CLS', - window.largestLayoutShiftNode(), + window.largestLayoutShiftTarget(), window.largestLayoutShiftTime() ) } - - callback(cls) } } } @@ -66,7 +84,7 @@ function slidingSessionWindow() { let endTime: RelativeTime let largestLayoutShift = 0 - let largestLayoutShiftNode: Node | undefined + let largestLayoutShiftTarget: HTMLElement | undefined let largestLayoutShiftTime: RelativeTime return { @@ -79,7 +97,7 @@ function slidingSessionWindow() { startTime = endTime = entry.startTime value = entry.value largestLayoutShift = 0 - largestLayoutShiftNode = undefined + largestLayoutShiftTarget = undefined } else { value += entry.value endTime = entry.startTime @@ -90,15 +108,17 @@ function slidingSessionWindow() { largestLayoutShiftTime = entry.startTime if (entry.sources?.length) { - const largestLayoutShiftSource = find(entry.sources, (s) => s.node?.nodeType === 1) || entry.sources[0] - largestLayoutShiftNode = largestLayoutShiftSource.node + largestLayoutShiftTarget = find( + entry.sources, + (s): s is { node: HTMLElement } => !!s.node && isElementNode(s.node) + )?.node } else { - largestLayoutShiftNode = undefined + largestLayoutShiftTarget = undefined } } }, value: () => value, - largestLayoutShiftNode: () => largestLayoutShiftNode, + largestLayoutShiftTarget: () => largestLayoutShiftTarget, largestLayoutShiftTime: () => largestLayoutShiftTime, } } diff --git a/packages/rum-core/src/rawRumEvent.types.ts b/packages/rum-core/src/rawRumEvent.types.ts index da42687651..3e64bee36d 100644 --- a/packages/rum-core/src/rawRumEvent.types.ts +++ b/packages/rum-core/src/rawRumEvent.types.ts @@ -87,6 +87,7 @@ export interface RawRumViewEvent { interaction_to_next_paint?: ServerDuration interaction_to_next_paint_target_selector?: string cumulative_layout_shift?: number + cumulative_layout_shift_target_selector?: string custom_timings?: { [key: string]: ServerDuration } From b1b61feac56ef27dbaf695aef0897ecad63a9cc6 Mon Sep 17 00:00:00 2001 From: Aymeric Date: Wed, 6 Sep 2023 17:37:14 +0200 Subject: [PATCH 06/18] Fix tests for IE --- .../view/viewMetrics/trackCumulativeLayoutShift.spec.ts | 2 +- .../view/viewMetrics/trackFirstInputTimings.spec.ts | 2 +- .../view/viewMetrics/trackInteractionToNextPaint.spec.ts | 2 +- .../view/viewMetrics/trackLargestContentfulPaint.spec.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackCumulativeLayoutShift.spec.ts b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackCumulativeLayoutShift.spec.ts index 0f38fc94ca..42abdf7e63 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackCumulativeLayoutShift.spec.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackCumulativeLayoutShift.spec.ts @@ -169,7 +169,7 @@ describe('trackCumulativeLayoutShift', () => { afterEach(() => { resetExperimentalFeatures() - sandbox.remove() + sandbox.parentNode!.removeChild(sandbox) }) it('should return the first target element selector amongst all the shifted nodes when FF enabled', () => { diff --git a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackFirstInputTimings.spec.ts b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackFirstInputTimings.spec.ts index f45ad6cc59..79b5eb61ea 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackFirstInputTimings.spec.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackFirstInputTimings.spec.ts @@ -61,7 +61,7 @@ describe('firstInputTimings', () => { afterEach(() => { setupBuilder.cleanup() - target.remove() + target.parentNode!.removeChild(target) restorePageVisibility() resetFirstHidden() resetExperimentalFeatures() diff --git a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackInteractionToNextPaint.spec.ts b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackInteractionToNextPaint.spec.ts index 07e3852201..b110281cc1 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackInteractionToNextPaint.spec.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackInteractionToNextPaint.spec.ts @@ -63,7 +63,7 @@ describe('trackInteractionToNextPaint', () => { }) afterEach(() => { - target.remove() + target.parentNode!.removeChild(target) interactionCountStub.clear() }) diff --git a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackLargestContentfulPaint.spec.ts b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackLargestContentfulPaint.spec.ts index 05748f4d14..c819f65dc5 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackLargestContentfulPaint.spec.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/view/viewMetrics/trackLargestContentfulPaint.spec.ts @@ -58,7 +58,7 @@ describe('trackLargestContentfulPaint', () => { setupBuilder.cleanup() restorePageVisibility() resetFirstHidden() - target.remove() + target.parentNode!.removeChild(target) resetExperimentalFeatures() }) From 3ba690721bfddeb78462eb8ebc4db55b85607022 Mon Sep 17 00:00:00 2001 From: Aymeric Date: Mon, 11 Sep 2023 10:03:39 +0200 Subject: [PATCH 07/18] Use fixtures --- packages/rum-core/src/boot/startRum.spec.ts | 3 +- .../src/browser/performanceCollection.ts | 76 +++++++++------ .../longTask/longTaskCollection.spec.ts | 8 +- .../src/domain/resource/resourceUtils.spec.ts | 4 +- .../domain/view/setupViewTest.specHelper.ts | 9 +- .../trackCumulativeLayoutShift.spec.ts | 19 +--- .../trackFirstContentfulPaint.spec.ts | 4 +- .../trackFirstInputTimings.spec.ts | 45 ++++----- .../trackInteractionToNextPaint.spec.ts | 47 +++++----- .../trackLargestContentfulPaint.spec.ts | 72 ++++++++------ .../view/viewMetrics/trackLoadingTime.spec.ts | 5 +- packages/rum-core/test/fixtures.ts | 94 ++++++++++++++++++- 12 files changed, 248 insertions(+), 138 deletions(-) diff --git a/packages/rum-core/src/boot/startRum.spec.ts b/packages/rum-core/src/boot/startRum.spec.ts index 74c8beca49..2aaec0b47b 100644 --- a/packages/rum-core/src/boot/startRum.spec.ts +++ b/packages/rum-core/src/boot/startRum.spec.ts @@ -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' @@ -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 diff --git a/packages/rum-core/src/browser/performanceCollection.ts b/packages/rum-core/src/browser/performanceCollection.ts index 40dac736bb..89372a97e2 100644 --- a/packages/rum-core/src/browser/performanceCollection.ts +++ b/packages/rum-core/src/browser/performanceCollection.ts @@ -11,6 +11,7 @@ import { relativeNow, runOnReadyState, addEventListener, + objectHasValue, } from '@datadog/browser-core' import type { RumConfiguration } from '../domain/configuration' @@ -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 @@ -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 @@ -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 @@ -92,7 +107,7 @@ export interface RumFirstInputTiming { } export interface RumPerformanceEventTiming { - entryType: 'event' + entryType: RumPerformanceEntryType.EVENT startTime: RelativeTime duration: Duration interactionId?: number @@ -100,7 +115,7 @@ export interface RumPerformanceEventTiming { } export interface RumLayoutShiftTiming { - entryType: 'layout-shift' + entryType: RumPerformanceEntryType.LAYOUT_SHIFT startTime: RelativeTime value: number hadRecentInput: boolean @@ -147,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() @@ -200,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() @@ -231,7 +259,7 @@ function retrieveNavigationTiming( function sendFakeTiming() { callback( assign(computeRelativePerformanceTiming(), { - entryType: 'navigation' as const, + entryType: RumPerformanceEntryType.NAVIGATION as const, }) ) } @@ -264,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 @@ -338,17 +366,9 @@ function handleRumPerformanceEntries( configuration: RumConfiguration, entries: Array ) { - 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) + ) const rumAllowedPerformanceEntries = rumPerformanceEntries.filter( (entry) => !isIncompleteNavigation(entry) && !isForbiddenResource(configuration, entry) @@ -360,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) } diff --git a/packages/rum-core/src/domain/longTask/longTaskCollection.spec.ts b/packages/rum-core/src/domain/longTask/longTaskCollection.spec.ts index c9bba45d70..db66ef7279 100644 --- a/packages/rum-core/src/domain/longTask/longTaskCollection.spec.ts +++ b/packages/rum-core/src/domain/longTask/longTaskCollection.spec.ts @@ -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 } diff --git a/packages/rum-core/src/domain/resource/resourceUtils.spec.ts b/packages/rum-core/src/domain/resource/resourceUtils.spec.ts index 30fad8995d..4254431759 100644 --- a/packages/rum-core/src/domain/resource/resourceUtils.spec.ts +++ b/packages/rum-core/src/domain/resource/resourceUtils.spec.ts @@ -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 { @@ -17,7 +17,7 @@ function generateResourceWith(overrides: Partial) 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, diff --git a/packages/rum-core/src/domain/view/setupViewTest.specHelper.ts b/packages/rum-core/src/domain/view/setupViewTest.specHelper.ts index 2cb46f20ba..a5fb26d777 100644 --- a/packages/rum-core/src/domain/view/setupViewTest.specHelper.ts +++ b/packages/rum-core/src/domain/view/setupViewTest.specHelper.ts @@ -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, @@ -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, } @@ -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, diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackCumulativeLayoutShift.spec.ts b/packages/rum-core/src/domain/view/viewMetrics/trackCumulativeLayoutShift.spec.ts index 2843ca0150..19148101db 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackCumulativeLayoutShift.spec.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackCumulativeLayoutShift.spec.ts @@ -1,17 +1,12 @@ -import { - ExperimentalFeature, - addExperimentalFeatures, - relativeNow, - resetExperimentalFeatures, -} from '@datadog/browser-core' +import { ExperimentalFeature, addExperimentalFeatures, resetExperimentalFeatures } from '@datadog/browser-core' import type { TestSetupBuilder } from '../../../../test' -import { setup } from '../../../../test' +import { createPerformanceEntry, setup } from '../../../../test' import type { LifeCycle } from '../../lifeCycle' import { LifeCycleEventType } from '../../lifeCycle' import { THROTTLE_VIEW_UPDATE_PERIOD } from '../trackViews' import type { ViewTest } from '../setupViewTest.specHelper' import { setupViewTest } from '../setupViewTest.specHelper' -import type { RumLayoutShiftTiming } from '../../../browser/performanceCollection' +import { RumPerformanceEntryType, type RumLayoutShiftTiming } from '../../../browser/performanceCollection' describe('trackCumulativeLayoutShift', () => { let setupBuilder: TestSetupBuilder @@ -21,13 +16,7 @@ describe('trackCumulativeLayoutShift', () => { function newLayoutShift(lifeCycle: LifeCycle, overrides: Partial) { lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [ - { - entryType: 'layout-shift', - startTime: relativeNow(), - hadRecentInput: false, - value: 0.1, - ...overrides, - }, + createPerformanceEntry(RumPerformanceEntryType.LAYOUT_SHIFT, overrides), ]) } diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackFirstContentfulPaint.spec.ts b/packages/rum-core/src/domain/view/viewMetrics/trackFirstContentfulPaint.spec.ts index 223f27a5a6..6622535859 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackFirstContentfulPaint.spec.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackFirstContentfulPaint.spec.ts @@ -1,6 +1,6 @@ import type { RelativeTime } from '@datadog/browser-core' import { restorePageVisibility, setPageVisibility } from '@datadog/browser-core/test' -import type { RumPerformancePaintTiming } from 'packages/rum-core/src/browser/performanceCollection' +import { RumPerformanceEntryType, type RumPerformancePaintTiming } from '../../../browser/performanceCollection' import type { TestSetupBuilder } from '../../../../test' import { setup } from '../../../../test' import { LifeCycleEventType } from '../../lifeCycle' @@ -9,7 +9,7 @@ import { FCP_MAXIMUM_DELAY, trackFirstContentfulPaint } from './trackFirstConten import { trackFirstHidden } from './trackFirstHidden' const FAKE_PAINT_ENTRY: RumPerformancePaintTiming = { - entryType: 'paint', + entryType: RumPerformanceEntryType.PAINT, name: 'first-contentful-paint', startTime: 123 as RelativeTime, } diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackFirstInputTimings.spec.ts b/packages/rum-core/src/domain/view/viewMetrics/trackFirstInputTimings.spec.ts index d94470dc05..4f0697eebe 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackFirstInputTimings.spec.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackFirstInputTimings.spec.ts @@ -8,11 +8,10 @@ import { } from '@datadog/browser-core' import { restorePageVisibility, setPageVisibility } from '@datadog/browser-core/test' import type { TestSetupBuilder } from '../../../../test' -import { setup } from '../../../../test' -import type { LifeCycle } from '../../lifeCycle' +import { createPerformanceEntry, setup } from '../../../../test' import { LifeCycleEventType } from '../../lifeCycle' import type { RumConfiguration } from '../../configuration' -import type { RumFirstInputTiming } from '../../../browser/performanceCollection' +import { RumPerformanceEntryType } from '../../../browser/performanceCollection' import { trackFirstInputTimings } from './trackFirstInputTimings' import { trackFirstHidden } from './trackFirstHidden' @@ -32,19 +31,6 @@ describe('firstInputTimings', () => { let configuration: RumConfiguration let target: HTMLButtonElement - function newFirstInput(lifeCycle: LifeCycle, overrides?: Partial) { - lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [ - { - entryType: 'first-input', - processingStart: 1100 as RelativeTime, - startTime: 1000 as RelativeTime, - duration: 0 as Duration, - target, - ...overrides, - }, - ]) - } - beforeEach(() => { configuration = {} as RumConfiguration fitCallback = jasmine.createSpy() @@ -82,7 +68,9 @@ describe('firstInputTimings', () => { it('should provide the first input timings', () => { const { lifeCycle } = setupBuilder.build() - newFirstInput(lifeCycle) + lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [ + createPerformanceEntry(RumPerformanceEntryType.FIRST_INPUT), + ]) expect(fitCallback).toHaveBeenCalledTimes(1) expect(fitCallback).toHaveBeenCalledWith({ @@ -96,7 +84,11 @@ describe('firstInputTimings', () => { addExperimentalFeatures([ExperimentalFeature.WEB_VITALS_ATTRIBUTION]) const { lifeCycle } = setupBuilder.build() - newFirstInput(lifeCycle) + lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [ + createPerformanceEntry(RumPerformanceEntryType.FIRST_INPUT, { + target, + }), + ]) expect(fitCallback).toHaveBeenCalledTimes(1) expect(fitCallback).toHaveBeenCalledWith( @@ -110,7 +102,9 @@ describe('firstInputTimings', () => { setPageVisibility('hidden') const { lifeCycle } = setupBuilder.build() - newFirstInput(lifeCycle) + lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [ + createPerformanceEntry(RumPerformanceEntryType.FIRST_INPUT), + ]) expect(fitCallback).not.toHaveBeenCalled() }) @@ -118,12 +112,13 @@ describe('firstInputTimings', () => { it('should be adjusted to 0 if the computed value would be negative due to browser timings imprecisions', () => { const { lifeCycle } = setupBuilder.build() - newFirstInput(lifeCycle, { - entryType: 'first-input' as const, - processingStart: 900 as RelativeTime, - startTime: 1000 as RelativeTime, - duration: 0 as Duration, - }) + lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [ + createPerformanceEntry(RumPerformanceEntryType.FIRST_INPUT, { + processingStart: 900 as RelativeTime, + startTime: 1000 as RelativeTime, + duration: 0 as Duration, + }), + ]) expect(fitCallback).toHaveBeenCalledTimes(1) expect(fitCallback).toHaveBeenCalledWith( diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.spec.ts b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.spec.ts index 7a8a7042db..64e6950f83 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.spec.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.spec.ts @@ -1,12 +1,8 @@ import type { Duration } from '@datadog/browser-core' -import { - ExperimentalFeature, - addExperimentalFeatures, - relativeNow, - resetExperimentalFeatures, -} from '@datadog/browser-core' +import { ExperimentalFeature, addExperimentalFeatures, resetExperimentalFeatures } from '@datadog/browser-core' import type { TestSetupBuilder } from '../../../../test' -import { setup } from '../../../../test' +import { createPerformanceEntry, setup } from '../../../../test' +import { RumPerformanceEntryType } from '../../../browser/performanceCollection' import type { BrowserWindow, RumFirstInputTiming, @@ -25,33 +21,20 @@ describe('trackInteractionToNextPaint', () => { let setupBuilder: TestSetupBuilder let interactionCountStub: ReturnType let getInteractionToNextPaint: ReturnType['getInteractionToNextPaint'] - let target: HTMLButtonElement function newInteraction(lifeCycle: LifeCycle, overrides: Partial) { if (overrides.interactionId) { interactionCountStub.incrementInteractionCount() } - lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [ - { - entryType: 'event', - processingStart: relativeNow(), - startTime: relativeNow(), - duration: 40 as Duration, - target, - ...overrides, - } as RumPerformanceEventTiming, - ]) + const entry = createPerformanceEntry(overrides.entryType || RumPerformanceEntryType.EVENT, overrides) + lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [entry]) } beforeEach(() => { if (!isInteractionToNextPaintSupported()) { pending('No INP support') } - interactionCountStub = subInteractionCount() - target = document.createElement('button') - target.setAttribute('id', 'inp-target-element') - document.body.appendChild(target) setupBuilder = setup().beforeBuild(({ lifeCycle, configuration }) => { ;({ getInteractionToNextPaint } = trackInteractionToNextPaint( @@ -63,7 +46,6 @@ describe('trackInteractionToNextPaint', () => { }) afterEach(() => { - target.parentNode!.removeChild(target) interactionCountStub.clear() }) @@ -83,6 +65,7 @@ describe('trackInteractionToNextPaint', () => { it('should ignore entries without interactionId', () => { const { lifeCycle } = setupBuilder.build() + createPerformanceEntry(RumPerformanceEntryType.EVENT) newInteraction(lifeCycle, { interactionId: undefined, }) @@ -113,7 +96,7 @@ describe('trackInteractionToNextPaint', () => { const { lifeCycle } = setupBuilder.build() newInteraction(lifeCycle, { interactionId: 1, - entryType: 'first-input', + entryType: RumPerformanceEntryType.FIRST_INPUT, }) expect(getInteractionToNextPaint()).toEqual({ interactionToNextPaint: 40 as Duration, @@ -141,10 +124,24 @@ describe('trackInteractionToNextPaint', () => { addExperimentalFeatures([ExperimentalFeature.WEB_VITALS_ATTRIBUTION]) const { lifeCycle } = setupBuilder.build() - newInteraction(lifeCycle, { interactionId: 2 }) + newInteraction(lifeCycle, { + interactionId: 2, + target, + }) expect(getInteractionToNextPaint()?.interactionToNextPaintTargetSelector).toEqual('#inp-target-element') }) + + it('should not return the target selector when FF web_vital_attribution is disabled', () => { + const { lifeCycle } = setupBuilder.build() + + newInteraction(lifeCycle, { + interactionId: 2, + target, + }) + + expect(getInteractionToNextPaint()?.interactionToNextPaintTargetSelector).toEqual(undefined) + }) }) describe('if feature flag disabled', () => { diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackLargestContentfulPaint.spec.ts b/packages/rum-core/src/domain/view/viewMetrics/trackLargestContentfulPaint.spec.ts index bf6c233d95..d71a67d161 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackLargestContentfulPaint.spec.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackLargestContentfulPaint.spec.ts @@ -7,10 +7,9 @@ import { resetExperimentalFeatures, } from '@datadog/browser-core' import { restorePageVisibility, setPageVisibility, createNewEvent } from '@datadog/browser-core/test' -import type { RumLargestContentfulPaintTiming } from '../../../browser/performanceCollection' +import { RumPerformanceEntryType } from '../../../browser/performanceCollection' import type { TestSetupBuilder } from '../../../../test' -import { setup } from '../../../../test' -import type { LifeCycle } from '../../lifeCycle' +import { createPerformanceEntry, setup } from '../../../../test' import { LifeCycleEventType } from '../../lifeCycle' import type { RumConfiguration } from '../../configuration' import { LCP_MAXIMUM_DELAY, trackLargestContentfulPaint } from './trackLargestContentfulPaint' @@ -19,29 +18,13 @@ import { trackFirstHidden } from './trackFirstHidden' describe('trackLargestContentfulPaint', () => { let setupBuilder: TestSetupBuilder let lcpCallback: jasmine.Spy<(value: RelativeTime, targetSelector?: string) => void> + let eventTarget: Window let configuration: RumConfiguration - let target: HTMLImageElement - - function newLargestContentfulPaint(lifeCycle: LifeCycle, overrides?: Partial) { - lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [ - { - entryType: 'largest-contentful-paint', - startTime: 789 as RelativeTime, - size: 10, - element: target, - ...overrides, - }, - ]) - } beforeEach(() => { configuration = {} as RumConfiguration lcpCallback = jasmine.createSpy() - - target = document.createElement('img') - target.setAttribute('id', 'lcp-target-element') - document.body.appendChild(target) - + eventTarget = document.createElement('div') as unknown as Window setupBuilder = setup().beforeBuild(({ lifeCycle }) => { const firstHidden = trackFirstHidden(configuration) const largestContentfulPaint = trackLargestContentfulPaint( @@ -49,7 +32,7 @@ describe('trackLargestContentfulPaint', () => { configuration, { addWebVitalTelemetryDebug: noop }, firstHidden, - target as unknown as Window, + eventTarget, lcpCallback ) return { @@ -64,14 +47,16 @@ describe('trackLargestContentfulPaint', () => { afterEach(() => { setupBuilder.cleanup() restorePageVisibility() - target.parentNode!.removeChild(target) resetExperimentalFeatures() }) it('should provide the largest contentful paint timing', () => { const { lifeCycle } = setupBuilder.build() - newLargestContentfulPaint(lifeCycle) + lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [ + createPerformanceEntry(RumPerformanceEntryType.LARGEST_CONTENTFUL_PAINT), + ]) + expect(lcpCallback).toHaveBeenCalledTimes(1 as RelativeTime) expect(lcpCallback).toHaveBeenCalledWith(789 as RelativeTime, undefined) }) @@ -80,17 +65,38 @@ describe('trackLargestContentfulPaint', () => { addExperimentalFeatures([ExperimentalFeature.WEB_VITALS_ATTRIBUTION]) const { lifeCycle } = setupBuilder.build() - newLargestContentfulPaint(lifeCycle) + lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [ + createPerformanceEntry(RumPerformanceEntryType.LARGEST_CONTENTFUL_PAINT, { + element: target, + }), + ]) + expect(lcpCallback).toHaveBeenCalledTimes(1 as RelativeTime) expect(lcpCallback).toHaveBeenCalledWith(789 as RelativeTime, '#lcp-target-element') }) + it('should not provide the largest contentful paint target selector if FF disabled', () => { + const { lifeCycle } = setupBuilder.build() + + lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [ + createPerformanceEntry(RumPerformanceEntryType.LARGEST_CONTENTFUL_PAINT, { + element: target, + }), + ]) + + expect(lcpCallback).toHaveBeenCalledTimes(1 as RelativeTime) + expect(lcpCallback).toHaveBeenCalledWith(789 as RelativeTime, undefined) + }) + it('should be discarded if it is reported after a user interaction', () => { const { lifeCycle } = setupBuilder.build() - target.dispatchEvent(createNewEvent(DOM_EVENT.KEY_DOWN, { timeStamp: 1 })) + eventTarget.dispatchEvent(createNewEvent(DOM_EVENT.KEY_DOWN, { timeStamp: 1 })) + + lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [ + createPerformanceEntry(RumPerformanceEntryType.LARGEST_CONTENTFUL_PAINT), + ]) - newLargestContentfulPaint(lifeCycle) expect(lcpCallback).not.toHaveBeenCalled() }) @@ -98,14 +104,22 @@ describe('trackLargestContentfulPaint', () => { setPageVisibility('hidden') const { lifeCycle } = setupBuilder.build() - newLargestContentfulPaint(lifeCycle) + lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [ + createPerformanceEntry(RumPerformanceEntryType.LARGEST_CONTENTFUL_PAINT), + ]) + expect(lcpCallback).not.toHaveBeenCalled() }) it('should be discarded if it is reported after a long time', () => { const { lifeCycle } = setupBuilder.build() - newLargestContentfulPaint(lifeCycle, { startTime: LCP_MAXIMUM_DELAY as RelativeTime }) + lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [ + createPerformanceEntry(RumPerformanceEntryType.LARGEST_CONTENTFUL_PAINT, { + startTime: LCP_MAXIMUM_DELAY as RelativeTime, + }), + ]) + expect(lcpCallback).not.toHaveBeenCalled() }) }) diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackLoadingTime.spec.ts b/packages/rum-core/src/domain/view/viewMetrics/trackLoadingTime.spec.ts index 9a3c6f4cef..e2b08d7303 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackLoadingTime.spec.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackLoadingTime.spec.ts @@ -3,6 +3,7 @@ import { addDuration } from '@datadog/browser-core' import type { TestSetupBuilder } from '../../../../test' import { setup } from '../../../../test' import type { RumPerformanceNavigationTiming } from '../../../browser/performanceCollection' +import { RumPerformanceEntryType } from '../../../browser/performanceCollection' import { LifeCycleEventType } from '../../lifeCycle' import { PAGE_ACTIVITY_END_DELAY, PAGE_ACTIVITY_VALIDATION_DELAY } from '../../waitPageActivityEnd' import { THROTTLE_VIEW_UPDATE_PERIOD } from '../trackViews' @@ -18,7 +19,7 @@ const FAKE_NAVIGATION_ENTRY_WITH_LOADEVENT_BEFORE_ACTIVITY_TIMING: RumPerformanc domComplete: 2 as RelativeTime, domContentLoadedEventEnd: 1 as RelativeTime, domInteractive: 1 as RelativeTime, - entryType: 'navigation', + entryType: RumPerformanceEntryType.NAVIGATION, loadEventEnd: (BEFORE_PAGE_ACTIVITY_VALIDATION_DELAY * 0.8) as RelativeTime, } @@ -27,7 +28,7 @@ const FAKE_NAVIGATION_ENTRY_WITH_LOADEVENT_AFTER_ACTIVITY_TIMING: RumPerformance domComplete: 2 as RelativeTime, domContentLoadedEventEnd: 1 as RelativeTime, domInteractive: 1 as RelativeTime, - entryType: 'navigation', + entryType: RumPerformanceEntryType.NAVIGATION, loadEventEnd: (BEFORE_PAGE_ACTIVITY_VALIDATION_DELAY * 1.2) as RelativeTime, } diff --git a/packages/rum-core/test/fixtures.ts b/packages/rum-core/test/fixtures.ts index deb22f9417..aa2cea13ae 100644 --- a/packages/rum-core/test/fixtures.ts +++ b/packages/rum-core/test/fixtures.ts @@ -1,6 +1,24 @@ import type { Context, Duration, RelativeTime, ServerDuration, TimeStamp } from '@datadog/browser-core' -import { combine, ErrorHandling, ErrorSource, generateUUID, ResourceType } from '@datadog/browser-core' -import type { RumPerformanceResourceTiming } from '../src/browser/performanceCollection' +import { + assign, + combine, + ErrorHandling, + ErrorSource, + generateUUID, + relativeNow, + ResourceType, +} from '@datadog/browser-core' +import { RumPerformanceEntryType } from '../src/browser/performanceCollection' +import type { + RumFirstInputTiming, + RumLargestContentfulPaintTiming, + RumLayoutShiftTiming, + RumPerformanceEventTiming, + RumPerformanceLongTaskTiming, + RumPerformanceNavigationTiming, + RumPerformancePaintTiming, + RumPerformanceResourceTiming, +} from '../src/browser/performanceCollection' import type { RawRumEvent } from '../src/rawRumEvent.types' import { ActionType, RumEventType, ViewLoadingType } from '../src/rawRumEvent.types' @@ -98,6 +116,76 @@ export function createRawRumEvent(type: RumEventType, overrides?: Context): RawR } } +type EntryTypeToReturnType = { + [RumPerformanceEntryType.EVENT]: RumPerformanceEventTiming + [RumPerformanceEntryType.FIRST_INPUT]: RumFirstInputTiming + [RumPerformanceEntryType.LARGEST_CONTENTFUL_PAINT]: RumLargestContentfulPaintTiming + [RumPerformanceEntryType.LAYOUT_SHIFT]: RumLayoutShiftTiming + [RumPerformanceEntryType.PAINT]: RumPerformancePaintTiming + [RumPerformanceEntryType.LONG_TASK]: RumPerformanceLongTaskTiming + [RumPerformanceEntryType.NAVIGATION]: RumPerformanceNavigationTiming + [RumPerformanceEntryType.RESOURCE]: RumPerformanceResourceTiming +} + +export function createPerformanceEntry( + entryType: T, + overrides?: Partial +): EntryTypeToReturnType[T] { + switch (entryType) { + case RumPerformanceEntryType.EVENT: + return assign( + { + entryType: RumPerformanceEntryType.EVENT, + processingStart: relativeNow(), + startTime: relativeNow(), + duration: 40 as Duration, + }, + overrides + ) as EntryTypeToReturnType[T] + case RumPerformanceEntryType.FIRST_INPUT: + return assign( + { + entryType: RumPerformanceEntryType.FIRST_INPUT, + processingStart: 1100 as RelativeTime, + startTime: 1000 as RelativeTime, + duration: 40 as Duration, + }, + overrides + ) as EntryTypeToReturnType[T] + case RumPerformanceEntryType.LARGEST_CONTENTFUL_PAINT: + return assign( + { + entryType: RumPerformanceEntryType.LARGEST_CONTENTFUL_PAINT, + startTime: 789 as RelativeTime, + size: 10, + }, + overrides + ) as EntryTypeToReturnType[T] + case RumPerformanceEntryType.LAYOUT_SHIFT: + return assign( + { + entryType: RumPerformanceEntryType.LAYOUT_SHIFT, + startTime: relativeNow(), + hadRecentInput: false, + value: 0.1, + }, + overrides + ) as EntryTypeToReturnType[T] + + case RumPerformanceEntryType.PAINT: + return assign( + { + entryType: RumPerformanceEntryType.PAINT, + name: 'first-contentful-paint', + startTime: 123 as RelativeTime, + }, + overrides + ) as EntryTypeToReturnType[T] + default: + throw new Error(`Unsupported entryType fixture: ${entryType}`) + } +} + export function createResourceEntry( overrides?: Partial ): RumPerformanceResourceTiming & PerformanceResourceTiming { @@ -108,7 +196,7 @@ export function createResourceEntry( domainLookupEnd: 200 as RelativeTime, domainLookupStart: 200 as RelativeTime, duration: 100 as Duration, - entryType: 'resource', + entryType: RumPerformanceEntryType.RESOURCE, fetchStart: 200 as RelativeTime, name: 'https://resource.com/valid', redirectEnd: 200 as RelativeTime, From 7c3e13168bf50a20e2a1f827f3c37dbd7467f693 Mon Sep 17 00:00:00 2001 From: Aymeric Date: Mon, 11 Sep 2023 10:06:45 +0200 Subject: [PATCH 08/18] Introduce appendElement --- .../trackCumulativeLayoutShift.spec.ts | 18 ++++---------- .../trackFirstInputTimings.spec.ts | 10 ++------ .../trackInteractionToNextPaint.spec.ts | 6 ++--- .../trackLargestContentfulPaint.spec.ts | 6 ++--- .../test/{createIsolatedDom.ts => dom.ts} | 24 +++++++++++++++++++ packages/rum-core/test/index.ts | 2 +- 6 files changed, 37 insertions(+), 29 deletions(-) rename packages/rum-core/test/{createIsolatedDom.ts => dom.ts} (58%) diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackCumulativeLayoutShift.spec.ts b/packages/rum-core/src/domain/view/viewMetrics/trackCumulativeLayoutShift.spec.ts index 19148101db..a71d4c2902 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackCumulativeLayoutShift.spec.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackCumulativeLayoutShift.spec.ts @@ -1,6 +1,6 @@ import { ExperimentalFeature, addExperimentalFeatures, resetExperimentalFeatures } from '@datadog/browser-core' import type { TestSetupBuilder } from '../../../../test' -import { createPerformanceEntry, setup } from '../../../../test' +import { appendElement, appendTextNode, createPerformanceEntry, setup } from '../../../../test' import type { LifeCycle } from '../../lifeCycle' import { LifeCycleEventType } from '../../lifeCycle' import { THROTTLE_VIEW_UPDATE_PERIOD } from '../trackViews' @@ -149,16 +149,8 @@ describe('trackCumulativeLayoutShift', () => { }) describe('cls target element', () => { - let sandbox: HTMLDivElement - - beforeEach(() => { - sandbox = document.createElement('div') - document.body.appendChild(sandbox) - }) - afterEach(() => { resetExperimentalFeatures() - sandbox.parentNode!.removeChild(sandbox) }) it('should return the first target element selector amongst all the shifted nodes when FF enabled', () => { @@ -166,9 +158,8 @@ describe('trackCumulativeLayoutShift', () => { const { lifeCycle } = setupBuilder.build() const { getViewUpdate, getViewUpdateCount } = viewTest - const textNode = sandbox.appendChild(document.createTextNode('')) - const divElement = sandbox.appendChild(document.createElement('div')) - divElement.setAttribute('id', 'div-element') + const textNode = appendTextNode('') + const divElement = appendElement('div', { id: 'div-element' }) newLayoutShift(lifeCycle, { sources: [{ node: textNode }, { node: divElement }, { node: textNode }] }) @@ -180,8 +171,7 @@ describe('trackCumulativeLayoutShift', () => { const { lifeCycle } = setupBuilder.build() const { getViewUpdate, getViewUpdateCount } = viewTest - const divElement = sandbox.appendChild(document.createElement('div')) - divElement.setAttribute('id', 'div-element') + const divElement = appendElement('div', { id: 'div-element' }) newLayoutShift(lifeCycle, { sources: [{ node: divElement }] }) diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackFirstInputTimings.spec.ts b/packages/rum-core/src/domain/view/viewMetrics/trackFirstInputTimings.spec.ts index 4f0697eebe..bc3fc874c5 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackFirstInputTimings.spec.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackFirstInputTimings.spec.ts @@ -8,7 +8,7 @@ import { } from '@datadog/browser-core' import { restorePageVisibility, setPageVisibility } from '@datadog/browser-core/test' import type { TestSetupBuilder } from '../../../../test' -import { createPerformanceEntry, setup } from '../../../../test' +import { appendElement, createPerformanceEntry, setup } from '../../../../test' import { LifeCycleEventType } from '../../lifeCycle' import type { RumConfiguration } from '../../configuration' import { RumPerformanceEntryType } from '../../../browser/performanceCollection' @@ -29,16 +29,11 @@ describe('firstInputTimings', () => { }) => void > let configuration: RumConfiguration - let target: HTMLButtonElement beforeEach(() => { configuration = {} as RumConfiguration fitCallback = jasmine.createSpy() - target = document.createElement('button') - target.setAttribute('id', 'fid-target-element') - document.body.appendChild(target) - setupBuilder = setup().beforeBuild(({ lifeCycle }) => { const firstHidden = trackFirstHidden(configuration) const firstInputTimings = trackFirstInputTimings( @@ -60,7 +55,6 @@ describe('firstInputTimings', () => { afterEach(() => { setupBuilder.cleanup() - target.parentNode!.removeChild(target) restorePageVisibility() resetExperimentalFeatures() }) @@ -86,7 +80,7 @@ describe('firstInputTimings', () => { lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [ createPerformanceEntry(RumPerformanceEntryType.FIRST_INPUT, { - target, + target: appendElement('button', { id: 'fid-target-element' }), }), ]) diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.spec.ts b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.spec.ts index 64e6950f83..1c04e1121f 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.spec.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.spec.ts @@ -1,7 +1,7 @@ import type { Duration } from '@datadog/browser-core' import { ExperimentalFeature, addExperimentalFeatures, resetExperimentalFeatures } from '@datadog/browser-core' import type { TestSetupBuilder } from '../../../../test' -import { createPerformanceEntry, setup } from '../../../../test' +import { appendElement, createPerformanceEntry, setup } from '../../../../test' import { RumPerformanceEntryType } from '../../../browser/performanceCollection' import type { BrowserWindow, @@ -126,7 +126,7 @@ describe('trackInteractionToNextPaint', () => { newInteraction(lifeCycle, { interactionId: 2, - target, + target: appendElement('button', { id: 'inp-target-element' }), }) expect(getInteractionToNextPaint()?.interactionToNextPaintTargetSelector).toEqual('#inp-target-element') @@ -137,7 +137,7 @@ describe('trackInteractionToNextPaint', () => { newInteraction(lifeCycle, { interactionId: 2, - target, + target: appendElement('button', { id: 'inp-target-element' }), }) expect(getInteractionToNextPaint()?.interactionToNextPaintTargetSelector).toEqual(undefined) diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackLargestContentfulPaint.spec.ts b/packages/rum-core/src/domain/view/viewMetrics/trackLargestContentfulPaint.spec.ts index d71a67d161..33f475d27c 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackLargestContentfulPaint.spec.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackLargestContentfulPaint.spec.ts @@ -9,7 +9,7 @@ import { import { restorePageVisibility, setPageVisibility, createNewEvent } from '@datadog/browser-core/test' import { RumPerformanceEntryType } from '../../../browser/performanceCollection' import type { TestSetupBuilder } from '../../../../test' -import { createPerformanceEntry, setup } from '../../../../test' +import { appendElement, createPerformanceEntry, setup } from '../../../../test' import { LifeCycleEventType } from '../../lifeCycle' import type { RumConfiguration } from '../../configuration' import { LCP_MAXIMUM_DELAY, trackLargestContentfulPaint } from './trackLargestContentfulPaint' @@ -67,7 +67,7 @@ describe('trackLargestContentfulPaint', () => { lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [ createPerformanceEntry(RumPerformanceEntryType.LARGEST_CONTENTFUL_PAINT, { - element: target, + element: appendElement('button', { id: 'lcp-target-element' }), }), ]) @@ -80,7 +80,7 @@ describe('trackLargestContentfulPaint', () => { lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [ createPerformanceEntry(RumPerformanceEntryType.LARGEST_CONTENTFUL_PAINT, { - element: target, + element: appendElement('button', { id: 'lcp-target-element' }), }), ]) diff --git a/packages/rum-core/test/createIsolatedDom.ts b/packages/rum-core/test/dom.ts similarity index 58% rename from packages/rum-core/test/createIsolatedDom.ts rename to packages/rum-core/test/dom.ts index 86ef0c97f3..79c1d4bfb8 100644 --- a/packages/rum-core/test/createIsolatedDom.ts +++ b/packages/rum-core/test/dom.ts @@ -1,3 +1,5 @@ +import { registerCleanupTask } from '@datadog/browser-core/test' + export type IsolatedDom = ReturnType export function createIsolatedDom() { @@ -28,3 +30,25 @@ export function createIsolatedDom() { }, } } + +export function appendElement(tagName: string, attributes: { [key: string]: string }) { + const element = document.createElement(tagName) + + for (const key in attributes) { + if (Object.prototype.hasOwnProperty.call(attributes, key)) { + element.setAttribute(key, attributes[key]) + } + } + + return append(element) +} + +export function appendTextNode(text: string) { + return append(document.createTextNode(text)) +} + +function append(node: T): T { + document.body.appendChild(node) + registerCleanupTask(() => node.parentNode!.removeChild(node)) + return node +} diff --git a/packages/rum-core/test/index.ts b/packages/rum-core/test/index.ts index 1e346416bd..6b9c8fe3b2 100644 --- a/packages/rum-core/test/index.ts +++ b/packages/rum-core/test/index.ts @@ -1,5 +1,5 @@ export * from './createFakeClick' -export * from './createIsolatedDom' +export * from './dom' export * from './fixtures' export * from './formatValidation' export * from './mockCiVisibilityWindowValues' From bdf6279cd20ebe7f3ecfb650f774ff95e45f15cb Mon Sep 17 00:00:00 2001 From: Aymeric Date: Mon, 11 Sep 2023 10:17:44 +0200 Subject: [PATCH 09/18] Test when the target is not an element --- .../trackFirstInputTimings.spec.ts | 22 +++++++++++++++++-- .../trackInteractionToNextPaint.spec.ts | 14 +++++++++++- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackFirstInputTimings.spec.ts b/packages/rum-core/src/domain/view/viewMetrics/trackFirstInputTimings.spec.ts index bc3fc874c5..77daacd2e7 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackFirstInputTimings.spec.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackFirstInputTimings.spec.ts @@ -8,7 +8,7 @@ import { } from '@datadog/browser-core' import { restorePageVisibility, setPageVisibility } from '@datadog/browser-core/test' import type { TestSetupBuilder } from '../../../../test' -import { appendElement, createPerformanceEntry, setup } from '../../../../test' +import { appendElement, appendTextNode, createPerformanceEntry, setup } from '../../../../test' import { LifeCycleEventType } from '../../lifeCycle' import type { RumConfiguration } from '../../configuration' import { RumPerformanceEntryType } from '../../../browser/performanceCollection' @@ -74,7 +74,7 @@ describe('firstInputTimings', () => { }) }) - it('should provide the first input target selector if FF enabled', () => { + it('should provide the first input target selector when FF web_vital_attribution is enabled', () => { addExperimentalFeatures([ExperimentalFeature.WEB_VITALS_ATTRIBUTION]) const { lifeCycle } = setupBuilder.build() @@ -92,6 +92,24 @@ describe('firstInputTimings', () => { ) }) + it("should not provide the first input target if it's not a DOM element when FF web_vital_attribution is enabled", () => { + addExperimentalFeatures([ExperimentalFeature.WEB_VITALS_ATTRIBUTION]) + const { lifeCycle } = setupBuilder.build() + + lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [ + createPerformanceEntry(RumPerformanceEntryType.FIRST_INPUT, { + target: appendTextNode(''), + }), + ]) + + expect(fitCallback).toHaveBeenCalledTimes(1) + expect(fitCallback).toHaveBeenCalledWith( + jasmine.objectContaining({ + firstInputTargetSelector: undefined, + }) + ) + }) + it('should be discarded if the page is hidden', () => { setPageVisibility('hidden') const { lifeCycle } = setupBuilder.build() diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.spec.ts b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.spec.ts index 1c04e1121f..4179c19728 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.spec.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.spec.ts @@ -1,7 +1,7 @@ import type { Duration } from '@datadog/browser-core' import { ExperimentalFeature, addExperimentalFeatures, resetExperimentalFeatures } from '@datadog/browser-core' import type { TestSetupBuilder } from '../../../../test' -import { appendElement, createPerformanceEntry, setup } from '../../../../test' +import { appendElement, appendTextNode, createPerformanceEntry, setup } from '../../../../test' import { RumPerformanceEntryType } from '../../../browser/performanceCollection' import type { BrowserWindow, @@ -132,6 +132,18 @@ describe('trackInteractionToNextPaint', () => { expect(getInteractionToNextPaint()?.interactionToNextPaintTargetSelector).toEqual('#inp-target-element') }) + it("should not return the target selector if it's not a DOM element when FF web_vital_attribution is enabled", () => { + addExperimentalFeatures([ExperimentalFeature.WEB_VITALS_ATTRIBUTION]) + const { lifeCycle } = setupBuilder.build() + + newInteraction(lifeCycle, { + interactionId: 2, + target: appendTextNode(''), + }) + + expect(getInteractionToNextPaint()?.interactionToNextPaintTargetSelector).toEqual(undefined) + }) + it('should not return the target selector when FF web_vital_attribution is disabled', () => { const { lifeCycle } = setupBuilder.build() From df732ef82620e534cc90aa13aa5b224fa957dfb7 Mon Sep 17 00:00:00 2001 From: Aymeric Date: Mon, 11 Sep 2023 11:36:18 +0200 Subject: [PATCH 10/18] Do not flatten the intermediate data structure --- .../src/domain/view/trackViews.spec.ts | 32 +++++++----- .../src/domain/view/viewCollection.spec.ts | 24 +++++---- .../src/domain/view/viewCollection.ts | 28 +++++----- .../viewMetrics/trackCommonViewMetrics.ts | 17 +++---- .../trackCumulativeLayoutShift.spec.ts | 20 ++++---- .../viewMetrics/trackCumulativeLayoutShift.ts | 13 +++-- .../viewMetrics/trackFirstInputTimings.ts | 17 +++---- .../trackInitialViewMetrics.spec.ts | 22 ++++---- .../viewMetrics/trackInitialViewMetrics.ts | 51 ++++++++----------- .../trackInteractionToNextPaint.spec.ts | 20 ++++---- .../trackInteractionToNextPaint.ts | 12 +++-- .../trackLargestContentfulPaint.spec.ts | 9 ++-- .../trackLargestContentfulPaint.ts | 11 +++- .../trackNavigationTimings.spec.ts | 4 +- 14 files changed, 147 insertions(+), 133 deletions(-) diff --git a/packages/rum-core/src/domain/view/trackViews.spec.ts b/packages/rum-core/src/domain/view/trackViews.spec.ts index 595a42b354..5a785c5dc7 100644 --- a/packages/rum-core/src/domain/view/trackViews.spec.ts +++ b/packages/rum-core/src/domain/view/trackViews.spec.ts @@ -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, @@ -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({}) @@ -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 }, }) ) }) @@ -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 ) }) diff --git a/packages/rum-core/src/domain/view/viewCollection.spec.ts b/packages/rum-core/src/domain/view/viewCollection.spec.ts index e09552ea1d..14f2af73bd 100644 --- a/packages/rum-core/src/domain/view/viewCollection.spec.ts +++ b/packages/rum-core/src/domain/view/viewCollection.spec.ts @@ -31,20 +31,24 @@ const VIEW: ViewEvent = { location: {} as Location, startClocks: { relative: 1234 as RelativeTime, timeStamp: 123456789 as TimeStamp }, initialViewMetrics: { - firstByte: 10 as Duration, - domComplete: 10 as Duration, - domContentLoaded: 10 as Duration, - domInteractive: 10 as Duration, + navigationTimings: { + firstByte: 10 as Duration, + domComplete: 10 as Duration, + domContentLoaded: 10 as Duration, + domInteractive: 10 as Duration, + loadEvent: 10 as Duration, + }, + firstInputTimings: { + firstInputDelay: 12 as Duration, + firstInputTime: 10 as RelativeTime, + }, firstContentfulPaint: 10 as Duration, - firstInputDelay: 12 as Duration, - firstInputTime: 10 as Duration, - largestContentfulPaint: 10 as Duration, - loadEvent: 10 as Duration, + largestContentfulPaint: { value: 10 as RelativeTime }, }, commonViewMetrics: { loadingTime: 20 as Duration, - cumulativeLayoutShift: 1, - interactionToNextPaint: 10 as Duration, + cumulativeLayoutShift: { value: 1 }, + interactionToNextPaint: { value: 10 as Duration }, scroll: { maxDepth: 2000, maxDepthScrollHeight: 3000, diff --git a/packages/rum-core/src/domain/view/viewCollection.ts b/packages/rum-core/src/domain/view/viewCollection.ts index 280a04b145..4b20dbf57f 100644 --- a/packages/rum-core/src/domain/view/viewCollection.ts +++ b/packages/rum-core/src/domain/view/viewCollection.ts @@ -77,26 +77,26 @@ function processViewUpdate( frustration: { count: view.eventCounts.frustrationCount, }, - cumulative_layout_shift: view.commonViewMetrics.cumulativeLayoutShift, - cumulative_layout_shift_target_selector: view.commonViewMetrics.cumulativeLayoutShiftTargetSelector, - first_byte: toServerDuration(view.initialViewMetrics.firstByte), - dom_complete: toServerDuration(view.initialViewMetrics.domComplete), - dom_content_loaded: toServerDuration(view.initialViewMetrics.domContentLoaded), - dom_interactive: toServerDuration(view.initialViewMetrics.domInteractive), + cumulative_layout_shift: view.commonViewMetrics.cumulativeLayoutShift?.value, + cumulative_layout_shift_target_selector: view.commonViewMetrics.cumulativeLayoutShift?.targetSelector, + first_byte: toServerDuration(view.initialViewMetrics.navigationTimings?.firstByte), + dom_complete: toServerDuration(view.initialViewMetrics.navigationTimings?.domComplete), + dom_content_loaded: toServerDuration(view.initialViewMetrics.navigationTimings?.domContentLoaded), + dom_interactive: toServerDuration(view.initialViewMetrics.navigationTimings?.domInteractive), error: { count: view.eventCounts.errorCount, }, first_contentful_paint: toServerDuration(view.initialViewMetrics.firstContentfulPaint), - first_input_delay: toServerDuration(view.initialViewMetrics.firstInputDelay), - first_input_time: toServerDuration(view.initialViewMetrics.firstInputTime), - first_input_target_selector: view.initialViewMetrics.firstInputTargetSelector, - interaction_to_next_paint: toServerDuration(view.commonViewMetrics.interactionToNextPaint), - interaction_to_next_paint_target_selector: view.commonViewMetrics.interactionToNextPaintTargetSelector, + first_input_delay: toServerDuration(view.initialViewMetrics.firstInputTimings?.firstInputDelay), + first_input_time: toServerDuration(view.initialViewMetrics.firstInputTimings?.firstInputTime), + first_input_target_selector: view.initialViewMetrics.firstInputTimings?.firstInputTargetSelector, + interaction_to_next_paint: toServerDuration(view.commonViewMetrics.interactionToNextPaint?.value), + interaction_to_next_paint_target_selector: view.commonViewMetrics.interactionToNextPaint?.targetSelector, is_active: view.isActive, name: view.name, - largest_contentful_paint: toServerDuration(view.initialViewMetrics.largestContentfulPaint), - largest_contentful_paint_target_selector: view.initialViewMetrics.largestContentfulPaintTargetSelector, - load_event: toServerDuration(view.initialViewMetrics.loadEvent), + largest_contentful_paint: toServerDuration(view.initialViewMetrics.largestContentfulPaint?.value), + largest_contentful_paint_target_selector: view.initialViewMetrics.largestContentfulPaint?.targetSelector, + load_event: toServerDuration(view.initialViewMetrics.navigationTimings?.loadEvent), loading_time: discardNegativeDuration(toServerDuration(view.commonViewMetrics.loadingTime)), loading_type: view.loadingType, long_task: { diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackCommonViewMetrics.ts b/packages/rum-core/src/domain/view/viewMetrics/trackCommonViewMetrics.ts index 3a078bbb45..f2c370de70 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackCommonViewMetrics.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackCommonViewMetrics.ts @@ -7,15 +7,15 @@ import type { WebVitalTelemetryDebug } from '../startWebVitalTelemetryDebug' import type { ScrollMetrics } from './trackScrollMetrics' import { computeScrollValues, trackScrollMetrics } from './trackScrollMetrics' import { trackLoadingTime } from './trackLoadingTime' +import type { CumulativeLayoutShift } from './trackCumulativeLayoutShift' import { isLayoutShiftSupported, trackCumulativeLayoutShift } from './trackCumulativeLayoutShift' +import type { InteractionToNextPaint } from './trackInteractionToNextPaint' import { trackInteractionToNextPaint } from './trackInteractionToNextPaint' export interface CommonViewMetrics { loadingTime?: Duration - cumulativeLayoutShift?: number - cumulativeLayoutShiftTargetSelector?: string - interactionToNextPaint?: Duration - interactionToNextPaintTargetSelector?: string + cumulativeLayoutShift?: CumulativeLayoutShift + interactionToNextPaint?: InteractionToNextPaint scroll?: ScrollMetrics } @@ -64,14 +64,13 @@ export function trackCommonViewMetrics( let stopCLSTracking: () => void if (isLayoutShiftSupported()) { - commonViewMetrics.cumulativeLayoutShift = 0 + commonViewMetrics.cumulativeLayoutShift = { value: 0 } ;({ stop: stopCLSTracking } = trackCumulativeLayoutShift( configuration, lifeCycle, webVitalTelemetryDebug, - (cumulativeLayoutShift, cumulativeLayoutShiftTargetSelector) => { + (cumulativeLayoutShift) => { commonViewMetrics.cumulativeLayoutShift = cumulativeLayoutShift - commonViewMetrics.cumulativeLayoutShiftTargetSelector = cumulativeLayoutShiftTargetSelector scheduleViewUpdate() } )) @@ -94,9 +93,7 @@ export function trackCommonViewMetrics( }, setLoadEvent, getCommonViewMetrics: () => { - const { interactionToNextPaint, interactionToNextPaintTargetSelector } = getInteractionToNextPaint() || {} - commonViewMetrics.interactionToNextPaint = interactionToNextPaint - commonViewMetrics.interactionToNextPaintTargetSelector = interactionToNextPaintTargetSelector + commonViewMetrics.interactionToNextPaint = getInteractionToNextPaint() return commonViewMetrics }, } diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackCumulativeLayoutShift.spec.ts b/packages/rum-core/src/domain/view/viewMetrics/trackCumulativeLayoutShift.spec.ts index a71d4c2902..6b1008c5a2 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackCumulativeLayoutShift.spec.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackCumulativeLayoutShift.spec.ts @@ -50,7 +50,7 @@ describe('trackCumulativeLayoutShift', () => { const { getViewUpdate, getViewUpdateCount } = viewTest expect(getViewUpdateCount()).toEqual(1) - expect(getViewUpdate(0).commonViewMetrics.cumulativeLayoutShift).toBe(0) + expect(getViewUpdate(0).commonViewMetrics.cumulativeLayoutShift?.value).toBe(0) }) it('should be initialized to undefined if layout-shift is not supported', () => { @@ -59,7 +59,7 @@ describe('trackCumulativeLayoutShift', () => { const { getViewUpdate, getViewUpdateCount } = viewTest expect(getViewUpdateCount()).toEqual(1) - expect(getViewUpdate(0).commonViewMetrics.cumulativeLayoutShift).toBe(undefined) + expect(getViewUpdate(0).commonViewMetrics.cumulativeLayoutShift?.value).toBe(undefined) }) it('should accumulate layout shift values for the first session window', () => { @@ -71,7 +71,7 @@ describe('trackCumulativeLayoutShift', () => { clock.tick(THROTTLE_VIEW_UPDATE_PERIOD) expect(getViewUpdateCount()).toEqual(2) - expect(getViewUpdate(1).commonViewMetrics.cumulativeLayoutShift).toBe(0.3) + expect(getViewUpdate(1).commonViewMetrics.cumulativeLayoutShift?.value).toBe(0.3) }) it('should round the cumulative layout shift value to 4 decimals', () => { @@ -83,7 +83,7 @@ describe('trackCumulativeLayoutShift', () => { clock.tick(THROTTLE_VIEW_UPDATE_PERIOD) expect(getViewUpdateCount()).toEqual(2) - expect(getViewUpdate(1).commonViewMetrics.cumulativeLayoutShift).toBe(2.3457) + expect(getViewUpdate(1).commonViewMetrics.cumulativeLayoutShift?.value).toBe(2.3457) }) it('should ignore entries with recent input', () => { @@ -95,7 +95,7 @@ describe('trackCumulativeLayoutShift', () => { clock.tick(THROTTLE_VIEW_UPDATE_PERIOD) expect(getViewUpdateCount()).toEqual(1) - expect(getViewUpdate(0).commonViewMetrics.cumulativeLayoutShift).toBe(0) + expect(getViewUpdate(0).commonViewMetrics.cumulativeLayoutShift?.value).toBe(0) }) it('should create a new session window if the gap is more than 1 second', () => { @@ -111,7 +111,7 @@ describe('trackCumulativeLayoutShift', () => { clock.tick(THROTTLE_VIEW_UPDATE_PERIOD) expect(getViewUpdateCount()).toEqual(2) - expect(getViewUpdate(1).commonViewMetrics.cumulativeLayoutShift).toBe(0.3) + expect(getViewUpdate(1).commonViewMetrics.cumulativeLayoutShift?.value).toBe(0.3) }) it('should create a new session window if the current session window is more than 5 second', () => { @@ -124,7 +124,7 @@ describe('trackCumulativeLayoutShift', () => { } // window 1: 0.5 | window 2: 0.1 clock.tick(THROTTLE_VIEW_UPDATE_PERIOD) expect(getViewUpdateCount()).toEqual(3) - expect(getViewUpdate(2).commonViewMetrics.cumulativeLayoutShift).toBe(0.5) + expect(getViewUpdate(2).commonViewMetrics.cumulativeLayoutShift?.value).toBe(0.5) }) it('should get the max value sessions', () => { @@ -145,7 +145,7 @@ describe('trackCumulativeLayoutShift', () => { clock.tick(THROTTLE_VIEW_UPDATE_PERIOD) expect(getViewUpdateCount()).toEqual(3) - expect(getViewUpdate(2).commonViewMetrics.cumulativeLayoutShift).toBe(0.5) + expect(getViewUpdate(2).commonViewMetrics.cumulativeLayoutShift?.value).toBe(0.5) }) describe('cls target element', () => { @@ -164,7 +164,7 @@ describe('trackCumulativeLayoutShift', () => { newLayoutShift(lifeCycle, { sources: [{ node: textNode }, { node: divElement }, { node: textNode }] }) expect(getViewUpdateCount()).toEqual(1) - expect(getViewUpdate(0).commonViewMetrics.cumulativeLayoutShiftTargetSelector).toBe('#div-element') + expect(getViewUpdate(0).commonViewMetrics.cumulativeLayoutShift?.targetSelector).toBe('#div-element') }) it('should not return the target element selector when FF disabled', () => { @@ -176,7 +176,7 @@ describe('trackCumulativeLayoutShift', () => { newLayoutShift(lifeCycle, { sources: [{ node: divElement }] }) expect(getViewUpdateCount()).toEqual(1) - expect(getViewUpdate(0).commonViewMetrics.cumulativeLayoutShiftTargetSelector).toBe(undefined) + expect(getViewUpdate(0).commonViewMetrics.cumulativeLayoutShift?.targetSelector).toBe(undefined) }) }) }) diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackCumulativeLayoutShift.ts b/packages/rum-core/src/domain/view/viewMetrics/trackCumulativeLayoutShift.ts index 0f1afadf4d..acee753906 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackCumulativeLayoutShift.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackCumulativeLayoutShift.ts @@ -14,6 +14,11 @@ import { getSelectorFromElement } from '../../getSelectorFromElement' import type { WebVitalTelemetryDebug } from '../startWebVitalTelemetryDebug' import type { RumConfiguration } from '../../configuration' +export interface CumulativeLayoutShift { + value: number + targetSelector?: string +} + /** * Track the cumulative layout shifts (CLS). * Layout shifts are grouped into session windows. @@ -31,12 +36,11 @@ import type { RumConfiguration } from '../../configuration' * https://web.dev/evolving-cls/ * Reference implementation: https://github.com/GoogleChrome/web-vitals/blob/master/src/getCLS.ts */ - export function trackCumulativeLayoutShift( configuration: RumConfiguration, lifeCycle: LifeCycle, webVitalTelemetryDebug: WebVitalTelemetryDebug, - callback: (cumulativeLayoutShift: number, cumulativeLayoutShiftTargetSelector?: string) => void + callback: (cumulativeLayoutShift: CumulativeLayoutShift) => void ) { let maxClsValue = 0 @@ -58,7 +62,10 @@ export function trackCumulativeLayoutShift( cslTargetSelector = getSelectorFromElement(clsTarget, configuration.actionNameAttribute) } - callback(cls, cslTargetSelector) + callback({ + value: cls, + targetSelector: cslTargetSelector, + }) if (!clsAttributionCollected) { clsAttributionCollected = true diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackFirstInputTimings.ts b/packages/rum-core/src/domain/view/viewMetrics/trackFirstInputTimings.ts index 987e3d3114..c6fef75a44 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackFirstInputTimings.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackFirstInputTimings.ts @@ -9,6 +9,12 @@ import type { WebVitalTelemetryDebug } from '../startWebVitalTelemetryDebug' import { getSelectorFromElement } from '../../getSelectorFromElement' import type { FirstHidden } from './trackFirstHidden' +export interface FirstInputTimings { + firstInputDelay: Duration + firstInputTime: RelativeTime + firstInputTargetSelector?: string +} + /** * Track the first input occurring during the initial View to return: * - First Input Delay @@ -17,21 +23,12 @@ import type { FirstHidden } from './trackFirstHidden' * Documentation: https://web.dev/fid/ * Reference implementation: https://github.com/GoogleChrome/web-vitals/blob/master/src/getFID.ts */ - export function trackFirstInputTimings( lifeCycle: LifeCycle, configuration: RumConfiguration, webVitalTelemetryDebug: WebVitalTelemetryDebug, firstHidden: FirstHidden, - callback: ({ - firstInputDelay, - firstInputTime, - firstInputTargetSelector, - }: { - firstInputDelay: Duration - firstInputTime: RelativeTime - firstInputTargetSelector?: string - }) => void + callback: (firstInputTimings: FirstInputTimings) => void ) { const { unsubscribe: unsubscribeLifeCycle } = lifeCycle.subscribe( LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackInitialViewMetrics.spec.ts b/packages/rum-core/src/domain/view/viewMetrics/trackInitialViewMetrics.spec.ts index 52054846e4..565428d7ff 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackInitialViewMetrics.spec.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackInitialViewMetrics.spec.ts @@ -1,4 +1,4 @@ -import type { Duration } from '@datadog/browser-core' +import type { Duration, RelativeTime } from '@datadog/browser-core' import type { TestSetupBuilder } from '../../../../test' import { noopWebVitalTelemetryDebug, setup } from '../../../../test' import { LifeCycleEventType } from '../../lifeCycle' @@ -45,15 +45,19 @@ describe('trackInitialViewMetrics', () => { expect(scheduleViewUpdateSpy).toHaveBeenCalledTimes(3) expect(trackInitialViewMetricsResult.initialViewMetrics).toEqual({ - firstByte: 123 as Duration, - domComplete: 456 as Duration, - domContentLoaded: 345 as Duration, - domInteractive: 234 as Duration, + navigationTimings: { + firstByte: 123 as Duration, + domComplete: 456 as Duration, + domContentLoaded: 345 as Duration, + domInteractive: 234 as Duration, + loadEvent: 567 as Duration, + }, firstContentfulPaint: 123 as Duration, - firstInputDelay: 100 as Duration, - firstInputTime: 1000 as Duration, - firstInputTargetSelector: undefined, - loadEvent: 567 as Duration, + firstInputTimings: { + firstInputDelay: 100 as Duration, + firstInputTime: 1000 as RelativeTime, + firstInputTargetSelector: undefined, + }, }) }) diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackInitialViewMetrics.ts b/packages/rum-core/src/domain/view/viewMetrics/trackInitialViewMetrics.ts index 4fa93f81f7..148fa38ee7 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackInitialViewMetrics.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackInitialViewMetrics.ts @@ -1,26 +1,21 @@ import type { Duration } from '@datadog/browser-core' -import { assign } from '@datadog/browser-core' import type { RumConfiguration } from '../../configuration' import type { LifeCycle } from '../../lifeCycle' import type { WebVitalTelemetryDebug } from '../startWebVitalTelemetryDebug' import { trackFirstContentfulPaint } from './trackFirstContentfulPaint' +import type { FirstInputTimings } from './trackFirstInputTimings' import { trackFirstInputTimings } from './trackFirstInputTimings' +import type { NavigationTimings } from './trackNavigationTimings' import { trackNavigationTimings } from './trackNavigationTimings' +import type { LargestContentfulPaint } from './trackLargestContentfulPaint' import { trackLargestContentfulPaint } from './trackLargestContentfulPaint' import { trackFirstHidden } from './trackFirstHidden' export interface InitialViewMetrics { firstContentfulPaint?: Duration - firstByte?: Duration - domInteractive?: Duration - domContentLoaded?: Duration - domComplete?: Duration - loadEvent?: Duration - largestContentfulPaint?: Duration - largestContentfulPaintTargetSelector?: string - firstInputDelay?: Duration - firstInputTime?: Duration - firstInputTargetSelector?: string + navigationTimings?: NavigationTimings + largestContentfulPaint?: LargestContentfulPaint + firstInputTimings?: FirstInputTimings } export function trackInitialViewMetrics( @@ -32,30 +27,27 @@ export function trackInitialViewMetrics( ) { const initialViewMetrics: InitialViewMetrics = {} - function setMetrics(newMetrics: Partial) { - assign(initialViewMetrics, newMetrics) - scheduleViewUpdate() - } - const { stop: stopNavigationTracking } = trackNavigationTimings(lifeCycle, (navigationTimings) => { setLoadEvent(navigationTimings.loadEvent) - setMetrics(navigationTimings) + initialViewMetrics.navigationTimings = navigationTimings + scheduleViewUpdate() }) + const firstHidden = trackFirstHidden(configuration) - const { stop: stopFCPTracking } = trackFirstContentfulPaint(lifeCycle, firstHidden, (firstContentfulPaint) => - setMetrics({ firstContentfulPaint }) - ) + const { stop: stopFCPTracking } = trackFirstContentfulPaint(lifeCycle, firstHidden, (firstContentfulPaint) => { + initialViewMetrics.firstContentfulPaint = firstContentfulPaint + scheduleViewUpdate() + }) + const { stop: stopLCPTracking } = trackLargestContentfulPaint( lifeCycle, configuration, webVitalTelemetryDebug, firstHidden, window, - (largestContentfulPaint, largestContentfulPaintTargetSelector) => { - setMetrics({ - largestContentfulPaint, - largestContentfulPaintTargetSelector, - }) + (largestContentfulPaint) => { + initialViewMetrics.largestContentfulPaint = largestContentfulPaint + scheduleViewUpdate() } ) @@ -64,12 +56,9 @@ export function trackInitialViewMetrics( configuration, webVitalTelemetryDebug, firstHidden, - ({ firstInputDelay, firstInputTime, firstInputTargetSelector }) => { - setMetrics({ - firstInputDelay, - firstInputTime, - firstInputTargetSelector, - }) + (firstInputTimings) => { + initialViewMetrics.firstInputTimings = firstInputTimings + scheduleViewUpdate() } ) diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.spec.ts b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.spec.ts index 4179c19728..719e3abc63 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.spec.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.spec.ts @@ -81,15 +81,15 @@ describe('trackInteractionToNextPaint', () => { }) } expect(getInteractionToNextPaint()).toEqual({ - interactionToNextPaint: 98 as Duration, - interactionToNextPaintTargetSelector: undefined, + value: 98 as Duration, + targetSelector: undefined, }) }) it('should return 0 when an interaction happened without generating a performance event (interaction duration below 40ms)', () => { setupBuilder.build() interactionCountStub.setInteractionCount(1 as Duration) // assumes an interaction happened but no PERFORMANCE_ENTRIES_COLLECTED have been triggered - expect(getInteractionToNextPaint()).toEqual({ interactionToNextPaint: 0 as Duration }) + expect(getInteractionToNextPaint()).toEqual({ value: 0 as Duration }) }) it('should take first-input entry into account', () => { @@ -99,8 +99,8 @@ describe('trackInteractionToNextPaint', () => { entryType: RumPerformanceEntryType.FIRST_INPUT, }) expect(getInteractionToNextPaint()).toEqual({ - interactionToNextPaint: 40 as Duration, - interactionToNextPaintTargetSelector: undefined, + value: 40 as Duration, + targetSelector: undefined, }) }) @@ -115,8 +115,8 @@ describe('trackInteractionToNextPaint', () => { } // the p98 return 100 which shows that the entry has been updated expect(getInteractionToNextPaint()).toEqual({ - interactionToNextPaint: 100 as Duration, - interactionToNextPaintTargetSelector: undefined, + value: 100 as Duration, + targetSelector: undefined, }) }) @@ -129,7 +129,7 @@ describe('trackInteractionToNextPaint', () => { target: appendElement('button', { id: 'inp-target-element' }), }) - expect(getInteractionToNextPaint()?.interactionToNextPaintTargetSelector).toEqual('#inp-target-element') + expect(getInteractionToNextPaint()?.targetSelector).toEqual('#inp-target-element') }) it("should not return the target selector if it's not a DOM element when FF web_vital_attribution is enabled", () => { @@ -141,7 +141,7 @@ describe('trackInteractionToNextPaint', () => { target: appendTextNode(''), }) - expect(getInteractionToNextPaint()?.interactionToNextPaintTargetSelector).toEqual(undefined) + expect(getInteractionToNextPaint()?.targetSelector).toEqual(undefined) }) it('should not return the target selector when FF web_vital_attribution is disabled', () => { @@ -152,7 +152,7 @@ describe('trackInteractionToNextPaint', () => { target: appendElement('button', { id: 'inp-target-element' }), }) - expect(getInteractionToNextPaint()?.interactionToNextPaintTargetSelector).toEqual(undefined) + expect(getInteractionToNextPaint()?.targetSelector).toEqual(undefined) }) }) diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts index d119d77d15..398a2ee272 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts @@ -12,6 +12,10 @@ import { getInteractionCount, initInteractionCountPolyfill } from './interaction // Arbitrary value to prevent unnecessary memory usage on views with lots of interactions. const MAX_INTERACTION_ENTRIES = 10 +export interface InteractionToNextPaint { + value: Duration + targetSelector?: string +} /** * Track the interaction to next paint (INP). * To avoid outliers, return the p98 worst interaction of the view. @@ -62,17 +66,17 @@ export function trackInteractionToNextPaint( }) return { - getInteractionToNextPaint: () => { + getInteractionToNextPaint: (): InteractionToNextPaint | undefined => { // If no INP duration where captured because of the performanceObserver 40ms threshold // but the view interaction count > 0 then report 0 if (interactionToNextPaint >= 0) { return { - interactionToNextPaint, - interactionToNextPaintTargetSelector, + value: interactionToNextPaint, + targetSelector: interactionToNextPaintTargetSelector, } } else if (getViewInteractionCount()) { return { - interactionToNextPaint: 0 as Duration, + value: 0 as Duration, } } }, diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackLargestContentfulPaint.spec.ts b/packages/rum-core/src/domain/view/viewMetrics/trackLargestContentfulPaint.spec.ts index 33f475d27c..c0daba0ba2 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackLargestContentfulPaint.spec.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackLargestContentfulPaint.spec.ts @@ -12,12 +12,13 @@ import type { TestSetupBuilder } from '../../../../test' import { appendElement, createPerformanceEntry, setup } from '../../../../test' import { LifeCycleEventType } from '../../lifeCycle' import type { RumConfiguration } from '../../configuration' +import type { LargestContentfulPaint } from './trackLargestContentfulPaint' import { LCP_MAXIMUM_DELAY, trackLargestContentfulPaint } from './trackLargestContentfulPaint' import { trackFirstHidden } from './trackFirstHidden' describe('trackLargestContentfulPaint', () => { let setupBuilder: TestSetupBuilder - let lcpCallback: jasmine.Spy<(value: RelativeTime, targetSelector?: string) => void> + let lcpCallback: jasmine.Spy<(lcp: LargestContentfulPaint) => void> let eventTarget: Window let configuration: RumConfiguration @@ -58,7 +59,7 @@ describe('trackLargestContentfulPaint', () => { ]) expect(lcpCallback).toHaveBeenCalledTimes(1 as RelativeTime) - expect(lcpCallback).toHaveBeenCalledWith(789 as RelativeTime, undefined) + expect(lcpCallback).toHaveBeenCalledWith({ value: 789 as RelativeTime, targetSelector: undefined }) }) it('should provide the largest contentful paint target selector if FF enabled', () => { @@ -72,7 +73,7 @@ describe('trackLargestContentfulPaint', () => { ]) expect(lcpCallback).toHaveBeenCalledTimes(1 as RelativeTime) - expect(lcpCallback).toHaveBeenCalledWith(789 as RelativeTime, '#lcp-target-element') + expect(lcpCallback).toHaveBeenCalledWith({ value: 789 as RelativeTime, targetSelector: '#lcp-target-element' }) }) it('should not provide the largest contentful paint target selector if FF disabled', () => { @@ -85,7 +86,7 @@ describe('trackLargestContentfulPaint', () => { ]) expect(lcpCallback).toHaveBeenCalledTimes(1 as RelativeTime) - expect(lcpCallback).toHaveBeenCalledWith(789 as RelativeTime, undefined) + expect(lcpCallback).toHaveBeenCalledWith({ value: 789 as RelativeTime, targetSelector: undefined }) }) it('should be discarded if it is reported after a user interaction', () => { diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackLargestContentfulPaint.ts b/packages/rum-core/src/domain/view/viewMetrics/trackLargestContentfulPaint.ts index a3f4598860..b565d5ecbb 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackLargestContentfulPaint.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackLargestContentfulPaint.ts @@ -18,6 +18,10 @@ import type { FirstHidden } from './trackFirstHidden' // It happens in some cases like sleep mode or some browser implementations export const LCP_MAXIMUM_DELAY = 10 * ONE_MINUTE +export interface LargestContentfulPaint { + value: RelativeTime + targetSelector?: string +} /** * Track the largest contentful paint (LCP) occurring during the initial View. This can yield * multiple values, only the most recent one should be used. @@ -30,7 +34,7 @@ export function trackLargestContentfulPaint( webVitalTelemetryDebug: WebVitalTelemetryDebug, firstHidden: FirstHidden, eventTarget: Window, - callback: (lcpTiming: RelativeTime, lcpTargetSelector?: string) => void + callback: (largestContentfulPaint: LargestContentfulPaint) => void ) { // Ignore entries that come after the first user interaction. According to the documentation, the // browser should not send largest-contentful-paint entries after a user interact with the page, @@ -64,7 +68,10 @@ export function trackLargestContentfulPaint( lcpTargetSelector = getSelectorFromElement(lcpEntry.element, configuration.actionNameAttribute) } - callback(lcpEntry.startTime, lcpTargetSelector) + callback({ + value: lcpEntry.startTime, + targetSelector: lcpTargetSelector, + }) webVitalTelemetryDebug.addWebVitalTelemetryDebug('LCP', lcpEntry.element, lcpEntry.startTime) } diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackNavigationTimings.spec.ts b/packages/rum-core/src/domain/view/viewMetrics/trackNavigationTimings.spec.ts index 69b408e31f..0f16c359d4 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackNavigationTimings.spec.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackNavigationTimings.spec.ts @@ -3,12 +3,12 @@ import type { TestSetupBuilder } from '../../../../test' import { setup } from '../../../../test' import { LifeCycleEventType } from '../../lifeCycle' import { FAKE_NAVIGATION_ENTRY } from '../setupViewTest.specHelper' -import type { InitialViewMetrics } from './trackInitialViewMetrics' +import type { NavigationTimings } from './trackNavigationTimings' import { trackNavigationTimings } from './trackNavigationTimings' describe('trackNavigationTimings', () => { let setupBuilder: TestSetupBuilder - let navigationTimingsCallback: jasmine.Spy<(value: Partial) => void> + let navigationTimingsCallback: jasmine.Spy<(timings: NavigationTimings) => void> beforeEach(() => { navigationTimingsCallback = jasmine.createSpy() From 4fabfc38313ccd000c66995f8c17ea700d9f1e42 Mon Sep 17 00:00:00 2001 From: Aymeric Mortemousque Date: Mon, 11 Sep 2023 17:20:24 +0200 Subject: [PATCH 11/18] Update rum event format --- .../domain/telemetry/telemetryEvent.types.ts | 4 ++++ packages/rum-core/src/rumEvent.types.ts | 18 +++++++++++++++++- rum-events-format | 2 +- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/core/src/domain/telemetry/telemetryEvent.types.ts b/packages/core/src/domain/telemetry/telemetryEvent.types.ts index e53a6ab504..03c7200cd7 100644 --- a/packages/core/src/domain/telemetry/telemetryEvent.types.ts +++ b/packages/core/src/domain/telemetry/telemetryEvent.types.ts @@ -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 */ diff --git a/packages/rum-core/src/rumEvent.types.ts b/packages/rum-core/src/rumEvent.types.ts index f4110e2255..66b71fea0c 100644 --- a/packages/rum-core/src/rumEvent.types.ts +++ b/packages/rum-core/src/rumEvent.types.ts @@ -567,6 +567,10 @@ export type RumViewEvent = CommonProperties & { * Duration in ns to the largest contentful paint */ readonly largest_contentful_paint?: number + /** + * CSS selector path of the largest contentful paint element + */ + readonly largest_contentful_paint_target_selector?: string /** * Duration in ns of the first input event delay */ @@ -575,14 +579,26 @@ export type RumViewEvent = CommonProperties & { * Duration in ns to the first input */ readonly first_input_time?: number + /** + * CSS selector path of the first input target element + */ + readonly first_input_target_selector?: string /** * Longest duration in ns between an interaction and the next paint */ readonly interaction_to_next_paint?: number + /** + * CSS selector path of the interacted element corresponding to INP + */ + readonly interaction_to_next_paint_target_selector?: string /** * Total layout shift score that occurred on the view */ readonly cumulative_layout_shift?: number + /** + * CSS selector path of the first element (in document order) of the largest layout shift contributing to CLS + */ + readonly cumulative_layout_shift_target_selector?: string /** * Duration in ns to the complete parsing and loading of the document and its sub resources */ @@ -1102,7 +1118,7 @@ export interface CommonProperties { /** * The percentage of sessions with RUM & Session Replay pricing tracked */ - readonly session_replay_sample_rate: number + readonly session_replay_sample_rate?: number [k: string]: unknown } /** diff --git a/rum-events-format b/rum-events-format index c6565f8c54..a4b8a7b4ab 160000 --- a/rum-events-format +++ b/rum-events-format @@ -1 +1 @@ -Subproject commit c6565f8c5402ed63ca061f08851aea08c92e9f7f +Subproject commit a4b8a7b4abc785498ebd2e06845981926a2cacf8 From 23590808ab414a8b76a01480daeb8566bed223d4 Mon Sep 17 00:00:00 2001 From: Aymeric Mortemousque Date: Tue, 12 Sep 2023 10:03:46 +0200 Subject: [PATCH 12/18] Fix inp --- .../src/domain/view/viewMetrics/trackInteractionToNextPaint.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts index 398a2ee272..1b906fea28 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts @@ -61,6 +61,8 @@ export function trackInteractionToNextPaint( newInteraction.target, configuration.actionNameAttribute ) + } else { + interactionToNextPaintTargetSelector = undefined } } }) From 115c4090af053e19e36d9ff3acff3d2a2540530d Mon Sep 17 00:00:00 2001 From: Aymeric Mortemousque Date: Tue, 12 Sep 2023 10:12:59 +0200 Subject: [PATCH 13/18] Rename firstInputTimings --- .../src/domain/view/viewCollection.spec.ts | 6 ++-- .../src/domain/view/viewCollection.ts | 6 ++-- .../trackFirstInputTimings.spec.ts | 31 +++++++------------ .../viewMetrics/trackFirstInputTimings.ts | 18 +++++------ .../trackInitialViewMetrics.spec.ts | 8 ++--- .../viewMetrics/trackInitialViewMetrics.ts | 12 +++---- 6 files changed, 36 insertions(+), 45 deletions(-) diff --git a/packages/rum-core/src/domain/view/viewCollection.spec.ts b/packages/rum-core/src/domain/view/viewCollection.spec.ts index 14f2af73bd..f32bea457a 100644 --- a/packages/rum-core/src/domain/view/viewCollection.spec.ts +++ b/packages/rum-core/src/domain/view/viewCollection.spec.ts @@ -38,9 +38,9 @@ const VIEW: ViewEvent = { domInteractive: 10 as Duration, loadEvent: 10 as Duration, }, - firstInputTimings: { - firstInputDelay: 12 as Duration, - firstInputTime: 10 as RelativeTime, + firstInput: { + delay: 12 as Duration, + time: 10 as RelativeTime, }, firstContentfulPaint: 10 as Duration, largestContentfulPaint: { value: 10 as RelativeTime }, diff --git a/packages/rum-core/src/domain/view/viewCollection.ts b/packages/rum-core/src/domain/view/viewCollection.ts index 4b20dbf57f..800e335ab7 100644 --- a/packages/rum-core/src/domain/view/viewCollection.ts +++ b/packages/rum-core/src/domain/view/viewCollection.ts @@ -87,9 +87,9 @@ function processViewUpdate( count: view.eventCounts.errorCount, }, first_contentful_paint: toServerDuration(view.initialViewMetrics.firstContentfulPaint), - first_input_delay: toServerDuration(view.initialViewMetrics.firstInputTimings?.firstInputDelay), - first_input_time: toServerDuration(view.initialViewMetrics.firstInputTimings?.firstInputTime), - first_input_target_selector: view.initialViewMetrics.firstInputTimings?.firstInputTargetSelector, + first_input_delay: toServerDuration(view.initialViewMetrics.firstInput?.delay), + first_input_time: toServerDuration(view.initialViewMetrics.firstInput?.time), + first_input_target_selector: view.initialViewMetrics.firstInput?.targetSelector, interaction_to_next_paint: toServerDuration(view.commonViewMetrics.interactionToNextPaint?.value), interaction_to_next_paint_target_selector: view.commonViewMetrics.interactionToNextPaint?.targetSelector, is_active: view.isActive, diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackFirstInputTimings.spec.ts b/packages/rum-core/src/domain/view/viewMetrics/trackFirstInputTimings.spec.ts index 77daacd2e7..97071d610a 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackFirstInputTimings.spec.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackFirstInputTimings.spec.ts @@ -12,22 +12,13 @@ import { appendElement, appendTextNode, createPerformanceEntry, setup } from '.. import { LifeCycleEventType } from '../../lifeCycle' import type { RumConfiguration } from '../../configuration' import { RumPerformanceEntryType } from '../../../browser/performanceCollection' -import { trackFirstInputTimings } from './trackFirstInputTimings' +import type { FirstInput } from './trackFirstInputTimings' +import { trackFirstInput } from './trackFirstInputTimings' import { trackFirstHidden } from './trackFirstHidden' describe('firstInputTimings', () => { let setupBuilder: TestSetupBuilder - let fitCallback: jasmine.Spy< - ({ - firstInputDelay, - firstInputTime, - firstInputTargetSelector, - }: { - firstInputDelay: number - firstInputTime: number - firstInputTargetSelector?: string - }) => void - > + let fitCallback: jasmine.Spy<(firstInput: FirstInput) => void> let configuration: RumConfiguration beforeEach(() => { @@ -36,7 +27,7 @@ describe('firstInputTimings', () => { setupBuilder = setup().beforeBuild(({ lifeCycle }) => { const firstHidden = trackFirstHidden(configuration) - const firstInputTimings = trackFirstInputTimings( + const firstInputTimings = trackFirstInput( lifeCycle, configuration, { addWebVitalTelemetryDebug: noop }, @@ -68,9 +59,9 @@ describe('firstInputTimings', () => { expect(fitCallback).toHaveBeenCalledTimes(1) expect(fitCallback).toHaveBeenCalledWith({ - firstInputDelay: 100, - firstInputTime: 1000, - firstInputTargetSelector: undefined, + delay: 100 as Duration, + time: 1000 as RelativeTime, + targetSelector: undefined, }) }) @@ -87,7 +78,7 @@ describe('firstInputTimings', () => { expect(fitCallback).toHaveBeenCalledTimes(1) expect(fitCallback).toHaveBeenCalledWith( jasmine.objectContaining({ - firstInputTargetSelector: '#fid-target-element', + targetSelector: '#fid-target-element', }) ) }) @@ -105,7 +96,7 @@ describe('firstInputTimings', () => { expect(fitCallback).toHaveBeenCalledTimes(1) expect(fitCallback).toHaveBeenCalledWith( jasmine.objectContaining({ - firstInputTargetSelector: undefined, + targetSelector: undefined, }) ) }) @@ -135,8 +126,8 @@ describe('firstInputTimings', () => { expect(fitCallback).toHaveBeenCalledTimes(1) expect(fitCallback).toHaveBeenCalledWith( jasmine.objectContaining({ - firstInputDelay: 0, - firstInputTime: 1000, + delay: 0, + time: 1000, }) ) }) diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackFirstInputTimings.ts b/packages/rum-core/src/domain/view/viewMetrics/trackFirstInputTimings.ts index c6fef75a44..6897794435 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackFirstInputTimings.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackFirstInputTimings.ts @@ -9,10 +9,10 @@ import type { WebVitalTelemetryDebug } from '../startWebVitalTelemetryDebug' import { getSelectorFromElement } from '../../getSelectorFromElement' import type { FirstHidden } from './trackFirstHidden' -export interface FirstInputTimings { - firstInputDelay: Duration - firstInputTime: RelativeTime - firstInputTargetSelector?: string +export interface FirstInput { + delay: Duration + time: RelativeTime + targetSelector?: string } /** @@ -23,12 +23,12 @@ export interface FirstInputTimings { * Documentation: https://web.dev/fid/ * Reference implementation: https://github.com/GoogleChrome/web-vitals/blob/master/src/getFID.ts */ -export function trackFirstInputTimings( +export function trackFirstInput( lifeCycle: LifeCycle, configuration: RumConfiguration, webVitalTelemetryDebug: WebVitalTelemetryDebug, firstHidden: FirstHidden, - callback: (firstInputTimings: FirstInputTimings) => void + callback: (firstInput: FirstInput) => void ) { const { unsubscribe: unsubscribeLifeCycle } = lifeCycle.subscribe( LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, @@ -53,9 +53,9 @@ export function trackFirstInputTimings( callback({ // Ensure firstInputDelay to be positive, see // https://bugs.chromium.org/p/chromium/issues/detail?id=1185815 - firstInputDelay: firstInputDelay >= 0 ? firstInputDelay : (0 as Duration), - firstInputTime: firstInputEntry.startTime, - firstInputTargetSelector, + delay: firstInputDelay >= 0 ? firstInputDelay : (0 as Duration), + time: firstInputEntry.startTime, + targetSelector: firstInputTargetSelector, }) webVitalTelemetryDebug.addWebVitalTelemetryDebug('FID', firstInputEntry.target, firstInputEntry.startTime) diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackInitialViewMetrics.spec.ts b/packages/rum-core/src/domain/view/viewMetrics/trackInitialViewMetrics.spec.ts index 565428d7ff..720f3adc07 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackInitialViewMetrics.spec.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackInitialViewMetrics.spec.ts @@ -53,10 +53,10 @@ describe('trackInitialViewMetrics', () => { loadEvent: 567 as Duration, }, firstContentfulPaint: 123 as Duration, - firstInputTimings: { - firstInputDelay: 100 as Duration, - firstInputTime: 1000 as RelativeTime, - firstInputTargetSelector: undefined, + firstInput: { + delay: 100 as Duration, + time: 1000 as RelativeTime, + targetSelector: undefined, }, }) }) diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackInitialViewMetrics.ts b/packages/rum-core/src/domain/view/viewMetrics/trackInitialViewMetrics.ts index 148fa38ee7..174f7357b9 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackInitialViewMetrics.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackInitialViewMetrics.ts @@ -3,8 +3,8 @@ import type { RumConfiguration } from '../../configuration' import type { LifeCycle } from '../../lifeCycle' import type { WebVitalTelemetryDebug } from '../startWebVitalTelemetryDebug' import { trackFirstContentfulPaint } from './trackFirstContentfulPaint' -import type { FirstInputTimings } from './trackFirstInputTimings' -import { trackFirstInputTimings } from './trackFirstInputTimings' +import type { FirstInput } from './trackFirstInputTimings' +import { trackFirstInput } from './trackFirstInputTimings' import type { NavigationTimings } from './trackNavigationTimings' import { trackNavigationTimings } from './trackNavigationTimings' import type { LargestContentfulPaint } from './trackLargestContentfulPaint' @@ -15,7 +15,7 @@ export interface InitialViewMetrics { firstContentfulPaint?: Duration navigationTimings?: NavigationTimings largestContentfulPaint?: LargestContentfulPaint - firstInputTimings?: FirstInputTimings + firstInput?: FirstInput } export function trackInitialViewMetrics( @@ -51,13 +51,13 @@ export function trackInitialViewMetrics( } ) - const { stop: stopFIDTracking } = trackFirstInputTimings( + const { stop: stopFIDTracking } = trackFirstInput( lifeCycle, configuration, webVitalTelemetryDebug, firstHidden, - (firstInputTimings) => { - initialViewMetrics.firstInputTimings = firstInputTimings + (firstInput) => { + initialViewMetrics.firstInput = firstInput scheduleViewUpdate() } ) From bb0e1eefab41fc01d863f42914859cd697e35bda Mon Sep 17 00:00:00 2001 From: Aymeric Mortemousque Date: Tue, 12 Sep 2023 10:14:16 +0200 Subject: [PATCH 14/18] Rename FirstInputTimings file name --- ...trackFirstInputTimings.spec.ts => trackFirstInput.spec.ts} | 4 ++-- .../{trackFirstInputTimings.ts => trackFirstInput.ts} | 0 .../src/domain/view/viewMetrics/trackInitialViewMetrics.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) rename packages/rum-core/src/domain/view/viewMetrics/{trackFirstInputTimings.spec.ts => trackFirstInput.spec.ts} (97%) rename packages/rum-core/src/domain/view/viewMetrics/{trackFirstInputTimings.ts => trackFirstInput.ts} (100%) diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackFirstInputTimings.spec.ts b/packages/rum-core/src/domain/view/viewMetrics/trackFirstInput.spec.ts similarity index 97% rename from packages/rum-core/src/domain/view/viewMetrics/trackFirstInputTimings.spec.ts rename to packages/rum-core/src/domain/view/viewMetrics/trackFirstInput.spec.ts index 97071d610a..9044c742e9 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackFirstInputTimings.spec.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackFirstInput.spec.ts @@ -12,8 +12,8 @@ import { appendElement, appendTextNode, createPerformanceEntry, setup } from '.. import { LifeCycleEventType } from '../../lifeCycle' import type { RumConfiguration } from '../../configuration' import { RumPerformanceEntryType } from '../../../browser/performanceCollection' -import type { FirstInput } from './trackFirstInputTimings' -import { trackFirstInput } from './trackFirstInputTimings' +import type { FirstInput } from './trackFirstInput' +import { trackFirstInput } from './trackFirstInput' import { trackFirstHidden } from './trackFirstHidden' describe('firstInputTimings', () => { diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackFirstInputTimings.ts b/packages/rum-core/src/domain/view/viewMetrics/trackFirstInput.ts similarity index 100% rename from packages/rum-core/src/domain/view/viewMetrics/trackFirstInputTimings.ts rename to packages/rum-core/src/domain/view/viewMetrics/trackFirstInput.ts diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackInitialViewMetrics.ts b/packages/rum-core/src/domain/view/viewMetrics/trackInitialViewMetrics.ts index 174f7357b9..b3261fde8b 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackInitialViewMetrics.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackInitialViewMetrics.ts @@ -3,8 +3,8 @@ import type { RumConfiguration } from '../../configuration' import type { LifeCycle } from '../../lifeCycle' import type { WebVitalTelemetryDebug } from '../startWebVitalTelemetryDebug' import { trackFirstContentfulPaint } from './trackFirstContentfulPaint' -import type { FirstInput } from './trackFirstInputTimings' -import { trackFirstInput } from './trackFirstInputTimings' +import type { FirstInput } from './trackFirstInput' +import { trackFirstInput } from './trackFirstInput' import type { NavigationTimings } from './trackNavigationTimings' import { trackNavigationTimings } from './trackNavigationTimings' import type { LargestContentfulPaint } from './trackLargestContentfulPaint' From fbe0200005f0581fab050d2f337a0e0dfe07d925 Mon Sep 17 00:00:00 2001 From: Aymeric Mortemousque Date: Wed, 13 Sep 2023 09:31:48 +0200 Subject: [PATCH 15/18] Fix format --- .../src/domain/view/viewMetrics/trackInteractionToNextPaint.ts | 3 ++- .../src/domain/view/viewMetrics/trackLargestContentfulPaint.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts index 1b906fea28..ceb31518e8 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts @@ -2,7 +2,8 @@ import { noop, isExperimentalFeatureEnabled, ExperimentalFeature } from '@datado import type { Duration } from '@datadog/browser-core' import { supportPerformanceTimingEvent } from '../../../browser/performanceCollection' import type { RumFirstInputTiming, RumPerformanceEventTiming } from '../../../browser/performanceCollection' -import { LifeCycleEventType, type LifeCycle } from '../../lifeCycle' +import { LifeCycleEventType } from '../../lifeCycle' +import type { LifeCycle } from '../../lifeCycle' import { ViewLoadingType } from '../../../rawRumEvent.types' import { getSelectorFromElement } from '../../getSelectorFromElement' import { isElementNode } from '../../../browser/htmlDomUtils' diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackLargestContentfulPaint.ts b/packages/rum-core/src/domain/view/viewMetrics/trackLargestContentfulPaint.ts index b565d5ecbb..cb53e142c4 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackLargestContentfulPaint.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackLargestContentfulPaint.ts @@ -7,7 +7,8 @@ import { findLast, isExperimentalFeatureEnabled, } from '@datadog/browser-core' -import { LifeCycleEventType, type LifeCycle } from '../../lifeCycle' +import { LifeCycleEventType } from '../../lifeCycle' +import type { LifeCycle } from '../../lifeCycle' import type { RumConfiguration } from '../../configuration' import type { RumLargestContentfulPaintTiming } from '../../../browser/performanceCollection' import type { WebVitalTelemetryDebug } from '../startWebVitalTelemetryDebug' From df23557f8377cfb35f6c4ddf0d8cbcd772487c70 Mon Sep 17 00:00:00 2001 From: Aymeric Mortemousque Date: Wed, 13 Sep 2023 14:32:27 +0200 Subject: [PATCH 16/18] Fix typescript no-unsafe-enum-comparison --- packages/rum-core/src/browser/performanceCollection.ts | 6 +++--- .../rum-core/src/domain/longTask/longTaskCollection.ts | 3 ++- .../rum-core/src/domain/resource/resourceCollection.ts | 3 ++- .../view/viewMetrics/trackCumulativeLayoutShift.ts | 7 ++++--- .../view/viewMetrics/trackFirstContentfulPaint.ts | 3 ++- .../src/domain/view/viewMetrics/trackFirstInput.ts | 3 ++- .../view/viewMetrics/trackInteractionToNextPaint.ts | 10 +++++++--- .../view/viewMetrics/trackLargestContentfulPaint.ts | 3 ++- .../domain/view/viewMetrics/trackNavigationTimings.ts | 3 ++- packages/rum-core/src/domain/waitPageActivityEnd.ts | 7 ++++++- 10 files changed, 32 insertions(+), 16 deletions(-) diff --git a/packages/rum-core/src/browser/performanceCollection.ts b/packages/rum-core/src/browser/performanceCollection.ts index 89372a97e2..b37206db6e 100644 --- a/packages/rum-core/src/browser/performanceCollection.ts +++ b/packages/rum-core/src/browser/performanceCollection.ts @@ -138,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 && @@ -205,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]) }) diff --git a/packages/rum-core/src/domain/longTask/longTaskCollection.ts b/packages/rum-core/src/domain/longTask/longTaskCollection.ts index 996b2e96b9..f9022ec98f 100644 --- a/packages/rum-core/src/domain/longTask/longTaskCollection.ts +++ b/packages/rum-core/src/domain/longTask/longTaskCollection.ts @@ -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) diff --git a/packages/rum-core/src/domain/resource/resourceCollection.ts b/packages/rum-core/src/domain/resource/resourceCollection.ts index 7853a81158..7a4b37329f 100644 --- a/packages/rum-core/src/domain/resource/resourceCollection.ts +++ b/packages/rum-core/src/domain/resource/resourceCollection.ts @@ -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, @@ -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) diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackCumulativeLayoutShift.ts b/packages/rum-core/src/domain/view/viewMetrics/trackCumulativeLayoutShift.ts index acee753906..232e2b888c 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackCumulativeLayoutShift.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackCumulativeLayoutShift.ts @@ -9,7 +9,8 @@ import { import { isElementNode } from '../../../browser/htmlDomUtils' import type { LifeCycle } from '../../lifeCycle' import { LifeCycleEventType } from '../../lifeCycle' -import { supportPerformanceTimingEvent, type RumLayoutShiftTiming } from '../../../browser/performanceCollection' +import type { RumLayoutShiftTiming } from '../../../browser/performanceCollection' +import { supportPerformanceTimingEvent, RumPerformanceEntryType } from '../../../browser/performanceCollection' import { getSelectorFromElement } from '../../getSelectorFromElement' import type { WebVitalTelemetryDebug } from '../startWebVitalTelemetryDebug' import type { RumConfiguration } from '../../configuration' @@ -49,7 +50,7 @@ export function trackCumulativeLayoutShift( const { unsubscribe: stop } = lifeCycle.subscribe(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, (entries) => { for (const entry of entries) { - if (entry.entryType === 'layout-shift' && !entry.hadRecentInput) { + if (entry.entryType === RumPerformanceEntryType.LAYOUT_SHIFT && !entry.hadRecentInput) { window.update(entry) if (window.value() > maxClsValue) { @@ -134,5 +135,5 @@ function slidingSessionWindow() { * Check whether `layout-shift` is supported by the browser. */ export function isLayoutShiftSupported() { - return supportPerformanceTimingEvent('layout-shift') + return supportPerformanceTimingEvent(RumPerformanceEntryType.LAYOUT_SHIFT) } diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackFirstContentfulPaint.ts b/packages/rum-core/src/domain/view/viewMetrics/trackFirstContentfulPaint.ts index 0697c495cd..3536caad19 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackFirstContentfulPaint.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackFirstContentfulPaint.ts @@ -3,6 +3,7 @@ import { ONE_MINUTE, find } from '@datadog/browser-core' import type { LifeCycle } from '../../lifeCycle' import { LifeCycleEventType } from '../../lifeCycle' import type { RumPerformancePaintTiming } from '../../../browser/performanceCollection' +import { RumPerformanceEntryType } from '../../../browser/performanceCollection' import type { FirstHidden } from './trackFirstHidden' // Discard FCP timings above a certain delay to avoid incorrect data @@ -20,7 +21,7 @@ export function trackFirstContentfulPaint( const fcpEntry = find( entries, (entry): entry is RumPerformancePaintTiming => - entry.entryType === 'paint' && + entry.entryType === RumPerformanceEntryType.PAINT && entry.name === 'first-contentful-paint' && entry.startTime < firstHidden.timeStamp && entry.startTime < FCP_MAXIMUM_DELAY diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackFirstInput.ts b/packages/rum-core/src/domain/view/viewMetrics/trackFirstInput.ts index 6897794435..a5b2224982 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackFirstInput.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackFirstInput.ts @@ -4,6 +4,7 @@ import { isElementNode } from '../../../browser/htmlDomUtils' import type { RumConfiguration } from '../../configuration' import type { LifeCycle } from '../../lifeCycle' import { LifeCycleEventType } from '../../lifeCycle' +import { RumPerformanceEntryType } from '../../../browser/performanceCollection' import type { RumFirstInputTiming } from '../../../browser/performanceCollection' import type { WebVitalTelemetryDebug } from '../startWebVitalTelemetryDebug' import { getSelectorFromElement } from '../../getSelectorFromElement' @@ -36,7 +37,7 @@ export function trackFirstInput( const firstInputEntry = find( entries, (entry): entry is RumFirstInputTiming => - entry.entryType === 'first-input' && entry.startTime < firstHidden.timeStamp + entry.entryType === RumPerformanceEntryType.FIRST_INPUT && entry.startTime < firstHidden.timeStamp ) if (firstInputEntry) { const firstInputDelay = elapsed(firstInputEntry.startTime, firstInputEntry.processingStart) diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts index ceb31518e8..0221b14901 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.ts @@ -1,6 +1,6 @@ import { noop, isExperimentalFeatureEnabled, ExperimentalFeature } from '@datadog/browser-core' import type { Duration } from '@datadog/browser-core' -import { supportPerformanceTimingEvent } from '../../../browser/performanceCollection' +import { RumPerformanceEntryType, supportPerformanceTimingEvent } from '../../../browser/performanceCollection' import type { RumFirstInputTiming, RumPerformanceEventTiming } from '../../../browser/performanceCollection' import { LifeCycleEventType } from '../../lifeCycle' import type { LifeCycle } from '../../lifeCycle' @@ -45,7 +45,11 @@ export function trackInteractionToNextPaint( const { unsubscribe: stop } = lifeCycle.subscribe(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, (entries) => { for (const entry of entries) { - if ((entry.entryType === 'event' || entry.entryType === 'first-input') && entry.interactionId) { + if ( + (entry.entryType === RumPerformanceEntryType.EVENT || + entry.entryType === RumPerformanceEntryType.FIRST_INPUT) && + entry.interactionId + ) { longestInteractions.process(entry) } } @@ -141,7 +145,7 @@ export function trackViewInteractionCount(viewLoadingType: ViewLoadingType) { export function isInteractionToNextPaintSupported() { return ( - supportPerformanceTimingEvent('event') && + supportPerformanceTimingEvent(RumPerformanceEntryType.EVENT) && window.PerformanceEventTiming && 'interactionId' in PerformanceEventTiming.prototype ) diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackLargestContentfulPaint.ts b/packages/rum-core/src/domain/view/viewMetrics/trackLargestContentfulPaint.ts index cb53e142c4..97810bff8f 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackLargestContentfulPaint.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackLargestContentfulPaint.ts @@ -10,6 +10,7 @@ import { import { LifeCycleEventType } from '../../lifeCycle' import type { LifeCycle } from '../../lifeCycle' import type { RumConfiguration } from '../../configuration' +import { RumPerformanceEntryType } from '../../../browser/performanceCollection' import type { RumLargestContentfulPaintTiming } from '../../../browser/performanceCollection' import type { WebVitalTelemetryDebug } from '../startWebVitalTelemetryDebug' import { getSelectorFromElement } from '../../getSelectorFromElement' @@ -57,7 +58,7 @@ export function trackLargestContentfulPaint( const lcpEntry = findLast( entries, (entry): entry is RumLargestContentfulPaintTiming => - entry.entryType === 'largest-contentful-paint' && + entry.entryType === RumPerformanceEntryType.LARGEST_CONTENTFUL_PAINT && entry.startTime < firstInteractionTimestamp && entry.startTime < firstHidden.timeStamp && entry.startTime < LCP_MAXIMUM_DELAY diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackNavigationTimings.ts b/packages/rum-core/src/domain/view/viewMetrics/trackNavigationTimings.ts index 1de0b3fc47..9d58dd950a 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackNavigationTimings.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackNavigationTimings.ts @@ -1,5 +1,6 @@ import type { Duration } from '@datadog/browser-core' import { relativeNow } from '@datadog/browser-core' +import { RumPerformanceEntryType } from '../../../browser/performanceCollection' import type { LifeCycle } from '../../lifeCycle' import { LifeCycleEventType } from '../../lifeCycle' @@ -14,7 +15,7 @@ export interface NavigationTimings { export function trackNavigationTimings(lifeCycle: LifeCycle, callback: (timings: NavigationTimings) => void) { const { unsubscribe: stop } = lifeCycle.subscribe(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, (entries) => { for (const entry of entries) { - if (entry.entryType === 'navigation') { + if (entry.entryType === RumPerformanceEntryType.NAVIGATION) { callback({ domComplete: entry.domComplete, domContentLoaded: entry.domContentLoadedEventEnd, diff --git a/packages/rum-core/src/domain/waitPageActivityEnd.ts b/packages/rum-core/src/domain/waitPageActivityEnd.ts index 7db2063b72..85ac72cfff 100644 --- a/packages/rum-core/src/domain/waitPageActivityEnd.ts +++ b/packages/rum-core/src/domain/waitPageActivityEnd.ts @@ -8,6 +8,7 @@ import { setTimeout, clearTimeout, } from '@datadog/browser-core' +import { RumPerformanceEntryType } from '../browser/performanceCollection' import type { RumConfiguration } from './configuration' import type { LifeCycle } from './lifeCycle' import { LifeCycleEventType } from './lifeCycle' @@ -127,7 +128,11 @@ export function createPageActivityObservable( subscriptions.push( domMutationObservable.subscribe(notifyPageActivity), lifeCycle.subscribe(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, (entries) => { - if (entries.some((entry) => entry.entryType === 'resource' && !isExcludedUrl(configuration, entry.name))) { + if ( + entries.some( + (entry) => entry.entryType === RumPerformanceEntryType.RESOURCE && !isExcludedUrl(configuration, entry.name) + ) + ) { notifyPageActivity() } }), From 09b190527c186578bf1e3d4fb0e2c65ea183f3a7 Mon Sep 17 00:00:00 2001 From: Aymeric Mortemousque Date: Thu, 14 Sep 2023 14:21:37 +0200 Subject: [PATCH 17/18] Fix perf event filtering --- packages/rum-core/src/browser/performanceCollection.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rum-core/src/browser/performanceCollection.ts b/packages/rum-core/src/browser/performanceCollection.ts index b37206db6e..a3d1f247d4 100644 --- a/packages/rum-core/src/browser/performanceCollection.ts +++ b/packages/rum-core/src/browser/performanceCollection.ts @@ -367,7 +367,7 @@ function handleRumPerformanceEntries( entries: Array ) { const rumPerformanceEntries = entries.filter((entry): entry is RumPerformanceEntry => - objectHasValue(RumPerformanceEntryType, entry) + objectHasValue(RumPerformanceEntryType, entry.entryType) ) const rumAllowedPerformanceEntries = rumPerformanceEntries.filter( From d54c7eff1765406d3d15976df79f70689868e7b7 Mon Sep 17 00:00:00 2001 From: Aymeric Mortemousque Date: Thu, 14 Sep 2023 14:34:54 +0200 Subject: [PATCH 18/18] Remove newLayoutShift helper --- .../trackCumulativeLayoutShift.spec.ts | 91 +++++++++++++------ 1 file changed, 62 insertions(+), 29 deletions(-) diff --git a/packages/rum-core/src/domain/view/viewMetrics/trackCumulativeLayoutShift.spec.ts b/packages/rum-core/src/domain/view/viewMetrics/trackCumulativeLayoutShift.spec.ts index 6b1008c5a2..b93d0d3fbf 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackCumulativeLayoutShift.spec.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackCumulativeLayoutShift.spec.ts @@ -1,12 +1,11 @@ import { ExperimentalFeature, addExperimentalFeatures, resetExperimentalFeatures } from '@datadog/browser-core' import type { TestSetupBuilder } from '../../../../test' import { appendElement, appendTextNode, createPerformanceEntry, setup } from '../../../../test' -import type { LifeCycle } from '../../lifeCycle' import { LifeCycleEventType } from '../../lifeCycle' import { THROTTLE_VIEW_UPDATE_PERIOD } from '../trackViews' import type { ViewTest } from '../setupViewTest.specHelper' import { setupViewTest } from '../setupViewTest.specHelper' -import { RumPerformanceEntryType, type RumLayoutShiftTiming } from '../../../browser/performanceCollection' +import { RumPerformanceEntryType } from '../../../browser/performanceCollection' describe('trackCumulativeLayoutShift', () => { let setupBuilder: TestSetupBuilder @@ -14,12 +13,6 @@ describe('trackCumulativeLayoutShift', () => { let isLayoutShiftSupported: boolean let originalSupportedEntryTypes: PropertyDescriptor | undefined - function newLayoutShift(lifeCycle: LifeCycle, overrides: Partial) { - lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [ - createPerformanceEntry(RumPerformanceEntryType.LAYOUT_SHIFT, overrides), - ]) - } - beforeEach(() => { if (!('PerformanceObserver' in window) || !('supportedEntryTypes' in PerformanceObserver)) { pending('No PerformanceObserver support') @@ -65,9 +58,13 @@ describe('trackCumulativeLayoutShift', () => { it('should accumulate layout shift values for the first session window', () => { const { lifeCycle, clock } = setupBuilder.withFakeClock().build() const { getViewUpdate, getViewUpdateCount } = viewTest - newLayoutShift(lifeCycle, { value: 0.1 }) + lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [ + createPerformanceEntry(RumPerformanceEntryType.LAYOUT_SHIFT, { value: 0.1 }), + ]) clock.tick(100) - newLayoutShift(lifeCycle, { value: 0.2 }) + lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [ + createPerformanceEntry(RumPerformanceEntryType.LAYOUT_SHIFT, { value: 0.2 }), + ]) clock.tick(THROTTLE_VIEW_UPDATE_PERIOD) expect(getViewUpdateCount()).toEqual(2) @@ -77,9 +74,13 @@ describe('trackCumulativeLayoutShift', () => { it('should round the cumulative layout shift value to 4 decimals', () => { const { lifeCycle, clock } = setupBuilder.withFakeClock().build() const { getViewUpdate, getViewUpdateCount } = viewTest - newLayoutShift(lifeCycle, { value: 1.23456789 }) + lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [ + createPerformanceEntry(RumPerformanceEntryType.LAYOUT_SHIFT, { value: 1.23456789 }), + ]) clock.tick(100) - newLayoutShift(lifeCycle, { value: 1.11111111111 }) + lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [ + createPerformanceEntry(RumPerformanceEntryType.LAYOUT_SHIFT, { value: 1.11111111111 }), + ]) clock.tick(THROTTLE_VIEW_UPDATE_PERIOD) expect(getViewUpdateCount()).toEqual(2) @@ -90,8 +91,9 @@ describe('trackCumulativeLayoutShift', () => { const { lifeCycle, clock } = setupBuilder.withFakeClock().build() const { getViewUpdate, getViewUpdateCount } = viewTest - newLayoutShift(lifeCycle, { value: 0.1, hadRecentInput: true }) - + lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [ + createPerformanceEntry(RumPerformanceEntryType.LAYOUT_SHIFT, { value: 0.1, hadRecentInput: true }), + ]) clock.tick(THROTTLE_VIEW_UPDATE_PERIOD) expect(getViewUpdateCount()).toEqual(1) @@ -102,12 +104,17 @@ describe('trackCumulativeLayoutShift', () => { const { lifeCycle, clock } = setupBuilder.withFakeClock().build() const { getViewUpdate, getViewUpdateCount } = viewTest // first session window - newLayoutShift(lifeCycle, { value: 0.1 }) + lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [ + createPerformanceEntry(RumPerformanceEntryType.LAYOUT_SHIFT, { value: 0.1 }), + ]) clock.tick(100) - newLayoutShift(lifeCycle, { value: 0.2 }) - // second session window + lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [ + createPerformanceEntry(RumPerformanceEntryType.LAYOUT_SHIFT, { value: 0.2 }), + ]) // second session window clock.tick(1001) - newLayoutShift(lifeCycle, { value: 0.1 }) + lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [ + createPerformanceEntry(RumPerformanceEntryType.LAYOUT_SHIFT, { value: 0.1 }), + ]) clock.tick(THROTTLE_VIEW_UPDATE_PERIOD) expect(getViewUpdateCount()).toEqual(2) @@ -117,10 +124,14 @@ describe('trackCumulativeLayoutShift', () => { it('should create a new session window if the current session window is more than 5 second', () => { const { lifeCycle, clock } = setupBuilder.withFakeClock().build() const { getViewUpdate, getViewUpdateCount } = viewTest - newLayoutShift(lifeCycle, { value: 0 }) + lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [ + createPerformanceEntry(RumPerformanceEntryType.LAYOUT_SHIFT, { value: 0 }), + ]) for (let i = 0; i < 6; i += 1) { clock.tick(999) - newLayoutShift(lifeCycle, { value: 0.1 }) + lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [ + createPerformanceEntry(RumPerformanceEntryType.LAYOUT_SHIFT, { value: 0.1 }), + ]) } // window 1: 0.5 | window 2: 0.1 clock.tick(THROTTLE_VIEW_UPDATE_PERIOD) expect(getViewUpdateCount()).toEqual(3) @@ -131,17 +142,31 @@ describe('trackCumulativeLayoutShift', () => { const { lifeCycle, clock } = setupBuilder.withFakeClock().build() const { getViewUpdate, getViewUpdateCount } = viewTest // first session window - newLayoutShift(lifeCycle, { value: 0.1 }) - newLayoutShift(lifeCycle, { value: 0.2 }) + lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [ + createPerformanceEntry(RumPerformanceEntryType.LAYOUT_SHIFT, { value: 0.1 }), + ]) + lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [ + createPerformanceEntry(RumPerformanceEntryType.LAYOUT_SHIFT, { value: 0.2 }), + ]) // second session window clock.tick(5001) - newLayoutShift(lifeCycle, { value: 0.1 }) - newLayoutShift(lifeCycle, { value: 0.2 }) - newLayoutShift(lifeCycle, { value: 0.2 }) + lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [ + createPerformanceEntry(RumPerformanceEntryType.LAYOUT_SHIFT, { value: 0.1 }), + ]) + lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [ + createPerformanceEntry(RumPerformanceEntryType.LAYOUT_SHIFT, { value: 0.2 }), + ]) + lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [ + createPerformanceEntry(RumPerformanceEntryType.LAYOUT_SHIFT, { value: 0.2 }), + ]) // third session window clock.tick(5001) - newLayoutShift(lifeCycle, { value: 0.2 }) - newLayoutShift(lifeCycle, { value: 0.2 }) + lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [ + createPerformanceEntry(RumPerformanceEntryType.LAYOUT_SHIFT, { value: 0.2 }), + ]) + lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [ + createPerformanceEntry(RumPerformanceEntryType.LAYOUT_SHIFT, { value: 0.2 }), + ]) clock.tick(THROTTLE_VIEW_UPDATE_PERIOD) expect(getViewUpdateCount()).toEqual(3) @@ -161,7 +186,11 @@ describe('trackCumulativeLayoutShift', () => { const textNode = appendTextNode('') const divElement = appendElement('div', { id: 'div-element' }) - newLayoutShift(lifeCycle, { sources: [{ node: textNode }, { node: divElement }, { node: textNode }] }) + lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [ + createPerformanceEntry(RumPerformanceEntryType.LAYOUT_SHIFT, { + sources: [{ node: textNode }, { node: divElement }, { node: textNode }], + }), + ]) expect(getViewUpdateCount()).toEqual(1) expect(getViewUpdate(0).commonViewMetrics.cumulativeLayoutShift?.targetSelector).toBe('#div-element') @@ -173,7 +202,11 @@ describe('trackCumulativeLayoutShift', () => { const divElement = appendElement('div', { id: 'div-element' }) - newLayoutShift(lifeCycle, { sources: [{ node: divElement }] }) + lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [ + createPerformanceEntry(RumPerformanceEntryType.LAYOUT_SHIFT, { + sources: [{ node: divElement }], + }), + ]) expect(getViewUpdateCount()).toEqual(1) expect(getViewUpdate(0).commonViewMetrics.cumulativeLayoutShift?.targetSelector).toBe(undefined)