@leanbase-giangnd/js 0.2.4 → 0.3.1
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 +104 -187
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +0 -38
- package/dist/index.mjs +104 -187
- package/dist/index.mjs.map +1 -1
- package/dist/leanbase.iife.js +104 -187
- 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 +115 -36
- package/src/extensions/replay/external/mutation-throttler.ts +1 -4
- package/src/extensions/replay/session-recording.ts +0 -59
- package/src/version.ts +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1381,29 +1381,17 @@ declare class SessionRecording$1 {
|
|
|
1381
1381
|
private readonly _instance;
|
|
1382
1382
|
_forceAllowLocalhostNetworkCapture: boolean;
|
|
1383
1383
|
private _receivedFlags;
|
|
1384
|
-
private _serverRecordingEnabled;
|
|
1385
1384
|
private _persistFlagsOnSessionListener;
|
|
1386
1385
|
private _lazyLoadedSessionRecording;
|
|
1387
1386
|
get started(): boolean;
|
|
1388
|
-
/**
|
|
1389
|
-
* defaults to buffering mode until a flags response is received
|
|
1390
|
-
* once a flags response is received status can be disabled, active or sampled
|
|
1391
|
-
*/
|
|
1392
1387
|
get status(): SessionRecordingStatus$1;
|
|
1393
1388
|
constructor(_instance: Leanbase);
|
|
1394
1389
|
private get _isRecordingEnabled();
|
|
1395
1390
|
startIfEnabledOrStop(startReason?: SessionStartReason$1): void;
|
|
1396
|
-
/**
|
|
1397
|
-
* session recording waits until it receives remote config before loading the script
|
|
1398
|
-
* this is to ensure we can control the script name remotely
|
|
1399
|
-
* and because we wait until we have local and remote config to determine if we should start at all
|
|
1400
|
-
* if start is called and there is no remote config then we wait until there is
|
|
1401
|
-
*/
|
|
1402
1391
|
private _lazyLoadAndStart;
|
|
1403
1392
|
stopRecording(): void;
|
|
1404
1393
|
private _resetSampling;
|
|
1405
1394
|
private _persistRemoteConfig;
|
|
1406
|
-
private _clearRemoteConfig;
|
|
1407
1395
|
onRemoteConfig(response: RemoteConfig$1): void;
|
|
1408
1396
|
log(message: string, level?: 'log' | 'warn' | 'error'): void;
|
|
1409
1397
|
private get _scriptName();
|
|
@@ -1414,36 +1402,10 @@ declare class SessionRecording$1 {
|
|
|
1414
1402
|
* @deprecated
|
|
1415
1403
|
*/
|
|
1416
1404
|
onRRwebEmit(rawEvent: eventWithTime$1): void;
|
|
1417
|
-
/**
|
|
1418
|
-
* this ignores the linked flag config and (if other conditions are met) causes capture to start
|
|
1419
|
-
*
|
|
1420
|
-
* It is not usual to call this directly,
|
|
1421
|
-
* instead call `posthog.startSessionRecording({linked_flag: true})`
|
|
1422
|
-
* */
|
|
1423
1405
|
overrideLinkedFlag(): void;
|
|
1424
|
-
/**
|
|
1425
|
-
* this ignores the sampling config and (if other conditions are met) causes capture to start
|
|
1426
|
-
*
|
|
1427
|
-
* It is not usual to call this directly,
|
|
1428
|
-
* instead call `posthog.startSessionRecording({sampling: true})`
|
|
1429
|
-
* */
|
|
1430
1406
|
overrideSampling(): void;
|
|
1431
|
-
/**
|
|
1432
|
-
* this ignores the URL/Event trigger config and (if other conditions are met) causes capture to start
|
|
1433
|
-
*
|
|
1434
|
-
* It is not usual to call this directly,
|
|
1435
|
-
* instead call `posthog.startSessionRecording({trigger: 'url' | 'event'})`
|
|
1436
|
-
* */
|
|
1437
1407
|
overrideTrigger(triggerType: TriggerType$1): void;
|
|
1438
1408
|
get sdkDebugProperties(): Properties$1;
|
|
1439
|
-
/**
|
|
1440
|
-
* This adds a custom event to the session recording
|
|
1441
|
-
*
|
|
1442
|
-
* It is not intended for arbitrary public use - playback only displays known custom events
|
|
1443
|
-
* And is exposed on the public interface only so that other parts of the SDK are able to use it
|
|
1444
|
-
*
|
|
1445
|
-
* if you are calling this from client code, you're probably looking for `posthog.capture('$custom_event', {...})`
|
|
1446
|
-
*/
|
|
1447
1409
|
tryAddCustomEvent(tag: string, payload: any): boolean;
|
|
1448
1410
|
}
|
|
1449
1411
|
|
package/dist/index.mjs
CHANGED
|
@@ -1169,7 +1169,7 @@ const detectDeviceType = function (user_agent) {
|
|
|
1169
1169
|
}
|
|
1170
1170
|
};
|
|
1171
1171
|
|
|
1172
|
-
var version = "0.
|
|
1172
|
+
var version = "0.3.1";
|
|
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,8 @@ class LazyLoadedSessionRecording {
|
|
|
4119
4037
|
*/
|
|
4120
4038
|
this._forceAllowLocalhostNetworkCapture = false;
|
|
4121
4039
|
this._stopRrweb = undefined;
|
|
4040
|
+
this._permanentlyDisabled = false;
|
|
4041
|
+
this._loggedPermanentlyDisabled = false;
|
|
4122
4042
|
this._lastActivityTimestamp = Date.now();
|
|
4123
4043
|
/**
|
|
4124
4044
|
* 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 +4228,9 @@ class LazyLoadedSessionRecording {
|
|
|
4308
4228
|
}
|
|
4309
4229
|
}
|
|
4310
4230
|
_tryAddCustomEvent(tag, payload) {
|
|
4231
|
+
if (!this.isStarted || !this._recording) {
|
|
4232
|
+
return false;
|
|
4233
|
+
}
|
|
4311
4234
|
const rrwebRecord = getRRWebRecord();
|
|
4312
4235
|
if (!rrwebRecord || typeof rrwebRecord.addCustomEvent !== 'function') {
|
|
4313
4236
|
return false;
|
|
@@ -4337,6 +4260,10 @@ class LazyLoadedSessionRecording {
|
|
|
4337
4260
|
}
|
|
4338
4261
|
}
|
|
4339
4262
|
_processQueuedEvents() {
|
|
4263
|
+
if (!this.isStarted || !this._recording) {
|
|
4264
|
+
this._queuedRRWebEvents = [];
|
|
4265
|
+
return;
|
|
4266
|
+
}
|
|
4340
4267
|
if (this._queuedRRWebEvents.length) {
|
|
4341
4268
|
// if rrweb isn't ready to accept events earlier, then we queued them up.
|
|
4342
4269
|
// now that `emit` has been called rrweb should be ready to accept them.
|
|
@@ -4358,6 +4285,9 @@ class LazyLoadedSessionRecording {
|
|
|
4358
4285
|
}
|
|
4359
4286
|
}
|
|
4360
4287
|
_tryTakeFullSnapshot() {
|
|
4288
|
+
if (!this.isStarted || !this._recording) {
|
|
4289
|
+
return false;
|
|
4290
|
+
}
|
|
4361
4291
|
const rrwebRecord = getRRWebRecord();
|
|
4362
4292
|
if (!rrwebRecord || typeof rrwebRecord.takeFullSnapshot !== 'function') {
|
|
4363
4293
|
return false;
|
|
@@ -4371,6 +4301,9 @@ class LazyLoadedSessionRecording {
|
|
|
4371
4301
|
return this._instance.config.session_recording?.full_snapshot_interval_millis ?? FIVE_MINUTES;
|
|
4372
4302
|
}
|
|
4373
4303
|
_scheduleFullSnapshot() {
|
|
4304
|
+
if (!this.isStarted || !this._recording) {
|
|
4305
|
+
return;
|
|
4306
|
+
}
|
|
4374
4307
|
if (this._fullSnapshotTimer) {
|
|
4375
4308
|
clearInterval(this._fullSnapshotTimer);
|
|
4376
4309
|
}
|
|
@@ -4387,6 +4320,9 @@ class LazyLoadedSessionRecording {
|
|
|
4387
4320
|
}, interval);
|
|
4388
4321
|
}
|
|
4389
4322
|
_pauseRecording() {
|
|
4323
|
+
if (!this.isStarted || !this._recording) {
|
|
4324
|
+
return;
|
|
4325
|
+
}
|
|
4390
4326
|
// we check _urlBlocked not status, since more than one thing can affect status
|
|
4391
4327
|
if (this._urlTriggerMatching.urlBlocked) {
|
|
4392
4328
|
return;
|
|
@@ -4404,6 +4340,9 @@ class LazyLoadedSessionRecording {
|
|
|
4404
4340
|
});
|
|
4405
4341
|
}
|
|
4406
4342
|
_resumeRecording() {
|
|
4343
|
+
if (!this.isStarted || !this._recording) {
|
|
4344
|
+
return;
|
|
4345
|
+
}
|
|
4407
4346
|
// we check _urlBlocked not status, since more than one thing can affect status
|
|
4408
4347
|
if (!this._urlTriggerMatching.urlBlocked) {
|
|
4409
4348
|
return;
|
|
@@ -4417,6 +4356,9 @@ class LazyLoadedSessionRecording {
|
|
|
4417
4356
|
logger.info('recording resumed');
|
|
4418
4357
|
}
|
|
4419
4358
|
_activateTrigger(triggerType) {
|
|
4359
|
+
if (!this.isStarted || !this._recording || !this._isFullyReady) {
|
|
4360
|
+
return;
|
|
4361
|
+
}
|
|
4420
4362
|
if (this._triggerMatching.triggerStatus(this.sessionId) === TRIGGER_PENDING) {
|
|
4421
4363
|
// status is stored separately for URL and event triggers
|
|
4422
4364
|
this._instance?.persistence?.register({
|
|
@@ -4427,7 +4369,7 @@ class LazyLoadedSessionRecording {
|
|
|
4427
4369
|
}
|
|
4428
4370
|
}
|
|
4429
4371
|
get isStarted() {
|
|
4430
|
-
return !!this.
|
|
4372
|
+
return !!this._recording?.stop;
|
|
4431
4373
|
}
|
|
4432
4374
|
get _remoteConfig() {
|
|
4433
4375
|
const persistedConfig = this._instance.get_property(SESSION_RECORDING_REMOTE_CONFIG);
|
|
@@ -4438,6 +4380,9 @@ class LazyLoadedSessionRecording {
|
|
|
4438
4380
|
return parsedConfig;
|
|
4439
4381
|
}
|
|
4440
4382
|
async start(startReason) {
|
|
4383
|
+
if (this._permanentlyDisabled) {
|
|
4384
|
+
return;
|
|
4385
|
+
}
|
|
4441
4386
|
this._isFullyReady = false;
|
|
4442
4387
|
const config = this._remoteConfig;
|
|
4443
4388
|
if (!config) {
|
|
@@ -4463,8 +4408,14 @@ class LazyLoadedSessionRecording {
|
|
|
4463
4408
|
});
|
|
4464
4409
|
this._urlTriggerMatching.onConfig(config);
|
|
4465
4410
|
this._eventTriggerMatching.onConfig(config);
|
|
4466
|
-
|
|
4467
|
-
this.
|
|
4411
|
+
// Start rrweb first; only once we have a valid recorder do we install any listeners/timers.
|
|
4412
|
+
await this._startRecorder();
|
|
4413
|
+
// If rrweb failed to load/start, do not proceed further.
|
|
4414
|
+
// This prevents installing listeners that assume rrweb is active.
|
|
4415
|
+
if (!this.isStarted) {
|
|
4416
|
+
return;
|
|
4417
|
+
}
|
|
4418
|
+
// Now that rrweb has started, we can safely install replay side-effects.
|
|
4468
4419
|
this._linkedFlagMatching.onConfig(config, (flag, variant) => {
|
|
4469
4420
|
this._reportStarted('linked_flag_matched', {
|
|
4470
4421
|
flag,
|
|
@@ -4472,12 +4423,8 @@ class LazyLoadedSessionRecording {
|
|
|
4472
4423
|
});
|
|
4473
4424
|
});
|
|
4474
4425
|
this._makeSamplingDecision(this.sessionId);
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
// This prevents installing listeners that assume rrweb is active.
|
|
4478
|
-
if (!this.isStarted) {
|
|
4479
|
-
return;
|
|
4480
|
-
}
|
|
4426
|
+
this._removeEventTriggerCaptureHook?.();
|
|
4427
|
+
this._addEventTriggerListener();
|
|
4481
4428
|
// Only start processing rrweb emits once the ingestion endpoint is available.
|
|
4482
4429
|
// If it isn't available, we must degrade to a no-op (never crash the host app).
|
|
4483
4430
|
this._isFullyReady = this._canCaptureSnapshots();
|
|
@@ -4556,7 +4503,8 @@ class LazyLoadedSessionRecording {
|
|
|
4556
4503
|
this._mutationThrottler?.stop();
|
|
4557
4504
|
// Clear any queued rrweb events to prevent memory leaks from closures
|
|
4558
4505
|
this._queuedRRWebEvents = [];
|
|
4559
|
-
this.
|
|
4506
|
+
this._recording?.stop?.();
|
|
4507
|
+
this._recording = undefined;
|
|
4560
4508
|
this._stopRrweb = undefined;
|
|
4561
4509
|
this._isFullyReady = false;
|
|
4562
4510
|
logger.info('stopped');
|
|
@@ -4576,8 +4524,12 @@ class LazyLoadedSessionRecording {
|
|
|
4576
4524
|
return !!this._snapshotIngestionUrl();
|
|
4577
4525
|
}
|
|
4578
4526
|
onRRwebEmit(rawEvent) {
|
|
4527
|
+
// First-line invariant gate: drop everything unless replay is truly started.
|
|
4528
|
+
if (!this.isStarted || !this._recording) {
|
|
4529
|
+
return;
|
|
4530
|
+
}
|
|
4579
4531
|
// Never process rrweb emits until we're fully ready.
|
|
4580
|
-
if (!this._isFullyReady
|
|
4532
|
+
if (!this._isFullyReady) {
|
|
4581
4533
|
return;
|
|
4582
4534
|
}
|
|
4583
4535
|
try {
|
|
@@ -4682,6 +4634,9 @@ class LazyLoadedSessionRecording {
|
|
|
4682
4634
|
});
|
|
4683
4635
|
}
|
|
4684
4636
|
overrideLinkedFlag() {
|
|
4637
|
+
if (!this.isStarted || !this._recording || !this._isFullyReady) {
|
|
4638
|
+
return;
|
|
4639
|
+
}
|
|
4685
4640
|
this._linkedFlagMatching.linkedFlagSeen = true;
|
|
4686
4641
|
this._tryTakeFullSnapshot();
|
|
4687
4642
|
this._reportStarted('linked_flag_overridden');
|
|
@@ -4693,6 +4648,9 @@ class LazyLoadedSessionRecording {
|
|
|
4693
4648
|
* instead call `posthog.startSessionRecording({sampling: true})`
|
|
4694
4649
|
* */
|
|
4695
4650
|
overrideSampling() {
|
|
4651
|
+
if (!this.isStarted || !this._recording || !this._isFullyReady) {
|
|
4652
|
+
return;
|
|
4653
|
+
}
|
|
4696
4654
|
this._instance.persistence?.register({
|
|
4697
4655
|
// short-circuits the `makeSamplingDecision` function in the session recording module
|
|
4698
4656
|
[SESSION_RECORDING_IS_SAMPLED]: this.sessionId
|
|
@@ -4707,6 +4665,9 @@ class LazyLoadedSessionRecording {
|
|
|
4707
4665
|
* instead call `posthog.startSessionRecording({trigger: 'url' | 'event'})`
|
|
4708
4666
|
* */
|
|
4709
4667
|
overrideTrigger(triggerType) {
|
|
4668
|
+
if (!this.isStarted || !this._recording || !this._isFullyReady) {
|
|
4669
|
+
return;
|
|
4670
|
+
}
|
|
4710
4671
|
this._activateTrigger(triggerType);
|
|
4711
4672
|
}
|
|
4712
4673
|
_clearFlushBufferTimer() {
|
|
@@ -4839,6 +4800,9 @@ class LazyLoadedSessionRecording {
|
|
|
4839
4800
|
return this._buffer;
|
|
4840
4801
|
}
|
|
4841
4802
|
_reportStarted(startReason, tagPayload) {
|
|
4803
|
+
if (!this.isStarted || !this._recording) {
|
|
4804
|
+
return;
|
|
4805
|
+
}
|
|
4842
4806
|
this._instance.registerForSession({
|
|
4843
4807
|
$session_recording_start_reason: startReason
|
|
4844
4808
|
});
|
|
@@ -4982,7 +4946,7 @@ class LazyLoadedSessionRecording {
|
|
|
4982
4946
|
};
|
|
4983
4947
|
}
|
|
4984
4948
|
async _startRecorder() {
|
|
4985
|
-
if (this.
|
|
4949
|
+
if (this._permanentlyDisabled || this._recording) {
|
|
4986
4950
|
return;
|
|
4987
4951
|
}
|
|
4988
4952
|
// rrweb config info: https://github.com/rrweb-io/rrweb/blob/7d5d0033258d6c29599fb08412202d9a2c7b9413/src/record/index.ts#L28
|
|
@@ -5044,27 +5008,18 @@ class LazyLoadedSessionRecording {
|
|
|
5044
5008
|
rrwebRecord = loaded ?? undefined;
|
|
5045
5009
|
}
|
|
5046
5010
|
if (!rrwebRecord) {
|
|
5047
|
-
|
|
5011
|
+
this._disablePermanently('rrweb record function unavailable');
|
|
5048
5012
|
return;
|
|
5049
5013
|
}
|
|
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
5014
|
const activePlugins = this._gatherRRWebPlugins();
|
|
5015
|
+
let stopHandler;
|
|
5062
5016
|
try {
|
|
5063
|
-
|
|
5017
|
+
stopHandler = rrwebRecord({
|
|
5064
5018
|
emit: event => {
|
|
5065
5019
|
try {
|
|
5066
5020
|
this.onRRwebEmit(event);
|
|
5067
5021
|
} catch (e) {
|
|
5022
|
+
// never throw from rrweb emit handler
|
|
5068
5023
|
logger.error('error in rrweb emit handler', e);
|
|
5069
5024
|
}
|
|
5070
5025
|
},
|
|
@@ -5072,10 +5027,30 @@ class LazyLoadedSessionRecording {
|
|
|
5072
5027
|
...sessionRecordingOptions
|
|
5073
5028
|
});
|
|
5074
5029
|
} catch (e) {
|
|
5075
|
-
|
|
5076
|
-
|
|
5030
|
+
this._disablePermanently('rrweb recorder threw during initialization', e);
|
|
5031
|
+
return;
|
|
5032
|
+
}
|
|
5033
|
+
if (typeof stopHandler !== 'function') {
|
|
5034
|
+
this._disablePermanently('rrweb recorder returned an invalid stop handler');
|
|
5077
5035
|
return;
|
|
5078
5036
|
}
|
|
5037
|
+
// Mark replay started only after rrweb has successfully returned a valid stop handler.
|
|
5038
|
+
this._recording = {
|
|
5039
|
+
stop: stopHandler
|
|
5040
|
+
};
|
|
5041
|
+
this._stopRrweb = stopHandler;
|
|
5042
|
+
// Only create mutation throttler once replay is truly started.
|
|
5043
|
+
this._mutationThrottler = this._mutationThrottler ?? new MutationThrottler(rrwebRecord, {
|
|
5044
|
+
refillRate: this._instance.config.session_recording?.__mutationThrottlerRefillRate,
|
|
5045
|
+
bucketSize: this._instance.config.session_recording?.__mutationThrottlerBucketSize,
|
|
5046
|
+
onBlockedNode: (id, node) => {
|
|
5047
|
+
const message = `Too many mutations on node '${id}'. Rate limiting. This could be due to SVG animations or something similar`;
|
|
5048
|
+
logger.info(message, {
|
|
5049
|
+
node: node
|
|
5050
|
+
});
|
|
5051
|
+
this.log(LOGGER_PREFIX$1 + ' ' + message, 'warn');
|
|
5052
|
+
}
|
|
5053
|
+
});
|
|
5079
5054
|
// We reset the last activity timestamp, resetting the idle timer
|
|
5080
5055
|
this._lastActivityTimestamp = Date.now();
|
|
5081
5056
|
// stay unknown if we're not sure if we're idle or not
|
|
@@ -5105,10 +5080,6 @@ class SessionRecording {
|
|
|
5105
5080
|
get started() {
|
|
5106
5081
|
return !!this._lazyLoadedSessionRecording?.isStarted;
|
|
5107
5082
|
}
|
|
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
5083
|
get status() {
|
|
5113
5084
|
if (this._lazyLoadedSessionRecording) {
|
|
5114
5085
|
return this._lazyLoadedSessionRecording.status;
|
|
@@ -5122,7 +5093,6 @@ class SessionRecording {
|
|
|
5122
5093
|
this._instance = _instance;
|
|
5123
5094
|
this._forceAllowLocalhostNetworkCapture = false;
|
|
5124
5095
|
this._receivedFlags = false;
|
|
5125
|
-
this._serverRecordingEnabled = false;
|
|
5126
5096
|
this._persistFlagsOnSessionListener = undefined;
|
|
5127
5097
|
if (!this._instance.sessionManager) {
|
|
5128
5098
|
log.error('started without valid sessionManager');
|
|
@@ -5145,26 +5115,14 @@ class SessionRecording {
|
|
|
5145
5115
|
const canRunReplay = !isUndefined(Object.assign) && !isUndefined(Array.from);
|
|
5146
5116
|
if (this._isRecordingEnabled && canRunReplay) {
|
|
5147
5117
|
this._lazyLoadAndStart(startReason);
|
|
5148
|
-
log.info('starting');
|
|
5149
5118
|
} else {
|
|
5150
5119
|
this.stopRecording();
|
|
5151
5120
|
}
|
|
5152
5121
|
}
|
|
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
5122
|
_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
5123
|
if (!this._isRecordingEnabled) {
|
|
5164
5124
|
return;
|
|
5165
5125
|
}
|
|
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
5126
|
const loader = assignableWindow.__PosthogExtensions__?.loadExternalDependency;
|
|
5169
5127
|
if (typeof loader === 'function') {
|
|
5170
5128
|
loader(this._instance, this._scriptName, err => {
|
|
@@ -5219,16 +5177,10 @@ class SessionRecording {
|
|
|
5219
5177
|
});
|
|
5220
5178
|
};
|
|
5221
5179
|
persistResponse();
|
|
5222
|
-
// in case we see multiple flags responses, we should only use the response from the most recent one
|
|
5223
5180
|
this._persistFlagsOnSessionListener?.();
|
|
5224
|
-
// we 100% know there is a session manager by this point
|
|
5225
5181
|
this._persistFlagsOnSessionListener = this._instance.sessionManager?.onSessionId(persistResponse);
|
|
5226
5182
|
}
|
|
5227
5183
|
}
|
|
5228
|
-
_clearRemoteConfig() {
|
|
5229
|
-
this._instance.persistence?.unregister(SESSION_RECORDING_REMOTE_CONFIG);
|
|
5230
|
-
this._resetSampling();
|
|
5231
|
-
}
|
|
5232
5184
|
onRemoteConfig(response) {
|
|
5233
5185
|
if (!('sessionRecording' in response)) {
|
|
5234
5186
|
// if sessionRecording is not in the response, we do nothing
|
|
@@ -5237,12 +5189,8 @@ class SessionRecording {
|
|
|
5237
5189
|
}
|
|
5238
5190
|
this._receivedFlags = true;
|
|
5239
5191
|
if (response.sessionRecording === false) {
|
|
5240
|
-
this._serverRecordingEnabled = false;
|
|
5241
|
-
this._clearRemoteConfig();
|
|
5242
|
-
this.stopRecording();
|
|
5243
5192
|
return;
|
|
5244
5193
|
}
|
|
5245
|
-
this._serverRecordingEnabled = true;
|
|
5246
5194
|
this._persistRemoteConfig(response);
|
|
5247
5195
|
this.startIfEnabledOrStop();
|
|
5248
5196
|
}
|
|
@@ -5303,54 +5251,23 @@ class SessionRecording {
|
|
|
5303
5251
|
onRRwebEmit(rawEvent) {
|
|
5304
5252
|
this._lazyLoadedSessionRecording?.onRRwebEmit?.(rawEvent);
|
|
5305
5253
|
}
|
|
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
5254
|
overrideLinkedFlag() {
|
|
5313
5255
|
// TODO what if this gets called before lazy loading is done
|
|
5314
5256
|
this._lazyLoadedSessionRecording?.overrideLinkedFlag();
|
|
5315
5257
|
}
|
|
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
5258
|
overrideSampling() {
|
|
5323
5259
|
// TODO what if this gets called before lazy loading is done
|
|
5324
5260
|
this._lazyLoadedSessionRecording?.overrideSampling();
|
|
5325
5261
|
}
|
|
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
5262
|
overrideTrigger(triggerType) {
|
|
5333
5263
|
// TODO what if this gets called before lazy loading is done
|
|
5334
5264
|
this._lazyLoadedSessionRecording?.overrideTrigger(triggerType);
|
|
5335
5265
|
}
|
|
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
5266
|
get sdkDebugProperties() {
|
|
5342
5267
|
return this._lazyLoadedSessionRecording?.sdkDebugProperties || {
|
|
5343
5268
|
$recording_status: this.status
|
|
5344
5269
|
};
|
|
5345
5270
|
}
|
|
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
5271
|
tryAddCustomEvent(tag, payload) {
|
|
5355
5272
|
return !!this._lazyLoadedSessionRecording?.tryAddCustomEvent(tag, payload);
|
|
5356
5273
|
}
|