diff --git a/packages/rum-recorder/src/boot/rumRecorderPublicApi.spec.ts b/packages/rum-recorder/src/boot/rumRecorderPublicApi.spec.ts index 34e3f720a5..669cee37cf 100644 --- a/packages/rum-recorder/src/boot/rumRecorderPublicApi.spec.ts +++ b/packages/rum-recorder/src/boot/rumRecorderPublicApi.spec.ts @@ -1,6 +1,6 @@ -/* eslint-disable @typescript-eslint/unbound-method */ import { Configuration, includes } from '@datadog/browser-core' import { StartRum } from '@datadog/browser-rum-core' +import { createNewEvent } from '../../../core/test/specHelper' import { makeRumRecorderPublicApi, RumRecorderPublicApi, StartRecording } from './rumRecorderPublicApi' const DEFAULT_INIT_CONFIGURATION = { applicationId: 'xxx', clientToken: 'xxx' } @@ -56,6 +56,14 @@ describe('makeRumRecorderPublicApi', () => { }) expect(startRecordingSpy).not.toHaveBeenCalled() }) + + it('does not start recording before the page "load"', () => { + const { triggerOnLoad } = mockDocumentReadyState() + rumRecorderPublicApi.init(DEFAULT_INIT_CONFIGURATION) + expect(startRecordingSpy).not.toHaveBeenCalled() + triggerOnLoad() + expect(startRecordingSpy).toHaveBeenCalled() + }) }) describe('startSessionReplayRecording()', () => { @@ -72,6 +80,15 @@ describe('makeRumRecorderPublicApi', () => { rumRecorderPublicApi.init({ ...DEFAULT_INIT_CONFIGURATION, manualSessionReplayRecordingStart: true }) expect(startRecordingSpy).toHaveBeenCalled() }) + + it('does not start recording multiple times if restarted before onload', () => { + const { triggerOnLoad } = mockDocumentReadyState() + rumRecorderPublicApi.init(DEFAULT_INIT_CONFIGURATION) + rumRecorderPublicApi.stopSessionReplayRecording() + rumRecorderPublicApi.startSessionReplayRecording() + triggerOnLoad() + expect(startRecordingSpy).toHaveBeenCalledTimes(1) + }) }) describe('stopSessionReplayRecording()', () => { @@ -85,7 +102,15 @@ describe('makeRumRecorderPublicApi', () => { it('does not start recording if called before init()', () => { rumRecorderPublicApi.stopSessionReplayRecording() - rumRecorderPublicApi.init({ ...DEFAULT_INIT_CONFIGURATION }) + rumRecorderPublicApi.init(DEFAULT_INIT_CONFIGURATION) + expect(startRecordingSpy).not.toHaveBeenCalled() + }) + + it('prevents recording to start at page "load"', () => { + const { triggerOnLoad } = mockDocumentReadyState() + rumRecorderPublicApi.init(DEFAULT_INIT_CONFIGURATION) + rumRecorderPublicApi.stopSessionReplayRecording() + triggerOnLoad() expect(startRecordingSpy).not.toHaveBeenCalled() }) }) @@ -99,5 +124,26 @@ describe('makeRumRecorderPublicApi', () => { rumRecorderPublicApi.stopSessionReplayRecording() expect(getCommonContext().hasReplay).toBeUndefined() }) + + it('is undefined before page "load"', () => { + const { triggerOnLoad } = mockDocumentReadyState() + rumRecorderPublicApi.init(DEFAULT_INIT_CONFIGURATION) + expect(getCommonContext().hasReplay).toBeUndefined() + rumRecorderPublicApi.startSessionReplayRecording() + expect(getCommonContext().hasReplay).toBeUndefined() + triggerOnLoad() + expect(getCommonContext().hasReplay).toBe(true) + }) }) }) + +function mockDocumentReadyState() { + let readyState = 'loading' + spyOnProperty(Document.prototype, 'readyState', 'get').and.callFake(() => readyState) + return { + triggerOnLoad: () => { + readyState = 'complete' + window.dispatchEvent(createNewEvent('load')) + }, + } +} diff --git a/packages/rum-recorder/src/boot/rumRecorderPublicApi.ts b/packages/rum-recorder/src/boot/rumRecorderPublicApi.ts index 1547cde58b..69b337f542 100644 --- a/packages/rum-recorder/src/boot/rumRecorderPublicApi.ts +++ b/packages/rum-recorder/src/boot/rumRecorderPublicApi.ts @@ -1,4 +1,4 @@ -import { Configuration, monitor, noop } from '@datadog/browser-core' +import { Configuration, monitor, noop, runOnReadyState } from '@datadog/browser-core' import { LifeCycleEventType, makeRumPublicApi, RumUserConfiguration, StartRum } from '@datadog/browser-rum-core' import { startRecording } from './recorder' @@ -12,12 +12,16 @@ export interface RumRecorderUserConfiguration extends RumUserConfiguration { const enum RecorderStatus { Stopped, + Starting, Started, } type RecorderState = | { status: RecorderStatus.Stopped } + | { + status: RecorderStatus.Starting + } | { status: RecorderStatus.Started stopRecording: () => void @@ -37,30 +41,41 @@ export function makeRumRecorderPublicApi(startRumImpl: StartRum, startRecordingI const { lifeCycle, parentContexts, configuration, session } = startRumResult startSessionReplayRecordingImpl = () => { - if (state.status === RecorderStatus.Started) { + if (state.status !== RecorderStatus.Stopped) { return } - const { stop: stopRecording } = startRecordingImpl( - lifeCycle, - userConfiguration.applicationId, - configuration, - session, - parentContexts - ) - state = { - status: RecorderStatus.Started, - stopRecording, - } - lifeCycle.notify(LifeCycleEventType.RECORD_STARTED) + state = { status: RecorderStatus.Starting } + + runOnReadyState('complete', () => { + if (state.status !== RecorderStatus.Starting) { + return + } + + const { stop: stopRecording } = startRecordingImpl( + lifeCycle, + userConfiguration.applicationId, + configuration, + session, + parentContexts + ) + state = { + status: RecorderStatus.Started, + stopRecording, + } + lifeCycle.notify(LifeCycleEventType.RECORD_STARTED) + }) } stopSessionReplayRecordingImpl = () => { - if (state.status !== RecorderStatus.Started) { + if (state.status === RecorderStatus.Stopped) { return } - state.stopRecording() + if (state.status === RecorderStatus.Started) { + state.stopRecording() + } + state = { status: RecorderStatus.Stopped, } diff --git a/packages/rum-recorder/src/domain/record/record.ts b/packages/rum-recorder/src/domain/record/record.ts index 15292f511e..b62daa4d4c 100644 --- a/packages/rum-recorder/src/domain/record/record.ts +++ b/packages/rum-recorder/src/domain/record/record.ts @@ -1,8 +1,7 @@ -import { runOnReadyState } from '@datadog/browser-core' import { RecordType } from '../../types' import { serializeDocument } from './serialize' import { initObservers } from './observer' -import { IncrementalSource, ListenerHandler, RecordAPI, RecordOptions } from './types' +import { IncrementalSource, RecordAPI, RecordOptions } from './types' import { getWindowHeight, getWindowWidth } from './utils' import { MutationController } from './mutationObserver' @@ -58,92 +57,83 @@ export function record(options: RecordOptions): RecordAPI { }) } - const handlers: ListenerHandler[] = [] - const init = () => { - takeFullSnapshot() + takeFullSnapshot() - handlers.push( - initObservers({ - mutationController, - inputCb: (v) => - emit({ - data: { - source: IncrementalSource.Input, - ...v, - }, - type: RecordType.IncrementalSnapshot, - }), - mediaInteractionCb: (p) => - emit({ - data: { - source: IncrementalSource.MediaInteraction, - ...p, - }, - type: RecordType.IncrementalSnapshot, - }), - mouseInteractionCb: (d) => - emit({ - data: { - source: IncrementalSource.MouseInteraction, - ...d, - }, - type: RecordType.IncrementalSnapshot, - }), - mousemoveCb: (positions, source) => - emit({ - data: { - positions, - source, - }, - type: RecordType.IncrementalSnapshot, - }), - mutationCb: (m) => - emit({ - data: { - source: IncrementalSource.Mutation, - ...m, - }, - type: RecordType.IncrementalSnapshot, - }), - scrollCb: (p) => - emit({ - data: { - source: IncrementalSource.Scroll, - ...p, - }, - type: RecordType.IncrementalSnapshot, - }), - styleSheetRuleCb: (r) => - emit({ - data: { - source: IncrementalSource.StyleSheetRule, - ...r, - }, - type: RecordType.IncrementalSnapshot, - }), - viewportResizeCb: (d) => - emit({ - data: { - source: IncrementalSource.ViewportResize, - ...d, - }, - type: RecordType.IncrementalSnapshot, - }), - focusCb: (data) => - emit({ - type: RecordType.Focus, - data, - }), - }) - ) - } - - runOnReadyState('complete', init) + const stopObservers = initObservers({ + mutationController, + inputCb: (v) => + emit({ + data: { + source: IncrementalSource.Input, + ...v, + }, + type: RecordType.IncrementalSnapshot, + }), + mediaInteractionCb: (p) => + emit({ + data: { + source: IncrementalSource.MediaInteraction, + ...p, + }, + type: RecordType.IncrementalSnapshot, + }), + mouseInteractionCb: (d) => + emit({ + data: { + source: IncrementalSource.MouseInteraction, + ...d, + }, + type: RecordType.IncrementalSnapshot, + }), + mousemoveCb: (positions, source) => + emit({ + data: { + positions, + source, + }, + type: RecordType.IncrementalSnapshot, + }), + mutationCb: (m) => + emit({ + data: { + source: IncrementalSource.Mutation, + ...m, + }, + type: RecordType.IncrementalSnapshot, + }), + scrollCb: (p) => + emit({ + data: { + source: IncrementalSource.Scroll, + ...p, + }, + type: RecordType.IncrementalSnapshot, + }), + styleSheetRuleCb: (r) => + emit({ + data: { + source: IncrementalSource.StyleSheetRule, + ...r, + }, + type: RecordType.IncrementalSnapshot, + }), + viewportResizeCb: (d) => + emit({ + data: { + source: IncrementalSource.ViewportResize, + ...d, + }, + type: RecordType.IncrementalSnapshot, + }), + focusCb: (data) => + emit({ + type: RecordType.Focus, + data, + }), + }) return { - stop: () => { - handlers.forEach((h) => h()) - }, + stop: stopObservers, takeFullSnapshot, } }