Skip to content

Commit 4f885c0

Browse files
authored
feat: add local sampleRate config for session recording (#3169)
1 parent f149eb9 commit 4f885c0

File tree

5 files changed

+116
-4
lines changed

5 files changed

+116
-4
lines changed

.changeset/tidy-bats-like.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'posthog-js': minor
3+
'@posthog/types': minor
4+
---
5+
6+
feat: add local sampleRate config for session recording

packages/browser/src/__tests__/extensions/replay/sessionRecording-onRemoteConfig.test.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,71 @@ describe('SessionRecording', () => {
361361
expect(sessionRecording['_lazyLoadedSessionRecording']['_sampleRate']).toBe(0.7)
362362
})
363363

364+
it('local sampleRate takes precedence over remote config', () => {
365+
posthog.config.session_recording.sampleRate = 0.3
366+
367+
sessionRecording.onRemoteConfig(
368+
makeFlagsResponse({
369+
sessionRecording: { endpoint: '/s/', sampleRate: '0.70' },
370+
})
371+
)
372+
373+
expect(posthog.get_property(SESSION_RECORDING_REMOTE_CONFIG).sampleRate).toBe(0.3)
374+
expect(sessionRecording['_lazyLoadedSessionRecording']['_sampleRate']).toBe(0.3)
375+
})
376+
377+
it('local sampleRate of 0 takes precedence over remote config', () => {
378+
posthog.config.session_recording.sampleRate = 0
379+
380+
sessionRecording.onRemoteConfig(
381+
makeFlagsResponse({
382+
sessionRecording: { endpoint: '/s/', sampleRate: '0.70' },
383+
})
384+
)
385+
386+
expect(posthog.get_property(SESSION_RECORDING_REMOTE_CONFIG).sampleRate).toBe(0)
387+
expect(sessionRecording['_lazyLoadedSessionRecording']['_sampleRate']).toBe(0)
388+
})
389+
390+
it('falls back to remote config when local sampleRate is undefined', () => {
391+
posthog.config.session_recording.sampleRate = undefined
392+
393+
sessionRecording.onRemoteConfig(
394+
makeFlagsResponse({
395+
sessionRecording: { endpoint: '/s/', sampleRate: '0.50' },
396+
})
397+
)
398+
399+
expect(posthog.get_property(SESSION_RECORDING_REMOTE_CONFIG).sampleRate).toBe(0.5)
400+
expect(sessionRecording['_lazyLoadedSessionRecording']['_sampleRate']).toBe(0.5)
401+
})
402+
403+
it('ignores local sampleRate greater than 1 and falls back to remote config', () => {
404+
posthog.config.session_recording.sampleRate = 1.5
405+
406+
sessionRecording.onRemoteConfig(
407+
makeFlagsResponse({
408+
sessionRecording: { endpoint: '/s/', sampleRate: '0.70' },
409+
})
410+
)
411+
412+
expect(posthog.get_property(SESSION_RECORDING_REMOTE_CONFIG).sampleRate).toBe(0.7)
413+
expect(sessionRecording['_lazyLoadedSessionRecording']['_sampleRate']).toBe(0.7)
414+
})
415+
416+
it('ignores local sampleRate less than 0 and falls back to remote config', () => {
417+
posthog.config.session_recording.sampleRate = -0.5
418+
419+
sessionRecording.onRemoteConfig(
420+
makeFlagsResponse({
421+
sessionRecording: { endpoint: '/s/', sampleRate: '0.70' },
422+
})
423+
)
424+
425+
expect(posthog.get_property(SESSION_RECORDING_REMOTE_CONFIG).sampleRate).toBe(0.7)
426+
expect(sessionRecording['_lazyLoadedSessionRecording']['_sampleRate']).toBe(0.7)
427+
})
428+
364429
it('starts session recording, saves setting and endpoint when enabled', () => {
365430
posthog.persistence?.register({ [SESSION_RECORDING_REMOTE_CONFIG]: undefined })
366431
sessionRecording.onRemoteConfig(

packages/browser/src/extensions/replay/session-recording.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,18 @@ export class SessionRecording {
148148
this._instance.persistence?.unregister(SESSION_RECORDING_IS_SAMPLED)
149149
}
150150

151+
private _validateSampleRate(rate: unknown, source: string): number | null {
152+
if (isNullish(rate)) {
153+
return null
154+
}
155+
const parsed = parseFloat(rate as string)
156+
if (isNaN(parsed) || parsed < 0 || parsed > 1) {
157+
logger.warn(`${source} must be between 0 and 1. Ignoring invalid value:`, rate)
158+
return null
159+
}
160+
return parsed
161+
}
162+
151163
private _persistRemoteConfig(response: RemoteConfig): void {
152164
if (this._instance.persistence) {
153165
const persistence = this._instance.persistence
@@ -156,9 +168,15 @@ export class SessionRecording {
156168
const sessionRecordingConfigResponse =
157169
response.sessionRecording === false ? undefined : response.sessionRecording
158170

159-
const receivedSampleRate = sessionRecordingConfigResponse?.sampleRate
160-
161-
const parsedSampleRate = isNullish(receivedSampleRate) ? null : parseFloat(receivedSampleRate)
171+
const localSampleRate = this._validateSampleRate(
172+
this._instance.config.session_recording?.sampleRate,
173+
'session_recording.sampleRate'
174+
)
175+
const remoteSampleRate = this._validateSampleRate(
176+
sessionRecordingConfigResponse?.sampleRate,
177+
'remote config sampleRate'
178+
)
179+
const parsedSampleRate = localSampleRate ?? remoteSampleRate
162180
if (isNullish(parsedSampleRate)) {
163181
this._resetSampling()
164182
}

packages/types/src/__tests__/__snapshots__/config-snapshot.spec.ts.snap

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,10 @@ exports[`config snapshot for PostHogConfig 1`] = `
411411
"undefined",
412412
"false",
413413
"true"
414+
],
415+
"sampleRate": [
416+
"undefined",
417+
"number"
414418
]
415419
},
416420
"error_tracking": {

packages/types/src/posthog-config.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,17 @@ export interface SessionRecordingOptions {
527527
* @default false
528528
*/
529529
strictMinimumDuration?: boolean
530+
531+
/**
532+
* The sample rate for session recordings, a number between 0 and 1.
533+
* For example, 0.5 means roughly 50% of sessions will be recorded.
534+
*
535+
* When `undefined`, falls back to the remote config setting.
536+
* When set, takes precedence over the remote config.
537+
*
538+
* @default undefined
539+
*/
540+
sampleRate?: number
530541
}
531542

532543
// we used to call a request that was sent to the queue with options attached `RequestQueueOptions`
@@ -867,6 +878,9 @@ export interface PostHogConfig {
867878

868879
/**
869880
* Determines whether PostHog should enable recording console logs.
881+
*
882+
* This is related to the Session Recording feature. For more session recording
883+
* settings, see the `session_recording` and `capture_performance` configuration option.
870884
* When undefined, it falls back to the remote config setting.
871885
*
872886
* @default undefined
@@ -1017,6 +1031,8 @@ export interface PostHogConfig {
10171031
/**
10181032
* Determines the session recording options.
10191033
*
1034+
* For more session recording settings, see the `enable_recording_console_log` and `capture_performance` configuration option.
1035+
*
10201036
* @see `SessionRecordingOptions`
10211037
* @default {}
10221038
*/
@@ -1246,7 +1262,10 @@ export interface PostHogConfig {
12461262

12471263
/**
12481264
* Determines whether to capture performance metrics.
1249-
* These include Network Timing and Web Vitals.
1265+
* These include Network Timing for Session Replay and Web Vitals.
1266+
*
1267+
* The `network_timing` option is only used by the Session Replay feature.
1268+
* For more session recording settings, see the `session_recording` and `enable_recording_console_log` configuration option.
12501269
*
12511270
* When `undefined`, fallback to the remote configuration.
12521271
* If `false`, neither network timing nor web vitals will work.

0 commit comments

Comments
 (0)