@leanbase-giangnd/js 0.2.3 → 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.mjs CHANGED
@@ -1169,7 +1169,7 @@ const detectDeviceType = function (user_agent) {
1169
1169
  }
1170
1170
  };
1171
1171
 
1172
- var version = "0.2.3";
1172
+ var version = "0.2.4";
1173
1173
  var packageInfo = {
1174
1174
  version: version};
1175
1175
 
@@ -3845,34 +3845,49 @@ class MutationThrottler {
3845
3845
  }
3846
3846
  return [id, node];
3847
3847
  };
3848
- this._getNode = id => this._rrweb.mirror.getNode(id);
3848
+ this._getNode = id => {
3849
+ // eslint-disable-next-line posthog-js/no-direct-undefined-check, posthog-js/no-direct-null-check
3850
+ if (id === null || id === undefined) {
3851
+ return null;
3852
+ }
3853
+ try {
3854
+ return this._rrweb.mirror.getNode(id) ?? null;
3855
+ } catch {
3856
+ return null;
3857
+ }
3858
+ };
3849
3859
  this._numberOfChanges = data => {
3850
3860
  return (data.removes?.length ?? 0) + (data.attributes?.length ?? 0) + (data.texts?.length ?? 0) + (data.adds?.length ?? 0);
3851
3861
  };
