Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/core/src/domain/session/sessionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export interface SessionContext<TrackingType extends string> extends Context {
* just because the user moved to another page.
*/
hasError: boolean
/** Where the detail stored for this session starts, when its events were withheld for a while. */
detailSampledFrom: number | undefined
anonymousId: string | undefined
}

Expand Down Expand Up @@ -99,6 +101,7 @@ export function startSessionManager<TrackingType extends string>(
trackingType: sessionStore.getSession()[productKey] as TrackingType,
isReplayForced: !!sessionStore.getSession().forcedReplay,
hasError: !!sessionStore.getSession().hasError,
detailSampledFrom: Number(sessionStore.getSession().detailFrom) || undefined,
anonymousId: sessionStore.getSession().anonymousId,
}
}
Expand Down
5 changes: 4 additions & 1 deletion packages/rum-core/src/boot/startRum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ export function startRum(
? startRumSessionManager(configuration, lifeCycle, trackingConsentState)
: startRumSessionManagerStub()

// Subscribed before the batch below, and it has to stay that way: the withheld event buffer runs
// on the same event, and only sees a session as released if this has already marked it. Reorder
// them and the release waits for whatever event happens to come next.
const sessionErrorTracking = startSessionErrorTracking(lifeCycle, session)
cleanupTasks.push(() => sessionErrorTracking.stop())

Expand All @@ -121,7 +124,7 @@ export function startRum(
telemetry.observable,
reportError,
pageMayExitObservable,
session.expireObservable,
session,
createEncoder
)
cleanupTasks.push(() => batch.stop())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,7 @@ describe('serializeRumConfiguration', () => {
subdomain: 'foo',
sessionReplaySampleRate: 60,
sessionReplayOnErrorSampleRate: 40,
sessionOnErrorSampleRate: 30,
startSessionReplayRecordingManually: true,
trackUserInteractions: true,
actionNameAttribute: 'test-id',
Expand Down Expand Up @@ -560,6 +561,7 @@ describe('serializeRumConfiguration', () => {
| 'propagateTraceBaggage'
// not reported yet: needs a rum-events-format schema change first
| 'sessionReplayOnErrorSampleRate'
| 'sessionOnErrorSampleRate'
? never
: CamelToSnakeCase<Key>
// By specifying the type here, we can ensure that serializeConfiguration is returning an
Expand Down
17 changes: 17 additions & 0 deletions packages/rum-core/src/domain/configuration/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,19 @@ export interface RumInitConfiguration extends InitConfiguration {
* the withheld minute is uploaded and recording continues normally for the rest of the session.
*/
sessionReplayOnErrorSampleRate?: number | undefined
/**
* The percentage of tracked sessions that collect events but only upload them if the session
* reports an error: 100 for all, 0 for none. Drawn only for sessions that the plain
* `sessionSampleRate` draw missed, so a session is never counted by both rates.
*
* Such a session collects from the start and keeps at most the last minute of it in memory. If it
* never reports an error, nothing is uploaded and the session is not stored. On the first error,
* the withheld minute is uploaded and collection continues normally.
*
* A session sampled this way never uploads its replay ahead of its events: until the events are
* released the session does not exist yet, and a replay sent then would have nothing to attach to.
*/
sessionOnErrorSampleRate?: number | undefined
/**
* If the session is sampled for Session Replay, only start the recording when `startSessionReplayRecording()` is called, instead of at the beginning of the session. Default: if startSessionReplayRecording is 0, true; otherwise, false.
* See [Session Replay Usage](https://docs.datadoghq.com/real_user_monitoring/session_replay/browser/#usage) for further information.
Expand Down Expand Up @@ -186,6 +199,7 @@ export interface RumConfiguration extends Configuration {
enablePrivacyForActionName: boolean
sessionReplaySampleRate: number
sessionReplayOnErrorSampleRate: number
sessionOnErrorSampleRate: number
startSessionReplayRecordingManually: boolean
trackUserInteractions: boolean
trackViewsManually: boolean
Expand Down Expand Up @@ -219,6 +233,7 @@ export function validateAndBuildRumConfiguration(
if (
!isSampleRate(initConfiguration.sessionReplaySampleRate, 'Session Replay') ||
!isSampleRate(initConfiguration.sessionReplayOnErrorSampleRate, 'Session Replay on Error') ||
!isSampleRate(initConfiguration.sessionOnErrorSampleRate, 'Session on Error') ||
!isSampleRate(initConfiguration.traceSampleRate, 'Trace')
) {
return
Expand All @@ -243,13 +258,15 @@ export function validateAndBuildRumConfiguration(

const sessionReplaySampleRate = initConfiguration.sessionReplaySampleRate ?? 0
const sessionReplayOnErrorSampleRate = initConfiguration.sessionReplayOnErrorSampleRate ?? 0
const sessionOnErrorSampleRate = initConfiguration.sessionOnErrorSampleRate ?? 0

return {
applicationId: initConfiguration.applicationId,
version: initConfiguration.version || undefined,
actionNameAttribute: initConfiguration.actionNameAttribute,
sessionReplaySampleRate,
sessionReplayOnErrorSampleRate,
sessionOnErrorSampleRate,
startSessionReplayRecordingManually:
initConfiguration.startSessionReplayRecordingManually !== undefined
? !!initConfiguration.startSessionReplayRecordingManually
Expand Down
8 changes: 8 additions & 0 deletions packages/rum-core/src/domain/contexts/sessionContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,17 @@ export function startSessionContext(

let hasReplay
let sampledForReplay
let sampledForError
let detailSampledFrom
let sampledForErrorReplay
let isActive
if (eventType === RumEventType.VIEW) {
hasReplay = !isReplayWithheld && recorderApi.getReplayStats(view.id) ? true : undefined
sampledForReplay = session.sessionReplay === SessionReplayState.SAMPLED
// Tells the backend that this session's detail only starts where the buffer reached, so the
// gap before it reads as "not collected" rather than as missing data.
sampledForError = session.sampledOnError || undefined
detailSampledFrom = session.detailSampledFrom
// Tells a replay collected only because the session errored apart from one collected
// unconditionally - the two cost differently and are answered by different questions.
sampledForErrorReplay = session.sampledOnErrorReplay || undefined
Expand All @@ -46,6 +52,8 @@ export function startSessionContext(
type: SessionType.USER,
has_replay: hasReplay,
sampled_for_replay: sampledForReplay,
sampled_for_error: sampledForError,
detail_sampled_from: detailSampledFrom,
sampled_for_error_replay: sampledForErrorReplay,
is_active: isActive,
},
Expand Down
73 changes: 73 additions & 0 deletions packages/rum-core/src/domain/rumSessionManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,79 @@ describe('rum session manager', () => {
})
})

describe('on-error session sampling', () => {
const ON_ERROR_ONLY = {
sessionSampleRate: 0,
sessionOnErrorSampleRate: 100,
sessionReplaySampleRate: 0,
sessionReplayOnErrorSampleRate: 0,
}

it('draws the on-error type only when the plain session draw missed', () => {
startRumSessionManagerWithDefaults({
configuration: { ...ON_ERROR_ONLY, sessionSampleRate: 100 },
})

expect(getSessionState(SESSION_STORE_KEY)[RUM_SESSION_KEY]).toBe(RumTrackingType.TRACKED_WITHOUT_SESSION_REPLAY)
})

it('withholds the events of a session drawn on error', () => {
const sessionManager = startRumSessionManagerWithDefaults({ configuration: ON_ERROR_ONLY })

expect(getSessionState(SESSION_STORE_KEY)[RUM_SESSION_KEY]).toBe(
RumTrackingType.TRACKED_ON_ERROR_WITHOUT_SESSION_REPLAY
)
expect(sessionManager.findTrackedSession()!.eventsWithheld).toBeTrue()

sessionManager.setSessionHasError()

expect(sessionManager.findTrackedSession()!.eventsWithheld).toBeFalse()
})

it('withholds the replay alongside the events, even when the plain replay rate was drawn', () => {
// a replay uploaded while the events are withheld would have no session to attach to
const sessionManager = startRumSessionManagerWithDefaults({
configuration: { ...ON_ERROR_ONLY, sessionReplaySampleRate: 100 },
})

expect(getSessionState(SESSION_STORE_KEY)[RUM_SESSION_KEY]).toBe(
RumTrackingType.TRACKED_ON_ERROR_WITH_SESSION_REPLAY
)
expect(sessionManager.findTrackedSession()!.sessionReplay).toBe(SessionReplayState.BUFFERED_ON_ERROR)
})

it('releases events and replay together on the first error', () => {
const sessionManager = startRumSessionManagerWithDefaults({
configuration: { ...ON_ERROR_ONLY, sessionReplaySampleRate: 100 },
})

sessionManager.setSessionHasError()

const session = sessionManager.findTrackedSession()!
expect(session.eventsWithheld).toBeFalse()
expect(session.sessionReplay).toBe(SessionReplayState.SAMPLED)
})

it('releases the events when capture is forced, so the forced replay is not left orphaned', () => {
const sessionManager = startRumSessionManagerWithDefaults({
configuration: { ...ON_ERROR_ONLY, sessionReplaySampleRate: 100 },
})

sessionManager.setForcedReplay()

const session = sessionManager.findTrackedSession()!
expect(session.eventsWithheld).toBeFalse()
expect(session.sessionReplay).toBe(SessionReplayState.FORCED)
})

it('keeps marking the session as on-error once its events have been released', () => {
const sessionManager = startRumSessionManagerWithDefaults({ configuration: ON_ERROR_ONLY })
sessionManager.setSessionHasError()

expect(sessionManager.findTrackedSession()!.sampledOnError).toBeTrue()
})
})

function startRumSessionManagerWithDefaults({ configuration }: { configuration?: Partial<RumConfiguration> } = {}) {
return startRumSessionManager(
mockRumConfiguration({
Expand Down
Loading