@leanbase-giangnd/js 0.2.2 → 0.2.4

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 CHANGED
@@ -1171,7 +1171,7 @@ const detectDeviceType = function (user_agent) {
1171
1171
  }
1172
1172
  };
1173
1173
 
1174
- var version = "0.2.2";
1174
+ var version = "0.2.4";
1175
1175
  var packageInfo = {
1176
1176
  version: version};
1177
1177
 
@@ -3847,34 +3847,49 @@ class MutationThrottler {
3847
3847
  }
3848
3848
  return [id, node];
3849
3849
  };
3850
- this._getNode = id => this._rrweb.mirror.getNode(id);
3850
+ this._getNode = id => {
3851
+ // eslint-disable-next-line posthog-js/no-direct-undefined-check, posthog-js/no-direct-null-check
3852
+ if (id === null || id === undefined) {
3853
+ return null;
3854
+ }
3855
+ try {
3856
+ return this._rrweb.mirror.getNode(id) ?? null;
3857
+ } catch {
3858
+ return null;
3859
+ }
3860
+ };
3851
3861
  this._numberOfChanges = data => {
3852
3862
  return (data.removes?.length ?? 0) + (data.attributes?.length ?? 0) + (data.texts?.length ?? 0) + (data.adds?.length ?? 0);
3853
3863
  };
