Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

馃悰 [RUM-3039] Fix missing pending mutations at view end #2598

Merged
merged 14 commits into from
Feb 19, 2024
356 changes: 184 additions & 172 deletions packages/rum/src/boot/recorderApi.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { DeflateEncoder, DeflateWorker, DeflateWorkerAction } from '@datadog/browser-core'
import { display, isIE } from '@datadog/browser-core'
import { PageExitReason, display, isIE } from '@datadog/browser-core'
import type { RecorderApi, ViewContexts, LifeCycle, RumConfiguration } from '@datadog/browser-rum-core'
import { LifeCycleEventType } from '@datadog/browser-rum-core'
import { deleteEventBridgeStub, initEventBridgeStub, createNewEvent } from '@datadog/browser-core/test'
Expand Down Expand Up @@ -240,7 +240,7 @@ describe('makeRecorderApi', () => {
})
})

describe('when session renewal change the tracking type', () => {
describe('recorder lifecycle', () => {
let sessionManager: RumSessionManagerMock
let lifeCycle: LifeCycle
beforeEach(() => {
Expand All @@ -249,179 +249,191 @@ describe('makeRecorderApi', () => {
;({ lifeCycle } = setupBuilder.build())
})

describe('from WITHOUT_REPLAY to WITH_REPLAY', () => {
beforeEach(() => {
sessionManager.setTrackedWithoutSessionReplay()
})

it('starts recording if startSessionReplayRecording was called', () => {
rumInit()
sessionManager.setTrackedWithSessionReplay()
lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED)
expect(startRecordingSpy).not.toHaveBeenCalled()
lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED)
expect(startRecordingSpy).toHaveBeenCalled()
expect(stopRecordingSpy).not.toHaveBeenCalled()
})

it('does not starts recording if stopSessionReplayRecording was called', () => {
rumInit()
recorderApi.stop()
sessionManager.setTrackedWithSessionReplay()
lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED)
lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED)
expect(startRecordingSpy).not.toHaveBeenCalled()
})
})

describe('from WITHOUT_REPLAY to untracked', () => {
beforeEach(() => {
sessionManager.setTrackedWithoutSessionReplay()
})

it('keeps not recording if startSessionReplayRecording was called', () => {
rumInit()
sessionManager.setNotTracked()
lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED)
lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED)
expect(startRecordingSpy).not.toHaveBeenCalled()
expect(stopRecordingSpy).not.toHaveBeenCalled()
})
})

describe('from WITHOUT_REPLAY to WITHOUT_REPLAY', () => {
beforeEach(() => {
sessionManager.setTrackedWithoutSessionReplay()
})

it('keeps not recording if startSessionReplayRecording was called', () => {
rumInit()
lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED)
lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED)
expect(startRecordingSpy).not.toHaveBeenCalled()
expect(stopRecordingSpy).not.toHaveBeenCalled()
})
})

describe('from WITH_REPLAY to WITHOUT_REPLAY', () => {
beforeEach(() => {
sessionManager.setTrackedWithSessionReplay()
})

it('stops recording if startSessionReplayRecording was called', () => {
rumInit()
expect(startRecordingSpy).toHaveBeenCalledTimes(1)
sessionManager.setTrackedWithoutSessionReplay()
lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED)
expect(stopRecordingSpy).toHaveBeenCalled()
lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED)
expect(startRecordingSpy).toHaveBeenCalledTimes(1)
})

it('prevents session recording to start if the session is renewed before the DOM is loaded', () => {
const { triggerOnDomLoaded } = mockDocumentReadyState()
rumInit()
sessionManager.setTrackedWithoutSessionReplay()
lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED)
lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED)
triggerOnDomLoaded()
expect(startRecordingSpy).not.toHaveBeenCalled()
})
})

describe('from WITH_REPLAY to untracked', () => {
beforeEach(() => {
sessionManager.setTrackedWithSessionReplay()
})

it('stops recording if startSessionReplayRecording was called', () => {
rumInit()
expect(startRecordingSpy).toHaveBeenCalledTimes(1)
sessionManager.setNotTracked()
lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED)
expect(stopRecordingSpy).toHaveBeenCalled()
lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED)
expect(startRecordingSpy).toHaveBeenCalledTimes(1)
})
})

describe('from WITH_REPLAY to WITH_REPLAY', () => {
beforeEach(() => {
sessionManager.setTrackedWithSessionReplay()
})

it('keeps recording if startSessionReplayRecording was called', () => {
rumInit()
expect(startRecordingSpy).toHaveBeenCalledTimes(1)
lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED)
expect(stopRecordingSpy).toHaveBeenCalled()
lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED)
expect(startRecordingSpy).toHaveBeenCalledTimes(2)
})

