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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow triggering sessions when events occur #1523

Merged
merged 12 commits into from
Nov 15, 2024
4 changes: 2 additions & 2 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ export const SESSION_RECORDING_SAMPLE_RATE = '$replay_sample_rate'
export const SESSION_RECORDING_MINIMUM_DURATION = '$replay_minimum_duration'
export const SESSION_ID = '$sesid'
export const SESSION_RECORDING_IS_SAMPLED = '$session_is_sampled'
export const SESSION_RECORDING_URL_TRIGGER_ACTIVATED_SESSION = '$session_recording_url_trigger_activated_session'
export const SESSION_RECORDING_URL_TRIGGER_STATUS = '$session_recording_url_trigger_status'
export const SESSION_RECORDING_TRIGGER_ACTIVATED_SESSION = '$session_recording_trigger_activated_session'
export const SESSION_RECORDING_TRIGGER_STATUS = '$session_recording_trigger_status'
export const ENABLED_FEATURE_FLAGS = '$enabled_feature_flags'
export const PERSISTENCE_EARLY_ACCESS_FEATURES = '$early_access_features'
export const STORED_PERSON_PROPERTIES_KEY = '$stored_person_properties'
Expand Down
75 changes: 51 additions & 24 deletions src/extensions/replay/sessionrecording.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import {
CONSOLE_LOG_RECORDING_ENABLED_SERVER_SIDE,
SESSION_RECORDING_CANVAS_RECORDING,
SESSION_RECORDING_ENABLED_SERVER_SIDE,
SESSION_RECORDING_TRIGGER_ACTIVATED_SESSION,
SESSION_RECORDING_TRIGGER_STATUS,
SESSION_RECORDING_IS_SAMPLED,
SESSION_RECORDING_MINIMUM_DURATION,
SESSION_RECORDING_NETWORK_PAYLOAD_CAPTURE,
SESSION_RECORDING_SAMPLE_RATE,
SESSION_RECORDING_URL_TRIGGER_ACTIVATED_SESSION,
SESSION_RECORDING_URL_TRIGGER_STATUS,
} from '../../constants'
import {
estimateSize,
Expand Down Expand Up @@ -266,6 +266,9 @@ export class SessionRecording {

private _urlBlocked: boolean = false

private _eventTriggers: string[] = []
private _removeEventTriggerCaptureHook: (() => void) | undefined = undefined
pauldambra marked this conversation as resolved.
Show resolved Hide resolved

// Util to help developers working on this feature manually override
_forceAllowLocalhostNetworkCapture = false

Expand All @@ -291,7 +294,7 @@ export class SessionRecording {
}

private get fullSnapshotIntervalMillis(): number {
if (this.urlTriggerStatus === 'trigger_pending') {
if (this.triggerStatus === 'trigger_pending') {
return ONE_MINUTE
}

Expand Down Expand Up @@ -389,7 +392,7 @@ export class SessionRecording {
return 'buffering'
}

if (this.urlTriggerStatus === 'trigger_pending') {
if (this.triggerStatus === 'trigger_pending') {
return 'buffering'
}

Expand All @@ -404,17 +407,17 @@ export class SessionRecording {
}
}

private get urlTriggerStatus(): TriggerStatus {
if (this._urlTriggers.length === 0) {
private get triggerStatus(): TriggerStatus {
if (this._urlTriggers.length === 0 && this._eventTriggers.length === 0) {
return 'trigger_disabled'
}

const currentStatus = this.instance?.get_property(SESSION_RECORDING_URL_TRIGGER_STATUS)
const currentTriggerSession = this.instance?.get_property(SESSION_RECORDING_URL_TRIGGER_ACTIVATED_SESSION)
const currentStatus = this.instance?.get_property(SESSION_RECORDING_TRIGGER_STATUS)
const currentTriggerSession = this.instance?.get_property(SESSION_RECORDING_TRIGGER_ACTIVATED_SESSION)

if (currentTriggerSession !== this.sessionId) {
this.instance?.persistence?.unregister(SESSION_RECORDING_URL_TRIGGER_ACTIVATED_SESSION)
this.instance?.persistence?.unregister(SESSION_RECORDING_URL_TRIGGER_STATUS)
this.instance?.persistence?.unregister(SESSION_RECORDING_TRIGGER_ACTIVATED_SESSION)
this.instance?.persistence?.unregister(SESSION_RECORDING_TRIGGER_STATUS)
return 'trigger_pending'
}

Expand All @@ -425,10 +428,10 @@ export class SessionRecording {
return 'trigger_pending'
}

private set urlTriggerStatus(status: TriggerStatus) {
private set triggerStatus(status: TriggerStatus) {
this.instance?.persistence?.register({
[SESSION_RECORDING_URL_TRIGGER_ACTIVATED_SESSION]: this.sessionId,
[SESSION_RECORDING_URL_TRIGGER_STATUS]: status,
[SESSION_RECORDING_TRIGGER_ACTIVATED_SESSION]: this.sessionId,
[SESSION_RECORDING_TRIGGER_STATUS]: status,
})
}

Expand Down Expand Up @@ -491,6 +494,8 @@ export class SessionRecording {
// so we call this here _and_ in the decide response
this._setupSampling()

this._addEventTriggerListener()

if (isNullish(this._removePageViewCaptureHook)) {
// :TRICKY: rrweb does not capture navigation within SPA-s, so hook into our $pageview events to get access to all events.
// Dropping the initial event is fine (it's always captured by rrweb).
Expand All @@ -516,8 +521,8 @@ export class SessionRecording {
if (changeReason) {
this._tryAddCustomEvent('$session_id_change', { sessionId, windowId, changeReason })

this.instance?.persistence?.unregister(SESSION_RECORDING_URL_TRIGGER_ACTIVATED_SESSION)
this.instance?.persistence?.unregister(SESSION_RECORDING_URL_TRIGGER_STATUS)
this.instance?.persistence?.unregister(SESSION_RECORDING_TRIGGER_ACTIVATED_SESSION)
this.instance?.persistence?.unregister(SESSION_RECORDING_TRIGGER_STATUS)
}
})
}
Expand Down Expand Up @@ -644,6 +649,10 @@ export class SessionRecording {
this._urlBlocklist = response.sessionRecording.urlBlocklist
}

if (response.sessionRecording?.eventTriggers) {
this._eventTriggers = response.sessionRecording.eventTriggers
}
pauldambra marked this conversation as resolved.
Show resolved Hide resolved

this.receivedDecide = true
this.startIfEnabledOrStop()
}
Expand Down Expand Up @@ -1012,7 +1021,7 @@ export class SessionRecording {
}

// Check if the URL matches any trigger patterns
this._checkTriggerConditions()
this._checkUrlTriggerConditions()

if (this.status === 'paused' && !isRecordingPausedEvent(rawEvent)) {
return
Expand All @@ -1024,7 +1033,7 @@ export class SessionRecording {
}

// Clear the buffer if waiting for a trigger, and only keep data from after the current full snapshot
if (rawEvent.type === EventType.FullSnapshot && this.urlTriggerStatus === 'trigger_pending') {
if (rawEvent.type === EventType.FullSnapshot && this.triggerStatus === 'trigger_pending') {
this.clearBuffer()
}

Expand Down Expand Up @@ -1205,7 +1214,7 @@ export class SessionRecording {
})
}

private _checkTriggerConditions() {
private _checkUrlTriggerConditions() {
if (typeof window === 'undefined' || !window.location.href) {
return
}
Expand All @@ -1222,16 +1231,16 @@ export class SessionRecording {
}

if (sessionRecordingUrlTriggerMatches(url, this._urlTriggers)) {
this._activateUrlTrigger()
this._activateTrigger('url')
}
}

private _activateUrlTrigger() {
if (this.urlTriggerStatus === 'trigger_pending') {
this.urlTriggerStatus = 'trigger_activated'
this._tryAddCustomEvent('url trigger activated', {})
private _activateTrigger(triggerType: 'url' | 'event') {
if (this.triggerStatus === 'trigger_pending') {
this.triggerStatus = 'trigger_activated'
this._tryAddCustomEvent(`${triggerType} trigger activated`, {})
richard-better marked this conversation as resolved.
Show resolved Hide resolved
this._flushBuffer()
logger.info(LOGGER_PREFIX + ' recording triggered by URL pattern match')
logger.info(LOGGER_PREFIX + ` recording triggered by ${triggerType}`)
pauldambra marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand Down Expand Up @@ -1270,6 +1279,24 @@ export class SessionRecording {
logger.info(LOGGER_PREFIX + ' recording resumed')
}

private _addEventTriggerListener() {
if (this._eventTriggers.length === 0 || !isNullish(this._removeEventTriggerCaptureHook)) {
return
}

this._removeEventTriggerCaptureHook = this.instance._addCaptureHook((eventName) => {
pauldambra marked this conversation as resolved.
Show resolved Hide resolved
// If anything could go wrong here it has the potential to block the main loop,
// so we catch all errors.
try {
if (this._eventTriggers.includes(eventName)) {
this._activateTrigger('event')
}
} catch (e) {
logger.error('Could not activate event trigger', e)
}
})
}

/**
* this ignores the linked flag config and causes capture to start
* (if recording would have started had the flag been received i.e. it does not override other config).
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,7 @@ export interface DecideResponse {
networkPayloadCapture?: Pick<NetworkRecordOptions, 'recordBody' | 'recordHeaders'>
urlTriggers?: SessionRecordingUrlTrigger[]
urlBlocklist?: SessionRecordingUrlTrigger[]
eventTriggers?: string[]
}
surveys?: boolean
toolbarParams: ToolbarParams
Expand Down
Loading