@leanbase-giangnd/js 0.2.4 → 0.3.1
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/dist/index.cjs +104 -187
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +0 -38
- package/dist/index.mjs +104 -187
- package/dist/index.mjs.map +1 -1
- package/dist/leanbase.iife.js +104 -187
- package/dist/leanbase.iife.js.map +1 -1
- package/package.json +1 -1
- package/src/extensions/replay/extension-shim.ts +1 -114
- package/src/extensions/replay/external/lazy-loaded-session-recorder.ts +115 -36
- package/src/extensions/replay/external/mutation-throttler.ts +1 -4
- package/src/extensions/replay/session-recording.ts +0 -59
- package/src/version.ts +1 -1
package/package.json
CHANGED
|
@@ -1,122 +1,13 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
2
2
|
import { window as win } from '../../utils'
|
|
3
|
-
|
|
4
|
-
// We avoid importing '@rrweb/record' at module load time to prevent IIFE builds
|
|
5
|
-
// from requiring a top-level global. Instead, expose a lazy proxy that will
|
|
6
|
-
// dynamically import the module the first time it's used.
|
|
7
|
-
|
|
8
|
-
let _cachedRRWeb: any | null = null
|
|
9
|
-
|
|
10
|
-
async function _loadRRWebModule(): Promise<any> {
|
|
11
|
-
if (_cachedRRWeb) return _cachedRRWeb
|
|
12
|
-
try {
|
|
13
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
14
|
-
const mod: any = await import('@rrweb/record')
|
|
15
|
-
_cachedRRWeb = mod
|
|
16
|
-
return _cachedRRWeb
|
|
17
|
-
} catch (e) {
|
|
18
|
-
return null
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// queue for method calls before rrweb loads
|
|
23
|
-
const _queuedCalls: Array<() => void> = []
|
|
24
|
-
|
|
25
|
-
// Create a proxy function that delegates to the real rrweb.record when called
|
|
26
|
-
const rrwebRecordProxy: any = function (...args: any[]) {
|
|
27
|
-
let realStop: (() => void) | undefined
|
|
28
|
-
let calledReal = false
|
|
29
|
-
|
|
30
|
-
// Start loading asynchronously and call the real record when available
|
|
31
|
-
void (async () => {
|
|
32
|
-
const mod = await _loadRRWebModule()
|
|
33
|
-
const real = mod && (mod.record ?? mod.default?.record)
|
|
34
|
-
if (real) {
|
|
35
|
-
try {
|
|
36
|
-
calledReal = true
|
|
37
|
-
realStop = real(...args)
|
|
38
|
-
// flush any queued calls that were waiting for rrweb
|
|
39
|
-
while (_queuedCalls.length) {
|
|
40
|
-
try {
|
|
41
|
-
const fn = _queuedCalls.shift()!
|
|
42
|
-
fn()
|
|
43
|
-
} catch (e) {
|
|
44
|
-
// ignore
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
} catch (e) {
|
|
48
|
-
// ignore
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
})()
|
|
52
|
-
|
|
53
|
-
// return a stop function that will call the real stop when available
|
|
54
|
-
return () => {
|
|
55
|
-
if (realStop) {
|
|
56
|
-
try {
|
|
57
|
-
realStop()
|
|
58
|
-
} catch (e) {
|
|
59
|
-
// ignore
|
|
60
|
-
}
|
|
61
|
-
} else if (!calledReal) {
|
|
62
|
-
// If rrweb hasn't been initialised yet, queue a stop request that will
|
|
63
|
-
// call the real stop once available.
|
|
64
|
-
_queuedCalls.push(() => {
|
|
65
|
-
try {
|
|
66
|
-
realStop?.()
|
|
67
|
-
} catch (e) {
|
|
68
|
-
// ignore
|
|
69
|
-
}
|
|
70
|
-
})
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// methods that can be called on the rrweb.record object - queue until real module is available
|
|
76
|
-
rrwebRecordProxy.addCustomEvent = function (tag?: string, payload?: any) {
|
|
77
|
-
const call = () => {
|
|
78
|
-
try {
|
|
79
|
-
const real = _cachedRRWeb && (_cachedRRWeb.record ?? _cachedRRWeb.default?.record)
|
|
80
|
-
real?.addCustomEvent?.(tag, payload)
|
|
81
|
-
} catch (e) {
|
|
82
|
-
// ignore
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
if (_cachedRRWeb) call()
|
|
86
|
-
else _queuedCalls.push(call)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
rrwebRecordProxy.takeFullSnapshot = function () {
|
|
90
|
-
const call = () => {
|
|
91
|
-
try {
|
|
92
|
-
const real = _cachedRRWeb && (_cachedRRWeb.record ?? _cachedRRWeb.default?.record)
|
|
93
|
-
real?.takeFullSnapshot?.()
|
|
94
|
-
} catch (e) {
|
|
95
|
-
// ignore
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
if (_cachedRRWeb) call()
|
|
99
|
-
else _queuedCalls.push(call)
|
|
100
|
-
}
|
|
101
|
-
// Delay importing heavy modules to avoid circular dependencies at build time.
|
|
102
|
-
// They will be required lazily when used at runtime.
|
|
103
|
-
// We avoid requiring the lazy-loaded recorder here to prevent circular dependencies during bundling.
|
|
104
|
-
// Instead, `LazyLoadedSessionRecording` will register a factory on the global under
|
|
105
|
-
// `__PosthogExtensions__._initSessionRecordingFactory` when it loads.
|
|
106
3
|
type InitSessionRecordingFactory = (instance: any) => any
|
|
107
4
|
|
|
108
|
-
// Use a safe global target (prefer `win`, fallback to globalThis)
|
|
109
5
|
const _target: any = (win as any) ?? (globalThis as any)
|
|
110
6
|
|
|
111
7
|
_target.__PosthogExtensions__ = _target.__PosthogExtensions__ || {}
|
|
112
8
|
|
|
113
|
-
|
|
114
|
-
// builds that execute this file don't require rrweb at module evaluation time.
|
|
115
|
-
_target.__PosthogExtensions__.rrweb = _target.__PosthogExtensions__.rrweb || {
|
|
116
|
-
record: rrwebRecordProxy,
|
|
117
|
-
}
|
|
9
|
+
_target.__PosthogExtensions__.rrweb = _target.__PosthogExtensions__.rrweb || {}
|
|
118
10
|
|
|
119
|
-
// Provide initSessionRecording if not present — return a new LazyLoadedSessionRecording when called
|
|
120
11
|
_target.__PosthogExtensions__.initSessionRecording =
|
|
121
12
|
_target.__PosthogExtensions__.initSessionRecording ||
|
|
122
13
|
((instance: any) => {
|
|
@@ -125,20 +16,16 @@ _target.__PosthogExtensions__.initSessionRecording =
|
|
|
125
16
|
if (factory) {
|
|
126
17
|
return factory(instance)
|
|
127
18
|
}
|
|
128
|
-
// If no factory is registered yet, return undefined — callers should handle lazy-loading.
|
|
129
19
|
return undefined
|
|
130
20
|
})
|
|
131
21
|
|
|
132
|
-
// Provide a no-op loadExternalDependency that calls the callback immediately (since rrweb is bundled)
|
|
133
22
|
_target.__PosthogExtensions__.loadExternalDependency =
|
|
134
23
|
_target.__PosthogExtensions__.loadExternalDependency ||
|
|
135
24
|
((instance: any, scriptName: string, cb?: (err?: any) => void) => {
|
|
136
25
|
if (cb) cb(undefined)
|
|
137
26
|
})
|
|
138
27
|
|
|
139
|
-
// Provide rrwebPlugins object with network plugin factory if not present
|
|
140
28
|
_target.__PosthogExtensions__.rrwebPlugins = _target.__PosthogExtensions__.rrwebPlugins || {}
|
|
141
|
-
// Default to undefined; the lazy-loaded recorder will register the real factory when it initializes.
|
|
142
29
|
_target.__PosthogExtensions__.rrwebPlugins.getRecordNetworkPlugin =
|
|
143
30
|
_target.__PosthogExtensions__.rrwebPlugins.getRecordNetworkPlugin || (() => undefined)
|
|
144
31
|
|
|
@@ -318,7 +318,12 @@ export class LazyLoadedSessionRecording {
|
|
|
318
318
|
* Util to help developers working on this feature manually override
|
|
319
319
|
*/
|
|
320
320
|
private _forceAllowLocalhostNetworkCapture = false
|
|
321
|
+
// "Started" is only true once we have a valid rrweb stop handler AND we have marked recording enabled.
|
|
322
|
+
// If rrweb fails to initialize, we permanently disable replay for this page load.
|
|
323
|
+
private _recording: { stop: listenerHandler } | undefined
|
|
321
324
|
private _stopRrweb: listenerHandler | undefined = undefined
|
|
325
|
+
private _permanentlyDisabled: boolean = false
|
|
326
|
+
private _loggedPermanentlyDisabled: boolean = false
|
|
322
327
|
private _lastActivityTimestamp: number = Date.now()
|
|
323
328
|
/**
|
|
324
329
|
* if pageview capture is disabled,
|
|
@@ -357,6 +362,25 @@ export class LazyLoadedSessionRecording {
|
|
|
357
362
|
|
|
358
363
|
private _removeEventTriggerCaptureHook: (() => void) | undefined = undefined
|
|
359
364
|
|
|
365
|
+
private _disablePermanently(reason: string, error?: unknown): void {
|
|
366
|
+
this._permanentlyDisabled = true
|
|
367
|
+
this._isFullyReady = false
|
|
368
|
+
this._mutationThrottler?.stop()
|
|
369
|
+
this._mutationThrottler = undefined
|
|
370
|
+
this._queuedRRWebEvents = []
|
|
371
|
+
this._recording = undefined
|
|
372
|
+
this._stopRrweb = undefined
|
|
373
|
+
|
|
374
|
+
if (!this._loggedPermanentlyDisabled) {
|
|
375
|
+
this._loggedPermanentlyDisabled = true
|
|
376
|
+
if (error) {
|
|
377
|
+
logger.error(`replay disabled: ${reason}`, error)
|
|
378
|
+
} else {
|
|
379
|
+
logger.error(`replay disabled: ${reason}`)
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
360
384
|
private get _sessionManager() {
|
|
361
385
|
if (!this._instance.sessionManager) {
|
|
362
386
|
throw new Error(LOGGER_PREFIX + ' must be started with a valid sessionManager.')
|
|
@@ -579,6 +603,9 @@ export class LazyLoadedSessionRecording {
|
|
|
579
603
|
}
|
|
580
604
|
|
|
581
605
|
private _tryAddCustomEvent(tag: string, payload: any): boolean {
|
|
606
|
+
if (!this.isStarted || !this._recording) {
|
|
607
|
+
return false
|
|
608
|
+
}
|
|
582
609
|
const rrwebRecord = getRRWebRecord()
|
|
583
610
|
if (!rrwebRecord || typeof (rrwebRecord as any).addCustomEvent !== 'function') {
|
|
584
611
|
return false
|
|
@@ -609,6 +636,10 @@ export class LazyLoadedSessionRecording {
|
|
|
609
636
|
}
|
|
610
637
|
|
|
611
638
|
private _processQueuedEvents() {
|
|
639
|
+
if (!this.isStarted || !this._recording) {
|
|
640
|
+
this._queuedRRWebEvents = []
|
|
641
|
+
return
|
|
642
|
+
}
|
|
612
643
|
if (this._queuedRRWebEvents.length) {
|
|
613
644
|
// if rrweb isn't ready to accept events earlier, then we queued them up.
|
|
614
645
|
// now that `emit` has been called rrweb should be ready to accept them.
|
|
@@ -631,6 +662,9 @@ export class LazyLoadedSessionRecording {
|
|
|
631
662
|
}
|
|
632
663
|
|
|
633
664
|
private _tryTakeFullSnapshot(): boolean {
|
|
665
|
+
if (!this.isStarted || !this._recording) {
|
|
666
|
+
return false
|
|
667
|
+
}
|
|
634
668
|
const rrwebRecord = getRRWebRecord()
|
|
635
669
|
if (!rrwebRecord || typeof (rrwebRecord as any).takeFullSnapshot !== 'function') {
|
|
636
670
|
return false
|
|
@@ -651,6 +685,9 @@ export class LazyLoadedSessionRecording {
|
|
|
651
685
|
}
|
|
652
686
|
|
|
653
687
|
private _scheduleFullSnapshot(): void {
|
|
688
|
+
if (!this.isStarted || !this._recording) {
|
|
689
|
+
return
|
|
690
|
+
}
|
|
654
691
|
if (this._fullSnapshotTimer) {
|
|
655
692
|
clearInterval(this._fullSnapshotTimer)
|
|
656
693
|
}
|
|
@@ -670,6 +707,9 @@ export class LazyLoadedSessionRecording {
|
|
|
670
707
|
}
|
|
671
708
|
|
|
672
709
|
private _pauseRecording() {
|
|
710
|
+
if (!this.isStarted || !this._recording) {
|
|
711
|
+
return
|
|
712
|
+
}
|
|
673
713
|
// we check _urlBlocked not status, since more than one thing can affect status
|
|
674
714
|
if (this._urlTriggerMatching.urlBlocked) {
|
|
675
715
|
return
|
|
@@ -689,6 +729,9 @@ export class LazyLoadedSessionRecording {
|
|
|
689
729
|
}
|
|
690
730
|
|
|
691
731
|
private _resumeRecording() {
|
|
732
|
+
if (!this.isStarted || !this._recording) {
|
|
733
|
+
return
|
|
734
|
+
}
|
|
692
735
|
// we check _urlBlocked not status, since more than one thing can affect status
|
|
693
736
|
if (!this._urlTriggerMatching.urlBlocked) {
|
|
694
737
|
return
|
|
@@ -704,6 +747,9 @@ export class LazyLoadedSessionRecording {
|
|
|
704
747
|
}
|
|
705
748
|
|
|
706
749
|
private _activateTrigger(triggerType: TriggerType) {
|
|
750
|
+
if (!this.isStarted || !this._recording || !this._isFullyReady) {
|
|
751
|
+
return
|
|
752
|
+
}
|
|
707
753
|
if (this._triggerMatching.triggerStatus(this.sessionId) === TRIGGER_PENDING) {
|
|
708
754
|
// status is stored separately for URL and event triggers
|
|
709
755
|
this._instance?.persistence?.register({
|
|
@@ -718,7 +764,7 @@ export class LazyLoadedSessionRecording {
|
|
|
718
764
|
}
|
|
719
765
|
|
|
720
766
|
get isStarted(): boolean {
|
|
721
|
-
return !!this.
|
|
767
|
+
return !!this._recording?.stop
|
|
722
768
|
}
|
|
723
769
|
|
|
724
770
|
get _remoteConfig(): SessionRecordingPersistedConfig | undefined {
|
|
@@ -731,6 +777,9 @@ export class LazyLoadedSessionRecording {
|
|
|
731
777
|
}
|
|
732
778
|
|
|
733
779
|
async start(startReason?: SessionStartReason) {
|
|
780
|
+
if (this._permanentlyDisabled) {
|
|
781
|
+
return
|
|
782
|
+
}
|
|
734
783
|
this._isFullyReady = false
|
|
735
784
|
const config = this._remoteConfig
|
|
736
785
|
if (!config) {
|
|
@@ -761,9 +810,17 @@ export class LazyLoadedSessionRecording {
|
|
|
761
810
|
this._urlTriggerMatching.onConfig(config)
|
|
762
811
|
|
|
763
812
|
this._eventTriggerMatching.onConfig(config)
|
|
764
|
-
this._removeEventTriggerCaptureHook?.()
|
|
765
|
-
this._addEventTriggerListener()
|
|
766
813
|
|
|
814
|
+
// Start rrweb first; only once we have a valid recorder do we install any listeners/timers.
|
|
815
|
+
await this._startRecorder()
|
|
816
|
+
|
|
817
|
+
// If rrweb failed to load/start, do not proceed further.
|
|
818
|
+
// This prevents installing listeners that assume rrweb is active.
|
|
819
|
+
if (!this.isStarted) {
|
|
820
|
+
return
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// Now that rrweb has started, we can safely install replay side-effects.
|
|
767
824
|
this._linkedFlagMatching.onConfig(config, (flag, variant) => {
|
|
768
825
|
this._reportStarted('linked_flag_matched', {
|
|
769
826
|
flag,
|
|
@@ -772,13 +829,8 @@ export class LazyLoadedSessionRecording {
|
|
|
772
829
|
})
|
|
773
830
|
|
|
774
831
|
this._makeSamplingDecision(this.sessionId)
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
// If rrweb failed to load/start, do not proceed further.
|
|
778
|
-
// This prevents installing listeners that assume rrweb is active.
|
|
779
|
-
if (!this.isStarted) {
|
|
780
|
-
return
|
|
781
|
-
}
|
|
832
|
+
this._removeEventTriggerCaptureHook?.()
|
|
833
|
+
this._addEventTriggerListener()
|
|
782
834
|
|
|
783
835
|
// Only start processing rrweb emits once the ingestion endpoint is available.
|
|
784
836
|
// If it isn't available, we must degrade to a no-op (never crash the host app).
|
|
@@ -886,9 +938,9 @@ export class LazyLoadedSessionRecording {
|
|
|
886
938
|
// Clear any queued rrweb events to prevent memory leaks from closures
|
|
887
939
|
this._queuedRRWebEvents = []
|
|
888
940
|
|
|
889
|
-
this.
|
|
941
|
+
this._recording?.stop?.()
|
|
942
|
+
this._recording = undefined
|
|
890
943
|
this._stopRrweb = undefined
|
|
891
|
-
|
|
892
944
|
this._isFullyReady = false
|
|
893
945
|
|
|
894
946
|
logger.info('stopped')
|
|
@@ -911,8 +963,13 @@ export class LazyLoadedSessionRecording {
|
|
|
911
963
|
}
|
|
912
964
|
|
|
913
965
|
onRRwebEmit(rawEvent: eventWithTime) {
|
|
966
|
+
// First-line invariant gate: drop everything unless replay is truly started.
|
|
967
|
+
if (!this.isStarted || !this._recording) {
|
|
968
|
+
return
|
|
969
|
+
}
|
|
970
|
+
|
|
914
971
|
// Never process rrweb emits until we're fully ready.
|
|
915
|
-
if (!this._isFullyReady
|
|
972
|
+
if (!this._isFullyReady) {
|
|
916
973
|
return
|
|
917
974
|
}
|
|
918
975
|
|
|
@@ -1046,6 +1103,9 @@ export class LazyLoadedSessionRecording {
|
|
|
1046
1103
|
}
|
|
1047
1104
|
|
|
1048
1105
|
public overrideLinkedFlag() {
|
|
1106
|
+
if (!this.isStarted || !this._recording || !this._isFullyReady) {
|
|
1107
|
+
return
|
|
1108
|
+
}
|
|
1049
1109
|
this._linkedFlagMatching.linkedFlagSeen = true
|
|
1050
1110
|
this._tryTakeFullSnapshot()
|
|
1051
1111
|
this._reportStarted('linked_flag_overridden')
|
|
@@ -1058,6 +1118,9 @@ export class LazyLoadedSessionRecording {
|
|
|
1058
1118
|
* instead call `posthog.startSessionRecording({sampling: true})`
|
|
1059
1119
|
* */
|
|
1060
1120
|
public overrideSampling() {
|
|
1121
|
+
if (!this.isStarted || !this._recording || !this._isFullyReady) {
|
|
1122
|
+
return
|
|
1123
|
+
}
|
|
1061
1124
|
this._instance.persistence?.register({
|
|
1062
1125
|
// short-circuits the `makeSamplingDecision` function in the session recording module
|
|
1063
1126
|
[SESSION_RECORDING_IS_SAMPLED]: this.sessionId,
|
|
@@ -1073,6 +1136,9 @@ export class LazyLoadedSessionRecording {
|
|
|
1073
1136
|
* instead call `posthog.startSessionRecording({trigger: 'url' | 'event'})`
|
|
1074
1137
|
* */
|
|
1075
1138
|
public overrideTrigger(triggerType: TriggerType) {
|
|
1139
|
+
if (!this.isStarted || !this._recording || !this._isFullyReady) {
|
|
1140
|
+
return
|
|
1141
|
+
}
|
|
1076
1142
|
this._activateTrigger(triggerType)
|
|
1077
1143
|
}
|
|
1078
1144
|
|
|
@@ -1243,6 +1309,9 @@ export class LazyLoadedSessionRecording {
|
|
|
1243
1309
|
}
|
|
1244
1310
|
|
|
1245
1311
|
private _reportStarted(startReason: SessionStartReason, tagPayload?: Record<string, any>) {
|
|
1312
|
+
if (!this.isStarted || !this._recording) {
|
|
1313
|
+
return
|
|
1314
|
+
}
|
|
1246
1315
|
this._instance.registerForSession({
|
|
1247
1316
|
$session_recording_start_reason: startReason,
|
|
1248
1317
|
})
|
|
@@ -1416,7 +1485,7 @@ export class LazyLoadedSessionRecording {
|
|
|
1416
1485
|
}
|
|
1417
1486
|
|
|
1418
1487
|
private async _startRecorder() {
|
|
1419
|
-
if (this.
|
|
1488
|
+
if (this._permanentlyDisabled || this._recording) {
|
|
1420
1489
|
return
|
|
1421
1490
|
}
|
|
1422
1491
|
|
|
@@ -1474,46 +1543,56 @@ export class LazyLoadedSessionRecording {
|
|
|
1474
1543
|
}
|
|
1475
1544
|
|
|
1476
1545
|
if (!rrwebRecord) {
|
|
1477
|
-
|
|
1478
|
-
'_startRecorder was called but rrwebRecord is not available. This indicates something has gone wrong.'
|
|
1479
|
-
)
|
|
1546
|
+
this._disablePermanently('rrweb record function unavailable')
|
|
1480
1547
|
return
|
|
1481
1548
|
}
|
|
1482
1549
|
|
|
1483
|
-
this._mutationThrottler =
|
|
1484
|
-
this._mutationThrottler ??
|
|
1485
|
-
new MutationThrottler(rrwebRecord, {
|
|
1486
|
-
refillRate: this._instance.config.session_recording?.__mutationThrottlerRefillRate,
|
|
1487
|
-
bucketSize: this._instance.config.session_recording?.__mutationThrottlerBucketSize,
|
|
1488
|
-
onBlockedNode: (id, node) => {
|
|
1489
|
-
const message = `Too many mutations on node '${id}'. Rate limiting. This could be due to SVG animations or something similar`
|
|
1490
|
-
logger.info(message, {
|
|
1491
|
-
node: node,
|
|
1492
|
-
})
|
|
1493
|
-
|
|
1494
|
-
this.log(LOGGER_PREFIX + ' ' + message, 'warn')
|
|
1495
|
-
},
|
|
1496
|
-
})
|
|
1497
|
-
|
|
1498
1550
|
const activePlugins = this._gatherRRWebPlugins()
|
|
1551
|
+
|
|
1552
|
+
let stopHandler: listenerHandler | undefined
|
|
1499
1553
|
try {
|
|
1500
|
-
|
|
1554
|
+
stopHandler = rrwebRecord({
|
|
1501
1555
|
emit: (event) => {
|
|
1502
1556
|
try {
|
|
1503
1557
|
this.onRRwebEmit(event)
|
|
1504
1558
|
} catch (e) {
|
|
1559
|
+
// never throw from rrweb emit handler
|
|
1505
1560
|
logger.error('error in rrweb emit handler', e)
|
|
1506
1561
|
}
|
|
1507
1562
|
},
|
|
1508
1563
|
plugins: activePlugins,
|
|
1509
1564
|
...sessionRecordingOptions,
|
|
1510
|
-
})
|
|
1565
|
+
}) as unknown as listenerHandler
|
|
1511
1566
|
} catch (e) {
|
|
1512
|
-
|
|
1513
|
-
|
|
1567
|
+
this._disablePermanently('rrweb recorder threw during initialization', e)
|
|
1568
|
+
return
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
if (typeof stopHandler !== 'function') {
|
|
1572
|
+
this._disablePermanently('rrweb recorder returned an invalid stop handler')
|
|
1514
1573
|
return
|
|
1515
1574
|
}
|
|
1516
1575
|
|
|
1576
|
+
// Mark replay started only after rrweb has successfully returned a valid stop handler.
|
|
1577
|
+
this._recording = { stop: stopHandler }
|
|
1578
|
+
this._stopRrweb = stopHandler
|
|
1579
|
+
|
|
1580
|
+
// Only create mutation throttler once replay is truly started.
|
|
1581
|
+
this._mutationThrottler =
|
|
1582
|
+
this._mutationThrottler ??
|
|
1583
|
+
new MutationThrottler(rrwebRecord, {
|
|
1584
|
+
refillRate: this._instance.config.session_recording?.__mutationThrottlerRefillRate,
|
|
1585
|
+
bucketSize: this._instance.config.session_recording?.__mutationThrottlerBucketSize,
|
|
1586
|
+
onBlockedNode: (id, node) => {
|
|
1587
|
+
const message = `Too many mutations on node '${id}'. Rate limiting. This could be due to SVG animations or something similar`
|
|
1588
|
+
logger.info(message, {
|
|
1589
|
+
node: node,
|
|
1590
|
+
})
|
|
1591
|
+
|
|
1592
|
+
this.log(LOGGER_PREFIX + ' ' + message, 'warn')
|
|
1593
|
+
},
|
|
1594
|
+
})
|
|
1595
|
+
|
|
1517
1596
|
// We reset the last activity timestamp, resetting the idle timer
|
|
1518
1597
|
this._lastActivityTimestamp = Date.now()
|
|
1519
1598
|
// stay unknown if we're not sure if we're idle or not
|
|
@@ -16,11 +16,8 @@ export class MutationThrottler {
|
|
|
16
16
|
onBlockedNode?: (id: number, node: Node | null) => void
|
|
17
17
|
} = {}
|
|
18
18
|
) {
|
|
19
|
-
const configuredBucketSize = this._options.bucketSize ?? 100
|
|
20
|
-
const effectiveBucketSize = Math.max(configuredBucketSize - 1, 1)
|
|
21
|
-
|
|
22
19
|
this._rateLimiter = new BucketedRateLimiter({
|
|
23
|
-
bucketSize:
|
|
20
|
+
bucketSize: this._options.bucketSize ?? 100,
|
|
24
21
|
refillRate: this._options.refillRate ?? 10,
|
|
25
22
|
refillInterval: 1000, // one second
|
|
26
23
|
_onBucketRateLimited: this._onNodeRateLimited,
|
|
@@ -21,7 +21,6 @@ export class SessionRecording {
|
|
|
21
21
|
_forceAllowLocalhostNetworkCapture: boolean = false
|
|
22
22
|
|
|
23
23
|
private _receivedFlags: boolean = false
|
|
24
|
-
private _serverRecordingEnabled: boolean = false
|
|
25
24
|
|
|
26
25
|
private _persistFlagsOnSessionListener: (() => void) | undefined = undefined
|
|
27
26
|
private _lazyLoadedSessionRecording: LazyLoadedSessionRecording | undefined
|
|
@@ -30,10 +29,6 @@ export class SessionRecording {
|
|
|
30
29
|
return !!this._lazyLoadedSessionRecording?.isStarted
|
|
31
30
|
}
|
|
32
31
|
|
|
33
|
-
/**
|
|
34
|
-
* defaults to buffering mode until a flags response is received
|
|
35
|
-
* once a flags response is received status can be disabled, active or sampled
|
|
36
|
-
*/
|
|
37
32
|
get status(): SessionRecordingStatus {
|
|
38
33
|
if (this._lazyLoadedSessionRecording) {
|
|
39
34
|
return this._lazyLoadedSessionRecording.status
|
|
@@ -72,28 +67,16 @@ export class SessionRecording {
|
|
|
72
67
|
const canRunReplay = !isUndefined(Object.assign) && !isUndefined(Array.from)
|
|
73
68
|
if (this._isRecordingEnabled && canRunReplay) {
|
|
74
69
|
this._lazyLoadAndStart(startReason)
|
|
75
|
-
log.info('starting')
|
|
76
70
|
} else {
|
|
77
71
|
this.stopRecording()
|
|
78
72
|
}
|
|
79
73
|
}
|
|
80
74
|
|
|
81
|
-
/**
|
|
82
|
-
* session recording waits until it receives remote config before loading the script
|
|
83
|
-
* this is to ensure we can control the script name remotely
|
|
84
|
-
* and because we wait until we have local and remote config to determine if we should start at all
|
|
85
|
-
* if start is called and there is no remote config then we wait until there is
|
|
86
|
-
*/
|
|
87
75
|
private _lazyLoadAndStart(startReason?: SessionStartReason) {
|
|
88
|
-
// by checking `_isRecordingEnabled` here we know that
|
|
89
|
-
// we have stored remote config and client config to read
|
|
90
|
-
// replay waits for both local and remote config before starting
|
|
91
76
|
if (!this._isRecordingEnabled) {
|
|
92
77
|
return
|
|
93
78
|
}
|
|
94
79
|
|
|
95
|
-
// If extensions provide a loader, use it. Otherwise fallback to the local _onScriptLoaded which
|
|
96
|
-
// will create the local LazyLoadedSessionRecording (so tests that mock it work correctly).
|
|
97
80
|
const loader = assignableWindow.__PosthogExtensions__?.loadExternalDependency
|
|
98
81
|
if (typeof loader === 'function') {
|
|
99
82
|
loader(this._instance, this._scriptName as any, (err: any) => {
|
|
@@ -162,18 +145,11 @@ export class SessionRecording {
|
|
|
162
145
|
|
|
163
146
|
persistResponse()
|
|
164
147
|
|
|
165
|
-
// in case we see multiple flags responses, we should only use the response from the most recent one
|
|
166
148
|
this._persistFlagsOnSessionListener?.()
|
|
167
|
-
// we 100% know there is a session manager by this point
|
|
168
149
|
this._persistFlagsOnSessionListener = this._instance.sessionManager?.onSessionId(persistResponse)
|
|
169
150
|
}
|
|
170
151
|
}
|
|
171
152
|
|
|
172
|
-
private _clearRemoteConfig() {
|
|
173
|
-
this._instance.persistence?.unregister(SESSION_RECORDING_REMOTE_CONFIG)
|
|
174
|
-
this._resetSampling()
|
|
175
|
-
}
|
|
176
|
-
|
|
177
153
|
onRemoteConfig(response: RemoteConfig) {
|
|
178
154
|
if (!('sessionRecording' in response)) {
|
|
179
155
|
// if sessionRecording is not in the response, we do nothing
|
|
@@ -183,13 +159,9 @@ export class SessionRecording {
|
|
|
183
159
|
this._receivedFlags = true
|
|
184
160
|
|
|
185
161
|
if (response.sessionRecording === false) {
|
|
186
|
-
this._serverRecordingEnabled = false
|
|
187
|
-
this._clearRemoteConfig()
|
|
188
|
-
this.stopRecording()
|
|
189
162
|
return
|
|
190
163
|
}
|
|
191
164
|
|
|
192
|
-
this._serverRecordingEnabled = true
|
|
193
165
|
this._persistRemoteConfig(response)
|
|
194
166
|
this.startIfEnabledOrStop()
|
|
195
167
|
}
|
|
@@ -264,44 +236,21 @@ export class SessionRecording {
|
|
|
264
236
|
this._lazyLoadedSessionRecording?.onRRwebEmit?.(rawEvent)
|
|
265
237
|
}
|
|
266
238
|
|
|
267
|
-
/**
|
|
268
|
-
* this ignores the linked flag config and (if other conditions are met) causes capture to start
|
|
269
|
-
*
|
|
270
|
-
* It is not usual to call this directly,
|
|
271
|
-
* instead call `posthog.startSessionRecording({linked_flag: true})`
|
|
272
|
-
* */
|
|
273
239
|
public overrideLinkedFlag() {
|
|
274
240
|
// TODO what if this gets called before lazy loading is done
|
|
275
241
|
this._lazyLoadedSessionRecording?.overrideLinkedFlag()
|
|
276
242
|
}
|
|
277
243
|
|
|
278
|
-
/**
|
|
279
|
-
* this ignores the sampling config and (if other conditions are met) causes capture to start
|
|
280
|
-
*
|
|
281
|
-
* It is not usual to call this directly,
|
|
282
|
-
* instead call `posthog.startSessionRecording({sampling: true})`
|
|
283
|
-
* */
|
|
284
244
|
public overrideSampling() {
|
|
285
245
|
// TODO what if this gets called before lazy loading is done
|
|
286
246
|
this._lazyLoadedSessionRecording?.overrideSampling()
|
|
287
247
|
}
|
|
288
248
|
|
|
289
|
-
/**
|
|
290
|
-
* this ignores the URL/Event trigger config and (if other conditions are met) causes capture to start
|
|
291
|
-
*
|
|
292
|
-
* It is not usual to call this directly,
|
|
293
|
-
* instead call `posthog.startSessionRecording({trigger: 'url' | 'event'})`
|
|
294
|
-
* */
|
|
295
249
|
public overrideTrigger(triggerType: TriggerType) {
|
|
296
250
|
// TODO what if this gets called before lazy loading is done
|
|
297
251
|
this._lazyLoadedSessionRecording?.overrideTrigger(triggerType)
|
|
298
252
|
}
|
|
299
253
|
|
|
300
|
-
/*
|
|
301
|
-
* whenever we capture an event, we add these properties to the event
|
|
302
|
-
* these are used to debug issues with the session recording
|
|
303
|
-
* when looking at the event feed for a session
|
|
304
|
-
*/
|
|
305
254
|
get sdkDebugProperties(): Properties {
|
|
306
255
|
return (
|
|
307
256
|
this._lazyLoadedSessionRecording?.sdkDebugProperties || {
|
|
@@ -310,14 +259,6 @@ export class SessionRecording {
|
|
|
310
259
|
)
|
|
311
260
|
}
|
|
312
261
|
|
|
313
|
-
/**
|
|
314
|
-
* This adds a custom event to the session recording
|
|
315
|
-
*
|
|
316
|
-
* It is not intended for arbitrary public use - playback only displays known custom events
|
|
317
|
-
* And is exposed on the public interface only so that other parts of the SDK are able to use it
|
|
318
|
-
*
|
|
319
|
-
* if you are calling this from client code, you're probably looking for `posthog.capture('$custom_event', {...})`
|
|
320
|
-
*/
|
|
321
262
|
tryAddCustomEvent(tag: string, payload: any): boolean {
|
|
322
263
|
return !!this._lazyLoadedSessionRecording?.tryAddCustomEvent(tag, payload)
|
|
323
264
|
}
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '0.
|
|
1
|
+
export const version = '0.3.1'
|