Skip to content

Commit

Permalink
fix: less strict idle event dropping (#1241)
Browse files Browse the repository at this point in the history
  • Loading branch information
pauldambra committed Jun 12, 2024
1 parent 1d54df9 commit 836cdfa
Show file tree
Hide file tree
Showing 2 changed files with 177 additions and 45 deletions.
174 changes: 153 additions & 21 deletions src/__tests__/extensions/replay/sessionrecording.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,17 @@ import {
} from '../../../extensions/replay/sessionrecording'
import { assignableWindow, window } from '../../../utils/globals'
import { RequestRouter } from '../../../utils/request-router'
import { customEvent, EventType, eventWithTime, pluginEvent } from '@rrweb/types'
import {
customEvent,
EventType,
eventWithTime,
fullSnapshotEvent,
incrementalSnapshotEvent,
IncrementalSource,
metaEvent,
pluginEvent,
} from '@rrweb/types'
import { loadScript } from '../../../utils'
import Mock = jest.Mock

// Type and source defined here designate a non-user-generated recording event
Expand All @@ -35,8 +45,6 @@ jest.mock('../../../utils', () => ({
}))
jest.mock('../../../config', () => ({ LIB_VERSION: 'v0.0.1' }))

import { loadScript } from '../../../utils'

const loadScriptMock = loadScript as jest.Mock

const EMPTY_BUFFER = {
Expand All @@ -46,19 +54,32 @@ const EMPTY_BUFFER = {
windowId: null,
}

const createMetaSnapshot = (event = {}) => ({
type: META_EVENT_TYPE,
data: {},
...event,
})

const createFullSnapshot = (event = {}) => ({
type: FULL_SNAPSHOT_EVENT_TYPE,
data: {},
...event,
})

const createIncrementalSnapshot = (event = {}) => ({
const createMetaSnapshot = (event = {}): metaEvent =>
({
type: META_EVENT_TYPE,
data: {
href: 'https://has-to-be-present-or-invalid.com',
},
...event,
} as metaEvent)

const createStyleSnapshot = (event = {}): incrementalSnapshotEvent =>
({
type: INCREMENTAL_SNAPSHOT_EVENT_TYPE,
data: {
source: IncrementalSource.StyleDeclaration,
},
...event,
} as incrementalSnapshotEvent)

const createFullSnapshot = (event = {}): fullSnapshotEvent =>
({
type: FULL_SNAPSHOT_EVENT_TYPE,
data: {},
...event,
} as fullSnapshotEvent)

const createIncrementalSnapshot = (event = {}): incrementalSnapshotEvent => ({
type: INCREMENTAL_SNAPSHOT_EVENT_TYPE,
data: {
source: 1,
Expand Down Expand Up @@ -1225,7 +1246,7 @@ describe('SessionRecording', () => {
_addCustomEvent.mockClear()
})

it('does not emit when idle', () => {
it('does not emit plugin events when idle', () => {
const emptyBuffer = {
...EMPTY_BUFFER,
sessionId: sessionId,
Expand All @@ -1236,11 +1257,41 @@ describe('SessionRecording', () => {
sessionRecording['isIdle'] = true
// buffer is empty
expect(sessionRecording['buffer']).toEqual(emptyBuffer)

sessionRecording.onRRwebEmit(createPluginSnapshot({}) as eventWithTime)

// a plugin event doesn't count as returning from idle
sessionRecording.onRRwebEmit(createPluginSnapshot({}) as unknown as eventWithTime)
expect(sessionRecording['isIdle']).toEqual(true)
expect(sessionRecording['buffer']).toEqual({
...EMPTY_BUFFER,
sessionId: sessionId,
windowId: 'windowId',
})
})

// buffer is still empty
it('active incremental events return from idle', () => {
const emptyBuffer = {
...EMPTY_BUFFER,
sessionId: sessionId,
windowId: 'windowId',
}

// force idle state
sessionRecording['isIdle'] = true
// buffer is empty
expect(sessionRecording['buffer']).toEqual(emptyBuffer)

sessionRecording.onRRwebEmit(createIncrementalSnapshot({}) as eventWithTime)

// an incremental event counts as returning from idle
expect(sessionRecording['isIdle']).toEqual(false)
// buffer contains event allowed when idle
expect(sessionRecording['buffer']).toEqual({
data: [createFullSnapshot(), createIncrementalSnapshot({})],
sessionId: sessionId,
size: 50,
windowId: 'windowId',
})
})

it('emits custom events even when idle', () => {
Expand All @@ -1253,7 +1304,7 @@ describe('SessionRecording', () => {
windowId: 'windowId',
})

sessionRecording.onRRwebEmit(createCustomSnapshot({}) as unknown as eventWithTime)
sessionRecording.onRRwebEmit(createCustomSnapshot({}) as eventWithTime)

// custom event is buffered
expect(sessionRecording['buffer']).toEqual({
Expand All @@ -1272,6 +1323,88 @@ describe('SessionRecording', () => {
})
})

it('emits full snapshot events even when idle', () => {
// force idle state
sessionRecording['isIdle'] = true
// buffer is empty
expect(sessionRecording['buffer']).toEqual({
...EMPTY_BUFFER,
sessionId: sessionId,
windowId: 'windowId',
})

sessionRecording.onRRwebEmit(createFullSnapshot({}) as eventWithTime)

// custom event is buffered
expect(sessionRecording['buffer']).toEqual({
data: [
{
data: {},
type: 2,
},
],
sessionId: sessionId,
size: 20,
windowId: 'windowId',
})
})

it('emits meta snapshot events even when idle', () => {
// force idle state
sessionRecording['isIdle'] = true
// buffer is empty
expect(sessionRecording['buffer']).toEqual({
...EMPTY_BUFFER,
sessionId: sessionId,
windowId: 'windowId',
})

sessionRecording.onRRwebEmit(createMetaSnapshot({}) as eventWithTime)

// custom event is buffered
expect(sessionRecording['buffer']).toEqual({
data: [
{
data: {
href: 'https://has-to-be-present-or-invalid.com',
},
type: 4,
},
],
sessionId: sessionId,
size: 69,
windowId: 'windowId',
})
})

it('emits style snapshot events even when idle', () => {
// force idle state
sessionRecording['isIdle'] = true
// buffer is empty
expect(sessionRecording['buffer']).toEqual({
...EMPTY_BUFFER,
sessionId: sessionId,
windowId: 'windowId',
})

sessionRecording.onRRwebEmit(createStyleSnapshot({}) as eventWithTime)

// custom event is buffered
expect(sessionRecording['buffer']).toEqual({
data: [
{
data: {
source: 13,
},
type: 3,
},
],
sessionId: sessionId,
size: 31,
windowId: 'windowId',
})
})

it("enters idle state within one session if the activity is non-user generated and there's no activity for (RECORDING_IDLE_ACTIVITY_TIMEOUT_MS) 5 minutes", () => {
const firstActivityTimestamp = startingTimestamp + 100
const secondActivityTimestamp = startingTimestamp + 200
Expand Down Expand Up @@ -1347,7 +1480,6 @@ describe('SessionRecording', () => {
)

// this triggers exit from idle state _and_ is a user interaction, so we take a full snapshot

const fourthSnapshot = emitActiveEvent(fourthActivityTimestamp)
expect(_addCustomEvent).toHaveBeenCalledWith('sessionNoLongerIdle', {
reason: 'user activity',
Expand Down
48 changes: 24 additions & 24 deletions src/extensions/replay/sessionrecording.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
} from './sessionrecording-utils'
import { PostHog } from '../../posthog-core'
import { DecideResponse, FlagVariant, NetworkRecordOptions, NetworkRequest, Properties } from '../../types'
import { EventType, type eventWithTime, type listenerHandler, RecordPlugin } from '@rrweb/types'
import { EventType, type eventWithTime, IncrementalSource, type listenerHandler, RecordPlugin } from '@rrweb/types'
import Config from '../../config'
import { timestamp, loadScript } from '../../utils'

Expand Down Expand Up @@ -50,26 +50,6 @@ export const SESSION_RECORDING_BATCH_KEY = 'recordings'
// import type { record } from 'rrweb2/typings'
// import type { recordOptions } from 'rrweb/typings/types'

// Copied from rrweb typings to avoid import
enum IncrementalSource {
Mutation = 0,
MouseMove = 1,
MouseInteraction = 2,
Scroll = 3,
ViewportResize = 4,
Input = 5,
TouchMove = 6,
MediaInteraction = 7,
StyleSheetRule = 8,
CanvasMutation = 9,
Font = 10,
Log = 11,
Drag = 12,
StyleDeclaration = 13,
Selection = 14,
AdoptedStyleSheet = 15,
}

const ACTIVE_SOURCES = [
IncrementalSource.MouseMove,
IncrementalSource.MouseInteraction,
Expand All @@ -81,6 +61,27 @@ const ACTIVE_SOURCES = [
IncrementalSource.Drag,
]

const STYLE_SOURCES = [
IncrementalSource.StyleSheetRule,
IncrementalSource.StyleDeclaration,
IncrementalSource.AdoptedStyleSheet,
IncrementalSource.Font,
]

const TYPES_ALLOWED_WHEN_IDLE = [EventType.Custom, EventType.Meta, EventType.FullSnapshot]

/**
* we want to restrict the data allowed when we've detected an idle session
* but allow data that the player might require for proper playback
*/
function allowedWhenIdle(event: eventWithTime): boolean {
const isAllowedIncremental =
event.type === EventType.IncrementalSnapshot &&
!isNullish(event.data.source) &&
STYLE_SOURCES.includes(event.data.source)
return TYPES_ALLOWED_WHEN_IDLE.includes(event.type) || isAllowedIncremental
}

/**
* Session recording starts in buffering mode while waiting for decide response
* Once the response is received it might be disabled, active or sampled
Expand Down Expand Up @@ -818,9 +819,8 @@ export class SessionRecording {

this._updateWindowAndSessionIds(event)

// allow custom events even when idle
if (this.isIdle && event.type !== EventType.Custom) {
// When in an idle state we keep recording, but don't capture the events
if (this.isIdle && !allowedWhenIdle(event)) {
// When in an idle state we keep recording, but don't capture all events
return
}

Expand Down

0 comments on commit 836cdfa

Please sign in to comment.