@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 +174 -95
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +174 -95
- package/dist/index.mjs.map +1 -1
- package/dist/leanbase.iife.js +175 -96
- package/dist/leanbase.iife.js.map +1 -1
- package/package.json +1 -1
- package/src/extensions/replay/external/lazy-loaded-session-recorder.ts +158 -82
- package/src/extensions/replay/external/mutation-throttler.ts +40 -27
- package/src/version.ts +1 -1
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.
|
|
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 =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4535
|
-
if (!
|
|
4579
|
+
// Never process rrweb emits until we're fully ready.
|
|
4580
|
+
if (!this._isFullyReady || !this.isStarted) {
|
|
4536
4581
|
return;
|
|
4537
4582
|
}
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
if (!href) {
|
|
4583
|
+
try {
|
|
4584
|
+
this._processQueuedEvents();
|
|
4585
|
+
if (!rawEvent || !isObject(rawEvent)) {
|
|
4542
4586
|
return;
|
|
4543
4587
|
}
|
|
4544
|
-
rawEvent.
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
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
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
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
|
-
|
|
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
|