it('does not starts recording if stopSessionReplayRecording was called', () => {
rumInit()
expect(startRecordingSpy).toHaveBeenCalledTimes(1)
recorderApi.stop()
expect(stopRecordingSpy).toHaveBeenCalledTimes(1)
lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED)
lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED)
expect(startRecordingSpy).toHaveBeenCalledTimes(1)
expect(stopRecordingSpy).toHaveBeenCalledTimes(1)
})
})

describe('from untracked to REPLAY', () => {
beforeEach(() => {
sessionManager.setNotTracked()
})

it('starts recording if startSessionReplayRecording was called', () => {
rumInit()
sessionManager.setTrackedWithSessionReplay()
lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED)
lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED)
expect(startRecordingSpy).toHaveBeenCalled()
expect(stopRecordingSpy).not.toHaveBeenCalled()
})

it('does not starts recording if stopSessionReplayRecording was called', () => {
rumInit()
recorderApi.stop()
sessionManager.setTrackedWithSessionReplay()
lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED)
lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED)
expect(startRecordingSpy).not.toHaveBeenCalled()
expect(stopRecordingSpy).not.toHaveBeenCalled()
})
})

describe('from untracked to WITHOUT_REPLAY', () => {
beforeEach(() => {
sessionManager.setNotTracked()
})
// prevent getting records after the before_unload event has been triggered.
it('stop recording when the page unloads', () => {
sessionManager.setTrackedWithSessionReplay()
rumInit()
expect(startRecordingSpy).toHaveBeenCalledTimes(1)

it('keeps not recording if startSessionReplayRecording was called', () => {
rumInit()
sessionManager.setTrackedWithoutSessionReplay()
lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED)
lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED)
expect(startRecordingSpy).not.toHaveBeenCalled()
expect(stopRecordingSpy).not.toHaveBeenCalled()
})
lifeCycle.notify(LifeCycleEventType.PAGE_EXITED, { reason: PageExitReason.UNLOADING })
expect(stopRecordingSpy).toHaveBeenCalled()
})

describe('from untracked to untracked', () => {
beforeEach(() => {
sessionManager.setNotTracked()
})

it('keeps not recording if startSessionReplayRecording was called', () => {
rumInit()
lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED)
lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED)
expect(startRecordingSpy).not.toHaveBeenCalled()
expect(stopRecordingSpy).not.toHaveBeenCalled()
describe('when session renewal change the tracking type', () => {
describe('from WITHOUT_REPLAY to WITH_REPLAY', () => {
beforeEach(() => {
sessionManager.setTrackedWithoutSessionReplay()
})

it('starts recording if startSessionReplayRecording was called', () => {
rumInit()
sessionManager.setTrackedWithSessionReplay()
lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED)
expect(startRecordingSpy).not.toHaveBeenCalled()
lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED)
expect(startRecordingSpy).toHaveBeenCalled()
expect(stopRecordingSpy).not.toHaveBeenCalled()
})

it('does not starts recording if stopSessionReplayRecording was called', () => {
rumInit()
recorderApi.stop()
sessionManager.setTrackedWithSessionReplay()
lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED)
lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED)
expect(startRecordingSpy).not.toHaveBeenCalled()
})
})

describe('from WITHOUT_REPLAY to untracked', () => {
beforeEach(() => {
sessionManager.setTrackedWithoutSessionReplay()
})

it('keeps not recording if startSessionReplayRecording was called', () => {
rumInit()
sessionManager.setNotTracked()
lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED)
lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED)
expect(startRecordingSpy).not.toHaveBeenCalled()
expect(stopRecordingSpy).not.toHaveBeenCalled()
})
})

describe('from WITHOUT_REPLAY to WITHOUT_REPLAY', () => {
beforeEach(() => {
sessionManager.setTrackedWithoutSessionReplay()
})

it('keeps not recording if startSessionReplayRecording was called', () => {
rumInit()
lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED)
lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED)
expect(startRecordingSpy).not.toHaveBeenCalled()
expect(stopRecordingSpy).not.toHaveBeenCalled()
})
})

describe('from WITH_REPLAY to WITHOUT_REPLAY', () => {
beforeEach(() => {
sessionManager.setTrackedWithSessionReplay()
})

it('stops recording if startSessionReplayRecording was called', () => {
rumInit()
expect(startRecordingSpy).toHaveBeenCalledTimes(1)
sessionManager.setTrackedWithoutSessionReplay()
lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED)
expect(stopRecordingSpy).toHaveBeenCalled()
lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED)
expect(startRecordingSpy).toHaveBeenCalledTimes(1)
})

