@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.cjs
CHANGED
|
@@ -1171,7 +1171,7 @@ const detectDeviceType = function (user_agent) {
|
|
|
1171
1171
|
}
|
|
1172
1172
|
};
|
|
1173
1173
|
|
|
1174
|
-
var version = "0.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 =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4537
|
-
if (!
|
|
4581
|
+
// Never process rrweb emits until we're fully ready.
|
|
4582
|
+
if (!this._isFullyReady || !this.isStarted) {
|
|
4538
4583
|
return;
|
|
4539
4584
|
}
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
if (!href) {
|
|
4585
|
+
try {
|
|
4586
|
+
this._processQueuedEvents();
|
|
4587
|
+
if (!rawEvent || !core.isObject(rawEvent)) {
|
|
4544
4588
|
return;
|
|
4545
4589
|
}
|
|
4546
|
-
rawEvent.
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
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
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
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
|
-
|
|
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
|