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