import type { Duration, RelativeTime, TimeoutId } from '@datadog/browser-core' import { addEventListener, Observable, setTimeout, clearTimeout, monitor } from '@datadog/browser-core' import type { RumConfiguration } from '../domain/configuration' import { hasValidResourceEntryDuration, isAllowedRequestUrl } from '../domain/resource/resourceUtils' type RumPerformanceObserverConstructor = new (callback: PerformanceObserverCallback) => RumPerformanceObserver export interface BrowserWindow extends Window { PerformanceObserver: RumPerformanceObserverConstructor performance: Performance & { interactionCount?: number } } 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', LONG_ANIMATION_FRAME = 'long-animation-frame', NAVIGATION = 'navigation', PAINT = 'paint', RESOURCE = 'resource', VISIBILITY_STATE = 'visibility-state', } export interface RumPerformanceLongTaskTiming { name: string entryType: RumPerformanceEntryType.LONG_TASK startTime: RelativeTime duration: Duration toJSON(): Omit } export interface RumPerformanceResourceTiming { entryType: RumPerformanceEntryType.RESOURCE initiatorType: string responseStatus?: number name: string startTime: RelativeTime duration: Duration fetchStart: RelativeTime workerStart: RelativeTime domainLookupStart: RelativeTime domainLookupEnd: RelativeTime connectStart: RelativeTime secureConnectionStart: RelativeTime connectEnd: RelativeTime requestStart: RelativeTime responseStart: RelativeTime responseEnd: RelativeTime redirectStart: RelativeTime redirectEnd: RelativeTime decodedBodySize?: number encodedBodySize?: number transferSize?: number nextHopProtocol?: string renderBlockingStatus?: string deliveryType?: 'cache' | 'navigational-prefetch' | '' contentType?: string toJSON(): Omit } export interface RumPerformancePaintTiming { entryType: RumPerformanceEntryType.PAINT name: 'first-paint' | 'first-contentful-paint' startTime: RelativeTime toJSON(): Omit } export interface RumPerformanceNavigationTiming extends Omit { entryType: RumPerformanceEntryType.NAVIGATION initiatorType: 'navigation' name: string domComplete: RelativeTime domContentLoadedEventEnd: RelativeTime domInteractive: RelativeTime loadEventEnd: RelativeTime toJSON(): Omit } export interface RumLargestContentfulPaintTiming { entryType: RumPerformanceEntryType.LARGEST_CONTENTFUL_PAINT startTime: RelativeTime size: number element?: Element url?: string toJSON(): Omit } export interface RumFirstInputTiming { entryType: RumPerformanceEntryType.FIRST_INPUT startTime: RelativeTime processingStart: RelativeTime processingEnd: RelativeTime duration: Duration target?: Node interactionId?: number toJSON(): Omit } export interface RumPerformanceEventTiming { entryType: RumPerformanceEntryType.EVENT startTime: RelativeTime processingStart: RelativeTime processingEnd: RelativeTime duration: Duration interactionId?: number target?: Node name: string toJSON(): Omit } export interface RumLayoutShiftAttribution { node: Node | null previousRect: DOMRectReadOnly currentRect: DOMRectReadOnly } export interface RumLayoutShiftTiming { entryType: RumPerformanceEntryType.LAYOUT_SHIFT startTime: RelativeTime value: number hadRecentInput: boolean sources: RumLayoutShiftAttribution[] toJSON(): Omit } // Documentation https://developer.chrome.com/docs/web-platform/long-animation-frames#better-attribution export interface RumPerformanceScriptTiming { duration: Duration entryType: 'script' executionStart: RelativeTime forcedStyleAndLayoutDuration: Duration invoker: string // e.g. "https://static.datadoghq.com/static/c/93085/chunk-bc4db53278fd4c77a637.min.js" invokerType: | 'user-callback' | 'event-listener' | 'resolve-promise' | 'reject-promise' | 'classic-script' | 'module-script' name: 'script' pauseDuration: Duration sourceCharPosition: number sourceFunctionName: string sourceURL: string startTime: RelativeTime window: Window windowAttribution: string } export interface RumPerformanceLongAnimationFrameTiming { blockingDuration: Duration duration: Duration entryType: RumPerformanceEntryType.LONG_ANIMATION_FRAME firstUIEventTimestamp: RelativeTime name: 'long-animation-frame' renderStart: RelativeTime scripts: RumPerformanceScriptTiming[] startTime: RelativeTime styleAndLayoutStart: RelativeTime toJSON(): Omit } export interface RumFirstHiddenTiming { entryType: RumPerformanceEntryType.VISIBILITY_STATE name: 'hidden' | 'visible' startTime: RelativeTime toJSON(): Omit } export type RumPerformanceEntry = | RumPerformanceResourceTiming | RumPerformanceLongTaskTiming | RumPerformanceLongAnimationFrameTiming | RumPerformancePaintTiming | RumPerformanceNavigationTiming | RumLargestContentfulPaintTiming | RumFirstInputTiming | RumPerformanceEventTiming | RumLayoutShiftTiming | RumFirstHiddenTiming export interface 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.LONG_ANIMATION_FRAME]: RumPerformanceLongAnimationFrameTiming [RumPerformanceEntryType.NAVIGATION]: RumPerformanceNavigationTiming [RumPerformanceEntryType.RESOURCE]: RumPerformanceResourceTiming [RumPerformanceEntryType.VISIBILITY_STATE]: RumFirstHiddenTiming } export function createPerformanceObservable( configuration: RumConfiguration, options: { type: T; buffered?: boolean; durationThreshold?: number } ) { return new Observable>((observable) => { const handlePerformanceEntries = (entries: PerformanceEntryList) => { const rumPerformanceEntries = filterRumPerformanceEntries(entries as Array) if (rumPerformanceEntries.length > 0) { observable.notify(rumPerformanceEntries) } } let timeoutId: TimeoutId | undefined let isObserverInitializing = true const observer = new PerformanceObserver( monitor((entries) => { // In Safari the performance observer callback is synchronous. // Because the buffered performance entry list can be quite large we delay the computation to prevent the SDK from blocking the main thread on init if (isObserverInitializing) { timeoutId = setTimeout(() => handlePerformanceEntries(entries.getEntries())) } else { handlePerformanceEntries(entries.getEntries()) } }) ) observer.observe(options) isObserverInitializing = false manageResourceTimingBufferFull(configuration) return () => { observer.disconnect() clearTimeout(timeoutId) } }) } let resourceTimingBufferFullListener: { stop: () => void } | undefined function manageResourceTimingBufferFull(configuration: RumConfiguration) { if (!resourceTimingBufferFullListener && supportPerformanceObject() && 'addEventListener' in performance) { // https://bugzilla.mozilla.org/show_bug.cgi?id=1559377 resourceTimingBufferFullListener = addEventListener(configuration, performance, 'resourcetimingbufferfull', () => { performance.clearResourceTimings() }) } } export function resetManageResourceTimingBufferFull() { if (resourceTimingBufferFullListener) { resourceTimingBufferFullListener.stop() resourceTimingBufferFullListener = undefined } } function supportPerformanceObject() { return window.performance !== undefined && 'getEntries' in performance } export function supportPerformanceTimingEvent(entryType: RumPerformanceEntryType) { return PerformanceObserver.supportedEntryTypes.includes(entryType) } function filterRumPerformanceEntries(entries: Array) { return entries.filter((entry) => !isForbiddenResource(entry)) } function isForbiddenResource(entry: RumPerformanceEntry) { return ( entry.entryType === RumPerformanceEntryType.RESOURCE && (!isAllowedRequestUrl(entry.name) || !hasValidResourceEntryDuration(entry)) ) }