diff --git a/packages/rum-core/src/index.ts b/packages/rum-core/src/index.ts index 497d5c2ab0..1292120b31 100644 --- a/packages/rum-core/src/index.ts +++ b/packages/rum-core/src/index.ts @@ -22,7 +22,7 @@ export { } from './domainContext.types' export { ReplayStats, ActionType, RumEventType, FrustrationType, RawRumActionEvent } from './rawRumEvent.types' export { startRum } from './boot/startRum' -export { LifeCycle, LifeCycleEventType } from './domain/lifeCycle' +export { LifeCycle, LifeCycleEventType, RawRumEventCollectedData } from './domain/lifeCycle' export { ViewCreatedEvent } from './domain/view/trackViews' export { ViewContexts, ViewContext, startViewContexts } from './domain/contexts/viewContexts' export { RumSessionManager, RumSession } from './domain/rumSessionManager' diff --git a/packages/rum/src/domain/record/record.spec.ts b/packages/rum/src/domain/record/record.spec.ts index 486a0b4346..93e00bcad7 100644 --- a/packages/rum/src/domain/record/record.spec.ts +++ b/packages/rum/src/domain/record/record.spec.ts @@ -3,14 +3,19 @@ import type { RumConfiguration, ViewCreatedEvent } from '@datadog/browser-rum-co import { LifeCycle, LifeCycleEventType } from '@datadog/browser-rum-core' import type { Clock } from '@datadog/browser-core/test' import { createNewEvent, collectAsyncCalls, registerCleanupTask } from '@datadog/browser-core/test' -import { findElement, findFullSnapshot, findNode, recordsPerFullSnapshot } from '../../../test' +import { + findElement, + findFullSnapshot, + findNode, + recordsPerFullSnapshot, + createRumFrustrationEvent, +} from '../../../test' import type { BrowserIncrementalSnapshotRecord, BrowserMutationData, BrowserRecord, DocumentFragmentNode, ElementNode, - FocusRecord, ScrollData, } from '../../types' import { NodeType, RecordType, IncrementalSource } from '../../types' @@ -146,64 +151,6 @@ describe('record', () => { }) }) - describe('Focus records', () => { - let hasFocus: boolean - - beforeEach(() => { - hasFocus = true - spyOn(Document.prototype, 'hasFocus').and.callFake(() => hasFocus) - }) - - it('adds an initial Focus record when starting to record', () => { - startRecording() - expect(getEmittedRecords()[1]).toEqual({ - type: RecordType.Focus, - timestamp: jasmine.any(Number), - data: { - has_focus: true, - }, - }) - }) - - it('adds a Focus record on focus', () => { - startRecording() - emitSpy.calls.reset() - - window.dispatchEvent(createNewEvent('focus')) - expect(getEmittedRecords()[0].type).toBe(RecordType.Focus) - }) - - it('adds a Focus record on blur', () => { - startRecording() - emitSpy.calls.reset() - - window.dispatchEvent(createNewEvent('blur')) - expect(getEmittedRecords()[0].type).toBe(RecordType.Focus) - }) - - it('adds a Focus record on when taking a full snapshot', () => { - startRecording() - emitSpy.calls.reset() - - // trigger full snapshot by starting a new view - newView() - - expect(getEmittedRecords()[1].type).toBe(RecordType.Focus) - }) - - it('set has_focus to true if the document has the focus', () => { - hasFocus = true - startRecording() - expect((getEmittedRecords()[1] as FocusRecord).data.has_focus).toBe(true) - }) - - it("set has_focus to false if the document doesn't have the focus", () => { - hasFocus = false - startRecording() - expect((getEmittedRecords()[1] as FocusRecord).data.has_focus).toBe(false) - }) - }) - describe('Shadow dom', () => { it('should record a simple mutation inside a shadow root', () => { const element = appendElement('
', createShadow()) @@ -422,6 +369,95 @@ describe('record', () => { }) }) + describe('should collect records', () => { + let div: HTMLDivElement + let input: HTMLInputElement + let audio: HTMLAudioElement + beforeEach(() => { + div = appendElement('
') as HTMLDivElement + input = appendElement('') as HTMLInputElement + audio = appendElement('') as HTMLAudioElement + startRecording() + emitSpy.calls.reset() + }) + + it('move', () => { + document.body.dispatchEvent(createNewEvent('mousemove', { clientX: 1, clientY: 2 })) + expect(getEmittedRecords()[0].type).toBe(RecordType.IncrementalSnapshot) + expect((getEmittedRecords()[0] as BrowserIncrementalSnapshotRecord).data.source).toBe(IncrementalSource.MouseMove) + }) + + it('interaction', () => { + document.body.dispatchEvent(createNewEvent('click', { clientX: 1, clientY: 2 })) + expect((getEmittedRecords()[0] as BrowserIncrementalSnapshotRecord).data.source).toBe( + IncrementalSource.MouseInteraction + ) + }) + + it('scroll', () => { + div.dispatchEvent(createNewEvent('scroll', { target: div })) + + expect(getEmittedRecords()[0].type).toBe(RecordType.IncrementalSnapshot) + expect((getEmittedRecords()[0] as BrowserIncrementalSnapshotRecord).data.source).toBe(IncrementalSource.Scroll) + }) + + it('viewport resize', () => { + window.dispatchEvent(createNewEvent('resize')) + + expect(getEmittedRecords()[0].type).toBe(RecordType.IncrementalSnapshot) + expect((getEmittedRecords()[0] as BrowserIncrementalSnapshotRecord).data.source).toBe( + IncrementalSource.ViewportResize + ) + }) + + it('input', () => { + input.value = 'newValue' + input.dispatchEvent(createNewEvent('input', { target: input })) + + expect(getEmittedRecords()[0].type).toBe(RecordType.IncrementalSnapshot) + expect((getEmittedRecords()[0] as BrowserIncrementalSnapshotRecord).data.source).toBe(IncrementalSource.Input) + }) + + it('media interaction', () => { + audio.dispatchEvent(createNewEvent('play', { target: audio })) + + expect(getEmittedRecords()[0].type).toBe(RecordType.IncrementalSnapshot) + expect((getEmittedRecords()[0] as BrowserIncrementalSnapshotRecord).data.source).toBe( + IncrementalSource.MediaInteraction + ) + }) + + it('focus', () => { + window.dispatchEvent(createNewEvent('blur')) + + expect(getEmittedRecords()[0].type).toBe(RecordType.Focus) + }) + + it('visual viewport resize', () => { + if (!window.visualViewport) { + pending('visualViewport not supported') + } + + visualViewport!.dispatchEvent(createNewEvent('resize')) + expect(getEmittedRecords()[0].type).toBe(RecordType.VisualViewport) + }) + + it('frustration', () => { + lifeCycle.notify( + LifeCycleEventType.RAW_RUM_EVENT_COLLECTED, + createRumFrustrationEvent(new MouseEvent('pointerup')) + ) + + expect(getEmittedRecords()[0].type).toBe(RecordType.FrustrationRecord) + }) + + it('view end event', () => { + lifeCycle.notify(LifeCycleEventType.VIEW_ENDED, {} as any) + + expect(getEmittedRecords()[0].type).toBe(RecordType.ViewEnd) + }) + }) + function startRecording() { lifeCycle = new LifeCycle() recordApi = record({ diff --git a/packages/rum/src/domain/record/record.ts b/packages/rum/src/domain/record/record.ts index f5b027d1ea..3dbbf9b737 100644 --- a/packages/rum/src/domain/record/record.ts +++ b/packages/rum/src/domain/record/record.ts @@ -15,6 +15,7 @@ import { trackStyleSheet, trackViewEnd, trackViewportResize, + trackVisualViewportResize, } from './trackers' import { createElementsScrollPositions } from './elementsScrollPositions' import type { ShadowRootsController } from './shadowRootsController' @@ -79,7 +80,7 @@ export function record(options: RecordOptions): RecordAPI { trackMediaInteraction(configuration, emitAndComputeStats), trackStyleSheet(emitAndComputeStats), trackFocus(configuration, emitAndComputeStats), - trackViewportResize(configuration, emitAndComputeStats), + trackVisualViewportResize(configuration, emitAndComputeStats), trackFrustration(lifeCycle, emitAndComputeStats, recordIds), trackViewEnd(lifeCycle, (viewEndRecord) => { flushMutations() diff --git a/packages/rum/src/domain/record/startFullSnapshots.spec.ts b/packages/rum/src/domain/record/startFullSnapshots.spec.ts index a8026ed705..264f2a2ddc 100644 --- a/packages/rum/src/domain/record/startFullSnapshots.spec.ts +++ b/packages/rum/src/domain/record/startFullSnapshots.spec.ts @@ -2,7 +2,7 @@ import type { RumConfiguration, ViewCreatedEvent } from '@datadog/browser-rum-co import { LifeCycle, LifeCycleEventType } from '@datadog/browser-rum-core' import type { TimeStamp } from '@datadog/browser-core' import { isIE, noop } from '@datadog/browser-core' -import type { BrowserRecord } from '../../types' +import { RecordType, type BrowserRecord } from '../../types' import { startFullSnapshots } from './startFullSnapshots' import { createElementsScrollPositions } from './elementsScrollPositions' import type { ShadowRootsController } from './shadowRootsController' @@ -51,4 +51,57 @@ describe('startFullSnapshots', () => { expect(records[1].timestamp).toEqual(1) expect(records[2].timestamp).toEqual(1) }) + + it('full snapshot records should contain Meta, Focus, FullSnapshot', () => { + const records = fullSnapshotCallback.calls.mostRecent().args[0] + + expect(records).toEqual( + jasmine.arrayContaining([ + { + data: { + height: jasmine.any(Number), + href: window.location.href, + width: jasmine.any(Number), + }, + type: RecordType.Meta, + timestamp: jasmine.any(Number), + }, + { + data: { + has_focus: document.hasFocus(), + }, + type: RecordType.Focus, + timestamp: jasmine.any(Number), + }, + { + data: { + node: jasmine.any(Object), + initialOffset: { + left: jasmine.any(Number), + top: jasmine.any(Number), + }, + }, + type: RecordType.FullSnapshot, + timestamp: jasmine.any(Number), + }, + ]) + ) + }) + + it('full snapshot records should contain visualViewport when supported', () => { + if (!window.visualViewport) { + pending('visualViewport not supported') + } + const records = fullSnapshotCallback.calls.mostRecent().args[0] + + expect(records).toEqual( + jasmine.arrayContaining([ + { + data: jasmine.any(Object), + type: RecordType.VisualViewport, + timestamp: jasmine.any(Number), + }, + ]) + ) + }) }) diff --git a/packages/rum/src/domain/record/trackers/index.ts b/packages/rum/src/domain/record/trackers/index.ts index 907c8f1236..8bb61d1b9d 100644 --- a/packages/rum/src/domain/record/trackers/index.ts +++ b/packages/rum/src/domain/record/trackers/index.ts @@ -1,7 +1,7 @@ export { trackMove } from './trackMove' export { trackMouseInteraction } from './trackMouseInteraction' export { trackScroll } from './trackScroll' -export { trackViewportResize, tackVisualViewportResize } from './trackViewportResize' +export { trackViewportResize, trackVisualViewportResize } from './trackViewportResize' export { trackMediaInteraction } from './trackMediaInteraction' export { trackStyleSheet } from './trackStyleSheet' export { trackFocus } from './trackFocus' diff --git a/packages/rum/src/domain/record/trackers/trackFrustration.spec.ts b/packages/rum/src/domain/record/trackers/trackFrustration.spec.ts index c1762fac1e..b59275ac56 100644 --- a/packages/rum/src/domain/record/trackers/trackFrustration.spec.ts +++ b/packages/rum/src/domain/record/trackers/trackFrustration.spec.ts @@ -1,8 +1,9 @@ -import { isIE, relativeNow, timeStampNow } from '@datadog/browser-core' +import { isIE } from '@datadog/browser-core' import type { RawRumActionEvent } from '@datadog/browser-rum-core' -import { ActionType, LifeCycle, LifeCycleEventType, RumEventType, FrustrationType } from '@datadog/browser-rum-core' +import { ActionType, LifeCycle, LifeCycleEventType } from '@datadog/browser-rum-core' import type { RawRumEventCollectedData } from 'packages/rum-core/src/domain/lifeCycle' import { registerCleanupTask } from '@datadog/browser-core/test' +import { createRumFrustrationEvent } from '../../../../test' import { RecordType } from '../../../types' import type { RecordIds } from '../recordIds' import { initRecordIds } from '../recordIds' @@ -26,24 +27,7 @@ describe('trackFrustration', () => { frustrationsCallbackSpy = jasmine.createSpy() recordIds = initRecordIds() - rumData = { - startTime: relativeNow(), - rawRumEvent: { - date: timeStampNow(), - type: RumEventType.ACTION, - action: { - id: '123e4567-e89b-12d3-a456-426614174000', - type: ActionType.CLICK, - frustration: { - type: [FrustrationType.DEAD_CLICK], - }, - target: { - name: '123e4567-e89b-12d3-a456-426614174000', - }, - }, - }, - domainContext: { events: [mouseEvent] }, - } + rumData = createRumFrustrationEvent(mouseEvent) registerCleanupTask(() => { frustrationTracker.stop() diff --git a/packages/rum/src/domain/record/trackers/trackViewportResize.spec.ts b/packages/rum/src/domain/record/trackers/trackViewportResize.spec.ts index c08838fb55..f4c8a5132a 100644 --- a/packages/rum/src/domain/record/trackers/trackViewportResize.spec.ts +++ b/packages/rum/src/domain/record/trackers/trackViewportResize.spec.ts @@ -7,7 +7,7 @@ import { createElementsScrollPositions } from '../elementsScrollPositions' import { RecordType } from '../../../types' import { DEFAULT_CONFIGURATION, DEFAULT_SHADOW_ROOT_CONTROLLER } from './trackers.specHelper' import type { VisualViewportResizeCallback } from './trackViewportResize' -import { tackVisualViewportResize } from './trackViewportResize' +import { trackVisualViewportResize } from './trackViewportResize' import type { Tracker } from './tracker.types' describe('trackViewportResize', () => { @@ -30,7 +30,7 @@ describe('trackViewportResize', () => { elementsScrollPositions, }) - viewportResizeTracker = tackVisualViewportResize(configuration, visualViewportResizeCallback) + viewportResizeTracker = trackVisualViewportResize(configuration, visualViewportResizeCallback) registerCleanupTask(() => { viewportResizeTracker.stop() diff --git a/packages/rum/src/domain/record/trackers/trackViewportResize.ts b/packages/rum/src/domain/record/trackers/trackViewportResize.ts index 8e931a3aaa..5fae722f93 100644 --- a/packages/rum/src/domain/record/trackers/trackViewportResize.ts +++ b/packages/rum/src/domain/record/trackers/trackViewportResize.ts @@ -28,7 +28,7 @@ export function trackViewportResize( } } -export function tackVisualViewportResize( +export function trackVisualViewportResize( configuration: RumConfiguration, visualViewportResizeCb: VisualViewportResizeCallback ): Tracker { diff --git a/packages/rum/test/index.ts b/packages/rum/test/index.ts index 486c21a7d3..227e480f65 100644 --- a/packages/rum/test/index.ts +++ b/packages/rum/test/index.ts @@ -4,3 +4,4 @@ export * from './mutationPayloadValidator' export * from './nodes' export * from './segments' export * from './readReplayPayload' +export * from './rumFrustrationEvent' diff --git a/packages/rum/test/rumFrustrationEvent.ts b/packages/rum/test/rumFrustrationEvent.ts new file mode 100644 index 0000000000..ce16199672 --- /dev/null +++ b/packages/rum/test/rumFrustrationEvent.ts @@ -0,0 +1,24 @@ +import { relativeNow, timeStampNow } from '@datadog/browser-core' +import type { RawRumActionEvent, RawRumEventCollectedData } from '@datadog/browser-rum-core' +import { ActionType, FrustrationType, RumEventType } from '@datadog/browser-rum-core' + +export function createRumFrustrationEvent(mouseEvent: MouseEvent): RawRumEventCollectedData { + return { + startTime: relativeNow(), + rawRumEvent: { + date: timeStampNow(), + type: RumEventType.ACTION, + action: { + id: '123e4567-e89b-12d3-a456-426614174000', + type: ActionType.CLICK, + frustration: { + type: [FrustrationType.DEAD_CLICK], + }, + target: { + name: '123e4567-e89b-12d3-a456-426614174000', + }, + }, + }, + domainContext: { events: [mouseEvent] }, + } +}