@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,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
|
+
}
|