@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.
Files changed (49) hide show
  1. package/README.md +143 -0
  2. package/dist/index.cjs +6012 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.ts +1484 -0
  5. package/dist/index.mjs +6010 -0
  6. package/dist/index.mjs.map +1 -0
  7. package/dist/leanbase.iife.js +13431 -0
  8. package/dist/leanbase.iife.js.map +1 -0
  9. package/package.json +48 -0
  10. package/src/autocapture-utils.ts +550 -0
  11. package/src/autocapture.ts +415 -0
  12. package/src/config.ts +8 -0
  13. package/src/constants.ts +108 -0
  14. package/src/extensions/rageclick.ts +34 -0
  15. package/src/extensions/replay/external/config.ts +278 -0
  16. package/src/extensions/replay/external/denylist.ts +32 -0
  17. package/src/extensions/replay/external/lazy-loaded-session-recorder.ts +1376 -0
  18. package/src/extensions/replay/external/mutation-throttler.ts +109 -0
  19. package/src/extensions/replay/external/network-plugin.ts +701 -0
  20. package/src/extensions/replay/external/sessionrecording-utils.ts +141 -0
  21. package/src/extensions/replay/external/triggerMatching.ts +422 -0
  22. package/src/extensions/replay/rrweb-plugins/patch.ts +39 -0
  23. package/src/extensions/replay/session-recording.ts +285 -0
  24. package/src/extensions/replay/types/rrweb-types.ts +575 -0
  25. package/src/extensions/replay/types/rrweb.ts +114 -0
  26. package/src/extensions/sampling.ts +26 -0
  27. package/src/iife.ts +87 -0
  28. package/src/index.ts +2 -0
  29. package/src/leanbase-logger.ts +26 -0
  30. package/src/leanbase-persistence.ts +374 -0
  31. package/src/leanbase.ts +457 -0
  32. package/src/page-view.ts +124 -0
  33. package/src/scroll-manager.ts +103 -0
  34. package/src/session-props.ts +114 -0
  35. package/src/sessionid.ts +330 -0
  36. package/src/storage.ts +410 -0
  37. package/src/types/fflate.d.ts +5 -0
  38. package/src/types/rrweb-record.d.ts +8 -0
  39. package/src/types.ts +807 -0
  40. package/src/utils/blocked-uas.ts +162 -0
  41. package/src/utils/element-utils.ts +50 -0
  42. package/src/utils/event-utils.ts +304 -0
  43. package/src/utils/index.ts +222 -0
  44. package/src/utils/logger.ts +26 -0
  45. package/src/utils/request-utils.ts +128 -0
  46. package/src/utils/simple-event-emitter.ts +27 -0
  47. package/src/utils/user-agent-utils.ts +357 -0
  48. package/src/uuidv7.ts +268 -0
  49. 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
+ }