3852
3862
  this.throttleMutations = event => {
3853
- if (event.type !== INCREMENTAL_SNAPSHOT_EVENT_TYPE || event.data.source !== MUTATION_SOURCE_TYPE) {
3863
+ try {
3864
+ if (event.type !== INCREMENTAL_SNAPSHOT_EVENT_TYPE || event.data.source !== MUTATION_SOURCE_TYPE) {
3865
+ return event;
3866
+ }
3867
+ const data = event.data;
3868
+ const initialMutationCount = this._numberOfChanges(data);
3869
+ if (data.attributes) {
3870
+ // Most problematic mutations come from attrs where the style or minor properties are changed rapidly
3871
+ data.attributes = data.attributes.filter(attr => {
3872
+ const id = attr?.id;
3873
+ if (typeof id !== 'number') {
3874
+ return true;
3875
+ }
3876
+ const [nodeId] = this._getNodeOrRelevantParent(id);
3877
+ const isRateLimited = this._rateLimiter.consumeRateLimit(nodeId);
3878
+ return !isRateLimited;
3879
+ });
3880
+ }
3881
+ // Check if every part of the mutation is empty in which case there is nothing to do
3882
+ const mutationCount = this._numberOfChanges(data);
3883
+ if (mutationCount === 0 && initialMutationCount !== mutationCount) {
3884
+ // If we have modified the mutation count and the remaining count is 0, then we don't need the event.
3885
+ return;
3886
+ }
3887
+ return event;
3888
+ } catch {
3854
3889
  return event;
3855
3890
  }
3856
- const data = event.data;
3857
- const initialMutationCount = this._numberOfChanges(data);
3858
- if (data.attributes) {
3859
- // Most problematic mutations come from attrs where the style or minor properties are changed rapidly
3860
- data.attributes = data.attributes.filter(attr => {
3861
- const [nodeId] = this._getNodeOrRelevantParent(attr.id);
3862
- const isRateLimited = this._rateLimiter.consumeRateLimit(nodeId);
3863
- if (isRateLimited) {
3864
- return false;
3865
- }
3866
- return attr;
3867
- });
3868
- }
3869
- // Check if every part of the mutation is empty in which case there is nothing to do
3870
- const mutationCount = this._numberOfChanges(data);
3871
- if (mutationCount === 0 && initialMutationCount !== mutationCount) {
3872
- // If we have modified the mutation count and the remaining count is 0, then we don't need the event.
3873
- return;
3874
- }
3875
- return event;
3876
3891
  };
3877
3892
  const configuredBucketSize = this._options.bucketSize ?? 100;
3878
3893
  const effectiveBucketSize = Math.max(configuredBucketSize - 1, 1);
@@ -4110,6 +4125,9 @@ class LazyLoadedSessionRecording {
4110
4125
  */
4111
4126
  this._queuedRRWebEvents = [];
4112
4127
  this._isIdle = 'unknown';
4128
+ // Replay should not process rrweb emits until all critical dependencies are ready.
4129
+ this._isFullyReady = false;
4130
+ this._loggedMissingEndpointFor = false;
4113
4131
  // we need to be able to check the state of the event and url triggers separately
4114
4132
  // as we make some decisions based on them without referencing LinkedFlag etc
4115
4133
  this._triggerMatching = new PendingTriggerMatching();
@@ -4290,7 +4308,11 @@ class LazyLoadedSessionRecording {
4290
4308
  }
4291
4309
  }
4292
4310
  _tryAddCustomEvent(tag, payload) {
4293
- return this._tryRRWebMethod(newQueuedEvent(() => getRRWebRecord().addCustomEvent(tag, payload)));
4311
+ const rrwebRecord = getRRWebRecord();
4312
+ if (!rrwebRecord || typeof rrwebRecord.addCustomEvent !== 'function') {
4313
+ return false;
4314
+ }
4315
+ return this._tryRRWebMethod(newQueuedEvent(() => rrwebRecord.addCustomEvent(tag, payload)));
4294
4316
  }
4295
4317
  _pageViewFallBack() {
4296
4318
  try {
@@ -4336,7 +4358,11 @@ class LazyLoadedSessionRecording {
4336
4358
  }
4337
4359
  }
4338
4360
  _tryTakeFullSnapshot() {
4339
- return this._tryRRWebMethod(newQueuedEvent(() => getRRWebRecord().takeFullSnapshot()));
4361
+ const rrwebRecord = getRRWebRecord();
4362
+ if (!rrwebRecord || typeof rrwebRecord.takeFullSnapshot !== 'function') {
4363
+ return false;
4364
+ }
4365
+ return this._tryRRWebMethod(newQueuedEvent(() => rrwebRecord.takeFullSnapshot()));
4340
4366
  }
4341
4367
  get _fullSnapshotIntervalMillis() {
4342
4368
  if (this._triggerMatching.triggerStatus(this.sessionId) === TRIGGER_PENDING && !['sampled', 'active'].includes(this.status)) {
@@ -4412,6 +4438,7 @@ class LazyLoadedSessionRecording {
4412
4438
  return parsedConfig;
4413
4439
  }
4414
4440
  async start(startReason) {
4441
+ this._isFullyReady = false;
4415
4442
  const config = this._remoteConfig;
4416
4443
  if (!config) {
4417
4444
  logger.info('remote config must be stored in persistence before recording can start');
@@ -4451,6 +4478,9 @@ class LazyLoadedSessionRecording {
4451
4478
  if (!this.isStarted) {
4452
4479
  return;
4453
4480
  }
4481
+ // Only start processing rrweb emits once the ingestion endpoint is available.
4482
+ // If it isn't available, we must degrade to a no-op (never crash the host app).
4483
+ this._isFullyReady = this._canCaptureSnapshots();
4454
4484
  // calling addEventListener multiple times is safe and will not add duplicates
4455
4485
  addEventListener(win, 'beforeunload', this._onBeforeUnload);
4456
4486
  addEventListener(win, 'offline', this._onOffline);
@@ -4528,77 +4558,100 @@ class LazyLoadedSessionRecording {
4528
4558
  this._queuedRRWebEvents = [];
4529
4559
  this._stopRrweb?.();
4530
4560
  this._stopRrweb = undefined;
4561
+ this._isFullyReady = false;
4531
4562
  logger.info('stopped');
4532
4563
  }
4564
+ _snapshotIngestionUrl() {
4565
+ const endpointFor = this._instance?.requestRouter?.endpointFor;
4566
+ if (typeof endpointFor !== 'function') {
4567
+ return null;
4568
+ }
4569
+ try {
4570
+ return endpointFor('api', this._endpoint);
4571
+ } catch {
4572
+ return null;
4573
+ }
4574
+ }
4575
+ _canCaptureSnapshots() {
4576
+ return !!this._snapshotIngestionUrl();
4577
+ }
4533
4578
  onRRwebEmit(rawEvent) {
4534
- this._processQueuedEvents();
4535
- if (!rawEvent || !isObject(rawEvent)) {
4579
+ // Never process rrweb emits until we're fully ready.
4580
+ if (!this._isFullyReady || !this.isStarted) {
4536
4581
  return;
4537
4582
  }
4538
- if (rawEvent.type === EventType.Meta) {
4539
- const href = this._maskUrl(rawEvent.data.href);
4540
- this._lastHref = href;
4541
- if (!href) {
4583
+ try {
4584
+ this._processQueuedEvents();
4585
+ if (!rawEvent || !isObject(rawEvent)) {
4542
4586
  return;
4543
4587
  }
4544
- rawEvent.data.href = href;
4545
- } else {
4546
- this._pageViewFallBack();
4547
- }
4548
- // Check if the URL matches any trigger patterns
4549
- this._urlTriggerMatching.checkUrlTriggerConditions(() => this._pauseRecording(), () => this._resumeRecording(), triggerType => this._activateTrigger(triggerType));
4550
- // always have to check if the URL is blocked really early,
4551
- // or you risk getting stuck in a loop
4552
- if (this._urlTriggerMatching.urlBlocked && !isRecordingPausedEvent(rawEvent)) {
4553
- return;
4554
- }
4555
- // we're processing a full snapshot, so we should reset the timer
4556
- if (rawEvent.type === EventType.FullSnapshot) {
4557
- this._scheduleFullSnapshot();
4558
- // Full snapshots reset rrweb's node IDs, so clear any logged node tracking
4559
- this._mutationThrottler?.reset();
4560
- }
4561
- // Clear the buffer if waiting for a trigger and only keep data from after the current full snapshot
4562
- // we always start trigger pending so need to wait for flags before we know if we're really pending
4563
- if (rawEvent.type === EventType.FullSnapshot && this._triggerMatching.triggerStatus(this.sessionId) === TRIGGER_PENDING) {
4564
- this._clearBufferBeforeMostRecentMeta();
4565
- }
4566
- const throttledEvent = this._mutationThrottler ? this._mutationThrottler.throttleMutations(rawEvent) : rawEvent;
4567
- if (!throttledEvent) {
4568
- return;
4569
- }
4570
- // TODO: Re-add ensureMaxMessageSize once we are confident in it
4571
- const event = truncateLargeConsoleLogs(throttledEvent);
4572
- this._updateWindowAndSessionIds(event);
4573
- // When in an idle state we keep recording but don't capture the events,
4574
- // we don't want to return early if idle is 'unknown'
4575
- if (this._isIdle === true && !isSessionIdleEvent(event)) {
4576
- return;
4577
- }
4578
- if (isSessionIdleEvent(event)) {
4579
- // session idle events have a timestamp when rrweb sees them
4580
- // which can artificially lengthen a session
4581
- // we know when we detected it based on the payload and can correct the timestamp
4582
- const payload = event.data.payload;
4583
- if (payload) {
4584
- const lastActivity = payload.lastActivityTimestamp;
4585
- const threshold = payload.threshold;
4586
- event.timestamp = lastActivity + threshold;
4588
+ if (rawEvent.type === EventType.Meta) {
4589
+ const href = this._maskUrl(rawEvent.data.href);
4590
+ this._lastHref = href;
4591
+ if (!href) {
4592
+ return;
4593
+ }
4594
+ rawEvent.data.href = href;
4595
+ } else {
4596
+ this._pageViewFallBack();
4587
4597
  }
4598
+ // Check if the URL matches any trigger patterns
4599
+ this._urlTriggerMatching.checkUrlTriggerConditions(() => this._pauseRecording(), () => this._resumeRecording(), triggerType => this._activateTrigger(triggerType));
4600
+ // always have to check if the URL is blocked really early,
4601
+ // or you risk getting stuck in a loop
4602
+ if (this._urlTriggerMatching.urlBlocked && !isRecordingPausedEvent(rawEvent)) {
4603
+ return;
4604
+ }
4605
+ // we're processing a full snapshot, so we should reset the timer
4606
+ if (rawEvent.type === EventType.FullSnapshot) {
4607
+ this._scheduleFullSnapshot();
4608
+ // Full snapshots reset rrweb's node IDs, so clear any logged node tracking
4609
+ this._mutationThrottler?.reset();
4610
+ }
4611
+ // Clear the buffer if waiting for a trigger and only keep data from after the current full snapshot
4612
+ // we always start trigger pending so need to wait for flags before we know if we're really pending
4613
+ if (rawEvent.type === EventType.FullSnapshot && this._triggerMatching.triggerStatus(this.sessionId) === TRIGGER_PENDING) {
4614
+ this._clearBufferBeforeMostRecentMeta();
4615
+ }
4616
+ const throttledEvent = this._mutationThrottler ? this._mutationThrottler.throttleMutations(rawEvent) : rawEvent;
4617
+ if (!throttledEvent) {
4618
+ return;
4619
+ }
4620
+ // TODO: Re-add ensureMaxMessageSize once we are confident in it
4621
+ const event = truncateLargeConsoleLogs(throttledEvent);
4622
+ this._updateWindowAndSessionIds(event);
4623
+ // When in an idle state we keep recording but don't capture the events,
4624
+ // we don't want to return early if idle is 'unknown'
4625
+ if (this._isIdle === true && !isSessionIdleEvent(event)) {
4626
+ return;
4627
+ }
4628
+ if (isSessionIdleEvent(event)) {
4629
+ // session idle events have a timestamp when rrweb sees them
4630
+ // which can artificially lengthen a session
4631
+ // we know when we detected it based on the payload and can correct the timestamp
4632
+ const payload = event.data.payload;
4633
+ if (payload) {
4634
+ const lastActivity = payload.lastActivityTimestamp;
4635
+ const threshold = payload.threshold;
4636
+ event.timestamp = lastActivity + threshold;
4637
+ }
4638
+ }
4639
+ const eventToSend = this._instance.config.session_recording?.compress_events ?? true ? compressEvent(event) : event;
4640
+ const size = estimateSize(eventToSend);
4641
+ const properties = {
4642
+ $snapshot_bytes: size,
4643
+ $snapshot_data: eventToSend,
4644
+ $session_id: this._sessionId,
4645
+ $window_id: this._windowId
4646
+ };
4647
+ if (this.status === DISABLED) {
4648
+ this._clearBuffer();
4649
+ return;
4650
+ }
4651
+ this._captureSnapshotBuffered(properties);
4652
+ } catch (e) {
4653
+ logger.error('error processing rrweb event', e);
4588
4654
  }
4589
- const eventToSend = this._instance.config.session_recording?.compress_events ?? true ? compressEvent(event) : event;
4590
- const size = estimateSize(eventToSend);
4591
- const properties = {
4592
- $snapshot_bytes: size,
4593
- $snapshot_data: eventToSend,
4594
- $session_id: this._sessionId,
4595
- $window_id: this._windowId
4596
- };
4597
- if (this.status === DISABLED) {
4598
- this._clearBuffer();
4599
- return;
4600
- }
4601
- this._captureSnapshotBuffered(properties);
4602
4655
  }
4603
4656
  get status() {
4604
4657
  return this._statusMatcher({
@@ -4676,6 +4729,16 @@ class LazyLoadedSessionRecording {
4676
4729
  }, RECORDING_BUFFER_TIMEOUT);
4677
4730
  return this._buffer;
4678
4731
  }
4732
+ if (!this._canCaptureSnapshots()) {
4733
+ if (!this._loggedMissingEndpointFor) {
4734
+ this._loggedMissingEndpointFor = true;
4735
+ logger.warn('snapshot capture skipped because requestRouter.endpointFor is unavailable');
4736
+ }
4737
+ this._flushBufferTimer = setTimeout(() => {
4738
+ this._flushBuffer();
4739
+ }, RECORDING_BUFFER_TIMEOUT);
4740
+ return this._buffer;
4741
+ }
4679
4742
  if (this._buffer.data.length > 0) {
4680
4743
  const snapshotEvents = splitBuffer(this._buffer);
4681
4744
  snapshotEvents.forEach(snapshotBuffer => {
@@ -4708,13 +4771,25 @@ class LazyLoadedSessionRecording {
4708
4771
  }
4709
4772
  }
4710
4773
  _captureSnapshot(properties) {
4711
- // :TRICKY: Make sure we batch these requests, use a custom endpoint and don't truncate the strings.
4712
- this._instance.capture('$snapshot', properties, {
4713
- _url: this._instance.requestRouter.endpointFor('api', this._endpoint),
4714
- _noTruncate: true,
4715
- _batchKey: SESSION_RECORDING_BATCH_KEY,
4716
- skip_client_rate_limiting: true
4717
- });
4774
+ const url = this._snapshotIngestionUrl();
4775
+ if (!url) {
4776
+ if (!this._loggedMissingEndpointFor) {
4777
+ this._loggedMissingEndpointFor = true;
4778
+ logger.warn('snapshot capture skipped because requestRouter.endpointFor is unavailable');
4779
+ }
4780
+ return;
4781
+ }
4782
+ try {
4783
+ // :TRICKY: Make sure we batch these requests, use a custom endpoint and don't truncate the strings.
4784
+ this._instance.capture('$snapshot', properties, {
4785
+ _url: url,
4786
+ _noTruncate: true,
4787
+ _batchKey: SESSION_RECORDING_BATCH_KEY,
4788
+ skip_client_rate_limiting: true
4789
+ });
4790
+ } catch (e) {
4791
+ logger.error('failed to capture snapshot', e);
4792
+ }
4718
4793
  }
4719
4794
  _snapshotUrl() {
4720
4795
  const host = this._instance.config.host || '';
@@ -4987,7 +5062,11 @@ class LazyLoadedSessionRecording {
4987
5062
  try {
4988
5063
  this._stopRrweb = rrwebRecord({
4989
5064
  emit: event => {
4990
- this.onRRwebEmit(event);
5065
+ try {
5066
+ this.onRRwebEmit(event);
5067
+ } catch (e) {
5068
+ logger.error('error in rrweb emit handler', e);
5069
+ }
4991
5070
  },
4992
5071
  plugins: activePlugins,
4993
5072
  ...sessionRecordingOptions