@leanbase-giangnd/js 0.3.0 → 0.3.2
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 +154 -189
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +58 -81
- package/dist/index.mjs +154 -189
- package/dist/index.mjs.map +1 -1
- package/dist/leanbase.iife.js +154 -189
- package/dist/leanbase.iife.js.map +1 -1
- package/package.json +1 -1
- package/src/extensions/replay/extension-shim.ts +1 -114
- package/src/extensions/replay/external/lazy-loaded-session-recorder.ts +130 -38
- package/src/extensions/replay/external/mutation-throttler.ts +1 -4
- package/src/extensions/replay/session-recording.ts +0 -59
- package/src/leanbase.ts +4 -1
- package/src/utils/request-router.ts +39 -0
- 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.3.
|
|
1174
|
+
var version = "0.3.2";
|
|
1175
1175
|
var packageInfo = {
|
|
1176
1176
|
version: version};
|
|
1177
1177
|
|
|
@@ -3110,117 +3110,20 @@ const isLikelyBot = function (navigator, customBlockedUserAgents) {
|
|
|
3110
3110
|
};
|
|
3111
3111
|
|
|
3112
3112
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
3113
|
-
// We avoid importing '@rrweb/record' at module load time to prevent IIFE builds
|
|
3114
|
-
// from requiring a top-level global. Instead, expose a lazy proxy that will
|
|
3115
|
-
// dynamically import the module the first time it's used.
|
|
3116
|
-
let _cachedRRWeb = null;
|
|
3117
|
-
async function _loadRRWebModule() {
|
|
3118
|
-
if (_cachedRRWeb) return _cachedRRWeb;
|
|
3119
|
-
try {
|
|
3120
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3121
|
-
const mod = await import('@rrweb/record');
|
|
3122
|
-
_cachedRRWeb = mod;
|
|
3123
|
-
return _cachedRRWeb;
|
|
3124
|
-
} catch (e) {
|
|
3125
|
-
return null;
|
|
3126
|
-
}
|
|
3127
|
-
}
|
|
3128
|
-
// queue for method calls before rrweb loads
|
|
3129
|
-
const _queuedCalls = [];
|
|
3130
|
-
// Create a proxy function that delegates to the real rrweb.record when called
|
|
3131
|
-
const rrwebRecordProxy = function (...args) {
|
|
3132
|
-
let realStop;
|
|
3133
|
-
let calledReal = false;
|
|
3134
|
-
// Start loading asynchronously and call the real record when available
|
|
3135
|
-
void (async () => {
|
|
3136
|
-
const mod = await _loadRRWebModule();
|
|
3137
|
-
const real = mod && (mod.record ?? mod.default?.record);
|
|
3138
|
-
if (real) {
|
|
3139
|
-
try {
|
|
3140
|
-
calledReal = true;
|
|
3141
|
-
realStop = real(...args);
|
|
3142
|
-
// flush any queued calls that were waiting for rrweb
|
|
3143
|
-
while (_queuedCalls.length) {
|
|
3144
|
-
try {
|
|
3145
|
-
const fn = _queuedCalls.shift();
|
|
3146
|
-
fn();
|
|
3147
|
-
} catch (e) {
|
|
3148
|
-
// ignore
|
|
3149
|
-
}
|
|
3150
|
-
}
|
|
3151
|
-
} catch (e) {
|
|
3152
|
-
// ignore
|
|
3153
|
-
}
|
|
3154
|
-
}
|
|
3155
|
-
})();
|
|
3156
|
-
// return a stop function that will call the real stop when available
|
|
3157
|
-
return () => {
|
|
3158
|
-
if (realStop) {
|
|
3159
|
-
try {
|
|
3160
|
-
realStop();
|
|
3161
|
-
} catch (e) {
|
|
3162
|
-
// ignore
|
|
3163
|
-
}
|
|
3164
|
-
} else if (!calledReal) {
|
|
3165
|
-
// If rrweb hasn't been initialised yet, queue a stop request that will
|
|
3166
|
-
// call the real stop once available.
|
|
3167
|
-
_queuedCalls.push(() => {
|
|
3168
|
-
try {
|
|
3169
|
-
realStop?.();
|
|
3170
|
-
} catch (e) {
|
|
3171
|
-
// ignore
|
|
3172
|
-
}
|
|
3173
|
-
});
|
|
3174
|
-
}
|
|
3175
|
-
};
|
|
3176
|
-
};
|
|
3177
|
-
// methods that can be called on the rrweb.record object - queue until real module is available
|
|
3178
|
-
rrwebRecordProxy.addCustomEvent = function (tag, payload) {
|
|
3179
|
-
const call = () => {
|
|
3180
|
-
try {
|
|
3181
|
-
const real = _cachedRRWeb && (_cachedRRWeb.record ?? _cachedRRWeb.default?.record);
|
|
3182
|
-
real?.addCustomEvent?.(tag, payload);
|
|
3183
|
-
} catch (e) {
|
|
3184
|
-
// ignore
|
|
3185
|
-
}
|
|
3186
|
-
};
|
|
3187
|
-
if (_cachedRRWeb) call();else _queuedCalls.push(call);
|
|
3188
|
-
};
|
|
3189
|
-
rrwebRecordProxy.takeFullSnapshot = function () {
|
|
3190
|
-
const call = () => {
|
|
3191
|
-
try {
|
|
3192
|
-
const real = _cachedRRWeb && (_cachedRRWeb.record ?? _cachedRRWeb.default?.record);
|
|
3193
|
-
real?.takeFullSnapshot?.();
|
|
3194
|
-
} catch (e) {
|
|
3195
|
-
// ignore
|
|
3196
|
-
}
|
|
3197
|
-
};
|
|
3198
|
-
if (_cachedRRWeb) call();else _queuedCalls.push(call);
|
|
3199
|
-
};
|
|
3200
|
-
// Use a safe global target (prefer `win`, fallback to globalThis)
|
|
3201
3113
|
const _target = win ?? globalThis;
|
|
3202
3114
|
_target.__PosthogExtensions__ = _target.__PosthogExtensions__ || {};
|
|
3203
|
-
|
|
3204
|
-
// builds that execute this file don't require rrweb at module evaluation time.
|
|
3205
|
-
_target.__PosthogExtensions__.rrweb = _target.__PosthogExtensions__.rrweb || {
|
|
3206
|
-
record: rrwebRecordProxy
|
|
3207
|
-
};
|
|
3208
|
-
// Provide initSessionRecording if not present — return a new LazyLoadedSessionRecording when called
|
|
3115
|
+
_target.__PosthogExtensions__.rrweb = _target.__PosthogExtensions__.rrweb || {};
|
|
3209
3116
|
_target.__PosthogExtensions__.initSessionRecording = _target.__PosthogExtensions__.initSessionRecording || (instance => {
|
|
3210
3117
|
const factory = _target.__PosthogExtensions__._initSessionRecordingFactory;
|
|
3211
3118
|
if (factory) {
|
|
3212
3119
|
return factory(instance);
|
|
3213
3120
|
}
|
|
3214
|
-
// If no factory is registered yet, return undefined — callers should handle lazy-loading.
|
|
3215
3121
|
return undefined;
|
|
3216
3122
|
});
|
|
3217
|
-
// Provide a no-op loadExternalDependency that calls the callback immediately (since rrweb is bundled)
|
|
3218
3123
|
_target.__PosthogExtensions__.loadExternalDependency = _target.__PosthogExtensions__.loadExternalDependency || ((instance, scriptName, cb) => {
|
|
3219
3124
|
if (cb) cb(undefined);
|
|
3220
3125
|
});
|
|
3221
|
-
// Provide rrwebPlugins object with network plugin factory if not present
|
|
3222
3126
|
_target.__PosthogExtensions__.rrwebPlugins = _target.__PosthogExtensions__.rrwebPlugins || {};
|
|
3223
|
-
// Default to undefined; the lazy-loaded recorder will register the real factory when it initializes.
|
|
3224
3127
|
_target.__PosthogExtensions__.rrwebPlugins.getRecordNetworkPlugin = _target.__PosthogExtensions__.rrwebPlugins.getRecordNetworkPlugin || (() => undefined);
|
|
3225
3128
|
|
|
3226
3129
|
// Type definitions copied from @rrweb/types@2.0.0-alpha.17 and rrweb-snapshot@2.0.0-alpha.17
|
|
@@ -3891,10 +3794,8 @@ class MutationThrottler {
|
|
|
3891
3794
|
return event;
|
|
3892
3795
|
}
|
|
3893
3796
|
};
|
|
3894
|
-
const configuredBucketSize = this._options.bucketSize ?? 100;
|
|
3895
|
-
const effectiveBucketSize = Math.max(configuredBucketSize - 1, 1);
|
|
3896
3797
|
this._rateLimiter = new core.BucketedRateLimiter({
|
|
3897
|
-
bucketSize:
|
|
3798
|
+
bucketSize: this._options.bucketSize ?? 100,
|
|
3898
3799
|
refillRate: this._options.refillRate ?? 10,
|
|
3899
3800
|
refillInterval: 1000,
|
|
3900
3801
|
// one second
|
|
@@ -4090,6 +3991,23 @@ class LazyLoadedSessionRecording {
|
|
|
4090
3991
|
get sessionId() {
|
|
4091
3992
|
return this._sessionId;
|
|
4092
3993
|
}
|
|
3994
|
+
_disablePermanently(reason, error) {
|
|
3995
|
+
this._permanentlyDisabled = true;
|
|
3996
|
+
this._isFullyReady = false;
|
|
3997
|
+
this._mutationThrottler?.stop();
|
|
3998
|
+
this._mutationThrottler = undefined;
|
|
3999
|
+
this._queuedRRWebEvents = [];
|
|
4000
|
+
this._recording = undefined;
|
|
4001
|
+
this._stopRrweb = undefined;
|
|
4002
|
+
if (!this._loggedPermanentlyDisabled) {
|
|
4003
|
+
this._loggedPermanentlyDisabled = true;
|
|
4004
|
+
if (error) {
|
|
4005
|
+
logger.error(`replay disabled: ${reason}`, error);
|
|
4006
|
+
} else {
|
|
4007
|
+
logger.error(`replay disabled: ${reason}`);
|
|
4008
|
+
}
|
|
4009
|
+
}
|
|
4010
|
+
}
|
|
4093
4011
|
get _sessionManager() {
|
|
4094
4012
|
if (!this._instance.sessionManager) {
|
|
4095
4013
|
throw new Error(LOGGER_PREFIX$1 + ' must be started with a valid sessionManager.');
|
|
@@ -4121,6 +4039,9 @@ class LazyLoadedSessionRecording {
|
|
|
4121
4039
|
*/
|
|
4122
4040
|
this._forceAllowLocalhostNetworkCapture = false;
|
|
4123
4041
|
this._stopRrweb = undefined;
|
|
4042
|
+
this._permanentlyDisabled = false;
|
|
4043
|
+
this._loggedPermanentlyDisabled = false;
|
|
4044
|
+
this._hasReportedRecordingInitialized = false;
|
|
4124
4045
|
this._lastActivityTimestamp = Date.now();
|
|
4125
4046
|
/**
|
|
4126
4047
|
* and a queue - that contains rrweb events that we want to send to rrweb, but rrweb wasn't able to accept them yet
|
|
@@ -4310,6 +4231,9 @@ class LazyLoadedSessionRecording {
|
|
|
4310
4231
|
}
|
|
4311
4232
|
}
|
|
4312
4233
|
_tryAddCustomEvent(tag, payload) {
|
|
4234
|
+
if (!this.isStarted || !this._recording) {
|
|
4235
|
+
return false;
|
|
4236
|
+
}
|
|
4313
4237
|
const rrwebRecord = getRRWebRecord();
|
|
4314
4238
|
if (!rrwebRecord || typeof rrwebRecord.addCustomEvent !== 'function') {
|
|
4315
4239
|
return false;
|
|
@@ -4339,6 +4263,10 @@ class LazyLoadedSessionRecording {
|
|
|
4339
4263
|
}
|
|
4340
4264
|
}
|
|
4341
4265
|
_processQueuedEvents() {
|
|
4266
|
+
if (!this.isStarted || !this._recording) {
|
|
4267
|
+
this._queuedRRWebEvents = [];
|
|
4268
|
+
return;
|
|
4269
|
+
}
|
|
4342
4270
|
if (this._queuedRRWebEvents.length) {
|
|
4343
4271
|
// if rrweb isn't ready to accept events earlier, then we queued them up.
|
|
4344
4272
|
// now that `emit` has been called rrweb should be ready to accept them.
|
|
@@ -4360,6 +4288,9 @@ class LazyLoadedSessionRecording {
|
|
|
4360
4288
|
}
|
|
4361
4289
|
}
|
|
4362
4290
|
_tryTakeFullSnapshot() {
|
|
4291
|
+
if (!this.isStarted || !this._recording) {
|
|
4292
|
+
return false;
|
|
4293
|
+
}
|
|
4363
4294
|
const rrwebRecord = getRRWebRecord();
|
|
4364
4295
|
if (!rrwebRecord || typeof rrwebRecord.takeFullSnapshot !== 'function') {
|
|
4365
4296
|
return false;
|
|
@@ -4373,6 +4304,9 @@ class LazyLoadedSessionRecording {
|
|
|
4373
4304
|
return this._instance.config.session_recording?.full_snapshot_interval_millis ?? FIVE_MINUTES;
|
|
4374
4305
|
}
|
|
4375
4306
|
_scheduleFullSnapshot() {
|
|
4307
|
+
if (!this.isStarted || !this._recording) {
|
|
4308
|
+
return;
|
|
4309
|
+
}
|
|
4376
4310
|
if (this._fullSnapshotTimer) {
|
|
4377
4311
|
clearInterval(this._fullSnapshotTimer);
|
|
4378
4312
|
}
|
|
@@ -4389,6 +4323,9 @@ class LazyLoadedSessionRecording {
|
|
|
4389
4323
|
}, interval);
|
|
4390
4324
|
}
|
|
4391
4325
|
_pauseRecording() {
|
|
4326
|
+
if (!this.isStarted || !this._recording) {
|
|
4327
|
+
return;
|
|
4328
|
+
}
|
|
4392
4329
|
// we check _urlBlocked not status, since more than one thing can affect status
|
|
4393
4330
|
if (this._urlTriggerMatching.urlBlocked) {
|
|
4394
4331
|
return;
|
|
@@ -4406,6 +4343,9 @@ class LazyLoadedSessionRecording {
|
|
|
4406
4343
|
});
|
|
4407
4344
|
}
|
|
4408
4345
|
_resumeRecording() {
|
|
4346
|
+
if (!this.isStarted || !this._recording) {
|
|
4347
|
+
return;
|
|
4348
|
+
}
|
|
4409
4349
|
// we check _urlBlocked not status, since more than one thing can affect status
|
|
4410
4350
|
if (!this._urlTriggerMatching.urlBlocked) {
|
|
4411
4351
|
return;
|
|
@@ -4419,6 +4359,9 @@ class LazyLoadedSessionRecording {
|
|
|
4419
4359
|
logger.info('recording resumed');
|
|
4420
4360
|
}
|
|
4421
4361
|
_activateTrigger(triggerType) {
|
|
4362
|
+
if (!this.isStarted || !this._recording || !this._isFullyReady) {
|
|
4363
|
+
return;
|
|
4364
|
+
}
|
|
4422
4365
|
if (this._triggerMatching.triggerStatus(this.sessionId) === TRIGGER_PENDING) {
|
|
4423
4366
|
// status is stored separately for URL and event triggers
|
|
4424
4367
|
this._instance?.persistence?.register({
|
|
@@ -4429,7 +4372,7 @@ class LazyLoadedSessionRecording {
|
|
|
4429
4372
|
}
|
|
4430
4373
|
}
|
|
4431
4374
|
get isStarted() {
|
|
4432
|
-
return !!this.
|
|
4375
|
+
return !!this._recording?.stop;
|
|
4433
4376
|
}
|
|
4434
4377
|
get _remoteConfig() {
|
|
4435
4378
|
const persistedConfig = this._instance.get_property(SESSION_RECORDING_REMOTE_CONFIG);
|
|
@@ -4440,6 +4383,9 @@ class LazyLoadedSessionRecording {
|
|
|
4440
4383
|
return parsedConfig;
|
|
4441
4384
|
}
|
|
4442
4385
|
async start(startReason) {
|
|
4386
|
+
if (this._permanentlyDisabled) {
|
|
4387
|
+
return;
|
|
4388
|
+
}
|
|
4443
4389
|
this._isFullyReady = false;
|
|
4444
4390
|
const config = this._remoteConfig;
|
|
4445
4391
|
if (!config) {
|
|
@@ -4465,8 +4411,14 @@ class LazyLoadedSessionRecording {
|
|
|
4465
4411
|
});
|
|
4466
4412
|
this._urlTriggerMatching.onConfig(config);
|
|
4467
4413
|
this._eventTriggerMatching.onConfig(config);
|
|
4468
|
-
|
|
4469
|
-
this.
|
|
4414
|
+
// Start rrweb first; only once we have a valid recorder do we install any listeners/timers.
|
|
4415
|
+
await this._startRecorder();
|
|
4416
|
+
// If rrweb failed to load/start, do not proceed further.
|
|
4417
|
+
// This prevents installing listeners that assume rrweb is active.
|
|
4418
|
+
if (!this.isStarted) {
|
|
4419
|
+
return;
|
|
4420
|
+
}
|
|
4421
|
+
// Now that rrweb has started, we can safely install replay side-effects.
|
|
4470
4422
|
this._linkedFlagMatching.onConfig(config, (flag, variant) => {
|
|
4471
4423
|
this._reportStarted('linked_flag_matched', {
|
|
4472
4424
|
flag,
|
|
@@ -4474,12 +4426,8 @@ class LazyLoadedSessionRecording {
|
|
|
4474
4426
|
});
|
|
4475
4427
|
});
|
|
4476
4428
|
this._makeSamplingDecision(this.sessionId);
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
// This prevents installing listeners that assume rrweb is active.
|
|
4480
|
-
if (!this.isStarted) {
|
|
4481
|
-
return;
|
|
4482
|
-
}
|
|
4429
|
+
this._removeEventTriggerCaptureHook?.();
|
|
4430
|
+
this._addEventTriggerListener();
|
|
4483
4431
|
// Only start processing rrweb emits once the ingestion endpoint is available.
|
|
4484
4432
|
// If it isn't available, we must degrade to a no-op (never crash the host app).
|
|
4485
4433
|
this._isFullyReady = this._canCaptureSnapshots();
|
|
@@ -4529,7 +4477,13 @@ class LazyLoadedSessionRecording {
|
|
|
4529
4477
|
});
|
|
4530
4478
|
}
|
|
4531
4479
|
if (this.status === ACTIVE) {
|
|
4532
|
-
|
|
4480
|
+
const reason = startReason || 'recording_initialized';
|
|
4481
|
+
if (reason !== 'recording_initialized' || !this._hasReportedRecordingInitialized) {
|
|
4482
|
+
if (reason === 'recording_initialized') {
|
|
4483
|
+
this._hasReportedRecordingInitialized = true;
|
|
4484
|
+
}
|
|
4485
|
+
this._reportStarted(reason);
|
|
4486
|
+
}
|
|
4533
4487
|
}
|
|
4534
4488
|
}
|
|
4535
4489
|
stop() {
|
|
@@ -4558,9 +4512,11 @@ class LazyLoadedSessionRecording {
|
|
|
4558
4512
|
this._mutationThrottler?.stop();
|
|
4559
4513
|
// Clear any queued rrweb events to prevent memory leaks from closures
|
|
4560
4514
|
this._queuedRRWebEvents = [];
|
|
4561
|
-
this.
|
|
4515
|
+
this._recording?.stop?.();
|
|
4516
|
+
this._recording = undefined;
|
|
4562
4517
|
this._stopRrweb = undefined;
|
|
4563
4518
|
this._isFullyReady = false;
|
|
4519
|
+
this._hasReportedRecordingInitialized = false;
|
|
4564
4520
|
logger.info('stopped');
|
|
4565
4521
|
}
|
|
4566
4522
|
_snapshotIngestionUrl() {
|
|
@@ -4578,8 +4534,12 @@ class LazyLoadedSessionRecording {
|
|
|
4578
4534
|
return !!this._snapshotIngestionUrl();
|
|
4579
4535
|
}
|
|
4580
4536
|
onRRwebEmit(rawEvent) {
|
|
4537
|
+
// First-line invariant gate: drop everything unless replay is truly started.
|
|
4538
|
+
if (!this.isStarted || !this._recording) {
|
|
4539
|
+
return;
|
|
4540
|
+
}
|
|
4581
4541
|
// Never process rrweb emits until we're fully ready.
|
|
4582
|
-
if (!this._isFullyReady
|
|
4542
|
+
if (!this._isFullyReady) {
|
|
4583
4543
|
return;
|
|
4584
4544
|
}
|
|
4585
4545
|
try {
|
|
@@ -4684,6 +4644,9 @@ class LazyLoadedSessionRecording {
|
|
|
4684
4644
|
});
|
|
4685
4645
|
}
|
|
4686
4646
|
overrideLinkedFlag() {
|
|
4647
|
+
if (!this.isStarted || !this._recording || !this._isFullyReady) {
|
|
4648
|
+
return;
|
|
4649
|
+
}
|
|
4687
4650
|
this._linkedFlagMatching.linkedFlagSeen = true;
|
|
4688
4651
|
this._tryTakeFullSnapshot();
|
|
4689
4652
|
this._reportStarted('linked_flag_overridden');
|
|
@@ -4695,6 +4658,9 @@ class LazyLoadedSessionRecording {
|
|
|
4695
4658
|
* instead call `posthog.startSessionRecording({sampling: true})`
|
|
4696
4659
|
* */
|
|
4697
4660
|
overrideSampling() {
|
|
4661
|
+
if (!this.isStarted || !this._recording || !this._isFullyReady) {
|
|
4662
|
+
return;
|
|
4663
|
+
}
|
|
4698
4664
|
this._instance.persistence?.register({
|
|
4699
4665
|
// short-circuits the `makeSamplingDecision` function in the session recording module
|
|
4700
4666
|
[SESSION_RECORDING_IS_SAMPLED]: this.sessionId
|
|
@@ -4709,6 +4675,9 @@ class LazyLoadedSessionRecording {
|
|
|
4709
4675
|
* instead call `posthog.startSessionRecording({trigger: 'url' | 'event'})`
|
|
4710
4676
|
* */
|
|
4711
4677
|
overrideTrigger(triggerType) {
|
|
4678
|
+
if (!this.isStarted || !this._recording || !this._isFullyReady) {
|
|
4679
|
+
return;
|
|
4680
|
+
}
|
|
4712
4681
|
this._activateTrigger(triggerType);
|
|
4713
4682
|
}
|
|
4714
4683
|
_clearFlushBufferTimer() {
|
|
@@ -4841,10 +4810,18 @@ class LazyLoadedSessionRecording {
|
|
|
4841
4810
|
return this._buffer;
|
|
4842
4811
|
}
|
|
4843
4812
|
_reportStarted(startReason, tagPayload) {
|
|
4813
|
+
if (!this.isStarted || !this._recording) {
|
|
4814
|
+
return;
|
|
4815
|
+
}
|
|
4844
4816
|
this._instance.registerForSession({
|
|
4845
4817
|
$session_recording_start_reason: startReason
|
|
4846
4818
|
});
|
|
4847
|
-
|
|
4819
|
+
const message = startReason.replace('_', ' ');
|
|
4820
|
+
if (typeof tagPayload === 'undefined') {
|
|
4821
|
+
logger.info(message);
|
|
4822
|
+
} else {
|
|
4823
|
+
logger.info(message, tagPayload);
|
|
4824
|
+
}
|
|
4848
4825
|
if (!core.includes(['recording_initialized', 'session_id_changed'], startReason)) {
|
|
4849
4826
|
this._tryAddCustomEvent(startReason, tagPayload);
|
|
4850
4827
|
}
|
|
@@ -4984,7 +4961,7 @@ class LazyLoadedSessionRecording {
|
|
|
4984
4961
|
};
|
|
4985
4962
|
}
|
|
4986
4963
|
async _startRecorder() {
|
|
4987
|
-
if (this.
|
|
4964
|
+
if (this._permanentlyDisabled || this._recording) {
|
|
4988
4965
|
return;
|
|
4989
4966
|
}
|
|
4990
4967
|
// rrweb config info: https://github.com/rrweb-io/rrweb/blob/7d5d0033258d6c29599fb08412202d9a2c7b9413/src/record/index.ts#L28
|
|
@@ -5046,27 +5023,18 @@ class LazyLoadedSessionRecording {
|
|
|
5046
5023
|
rrwebRecord = loaded ?? undefined;
|
|
5047
5024
|
}
|
|
5048
5025
|
if (!rrwebRecord) {
|
|
5049
|
-
|
|
5026
|
+
this._disablePermanently('rrweb record function unavailable');
|
|
5050
5027
|
return;
|
|
5051
5028
|
}
|
|
5052
|
-
this._mutationThrottler = this._mutationThrottler ?? new MutationThrottler(rrwebRecord, {
|
|
5053
|
-
refillRate: this._instance.config.session_recording?.__mutationThrottlerRefillRate,
|
|
5054
|
-
bucketSize: this._instance.config.session_recording?.__mutationThrottlerBucketSize,
|
|
5055
|
-
onBlockedNode: (id, node) => {
|
|
5056
|
-
const message = `Too many mutations on node '${id}'. Rate limiting. This could be due to SVG animations or something similar`;
|
|
5057
|
-
logger.info(message, {
|
|
5058
|
-
node: node
|
|
5059
|
-
});
|
|
5060
|
-
this.log(LOGGER_PREFIX$1 + ' ' + message, 'warn');
|
|
5061
|
-
}
|
|
5062
|
-
});
|
|
5063
5029
|
const activePlugins = this._gatherRRWebPlugins();
|
|
5030
|
+
let stopHandler;
|
|
5064
5031
|
try {
|
|
5065
|
-
|
|
5032
|
+
stopHandler = rrwebRecord({
|
|
5066
5033
|
emit: event => {
|
|
5067
5034
|
try {
|
|
5068
5035
|
this.onRRwebEmit(event);
|
|
5069
5036
|
} catch (e) {
|
|
5037
|
+
// never throw from rrweb emit handler
|
|
5070
5038
|
logger.error('error in rrweb emit handler', e);
|
|
5071
5039
|
}
|
|
5072
5040
|
},
|
|
@@ -5074,10 +5042,30 @@ class LazyLoadedSessionRecording {
|
|
|
5074
5042
|
...sessionRecordingOptions
|
|
5075
5043
|
});
|
|
5076
5044
|
} catch (e) {
|
|
5077
|
-
|
|
5078
|
-
|
|
5045
|
+
this._disablePermanently('rrweb recorder threw during initialization', e);
|
|
5046
|
+
return;
|
|
5047
|
+
}
|
|
5048
|
+
if (typeof stopHandler !== 'function') {
|
|
5049
|
+
this._disablePermanently('rrweb recorder returned an invalid stop handler');
|
|
5079
5050
|
return;
|
|
5080
5051
|
}
|
|
5052
|
+
// Mark replay started only after rrweb has successfully returned a valid stop handler.
|
|
5053
|
+
this._recording = {
|
|
5054
|
+
stop: stopHandler
|
|
5055
|
+
};
|
|
5056
|
+
this._stopRrweb = stopHandler;
|
|
5057
|
+
// Only create mutation throttler once replay is truly started.
|
|
5058
|
+
this._mutationThrottler = this._mutationThrottler ?? new MutationThrottler(rrwebRecord, {
|
|
5059
|
+
refillRate: this._instance.config.session_recording?.__mutationThrottlerRefillRate,
|
|
5060
|
+
bucketSize: this._instance.config.session_recording?.__mutationThrottlerBucketSize,
|
|
5061
|
+
onBlockedNode: (id, node) => {
|
|
5062
|
+
const message = `Too many mutations on node '${id}'. Rate limiting. This could be due to SVG animations or something similar`;
|
|
5063
|
+
logger.info(message, {
|
|
5064
|
+
node: node
|
|
5065
|
+
});
|
|
5066
|
+
this.log(LOGGER_PREFIX$1 + ' ' + message, 'warn');
|
|
5067
|
+
}
|
|
5068
|
+
});
|
|
5081
5069
|
// We reset the last activity timestamp, resetting the idle timer
|
|
5082
5070
|
this._lastActivityTimestamp = Date.now();
|
|
5083
5071
|
// stay unknown if we're not sure if we're idle or not
|
|
@@ -5107,10 +5095,6 @@ class SessionRecording {
|
|
|
5107
5095
|
get started() {
|
|
5108
5096
|
return !!this._lazyLoadedSessionRecording?.isStarted;
|
|
5109
5097
|
}
|
|
5110
|
-
/**
|
|
5111
|
-
* defaults to buffering mode until a flags response is received
|
|
5112
|
-
* once a flags response is received status can be disabled, active or sampled
|
|
5113
|
-
*/
|
|
5114
5098
|
get status() {
|
|
5115
5099
|
if (this._lazyLoadedSessionRecording) {
|
|
5116
5100
|
return this._lazyLoadedSessionRecording.status;
|
|
@@ -5124,7 +5108,6 @@ class SessionRecording {
|
|
|
5124
5108
|
this._instance = _instance;
|
|
5125
5109
|
this._forceAllowLocalhostNetworkCapture = false;
|
|
5126
5110
|
this._receivedFlags = false;
|
|
5127
|
-
this._serverRecordingEnabled = false;
|
|
5128
5111
|
this._persistFlagsOnSessionListener = undefined;
|
|
5129
5112
|
if (!this._instance.sessionManager) {
|
|
5130
5113
|
log.error('started without valid sessionManager');
|
|
@@ -5147,26 +5130,14 @@ class SessionRecording {
|
|
|
5147
5130
|
const canRunReplay = !core.isUndefined(Object.assign) && !core.isUndefined(Array.from);
|
|
5148
5131
|
if (this._isRecordingEnabled && canRunReplay) {
|
|
5149
5132
|
this._lazyLoadAndStart(startReason);
|
|
5150
|
-
log.info('starting');
|
|
5151
5133
|
} else {
|
|
5152
5134
|
this.stopRecording();
|
|
5153
5135
|
}
|
|
5154
5136
|
}
|
|
5155
|
-
/**
|
|
5156
|
-
* session recording waits until it receives remote config before loading the script
|
|
5157
|
-
* this is to ensure we can control the script name remotely
|
|
5158
|
-
* and because we wait until we have local and remote config to determine if we should start at all
|
|
5159
|
-
* if start is called and there is no remote config then we wait until there is
|
|
5160
|
-
*/
|
|
5161
5137
|
_lazyLoadAndStart(startReason) {
|
|
5162
|
-
// by checking `_isRecordingEnabled` here we know that
|
|
5163
|
-
// we have stored remote config and client config to read
|
|
5164
|
-
// replay waits for both local and remote config before starting
|
|
5165
5138
|
if (!this._isRecordingEnabled) {
|
|
5166
5139
|
return;
|
|
5167
5140
|
}
|
|
5168
|
-
// If extensions provide a loader, use it. Otherwise fallback to the local _onScriptLoaded which
|
|
5169
|
-
// will create the local LazyLoadedSessionRecording (so tests that mock it work correctly).
|
|
5170
5141
|
const loader = assignableWindow.__PosthogExtensions__?.loadExternalDependency;
|
|
5171
5142
|
if (typeof loader === 'function') {
|
|
5172
5143
|
loader(this._instance, this._scriptName, err => {
|
|
@@ -5221,16 +5192,10 @@ class SessionRecording {
|
|
|
5221
5192
|
});
|
|
5222
5193
|
};
|
|
5223
5194
|
persistResponse();
|
|
5224
|
-
// in case we see multiple flags responses, we should only use the response from the most recent one
|
|
5225
5195
|
this._persistFlagsOnSessionListener?.();
|
|
5226
|
-
// we 100% know there is a session manager by this point
|
|
5227
5196
|
this._persistFlagsOnSessionListener = this._instance.sessionManager?.onSessionId(persistResponse);
|
|
5228
5197
|
}
|
|
5229
5198
|
}
|
|
5230
|
-
_clearRemoteConfig() {
|
|
5231
|
-
this._instance.persistence?.unregister(SESSION_RECORDING_REMOTE_CONFIG);
|
|
5232
|
-
this._resetSampling();
|
|
5233
|
-
}
|
|
5234
5199
|
onRemoteConfig(response) {
|
|
5235
5200
|
if (!('sessionRecording' in response)) {
|
|
5236
5201
|
// if sessionRecording is not in the response, we do nothing
|
|
@@ -5239,12 +5204,8 @@ class SessionRecording {
|
|
|
5239
5204
|
}
|
|
5240
5205
|
this._receivedFlags = true;
|
|
5241
5206
|
if (response.sessionRecording === false) {
|
|
5242
|
-
this._serverRecordingEnabled = false;
|
|
5243
|
-
this._clearRemoteConfig();
|
|
5244
|
-
this.stopRecording();
|
|
5245
5207
|
return;
|
|
5246
5208
|
}
|
|
5247
|
-
this._serverRecordingEnabled = true;
|
|
5248
5209
|
this._persistRemoteConfig(response);
|
|
5249
5210
|
this.startIfEnabledOrStop();
|
|
5250
5211
|
}
|
|
@@ -5305,59 +5266,61 @@ class SessionRecording {
|
|
|
5305
5266
|
onRRwebEmit(rawEvent) {
|
|
5306
5267
|
this._lazyLoadedSessionRecording?.onRRwebEmit?.(rawEvent);
|
|
5307
5268
|
}
|
|
5308
|
-
/**
|
|
5309
|
-
* this ignores the linked flag config and (if other conditions are met) causes capture to start
|
|
5310
|
-
*
|
|
5311
|
-
* It is not usual to call this directly,
|
|
5312
|
-
* instead call `posthog.startSessionRecording({linked_flag: true})`
|
|
5313
|
-
* */
|
|
5314
5269
|
overrideLinkedFlag() {
|
|
5315
5270
|
// TODO what if this gets called before lazy loading is done
|
|
5316
5271
|
this._lazyLoadedSessionRecording?.overrideLinkedFlag();
|
|
5317
5272
|
}
|
|
5318
|
-
/**
|
|
5319
|
-
* this ignores the sampling config and (if other conditions are met) causes capture to start
|
|
5320
|
-
*
|
|
5321
|
-
* It is not usual to call this directly,
|
|
5322
|
-
* instead call `posthog.startSessionRecording({sampling: true})`
|
|
5323
|
-
* */
|
|
5324
5273
|
overrideSampling() {
|
|
5325
5274
|
// TODO what if this gets called before lazy loading is done
|
|
5326
5275
|
this._lazyLoadedSessionRecording?.overrideSampling();
|
|
5327
5276
|
}
|
|
5328
|
-
/**
|
|
5329
|
-
* this ignores the URL/Event trigger config and (if other conditions are met) causes capture to start
|
|
5330
|
-
*
|
|
5331
|
-
* It is not usual to call this directly,
|
|
5332
|
-
* instead call `posthog.startSessionRecording({trigger: 'url' | 'event'})`
|
|
5333
|
-
* */
|
|
5334
5277
|
overrideTrigger(triggerType) {
|
|
5335
5278
|
// TODO what if this gets called before lazy loading is done
|
|
5336
5279
|
this._lazyLoadedSessionRecording?.overrideTrigger(triggerType);
|
|
5337
5280
|
}
|
|
5338
|
-
/*
|
|
5339
|
-
* whenever we capture an event, we add these properties to the event
|
|
5340
|
-
* these are used to debug issues with the session recording
|
|
5341
|
-
* when looking at the event feed for a session
|
|
5342
|
-
*/
|
|
5343
5281
|
get sdkDebugProperties() {
|
|
5344
5282
|
return this._lazyLoadedSessionRecording?.sdkDebugProperties || {
|
|
5345
5283
|
$recording_status: this.status
|
|
5346
5284
|
};
|
|
5347
5285
|
}
|
|
5348
|
-
/**
|
|
5349
|
-
* This adds a custom event to the session recording
|
|
5350
|
-
*
|
|
5351
|
-
* It is not intended for arbitrary public use - playback only displays known custom events
|
|
5352
|
-
* And is exposed on the public interface only so that other parts of the SDK are able to use it
|
|
5353
|
-
*
|
|
5354
|
-
* if you are calling this from client code, you're probably looking for `posthog.capture('$custom_event', {...})`
|
|
5355
|
-
*/
|
|
5356
5286
|
tryAddCustomEvent(tag, payload) {
|
|
5357
5287
|
return !!this._lazyLoadedSessionRecording?.tryAddCustomEvent(tag, payload);
|
|
5358
5288
|
}
|
|
5359
5289
|
}
|
|
5360
5290
|
|
|
5291
|
+
/**
|
|
5292
|
+
* Leanbase-local version of PostHog's RequestRouter.
|
|
5293
|
+
*
|
|
5294
|
+
* Browser SDK always has a requestRouter instance; Leanbase IIFE needs this too so
|
|
5295
|
+
* features like Session Replay can construct ingestion URLs.
|
|
5296
|
+
*/
|
|
5297
|
+
class RequestRouter {
|
|
5298
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
5299
|
+
constructor(instance) {
|
|
5300
|
+
this.instance = instance;
|
|
5301
|
+
}
|
|
5302
|
+
get apiHost() {
|
|
5303
|
+
const configured = (this.instance.config.api_host || this.instance.config.host || '').trim();
|
|
5304
|
+
return configured.replace(/\/$/, '');
|
|
5305
|
+
}
|
|
5306
|
+
get uiHost() {
|
|
5307
|
+
const configured = this.instance.config.ui_host?.trim().replace(/\/$/, '');
|
|
5308
|
+
return configured || undefined;
|
|
5309
|
+
}
|
|
5310
|
+
endpointFor(target, path = '') {
|
|
5311
|
+
if (path) {
|
|
5312
|
+
path = path[0] === '/' ? path : `/${path}`;
|
|
5313
|
+
}
|
|
5314
|
+
if (target === 'ui') {
|
|
5315
|
+
const host = this.uiHost || this.apiHost;
|
|
5316
|
+
return host + path;
|
|
5317
|
+
}
|
|
5318
|
+
// Leanbase doesn't currently do region-based routing; default to apiHost.
|
|
5319
|
+
// Browser's router has special handling for assets; we keep parity in interface, not domains.
|
|
5320
|
+
return this.apiHost + path;
|
|
5321
|
+
}
|
|
5322
|
+
}
|
|
5323
|
+
|
|
5361
5324
|
const defaultConfig = () => ({
|
|
5362
5325
|
host: 'https://i.leanbase.co',
|
|
5363
5326
|
token: '',
|
|
@@ -5415,6 +5378,8 @@ class Leanbase extends core.PostHogCore {
|
|
|
5415
5378
|
}));
|
|
5416
5379
|
this.isLoaded = true;
|
|
5417
5380
|
this.persistence = new LeanbasePersistence(this.config);
|
|
5381
|
+
// Browser SDK always has a requestRouter; session replay relies on it for $snapshot ingestion URLs.
|
|
5382
|
+
this.requestRouter = new RequestRouter(this);
|
|
5418
5383
|
if (this.config.cookieless_mode !== 'always') {
|
|
5419
5384
|
this.sessionManager = new SessionIdManager(this);
|
|
5420
5385
|
this.sessionPropsManager = new SessionPropsManager(this, this.sessionManager, this.persistence);
|