From b43b54bd974409bcd7a2b859e699cc3025c21a5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Zugmeyer?= Date: Thu, 12 Nov 2020 15:18:33 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20[RUMF-775]=20implement=20Largest=20?= =?UTF-8?q?Contentful=20Paint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/src/tools/utils.ts | 1 + .../rum/src/browser/performanceCollection.ts | 11 ++- .../view/trackTimings.spec.ts | 74 ++++++++++++++++++- .../rumEventsCollection/view/trackTimings.ts | 47 ++++++++++++ .../view/trackViews.spec.ts | 14 +++- .../view/viewCollection.spec.ts | 3 + .../view/viewCollection.ts | 1 + packages/rum/src/typesV2.ts | 1 + 8 files changed, 146 insertions(+), 6 deletions(-) diff --git a/packages/core/src/tools/utils.ts b/packages/core/src/tools/utils.ts index d49b751f69..1b8205e379 100644 --- a/packages/core/src/tools/utils.ts +++ b/packages/core/src/tools/utils.ts @@ -15,6 +15,7 @@ export enum DOM_EVENT { TOUCH_START = 'touchstart', VISIBILITY_CHANGE = 'visibilitychange', DOM_CONTENT_LOADED = 'DOMContentLoaded', + POINTER_DOWN = 'pointerdown', HASH_CHANGE = 'hashchange', PAGE_HIDE = 'pagehide', } diff --git a/packages/rum/src/browser/performanceCollection.ts b/packages/rum/src/browser/performanceCollection.ts index d3f9719721..3ede835cd0 100644 --- a/packages/rum/src/browser/performanceCollection.ts +++ b/packages/rum/src/browser/performanceCollection.ts @@ -49,11 +49,17 @@ export interface RumPerformanceNavigationTiming { loadEventEnd: number } +export interface RumLargestContentfulPaintTiming { + entryType: 'largest-contentful-paint' + startTime: number +} + export type RumPerformanceEntry = | RumPerformanceResourceTiming | RumPerformanceLongTaskTiming | RumPerformancePaintTiming | RumPerformanceNavigationTiming + | RumLargestContentfulPaintTiming function supportPerformanceObject() { return window.performance !== undefined && 'getEntries' in performance @@ -79,7 +85,7 @@ export function startPerformanceCollection(lifeCycle: LifeCycle, configuration: const observer = new PerformanceObserver( monitor((entries) => handlePerformanceEntries(lifeCycle, configuration, entries.getEntries())) ) - const entryTypes = ['resource', 'navigation', 'longtask', 'paint'] + const entryTypes = ['resource', 'navigation', 'longtask', 'paint', 'largest-contentful-paint'] observer.observe({ entryTypes }) @@ -168,7 +174,8 @@ function handlePerformanceEntries(lifeCycle: LifeCycle, configuration: Configura entry.entryType === 'resource' || entry.entryType === 'navigation' || entry.entryType === 'paint' || - entry.entryType === 'longtask' + entry.entryType === 'longtask' || + entry.entryType === 'largest-contentful-paint' ) { handleRumPerformanceEntry(lifeCycle, configuration, entry as RumPerformanceEntry) } diff --git a/packages/rum/src/domain/rumEventsCollection/view/trackTimings.spec.ts b/packages/rum/src/domain/rumEventsCollection/view/trackTimings.spec.ts index 921a69b328..f045751c54 100644 --- a/packages/rum/src/domain/rumEventsCollection/view/trackTimings.spec.ts +++ b/packages/rum/src/domain/rumEventsCollection/view/trackTimings.spec.ts @@ -1,9 +1,19 @@ -import { restorePageVisibility, setPageVisibility } from '@datadog/browser-core' +import { createNewEvent, DOM_EVENT, restorePageVisibility, setPageVisibility } from '@datadog/browser-core' import { setup, TestSetupBuilder } from '../../../../test/specHelper' -import { RumPerformanceNavigationTiming, RumPerformancePaintTiming } from '../../../browser/performanceCollection' +import { + RumLargestContentfulPaintTiming, + RumPerformanceNavigationTiming, + RumPerformancePaintTiming, +} from '../../../browser/performanceCollection' import { LifeCycleEventType } from '../../lifeCycle' import { resetFirstHidden } from './trackFirstHidden' -import { Timings, trackFirstContentfulPaint, trackNavigationTimings, trackTimings } from './trackTimings' +import { + Timings, + trackFirstContentfulPaint, + trackLargestContentfulPaint, + trackNavigationTimings, + trackTimings, +} from './trackTimings' const FAKE_PAINT_ENTRY: RumPerformancePaintTiming = { entryType: 'paint', @@ -19,6 +29,11 @@ const FAKE_NAVIGATION_ENTRY: RumPerformanceNavigationTiming = { loadEventEnd: 567, } +const FAKE_LARGEST_CONTENTFUL_PAINT_ENTRY: RumLargestContentfulPaintTiming = { + entryType: 'largest-contentful-paint', + startTime: 789, +} + describe('trackTimings', () => { let setupBuilder: TestSetupBuilder let spy: jasmine.Spy<(value: Partial) => void> @@ -115,3 +130,56 @@ describe('trackFirstContentfulPaint', () => { expect(spy).not.toHaveBeenCalled() }) }) + +describe('largestContentfulPaint', () => { + let setupBuilder: TestSetupBuilder + let spy: jasmine.Spy<(value: number) => void> + let emitter: Element + + beforeEach(() => { + spy = jasmine.createSpy() + emitter = document.createElement('div') + setupBuilder = setup().beforeBuild(({ lifeCycle }) => { + return trackLargestContentfulPaint(lifeCycle, emitter, spy) + }) + resetFirstHidden() + }) + + afterEach(() => { + setupBuilder.cleanup() + restorePageVisibility() + resetFirstHidden() + }) + + it('should provide the largest contentful paint timing', () => { + const { lifeCycle } = setupBuilder.build() + + lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, FAKE_LARGEST_CONTENTFUL_PAINT_ENTRY) + expect(spy).toHaveBeenCalledTimes(1) + expect(spy).toHaveBeenCalledWith(789) + }) + + it('should not be present if it happens after a user interaction', () => { + const { lifeCycle } = setupBuilder.build() + + const event = createNewEvent(DOM_EVENT.KEY_DOWN) + Object.defineProperty(event, 'timeStamp', { + get() { + return 1 + }, + }) + emitter.dispatchEvent(event) + + lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, FAKE_LARGEST_CONTENTFUL_PAINT_ENTRY) + expect(spy).not.toHaveBeenCalled() + }) + + it('should not be present if the page is hidden', () => { + setPageVisibility('hidden') + const { lifeCycle } = setupBuilder.build() + + lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, FAKE_LARGEST_CONTENTFUL_PAINT_ENTRY) + + expect(spy).not.toHaveBeenCalled() + }) +}) diff --git a/packages/rum/src/domain/rumEventsCollection/view/trackTimings.ts b/packages/rum/src/domain/rumEventsCollection/view/trackTimings.ts index ac7fd5445c..b97b9bc784 100644 --- a/packages/rum/src/domain/rumEventsCollection/view/trackTimings.ts +++ b/packages/rum/src/domain/rumEventsCollection/view/trackTimings.ts @@ -1,3 +1,4 @@ +import { addEventListeners, DOM_EVENT, EventEmitter } from '@datadog/browser-core' import { LifeCycle, LifeCycleEventType } from '../../lifeCycle' import { trackFirstHidden } from './trackFirstHidden' @@ -7,6 +8,7 @@ export interface Timings { domContentLoaded?: number domComplete?: number loadEventEnd?: number + largestContentfulPaint?: number } export function trackTimings(lifeCycle: LifeCycle, callback: (timings: Timings) => void) { @@ -20,11 +22,17 @@ export function trackTimings(lifeCycle: LifeCycle, callback: (timings: Timings) const { stop: stopFCPTracking } = trackFirstContentfulPaint(lifeCycle, (firstContentfulPaint) => setTimings({ firstContentfulPaint }) ) + const { stop: stopLCPTracking } = trackLargestContentfulPaint(lifeCycle, window, (largestContentfulPaint) => { + setTimings({ + largestContentfulPaint, + }) + }) return { stop() { stopNavigationTracking() stopFCPTracking() + stopLCPTracking() }, } } @@ -57,3 +65,42 @@ export function trackFirstContentfulPaint(lifeCycle: LifeCycle, callback: (fcp: }) return { stop } } + +export function trackLargestContentfulPaint( + lifeCycle: LifeCycle, + emitter: EventEmitter, + callback: (value: number) => void +) { + const firstHidden = trackFirstHidden() + + // Ignore entries that come after the first user interaction + let firstInteractionTimestamp: number = Infinity + const { stop: stopEventListener } = addEventListeners( + emitter, + [DOM_EVENT.POINTER_DOWN, DOM_EVENT.KEY_DOWN, DOM_EVENT.SCROLL], + (event) => { + firstInteractionTimestamp = event.timeStamp + }, + { capture: true, once: true } + ) + + const { unsubscribe: unsubcribeLifeCycle } = lifeCycle.subscribe( + LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, + (entry) => { + if ( + entry.entryType === 'largest-contentful-paint' && + entry.startTime < firstInteractionTimestamp && + entry.startTime < firstHidden.timeStamp + ) { + callback(entry.startTime) + } + } + ) + + return { + stop() { + stopEventListener() + unsubcribeLifeCycle() + }, + } +} diff --git a/packages/rum/src/domain/rumEventsCollection/view/trackViews.spec.ts b/packages/rum/src/domain/rumEventsCollection/view/trackViews.spec.ts index a7c273a0b4..540496d435 100644 --- a/packages/rum/src/domain/rumEventsCollection/view/trackViews.spec.ts +++ b/packages/rum/src/domain/rumEventsCollection/view/trackViews.spec.ts @@ -1,6 +1,10 @@ import { createRawRumEvent } from '../../../../test/fixtures' import { setup, TestSetupBuilder } from '../../../../test/specHelper' -import { RumPerformanceNavigationTiming, RumPerformancePaintTiming } from '../../../browser/performanceCollection' +import { + RumLargestContentfulPaintTiming, + RumPerformanceNavigationTiming, + RumPerformancePaintTiming, +} from '../../../browser/performanceCollection' import { RawRumEvent, RumEventCategory } from '../../../types' import { RumEventType } from '../../../typesV2' import { LifeCycleEventType } from '../../lifeCycle' @@ -20,6 +24,10 @@ const FAKE_PAINT_ENTRY: RumPerformancePaintTiming = { name: 'first-contentful-paint', startTime: 123, } +const FAKE_LARGEST_CONTENTFUL_PAINT_ENTRY: RumLargestContentfulPaintTiming = { + entryType: 'largest-contentful-paint', + startTime: 789, +} const FAKE_NAVIGATION_ENTRY: RumPerformanceNavigationTiming = { domComplete: 456, domContentLoadedEventEnd: 345, @@ -491,6 +499,7 @@ describe('rum view measures', () => { expect(getViewEvent(0).timings).toEqual({}) lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, FAKE_PAINT_ENTRY) + lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, FAKE_LARGEST_CONTENTFUL_PAINT_ENTRY) lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, FAKE_NAVIGATION_ENTRY) expect(getHandledCount()).toEqual(1) @@ -502,6 +511,7 @@ describe('rum view measures', () => { domContentLoaded: 345, domInteractive: 234, firstContentfulPaint: 123, + largestContentfulPaint: 789, loadEventEnd: 567, }) expect(getViewEvent(2).timings).toEqual({}) @@ -525,6 +535,7 @@ describe('rum view measures', () => { expect(getHandledCount()).toEqual(3) lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, FAKE_PAINT_ENTRY) + lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, FAKE_LARGEST_CONTENTFUL_PAINT_ENTRY) lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, FAKE_NAVIGATION_ENTRY) clock.tick(THROTTLE_VIEW_UPDATE_PERIOD) @@ -552,6 +563,7 @@ describe('rum view measures', () => { domContentLoaded: 345, domInteractive: 234, firstContentfulPaint: 123, + largestContentfulPaint: 789, loadEventEnd: 567, }) }) diff --git a/packages/rum/src/domain/rumEventsCollection/view/viewCollection.spec.ts b/packages/rum/src/domain/rumEventsCollection/view/viewCollection.spec.ts index 193ab3f0bf..343f7ddef2 100644 --- a/packages/rum/src/domain/rumEventsCollection/view/viewCollection.spec.ts +++ b/packages/rum/src/domain/rumEventsCollection/view/viewCollection.spec.ts @@ -44,6 +44,7 @@ describe('viewCollection', () => { domContentLoaded: 10, domInteractive: 10, firstContentfulPaint: 10, + largestContentfulPaint: 10, loadEventEnd: 10, }, } @@ -117,6 +118,7 @@ describe('viewCollection V2', () => { domContentLoaded: 10, domInteractive: 10, firstContentfulPaint: 10, + largestContentfulPaint: 10, loadEventEnd: 10, }, } @@ -140,6 +142,7 @@ describe('viewCollection V2', () => { count: 10, }, firstContentfulPaint: 10 * 1e6, + largestContentfulPaint: 10 * 1e6, loadEventEnd: 10 * 1e6, loadingTime: 20 * 1e6, loadingType: ViewLoadingType.INITIAL_LOAD, diff --git a/packages/rum/src/domain/rumEventsCollection/view/viewCollection.ts b/packages/rum/src/domain/rumEventsCollection/view/viewCollection.ts index b91cb509f0..9eb0c1028c 100644 --- a/packages/rum/src/domain/rumEventsCollection/view/viewCollection.ts +++ b/packages/rum/src/domain/rumEventsCollection/view/viewCollection.ts @@ -61,6 +61,7 @@ function processViewUpdateV2(view: View) { count: view.eventCounts.errorCount, }, firstContentfulPaint: msToNs(view.timings.firstContentfulPaint), + largestContentfulPaint: msToNs(view.timings.largestContentfulPaint), loadEventEnd: msToNs(view.timings.loadEventEnd), loadingTime: msToNs(view.loadingTime), loadingType: view.loadingType, diff --git a/packages/rum/src/typesV2.ts b/packages/rum/src/typesV2.ts index 854d11502c..667be53f27 100644 --- a/packages/rum/src/typesV2.ts +++ b/packages/rum/src/typesV2.ts @@ -57,6 +57,7 @@ export interface RumViewEventV2 { view: { loadingType: ViewLoadingType firstContentfulPaint?: number + largestContentfulPaint?: number domInteractive?: number domContentLoaded?: number domComplete?: number