it('prevents session recording to start if the session is renewed before the DOM is loaded', () => {
const { triggerOnDomLoaded } = mockDocumentReadyState()
rumInit()
sessionManager.setTrackedWithoutSessionReplay()
lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED)
lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED)
triggerOnDomLoaded()
expect(startRecordingSpy).not.toHaveBeenCalled()
})
})

describe('from WITH_REPLAY to untracked', () => {
beforeEach(() => {
sessionManager.setTrackedWithSessionReplay()
})

it('stops recording if startSessionReplayRecording was called', () => {
rumInit()
expect(startRecordingSpy).toHaveBeenCalledTimes(1)
sessionManager.setNotTracked()
lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED)
expect(stopRecordingSpy).toHaveBeenCalled()
lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED)
expect(startRecordingSpy).toHaveBeenCalledTimes(1)
})
})

describe('from WITH_REPLAY to WITH_REPLAY', () => {
beforeEach(() => {
sessionManager.setTrackedWithSessionReplay()
})

it('keeps recording if startSessionReplayRecording was called', () => {
rumInit()
expect(startRecordingSpy).toHaveBeenCalledTimes(1)
lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED)
expect(stopRecordingSpy).toHaveBeenCalled()
lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED)
expect(startRecordingSpy).toHaveBeenCalledTimes(2)
})

it('does not starts recording if stopSessionReplayRecording was called', () => {
rumInit()
expect(startRecordingSpy).toHaveBeenCalledTimes(1)
recorderApi.stop()
expect(stopRecordingSpy).toHaveBeenCalledTimes(1)
lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED)
lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED)
expect(startRecordingSpy).toHaveBeenCalledTimes(1)
expect(stopRecordingSpy).toHaveBeenCalledTimes(1)
})
})

describe('from untracked to REPLAY', () => {
beforeEach(() => {
sessionManager.setNotTracked()
})

it('starts recording if startSessionReplayRecording was called', () => {
rumInit()
sessionManager.setTrackedWithSessionReplay()
lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED)
lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED)
expect(startRecordingSpy).toHaveBeenCalled()
expect(stopRecordingSpy).not.toHaveBeenCalled()
})

it('does not starts recording if stopSessionReplayRecording was called', () => {
rumInit()
recorderApi.stop()
sessionManager.setTrackedWithSessionReplay()
lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED)
lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED)
expect(startRecordingSpy).not.toHaveBeenCalled()
expect(stopRecordingSpy).not.toHaveBeenCalled()
})
})

describe('from untracked to WITHOUT_REPLAY', () => {
beforeEach(() => {
sessionManager.setNotTracked()
})

it('keeps not recording if startSessionReplayRecording was called', () => {
rumInit()
sessionManager.setTrackedWithoutSessionReplay()
lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED)
lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED)
expect(startRecordingSpy).not.toHaveBeenCalled()
expect(stopRecordingSpy).not.toHaveBeenCalled()
})
})

describe('from untracked to untracked', () => {
beforeEach(() => {
sessionManager.setNotTracked()
})

it('keeps not recording if startSessionReplayRecording was called', () => {
rumInit()
lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED)
lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED)
expect(startRecordingSpy).not.toHaveBeenCalled()
expect(stopRecordingSpy).not.toHaveBeenCalled()
})
})
})
})
Expand Down
8 changes: 7 additions & 1 deletion packages/rum/src/boot/recorderApi.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { DeflateEncoder } from '@datadog/browser-core'
import { DeflateEncoderStreamId, canUseEventBridge, noop, runOnReadyState } from '@datadog/browser-core'
import { DeflateEncoderStreamId, PageExitReason, canUseEventBridge, noop, runOnReadyState } from '@datadog/browser-core'
import type {
LifeCycle,
ViewContexts,
Expand Down Expand Up @@ -97,6 +97,12 @@ export function makeRecorderApi(
}
})

lifeCycle.subscribe(LifeCycleEventType.PAGE_EXITED, (pageExitEvent) => {
if (pageExitEvent.reason === PageExitReason.UNLOADING || pageExitEvent.reason === PageExitReason.PAGEHIDE) {
stopStrategy()
}
})
amortemousque marked this conversation as resolved.
Show resolved Hide resolved

lifeCycle.subscribe(LifeCycleEventType.SESSION_RENEWED, () => {
if (state.status === RecorderStatus.IntentToStart) {
startStrategy()
Expand Down