diff --git a/packages/core/src/domain/telemetry/telemetryEvent.types.ts b/packages/core/src/domain/telemetry/telemetryEvent.types.ts index 59a93bfc07..c986d4779d 100644 --- a/packages/core/src/domain/telemetry/telemetryEvent.types.ts +++ b/packages/core/src/domain/telemetry/telemetryEvent.types.ts @@ -133,6 +133,10 @@ export type TelemetryConfigurationEvent = CommonTelemetryProperties & { * Whether the session replay start is handled manually */ start_session_replay_recording_manually?: boolean + /** + * Whether Session Replay should automatically start a recording when enabled + */ + start_recording_immediately?: boolean /** * Whether a proxy is used */ @@ -476,6 +480,13 @@ export type TelemetryBrowserFeaturesUsage = feature: 'start-duration-vital' [k: string]: unknown } + | { + /** + * stopDurationVital API + */ + feature: 'stop-duration-vital' + [k: string]: unknown + } | { /** * addDurationVital API @@ -513,7 +524,7 @@ export interface CommonTelemetryProperties { /** * The source of this event */ - readonly source: 'android' | 'ios' | 'browser' | 'flutter' | 'react-native' | 'unity' + readonly source: 'android' | 'ios' | 'browser' | 'flutter' | 'react-native' | 'unity' | 'kotlin-multiplatform' /** * The version of the SDK generating the telemetry event */ diff --git a/packages/core/src/tools/experimentalFeatures.ts b/packages/core/src/tools/experimentalFeatures.ts index 7ffa745ea5..6d2cf81486 100644 --- a/packages/core/src/tools/experimentalFeatures.ts +++ b/packages/core/src/tools/experimentalFeatures.ts @@ -20,6 +20,7 @@ export enum ExperimentalFeature { REMOTE_CONFIGURATION = 'remote_configuration', UPDATE_VIEW_NAME = 'update_view_name', NULL_INP_TELEMETRY = 'null_inp_telemetry', + LONG_ANIMATION_FRAME = 'long_animation_frame', } const enabledExperimentalFeatures: Set = new Set() diff --git a/packages/rum-core/src/boot/startRum.ts b/packages/rum-core/src/boot/startRum.ts index 7c4a591d4c..e68dd0cd32 100644 --- a/packages/rum-core/src/boot/startRum.ts +++ b/packages/rum-core/src/boot/startRum.ts @@ -17,6 +17,8 @@ import { addTelemetryDebug, CustomerDataType, drainPreStartTelemetry, + isExperimentalFeatureEnabled, + ExperimentalFeature, } from '@datadog/browser-core' import { createDOMMutationObservable } from '../browser/domMutationObservable' import { startPerformanceCollection } from '../browser/performanceCollection' @@ -47,6 +49,7 @@ import type { CommonContext } from '../domain/contexts/commonContext' import { startDisplayContext } from '../domain/contexts/displayContext' import { startVitalCollection } from '../domain/vital/vitalCollection' import { startCiVisibilityContext } from '../domain/contexts/ciVisibilityContext' +import { startLongAnimationFrameCollection } from '../domain/longAnimationFrame/longAnimationFrameCollection' import type { RecorderApi } from './rumPublicApi' export type StartRum = typeof startRum @@ -165,7 +168,14 @@ export function startRum( const { stop: stopResourceCollection } = startResourceCollection(lifeCycle, configuration, pageStateHistory) cleanupTasks.push(stopResourceCollection) - startLongTaskCollection(lifeCycle, configuration) + if (isExperimentalFeatureEnabled(ExperimentalFeature.LONG_ANIMATION_FRAME)) { + if (configuration.trackLongTasks) { + const { stop: stopLongAnimationFrameCollection } = startLongAnimationFrameCollection(lifeCycle, configuration) + cleanupTasks.push(stopLongAnimationFrameCollection) + } + } else { + startLongTaskCollection(lifeCycle, configuration) + } const { addError } = startErrorCollection(lifeCycle, configuration, pageStateHistory, featureFlagContexts) diff --git a/packages/rum-core/src/browser/performanceObservable.ts b/packages/rum-core/src/browser/performanceObservable.ts index a374a00912..6c73c52101 100644 --- a/packages/rum-core/src/browser/performanceObservable.ts +++ b/packages/rum-core/src/browser/performanceObservable.ts @@ -23,6 +23,7 @@ export enum RumPerformanceEntryType { 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', @@ -117,9 +118,46 @@ export interface RumLayoutShiftTiming { }> } +// Documentation https://developer.chrome.com/docs/web-platform/long-animation-frames#better-attribution +export type 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 +} + export type RumPerformanceEntry = | RumPerformanceResourceTiming | RumPerformanceLongTaskTiming + | RumPerformanceLongAnimationFrameTiming | RumPerformancePaintTiming | RumPerformanceNavigationTiming | RumLargestContentfulPaintTiming @@ -134,6 +172,7 @@ export type EntryTypeToReturnType = { [RumPerformanceEntryType.LAYOUT_SHIFT]: RumLayoutShiftTiming [RumPerformanceEntryType.PAINT]: RumPerformancePaintTiming [RumPerformanceEntryType.LONG_TASK]: RumPerformanceLongTaskTiming + [RumPerformanceEntryType.LONG_ANIMATION_FRAME]: RumPerformanceLongAnimationFrameTiming [RumPerformanceEntryType.NAVIGATION]: RumPerformanceNavigationTiming [RumPerformanceEntryType.RESOURCE]: RumPerformanceResourceTiming } diff --git a/packages/rum-core/src/domain/longAnimationFrame/longAnimationFrameCollection.spec.ts b/packages/rum-core/src/domain/longAnimationFrame/longAnimationFrameCollection.spec.ts new file mode 100644 index 0000000000..7fd3b62cd5 --- /dev/null +++ b/packages/rum-core/src/domain/longAnimationFrame/longAnimationFrameCollection.spec.ts @@ -0,0 +1,98 @@ +import { type RelativeTime, type ServerDuration } from '@datadog/browser-core' +import { registerCleanupTask } from '@datadog/browser-core/test' +import { collectAndValidateRawRumEvents, createPerformanceEntry, mockPerformanceObserver } from '../../../test' +import { RumPerformanceEntryType } from '../../browser/performanceObservable' +import { RumEventType, RumLongTaskEntryType } from '../../rawRumEvent.types' +import { LifeCycle } from '../lifeCycle' +import type { RumConfiguration } from '../configuration' +import { startLongAnimationFrameCollection } from './longAnimationFrameCollection' + +describe('long animation frames collection', () => { + it('should create raw rum event from long animation frame performance entry', () => { + const { notifyPerformanceEntries, rawRumEvents } = setupLongAnimationFrameCollection() + const PerformanceLongAnimationFrameTiming = createPerformanceEntry(RumPerformanceEntryType.LONG_ANIMATION_FRAME) + + notifyPerformanceEntries([PerformanceLongAnimationFrameTiming]) + + expect(rawRumEvents[0].startTime).toBe(1234 as RelativeTime) + expect(rawRumEvents[0].rawRumEvent).toEqual({ + date: jasmine.any(Number), + long_task: { + id: jasmine.any(String), + entry_type: RumLongTaskEntryType.LONG_ANIMATION_FRAME, + duration: (82 * 1e6) as ServerDuration, + blocking_duration: 0 as ServerDuration, + first_ui_event_timestamp: 0 as RelativeTime, + render_start: 1421.5 as RelativeTime, + style_and_layout_start: 1428 as RelativeTime, + scripts: [ + { + duration: (6 * 1e6) as ServerDuration, + pause_duration: 0 as ServerDuration, + forced_style_and_layout_duration: 0 as ServerDuration, + start_time: 1348 as RelativeTime, + execution_start: 1348.7 as RelativeTime, + source_url: 'http://example.com/script.js', + source_function_name: '', + source_char_position: 9876, + invoker: 'http://example.com/script.js', + invoker_type: 'classic-script', + window_attribution: 'self', + }, + ], + }, + type: RumEventType.LONG_TASK, + _dd: { + discarded: false, + }, + }) + expect(rawRumEvents[0].domainContext).toEqual({ + performanceEntry: { + name: 'long-animation-frame', + duration: 82, + entryType: 'long-animation-frame', + startTime: 1234, + renderStart: 1421.5, + styleAndLayoutStart: 1428, + firstUIEventTimestamp: 0, + blockingDuration: 0, + scripts: [ + { + name: 'script', + entryType: 'script', + startTime: 1348, + duration: 6, + invoker: 'http://example.com/script.js', + invokerType: 'classic-script', + windowAttribution: 'self', + executionStart: 1348.7, + forcedStyleAndLayoutDuration: 0, + pauseDuration: 0, + sourceURL: 'http://example.com/script.js', + sourceFunctionName: '', + sourceCharPosition: 9876, + }, + ], + toJSON: jasmine.any(Function), + }, + }) + }) +}) + +function setupLongAnimationFrameCollection() { + const lifeCycle = new LifeCycle() + const configuration = {} as RumConfiguration + + const notifyPerformanceEntries = mockPerformanceObserver().notifyPerformanceEntries + const rawRumEvents = collectAndValidateRawRumEvents(lifeCycle) + const { stop: stopLongAnimationFrameCollection } = startLongAnimationFrameCollection(lifeCycle, configuration) + + registerCleanupTask(() => { + stopLongAnimationFrameCollection() + }) + + return { + notifyPerformanceEntries, + rawRumEvents, + } +} diff --git a/packages/rum-core/src/domain/longAnimationFrame/longAnimationFrameCollection.ts b/packages/rum-core/src/domain/longAnimationFrame/longAnimationFrameCollection.ts new file mode 100644 index 0000000000..f16ce5220b --- /dev/null +++ b/packages/rum-core/src/domain/longAnimationFrame/longAnimationFrameCollection.ts @@ -0,0 +1,58 @@ +import { toServerDuration, relativeToClocks, generateUUID } from '@datadog/browser-core' +import type { RawRumLongAnimationFrameEvent } from '../../rawRumEvent.types' +import { RumEventType, RumLongTaskEntryType } from '../../rawRumEvent.types' +import type { LifeCycle } from '../lifeCycle' +import { LifeCycleEventType } from '../lifeCycle' +import { createPerformanceObservable, RumPerformanceEntryType } from '../../browser/performanceObservable' +import type { RumConfiguration } from '../configuration' + +export function startLongAnimationFrameCollection(lifeCycle: LifeCycle, configuration: RumConfiguration) { + const performanceResourceSubscription = createPerformanceObservable(configuration, { + type: RumPerformanceEntryType.LONG_ANIMATION_FRAME, + buffered: true, + }).subscribe((entries) => { + for (const entry of entries) { + const startClocks = relativeToClocks(entry.startTime) + + const rawRumEvent: RawRumLongAnimationFrameEvent = { + date: startClocks.timeStamp, + long_task: { + id: generateUUID(), + entry_type: RumLongTaskEntryType.LONG_ANIMATION_FRAME, + duration: toServerDuration(entry.duration), + blocking_duration: toServerDuration(entry.blockingDuration), + first_ui_event_timestamp: relativeToClocks(entry.firstUIEventTimestamp).relative, + render_start: relativeToClocks(entry.renderStart).relative, + style_and_layout_start: relativeToClocks(entry.styleAndLayoutStart).relative, + scripts: entry.scripts.map((script) => ({ + duration: toServerDuration(script.duration), + pause_duration: toServerDuration(script.pauseDuration), + forced_style_and_layout_duration: toServerDuration(script.forcedStyleAndLayoutDuration), + start_time: relativeToClocks(script.startTime).relative, + execution_start: relativeToClocks(script.executionStart).relative, + source_url: script.sourceURL, + source_function_name: script.sourceFunctionName, + source_char_position: script.sourceCharPosition, + invoker: script.invoker, + invoker_type: script.invokerType, + window_attribution: script.windowAttribution, + })), + }, + type: RumEventType.LONG_TASK, + _dd: { + discarded: false, + }, + } + + lifeCycle.notify(LifeCycleEventType.RAW_RUM_EVENT_COLLECTED, { + rawRumEvent, + startTime: startClocks.relative, + domainContext: { performanceEntry: entry }, + }) + } + }) + + return { + stop: () => performanceResourceSubscription.unsubscribe(), + } +} diff --git a/packages/rum-core/src/domain/longTask/longTaskCollection.spec.ts b/packages/rum-core/src/domain/longTask/longTaskCollection.spec.ts index 855ec7ac6b..489ecbda6b 100644 --- a/packages/rum-core/src/domain/longTask/longTaskCollection.spec.ts +++ b/packages/rum-core/src/domain/longTask/longTaskCollection.spec.ts @@ -2,7 +2,7 @@ import type { RelativeTime, ServerDuration } from '@datadog/browser-core' import { collectAndValidateRawRumEvents, createPerformanceEntry, mockPerformanceObserver } from '../../../test' import { RumPerformanceEntryType } from '../../browser/performanceObservable' import type { RawRumEvent } from '../../rawRumEvent.types' -import { RumEventType } from '../../rawRumEvent.types' +import { RumEventType, RumLongTaskEntryType } from '../../rawRumEvent.types' import type { RawRumEventCollectedData } from '../lifeCycle' import { LifeCycle, LifeCycleEventType } from '../lifeCycle' import type { RumConfiguration } from '../configuration' @@ -61,6 +61,7 @@ describe('long task collection', () => { date: jasmine.any(Number), long_task: { id: jasmine.any(String), + entry_type: RumLongTaskEntryType.LONG_TASK, duration: (100 * 1e6) as ServerDuration, }, type: RumEventType.LONG_TASK, diff --git a/packages/rum-core/src/domain/longTask/longTaskCollection.ts b/packages/rum-core/src/domain/longTask/longTaskCollection.ts index 1d38e0af66..705103d2f2 100644 --- a/packages/rum-core/src/domain/longTask/longTaskCollection.ts +++ b/packages/rum-core/src/domain/longTask/longTaskCollection.ts @@ -1,6 +1,6 @@ import { toServerDuration, relativeToClocks, generateUUID } from '@datadog/browser-core' import type { RawRumLongTaskEvent } from '../../rawRumEvent.types' -import { RumEventType } from '../../rawRumEvent.types' +import { RumEventType, RumLongTaskEntryType } from '../../rawRumEvent.types' import type { LifeCycle } from '../lifeCycle' import { LifeCycleEventType } from '../lifeCycle' import { RumPerformanceEntryType } from '../../browser/performanceObservable' @@ -20,6 +20,7 @@ export function startLongTaskCollection(lifeCycle: LifeCycle, configuration: Rum date: startClocks.timeStamp, long_task: { id: generateUUID(), + entry_type: RumLongTaskEntryType.LONG_TASK, duration: toServerDuration(entry.duration), }, type: RumEventType.LONG_TASK, diff --git a/packages/rum-core/src/rawRumEvent.types.ts b/packages/rum-core/src/rawRumEvent.types.ts index 0744cd0a04..dbc7e52fdc 100644 --- a/packages/rum-core/src/rawRumEvent.types.ts +++ b/packages/rum-core/src/rawRumEvent.types.ts @@ -10,6 +10,7 @@ import type { DefaultPrivacyLevel, Connectivity, Csp, + RelativeTime, } from '@datadog/browser-core' import type { PageState } from './domain/contexts/pageStateHistory' @@ -22,6 +23,11 @@ export const enum RumEventType { VITAL = 'vital', } +export const enum RumLongTaskEntryType { + LONG_TASK = 'long-task', + LONG_ANIMATION_FRAME = 'long-animation-frame', +} + export interface RawRumResourceEvent { date: TimeStamp type: RumEventType.RESOURCE @@ -170,7 +176,46 @@ export interface RawRumLongTaskEvent { type: RumEventType.LONG_TASK long_task: { id: string + entry_type: RumLongTaskEntryType.LONG_TASK + duration: ServerDuration + } + _dd: { + discarded: boolean + } +} + +export type InvokerType = + | 'user-callback' + | 'event-listener' + | 'resolve-promise' + | 'reject-promise' + | 'classic-script' + | 'module-script' + +export interface RawRumLongAnimationFrameEvent { + date: TimeStamp + type: RumEventType.LONG_TASK // LoAF are ingested as Long Task + long_task: { + id: string + entry_type: RumLongTaskEntryType.LONG_ANIMATION_FRAME duration: ServerDuration + blocking_duration: ServerDuration + first_ui_event_timestamp: RelativeTime + render_start: RelativeTime + style_and_layout_start: RelativeTime + scripts: Array<{ + duration: ServerDuration + pause_duration: ServerDuration + forced_style_and_layout_duration: ServerDuration + start_time: RelativeTime + execution_start: RelativeTime + source_url: string + source_function_name: string + source_char_position: number + invoker: string + invoker_type: InvokerType + window_attribution: string + }> } _dd: { discarded: boolean @@ -250,6 +295,7 @@ export type RawRumEvent = | RawRumResourceEvent | RawRumViewEvent | RawRumLongTaskEvent + | RawRumLongAnimationFrameEvent | RawRumActionEvent | RawRumVitalEvent diff --git a/packages/rum-core/src/rumEvent.types.ts b/packages/rum-core/src/rumEvent.types.ts index 64bfdaf2ac..c9750326a2 100644 --- a/packages/rum-core/src/rumEvent.types.ts +++ b/packages/rum-core/src/rumEvent.types.ts @@ -223,7 +223,7 @@ export type RumErrorEvent = CommonProperties & /** * The specific category of the error. It provides a high-level grouping for different types of errors. */ - readonly category?: 'ANR' | 'App Hang' | 'Exception' + readonly category?: 'ANR' | 'App Hang' | 'Exception' | 'Watchdog Termination' | 'Memory Warning' /** * Whether the error has been handled manually in the source code or not */ @@ -444,17 +444,93 @@ export type RumLongTaskEvent = CommonProperties & */ readonly long_task: { /** - * UUID of the long task + * UUID of the long task or long animation frame */ readonly id?: string /** - * Duration in ns of the long task + * Type of the event: long task or long animation frame + */ + readonly entry_type?: 'long-task' | 'long-animation-frame' + /** + * Duration in ns of the long task or long animation frame */ readonly duration: number + /** + * Duration in ns for which the animation frame was being blocked + */ + readonly blocking_duration?: number + /** + * Start time of the rendering cycle, which includes requestAnimationFrame callbacks, style and layout calculation, resize observer and intersection observer callbacks + */ + readonly render_start?: number + /** + * Start time of the time period spent in style and layout calculations + */ + readonly style_and_layout_start?: number + /** + * Start time of of the first UI event (mouse/keyboard and so on) to be handled during the course of this frame + */ + readonly first_ui_event_timestamp?: number /** * Whether this long task is considered a frozen frame */ readonly is_frozen_frame?: boolean + /** + * A list of long scripts that were executed over the course of the long frame + */ + readonly scripts?: { + /** + * Duration in ns between startTime and when the subsequent microtask queue has finished processing + */ + readonly duration?: number + /** + * Duration in ns of the total time spent in 'pausing' synchronous operations (alert, synchronous XHR) + */ + readonly pause_duration?: number + /** + * Duration in ns of the the total time spent processing forced layout and style inside this function + */ + readonly forced_style_and_layout_duration?: number + /** + * Time the entry function was invoked + */ + readonly start_time?: number + /** + * Time after compilation + */ + readonly execution_start?: number + /** + * The script resource name where available (or empty if not found) + */ + readonly source_url?: string + /** + * The script function name where available (or empty if not found) + */ + readonly source_function_name?: string + /** + * The script character position where available (or -1 if not found) + */ + readonly source_char_position?: number + /** + * Information about the invoker of the script + */ + readonly invoker?: string + /** + * Type of the invoker of the script + */ + readonly invoker_type?: + | 'user-callback' + | 'event-listener' + | 'resolve-promise' + | 'reject-promise' + | 'classic-script' + | 'module-script' + /** + * The container (the top-level document, or an