@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/leanbase.iife.js
CHANGED
|
@@ -2846,7 +2846,7 @@ var leanbase = (function () {
|
|
|
2846
2846
|
}
|
|
2847
2847
|
};
|
|
2848
2848
|
|
|
2849
|
-
var version = "0.2.
|
|
2849
|
+
var version = "0.2.4";
|
|
2850
2850
|
var packageInfo = {
|
|
2851
2851
|
version: version};
|
|
2852
2852
|
|
|
@@ -6106,34 +6106,49 @@ var leanbase = (function () {
|
|
|
6106
6106
|
}
|
|
6107
6107
|
return [id, node];
|
|
6108
6108
|
};
|
|
6109
|
-
this._getNode = id =>
|
|
6109
|
+
this._getNode = id => {
|
|
6110
|
+
// eslint-disable-next-line posthog-js/no-direct-undefined-check, posthog-js/no-direct-null-check
|
|
6111
|
+
if (id === null || id === undefined) {
|
|
6112
|
+
return null;
|
|
6113
|
+
}
|
|
6114
|
+
try {
|
|
6115
|
+
return this._rrweb.mirror.getNode(id) ?? null;
|
|
6116
|
+
} catch {
|
|
6117
|
+
return null;
|
|
6118
|
+
}
|
|
6119
|
+
};
|
|
6110
6120
|
this._numberOfChanges = data => {
|
|
6111
6121
|
return (data.removes?.length ?? 0) + (data.attributes?.length ?? 0) + (data.texts?.length ?? 0) + (data.adds?.length ?? 0);
|
|
6112
6122
|
};
|
|
6113
6123
|
this.throttleMutations = event => {
|
|
6114
|
-
|
|
6124
|
+
try {
|
|
6125
|
+
if (event.type !== INCREMENTAL_SNAPSHOT_EVENT_TYPE || event.data.source !== MUTATION_SOURCE_TYPE) {
|
|
6126
|
+
return event;
|
|
6127
|
+
}
|
|
6128
|
+
const data = event.data;
|
|
6129
|
+
const initialMutationCount = this._numberOfChanges(data);
|
|
6130
|
+
if (data.attributes) {
|
|
6131
|
+
// Most problematic mutations come from attrs where the style or minor properties are changed rapidly
|
|
6132
|
+
data.attributes = data.attributes.filter(attr => {
|
|
6133
|
+
const id = attr?.id;
|
|
6134
|
+
if (typeof id !== 'number') {
|
|
6135
|
+
return true;
|
|
6136
|
+
}
|
|
6137
|
+
const [nodeId] = this._getNodeOrRelevantParent(id);
|
|
6138
|
+
const isRateLimited = this._rateLimiter.consumeRateLimit(nodeId);
|
|
6139
|
+
return !isRateLimited;
|
|
6140
|
+
});
|
|
6141
|
+
}
|
|
6142
|
+
// Check if every part of the mutation is empty in which case there is nothing to do
|
|
6143
|
+
const mutationCount = this._numberOfChanges(data);
|
|
6144
|
+
if (mutationCount === 0 && initialMutationCount !== mutationCount) {
|
|
6145
|
+
// If we have modified the mutation count and the remaining count is 0, then we don't need the event.
|
|
6146
|
+
return;
|
|
6147
|
+
}
|
|
6148
|
+
return event;
|
|
6149
|
+
} catch {
|
|
6115
6150
|
return event;
|
|
6116
6151
|
}
|
|
6117
|
-
const data = event.data;
|
|
6118
|
-
const initialMutationCount = this._numberOfChanges(data);
|
|
6119
|
-
if (data.attributes) {
|
|
6120
|
-
// Most problematic mutations come from attrs where the style or minor properties are changed rapidly
|
|
6121
|
-
data.attributes = data.attributes.filter(attr => {
|
|
6122
|
-
const [nodeId] = this._getNodeOrRelevantParent(attr.id);
|
|
6123
|
-
const isRateLimited = this._rateLimiter.consumeRateLimit(nodeId);
|
|
6124
|
-
if (isRateLimited) {
|
|
6125
|
-
return false;
|
|
6126
|
-
}
|
|
6127
|
-
return attr;
|
|
6128
|
-
});
|
|
6129
|
-
}
|
|
6130
|
-
// Check if every part of the mutation is empty in which case there is nothing to do
|
|
6131
|
-
const mutationCount = this._numberOfChanges(data);
|
|
6132
|
-
if (mutationCount === 0 && initialMutationCount !== mutationCount) {
|
|
6133
|
-
// If we have modified the mutation count and the remaining count is 0, then we don't need the event.
|
|
6134
|
-
return;
|
|
6135
|
-
}
|
|
6136
|
-
return event;
|
|
6137
6152
|
};
|
|
6138
6153
|
const configuredBucketSize = this._options.bucketSize ?? 100;
|
|
6139
6154
|
const effectiveBucketSize = Math.max(configuredBucketSize - 1, 1);
|
|
@@ -6371,6 +6386,9 @@ var leanbase = (function () {
|
|
|
6371
6386
|
*/
|
|
6372
6387
|
this._queuedRRWebEvents = [];
|
|
6373
6388
|
this._isIdle = 'unknown';
|
|
6389
|
+
// Replay should not process rrweb emits until all critical dependencies are ready.
|
|
6390
|
+
this._isFullyReady = false;
|
|
6391
|
+
this._loggedMissingEndpointFor = false;
|
|
6374
6392
|
// we need to be able to check the state of the event and url triggers separately
|
|
6375
6393
|
// as we make some decisions based on them without referencing LinkedFlag etc
|
|
6376
6394
|
this._triggerMatching = new PendingTriggerMatching();
|
|
@@ -6551,7 +6569,11 @@ var leanbase = (function () {
|
|
|
6551
6569
|
}
|
|
6552
6570
|
}
|
|
6553
6571
|
_tryAddCustomEvent(tag, payload) {
|
|
6554
|
-
|
|
6572
|
+
const rrwebRecord = getRRWebRecord();
|
|
6573
|
+
if (!rrwebRecord || typeof rrwebRecord.addCustomEvent !== 'function') {
|
|
6574
|
+
return false;
|
|
6575
|
+
}
|
|
6576
|
+
return this._tryRRWebMethod(newQueuedEvent(() => rrwebRecord.addCustomEvent(tag, payload)));
|
|
6555
6577
|
}
|
|
6556
6578
|
_pageViewFallBack() {
|
|
6557
6579
|
try {
|
|
@@ -6597,7 +6619,11 @@ var leanbase = (function () {
|
|
|
6597
6619
|
}
|
|
6598
6620
|
}
|
|
6599
6621
|
_tryTakeFullSnapshot() {
|
|
6600
|
-
|
|
6622
|
+
const rrwebRecord = getRRWebRecord();
|
|
6623
|
+
if (!rrwebRecord || typeof rrwebRecord.takeFullSnapshot !== 'function') {
|
|
6624
|
+
return false;
|
|
6625
|
+
}
|
|
6626
|
+
return this._tryRRWebMethod(newQueuedEvent(() => rrwebRecord.takeFullSnapshot()));
|
|
6601
6627
|
}
|
|
6602
6628
|
get _fullSnapshotIntervalMillis() {
|
|
6603
6629
|
if (this._triggerMatching.triggerStatus(this.sessionId) === TRIGGER_PENDING && !['sampled', 'active'].includes(this.status)) {
|
|
@@ -6673,6 +6699,7 @@ var leanbase = (function () {
|
|
|
6673
6699
|
return parsedConfig;
|
|
6674
6700
|
}
|
|
6675
6701
|
async start(startReason) {
|
|
6702
|
+
this._isFullyReady = false;
|
|
6676
6703
|
const config = this._remoteConfig;
|
|
6677
6704
|
if (!config) {
|
|
6678
6705
|
logger.info('remote config must be stored in persistence before recording can start');
|
|
@@ -6712,6 +6739,9 @@ var leanbase = (function () {
|
|
|
6712
6739
|
if (!this.isStarted) {
|
|
6713
6740
|
return;
|
|
6714
6741
|
}
|
|
6742
|
+
// Only start processing rrweb emits once the ingestion endpoint is available.
|
|
6743
|
+
// If it isn't available, we must degrade to a no-op (never crash the host app).
|
|
6744
|
+
this._isFullyReady = this._canCaptureSnapshots();
|
|
6715
6745
|
// calling addEventListener multiple times is safe and will not add duplicates
|
|
6716
6746
|
addEventListener(win, 'beforeunload', this._onBeforeUnload);
|
|
6717
6747
|
addEventListener(win, 'offline', this._onOffline);
|
|
@@ -6789,77 +6819,100 @@ var leanbase = (function () {
|
|
|
6789
6819
|
this._queuedRRWebEvents = [];
|
|
6790
6820
|
this._stopRrweb?.();
|
|
6791
6821
|
this._stopRrweb = undefined;
|
|
6822
|
+
this._isFullyReady = false;
|
|
6792
6823
|
logger.info('stopped');
|
|
6793
6824
|
}
|
|
6825
|
+
_snapshotIngestionUrl() {
|
|
6826
|
+
const endpointFor = this._instance?.requestRouter?.endpointFor;
|
|
6827
|
+
if (typeof endpointFor !== 'function') {
|
|
6828
|
+
return null;
|
|
6829
|
+
}
|
|
6830
|
+
try {
|
|
6831
|
+
return endpointFor('api', this._endpoint);
|
|
6832
|
+
} catch {
|
|
6833
|
+
return null;
|
|
6834
|
+
}
|
|
6835
|
+
}
|
|
6836
|
+
_canCaptureSnapshots() {
|
|
6837
|
+
return !!this._snapshotIngestionUrl();
|
|
6838
|
+
}
|
|
6794
6839
|
onRRwebEmit(rawEvent) {
|
|
6795
|
-
|
|
6796
|
-
if (!
|
|
6840
|
+
// Never process rrweb emits until we're fully ready.
|
|
6841
|
+
if (!this._isFullyReady || !this.isStarted) {
|
|
6797
6842
|
return;
|
|
6798
6843
|
}
|
|
6799
|
-
|
|
6800
|
-
|
|
6801
|
-
|
|
6802
|
-
if (!href) {
|
|
6844
|
+
try {
|
|
6845
|
+
this._processQueuedEvents();
|
|
6846
|
+
if (!rawEvent || !isObject(rawEvent)) {
|
|
6803
6847
|
return;
|
|
6804
6848
|
}
|
|
6805
|
-
rawEvent.
|
|
6806
|
-
|
|
6807
|
-
|
|
6808
|
-
|
|
6809
|
-
|
|
6810
|
-
|
|
6811
|
-
|
|
6812
|
-
|
|
6813
|
-
|
|
6814
|
-
|
|
6815
|
-
|
|
6816
|
-
|
|
6817
|
-
|
|
6818
|
-
|
|
6819
|
-
|
|
6820
|
-
|
|
6821
|
-
|
|
6822
|
-
|
|
6823
|
-
|
|
6824
|
-
|
|
6825
|
-
|
|
6826
|
-
|
|
6827
|
-
|
|
6828
|
-
|
|
6829
|
-
|
|
6830
|
-
|
|
6831
|
-
|
|
6832
|
-
|
|
6833
|
-
|
|
6834
|
-
|
|
6835
|
-
|
|
6836
|
-
|
|
6837
|
-
|
|
6838
|
-
|
|
6839
|
-
|
|
6840
|
-
//
|
|
6841
|
-
//
|
|
6842
|
-
|
|
6843
|
-
|
|
6844
|
-
|
|
6845
|
-
|
|
6846
|
-
|
|
6847
|
-
|
|
6848
|
-
|
|
6849
|
-
|
|
6850
|
-
|
|
6851
|
-
|
|
6852
|
-
|
|
6853
|
-
|
|
6854
|
-
|
|
6855
|
-
|
|
6856
|
-
|
|
6857
|
-
|
|
6858
|
-
|
|
6859
|
-
|
|
6860
|
-
|
|
6849
|
+
if (rawEvent.type === EventType$1.Meta) {
|
|
6850
|
+
const href = this._maskUrl(rawEvent.data.href);
|
|
6851
|
+
this._lastHref = href;
|
|
6852
|
+
if (!href) {
|
|
6853
|
+
return;
|
|
6854
|
+
}
|
|
6855
|
+
rawEvent.data.href = href;
|
|
6856
|
+
} else {
|
|
6857
|
+
this._pageViewFallBack();
|
|
6858
|
+
}
|
|
6859
|
+
// Check if the URL matches any trigger patterns
|
|
6860
|
+
this._urlTriggerMatching.checkUrlTriggerConditions(() => this._pauseRecording(), () => this._resumeRecording(), triggerType => this._activateTrigger(triggerType));
|
|
6861
|
+
// always have to check if the URL is blocked really early,
|
|
6862
|
+
// or you risk getting stuck in a loop
|
|
6863
|
+
if (this._urlTriggerMatching.urlBlocked && !isRecordingPausedEvent(rawEvent)) {
|
|
6864
|
+
return;
|
|
6865
|
+
}
|
|
6866
|
+
// we're processing a full snapshot, so we should reset the timer
|
|
6867
|
+
if (rawEvent.type === EventType$1.FullSnapshot) {
|
|
6868
|
+
this._scheduleFullSnapshot();
|
|
6869
|
+
// Full snapshots reset rrweb's node IDs, so clear any logged node tracking
|
|
6870
|
+
this._mutationThrottler?.reset();
|
|
6871
|
+
}
|
|
6872
|
+
// Clear the buffer if waiting for a trigger and only keep data from after the current full snapshot
|
|
6873
|
+
// we always start trigger pending so need to wait for flags before we know if we're really pending
|
|
6874
|
+
if (rawEvent.type === EventType$1.FullSnapshot && this._triggerMatching.triggerStatus(this.sessionId) === TRIGGER_PENDING) {
|
|
6875
|
+
this._clearBufferBeforeMostRecentMeta();
|
|
6876
|
+
}
|
|
6877
|
+
const throttledEvent = this._mutationThrottler ? this._mutationThrottler.throttleMutations(rawEvent) : rawEvent;
|
|
6878
|
+
if (!throttledEvent) {
|
|
6879
|
+
return;
|
|
6880
|
+
}
|
|
6881
|
+
// TODO: Re-add ensureMaxMessageSize once we are confident in it
|
|
6882
|
+
const event = truncateLargeConsoleLogs(throttledEvent);
|
|
6883
|
+
this._updateWindowAndSessionIds(event);
|
|
6884
|
+
// When in an idle state we keep recording but don't capture the events,
|
|
6885
|
+
// we don't want to return early if idle is 'unknown'
|
|
6886
|
+
if (this._isIdle === true && !isSessionIdleEvent(event)) {
|
|
6887
|
+
return;
|
|
6888
|
+
}
|
|
6889
|
+
if (isSessionIdleEvent(event)) {
|
|
6890
|
+
// session idle events have a timestamp when rrweb sees them
|
|
6891
|
+
// which can artificially lengthen a session
|
|
6892
|
+
// we know when we detected it based on the payload and can correct the timestamp
|
|
6893
|
+
const payload = event.data.payload;
|
|
6894
|
+
if (payload) {
|
|
6895
|
+
const lastActivity = payload.lastActivityTimestamp;
|
|
6896
|
+
const threshold = payload.threshold;
|
|
6897
|
+
event.timestamp = lastActivity + threshold;
|
|
6898
|
+
}
|
|
6899
|
+
}
|
|
6900
|
+
const eventToSend = this._instance.config.session_recording?.compress_events ?? true ? compressEvent(event) : event;
|
|
6901
|
+
const size = estimateSize(eventToSend);
|
|
6902
|
+
const properties = {
|
|
6903
|
+
$snapshot_bytes: size,
|
|
6904
|
+
$snapshot_data: eventToSend,
|
|
6905
|
+
$session_id: this._sessionId,
|
|
6906
|
+
$window_id: this._windowId
|
|
6907
|
+
};
|
|
6908
|
+
if (this.status === DISABLED) {
|
|
6909
|
+
this._clearBuffer();
|
|
6910
|
+
return;
|
|
6911
|
+
}
|
|
6912
|
+
this._captureSnapshotBuffered(properties);
|
|
6913
|
+
} catch (e) {
|
|
6914
|
+
logger.error('error processing rrweb event', e);
|
|
6861
6915
|
}
|
|
6862
|
-
this._captureSnapshotBuffered(properties);
|
|
6863
6916
|
}
|
|
6864
6917
|
get status() {
|
|
6865
6918
|
return this._statusMatcher({
|
|
@@ -6937,6 +6990,16 @@ var leanbase = (function () {
|
|
|
6937
6990
|
}, RECORDING_BUFFER_TIMEOUT);
|
|
6938
6991
|
return this._buffer;
|
|
6939
6992
|
}
|
|
6993
|
+
if (!this._canCaptureSnapshots()) {
|
|
6994
|
+
if (!this._loggedMissingEndpointFor) {
|
|
6995
|
+
this._loggedMissingEndpointFor = true;
|
|
6996
|
+
logger.warn('snapshot capture skipped because requestRouter.endpointFor is unavailable');
|
|
6997
|
+
}
|
|
6998
|
+
this._flushBufferTimer = setTimeout(() => {
|
|
6999
|
+
this._flushBuffer();
|
|
7000
|
+
}, RECORDING_BUFFER_TIMEOUT);
|
|
7001
|
+
return this._buffer;
|
|
7002
|
+
}
|
|
6940
7003
|
if (this._buffer.data.length > 0) {
|
|
6941
7004
|
const snapshotEvents = splitBuffer(this._buffer);
|
|
6942
7005
|
snapshotEvents.forEach(snapshotBuffer => {
|
|
@@ -6969,13 +7032,25 @@ var leanbase = (function () {
|
|
|
6969
7032
|
}
|
|
6970
7033
|
}
|
|
6971
7034
|
_captureSnapshot(properties) {
|
|
6972
|
-
|
|
6973
|
-
|
|
6974
|
-
|
|
6975
|
-
|
|
6976
|
-
|
|
6977
|
-
|
|
6978
|
-
|
|
7035
|
+
const url = this._snapshotIngestionUrl();
|
|
7036
|
+
if (!url) {
|
|
7037
|
+
if (!this._loggedMissingEndpointFor) {
|
|
7038
|
+
this._loggedMissingEndpointFor = true;
|
|
7039
|
+
logger.warn('snapshot capture skipped because requestRouter.endpointFor is unavailable');
|
|
7040
|
+
}
|
|
7041
|
+
return;
|
|
7042
|
+
}
|
|
7043
|
+
try {
|
|
7044
|
+
// :TRICKY: Make sure we batch these requests, use a custom endpoint and don't truncate the strings.
|
|
7045
|
+
this._instance.capture('$snapshot', properties, {
|
|
7046
|
+
_url: url,
|
|
7047
|
+
_noTruncate: true,
|
|
7048
|
+
_batchKey: SESSION_RECORDING_BATCH_KEY,
|
|
7049
|
+
skip_client_rate_limiting: true
|
|
7050
|
+
});
|
|
7051
|
+
} catch (e) {
|
|
7052
|
+
logger.error('failed to capture snapshot', e);
|
|
7053
|
+
}
|
|
6979
7054
|
}
|
|
6980
7055
|
_snapshotUrl() {
|
|
6981
7056
|
const host = this._instance.config.host || '';
|
|
@@ -7248,7 +7323,11 @@ var leanbase = (function () {
|
|
|
7248
7323
|
try {
|
|
7249
7324
|
this._stopRrweb = rrwebRecord({
|
|
7250
7325
|
emit: event => {
|
|
7251
|
-
|
|
7326
|
+
try {
|
|
7327
|
+
this.onRRwebEmit(event);
|
|
7328
|
+
} catch (e) {
|
|
7329
|
+
logger.error('error in rrweb emit handler', e);
|
|
7330
|
+
}
|
|
7252
7331
|
},
|
|
7253
7332
|
plugins: activePlugins,
|
|
7254
7333
|
...sessionRecordingOptions
|