@leanbase-giangnd/js 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +143 -0
- package/dist/index.cjs +6012 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +1484 -0
- package/dist/index.mjs +6010 -0
- package/dist/index.mjs.map +1 -0
- package/dist/leanbase.iife.js +13431 -0
- package/dist/leanbase.iife.js.map +1 -0
- package/package.json +48 -0
- package/src/autocapture-utils.ts +550 -0
- package/src/autocapture.ts +415 -0
- package/src/config.ts +8 -0
- package/src/constants.ts +108 -0
- package/src/extensions/rageclick.ts +34 -0
- package/src/extensions/replay/external/config.ts +278 -0
- package/src/extensions/replay/external/denylist.ts +32 -0
- package/src/extensions/replay/external/lazy-loaded-session-recorder.ts +1376 -0
- package/src/extensions/replay/external/mutation-throttler.ts +109 -0
- package/src/extensions/replay/external/network-plugin.ts +701 -0
- package/src/extensions/replay/external/sessionrecording-utils.ts +141 -0
- package/src/extensions/replay/external/triggerMatching.ts +422 -0
- package/src/extensions/replay/rrweb-plugins/patch.ts +39 -0
- package/src/extensions/replay/session-recording.ts +285 -0
- package/src/extensions/replay/types/rrweb-types.ts +575 -0
- package/src/extensions/replay/types/rrweb.ts +114 -0
- package/src/extensions/sampling.ts +26 -0
- package/src/iife.ts +87 -0
- package/src/index.ts +2 -0
- package/src/leanbase-logger.ts +26 -0
- package/src/leanbase-persistence.ts +374 -0
- package/src/leanbase.ts +457 -0
- package/src/page-view.ts +124 -0
- package/src/scroll-manager.ts +103 -0
- package/src/session-props.ts +114 -0
- package/src/sessionid.ts +330 -0
- package/src/storage.ts +410 -0
- package/src/types/fflate.d.ts +5 -0
- package/src/types/rrweb-record.d.ts +8 -0
- package/src/types.ts +807 -0
- package/src/utils/blocked-uas.ts +162 -0
- package/src/utils/element-utils.ts +50 -0
- package/src/utils/event-utils.ts +304 -0
- package/src/utils/index.ts +222 -0
- package/src/utils/logger.ts +26 -0
- package/src/utils/request-utils.ts +128 -0
- package/src/utils/simple-event-emitter.ts +27 -0
- package/src/utils/user-agent-utils.ts +357 -0
- package/src/uuidv7.ts +268 -0
- package/src/version.ts +1 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { SESSION_RECORDING_IS_SAMPLED, SESSION_RECORDING_REMOTE_CONFIG } from '../../constants'
|
|
2
|
+
import { Leanbase } from '../../leanbase'
|
|
3
|
+
import { Properties, RemoteConfig, SessionRecordingPersistedConfig, SessionStartReason } from '../../types'
|
|
4
|
+
import { type eventWithTime } from './types/rrweb-types'
|
|
5
|
+
|
|
6
|
+
import { isNullish, isUndefined } from '@posthog/core'
|
|
7
|
+
import { logger } from '../../leanbase-logger'
|
|
8
|
+
import { window } from '../../utils'
|
|
9
|
+
import { LazyLoadedSessionRecording } from './external/lazy-loaded-session-recorder'
|
|
10
|
+
import { DISABLED, LAZY_LOADING, SessionRecordingStatus, TriggerType } from './external/triggerMatching'
|
|
11
|
+
|
|
12
|
+
const LOGGER_PREFIX = '[SessionRecording]'
|
|
13
|
+
const log = {
|
|
14
|
+
info: (...args: any[]) => logger.info(LOGGER_PREFIX, ...args),
|
|
15
|
+
warn: (...args: any[]) => logger.warn(LOGGER_PREFIX, ...args),
|
|
16
|
+
error: (...args: any[]) => logger.error(LOGGER_PREFIX, ...args),
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class SessionRecording {
|
|
20
|
+
_forceAllowLocalhostNetworkCapture: boolean = false
|
|
21
|
+
|
|
22
|
+
private _receivedFlags: boolean = false
|
|
23
|
+
private _serverRecordingEnabled: boolean = false
|
|
24
|
+
|
|
25
|
+
private _persistFlagsOnSessionListener: (() => void) | undefined = undefined
|
|
26
|
+
private _lazyLoadedSessionRecording: LazyLoadedSessionRecording | undefined
|
|
27
|
+
|
|
28
|
+
public get started(): boolean {
|
|
29
|
+
return !!this._lazyLoadedSessionRecording?.isStarted
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* defaults to buffering mode until a flags response is received
|
|
34
|
+
* once a flags response is received status can be disabled, active or sampled
|
|
35
|
+
*/
|
|
36
|
+
get status(): SessionRecordingStatus {
|
|
37
|
+
if (this._lazyLoadedSessionRecording) {
|
|
38
|
+
return this._lazyLoadedSessionRecording.status
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (this._receivedFlags && !this._isRecordingEnabled) {
|
|
42
|
+
return DISABLED
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return LAZY_LOADING
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
constructor(private readonly _instance: Leanbase) {
|
|
49
|
+
if (!this._instance.sessionManager) {
|
|
50
|
+
log.error('started without valid sessionManager')
|
|
51
|
+
throw new Error(LOGGER_PREFIX + ' started without valid sessionManager. This is a bug.')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (this._instance.config.cookieless_mode === 'always') {
|
|
55
|
+
throw new Error(LOGGER_PREFIX + ' cannot be used with cookieless_mode="always"')
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private get _isRecordingEnabled() {
|
|
60
|
+
if (!window) {
|
|
61
|
+
return false
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!this._serverRecordingEnabled) {
|
|
65
|
+
return false
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (this._instance.config.disable_session_recording) {
|
|
69
|
+
return false
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return true
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
startIfEnabledOrStop(startReason?: SessionStartReason) {
|
|
76
|
+
const canRunReplay = !isUndefined(Object.assign) && !isUndefined(Array.from)
|
|
77
|
+
|
|
78
|
+
if (!this._isRecordingEnabled || !canRunReplay) {
|
|
79
|
+
this.stopRecording()
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (this._lazyLoadedSessionRecording?.isStarted) {
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// According to the rrweb docs, rrweb is not supported on IE11 and below:
|
|
88
|
+
// "rrweb does not support IE11 and below because it uses the MutationObserver API, which was supported by these browsers."
|
|
89
|
+
// https://github.com/rrweb-io/rrweb/blob/master/guide.md#compatibility-note
|
|
90
|
+
//
|
|
91
|
+
// However, MutationObserver does exist on IE11, it just doesn't work well and does not detect all changes.
|
|
92
|
+
// Instead, when we load "recorder.js", the first JS error is about "Object.assign" and "Array.from" being undefined.
|
|
93
|
+
// Thus instead of MutationObserver, we look for this function and block recording if it's undefined.
|
|
94
|
+
this._lazyLoadAndStart(startReason)
|
|
95
|
+
log.info('starting')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* session recording waits until it receives remote config before loading the script
|
|
100
|
+
* this is to ensure we can control the script name remotely
|
|
101
|
+
* and because we wait until we have local and remote config to determine if we should start at all
|
|
102
|
+
* if start is called and there is no remote config then we wait until there is
|
|
103
|
+
*/
|
|
104
|
+
private _lazyLoadAndStart(startReason?: SessionStartReason) {
|
|
105
|
+
// by checking `_isRecordingEnabled` here we know that
|
|
106
|
+
// we have stored remote config and client config to read
|
|
107
|
+
// replay waits for both local and remote config before starting
|
|
108
|
+
if (!this._isRecordingEnabled) {
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
this._onScriptLoaded(startReason)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
stopRecording() {
|
|
116
|
+
this._persistFlagsOnSessionListener?.()
|
|
117
|
+
this._persistFlagsOnSessionListener = undefined
|
|
118
|
+
this._lazyLoadedSessionRecording?.stop()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private _resetSampling() {
|
|
122
|
+
this._instance.persistence?.unregister(SESSION_RECORDING_IS_SAMPLED)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private _persistRemoteConfig(response: RemoteConfig): void {
|
|
126
|
+
if (this._instance.persistence) {
|
|
127
|
+
const persistence = this._instance.persistence
|
|
128
|
+
|
|
129
|
+
const persistResponse = () => {
|
|
130
|
+
const sessionRecordingConfigResponse =
|
|
131
|
+
response.sessionRecording === false ? undefined : response.sessionRecording
|
|
132
|
+
|
|
133
|
+
const receivedSampleRate = sessionRecordingConfigResponse?.sampleRate
|
|
134
|
+
|
|
135
|
+
const parsedSampleRate = isNullish(receivedSampleRate) ? null : parseFloat(receivedSampleRate)
|
|
136
|
+
if (isNullish(parsedSampleRate)) {
|
|
137
|
+
this._resetSampling()
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const receivedMinimumDuration = sessionRecordingConfigResponse?.minimumDurationMilliseconds
|
|
141
|
+
|
|
142
|
+
persistence.register({
|
|
143
|
+
[SESSION_RECORDING_REMOTE_CONFIG]: {
|
|
144
|
+
enabled: !!sessionRecordingConfigResponse,
|
|
145
|
+
...sessionRecordingConfigResponse,
|
|
146
|
+
networkPayloadCapture: {
|
|
147
|
+
capturePerformance: response.capturePerformance,
|
|
148
|
+
...sessionRecordingConfigResponse?.networkPayloadCapture,
|
|
149
|
+
},
|
|
150
|
+
canvasRecording: {
|
|
151
|
+
enabled: sessionRecordingConfigResponse?.recordCanvas,
|
|
152
|
+
fps: sessionRecordingConfigResponse?.canvasFps,
|
|
153
|
+
quality: sessionRecordingConfigResponse?.canvasQuality,
|
|
154
|
+
},
|
|
155
|
+
sampleRate: parsedSampleRate,
|
|
156
|
+
minimumDurationMilliseconds: isUndefined(receivedMinimumDuration)
|
|
157
|
+
? null
|
|
158
|
+
: receivedMinimumDuration,
|
|
159
|
+
endpoint: sessionRecordingConfigResponse?.endpoint,
|
|
160
|
+
triggerMatchType: sessionRecordingConfigResponse?.triggerMatchType,
|
|
161
|
+
masking: sessionRecordingConfigResponse?.masking,
|
|
162
|
+
urlTriggers: sessionRecordingConfigResponse?.urlTriggers,
|
|
163
|
+
} satisfies SessionRecordingPersistedConfig,
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
persistResponse()
|
|
168
|
+
|
|
169
|
+
// in case we see multiple flags responses, we should only use the response from the most recent one
|
|
170
|
+
this._persistFlagsOnSessionListener?.()
|
|
171
|
+
// we 100% know there is a session manager by this point
|
|
172
|
+
this._persistFlagsOnSessionListener = this._instance.sessionManager?.onSessionId(persistResponse)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private _clearRemoteConfig() {
|
|
177
|
+
this._instance.persistence?.unregister(SESSION_RECORDING_REMOTE_CONFIG)
|
|
178
|
+
this._resetSampling()
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
onRemoteConfig(response: RemoteConfig) {
|
|
182
|
+
if (!('sessionRecording' in response)) {
|
|
183
|
+
// if sessionRecording is not in the response, we do nothing
|
|
184
|
+
log.info('skipping remote config with no sessionRecording', response)
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
this._receivedFlags = true
|
|
188
|
+
|
|
189
|
+
if (response.sessionRecording === false) {
|
|
190
|
+
this._serverRecordingEnabled = false
|
|
191
|
+
this._clearRemoteConfig()
|
|
192
|
+
this.stopRecording()
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
this._serverRecordingEnabled = true
|
|
197
|
+
this._persistRemoteConfig(response)
|
|
198
|
+
this.startIfEnabledOrStop()
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
log(message: string, level: 'log' | 'warn' | 'error' = 'log') {
|
|
202
|
+
if (this._lazyLoadedSessionRecording?.log) {
|
|
203
|
+
this._lazyLoadedSessionRecording.log(message, level)
|
|
204
|
+
} else {
|
|
205
|
+
logger.warn('log called before recorder was ready')
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private _onScriptLoaded(startReason?: SessionStartReason) {
|
|
210
|
+
if (!this._lazyLoadedSessionRecording) {
|
|
211
|
+
this._lazyLoadedSessionRecording = new LazyLoadedSessionRecording(this._instance)
|
|
212
|
+
;(this._lazyLoadedSessionRecording as any)._forceAllowLocalhostNetworkCapture =
|
|
213
|
+
this._forceAllowLocalhostNetworkCapture
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
this._lazyLoadedSessionRecording.start(startReason)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* this is maintained on the public API only because it has always been on the public API
|
|
221
|
+
* if you are calling this directly you are certainly doing something wrong
|
|
222
|
+
* @deprecated
|
|
223
|
+
*/
|
|
224
|
+
onRRwebEmit(rawEvent: eventWithTime) {
|
|
225
|
+
this._lazyLoadedSessionRecording?.onRRwebEmit?.(rawEvent)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* this ignores the linked flag config and (if other conditions are met) causes capture to start
|
|
230
|
+
*
|
|
231
|
+
* It is not usual to call this directly,
|
|
232
|
+
* instead call `posthog.startSessionRecording({linked_flag: true})`
|
|
233
|
+
* */
|
|
234
|
+
public overrideLinkedFlag() {
|
|
235
|
+
// TODO what if this gets called before lazy loading is done
|
|
236
|
+
this._lazyLoadedSessionRecording?.overrideLinkedFlag()
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* this ignores the sampling config and (if other conditions are met) causes capture to start
|
|
241
|
+
*
|
|
242
|
+
* It is not usual to call this directly,
|
|
243
|
+
* instead call `posthog.startSessionRecording({sampling: true})`
|
|
244
|
+
* */
|
|
245
|
+
public overrideSampling() {
|
|
246
|
+
// TODO what if this gets called before lazy loading is done
|
|
247
|
+
this._lazyLoadedSessionRecording?.overrideSampling()
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* this ignores the URL/Event trigger config and (if other conditions are met) causes capture to start
|
|
252
|
+
*
|
|
253
|
+
* It is not usual to call this directly,
|
|
254
|
+
* instead call `posthog.startSessionRecording({trigger: 'url' | 'event'})`
|
|
255
|
+
* */
|
|
256
|
+
public overrideTrigger(triggerType: TriggerType) {
|
|
257
|
+
// TODO what if this gets called before lazy loading is done
|
|
258
|
+
this._lazyLoadedSessionRecording?.overrideTrigger(triggerType)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/*
|
|
262
|
+
* whenever we capture an event, we add these properties to the event
|
|
263
|
+
* these are used to debug issues with the session recording
|
|
264
|
+
* when looking at the event feed for a session
|
|
265
|
+
*/
|
|
266
|
+
get sdkDebugProperties(): Properties {
|
|
267
|
+
return (
|
|
268
|
+
this._lazyLoadedSessionRecording?.sdkDebugProperties || {
|
|
269
|
+
$recording_status: this.status,
|
|
270
|
+
}
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* This adds a custom event to the session recording
|
|
276
|
+
*
|
|
277
|
+
* It is not intended for arbitrary public use - playback only displays known custom events
|
|
278
|
+
* And is exposed on the public interface only so that other parts of the SDK are able to use it
|
|
279
|
+
*
|
|
280
|
+
* if you are calling this from client code, you're probably looking for `posthog.capture('$custom_event', {...})`
|
|
281
|
+
*/
|
|
282
|
+
tryAddCustomEvent(tag: string, payload: any): boolean {
|
|
283
|
+
return !!this._lazyLoadedSessionRecording?.tryAddCustomEvent(tag, payload)
|
|
284
|
+
}
|
|
285
|
+
}
|