Skip to content

Commit

Permalink
First pass
Browse files Browse the repository at this point in the history
  • Loading branch information
pauldambra committed Dec 8, 2024
1 parent 26e8ddd commit 262f377
Show file tree
Hide file tree
Showing 8 changed files with 167 additions and 55 deletions.
2 changes: 1 addition & 1 deletion src/__tests__/extensions/replay/sessionrecording.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import {
RECORDING_IDLE_THRESHOLD_MS,
RECORDING_MAX_EVENT_SIZE,
SessionRecording,
} from '../../../extensions/replay/sessionrecording'
} from '../../../entrypoints/sessionrecording'
import { assignableWindow, window } from '../../../utils/globals'
import { RequestRouter } from '../../../utils/request-router'
import {
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/posthog-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { PostHog } from '../posthog-core'
import { PostHogPersistence } from '../posthog-persistence'
import { SessionIdManager } from '../sessionid'
import { RequestQueue } from '../request-queue'
import { SessionRecording } from '../extensions/replay/sessionrecording'
import { SessionRecording } from '../entrypoints/sessionrecording'
import { PostHogFeatureFlags } from '../posthog-featureflags'

describe('posthog core', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,28 @@ import {
SESSION_RECORDING_SAMPLE_RATE,
SESSION_RECORDING_SCRIPT_CONFIG,
SESSION_RECORDING_URL_TRIGGER_ACTIVATED_SESSION,
} from '../../constants'
} from '../constants'
import {
estimateSize,
INCREMENTAL_SNAPSHOT_EVENT_TYPE,
recordOptions,
rrwebRecord,
splitBuffer,
truncateLargeConsoleLogs,
} from './sessionrecording-utils'
import { PostHog } from '../../posthog-core'
} from '../extensions/replay/sessionrecording-utils'
import { PostHog } from '../posthog-core'
import {
CaptureResult,
FlagVariant,
NetworkRecordOptions,
NetworkRequest,
Properties,
RemoteConfig,
SessionRecordingStatus,
SessionRecordingUrlTrigger,
} from '../../types'
SessionStartReason,
TriggerType,
} from '../types'
import {
customEvent,
EventType,
Expand All @@ -37,29 +40,25 @@ import {
RecordPlugin,
} from '@rrweb/types'

import { isBoolean, isFunction, isNullish, isNumber, isObject, isString, isUndefined } from '../../utils/type-utils'
import { createLogger } from '../../utils/logger'
import { assignableWindow, document, PostHogExtensionKind, window } from '../../utils/globals'
import { buildNetworkRequestOptions } from './config'
import { isLocalhost } from '../../utils/request-utils'
import { MutationRateLimiter } from './mutation-rate-limiter'
import { isBoolean, isFunction, isNullish, isNumber, isObject, isString, isUndefined } from '../utils/type-utils'
import { createLogger } from '../utils/logger'
import {
assignableWindow,
document,
LazyLoadedSessionRecordingInterface,
PostHogExtensionKind,
window,
} from '../utils/globals'
import { buildNetworkRequestOptions } from '../extensions/replay/config'
import { isLocalhost } from '../utils/request-utils'
import { MutationRateLimiter } from '../extensions/replay/mutation-rate-limiter'
import { gzipSync, strFromU8, strToU8 } from 'fflate'
import { clampToRange } from '../../utils/number-utils'
import { includes } from '../../utils'
import { clampToRange } from '../utils/number-utils'
import { includes } from '../utils'

const LOGGER_PREFIX = '[SessionRecording]'
const logger = createLogger(LOGGER_PREFIX)

type SessionStartReason =
| 'sampling_overridden'
| 'recording_initialized'
| 'linked_flag_matched'
| 'linked_flag_overridden'
| 'sampled'
| 'session_id_changed'
| 'url_trigger_matched'
| 'event_trigger_matched'

const BASE_ENDPOINT = '/s/'

const ONE_MINUTE = 1000 * 60
Expand All @@ -83,17 +82,8 @@ const ACTIVE_SOURCES = [
IncrementalSource.Drag,
]

export type TriggerType = 'url' | 'event'
type TriggerStatus = 'trigger_activated' | 'trigger_pending' | 'trigger_disabled'

/**
* Session recording starts in buffering mode while waiting for decide response
* Once the response is received it might be disabled, active or sampled
* When sampled that means a sample rate is set and the last time the session id was rotated
* the sample rate determined this session should be sent to the server.
*/
type SessionRecordingStatus = 'disabled' | 'sampled' | 'active' | 'buffering' | 'paused'

export interface SnapshotBuffer {
size: number
data: any[]
Expand Down Expand Up @@ -235,7 +225,7 @@ function isRecordingPausedEvent(e: eventWithTime) {
return e.type === EventType.Custom && e.data.tag === 'recording paused'
}

export class SessionRecording {
export class LazyLoadedSessionRecording implements LazyLoadedSessionRecordingInterface {
private _endpoint: string
private flushBufferTimer?: any

Expand Down Expand Up @@ -483,7 +473,7 @@ export class SessionRecording {
}
}

startIfEnabledOrStop(startReason?: SessionStartReason) {
start(startReason?: SessionStartReason) {
if (this.isRecordingEnabled) {
this._startCapture(startReason)

Expand Down Expand Up @@ -532,11 +522,11 @@ export class SessionRecording {
})
}
} else {
this.stopRecording()
this.stop()
}
}

stopRecording() {
stop() {
if (this._captureStarted && this.stopRrweb) {
this.stopRrweb()
this.stopRrweb = undefined
Expand Down Expand Up @@ -656,7 +646,7 @@ export class SessionRecording {
}

this.receivedDecide = true
this.startIfEnabledOrStop()
this.start()
}

/**
Expand Down Expand Up @@ -709,7 +699,7 @@ export class SessionRecording {
}

log(message: string, level: 'log' | 'warn' | 'error' = 'log') {
this.instance.sessionRecording?.onRRwebEmit({
this.onRRwebEmit({
type: 6,
data: {
plugin: 'rrweb/console@1',
Expand Down Expand Up @@ -847,8 +837,8 @@ export class SessionRecording {
this.sessionId = sessionId

if (sessionIdChanged || windowIdChanged) {
this.stopRecording()
this.startIfEnabledOrStop('session_id_changed')
this.stop()
this.start('session_id_changed')
} else if (returningFromIdle) {
this._scheduleFullSnapshot()
}
Expand Down Expand Up @@ -1351,8 +1341,13 @@ export class SessionRecording {
$session_recording_start_reason: startReason,
})
logger.info(startReason.replace('_', ' '), tagPayload)
if (!includes(['recording_initialized', 'session_id_changed'], startReason)) {
if (!includes(['session_id_changed'], startReason)) {
this._tryAddCustomEvent(startReason, tagPayload)
}
}
}

assignableWindow.__PosthogExtensions__ = assignableWindow.__PosthogExtensions__ || {}
assignableWindow.__PosthogExtensions__.initSessionRecording = (ph) => new LazyLoadedSessionRecording(ph)

export default LazyLoadedSessionRecording
75 changes: 75 additions & 0 deletions src/extensions/replay/session-recording-loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { assignableWindow, document, LazyLoadedSessionRecordingInterface, window } from '../../utils/globals'
import { PostHog } from '../../posthog-core'
import { RemoteConfig } from '../../types'
import { createLogger } from '../../utils/logger'
import { SESSION_RECORDING_ENABLED_SERVER_SIDE } from '../../constants'

const logger = createLogger('[Session-Recording-Loader]')

export const isSessionRecordingEnabled = (loader: SessionRecordingLoader) => {
const enabled_server_side = !!loader.instance.get_property(SESSION_RECORDING_ENABLED_SERVER_SIDE)
const enabled_client_side = !loader.instance.config.disable_session_recording
return !!window && enabled_server_side && enabled_client_side
}

export class SessionRecordingLoader {
get lazyLoaded(): LazyLoadedSessionRecordingInterface | undefined {
return this._lazyLoadedSessionRecording
}

private _lazyLoadedSessionRecording: LazyLoadedSessionRecordingInterface | undefined

constructor(readonly instance: PostHog, readonly isEnabled: (srl: SessionRecordingLoader) => boolean) {
this.startIfEnabled()
}

public onRemoteConfig(response: RemoteConfig) {
if (this.instance.persistence) {
this._lazyLoadedSessionRecording?.onRemoteConfig(response)
}
this.startIfEnabled()
}

public startIfEnabled() {
if (this.isEnabled(this)) {
this.loadScript(() => {
this.start()
})
}
}

private loadScript(cb: () => void): void {
if (assignableWindow.__PosthogExtensions__?.initSessionRecording) {
// already loaded
cb()
}
assignableWindow.__PosthogExtensions__?.loadExternalDependency?.(this.instance, 'session-recording', (err) => {
if (err) {
logger.error('failed to load script', err)
return
}
cb()
})
}

private start() {
if (!document) {
logger.error('`document` not found. Cannot start.')
return
}

if (!this._lazyLoadedSessionRecording && assignableWindow.__PosthogExtensions__?.initSessionRecording) {
this._lazyLoadedSessionRecording = assignableWindow.__PosthogExtensions__.initSessionRecording(
this.instance
)
this._lazyLoadedSessionRecording.start()
}
}

stop() {
if (this._lazyLoadedSessionRecording) {
this._lazyLoadedSessionRecording.stop()
this._lazyLoadedSessionRecording = undefined
}
}
}
2 changes: 1 addition & 1 deletion src/extensions/replay/sessionrecording-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { eventWithTime, listenerHandler, pluginEvent } from '@rrweb/types'
import type { record } from '@rrweb/record'

import { isObject } from '../../utils/type-utils'
import { SnapshotBuffer } from './sessionrecording'
import { SnapshotBuffer } from '../../entrypoints/sessionrecording'

Check failure on line 5 in src/extensions/replay/sessionrecording-utils.ts

View workflow job for this annotation

GitHub Actions / Build and check ES5/ES6 support

Cannot find module '../../entrypoints/sessionrecording' or its corresponding type declarations.

Check failure on line 5 in src/extensions/replay/sessionrecording-utils.ts

View workflow job for this annotation

GitHub Actions / Test with React

Cannot find module '../../entrypoints/sessionrecording' or its corresponding type declarations.

Check failure on line 5 in src/extensions/replay/sessionrecording-utils.ts

View workflow job for this annotation

GitHub Actions / Test on Chrome

Cannot find module '../../entrypoints/sessionrecording' or its corresponding type declarations.

Check failure on line 5 in src/extensions/replay/sessionrecording-utils.ts

View workflow job for this annotation

GitHub Actions / Lint

Cannot find module '../../entrypoints/sessionrecording' or its corresponding type declarations.

Check failure on line 5 in src/extensions/replay/sessionrecording-utils.ts

View workflow job for this annotation

GitHub Actions / Test on Firefox

Cannot find module '../../entrypoints/sessionrecording' or its corresponding type declarations.

Check failure on line 5 in src/extensions/replay/sessionrecording-utils.ts

View workflow job for this annotation

GitHub Actions / Test on IE11

Cannot find module '../../entrypoints/sessionrecording' or its corresponding type declarations.

Check failure on line 5 in src/extensions/replay/sessionrecording-utils.ts

View workflow job for this annotation

GitHub Actions / Cypress

Cannot find module '../../entrypoints/sessionrecording' or its corresponding type declarations.

Check failure on line 5 in src/extensions/replay/sessionrecording-utils.ts

View workflow job for this annotation

GitHub Actions / Unit tests

Cannot find module '../../entrypoints/sessionrecording' or its corresponding type declarations.

Check failure on line 5 in src/extensions/replay/sessionrecording-utils.ts

View workflow job for this annotation

GitHub Actions / Test on Safari

Cannot find module '../../entrypoints/sessionrecording' or its corresponding type declarations.

// taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value#circular_references
export function circularReferenceReplacer() {
Expand Down
22 changes: 11 additions & 11 deletions src/posthog-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import {
USER_STATE,
ENABLE_PERSON_PROCESSING,
} from './constants'
import { SessionRecording } from './extensions/replay/sessionrecording'
import { Decide } from './decide'
import { Toolbar } from './extensions/toolbar'
import { localStore } from './storage'
Expand Down Expand Up @@ -82,6 +81,7 @@ import { WebExperiments } from './web-experiments'
import { PostHogExceptions } from './posthog-exceptions'
import { SiteApps } from './site-apps'
import { DeadClicksAutocapture, isDeadClicksEnabledForAutocapture } from './extensions/dead-clicks-autocapture'
import { isSessionRecordingEnabled, SessionRecordingLoader } from './extensions/replay/session-recording-loader'

/*
SIMPLE STYLE GUIDE:
Expand Down Expand Up @@ -268,7 +268,7 @@ export class PostHog {

_requestQueue?: RequestQueue
_retryQueue?: RetryQueue
sessionRecording?: SessionRecording
sessionRecording?: SessionRecordingLoader
webPerformance = new DeprecatedWebPerformanceObserver()

_initialPageviewCaptured: boolean
Expand Down Expand Up @@ -437,8 +437,8 @@ export class PostHog {
this.siteApps = new SiteApps(this)
this.siteApps?.init()

this.sessionRecording = new SessionRecording(this)
this.sessionRecording.startIfEnabledOrStop()
this.sessionRecording = new SessionRecordingLoader(this, isSessionRecordingEnabled)
this.sessionRecording.startIfEnabled()

if (!this.config.disable_scroll_properties) {
this.scrollManager.startMeasuringScrollPosition()
Expand Down Expand Up @@ -937,7 +937,7 @@ export class PostHog {
}

if (this.sessionRecording) {
properties['$recording_status'] = this.sessionRecording.status
properties['$recording_status'] = this.sessionRecording.lazyLoaded?.status
}

if (this.requestRouter.region === RequestRouterRegion.CUSTOM) {
Expand Down Expand Up @@ -1817,7 +1817,7 @@ export class PostHog {
})
}

this.sessionRecording?.startIfEnabledOrStop()
this.sessionRecording?.startIfEnabled()
this.autocapture?.startIfEnabled()
this.heatmaps?.startIfEnabled()
this.surveys.loadIfEnabled()
Expand Down Expand Up @@ -1849,19 +1849,19 @@ export class PostHog {
this.sessionManager?.checkAndGetSessionAndWindowId()

if (overrideConfig.sampling) {
this.sessionRecording?.overrideSampling()
this.sessionRecording?.lazyLoaded?.overrideSampling()
}

if (overrideConfig.linked_flag) {
this.sessionRecording?.overrideLinkedFlag()
this.sessionRecording?.lazyLoaded?.overrideLinkedFlag()
}

if (overrideConfig.url_trigger) {
this.sessionRecording?.overrideTrigger('url')
this.sessionRecording?.lazyLoaded?.overrideTrigger('url')
}

if (overrideConfig.event_trigger) {
this.sessionRecording?.overrideTrigger('event')
this.sessionRecording?.lazyLoaded?.overrideTrigger('event')
}
}

Expand All @@ -1881,7 +1881,7 @@ export class PostHog {
* is currently running
*/
sessionRecordingStarted(): boolean {
return !!this.sessionRecording?.started
return !!this.sessionRecording?.lazyLoaded?.started
}

/** Capture a caught exception manually */
Expand Down
20 changes: 20 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,26 @@ export type SessionIdChangedCallback = (
changeReason?: { noSessionId: boolean; activityTimeout: boolean; sessionPastMaximumLength: boolean }
) => void

export type SessionStartReason =
| 'recording_initialized'
| 'sampling_overridden'
| 'linked_flag_matched'
| 'linked_flag_overridden'
| 'sampled'
| 'session_id_changed'
| 'url_trigger_matched'
| 'event_trigger_matched'

/**
* Session recording starts in buffering mode while waiting for decide response
* Once the response is received it might be disabled, active or sampled
* When sampled that means a sample rate is set and the last time the session id was rotated
* the sample rate determined this session should be sent to the server.
*/
export type SessionRecordingStatus = 'disabled' | 'sampled' | 'active' | 'buffering' | 'paused'

export type TriggerType = 'url' | 'event'

export enum Compression {
GZipJS = 'gzip-js',
Base64 = 'base64',
Expand Down
Loading

0 comments on commit 262f377

Please sign in to comment.