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..fbe4335a20 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 { 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 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), ]) } @@ -160,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', () => { @@ -177,10 +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 }] }) expect(getViewUpdateCount()).toEqual(1) @@ -191,9 +170,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 }] }) expect(getViewUpdateCount()).toEqual(1) 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..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,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 { appendElement, 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' @@ -30,29 +29,11 @@ describe('firstInputTimings', () => { }) => 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 }) => { const firstHidden = trackFirstHidden(configuration) const firstInputTimings = trackFirstInputTimings( @@ -74,7 +55,6 @@ describe('firstInputTimings', () => { afterEach(() => { setupBuilder.cleanup() - target.parentNode!.removeChild(target) restorePageVisibility() resetExperimentalFeatures() }) @@ -82,7 +62,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 +78,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: appendElement('button', { id: 'fid-target-element' }), + }), + ]) expect(fitCallback).toHaveBeenCalledTimes(1) expect(fitCallback).toHaveBeenCalledWith( @@ -110,7 +96,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 +106,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..c4ee7516d4 100644 --- a/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.spec.ts +++ b/packages/rum-core/src/domain/view/viewMetrics/trackInteractionToNextPaint.spec.ts @@ -1,16 +1,12 @@ 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 type { - BrowserWindow, - RumFirstInputTiming, - RumPerformanceEventTiming, +import { appendElement, createPerformanceEntry, setup } from '../../../../test' +import { + RumPerformanceEntryType, + type BrowserWindow, + type RumFirstInputTiming, + type RumPerformanceEventTiming, } from '../../../browser/performanceCollection' import { ViewLoadingType } from '../../../rawRumEvent.types' import type { LifeCycle } from '../../lifeCycle' @@ -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: appendElement('button', { id: 'inp-target-element' }), + }) 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: appendElement('button', { id: 'inp-target-element' }), + }) + + 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..33f475d27c 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 { appendElement, 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: appendElement('button', { id: 'lcp-target-element' }), + }), + ]) + 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: appendElement('button', { id: 'lcp-target-element' }), + }), + ]) + + 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/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/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, 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'