Skip to content

Commit 17a1cbb

Browse files
authored
feat: do not start recorder active-active (#1839)
* feat: do not start recorder active-active * flyby maybe reduction * that's just testing the mock * CSP needs updating 🤷
1 parent 3a98f92 commit 17a1cbb

File tree

3 files changed

+55
-21
lines changed

3 files changed

+55
-21
lines changed

playground/nextjs/pages/_app.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export default function App({ Component, pageProps }: AppProps) {
6060
script-src 'self' 'unsafe-eval' 'unsafe-inline' ${localhostDomain} https://*.posthog.com ${CDP_DOMAINS};
6161
style-src 'self' 'unsafe-inline' ${localhostDomain} https://*.posthog.com;
6262
img-src 'self' ${localhostDomain} https://*.posthog.com https://lottie.host https://cataas.com ${CDP_DOMAINS};
63+
worker-src 'self' blob:;
6364
`}
6465
/>
6566
</Head>

src/__tests__/extensions/replay/sessionrecording.test.ts

+27-2
Original file line numberDiff line numberDiff line change
@@ -1478,7 +1478,7 @@ describe('SessionRecording', () => {
14781478
describe('idle timeouts', () => {
14791479
let startingTimestamp = -1
14801480

1481-
function emitInactiveEvent(activityTimestamp: number, expectIdle: boolean = false) {
1481+
function emitInactiveEvent(activityTimestamp: number, expectIdle: boolean | 'unknown' = false) {
14821482
const snapshotEvent = {
14831483
event: 123,
14841484
type: INCREMENTAL_SNAPSHOT_EVENT_TYPE,
@@ -1540,6 +1540,29 @@ describe('SessionRecording', () => {
15401540
jest.useRealTimers()
15411541
})
15421542

1543+
it('starts neither idle nor active', () => {
1544+
expect(sessionRecording['isIdle']).toEqual('unknown')
1545+
})
1546+
1547+
it('does not emit events until after first active event', () => {
1548+
const a = emitInactiveEvent(startingTimestamp + 100, 'unknown')
1549+
const b = emitInactiveEvent(startingTimestamp + 110, 'unknown')
1550+
const c = emitInactiveEvent(startingTimestamp + 120, 'unknown')
1551+
_emit(createFullSnapshot({}), 'unknown')
1552+
expect(sessionRecording['isIdle']).toEqual('unknown')
1553+
expect(posthog.capture).not.toHaveBeenCalled()
1554+
1555+
const d = emitActiveEvent(startingTimestamp + 200)
1556+
expect(sessionRecording['isIdle']).toEqual(false)
1557+
// but all events are buffered
1558+
expect(sessionRecording['buffer']).toEqual({
1559+
data: [a, b, c, createFullSnapshot({}), d],
1560+
sessionId: sessionId,
1561+
size: 442,
1562+
windowId: expect.any(String),
1563+
})
1564+
})
1565+
15431566
it('does not emit plugin events when idle', () => {
15441567
const emptyBuffer = {
15451568
...EMPTY_BUFFER,
@@ -2163,7 +2186,6 @@ describe('SessionRecording', () => {
21632186
sessionRecording.onRRwebEmit(createIncrementalSnapshot({ data: { source: 1 } }) as any)
21642187

21652188
expect(sessionRecording['queuedRRWebEvents']).toHaveLength(0)
2166-
expect(sessionRecording['rrwebRecord']).not.toBeUndefined()
21672189
})
21682190
})
21692191

@@ -2255,6 +2277,9 @@ describe('SessionRecording', () => {
22552277
posthog.config.session_recording.compress_events = true
22562278
sessionRecording.onRemoteConfig(makeDecideResponse({ sessionRecording: { endpoint: '/s/' } }))
22572279
sessionRecording.startIfEnabledOrStop()
2280+
// need to have active event to start recording
2281+
_emit(createIncrementalSnapshot({ type: 3 }))
2282+
sessionRecording['_flushBuffer']()
22582283
})
22592284

22602285
it('compresses full snapshot data', () => {

src/extensions/replay/sessionrecording.ts

+27-19
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ import { sampleOnProperty } from '../sampling'
5454
const LOGGER_PREFIX = '[SessionRecording]'
5555
const logger = createLogger(LOGGER_PREFIX)
5656

57+
function getRRWebRecord(): rrwebRecord | undefined {
58+
return assignableWindow?.__PosthogExtensions__?.rrweb?.record
59+
}
60+
5761
type SessionStartReason =
5862
| 'sampling_overridden'
5963
| 'recording_initialized'
@@ -256,7 +260,7 @@ export class SessionRecording {
256260
private _captureStarted: boolean
257261
private stopRrweb: listenerHandler | undefined
258262
private receivedDecide: boolean
259-
private isIdle = false
263+
private isIdle: boolean | 'unknown' = 'unknown'
260264

261265
private _linkedFlagSeen: boolean = false
262266
private _lastActivityTimestamp: number = Date.now()
@@ -290,10 +294,6 @@ export class SessionRecording {
290294
return this.instance.config.session_recording.session_idle_threshold_ms || RECORDING_IDLE_THRESHOLD_MS
291295
}
292296

293-
private get rrwebRecord(): rrwebRecord | undefined {
294-
return assignableWindow?.__PosthogExtensions__?.rrweb?.record
295-
}
296-
297297
public get started(): boolean {
298298
// TODO could we use status instead of _captureStarted?
299299
return this._captureStarted
@@ -795,7 +795,7 @@ export class SessionRecording {
795795

796796
// If recorder.js is already loaded (if array.full.js snippet is used or posthog-js/dist/recorder is
797797
// imported), don't load script. Otherwise, remotely import recorder.js from cdn since it hasn't been loaded.
798-
if (!this.rrwebRecord) {
798+
if (!getRRWebRecord()) {
799799
assignableWindow.__PosthogExtensions__?.loadExternalDependency?.(this.instance, this.scriptName, (err) => {
800800
if (err) {
801801
return logger.error('could not load recorder', err)
@@ -863,13 +863,18 @@ export class SessionRecording {
863863
if (isUserInteraction) {
864864
this._lastActivityTimestamp = event.timestamp
865865
if (this.isIdle) {
866+
const idleWasUnknown = this.isIdle === 'unknown'
866867
// Remove the idle state
867868
this.isIdle = false
868-
this._tryAddCustomEvent('sessionNoLongerIdle', {
869-
reason: 'user activity',
870-
type: event.type,
871-
})
872-
returningFromIdle = true
869+
// if the idle state was unknown, we don't want to add an event, since we're just in bootup
870+
// whereas if it was true, we know we've been idle for a while, and we can mark ourselves as returning from idle
871+
if (!idleWasUnknown) {
872+
this._tryAddCustomEvent('sessionNoLongerIdle', {
873+
reason: 'user activity',
874+
type: event.type,
875+
})
876+
returningFromIdle = true
877+
}
873878
}
874879
}
875880

@@ -918,11 +923,11 @@ export class SessionRecording {
918923
}
919924

920925
private _tryAddCustomEvent(tag: string, payload: any): boolean {
921-
return this._tryRRWebMethod(newQueuedEvent(() => this.rrwebRecord!.addCustomEvent(tag, payload)))
926+
return this._tryRRWebMethod(newQueuedEvent(() => getRRWebRecord()!.addCustomEvent(tag, payload)))
922927
}
923928

924929
private _tryTakeFullSnapshot(): boolean {
925-
return this._tryRRWebMethod(newQueuedEvent(() => this.rrwebRecord!.takeFullSnapshot()))
930+
return this._tryRRWebMethod(newQueuedEvent(() => getRRWebRecord()!.takeFullSnapshot()))
926931
}
927932

928933
private _onScriptLoaded() {
@@ -972,7 +977,8 @@ export class SessionRecording {
972977
sessionRecordingOptions.blockSelector = this.masking.blockSelector ?? undefined
973978
}
974979

975-
if (!this.rrwebRecord) {
980+
const rrwebRecord = getRRWebRecord()
981+
if (!rrwebRecord) {
976982
logger.error(
977983
'onScriptLoaded was called but rrwebRecord is not available. This indicates something has gone wrong.'
978984
)
@@ -981,7 +987,7 @@ export class SessionRecording {
981987

982988
this.mutationRateLimiter =
983989
this.mutationRateLimiter ??
984-
new MutationRateLimiter(this.rrwebRecord, {
990+
new MutationRateLimiter(rrwebRecord, {
985991
refillRate: this.instance.config.session_recording.__mutationRateLimiterRefillRate,
986992
bucketSize: this.instance.config.session_recording.__mutationRateLimiterBucketSize,
987993
onBlockedNode: (id, node) => {
@@ -995,7 +1001,7 @@ export class SessionRecording {
9951001
})
9961002

9971003
const activePlugins = this._gatherRRWebPlugins()
998-
this.stopRrweb = this.rrwebRecord({
1004+
this.stopRrweb = rrwebRecord({
9991005
emit: (event) => {
10001006
this.onRRwebEmit(event)
10011007
},
@@ -1005,7 +1011,8 @@ export class SessionRecording {
10051011

10061012
// We reset the last activity timestamp, resetting the idle timer
10071013
this._lastActivityTimestamp = Date.now()
1008-
this.isIdle = false
1014+
// stay unknown if we're not sure if we're idle or not
1015+
this.isIdle = isBoolean(this.isIdle) ? this.isIdle : 'unknown'
10091016

10101017
this._tryAddCustomEvent('$session_options', {
10111018
sessionRecordingOptions,
@@ -1022,7 +1029,7 @@ export class SessionRecording {
10221029
clearInterval(this._fullSnapshotTimer)
10231030
}
10241031
// we don't schedule snapshots while idle
1025-
if (this.isIdle) {
1032+
if (this.isIdle === true) {
10261033
return
10271034
}
10281035

@@ -1109,7 +1116,8 @@ export class SessionRecording {
11091116
this._updateWindowAndSessionIds(event)
11101117

11111118
// When in an idle state we keep recording, but don't capture the events,
1112-
if (this.isIdle && !isSessionIdleEvent(event)) {
1119+
// we don't want to return early if idle is 'unknown'
1120+
if (this.isIdle === true && !isSessionIdleEvent(event)) {
11131121
return
11141122
}
11151123

0 commit comments

Comments
 (0)