-
Notifications
You must be signed in to change notification settings - Fork 142
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
d504bde
commit 73c2fdc
Showing
11 changed files
with
359 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
40 changes: 40 additions & 0 deletions
40
packages/rum-core/src/domain/rumEventsCollection/view/interactionCountPolyfill.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
/** | ||
* interactionCount polyfill | ||
* Reference implementation: https://github.com/GoogleChrome/web-vitals/blob/main/src/lib/polyfills/interactionCountPolyfill.ts | ||
*/ | ||
|
||
import type { BrowserWindow, RumEventTiming, RumPerformanceObserver } from '../../../browser/performanceCollection' | ||
|
||
let observer: RumPerformanceObserver | undefined | ||
|
||
let interactionCountEstimate = 0 | ||
let minKnownInteractionId = Infinity | ||
let maxKnownInteractionId = 0 | ||
|
||
export function initInteractionCountPolyfill() { | ||
if ('interactionCount' in performance || observer) { | ||
return | ||
} | ||
|
||
observer = new (window as BrowserWindow).PerformanceObserver((entries: PerformanceObserverEntryList) => { | ||
entries.getEntries().forEach((e) => { | ||
const entry = e as unknown as RumEventTiming | ||
|
||
if (entry.interactionId) { | ||
minKnownInteractionId = Math.min(minKnownInteractionId, entry.interactionId) | ||
maxKnownInteractionId = Math.max(maxKnownInteractionId, entry.interactionId) | ||
|
||
interactionCountEstimate = maxKnownInteractionId ? (maxKnownInteractionId - minKnownInteractionId) / 7 + 1 : 0 | ||
} | ||
}) | ||
}) | ||
|
||
observer.observe({ type: 'event', buffered: true, durationThreshold: 0 }) | ||
} | ||
|
||
/** | ||
* Returns the `interactionCount` value using the native API (if available) | ||
* or the polyfill estimate in this module. | ||
*/ | ||
export const getInteractionCount = () => | ||
observer ? interactionCountEstimate : (window as BrowserWindow).performance.interactionCount! || 0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
153 changes: 153 additions & 0 deletions
153
packages/rum-core/src/domain/rumEventsCollection/view/trackInteractionToNextPaint.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
import type { Duration } from '@datadog/browser-core' | ||
import { | ||
ExperimentalFeature, | ||
addExperimentalFeatures, | ||
relativeNow, | ||
resetExperimentalFeatures, | ||
} from '@datadog/browser-core' | ||
import type { TestSetupBuilder } from '../../../../test' | ||
import { setup } from '../../../../test' | ||
import type { BrowserWindow, RumEventTiming, RumFirstInputTiming } from '../../../browser/performanceCollection' | ||
import { ViewLoadingType } from '../../../rawRumEvent.types' | ||
import type { LifeCycle } from '../../lifeCycle' | ||
import { LifeCycleEventType } from '../../lifeCycle' | ||
import { | ||
trackInteractionToNextPaint, | ||
trackViewInteractionCount, | ||
isInteractionToNextPaintSupported, | ||
} from './trackInteractionToNextPaint' | ||
|
||
describe('trackInteractionToNextPaint', () => { | ||
let setupBuilder: TestSetupBuilder | ||
let interactionCountStub: ReturnType<typeof subInteractionCount> | ||
let getInteractionToNextPaint: () => Duration | undefined | ||
|
||
function newInteraction( | ||
lifeCycle: LifeCycle, | ||
{ interactionId, duration = 40 as Duration, entryType = 'event' }: Partial<RumEventTiming | RumFirstInputTiming> | ||
) { | ||
if (interactionId) { | ||
interactionCountStub.incrementInteractionCount() | ||
} | ||
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [ | ||
{ | ||
entryType, | ||
startTime: relativeNow(), | ||
duration, | ||
interactionId, | ||
processingStart: relativeNow(), | ||
}, | ||
]) | ||
} | ||
|
||
beforeEach(() => { | ||
if (!isInteractionToNextPaintSupported()) { | ||
pending('No PerformanceObserver support') | ||
} | ||
|
||
interactionCountStub = subInteractionCount() | ||
setupBuilder = setup().beforeBuild(({ lifeCycle }) => { | ||
;({ getInteractionToNextPaint } = trackInteractionToNextPaint(ViewLoadingType.INITIAL_LOAD, lifeCycle)) | ||
}) | ||
}) | ||
|
||
afterEach(() => { | ||
interactionCountStub.clear() | ||
}) | ||
|
||
describe('if feature flag enabled', () => { | ||
beforeEach(() => { | ||
addExperimentalFeatures([ExperimentalFeature.INTERACTION_TO_NEXT_PAINT]) | ||
}) | ||
|
||
afterEach(() => { | ||
resetExperimentalFeatures() | ||
}) | ||
|
||
it('should return undefined when there are no interactions', () => { | ||
setupBuilder.build() | ||
expect(getInteractionToNextPaint()).toEqual(undefined) | ||
}) | ||
|
||
it('should ignore entries without interactionId', () => { | ||
const { lifeCycle } = setupBuilder.build() | ||
newInteraction(lifeCycle, { | ||
interactionId: undefined, | ||
}) | ||
expect(getInteractionToNextPaint()).toEqual(undefined) | ||
}) | ||
|
||
it('should return the p98 worst interaction', () => { | ||
const { lifeCycle } = setupBuilder.build() | ||
for (let index = 1; index <= 100; index++) { | ||
newInteraction(lifeCycle, { | ||
duration: index as Duration, | ||
interactionId: index, | ||
}) | ||
} | ||
expect(getInteractionToNextPaint()).toEqual(98 as Duration) | ||
}) | ||
|
||
it('should return 0 if no interaction is tracked (because the duration is 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) | ||
}) | ||
}) | ||
|
||
describe('if feature flag disabled', () => { | ||
it('should return undefined', () => { | ||
const { lifeCycle } = setupBuilder.build() | ||
newInteraction(lifeCycle, { | ||
interactionId: 1, | ||
}) | ||
expect(getInteractionToNextPaint()).toEqual(undefined) | ||
}) | ||
}) | ||
}) | ||
|
||
describe('trackViewInteractionCount', () => { | ||
let interactionCountStub: ReturnType<typeof subInteractionCount> | ||
|
||
beforeEach(() => { | ||
interactionCountStub = subInteractionCount() | ||
interactionCountStub.setInteractionCount(5 as Duration) | ||
}) | ||
afterEach(() => { | ||
interactionCountStub.clear() | ||
}) | ||
|
||
it('should count the interaction happening since the time origin when view loading type is initial_load', () => { | ||
const { getViewInteractionCount } = trackViewInteractionCount(ViewLoadingType.INITIAL_LOAD) | ||
|
||
expect(getViewInteractionCount()).toEqual(5) | ||
}) | ||
|
||
it('should count the interaction from the moment the function is called when view loading type is route_change', () => { | ||
const { getViewInteractionCount } = trackViewInteractionCount(ViewLoadingType.ROUTE_CHANGE) | ||
|
||
expect(getViewInteractionCount()).toEqual(0) | ||
}) | ||
}) | ||
|
||
function subInteractionCount() { | ||
let interactionCount = 0 | ||
const originalInteractionCount = Object.getOwnPropertyDescriptor(window.performance, 'interactionCount') | ||
Object.defineProperty(window.performance, 'interactionCount', { get: () => interactionCount, configurable: true }) | ||
|
||
return { | ||
setInteractionCount: (newInteractionCount: Duration) => { | ||
interactionCount = newInteractionCount | ||
}, | ||
incrementInteractionCount() { | ||
interactionCount++ | ||
}, | ||
clear: () => { | ||
if (originalInteractionCount) { | ||
Object.defineProperty(window.performance, 'interactionCount', originalInteractionCount) | ||
} else { | ||
delete (window as BrowserWindow).performance.interactionCount | ||
} | ||
}, | ||
} | ||
} |
Oops, something went wrong.