@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leanbase-giangnd/js",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Leanbase browser SDK - event tracking, autocapture, and session replay",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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
- // Expose rrweb.record under the same contract. We provide a lazy proxy so
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._stopRrweb
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
- await this._startRecorder()
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
- this._reportStarted(startReason || 'recording_initialized')
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._stopRrweb?.()
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 || !this.isStarted) {
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
- logger.info(startReason.replace('_', ' '), tagPayload)
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._stopRrweb) {
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
- logger.error(
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
- this._stopRrweb = rrwebRecord({
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
- logger.error('failed to start rrweb recorder', e)
1513
- this._stopRrweb = undefined
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: effectiveBucketSize,
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 type { RequestRouter } from '../../browser/lib/src/utils/request-router'
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)