@leanbase-giangnd/js 0.2.2 → 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 +272 -116
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.mjs +272 -116
- package/dist/index.mjs.map +1 -1
- package/dist/leanbase.iife.js +273 -117
- package/dist/leanbase.iife.js.map +1 -1
- package/package.json +47 -46
- package/src/extensions/replay/external/lazy-loaded-session-recorder.ts +176 -88
- package/src/extensions/replay/external/mutation-throttler.ts +40 -27
- package/src/extensions/replay/session-recording.ts +19 -12
- package/src/leanbase.ts +68 -6
- package/src/version.ts +1 -1
- package/LICENSE +0 -37
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');
|
|
@@ -4448,6 +4475,14 @@ class LazyLoadedSessionRecording {
|
|
|
4448
4475
|
});
|
|
4449
4476
|
this._makeSamplingDecision(this.sessionId);
|
|
4450
4477
|
await this._startRecorder();
|
|
4478
|
+
// If rrweb failed to load/start, do not proceed further.
|
|
4479
|
+
// This prevents installing listeners that assume rrweb is active.
|
|
4480
|
+
if (!this.isStarted) {
|
|
4481
|
+
return;
|
|
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();
|
|
4451
4486
|
// calling addEventListener multiple times is safe and will not add duplicates
|
|
4452
4487
|
addEventListener(win, 'beforeunload', this._onBeforeUnload);
|
|
4453
4488
|
addEventListener(win, 'offline', this._onOffline);
|
|
@@ -4525,77 +4560,100 @@ class LazyLoadedSessionRecording {
|
|
|
4525
4560
|
this._queuedRRWebEvents = [];
|
|
4526
4561
|
this._stopRrweb?.();
|
|
4527
4562
|
this._stopRrweb = undefined;
|
|
4563
|
+
this._isFullyReady = false;
|
|
4528
4564
|
logger.info('stopped');
|
|
4529
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
|
+
}
|
|
4530
4580
|
onRRwebEmit(rawEvent) {
|
|
4531
|
-
|
|
4532
|
-
if (!
|
|
4581
|
+
// Never process rrweb emits until we're fully ready.
|
|
4582
|
+
if (!this._isFullyReady || !this.isStarted) {
|
|
4533
4583
|
return;
|
|
4534
4584
|
}
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
if (!href) {
|
|
4585
|
+
try {
|
|
4586
|
+
this._processQueuedEvents();
|
|
4587
|
+
if (!rawEvent || !core.isObject(rawEvent)) {
|
|
4539
4588
|
return;
|
|
4540
4589
|
}
|
|
4541
|
-
rawEvent.
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
return;
|
|
4551
|
-
}
|
|
4552
|
-
// we're processing a full snapshot, so we should reset the timer
|
|
4553
|
-
if (rawEvent.type === EventType.FullSnapshot) {
|
|
4554
|
-
this._scheduleFullSnapshot();
|
|
4555
|
-
// Full snapshots reset rrweb's node IDs, so clear any logged node tracking
|
|
4556
|
-
this._mutationThrottler?.reset();
|
|
4557
|
-
}
|
|
4558
|
-
// Clear the buffer if waiting for a trigger and only keep data from after the current full snapshot
|
|
4559
|
-
// we always start trigger pending so need to wait for flags before we know if we're really pending
|
|
4560
|
-
if (rawEvent.type === EventType.FullSnapshot && this._triggerMatching.triggerStatus(this.sessionId) === TRIGGER_PENDING) {
|
|
4561
|
-
this._clearBufferBeforeMostRecentMeta();
|
|
4562
|
-
}
|
|
4563
|
-
const throttledEvent = this._mutationThrottler ? this._mutationThrottler.throttleMutations(rawEvent) : rawEvent;
|
|
4564
|
-
if (!throttledEvent) {
|
|
4565
|
-
return;
|
|
4566
|
-
}
|
|
4567
|
-
// TODO: Re-add ensureMaxMessageSize once we are confident in it
|
|
4568
|
-
const event = truncateLargeConsoleLogs(throttledEvent);
|
|
4569
|
-
this._updateWindowAndSessionIds(event);
|
|
4570
|
-
// When in an idle state we keep recording but don't capture the events,
|
|
4571
|
-
// we don't want to return early if idle is 'unknown'
|
|
4572
|
-
if (this._isIdle === true && !isSessionIdleEvent(event)) {
|
|
4573
|
-
return;
|
|
4574
|
-
}
|
|
4575
|
-
if (isSessionIdleEvent(event)) {
|
|
4576
|
-
// session idle events have a timestamp when rrweb sees them
|
|
4577
|
-
// which can artificially lengthen a session
|
|
4578
|
-
// we know when we detected it based on the payload and can correct the timestamp
|
|
4579
|
-
const payload = event.data.payload;
|
|
4580
|
-
if (payload) {
|
|
4581
|
-
const lastActivity = payload.lastActivityTimestamp;
|
|
4582
|
-
const threshold = payload.threshold;
|
|
4583
|
-
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();
|
|
4584
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);
|
|
4585
4656
|
}
|
|
4586
|
-
const eventToSend = this._instance.config.session_recording?.compress_events ?? true ? compressEvent(event) : event;
|
|
4587
|
-
const size = estimateSize(eventToSend);
|
|
4588
|
-
const properties = {
|
|
4589
|
-
$snapshot_bytes: size,
|
|
4590
|
-
$snapshot_data: eventToSend,
|
|
4591
|
-
$session_id: this._sessionId,
|
|
4592
|
-
$window_id: this._windowId
|
|
4593
|
-
};
|
|
4594
|
-
if (this.status === DISABLED) {
|
|
4595
|
-
this._clearBuffer();
|
|
4596
|
-
return;
|
|
4597
|
-
}
|
|
4598
|
-
this._captureSnapshotBuffered(properties);
|
|
4599
4657
|
}
|
|
4600
4658
|
get status() {
|
|
4601
4659
|
return this._statusMatcher({
|
|
@@ -4673,6 +4731,16 @@ class LazyLoadedSessionRecording {
|
|
|
4673
4731
|
}, RECORDING_BUFFER_TIMEOUT);
|
|
4674
4732
|
return this._buffer;
|
|
4675
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
|
+
}
|
|
4676
4744
|
if (this._buffer.data.length > 0) {
|
|
4677
4745
|
const snapshotEvents = splitBuffer(this._buffer);
|
|
4678
4746
|
snapshotEvents.forEach(snapshotBuffer => {
|
|
@@ -4705,13 +4773,25 @@ class LazyLoadedSessionRecording {
|
|
|
4705
4773
|
}
|
|
4706
4774
|
}
|
|
4707
4775
|
_captureSnapshot(properties) {
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
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
|
+
}
|
|
4715
4795
|
}
|
|
4716
4796
|
_snapshotUrl() {
|
|
4717
4797
|
const host = this._instance.config.host || '';
|
|
@@ -4981,13 +5061,23 @@ class LazyLoadedSessionRecording {
|
|
|
4981
5061
|
}
|
|
4982
5062
|
});
|
|
4983
5063
|
const activePlugins = this._gatherRRWebPlugins();
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
5064
|
+
try {
|
|
5065
|
+
this._stopRrweb = rrwebRecord({
|
|
5066
|
+
emit: event => {
|
|
5067
|
+
try {
|
|
5068
|
+
this.onRRwebEmit(event);
|
|
5069
|
+
} catch (e) {
|
|
5070
|
+
logger.error('error in rrweb emit handler', e);
|
|
5071
|
+
}
|
|
5072
|
+
},
|
|
5073
|
+
plugins: activePlugins,
|
|
5074
|
+
...sessionRecordingOptions
|
|
5075
|
+
});
|
|
5076
|
+
} catch (e) {
|
|
5077
|
+
logger.error('failed to start rrweb recorder', e);
|
|
5078
|
+
this._stopRrweb = undefined;
|
|
5079
|
+
return;
|
|
5080
|
+
}
|
|
4991
5081
|
// We reset the last activity timestamp, resetting the idle timer
|
|
4992
5082
|
this._lastActivityTimestamp = Date.now();
|
|
4993
5083
|
// stay unknown if we're not sure if we're idle or not
|
|
@@ -5173,18 +5263,25 @@ class SessionRecording {
|
|
|
5173
5263
|
// If extensions provide an init function, use it. Otherwise, fall back to the local LazyLoadedSessionRecording
|
|
5174
5264
|
if (assignableWindow.__PosthogExtensions__?.initSessionRecording) {
|
|
5175
5265
|
if (!this._lazyLoadedSessionRecording) {
|
|
5176
|
-
|
|
5177
|
-
|
|
5266
|
+
const maybeRecording = assignableWindow.__PosthogExtensions__?.initSessionRecording(this._instance);
|
|
5267
|
+
if (maybeRecording && typeof maybeRecording.start === 'function') {
|
|
5268
|
+
this._lazyLoadedSessionRecording = maybeRecording;
|
|
5269
|
+
this._lazyLoadedSessionRecording._forceAllowLocalhostNetworkCapture = this._forceAllowLocalhostNetworkCapture;
|
|
5270
|
+
} else {
|
|
5271
|
+
log.warn('initSessionRecording was present but did not return a recorder instance; falling back to local recorder');
|
|
5272
|
+
}
|
|
5178
5273
|
}
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
maybePromise
|
|
5274
|
+
if (this._lazyLoadedSessionRecording) {
|
|
5275
|
+
try {
|
|
5276
|
+
const maybePromise = this._lazyLoadedSessionRecording.start(startReason);
|
|
5277
|
+
if (maybePromise && typeof maybePromise.catch === 'function') {
|
|
5278
|
+
maybePromise.catch(e => logger$2.error('error starting session recording', e));
|
|
5279
|
+
}
|
|
5280
|
+
} catch (e) {
|
|
5281
|
+
logger$2.error('error starting session recording', e);
|
|
5183
5282
|
}
|
|
5184
|
-
|
|
5185
|
-
logger$2.error('error starting session recording', e);
|
|
5283
|
+
return;
|
|
5186
5284
|
}
|
|
5187
|
-
return;
|
|
5188
5285
|
}
|
|
5189
5286
|
if (!this._lazyLoadedSessionRecording) {
|
|
5190
5287
|
this._lazyLoadedSessionRecording = new LazyLoadedSessionRecording(this._instance);
|
|
@@ -5299,6 +5396,10 @@ class Leanbase extends core.PostHogCore {
|
|
|
5299
5396
|
token
|
|
5300
5397
|
});
|
|
5301
5398
|
super(token, mergedConfig);
|
|
5399
|
+
this._remoteConfigLoadAttempted = false;
|
|
5400
|
+
this._remoteConfigResolved = false;
|
|
5401
|
+
this._featureFlagsResolved = false;
|
|
5402
|
+
this._maybeStartedSessionRecording = false;
|
|
5302
5403
|
this.personProcessingSetOncePropertiesSent = false;
|
|
5303
5404
|
this.isLoaded = false;
|
|
5304
5405
|
this.config = mergedConfig;
|
|
@@ -5322,10 +5423,20 @@ class Leanbase extends core.PostHogCore {
|
|
|
5322
5423
|
this.replayAutocapture.startIfEnabled();
|
|
5323
5424
|
if (this.sessionManager && this.config.cookieless_mode !== 'always') {
|
|
5324
5425
|
this.sessionRecording = new SessionRecording(this);
|
|
5325
|
-
this.sessionRecording.startIfEnabledOrStop();
|
|
5326
5426
|
}
|
|
5427
|
+
// Start session recording only once flags + remote config have been resolved.
|
|
5428
|
+
// This matches the PostHog browser SDK where replay activation is driven by remote config and flags.
|
|
5327
5429
|
if (this.config.preloadFeatureFlags !== false) {
|
|
5328
|
-
this.reloadFeatureFlags(
|
|
5430
|
+
this.reloadFeatureFlags({
|
|
5431
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
5432
|
+
cb: _err => {
|
|
5433
|
+
this._featureFlagsResolved = true;
|
|
5434
|
+
this._maybeStartSessionRecording();
|
|
5435
|
+
}
|
|
5436
|
+
});
|
|
5437
|
+
} else {
|
|
5438
|
+
// If feature flags preload is explicitly disabled, treat this requirement as satisfied.
|
|
5439
|
+
this._featureFlagsResolved = true;
|
|
5329
5440
|
}
|
|
5330
5441
|
this.config.loaded?.(this);
|
|
5331
5442
|
if (this.config.capture_pageview) {
|
|
@@ -5335,9 +5446,26 @@ class Leanbase extends core.PostHogCore {
|
|
|
5335
5446
|
}
|
|
5336
5447
|
}, 1);
|
|
5337
5448
|
}
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5449
|
+
const triggerRemoteConfigLoad = reason => {
|
|
5450
|
+
logger$2.info(`remote config load triggered via ${reason}`);
|
|
5451
|
+
void this.loadRemoteConfig();
|
|
5452
|
+
};
|
|
5453
|
+
if (document) {
|
|
5454
|
+
if (document.readyState === 'loading') {
|
|
5455
|
+
logger$2.info('remote config load deferred until DOMContentLoaded');
|
|
5456
|
+
const onDomReady = () => {
|
|
5457
|
+
document?.removeEventListener('DOMContentLoaded', onDomReady);
|
|
5458
|
+
triggerRemoteConfigLoad('dom');
|
|
5459
|
+
};
|
|
5460
|
+
addEventListener(document, 'DOMContentLoaded', onDomReady, {
|
|
5461
|
+
once: true
|
|
5462
|
+
});
|
|
5463
|
+
} else {
|
|
5464
|
+
triggerRemoteConfigLoad('immediate');
|
|
5465
|
+
}
|
|
5466
|
+
} else {
|
|
5467
|
+
triggerRemoteConfigLoad('no-document');
|
|
5468
|
+
}
|
|
5341
5469
|
addEventListener(window, 'onpagehide' in self ? 'pagehide' : 'unload', this.capturePageLeave.bind(this), {
|
|
5342
5470
|
passive: false
|
|
5343
5471
|
});
|
|
@@ -5374,11 +5502,19 @@ class Leanbase extends core.PostHogCore {
|
|
|
5374
5502
|
}
|
|
5375
5503
|
}
|
|
5376
5504
|
async loadRemoteConfig() {
|
|
5377
|
-
if (
|
|
5505
|
+
if (this._remoteConfigLoadAttempted) {
|
|
5506
|
+
return;
|
|
5507
|
+
}
|
|
5508
|
+
this._remoteConfigLoadAttempted = true;
|
|
5509
|
+
try {
|
|
5378
5510
|
const remoteConfig = await this.reloadRemoteConfigAsync();
|
|
5379
5511
|
if (remoteConfig) {
|
|
5380
5512
|
this.onRemoteConfig(remoteConfig);
|
|
5381
5513
|
}
|
|
5514
|
+
} finally {
|
|
5515
|
+
// Regardless of success/failure, we consider remote config "resolved" so replay isn't blocked forever.
|
|
5516
|
+
this._remoteConfigResolved = true;
|
|
5517
|
+
this._maybeStartSessionRecording();
|
|
5382
5518
|
}
|
|
5383
5519
|
}
|
|
5384
5520
|
onRemoteConfig(config) {
|
|
@@ -5391,6 +5527,26 @@ class Leanbase extends core.PostHogCore {
|
|
|
5391
5527
|
this.isRemoteConfigLoaded = true;
|
|
5392
5528
|
this.replayAutocapture?.onRemoteConfig(config);
|
|
5393
5529
|
this.sessionRecording?.onRemoteConfig(config);
|
|
5530
|
+
// Remote config has been applied; allow replay start if flags are also ready.
|
|
5531
|
+
this._remoteConfigResolved = true;
|
|
5532
|
+
this._maybeStartSessionRecording();
|
|
5533
|
+
}
|
|
5534
|
+
_maybeStartSessionRecording() {
|
|
5535
|
+
if (this._maybeStartedSessionRecording) {
|
|
5536
|
+
return;
|
|
5537
|
+
}
|
|
5538
|
+
if (!this.sessionRecording) {
|
|
5539
|
+
return;
|
|
5540
|
+
}
|
|
5541
|
+
if (!this._featureFlagsResolved || !this._remoteConfigResolved) {
|
|
5542
|
+
return;
|
|
5543
|
+
}
|
|
5544
|
+
this._maybeStartedSessionRecording = true;
|
|
5545
|
+
try {
|
|
5546
|
+
this.sessionRecording.startIfEnabledOrStop();
|
|
5547
|
+
} catch (e) {
|
|
5548
|
+
logger$2.error('Failed to start session recording', e);
|
|
5549
|
+
}
|
|
5394
5550
|
}
|
|
5395
5551
|
fetch(url, options) {
|
|
5396
5552
|
const fetchFn = core.getFetch();
|