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