@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.cjs CHANGED
@@ -1171,7 +1171,7 @@ const detectDeviceType = function (user_agent) {
1171
1171
  }
1172
1172
  };
1173
1173
 
1174
- var version = "0.2.3";
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');
@@ -4453,6 +4480,9 @@ class LazyLoadedSessionRecording {
4453
4480
  if (!this.isStarted) {
4454
4481
  return;
4455
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();
4456
4486
  // calling addEventListener multiple times is safe and will not add duplicates
4457
4487
  addEventListener(win, 'beforeunload', this._onBeforeUnload);
4458
4488
  addEventListener(win, 'offline', this._onOffline);
@@ -4530,77 +4560,100 @@ class LazyLoadedSessionRecording {
4530
4560
  this._queuedRRWebEvents = [];
4531
4561
  this._stopRrweb?.();
4532
4562
  this._stopRrweb = undefined;
4563
+ this._isFullyReady = false;
4533
4564
  logger.info('stopped');
4534
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
+ }
4535
4580
  onRRwebEmit(rawEvent) {
4536
- this._processQueuedEvents();
4537
- if (!rawEvent || !core.isObject(rawEvent)) {
4581
+ // Never process rrweb emits until we're fully ready.
4582
+ if (!this._isFullyReady || !this.isStarted) {
4538
4583
  return;
4539
4584
  }
4540
- if (rawEvent.type === EventType.Meta) {
4541
- const href = this._maskUrl(rawEvent.data.href);
4542
- this._lastHref = href;
4543
- if (!href) {
4585
+ try {
4586
+ this._processQueuedEvents();
4587
+ if (!rawEvent || !core.isObject(rawEvent)) {
4544
4588
  return;
4545
4589
  }
4546
- rawEvent.data.href = href;
4547
- } else {
4548
- this._pageViewFallBack();
4549
- }
4550
- // Check if the URL matches any trigger patterns
4551
- this._urlTriggerMatching.checkUrlTriggerConditions(() => this._pauseRecording(), () => this._resumeRecording(), triggerType => this._activateTrigger(triggerType));
4552
- // always have to check if the URL is blocked really early,
4553
- // or you risk getting stuck in a loop
4554
- if (this._urlTriggerMatching.urlBlocked && !isRecordingPausedEvent(rawEvent)) {
4555
- return;
4556
- }
4557
- // we're processing a full snapshot, so we should reset the timer
4558
- if (rawEvent.type === EventType.FullSnapshot) {
4559
- this._scheduleFullSnapshot();
4560
- // Full snapshots reset rrweb's node IDs, so clear any logged node tracking
4561
- this._mutationThrottler?.reset();
4562
- }
4563
- // Clear the buffer if waiting for a trigger and only keep data from after the current full snapshot
4564
- // we always start trigger pending so need to wait for flags before we know if we're really pending
4565
- if (rawEvent.type === EventType.FullSnapshot && this._triggerMatching.triggerStatus(this.sessionId) === TRIGGER_PENDING) {
4566
- this._clearBufferBeforeMostRecentMeta();
4567
- }
4568
- const throttledEvent = this._mutationThrottler ? this._mutationThrottler.throttleMutations(rawEvent) : rawEvent;
4569
- if (!throttledEvent) {
4570
- return;
4571
- }
4572
- // TODO: Re-add ensureMaxMessageSize once we are confident in it
4573
- const event = truncateLargeConsoleLogs(throttledEvent);
4574
- this._updateWindowAndSessionIds(event);
4575
- // When in an idle state we keep recording but don't capture the events,
4576
- // we don't want to return early if idle is 'unknown'
4577
- if (this._isIdle === true && !isSessionIdleEvent(event)) {
4578
- return;
4579
- }
4580
- if (isSessionIdleEvent(event)) {
4581
- // session idle events have a timestamp when rrweb sees them
4582
- // which can artificially lengthen a session
4583
- // we know when we detected it based on the payload and can correct the timestamp
4584
- const payload = event.data.payload;
4585
- if (payload) {
4586
- const lastActivity = payload.lastActivityTimestamp;
4587
- const threshold = payload.threshold;
4588
- 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();
4589
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);
4590
4656
  }
4591
- const eventToSend = this._instance.config.session_recording?.compress_events ?? true ? compressEvent(event) : event;
4592
- const size = estimateSize(eventToSend);
4593
- const properties = {
4594
- $snapshot_bytes: size,
4595
- $snapshot_data: eventToSend,
4596
- $session_id: this._sessionId,
4597
- $window_id: this._windowId
4598
- };
4599
- if (this.status === DISABLED) {
4600
- this._clearBuffer();
4601
- return;
4602
- }
4603
- this._captureSnapshotBuffered(properties);
4604
4657
  }
4605
4658
  get status() {
4606
4659
  return this._statusMatcher({
@@ -4678,6 +4731,16 @@ class LazyLoadedSessionRecording {
4678
4731
  }, RECORDING_BUFFER_TIMEOUT);
4679
4732
  return this._buffer;
4680
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
+ }
4681
4744
  if (this._buffer.data.length > 0) {
4682
4745
  const snapshotEvents = splitBuffer(this._buffer);
4683
4746
  snapshotEvents.forEach(snapshotBuffer => {
@@ -4710,13 +4773,25 @@ class LazyLoadedSessionRecording {
4710
4773
  }
4711
4774
  }
4712
4775
  _captureSnapshot(properties) {
4713
- // :TRICKY: Make sure we batch these requests, use a custom endpoint and don't truncate the strings.
4714
- this._instance.capture('$snapshot', properties, {
4715
- _url: this._instance.requestRouter.endpointFor('api', this._endpoint),
4716
- _noTruncate: true,
4717
- _batchKey: SESSION_RECORDING_BATCH_KEY,
4718
- skip_client_rate_limiting: true
4719
- });
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
+ }
4720
4795
  }
4721
4796
  _snapshotUrl() {
4722
4797
  const host = this._instance.config.host || '';
@@ -4989,7 +5064,11 @@ class LazyLoadedSessionRecording {
4989
5064
  try {
4990
5065
  this._stopRrweb = rrwebRecord({
4991
5066
  emit: event => {
4992
- this.onRRwebEmit(event);
5067
+ try {
5068
+ this.onRRwebEmit(event);
5069
+ } catch (e) {
5070
+ logger.error('error in rrweb emit handler', e);
5071
+ }
4993
5072
  },
4994
5073
  plugins: activePlugins,
4995
5074
  ...sessionRecordingOptions