3854
3864
  this.throttleMutations = event => {
3855
- if (event.type !== INCREMENTAL_SNAPSHOT_EVENT_TYPE || event.data.source !== MUTATION_SOURCE_TYPE) {
3865
+ try {
3866
+ if (event.type !== INCREMENTAL_SNAPSHOT_EVENT_TYPE || event.data.source !== MUTATION_SOURCE_TYPE) {
3867
+ return event;
3868
+ }
3869
+ const data = event.data;
3870
+ const initialMutationCount = this._numberOfChanges(data);
3871
+ if (data.attributes) {
3872
+ // Most problematic mutations come from attrs where the style or minor properties are changed rapidly
3873
+ data.attributes = data.attributes.filter(attr => {
3874
+ const id = attr?.id;
3875
+ if (typeof id !== 'number') {
3876
+ return true;
3877
+ }
3878
+ const [nodeId] = this._getNodeOrRelevantParent(id);
3879
+ const isRateLimited = this._rateLimiter.consumeRateLimit(nodeId);
3880
+ return !isRateLimited;
3881
+ });
3882
+ }
3883
+ // Check if every part of the mutation is empty in which case there is nothing to do
3884
+ const mutationCount = this._numberOfChanges(data);
3885
+ if (mutationCount === 0 && initialMutationCount !== mutationCount) {
3886
+ // If we have modified the mutation count and the remaining count is 0, then we don't need the event.
3887
+ return;
3888
+ }
3889
+ return event;
3890
+ } catch {
3856
3891
  return event;
3857
3892
  }
3858
- const data = event.data;
3859
- const initialMutationCount = this._numberOfChanges(data);
3860
- if (data.attributes) {
3861
- // Most problematic mutations come from attrs where the style or minor properties are changed rapidly
3862
- data.attributes = data.attributes.filter(attr => {
3863
- const [nodeId] = this._getNodeOrRelevantParent(attr.id);
3864
- const isRateLimited = this._rateLimiter.consumeRateLimit(nodeId);
3865
- if (isRateLimited) {
3866
- return false;
3867
- }
3868
- return attr;
3869
- });
3870
- }
3871
- // Check if every part of the mutation is empty in which case there is nothing to do
3872
- const mutationCount = this._numberOfChanges(data);
3873
- if (mutationCount === 0 && initialMutationCount !== mutationCount) {
3874
- // If we have modified the mutation count and the remaining count is 0, then we don't need the event.
3875
- return;
3876
- }
3877
- return event;
3878
3893
  };
3879
3894
  const configuredBucketSize = this._options.bucketSize ?? 100;
3880
3895
  const effectiveBucketSize = Math.max(configuredBucketSize - 1, 1);
@@ -4112,6 +4127,9 @@ class LazyLoadedSessionRecording {
4112
4127
  */
4113
4128
  this._queuedRRWebEvents = [];
4114
4129
  this._isIdle = 'unknown';
4130
+ // Replay should not process rrweb emits until all critical dependencies are ready.
4131
+ this._isFullyReady = false;
4132
+ this._loggedMissingEndpointFor = false;
4115
4133
  // we need to be able to check the state of the event and url triggers separately
4116
4134
  // as we make some decisions based on them without referencing LinkedFlag etc
4117
4135
  this._triggerMatching = new PendingTriggerMatching();
@@ -4292,7 +4310,11 @@ class LazyLoadedSessionRecording {
4292
4310
  }
4293
4311
  }
4294
4312
  _tryAddCustomEvent(tag, payload) {
4295
- return this._tryRRWebMethod(newQueuedEvent(() => getRRWebRecord().addCustomEvent(tag, payload)));
4313
+ const rrwebRecord = getRRWebRecord();
4314
+ if (!rrwebRecord || typeof rrwebRecord.addCustomEvent !== 'function') {
4315
+ return false;
4316
+ }
4317
+ return this._tryRRWebMethod(newQueuedEvent(() => rrwebRecord.addCustomEvent(tag, payload)));
4296
4318
  }
4297
4319
  _pageViewFallBack() {
4298
4320
  try {
@@ -4338,7 +4360,11 @@ class LazyLoadedSessionRecording {
4338
4360
  }
4339
4361
  }
4340
4362
  _tryTakeFullSnapshot() {
4341
- return this._tryRRWebMethod(newQueuedEvent(() => getRRWebRecord().takeFullSnapshot()));
4363
+ const rrwebRecord = getRRWebRecord();
4364
+ if (!rrwebRecord || typeof rrwebRecord.takeFullSnapshot !== 'function') {
4365
+ return false;
4366
+ }
4367
+ return this._tryRRWebMethod(newQueuedEvent(() => rrwebRecord.takeFullSnapshot()));
4342
4368
  }
4343
4369
  get _fullSnapshotIntervalMillis() {
4344
4370
  if (this._triggerMatching.triggerStatus(this.sessionId) === TRIGGER_PENDING && !['sampled', 'active'].includes(this.status)) {
@@ -4414,6 +4440,7 @@ class LazyLoadedSessionRecording {
4414
4440
  return parsedConfig;
4415
4441
  }
4416
4442
  async start(startReason) {
4443
+ this._isFullyReady = false;
4417
4444
  const config = this._remoteConfig;
4418
4445
  if (!config) {
4419
4446
  logger.info('remote config must be stored in persistence before recording can start');
@@ -4448,6 +4475,14 @@ class LazyLoadedSessionRecording {
4448
4475
  });
4449
4476
  this._makeSamplingDecision(this.sessionId);
4450
4477
  await this._startRecorder();
4478
+ // If rrweb failed to load/start, do not proceed further.
4479
+ // This prevents installing listeners that assume rrweb is active.
4480
+ if (!this.isStarted) {
4481
+ return;
4482
+ }
4483
+ // Only start processing rrweb emits once the ingestion endpoint is available.
4484
+ // If it isn't available, we must degrade to a no-op (never crash the host app).
4485
+ this._isFullyReady = this._canCaptureSnapshots();
4451
4486
  // calling addEventListener multiple times is safe and will not add duplicates
4452
4487
  addEventListener(win, 'beforeunload', this._onBeforeUnload);
4453
4488
  addEventListener(win, 'offline', this._onOffline);
@@ -4525,77 +4560,100 @@ class LazyLoadedSessionRecording {
4525
4560
  this._queuedRRWebEvents = [];
4526
4561
  this._stopRrweb?.();
4527
4562
  this._stopRrweb = undefined;
4563
+ this._isFullyReady = false;
4528
4564
  logger.info('stopped');
4529
4565
  }
4566
+ _snapshotIngestionUrl() {
4567
+ const endpointFor = this._instance?.requestRouter?.endpointFor;
4568
+ if (typeof endpointFor !== 'function') {
4569
+ return null;
4570
+ }
4571
+ try {
4572
+ return endpointFor('api', this._endpoint);
4573
+ } catch {
4574
+ return null;
4575
+ }
4576
+ }
4577
+ _canCaptureSnapshots() {
4578
+ return !!this._snapshotIngestionUrl();
4579
+ }
4530
4580
  onRRwebEmit(rawEvent) {
4531
- this._processQueuedEvents();
4532
- if (!rawEvent || !core.isObject(rawEvent)) {
4581
+ // Never process rrweb emits until we're fully ready.
4582
+ if (!this._isFullyReady || !this.isStarted) {
4533
4583
  return;
4534
4584
  }
4535
- if (rawEvent.type === EventType.Meta) {
4536
- const href = this._maskUrl(rawEvent.data.href);
4537
- this._lastHref = href;
4538
- if (!href) {
4585
+ try {
4586
+ this._processQueuedEvents();
4587
+ if (!rawEvent || !core.isObject(rawEvent)) {
4539
4588
  return;
4540
4589
  }
4541
- rawEvent.data.href = href;
4542
- } else {
4543
- this._pageViewFallBack();
4544
- }
4545
- // Check if the URL matches any trigger patterns
4546
- this._urlTriggerMatching.checkUrlTriggerConditions(() => this._pauseRecording(), () => this._resumeRecording(), triggerType => this._activateTrigger(triggerType));
4547
- // always have to check if the URL is blocked really early,
4548
- // or you risk getting stuck in a loop
4549
- if (this._urlTriggerMatching.urlBlocked && !isRecordingPausedEvent(rawEvent)) {
4550
- return;
4551
- }
4552
- // we're processing a full snapshot, so we should reset the timer
4553
- if (rawEvent.type === EventType.FullSnapshot) {
4554
- this._scheduleFullSnapshot();
4555
- // Full snapshots reset rrweb's node IDs, so clear any logged node tracking
4556
- this._mutationThrottler?.reset();
4557
- }
4558
- // Clear the buffer if waiting for a trigger and only keep data from after the current full snapshot
4559
- // we always start trigger pending so need to wait for flags before we know if we're really pending
4560
- if (rawEvent.type === EventType.FullSnapshot && this._triggerMatching.triggerStatus(this.sessionId) === TRIGGER_PENDING) {
4561
- this._clearBufferBeforeMostRecentMeta();
4562
- }
4563
- const throttledEvent = this._mutationThrottler ? this._mutationThrottler.throttleMutations(rawEvent) : rawEvent;
4564
- if (!throttledEvent) {
4565
- return;
4566
- }
4567
- // TODO: Re-add ensureMaxMessageSize once we are confident in it
4568
- const event = truncateLargeConsoleLogs(throttledEvent);
4569
- this._updateWindowAndSessionIds(event);
4570
- // When in an idle state we keep recording but don't capture the events,
4571
- // we don't want to return early if idle is 'unknown'
4572
- if (this._isIdle === true && !isSessionIdleEvent(event)) {
4573
- return;
4574
- }
4575
- if (isSessionIdleEvent(event)) {
4576
- // session idle events have a timestamp when rrweb sees them
4577
- // which can artificially lengthen a session
4578
- // we know when we detected it based on the payload and can correct the timestamp
4579
- const payload = event.data.payload;
4580
- if (payload) {
4581
- const lastActivity = payload.lastActivityTimestamp;
4582
- const threshold = payload.threshold;
4583
- event.timestamp = lastActivity + threshold;
4590
+ if (rawEvent.type === EventType.Meta) {
4591
+ const href = this._maskUrl(rawEvent.data.href);
4592
+ this._lastHref = href;
4593
+ if (!href) {
4594
+ return;
4595
+ }
4596
+ rawEvent.data.href = href;
4597
+ } else {
4598
+ this._pageViewFallBack();
4584
4599
  }
4600
+ // Check if the URL matches any trigger patterns
4601
+ this._urlTriggerMatching.checkUrlTriggerConditions(() => this._pauseRecording(), () => this._resumeRecording(), triggerType => this._activateTrigger(triggerType));
4602
+ // always have to check if the URL is blocked really early,
4603
+ // or you risk getting stuck in a loop
4604
+ if (this._urlTriggerMatching.urlBlocked && !isRecordingPausedEvent(rawEvent)) {
4605
+ return;
4606
+ }
4607
+ // we're processing a full snapshot, so we should reset the timer
4608
+ if (rawEvent.type === EventType.FullSnapshot) {
4609
+ this._scheduleFullSnapshot();
4610
+ // Full snapshots reset rrweb's node IDs, so clear any logged node tracking
4611
+ this._mutationThrottler?.reset();
4612
+ }
4613
+ // Clear the buffer if waiting for a trigger and only keep data from after the current full snapshot
4614
+ // we always start trigger pending so need to wait for flags before we know if we're really pending
4615
+ if (rawEvent.type === EventType.FullSnapshot && this._triggerMatching.triggerStatus(this.sessionId) === TRIGGER_PENDING) {
4616
+ this._clearBufferBeforeMostRecentMeta();
4617
+ }
4618
+ const throttledEvent = this._mutationThrottler ? this._mutationThrottler.throttleMutations(rawEvent) : rawEvent;
4619
+ if (!throttledEvent) {
4620
+ return;
4621
+ }
4622
+ // TODO: Re-add ensureMaxMessageSize once we are confident in it
4623
+ const event = truncateLargeConsoleLogs(throttledEvent);
4624
+ this._updateWindowAndSessionIds(event);
4625
+ // When in an idle state we keep recording but don't capture the events,
4626
+ // we don't want to return early if idle is 'unknown'
4627
+ if (this._isIdle === true && !isSessionIdleEvent(event)) {
4628
+ return;
4629
+ }
4630
+ if (isSessionIdleEvent(event)) {
4631
+ // session idle events have a timestamp when rrweb sees them
4632
+ // which can artificially lengthen a session
4633
+ // we know when we detected it based on the payload and can correct the timestamp
4634
+ const payload = event.data.payload;
4635
+ if (payload) {
4636
+ const lastActivity = payload.lastActivityTimestamp;
4637
+ const threshold = payload.threshold;
4638
+ event.timestamp = lastActivity + threshold;
4639
+ }
4640
+ }
4641
+ const eventToSend = this._instance.config.session_recording?.compress_events ?? true ? compressEvent(event) : event;
4642
+ const size = estimateSize(eventToSend);
4643
+ const properties = {
4644
+ $snapshot_bytes: size,
4645
+ $snapshot_data: eventToSend,
4646
+ $session_id: this._sessionId,
4647
+ $window_id: this._windowId
4648
+ };
4649
+ if (this.status === DISABLED) {
4650
+ this._clearBuffer();
4651
+ return;
4652
+ }
4653
+ this._captureSnapshotBuffered(properties);
4654
+ } catch (e) {
4655
+ logger.error('error processing rrweb event', e);
4585
4656
  }
4586
- const eventToSend = this._instance.config.session_recording?.compress_events ?? true ? compressEvent(event) : event;
4587
- const size = estimateSize(eventToSend);
4588
- const properties = {
4589
- $snapshot_bytes: size,
4590
- $snapshot_data: eventToSend,
4591
- $session_id: this._sessionId,
4592
- $window_id: this._windowId
4593
- };
4594
- if (this.status === DISABLED) {
4595
- this._clearBuffer();
4596
- return;
4597
- }
4598
- this._captureSnapshotBuffered(properties);
4599
4657
  }
4600
4658
  get status() {
4601
4659
  return this._statusMatcher({
@@ -4673,6 +4731,16 @@ class LazyLoadedSessionRecording {
4673
4731
  }, RECORDING_BUFFER_TIMEOUT);
4674
4732
  return this._buffer;
4675
4733
  }
4734
+ if (!this._canCaptureSnapshots()) {
4735
+ if (!this._loggedMissingEndpointFor) {
4736
+ this._loggedMissingEndpointFor = true;
4737
+ logger.warn('snapshot capture skipped because requestRouter.endpointFor is unavailable');
4738
+ }
4739
+ this._flushBufferTimer = setTimeout(() => {
4740
+ this._flushBuffer();
4741
+ }, RECORDING_BUFFER_TIMEOUT);
4742
+ return this._buffer;
4743
+ }
4676
4744
  if (this._buffer.data.length > 0) {
4677
4745
  const snapshotEvents = splitBuffer(this._buffer);
4678
4746
  snapshotEvents.forEach(snapshotBuffer => {
@@ -4705,13 +4773,25 @@ class LazyLoadedSessionRecording {
4705
4773
  }
4706
4774
  }
4707
4775
  _captureSnapshot(properties) {
4708
- // :TRICKY: Make sure we batch these requests, use a custom endpoint and don't truncate the strings.
4709
- this._instance.capture('$snapshot', properties, {
4710
- _url: this._instance.requestRouter.endpointFor('api', this._endpoint),
4711
- _noTruncate: true,
4712
- _batchKey: SESSION_RECORDING_BATCH_KEY,
4713
- skip_client_rate_limiting: true
4714
- });
4776
+ const url = this._snapshotIngestionUrl();
4777
+ if (!url) {
4778
+ if (!this._loggedMissingEndpointFor) {
4779
+ this._loggedMissingEndpointFor = true;
4780
+ logger.warn('snapshot capture skipped because requestRouter.endpointFor is unavailable');
4781
+ }
4782
+ return;
4783
+ }
4784
+ try {
4785
+ // :TRICKY: Make sure we batch these requests, use a custom endpoint and don't truncate the strings.
4786
+ this._instance.capture('$snapshot', properties, {
4787
+ _url: url,
4788
+ _noTruncate: true,
4789
+ _batchKey: SESSION_RECORDING_BATCH_KEY,
4790
+ skip_client_rate_limiting: true
4791
+ });
4792
+ } catch (e) {
4793
+ logger.error('failed to capture snapshot', e);
4794
+ }
4715
4795
  }
4716
4796
  _snapshotUrl() {
4717
4797
  const host = this._instance.config.host || '';
@@ -4981,13 +5061,23 @@ class LazyLoadedSessionRecording {
4981
5061
  }
4982
5062
  });
4983
5063
  const activePlugins = this._gatherRRWebPlugins();
4984
- this._stopRrweb = rrwebRecord({
4985
- emit: event => {
4986
- this.onRRwebEmit(event);
4987
- },
4988
- plugins: activePlugins,
4989
- ...sessionRecordingOptions
4990
- });
5064
+ try {
5065
+ this._stopRrweb = rrwebRecord({
5066
+ emit: event => {
5067
+ try {
5068
+ this.onRRwebEmit(event);
5069
+ } catch (e) {
5070
+ logger.error('error in rrweb emit handler', e);
5071
+ }
5072
+ },
5073
+ plugins: activePlugins,
5074
+ ...sessionRecordingOptions
5075
+ });
5076
+ } catch (e) {
5077
+ logger.error('failed to start rrweb recorder', e);
5078
+ this._stopRrweb = undefined;
5079
+ return;
5080
+ }
4991
5081
  // We reset the last activity timestamp, resetting the idle timer
4992
5082
  this._lastActivityTimestamp = Date.now();
4993
5083
  // stay unknown if we're not sure if we're idle or not
@@ -5173,18 +5263,25 @@ class SessionRecording {
5173
5263
  // If extensions provide an init function, use it. Otherwise, fall back to the local LazyLoadedSessionRecording
5174
5264
  if (assignableWindow.__PosthogExtensions__?.initSessionRecording) {
5175
5265
  if (!this._lazyLoadedSessionRecording) {
5176
- this._lazyLoadedSessionRecording = assignableWindow.__PosthogExtensions__?.initSessionRecording(this._instance);
5177
- this._lazyLoadedSessionRecording._forceAllowLocalhostNetworkCapture = this._forceAllowLocalhostNetworkCapture;
5266
+ const maybeRecording = assignableWindow.__PosthogExtensions__?.initSessionRecording(this._instance);
5267
+ if (maybeRecording && typeof maybeRecording.start === 'function') {
5268
+ this._lazyLoadedSessionRecording = maybeRecording;
5269
+ this._lazyLoadedSessionRecording._forceAllowLocalhostNetworkCapture = this._forceAllowLocalhostNetworkCapture;
5270
+ } else {
5271
+ log.warn('initSessionRecording was present but did not return a recorder instance; falling back to local recorder');
5272
+ }
5178
5273
  }
5179
- try {
5180
- const maybePromise = this._lazyLoadedSessionRecording.start(startReason);
5181
- if (maybePromise && typeof maybePromise.catch === 'function') {
5182
- maybePromise.catch(e => logger$2.error('error starting session recording', e));
5274
+ if (this._lazyLoadedSessionRecording) {
5275
+ try {
5276
+ const maybePromise = this._lazyLoadedSessionRecording.start(startReason);
5277
+ if (maybePromise && typeof maybePromise.catch === 'function') {
5278
+ maybePromise.catch(e => logger$2.error('error starting session recording', e));
5279
+ }
5280
+ } catch (e) {
5281
+ logger$2.error('error starting session recording', e);
5183
5282
  }
5184
- } catch (e) {
5185
- logger$2.error('error starting session recording', e);
5283
+ return;
5186
5284
  }
5187
- return;
5188
5285
  }
5189
5286
  if (!this._lazyLoadedSessionRecording) {
5190
5287
  this._lazyLoadedSessionRecording = new LazyLoadedSessionRecording(this._instance);
@@ -5299,6 +5396,10 @@ class Leanbase extends core.PostHogCore {
5299
5396
  token
5300
5397
  });
5301
5398
  super(token, mergedConfig);
5399
+ this._remoteConfigLoadAttempted = false;
5400
+ this._remoteConfigResolved = false;
5401
+ this._featureFlagsResolved = false;
5402
+ this._maybeStartedSessionRecording = false;
5302
5403
  this.personProcessingSetOncePropertiesSent = false;
5303
5404
  this.isLoaded = false;
5304
5405
  this.config = mergedConfig;
@@ -5322,10 +5423,20 @@ class Leanbase extends core.PostHogCore {
5322
5423
  this.replayAutocapture.startIfEnabled();
5323
5424
  if (this.sessionManager && this.config.cookieless_mode !== 'always') {
5324
5425
  this.sessionRecording = new SessionRecording(this);
5325
- this.sessionRecording.startIfEnabledOrStop();
5326
5426
  }
5427
+ // Start session recording only once flags + remote config have been resolved.
5428
+ // This matches the PostHog browser SDK where replay activation is driven by remote config and flags.
5327
5429
  if (this.config.preloadFeatureFlags !== false) {
5328
- this.reloadFeatureFlags();
5430
+ this.reloadFeatureFlags({
5431
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
5432
+ cb: _err => {
5433
+ this._featureFlagsResolved = true;
5434
+ this._maybeStartSessionRecording();
5435
+ }
5436
+ });
5437
+ } else {
5438
+ // If feature flags preload is explicitly disabled, treat this requirement as satisfied.
5439
+ this._featureFlagsResolved = true;
5329
5440
  }
5330
5441
  this.config.loaded?.(this);
5331
5442
  if (this.config.capture_pageview) {
@@ -5335,9 +5446,26 @@ class Leanbase extends core.PostHogCore {
5335
5446
  }
5336
5447
  }, 1);
5337
5448
  }
5338
- addEventListener(document, 'DOMContentLoaded', () => {
5339
- this.loadRemoteConfig();
5340
- });
5449
+ const triggerRemoteConfigLoad = reason => {
5450
+ logger$2.info(`remote config load triggered via ${reason}`);
5451
+ void this.loadRemoteConfig();
5452
+ };
5453
+ if (document) {
5454
+ if (document.readyState === 'loading') {
5455
+ logger$2.info('remote config load deferred until DOMContentLoaded');
5456
+ const onDomReady = () => {
5457
+ document?.removeEventListener('DOMContentLoaded', onDomReady);
5458
+ triggerRemoteConfigLoad('dom');
5459
+ };
5460
+ addEventListener(document, 'DOMContentLoaded', onDomReady, {
5461
+ once: true
5462
+ });
5463
+ } else {
5464
+ triggerRemoteConfigLoad('immediate');
5465
+ }
5466
+ } else {
5467
+ triggerRemoteConfigLoad('no-document');
5468
+ }
5341
5469
  addEventListener(window, 'onpagehide' in self ? 'pagehide' : 'unload', this.capturePageLeave.bind(this), {
5342
5470
  passive: false
5343
5471
  });
@@ -5374,11 +5502,19 @@ class Leanbase extends core.PostHogCore {
5374
5502
  }
5375
5503
  }
5376
5504
  async loadRemoteConfig() {
5377
- if (!this.isRemoteConfigLoaded) {
5505
+ if (this._remoteConfigLoadAttempted) {
5506
+ return;
5507
+ }
5508
+ this._remoteConfigLoadAttempted = true;
5509
+ try {
5378
5510
  const remoteConfig = await this.reloadRemoteConfigAsync();
5379
5511
  if (remoteConfig) {
5380
5512
  this.onRemoteConfig(remoteConfig);
5381
5513
  }
5514
+ } finally {
5515
+ // Regardless of success/failure, we consider remote config "resolved" so replay isn't blocked forever.
5516
+ this._remoteConfigResolved = true;
5517
+ this._maybeStartSessionRecording();
5382
5518
  }
5383
5519
  }
5384
5520
  onRemoteConfig(config) {
@@ -5391,6 +5527,26 @@ class Leanbase extends core.PostHogCore {
5391
5527
  this.isRemoteConfigLoaded = true;
5392
5528
  this.replayAutocapture?.onRemoteConfig(config);
5393
5529
  this.sessionRecording?.onRemoteConfig(config);
5530
+ // Remote config has been applied; allow replay start if flags are also ready.
5531
+ this._remoteConfigResolved = true;
5532
+ this._maybeStartSessionRecording();
5533
+ }
5534
+ _maybeStartSessionRecording() {
5535
+ if (this._maybeStartedSessionRecording) {
5536
+ return;
5537
+ }
5538
+ if (!this.sessionRecording) {
5539
+ return;
5540
+ }
5541
+ if (!this._featureFlagsResolved || !this._remoteConfigResolved) {
5542
+ return;
5543
+ }
5544
+ this._maybeStartedSessionRecording = true;
5545
+ try {
5546
+ this.sessionRecording.startIfEnabledOrStop();
5547
+ } catch (e) {
5548
+ logger$2.error('Failed to start session recording', e);
5549
+ }
5394
5550
  }
5395
5551
  fetch(url, options) {
5396
5552
  const fetchFn = core.getFetch();