@leanbase-giangnd/js 0.3.0 → 0.3.2
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 +154 -189
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +58 -81
- package/dist/index.mjs +154 -189
- package/dist/index.mjs.map +1 -1
- package/dist/leanbase.iife.js +154 -189
- 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 +130 -38
- package/src/extensions/replay/external/mutation-throttler.ts +1 -4
- package/src/extensions/replay/session-recording.ts +0 -59
- package/src/leanbase.ts +4 -1
- package/src/utils/request-router.ts +39 -0
- 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,13 @@ 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
|
|
327
|
+
private _hasReportedRecordingInitialized: boolean = false
|
|
322
328
|
private _lastActivityTimestamp: number = Date.now()
|
|
323
329
|
/**
|
|
324
330
|
* if pageview capture is disabled,
|
|
@@ -357,6 +363,25 @@ export class LazyLoadedSessionRecording {
|
|
|
357
363
|
|
|
358
364
|
private _removeEventTriggerCaptureHook: (() => void) | undefined = undefined
|
|
359
365
|
|
|
366
|
+
private _disablePermanently(reason: string, error?: unknown): void {
|
|
367
|
+
this._permanentlyDisabled = true
|
|
368
|
+
this._isFullyReady = false
|
|
369
|
+
this._mutationThrottler?.stop()
|
|
370
|
+
this._mutationThrottler = undefined
|
|
371
|
+
this._queuedRRWebEvents = []
|
|
372
|
+
this._recording = undefined
|
|
373
|
+
this._stopRrweb = undefined
|
|
374
|
+
|
|
375
|
+
if (!this._loggedPermanentlyDisabled) {
|
|
376
|
+
this._loggedPermanentlyDisabled = true
|
|
377
|
+
if (error) {
|
|
378
|
+
logger.error(`replay disabled: ${reason}`, error)
|
|
379
|
+
} else {
|
|
380
|
+
logger.error(`replay disabled: ${reason}`)
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
360
385
|
private get _sessionManager() {
|
|
361
386
|
if (!this._instance.sessionManager) {
|
|
362
387
|
throw new Error(LOGGER_PREFIX + ' must be started with a valid sessionManager.')
|
|
@@ -579,6 +604,9 @@ export class LazyLoadedSessionRecording {
|
|
|
579
604
|
}
|
|
580
605
|
|
|
581
606
|
private _tryAddCustomEvent(tag: string, payload: any): boolean {
|
|
607
|
+
if (!this.isStarted || !this._recording) {
|
|
608
|
+
return false
|
|
609
|
+
}
|
|
582
610
|
const rrwebRecord = getRRWebRecord()
|
|
583
611
|
if (!rrwebRecord || typeof (rrwebRecord as any).addCustomEvent !== 'function') {
|
|
584
612
|
return false
|
|
@@ -609,6 +637,10 @@ export class LazyLoadedSessionRecording {
|
|
|
609
637
|
}
|
|
610
638
|
|
|
611
639
|
private _processQueuedEvents() {
|
|
640
|
+
if (!this.isStarted || !this._recording) {
|
|
641
|
+
this._queuedRRWebEvents = []
|
|
642
|
+
return
|
|
643
|
+
}
|
|
612
644
|
if (this._queuedRRWebEvents.length) {
|
|
613
645
|
// if rrweb isn't ready to accept events earlier, then we queued them up.
|
|
614
646
|
// now that `emit` has been called rrweb should be ready to accept them.
|
|
@@ -631,6 +663,9 @@ export class LazyLoadedSessionRecording {
|
|
|
631
663
|
}
|
|
632
664
|
|
|
633
665
|
private _tryTakeFullSnapshot(): boolean {
|
|
666
|
+
if (!this.isStarted || !this._recording) {
|
|
667
|
+
return false
|
|
668
|
+
}
|
|
634
669
|
const rrwebRecord = getRRWebRecord()
|
|
635
670
|
if (!rrwebRecord || typeof (rrwebRecord as any).takeFullSnapshot !== 'function') {
|
|
636
671
|
return false
|
|
@@ -651,6 +686,9 @@ export class LazyLoadedSessionRecording {
|
|
|
651
686
|
}
|
|
652
687
|
|
|
653
688
|
private _scheduleFullSnapshot(): void {
|
|
689
|
+
if (!this.isStarted || !this._recording) {
|
|
690
|
+
return
|
|
691
|
+
}
|
|
654
692
|
if (this._fullSnapshotTimer) {
|
|
655
693
|
clearInterval(this._fullSnapshotTimer)
|
|
656
694
|
}
|
|
@@ -670,6 +708,9 @@ export class LazyLoadedSessionRecording {
|
|
|
670
708
|
}
|
|
671
709
|
|
|
672
710
|
private _pauseRecording() {
|
|
711
|
+
if (!this.isStarted || !this._recording) {
|
|
712
|
+
return
|
|
713
|
+
}
|
|
673
714
|
// we check _urlBlocked not status, since more than one thing can affect status
|
|
674
715
|
if (this._urlTriggerMatching.urlBlocked) {
|
|
675
716
|
return
|
|
@@ -689,6 +730,9 @@ export class LazyLoadedSessionRecording {
|
|
|
689
730
|
}
|
|
690
731
|
|
|
691
732
|
private _resumeRecording() {
|
|
733
|
+
if (!this.isStarted || !this._recording) {
|
|
734
|
+
return
|
|
735
|
+
}
|
|
692
736
|
// we check _urlBlocked not status, since more than one thing can affect status
|
|
693
737
|
if (!this._urlTriggerMatching.urlBlocked) {
|
|
694
738
|
return
|
|
@@ -704,6 +748,9 @@ export class LazyLoadedSessionRecording {
|
|
|
704
748
|
}
|
|
705
749
|
|
|
706
750
|
private _activateTrigger(triggerType: TriggerType) {
|
|
751
|
+
if (!this.isStarted || !this._recording || !this._isFullyReady) {
|
|
752
|
+
return
|
|
753
|
+
}
|
|
707
754
|
if (this._triggerMatching.triggerStatus(this.sessionId) === TRIGGER_PENDING) {
|
|
708
755
|
// status is stored separately for URL and event triggers
|
|
709
756
|
this._instance?.persistence?.register({
|
|
@@ -718,7 +765,7 @@ export class LazyLoadedSessionRecording {
|
|
|
718
765
|
}
|
|
719
766
|
|
|
720
767
|
get isStarted(): boolean {
|
|
721
|
-
return !!this.
|
|
768
|
+
return !!this._recording?.stop
|
|
722
769
|
}
|
|
723
770
|
|
|
724
771
|
get _remoteConfig(): SessionRecordingPersistedConfig | undefined {
|
|
@@ -731,6 +778,9 @@ export class LazyLoadedSessionRecording {
|
|
|
731
778
|
}
|
|
732
779
|
|
|
733
780
|
async start(startReason?: SessionStartReason) {
|
|
781
|
+
if (this._permanentlyDisabled) {
|
|
782
|
+
return
|
|
783
|
+
}
|
|
734
784
|
this._isFullyReady = false
|
|
735
785
|
const config = this._remoteConfig
|
|
736
786
|
if (!config) {
|
|
@@ -761,9 +811,17 @@ export class LazyLoadedSessionRecording {
|
|
|
761
811
|
this._urlTriggerMatching.onConfig(config)
|
|
762
812
|
|
|
763
813
|
this._eventTriggerMatching.onConfig(config)
|
|
764
|
-
this._removeEventTriggerCaptureHook?.()
|
|
765
|
-
this._addEventTriggerListener()
|
|
766
814
|
|
|
815
|
+
// Start rrweb first; only once we have a valid recorder do we install any listeners/timers.
|
|
816
|
+
await this._startRecorder()
|
|
817
|
+
|
|
818
|
+
// If rrweb failed to load/start, do not proceed further.
|
|
819
|
+
// This prevents installing listeners that assume rrweb is active.
|
|
820
|
+
if (!this.isStarted) {
|
|
821
|
+
return
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// Now that rrweb has started, we can safely install replay side-effects.
|
|
767
825
|
this._linkedFlagMatching.onConfig(config, (flag, variant) => {
|
|
768
826
|
this._reportStarted('linked_flag_matched', {
|
|
769
827
|
flag,
|
|
@@ -772,13 +830,8 @@ export class LazyLoadedSessionRecording {
|
|
|
772
830
|
})
|
|
773
831
|
|
|
774
832
|
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
|
-
}
|
|
833
|
+
this._removeEventTriggerCaptureHook?.()
|
|
834
|
+
this._addEventTriggerListener()
|
|
782
835
|
|
|
783
836
|
// Only start processing rrweb emits once the ingestion endpoint is available.
|
|
784
837
|
// If it isn't available, we must degrade to a no-op (never crash the host app).
|
|
@@ -834,7 +887,13 @@ export class LazyLoadedSessionRecording {
|
|
|
834
887
|
}
|
|
835
888
|
|
|
836
889
|
if (this.status === ACTIVE) {
|
|
837
|
-
|
|
890
|
+
const reason = startReason || 'recording_initialized'
|
|
891
|
+
if (reason !== 'recording_initialized' || !this._hasReportedRecordingInitialized) {
|
|
892
|
+
if (reason === 'recording_initialized') {
|
|
893
|
+
this._hasReportedRecordingInitialized = true
|
|
894
|
+
}
|
|
895
|
+
this._reportStarted(reason)
|
|
896
|
+
}
|
|
838
897
|
}
|
|
839
898
|
}
|
|
840
899
|
|
|
@@ -886,10 +945,11 @@ export class LazyLoadedSessionRecording {
|
|
|
886
945
|
// Clear any queued rrweb events to prevent memory leaks from closures
|
|
887
946
|
this._queuedRRWebEvents = []
|
|
888
947
|
|
|
889
|
-
this.
|
|
948
|
+
this._recording?.stop?.()
|
|
949
|
+
this._recording = undefined
|
|
890
950
|
this._stopRrweb = undefined
|
|
891
|
-
|
|
892
951
|
this._isFullyReady = false
|
|
952
|
+
this._hasReportedRecordingInitialized = false
|
|
893
953
|
|
|
894
954
|
logger.info('stopped')
|
|
895
955
|
}
|
|
@@ -911,8 +971,13 @@ export class LazyLoadedSessionRecording {
|
|
|
911
971
|
}
|
|
912
972
|
|
|
913
973
|
onRRwebEmit(rawEvent: eventWithTime) {
|
|
974
|
+
// First-line invariant gate: drop everything unless replay is truly started.
|
|
975
|
+
if (!this.isStarted || !this._recording) {
|
|
976
|
+
return
|
|
977
|
+
}
|
|
978
|
+
|
|
914
979
|
// Never process rrweb emits until we're fully ready.
|
|
915
|
-
if (!this._isFullyReady
|
|
980
|
+
if (!this._isFullyReady) {
|
|
916
981
|
return
|
|
917
982
|
}
|
|
918
983
|
|
|
@@ -1046,6 +1111,9 @@ export class LazyLoadedSessionRecording {
|
|
|
1046
1111
|
}
|
|
1047
1112
|
|
|
1048
1113
|
public overrideLinkedFlag() {
|
|
1114
|
+
if (!this.isStarted || !this._recording || !this._isFullyReady) {
|
|
1115
|
+
return
|
|
1116
|
+
}
|
|
1049
1117
|
this._linkedFlagMatching.linkedFlagSeen = true
|
|
1050
1118
|
this._tryTakeFullSnapshot()
|
|
1051
1119
|
this._reportStarted('linked_flag_overridden')
|
|
@@ -1058,6 +1126,9 @@ export class LazyLoadedSessionRecording {
|
|
|
1058
1126
|
* instead call `posthog.startSessionRecording({sampling: true})`
|
|
1059
1127
|
* */
|
|
1060
1128
|
public overrideSampling() {
|
|
1129
|
+
if (!this.isStarted || !this._recording || !this._isFullyReady) {
|
|
1130
|
+
return
|
|
1131
|
+
}
|
|
1061
1132
|
this._instance.persistence?.register({
|
|
1062
1133
|
// short-circuits the `makeSamplingDecision` function in the session recording module
|
|
1063
1134
|
[SESSION_RECORDING_IS_SAMPLED]: this.sessionId,
|
|
@@ -1073,6 +1144,9 @@ export class LazyLoadedSessionRecording {
|
|
|
1073
1144
|
* instead call `posthog.startSessionRecording({trigger: 'url' | 'event'})`
|
|
1074
1145
|
* */
|
|
1075
1146
|
public overrideTrigger(triggerType: TriggerType) {
|
|
1147
|
+
if (!this.isStarted || !this._recording || !this._isFullyReady) {
|
|
1148
|
+
return
|
|
1149
|
+
}
|
|
1076
1150
|
this._activateTrigger(triggerType)
|
|
1077
1151
|
}
|
|
1078
1152
|
|
|
@@ -1243,10 +1317,18 @@ export class LazyLoadedSessionRecording {
|
|
|
1243
1317
|
}
|
|
1244
1318
|
|
|
1245
1319
|
private _reportStarted(startReason: SessionStartReason, tagPayload?: Record<string, any>) {
|
|
1320
|
+
if (!this.isStarted || !this._recording) {
|
|
1321
|
+
return
|
|
1322
|
+
}
|
|
1246
1323
|
this._instance.registerForSession({
|
|
1247
1324
|
$session_recording_start_reason: startReason,
|
|
1248
1325
|
})
|
|
1249
|
-
|
|
1326
|
+
const message = startReason.replace('_', ' ')
|
|
1327
|
+
if (typeof tagPayload === 'undefined') {
|
|
1328
|
+
logger.info(message)
|
|
1329
|
+
} else {
|
|
1330
|
+
logger.info(message, tagPayload)
|
|
1331
|
+
}
|
|
1250
1332
|
if (!includes(['recording_initialized', 'session_id_changed'], startReason)) {
|
|
1251
1333
|
this._tryAddCustomEvent(startReason, tagPayload)
|
|
1252
1334
|
}
|
|
@@ -1416,7 +1498,7 @@ export class LazyLoadedSessionRecording {
|
|
|
1416
1498
|
}
|
|
1417
1499
|
|
|
1418
1500
|
private async _startRecorder() {
|
|
1419
|
-
if (this.
|
|
1501
|
+
if (this._permanentlyDisabled || this._recording) {
|
|
1420
1502
|
return
|
|
1421
1503
|
}
|
|
1422
1504
|
|
|
@@ -1474,46 +1556,56 @@ export class LazyLoadedSessionRecording {
|
|
|
1474
1556
|
}
|
|
1475
1557
|
|
|
1476
1558
|
if (!rrwebRecord) {
|
|
1477
|
-
|
|
1478
|
-
'_startRecorder was called but rrwebRecord is not available. This indicates something has gone wrong.'
|
|
1479
|
-
)
|
|
1559
|
+
this._disablePermanently('rrweb record function unavailable')
|
|
1480
1560
|
return
|
|
1481
1561
|
}
|
|
1482
1562
|
|
|
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
1563
|
const activePlugins = this._gatherRRWebPlugins()
|
|
1564
|
+
|
|
1565
|
+
let stopHandler: listenerHandler | undefined
|
|
1499
1566
|
try {
|
|
1500
|
-
|
|
1567
|
+
stopHandler = rrwebRecord({
|
|
1501
1568
|
emit: (event) => {
|
|
1502
1569
|
try {
|
|
1503
1570
|
this.onRRwebEmit(event)
|
|
1504
1571
|
} catch (e) {
|
|
1572
|
+
// never throw from rrweb emit handler
|
|
1505
1573
|
logger.error('error in rrweb emit handler', e)
|
|
1506
1574
|
}
|
|
1507
1575
|
},
|
|
1508
1576
|
plugins: activePlugins,
|
|
1509
1577
|
...sessionRecordingOptions,
|
|
1510
|
-
})
|
|
1578
|
+
}) as unknown as listenerHandler
|
|
1511
1579
|
} catch (e) {
|
|
1512
|
-
|
|
1513
|
-
|
|
1580
|
+
this._disablePermanently('rrweb recorder threw during initialization', e)
|
|
1581
|
+
return
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
if (typeof stopHandler !== 'function') {
|
|
1585
|
+
this._disablePermanently('rrweb recorder returned an invalid stop handler')
|
|
1514
1586
|
return
|
|
1515
1587
|
}
|
|
1516
1588
|
|
|
1589
|
+
// Mark replay started only after rrweb has successfully returned a valid stop handler.
|
|
1590
|
+
this._recording = { stop: stopHandler }
|
|
1591
|
+
this._stopRrweb = stopHandler
|
|
1592
|
+
|
|
1593
|
+
// Only create mutation throttler once replay is truly started.
|
|
1594
|
+
this._mutationThrottler =
|
|
1595
|
+
this._mutationThrottler ??
|
|
1596
|
+
new MutationThrottler(rrwebRecord, {
|
|
1597
|
+
refillRate: this._instance.config.session_recording?.__mutationThrottlerRefillRate,
|
|
1598
|
+
bucketSize: this._instance.config.session_recording?.__mutationThrottlerBucketSize,
|
|
1599
|
+
onBlockedNode: (id, node) => {
|
|
1600
|
+
const message = `Too many mutations on node '${id}'. Rate limiting. This could be due to SVG animations or something similar`
|
|
1601
|
+
logger.info(message, {
|
|
1602
|
+
node: node,
|
|
1603
|
+
})
|
|
1604
|
+
|
|
1605
|
+
this.log(LOGGER_PREFIX + ' ' + message, 'warn')
|
|
1606
|
+
},
|
|
1607
|
+
})
|
|
1608
|
+
|
|
1517
1609
|
// We reset the last activity timestamp, resetting the idle timer
|
|
1518
1610
|
this._lastActivityTimestamp = Date.now()
|
|
1519
1611
|
// 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/leanbase.ts
CHANGED
|
@@ -43,7 +43,7 @@ import { PageViewManager } from './page-view'
|
|
|
43
43
|
import { ScrollManager } from './scroll-manager'
|
|
44
44
|
import { isLikelyBot } from './utils/blocked-uas'
|
|
45
45
|
import { SessionRecording } from './extensions/replay/session-recording'
|
|
46
|
-
import
|
|
46
|
+
import { RequestRouter } from './utils/request-router'
|
|
47
47
|
import type { ConsentManager } from '../../browser/lib/src/consent'
|
|
48
48
|
|
|
49
49
|
const defaultConfig = (): LeanbaseConfig => ({
|
|
@@ -124,6 +124,9 @@ export class Leanbase extends PostHogCore {
|
|
|
124
124
|
this.isLoaded = true
|
|
125
125
|
this.persistence = new LeanbasePersistence(this.config)
|
|
126
126
|
|
|
127
|
+
// Browser SDK always has a requestRouter; session replay relies on it for $snapshot ingestion URLs.
|
|
128
|
+
this.requestRouter = new RequestRouter(this)
|
|
129
|
+
|
|
127
130
|
if (this.config.cookieless_mode !== 'always') {
|
|
128
131
|
this.sessionManager = new SessionIdManager(this)
|
|
129
132
|
this.sessionPropsManager = new SessionPropsManager(this, this.sessionManager, this.persistence)
|