@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,141 @@
1
+ import type { eventWithTime, pluginEvent } from '../types/rrweb-types'
2
+
3
+ import { isObject } from '@posthog/core'
4
+ import { SnapshotBuffer } from './lazy-loaded-session-recorder'
5
+
6
+ // taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value#circular_references
7
+ export function circularReferenceReplacer() {
8
+ const ancestors: any[] = []
9
+ return function (this: any, _key: string, value: any) {
10
+ if (isObject(value)) {
11
+ // `this` is the object that value is contained in,
12
+ // i.e., its direct parent.
13
+ while (ancestors.length > 0 && ancestors[ancestors.length - 1] !== this) {
14
+ ancestors.pop()
15
+ }
16
+ if (ancestors.includes(value)) {
17
+ return '[Circular]'
18
+ }
19
+ ancestors.push(value)
20
+ return value
21
+ } else {
22
+ return value
23
+ }
24
+ }
25
+ }
26
+
27
+ export function estimateSize(sizeable: unknown): number {
28
+ return JSON.stringify(sizeable, circularReferenceReplacer())?.length || 0
29
+ }
30
+
31
+ export const replacementImageURI =
32
+ 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2IiBmaWxsPSJibGFjayIvPgo8cGF0aCBkPSJNOCAwSDE2TDAgMTZWOEw4IDBaIiBmaWxsPSIjMkQyRDJEIi8+CjxwYXRoIGQ9Ik0xNiA4VjE2SDhMMTYgOFoiIGZpbGw9IiMyRDJEMkQiLz4KPC9zdmc+Cg=='
33
+
34
+ export const FULL_SNAPSHOT_EVENT_TYPE = 2
35
+ export const META_EVENT_TYPE = 4
36
+ export const INCREMENTAL_SNAPSHOT_EVENT_TYPE = 3
37
+ export const PLUGIN_EVENT_TYPE = 6
38
+ export const MUTATION_SOURCE_TYPE = 0
39
+
40
+ export const MAX_MESSAGE_SIZE = 5000000 // ~5mb
41
+
42
+ /*
43
+ * Check whether a data payload is nearing 5mb. If it is, it checks the data for
44
+ * data URIs (the likely culprit for large payloads). If it finds data URIs, it either replaces
45
+ * it with a generic image (if it's an image) or removes it.
46
+ * @data {object} the rr-web data object
47
+ * @returns {object} the rr-web data object with data uris filtered out
48
+ */
49
+ export function ensureMaxMessageSize(data: eventWithTime): { event: eventWithTime; size: number } {
50
+ let stringifiedData = JSON.stringify(data)
51
+ // Note: with compression, this limit may be able to be increased
52
+ // but we're assuming most of the size is from a data uri which
53
+ // is unlikely to be compressed further
54
+
55
+ if (stringifiedData.length > MAX_MESSAGE_SIZE) {
56
+ // Regex that matches the pattern for a dataURI with the shape 'data:{mime type};{encoding},{data}'. It:
57
+ // 1) Checks if the pattern starts with 'data:' (potentially, not at the start of the string)
58
+ // 2) Extracts the mime type of the data uri in the first group
59
+ // 3) Determines when the data URI ends.Depending on if it's used in the src tag or css, it can end with a ) or "
60
+ const dataURIRegex = /data:([\w/\-.]+);(\w+),([^)"]*)/gim
61
+ const matches = stringifiedData.matchAll(dataURIRegex)
62
+ for (const match of matches) {
63
+ if (match[1].toLocaleLowerCase().slice(0, 6) === 'image/') {
64
+ stringifiedData = stringifiedData.replace(match[0], replacementImageURI)
65
+ } else {
66
+ stringifiedData = stringifiedData.replace(match[0], '')
67
+ }
68
+ }
69
+ }
70
+ return { event: JSON.parse(stringifiedData), size: stringifiedData.length }
71
+ }
72
+
73
+ export const CONSOLE_LOG_PLUGIN_NAME = 'rrweb/console@1' // The name of the rr-web plugin that emits console logs
74
+
75
+ // Console logs can be really large. This function truncates large logs
76
+ // It's a simple function that just truncates long strings.
77
+ // TODO: Ideally this function would have better handling of objects + lists,
78
+ // so they could still be rendered in a pretty way after truncation.
79
+ export function truncateLargeConsoleLogs(_event: eventWithTime) {
80
+ const event = _event as pluginEvent<{ payload: string[] }>
81
+
82
+ const MAX_STRING_SIZE = 2000 // Maximum number of characters allowed in a string
83
+ const MAX_STRINGS_PER_LOG = 10 // A log can consist of multiple strings (e.g. consol.log('string1', 'string2'))
84
+
85
+ if (
86
+ event &&
87
+ isObject(event) &&
88
+ event.type === PLUGIN_EVENT_TYPE &&
89
+ isObject(event.data) &&
90
+ event.data.plugin === CONSOLE_LOG_PLUGIN_NAME
91
+ ) {
92
+ // Note: event.data.payload.payload comes from rr-web, and is an array of strings
93
+ if (event.data.payload.payload.length > MAX_STRINGS_PER_LOG) {
94
+ event.data.payload.payload = event.data.payload.payload.slice(0, MAX_STRINGS_PER_LOG)
95
+ event.data.payload.payload.push('...[truncated]')
96
+ }
97
+ const updatedPayload = []
98
+ for (let i = 0; i < event.data.payload.payload.length; i++) {
99
+ if (
100
+ event.data.payload.payload[i] && // Value can be null
101
+ event.data.payload.payload[i].length > MAX_STRING_SIZE
102
+ ) {
103
+ updatedPayload.push(event.data.payload.payload[i].slice(0, MAX_STRING_SIZE) + '...[truncated]')
104
+ } else {
105
+ updatedPayload.push(event.data.payload.payload[i])
106
+ }
107
+ }
108
+ event.data.payload.payload = updatedPayload
109
+ // Return original type
110
+ return _event
111
+ }
112
+ return _event
113
+ }
114
+
115
+ export const SEVEN_MEGABYTES = 1024 * 1024 * 7 * 0.9 // ~7mb (with some wiggle room)
116
+
117
+ // recursively splits large buffers into smaller ones
118
+ // uses a pretty high size limit to avoid splitting too much
119
+ export function splitBuffer(buffer: SnapshotBuffer, sizeLimit: number = SEVEN_MEGABYTES): SnapshotBuffer[] {
120
+ if (buffer.size >= sizeLimit && buffer.data.length > 1) {
121
+ const half = Math.floor(buffer.data.length / 2)
122
+ const firstHalf = buffer.data.slice(0, half)
123
+ const secondHalf = buffer.data.slice(half)
124
+ return [
125
+ splitBuffer({
126
+ size: estimateSize(firstHalf),
127
+ data: firstHalf,
128
+ sessionId: buffer.sessionId,
129
+ windowId: buffer.windowId,
130
+ }),
131
+ splitBuffer({
132
+ size: estimateSize(secondHalf),
133
+ data: secondHalf,
134
+ sessionId: buffer.sessionId,
135
+ windowId: buffer.windowId,
136
+ }),
137
+ ].flatMap((x) => x)
138
+ } else {
139
+ return [buffer]
140
+ }
141
+ }
@@ -0,0 +1,422 @@
1
+ import {
2
+ SESSION_RECORDING_EVENT_TRIGGER_ACTIVATED_SESSION,
3
+ SESSION_RECORDING_URL_TRIGGER_ACTIVATED_SESSION,
4
+ } from '../../../constants'
5
+ import { Leanbase } from '../../../leanbase'
6
+ import { RemoteConfig, SessionRecordingPersistedConfig, SessionRecordingUrlTrigger } from '../../../types'
7
+ import { isNullish, isBoolean, isString, isObject } from '@posthog/core'
8
+ import { window } from '../../../utils'
9
+
10
+ export const DISABLED = 'disabled'
11
+ export const SAMPLED = 'sampled'
12
+ export const ACTIVE = 'active'
13
+ export const BUFFERING = 'buffering'
14
+ export const PAUSED = 'paused'
15
+ export const LAZY_LOADING = 'lazy_loading'
16
+
17
+ const TRIGGER = 'trigger'
18
+ export const TRIGGER_ACTIVATED = TRIGGER + '_activated'
19
+ export const TRIGGER_PENDING = TRIGGER + '_pending'
20
+ export const TRIGGER_DISABLED = TRIGGER + '_' + DISABLED
21
+
22
+ export interface RecordingTriggersStatus {
23
+ get receivedFlags(): boolean
24
+ get isRecordingEnabled(): false | true | undefined
25
+ get isSampled(): false | true | null
26
+ get urlTriggerMatching(): URLTriggerMatching
27
+ get eventTriggerMatching(): EventTriggerMatching
28
+ get linkedFlagMatching(): LinkedFlagMatching
29
+ get sessionId(): string
30
+ }
31
+
32
+ export type TriggerType = 'url' | 'event'
33
+ /*
34
+ triggers can have one of three statuses:
35
+ * - trigger_activated: the trigger met conditions to start recording
36
+ * - trigger_pending: the trigger is present, but the conditions are not yet met
37
+ * - trigger_disabled: the trigger is not present
38
+ */
39
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
40
+ const triggerStatuses = [TRIGGER_ACTIVATED, TRIGGER_PENDING, TRIGGER_DISABLED] as const
41
+ export type TriggerStatus = (typeof triggerStatuses)[number]
42
+
43
+ /**
44
+ * Session recording starts in buffering mode while waiting for "flags response".
45
+ * Once the response is received, it might be disabled, active or sampled.
46
+ * When "sampled" that means a sample rate is set, and the last time the session ID rotated
47
+ * the sample rate determined this session should be sent to the server.
48
+ */
49
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
50
+ const sessionRecordingStatuses = [DISABLED, SAMPLED, ACTIVE, BUFFERING, PAUSED, LAZY_LOADING] as const
51
+ export type SessionRecordingStatus = (typeof sessionRecordingStatuses)[number]
52
+
53
+ // while we have both lazy and eager loaded replay we might get either type of config
54
+ type ReplayConfigType = RemoteConfig | SessionRecordingPersistedConfig
55
+
56
+ function sessionRecordingUrlTriggerMatches(url: string, triggers: SessionRecordingUrlTrigger[]) {
57
+ return triggers.some((trigger) => {
58
+ switch (trigger.matching) {
59
+ case 'regex':
60
+ return new RegExp(trigger.url).test(url)
61
+ default:
62
+ return false
63
+ }
64
+ })
65
+ }
66
+
67
+ export interface TriggerStatusMatching {
68
+ triggerStatus(sessionId: string): TriggerStatus
69
+ stop(): void
70
+ }
71
+ export class OrTriggerMatching implements TriggerStatusMatching {
72
+ constructor(private readonly _matchers: TriggerStatusMatching[]) {}
73
+
74
+ triggerStatus(sessionId: string): TriggerStatus {
75
+ const statuses = this._matchers.map((m) => m.triggerStatus(sessionId))
76
+ if (statuses.includes(TRIGGER_ACTIVATED)) {
77
+ return TRIGGER_ACTIVATED
78
+ }
79
+ if (statuses.includes(TRIGGER_PENDING)) {
80
+ return TRIGGER_PENDING
81
+ }
82
+ return TRIGGER_DISABLED
83
+ }
84
+
85
+ stop(): void {
86
+ this._matchers.forEach((m) => m.stop())
87
+ }
88
+ }
89
+
90
+ export class AndTriggerMatching implements TriggerStatusMatching {
91
+ constructor(private readonly _matchers: TriggerStatusMatching[]) {}
92
+
93
+ triggerStatus(sessionId: string): TriggerStatus {
94
+ const statuses = new Set<TriggerStatus>()
95
+ for (const matcher of this._matchers) {
96
+ statuses.add(matcher.triggerStatus(sessionId))
97
+ }
98
+
99
+ // trigger_disabled means no config
100
+ statuses.delete(TRIGGER_DISABLED)
101
+ switch (statuses.size) {
102
+ case 0:
103
+ return TRIGGER_DISABLED
104
+ case 1:
105
+ return Array.from(statuses)[0]
106
+ default:
107
+ return TRIGGER_PENDING
108
+ }
109
+ }
110
+
111
+ stop(): void {
112
+ this._matchers.forEach((m) => m.stop())
113
+ }
114
+ }
115
+
116
+ export class PendingTriggerMatching implements TriggerStatusMatching {
117
+ triggerStatus(): TriggerStatus {
118
+ return TRIGGER_PENDING
119
+ }
120
+
121
+ stop(): void {
122
+ // no-op
123
+ }
124
+ }
125
+
126
+ const isEagerLoadedConfig = (x: ReplayConfigType): x is RemoteConfig => {
127
+ return 'sessionRecording' in x
128
+ }
129
+
130
+ export class URLTriggerMatching implements TriggerStatusMatching {
131
+ _urlTriggers: SessionRecordingUrlTrigger[] = []
132
+ _urlBlocklist: SessionRecordingUrlTrigger[] = []
133
+
134
+ urlBlocked: boolean = false
135
+
136
+ constructor(private readonly _instance: Leanbase) {}
137
+
138
+ onConfig(config: ReplayConfigType) {
139
+ this._urlTriggers =
140
+ (isEagerLoadedConfig(config)
141
+ ? isObject(config.sessionRecording)
142
+ ? config.sessionRecording?.urlTriggers
143
+ : []
144
+ : config?.urlTriggers) || []
145
+ this._urlBlocklist =
146
+ (isEagerLoadedConfig(config)
147
+ ? isObject(config.sessionRecording)
148
+ ? config.sessionRecording?.urlBlocklist
149
+ : []
150
+ : config?.urlBlocklist) || []
151
+ }
152
+
153
+ /**
154
+ * @deprecated Use onConfig instead
155
+ */
156
+ onRemoteConfig(response: RemoteConfig) {
157
+ this.onConfig(response)
158
+ }
159
+
160
+ private _urlTriggerStatus(sessionId: string): TriggerStatus {
161
+ if (this._urlTriggers.length === 0) {
162
+ return TRIGGER_DISABLED
163
+ }
164
+
165
+ const currentTriggerSession = this._instance?.get_property(SESSION_RECORDING_URL_TRIGGER_ACTIVATED_SESSION)
166
+ return currentTriggerSession === sessionId ? TRIGGER_ACTIVATED : TRIGGER_PENDING
167
+ }
168
+
169
+ triggerStatus(sessionId: string): TriggerStatus {
170
+ const urlTriggerStatus = this._urlTriggerStatus(sessionId)
171
+ const eitherIsActivated = urlTriggerStatus === TRIGGER_ACTIVATED
172
+ const eitherIsPending = urlTriggerStatus === TRIGGER_PENDING
173
+
174
+ const result = eitherIsActivated ? TRIGGER_ACTIVATED : eitherIsPending ? TRIGGER_PENDING : TRIGGER_DISABLED
175
+ this._instance.registerForSession({
176
+ $sdk_debug_replay_url_trigger_status: result,
177
+ })
178
+ return result
179
+ }
180
+
181
+ checkUrlTriggerConditions(
182
+ onPause: () => void,
183
+ onResume: () => void,
184
+ onActivate: (triggerType: TriggerType) => void
185
+ ) {
186
+ if (typeof window === 'undefined' || !window.location.href) {
187
+ return
188
+ }
189
+
190
+ const url = window.location.href
191
+
192
+ const wasBlocked = this.urlBlocked
193
+ const isNowBlocked = sessionRecordingUrlTriggerMatches(url, this._urlBlocklist)
194
+
195
+ if (wasBlocked && isNowBlocked) {
196
+ // if the url is blocked and was already blocked, do nothing
197
+ return
198
+ } else if (isNowBlocked && !wasBlocked) {
199
+ onPause()
200
+ } else if (!isNowBlocked && wasBlocked) {
201
+ onResume()
202
+ }
203
+
204
+ if (sessionRecordingUrlTriggerMatches(url, this._urlTriggers)) {
205
+ onActivate('url')
206
+ }
207
+ }
208
+
209
+ stop(): void {
210
+ // no-op
211
+ }
212
+ }
213
+
214
+ export class LinkedFlagMatching implements TriggerStatusMatching {
215
+ linkedFlag: string | { flag: string; variant: string } | null = null
216
+ linkedFlagSeen: boolean = false
217
+ private _flagListenerCleanup: () => void = () => {}
218
+ constructor(private readonly _instance: Leanbase) {}
219
+
220
+ triggerStatus(): TriggerStatus {
221
+ let result = TRIGGER_PENDING
222
+ if (isNullish(this.linkedFlag)) {
223
+ result = TRIGGER_DISABLED
224
+ }
225
+ if (this.linkedFlagSeen) {
226
+ result = TRIGGER_ACTIVATED
227
+ }
228
+ this._instance.registerForSession({
229
+ $sdk_debug_replay_linked_flag_trigger_status: result,
230
+ })
231
+ return result
232
+ }
233
+
234
+ onConfig(config: ReplayConfigType, onStarted: (flag: string, variant: string | null) => void) {
235
+ this.linkedFlag =
236
+ (isEagerLoadedConfig(config)
237
+ ? isObject(config.sessionRecording)
238
+ ? config.sessionRecording?.linkedFlag
239
+ : null
240
+ : config?.linkedFlag) || null
241
+
242
+ if (!isNullish(this.linkedFlag) && !this.linkedFlagSeen) {
243
+ const linkedFlag = isString(this.linkedFlag) ? this.linkedFlag : this.linkedFlag.flag
244
+ const linkedVariant = isString(this.linkedFlag) ? null : this.linkedFlag.variant
245
+ this._flagListenerCleanup = this._instance.onFeatureFlags((flags) => {
246
+ const flagIsPresent = isObject(flags) && linkedFlag in (flags as any)
247
+ let linkedFlagMatches = false
248
+ if (flagIsPresent) {
249
+ const variantForFlagKey = (flags as any)[linkedFlag]
250
+ if (isBoolean(variantForFlagKey)) {
251
+ linkedFlagMatches = variantForFlagKey === true
252
+ } else if (linkedVariant) {
253
+ linkedFlagMatches = variantForFlagKey === linkedVariant
254
+ } else {
255
+ // then this is a variant flag and we want to match any string
256
+ linkedFlagMatches = !!variantForFlagKey
257
+ }
258
+ }
259
+ this.linkedFlagSeen = linkedFlagMatches
260
+ if (linkedFlagMatches) {
261
+ onStarted(linkedFlag, linkedVariant)
262
+ }
263
+ })
264
+ }
265
+ }
266
+
267
+ /**
268
+ * @deprecated Use onConfig instead
269
+ */
270
+ onRemoteConfig(response: RemoteConfig, onStarted: (flag: string, variant: string | null) => void) {
271
+ this.onConfig(response, onStarted)
272
+ }
273
+
274
+ stop(): void {
275
+ this._flagListenerCleanup()
276
+ }
277
+ }
278
+
279
+ export class EventTriggerMatching implements TriggerStatusMatching {
280
+ _eventTriggers: string[] = []
281
+
282
+ constructor(private readonly _instance: Leanbase) {}
283
+
284
+ onConfig(config: ReplayConfigType) {
285
+ this._eventTriggers =
286
+ (isEagerLoadedConfig(config)
287
+ ? isObject(config.sessionRecording)
288
+ ? config.sessionRecording?.eventTriggers
289
+ : []
290
+ : config?.eventTriggers) || []
291
+ }
292
+
293
+ /**
294
+ * @deprecated Use onConfig instead
295
+ */
296
+ onRemoteConfig(response: RemoteConfig) {
297
+ this.onConfig(response)
298
+ }
299
+
300
+ private _eventTriggerStatus(sessionId: string): TriggerStatus {
301
+ if (this._eventTriggers.length === 0) {
302
+ return TRIGGER_DISABLED
303
+ }
304
+
305
+ const currentTriggerSession = this._instance?.get_property(SESSION_RECORDING_EVENT_TRIGGER_ACTIVATED_SESSION)
306
+ return currentTriggerSession === sessionId ? TRIGGER_ACTIVATED : TRIGGER_PENDING
307
+ }
308
+
309
+ triggerStatus(sessionId: string): TriggerStatus {
310
+ const eventTriggerStatus = this._eventTriggerStatus(sessionId)
311
+ const result =
312
+ eventTriggerStatus === TRIGGER_ACTIVATED
313
+ ? TRIGGER_ACTIVATED
314
+ : eventTriggerStatus === TRIGGER_PENDING
315
+ ? TRIGGER_PENDING
316
+ : TRIGGER_DISABLED
317
+ this._instance.registerForSession({
318
+ $sdk_debug_replay_event_trigger_status: result,
319
+ })
320
+ return result
321
+ }
322
+
323
+ stop(): void {
324
+ // no-op
325
+ }
326
+ }
327
+
328
+ // we need a no-op matcher before we can lazy-load the other matches, since all matchers wait on remote config anyway
329
+ export function nullMatchSessionRecordingStatus(triggersStatus: RecordingTriggersStatus): SessionRecordingStatus {
330
+ if (!triggersStatus.isRecordingEnabled) {
331
+ return DISABLED
332
+ }
333
+
334
+ return BUFFERING
335
+ }
336
+
337
+ export function anyMatchSessionRecordingStatus(triggersStatus: RecordingTriggersStatus): SessionRecordingStatus {
338
+ if (!triggersStatus.receivedFlags) {
339
+ return BUFFERING
340
+ }
341
+
342
+ if (!triggersStatus.isRecordingEnabled) {
343
+ return DISABLED
344
+ }
345
+
346
+ if (triggersStatus.urlTriggerMatching.urlBlocked) {
347
+ return PAUSED
348
+ }
349
+
350
+ const sampledActive = triggersStatus.isSampled === true
351
+ const triggerMatches = new OrTriggerMatching([
352
+ triggersStatus.eventTriggerMatching,
353
+ triggersStatus.urlTriggerMatching,
354
+ triggersStatus.linkedFlagMatching,
355
+ ]).triggerStatus(triggersStatus.sessionId)
356
+
357
+ if (sampledActive) {
358
+ return SAMPLED
359
+ }
360
+
361
+ if (triggerMatches === TRIGGER_ACTIVATED) {
362
+ return ACTIVE
363
+ }
364
+
365
+ if (triggerMatches === TRIGGER_PENDING) {
366
+ // even if sampled active is false, we should still be buffering
367
+ // since a pending trigger could override it
368
+ return BUFFERING
369
+ }
370
+
371
+ // if sampling is set and the session is already decided to not be sampled
372
+ // then we should never be active
373
+ if (triggersStatus.isSampled === false) {
374
+ return DISABLED
375
+ }
376
+
377
+ return ACTIVE
378
+ }
379
+
380
+ export function allMatchSessionRecordingStatus(triggersStatus: RecordingTriggersStatus): SessionRecordingStatus {
381
+ if (!triggersStatus.receivedFlags) {
382
+ return BUFFERING
383
+ }
384
+
385
+ if (!triggersStatus.isRecordingEnabled) {
386
+ return DISABLED
387
+ }
388
+
389
+ if (triggersStatus.urlTriggerMatching.urlBlocked) {
390
+ return PAUSED
391
+ }
392
+
393
+ const andTriggerMatch = new AndTriggerMatching([
394
+ triggersStatus.eventTriggerMatching,
395
+ triggersStatus.urlTriggerMatching,
396
+ triggersStatus.linkedFlagMatching,
397
+ ])
398
+ const currentTriggerStatus = andTriggerMatch.triggerStatus(triggersStatus.sessionId)
399
+ const hasTriggersConfigured = currentTriggerStatus !== TRIGGER_DISABLED
400
+
401
+ const hasSamplingConfigured = isBoolean(triggersStatus.isSampled)
402
+
403
+ if (hasTriggersConfigured && currentTriggerStatus === TRIGGER_PENDING) {
404
+ return BUFFERING
405
+ }
406
+
407
+ if (hasTriggersConfigured && currentTriggerStatus === TRIGGER_DISABLED) {
408
+ return DISABLED
409
+ }
410
+
411
+ // sampling can't ever cause buffering, it's always determined right away or not configured
412
+ if (hasSamplingConfigured && !triggersStatus.isSampled) {
413
+ return DISABLED
414
+ }
415
+
416
+ // If sampling is configured and set to true, return sampled
417
+ if (triggersStatus.isSampled === true) {
418
+ return SAMPLED
419
+ }
420
+
421
+ return ACTIVE
422
+ }
@@ -0,0 +1,39 @@
1
+ import { isFunction } from '@posthog/core'
2
+
3
+ export function patch(
4
+ source: { [key: string]: any },
5
+ name: string,
6
+ replacement: (...args: unknown[]) => unknown
7
+ ): () => void {
8
+ try {
9
+ if (!(name in source)) {
10
+ return () => {
11
+ //
12
+ }
13
+ }
14
+
15
+ const original = source[name] as () => unknown
16
+ const wrapped = replacement(original)
17
+
18
+ if (isFunction(wrapped)) {
19
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
20
+ wrapped.prototype = wrapped.prototype || {}
21
+ Object.defineProperties(wrapped, {
22
+ __posthog_wrapped__: {
23
+ enumerable: false,
24
+ value: true,
25
+ },
26
+ })
27
+ }
28
+
29
+ source[name] = wrapped
30
+
31
+ return () => {
32
+ source[name] = original
33
+ }
34
+ } catch {
35
+ return () => {
36
+ //
37
+ }
38
+ }
39
+ }