@leanbase-giangnd/js 0.1.5 → 0.2.3
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 +270 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.mjs +270 -23
- package/dist/index.mjs.map +1 -1
- package/dist/leanbase.iife.js +5418 -92
- package/dist/leanbase.iife.js.map +1 -1
- package/package.json +1 -1
- package/src/extensions/replay/extension-shim.ts +102 -3
- package/src/extensions/replay/external/lazy-loaded-session-recorder.ts +66 -13
- package/src/extensions/replay/session-recording.ts +59 -2
- package/src/leanbase.ts +68 -6
- package/src/utils/index.ts +3 -0
- package/src/version.ts +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1406,6 +1406,7 @@ declare class SessionRecording$1 {
|
|
|
1406
1406
|
private _clearRemoteConfig;
|
|
1407
1407
|
onRemoteConfig(response: RemoteConfig$1): void;
|
|
1408
1408
|
log(message: string, level?: 'log' | 'warn' | 'error'): void;
|
|
1409
|
+
private get _scriptName();
|
|
1409
1410
|
private _onScriptLoaded;
|
|
1410
1411
|
/**
|
|
1411
1412
|
* this is maintained on the public API only because it has always been on the public API
|
|
@@ -3824,7 +3825,7 @@ declare class SessionIdManager {
|
|
|
3824
3825
|
changeReason: {
|
|
3825
3826
|
noSessionId: boolean;
|
|
3826
3827
|
activityTimeout: boolean;
|
|
3827
|
-
sessionPastMaximumLength:
|
|
3828
|
+
sessionPastMaximumLength: boolean;
|
|
3828
3829
|
} | undefined;
|
|
3829
3830
|
lastActivityTimestamp: number;
|
|
3830
3831
|
};
|
|
@@ -4292,7 +4293,7 @@ declare class PostHogSurveys {
|
|
|
4292
4293
|
private _isInitializingSurveys;
|
|
4293
4294
|
private _surveyCallbacks;
|
|
4294
4295
|
constructor(_instance: PostHog);
|
|
4295
|
-
onRemoteConfig(response: RemoteConfig):
|
|
4296
|
+
onRemoteConfig(response: RemoteConfig): void;
|
|
4296
4297
|
reset(): void;
|
|
4297
4298
|
loadIfEnabled(): void;
|
|
4298
4299
|
/** Helper to finalize survey initialization */
|
|
@@ -6126,6 +6127,10 @@ declare class Leanbase extends PostHogCore {
|
|
|
6126
6127
|
consent: ConsentManager;
|
|
6127
6128
|
sessionRecording?: SessionRecording$1;
|
|
6128
6129
|
isRemoteConfigLoaded?: boolean;
|
|
6130
|
+
private _remoteConfigLoadAttempted;
|
|
6131
|
+
private _remoteConfigResolved;
|
|
6132
|
+
private _featureFlagsResolved;
|
|
6133
|
+
private _maybeStartedSessionRecording;
|
|
6129
6134
|
personProcessingSetOncePropertiesSent: boolean;
|
|
6130
6135
|
isLoaded: boolean;
|
|
6131
6136
|
initialPageviewCaptured: boolean;
|
|
@@ -6136,6 +6141,7 @@ declare class Leanbase extends PostHogCore {
|
|
|
6136
6141
|
capturePageLeave(): void;
|
|
6137
6142
|
loadRemoteConfig(): Promise<void>;
|
|
6138
6143
|
onRemoteConfig(config: RemoteConfig$1): void;
|
|
6144
|
+
private _maybeStartSessionRecording;
|
|
6139
6145
|
fetch(url: string, options: PostHogFetchOptions): Promise<PostHogFetchResponse>;
|
|
6140
6146
|
setConfig(config: Partial<LeanbaseConfig>): void;
|
|
6141
6147
|
getLibraryId(): string;
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { isArray, isNullish, isFormData, hasOwnProperty, isString, isNull, isNumber, PostHogPersistedProperty, isUndefined, isFunction, includes, stripLeadingDollar, isObject, isEmptyObject, trim, isBoolean, clampToRange, BucketedRateLimiter, PostHogCore, getFetch, isEmptyString } from '@posthog/core';
|
|
2
|
-
import { record } from '@rrweb/record';
|
|
3
2
|
import { strFromU8, gzipSync, strToU8 } from 'fflate';
|
|
4
3
|
|
|
5
4
|
const breaker = {};
|
|
@@ -15,6 +14,8 @@ global?.fetch;
|
|
|
15
14
|
global?.XMLHttpRequest && 'withCredentials' in new global.XMLHttpRequest() ? global.XMLHttpRequest : undefined;
|
|
16
15
|
global?.AbortController;
|
|
17
16
|
const userAgent = navigator$1?.userAgent;
|
|
17
|
+
// assignableWindow mirrors browser package's assignableWindow for extension loading shims
|
|
18
|
+
const assignableWindow = win ?? {};
|
|
18
19
|
function eachArray(obj, iterator, thisArg) {
|
|
19
20
|
if (isArray(obj)) {
|
|
20
21
|
if (nativeForEach && obj.forEach === nativeForEach) {
|
|
@@ -1168,7 +1169,7 @@ const detectDeviceType = function (user_agent) {
|
|
|
1168
1169
|
}
|
|
1169
1170
|
};
|
|
1170
1171
|
|
|
1171
|
-
var version = "0.
|
|
1172
|
+
var version = "0.2.3";
|
|
1172
1173
|
var packageInfo = {
|
|
1173
1174
|
version: version};
|
|
1174
1175
|
|
|
@@ -3106,12 +3107,101 @@ const isLikelyBot = function (navigator, customBlockedUserAgents) {
|
|
|
3106
3107
|
// It would be very bad if posthog-js caused a permission prompt to appear on every page load.
|
|
3107
3108
|
};
|
|
3108
3109
|
|
|
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
|
+
};
|
|
3109
3198
|
// Use a safe global target (prefer `win`, fallback to globalThis)
|
|
3110
3199
|
const _target = win ?? globalThis;
|
|
3111
3200
|
_target.__PosthogExtensions__ = _target.__PosthogExtensions__ || {};
|
|
3112
|
-
// Expose rrweb.record under the same contract
|
|
3201
|
+
// Expose rrweb.record under the same contract. We provide a lazy proxy so
|
|
3202
|
+
// builds that execute this file don't require rrweb at module evaluation time.
|
|
3113
3203
|
_target.__PosthogExtensions__.rrweb = _target.__PosthogExtensions__.rrweb || {
|
|
3114
|
-
record:
|
|
3204
|
+
record: rrwebRecordProxy
|
|
3115
3205
|
};
|
|
3116
3206
|
// Provide initSessionRecording if not present — return a new LazyLoadedSessionRecording when called
|
|
3117
3207
|
_target.__PosthogExtensions__.initSessionRecording = _target.__PosthogExtensions__.initSessionRecording || (instance => {
|
|
@@ -3864,7 +3954,41 @@ function getRRWebRecord() {
|
|
|
3864
3954
|
} catch {
|
|
3865
3955
|
// ignore
|
|
3866
3956
|
}
|
|
3867
|
-
return
|
|
3957
|
+
// If we've previously loaded rrweb via dynamic import, return the cached reference
|
|
3958
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3959
|
+
const cached = getRRWebRecord._cachedRRWebRecord;
|
|
3960
|
+
return cached;
|
|
3961
|
+
}
|
|
3962
|
+
async function loadRRWeb() {
|
|
3963
|
+
try {
|
|
3964
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3965
|
+
const ext = globalThis.__PosthogExtensions__;
|
|
3966
|
+
if (ext && ext.rrweb && ext.rrweb.record) {
|
|
3967
|
+
;
|
|
3968
|
+
getRRWebRecord._cachedRRWebRecord = ext.rrweb.record;
|
|
3969
|
+
return ext.rrweb.record;
|
|
3970
|
+
}
|
|
3971
|
+
// If already cached, return it
|
|
3972
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3973
|
+
const already = getRRWebRecord._cachedRRWebRecord;
|
|
3974
|
+
if (already) {
|
|
3975
|
+
return already;
|
|
3976
|
+
}
|
|
3977
|
+
// Dynamic import - let the bundler (IIFE build) include rrweb in the bundle or allow lazy-load
|
|
3978
|
+
// Note: we intentionally use a dynamic import so rrweb is not referenced at the module top-level
|
|
3979
|
+
// which would cause IIFE builds to assume a global is present at script execution.
|
|
3980
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3981
|
+
const mod = await import('@rrweb/record');
|
|
3982
|
+
const rr = mod && (mod.record ?? (mod.default && mod.default.record));
|
|
3983
|
+
if (rr) {
|
|
3984
|
+
;
|
|
3985
|
+
getRRWebRecord._cachedRRWebRecord = rr;
|
|
3986
|
+
return rr;
|
|
3987
|
+
}
|
|
3988
|
+
} catch (e) {
|
|
3989
|
+
logger.error('could not dynamically load rrweb', e);
|
|
3990
|
+
}
|
|
3991
|
+
return null;
|
|
3868
3992
|
}
|
|
3869
3993
|
function gzipToString(data) {
|
|
3870
3994
|
return strFromU8(gzipSync(strToU8(JSON.stringify(data))), true);
|
|
@@ -4287,7 +4411,7 @@ class LazyLoadedSessionRecording {
|
|
|
4287
4411
|
const parsedConfig = isObject(persistedConfig) ? persistedConfig : JSON.parse(persistedConfig);
|
|
4288
4412
|
return parsedConfig;
|
|
4289
4413
|
}
|
|
4290
|
-
start(startReason) {
|
|
4414
|
+
async start(startReason) {
|
|
4291
4415
|
const config = this._remoteConfig;
|
|
4292
4416
|
if (!config) {
|
|
4293
4417
|
logger.info('remote config must be stored in persistence before recording can start');
|
|
@@ -4321,7 +4445,12 @@ class LazyLoadedSessionRecording {
|
|
|
4321
4445
|
});
|
|
4322
4446
|
});
|
|
4323
4447
|
this._makeSamplingDecision(this.sessionId);
|
|
4324
|
-
this._startRecorder();
|
|
4448
|
+
await this._startRecorder();
|
|
4449
|
+
// If rrweb failed to load/start, do not proceed further.
|
|
4450
|
+
// This prevents installing listeners that assume rrweb is active.
|
|
4451
|
+
if (!this.isStarted) {
|
|
4452
|
+
return;
|
|
4453
|
+
}
|
|
4325
4454
|
// calling addEventListener multiple times is safe and will not add duplicates
|
|
4326
4455
|
addEventListener(win, 'beforeunload', this._onBeforeUnload);
|
|
4327
4456
|
addEventListener(win, 'offline', this._onOffline);
|
|
@@ -4777,7 +4906,7 @@ class LazyLoadedSessionRecording {
|
|
|
4777
4906
|
$sdk_debug_session_start: sessionStartTimestamp
|
|
4778
4907
|
};
|
|
4779
4908
|
}
|
|
4780
|
-
_startRecorder() {
|
|
4909
|
+
async _startRecorder() {
|
|
4781
4910
|
if (this._stopRrweb) {
|
|
4782
4911
|
return;
|
|
4783
4912
|
}
|
|
@@ -4833,7 +4962,12 @@ class LazyLoadedSessionRecording {
|
|
|
4833
4962
|
sessionRecordingOptions.maskTextSelector = this._masking.maskTextSelector ?? undefined;
|
|
4834
4963
|
sessionRecordingOptions.blockSelector = this._masking.blockSelector ?? undefined;
|
|
4835
4964
|
}
|
|
4836
|
-
|
|
4965
|
+
// Ensure rrweb is loaded (either via global extension or dynamic import)
|
|
4966
|
+
let rrwebRecord = getRRWebRecord();
|
|
4967
|
+
if (!rrwebRecord) {
|
|
4968
|
+
const loaded = await loadRRWeb();
|
|
4969
|
+
rrwebRecord = loaded ?? undefined;
|
|
4970
|
+
}
|
|
4837
4971
|
if (!rrwebRecord) {
|
|
4838
4972
|
logger.error('_startRecorder was called but rrwebRecord is not available. This indicates something has gone wrong.');
|
|
4839
4973
|
return;
|
|
@@ -4850,13 +4984,19 @@ class LazyLoadedSessionRecording {
|
|
|
4850
4984
|
}
|
|
4851
4985
|
});
|
|
4852
4986
|
const activePlugins = this._gatherRRWebPlugins();
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4987
|
+
try {
|
|
4988
|
+
this._stopRrweb = rrwebRecord({
|
|
4989
|
+
emit: event => {
|
|
4990
|
+
this.onRRwebEmit(event);
|
|
4991
|
+
},
|
|
4992
|
+
plugins: activePlugins,
|
|
4993
|
+
...sessionRecordingOptions
|
|
4994
|
+
});
|
|
4995
|
+
} catch (e) {
|
|
4996
|
+
logger.error('failed to start rrweb recorder', e);
|
|
4997
|
+
this._stopRrweb = undefined;
|
|
4998
|
+
return;
|
|
4999
|
+
}
|
|
4860
5000
|
// We reset the last activity timestamp, resetting the idle timer
|
|
4861
5001
|
this._lastActivityTimestamp = Date.now();
|
|
4862
5002
|
// stay unknown if we're not sure if we're idle or not
|
|
@@ -4875,6 +5015,7 @@ class LazyLoadedSessionRecording {
|
|
|
4875
5015
|
}
|
|
4876
5016
|
}
|
|
4877
5017
|
|
|
5018
|
+
/* eslint-disable posthog-js/no-direct-function-check */
|
|
4878
5019
|
const LOGGER_PREFIX = '[SessionRecording]';
|
|
4879
5020
|
const log = {
|
|
4880
5021
|
info: (...args) => logger$2.info(LOGGER_PREFIX, ...args),
|
|
@@ -4943,6 +5084,18 @@ class SessionRecording {
|
|
|
4943
5084
|
if (!this._isRecordingEnabled) {
|
|
4944
5085
|
return;
|
|
4945
5086
|
}
|
|
5087
|
+
// If extensions provide a loader, use it. Otherwise fallback to the local _onScriptLoaded which
|
|
5088
|
+
// will create the local LazyLoadedSessionRecording (so tests that mock it work correctly).
|
|
5089
|
+
const loader = assignableWindow.__PosthogExtensions__?.loadExternalDependency;
|
|
5090
|
+
if (typeof loader === 'function') {
|
|
5091
|
+
loader(this._instance, this._scriptName, err => {
|
|
5092
|
+
if (err) {
|
|
5093
|
+
return log.error('could not load recorder', err);
|
|
5094
|
+
}
|
|
5095
|
+
this._onScriptLoaded(startReason);
|
|
5096
|
+
});
|
|
5097
|
+
return;
|
|
5098
|
+
}
|
|
4946
5099
|
this._onScriptLoaded(startReason);
|
|
4947
5100
|
}
|
|
4948
5101
|
stopRecording() {
|
|
@@ -5021,12 +5174,47 @@ class SessionRecording {
|
|
|
5021
5174
|
logger$2.warn('log called before recorder was ready');
|
|
5022
5175
|
}
|
|
5023
5176
|
}
|
|
5177
|
+
get _scriptName() {
|
|
5178
|
+
const remoteConfig = this._instance?.persistence?.get_property(SESSION_RECORDING_REMOTE_CONFIG);
|
|
5179
|
+
return remoteConfig?.scriptConfig?.script || 'lazy-recorder';
|
|
5180
|
+
}
|
|
5024
5181
|
_onScriptLoaded(startReason) {
|
|
5182
|
+
// If extensions provide an init function, use it. Otherwise, fall back to the local LazyLoadedSessionRecording
|
|
5183
|
+
if (assignableWindow.__PosthogExtensions__?.initSessionRecording) {
|
|
5184
|
+
if (!this._lazyLoadedSessionRecording) {
|
|
5185
|
+
const maybeRecording = assignableWindow.__PosthogExtensions__?.initSessionRecording(this._instance);
|
|
5186
|
+
if (maybeRecording && typeof maybeRecording.start === 'function') {
|
|
5187
|
+
this._lazyLoadedSessionRecording = maybeRecording;
|
|
5188
|
+
this._lazyLoadedSessionRecording._forceAllowLocalhostNetworkCapture = this._forceAllowLocalhostNetworkCapture;
|
|
5189
|
+
} else {
|
|
5190
|
+
log.warn('initSessionRecording was present but did not return a recorder instance; falling back to local recorder');
|
|
5191
|
+
}
|
|
5192
|
+
}
|
|
5193
|
+
if (this._lazyLoadedSessionRecording) {
|
|
5194
|
+
try {
|
|
5195
|
+
const maybePromise = this._lazyLoadedSessionRecording.start(startReason);
|
|
5196
|
+
if (maybePromise && typeof maybePromise.catch === 'function') {
|
|
5197
|
+
maybePromise.catch(e => logger$2.error('error starting session recording', e));
|
|
5198
|
+
}
|
|
5199
|
+
} catch (e) {
|
|
5200
|
+
logger$2.error('error starting session recording', e);
|
|
5201
|
+
}
|
|
5202
|
+
return;
|
|
5203
|
+
}
|
|
5204
|
+
}
|
|
5025
5205
|
if (!this._lazyLoadedSessionRecording) {
|
|
5026
5206
|
this._lazyLoadedSessionRecording = new LazyLoadedSessionRecording(this._instance);
|
|
5027
5207
|
this._lazyLoadedSessionRecording._forceAllowLocalhostNetworkCapture = this._forceAllowLocalhostNetworkCapture;
|
|
5028
5208
|
}
|
|
5029
|
-
|
|
5209
|
+
// start may perform a dynamic import; handle both sync and Promise returns
|
|
5210
|
+
try {
|
|
5211
|
+
const maybePromise = this._lazyLoadedSessionRecording.start(startReason);
|
|
5212
|
+
if (maybePromise && typeof maybePromise.catch === 'function') {
|
|
5213
|
+
maybePromise.catch(e => logger$2.error('error starting session recording', e));
|
|
5214
|
+
}
|
|
5215
|
+
} catch (e) {
|
|
5216
|
+
logger$2.error('error starting session recording', e);
|
|
5217
|
+
}
|
|
5030
5218
|
}
|
|
5031
5219
|
/**
|
|
5032
5220
|
* this is maintained on the public API only because it has always been on the public API
|
|
@@ -5127,6 +5315,10 @@ class Leanbase extends PostHogCore {
|
|
|
5127
5315
|
token
|
|
5128
5316
|
});
|
|
5129
5317
|
super(token, mergedConfig);
|
|
5318
|
+
this._remoteConfigLoadAttempted = false;
|
|
5319
|
+
this._remoteConfigResolved = false;
|
|
5320
|
+
this._featureFlagsResolved = false;
|
|
5321
|
+
this._maybeStartedSessionRecording = false;
|
|
5130
5322
|
this.personProcessingSetOncePropertiesSent = false;
|
|
5131
5323
|
this.isLoaded = false;
|
|
5132
5324
|
this.config = mergedConfig;
|
|
@@ -5150,10 +5342,20 @@ class Leanbase extends PostHogCore {
|
|
|
5150
5342
|
this.replayAutocapture.startIfEnabled();
|
|
5151
5343
|
if (this.sessionManager && this.config.cookieless_mode !== 'always') {
|
|
5152
5344
|
this.sessionRecording = new SessionRecording(this);
|
|
5153
|
-
this.sessionRecording.startIfEnabledOrStop();
|
|
5154
5345
|
}
|
|
5346
|
+
// Start session recording only once flags + remote config have been resolved.
|
|
5347
|
+
// This matches the PostHog browser SDK where replay activation is driven by remote config and flags.
|
|
5155
5348
|
if (this.config.preloadFeatureFlags !== false) {
|
|
5156
|
-
this.reloadFeatureFlags(
|
|
5349
|
+
this.reloadFeatureFlags({
|
|
5350
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
5351
|
+
cb: _err => {
|
|
5352
|
+
this._featureFlagsResolved = true;
|
|
5353
|
+
this._maybeStartSessionRecording();
|
|
5354
|
+
}
|
|
5355
|
+
});
|
|
5356
|
+
} else {
|
|
5357
|
+
// If feature flags preload is explicitly disabled, treat this requirement as satisfied.
|
|
5358
|
+
this._featureFlagsResolved = true;
|
|
5157
5359
|
}
|
|
5158
5360
|
this.config.loaded?.(this);
|
|
5159
5361
|
if (this.config.capture_pageview) {
|
|
@@ -5163,9 +5365,26 @@ class Leanbase extends PostHogCore {
|
|
|
5163
5365
|
}
|
|
5164
5366
|
}, 1);
|
|
5165
5367
|
}
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5368
|
+
const triggerRemoteConfigLoad = reason => {
|
|
5369
|
+
logger$2.info(`remote config load triggered via ${reason}`);
|
|
5370
|
+
void this.loadRemoteConfig();
|
|
5371
|
+
};
|
|
5372
|
+
if (document) {
|
|
5373
|
+
if (document.readyState === 'loading') {
|
|
5374
|
+
logger$2.info('remote config load deferred until DOMContentLoaded');
|
|
5375
|
+
const onDomReady = () => {
|
|
5376
|
+
document?.removeEventListener('DOMContentLoaded', onDomReady);
|
|
5377
|
+
triggerRemoteConfigLoad('dom');
|
|
5378
|
+
};
|
|
5379
|
+
addEventListener(document, 'DOMContentLoaded', onDomReady, {
|
|
5380
|
+
once: true
|
|
5381
|
+
});
|
|
5382
|
+
} else {
|
|
5383
|
+
triggerRemoteConfigLoad('immediate');
|
|
5384
|
+
}
|
|
5385
|
+
} else {
|
|
5386
|
+
triggerRemoteConfigLoad('no-document');
|
|
5387
|
+
}
|
|
5169
5388
|
addEventListener(window, 'onpagehide' in self ? 'pagehide' : 'unload', this.capturePageLeave.bind(this), {
|
|
5170
5389
|
passive: false
|
|
5171
5390
|
});
|
|
@@ -5202,11 +5421,19 @@ class Leanbase extends PostHogCore {
|
|
|
5202
5421
|
}
|
|
5203
5422
|
}
|
|
5204
5423
|
async loadRemoteConfig() {
|
|
5205
|
-
if (
|
|
5424
|
+
if (this._remoteConfigLoadAttempted) {
|
|
5425
|
+
return;
|
|
5426
|
+
}
|
|
5427
|
+
this._remoteConfigLoadAttempted = true;
|
|
5428
|
+
try {
|
|
5206
5429
|
const remoteConfig = await this.reloadRemoteConfigAsync();
|
|
5207
5430
|
if (remoteConfig) {
|
|
5208
5431
|
this.onRemoteConfig(remoteConfig);
|
|
5209
5432
|
}
|
|
5433
|
+
} finally {
|
|
5434
|
+
// Regardless of success/failure, we consider remote config "resolved" so replay isn't blocked forever.
|
|
5435
|
+
this._remoteConfigResolved = true;
|
|
5436
|
+
this._maybeStartSessionRecording();
|
|
5210
5437
|
}
|
|
5211
5438
|
}
|
|
5212
5439
|
onRemoteConfig(config) {
|
|
@@ -5219,6 +5446,26 @@ class Leanbase extends PostHogCore {
|
|
|
5219
5446
|
this.isRemoteConfigLoaded = true;
|
|
5220
5447
|
this.replayAutocapture?.onRemoteConfig(config);
|
|
5221
5448
|
this.sessionRecording?.onRemoteConfig(config);
|
|
5449
|
+
// Remote config has been applied; allow replay start if flags are also ready.
|
|
5450
|
+
this._remoteConfigResolved = true;
|
|
5451
|
+
this._maybeStartSessionRecording();
|
|
5452
|
+
}
|
|
5453
|
+
_maybeStartSessionRecording() {
|
|
5454
|
+
if (this._maybeStartedSessionRecording) {
|
|
5455
|
+
return;
|
|
5456
|
+
}
|
|
5457
|
+
if (!this.sessionRecording) {
|
|
5458
|
+
return;
|
|
5459
|
+
}
|
|
5460
|
+
if (!this._featureFlagsResolved || !this._remoteConfigResolved) {
|
|
5461
|
+
return;
|
|
5462
|
+
}
|
|
5463
|
+
this._maybeStartedSessionRecording = true;
|
|
5464
|
+
try {
|
|
5465
|
+
this.sessionRecording.startIfEnabledOrStop();
|
|
5466
|
+
} catch (e) {
|
|
5467
|
+
logger$2.error('Failed to start session recording', e);
|
|
5468
|
+
}
|
|
5222
5469
|
}
|
|
5223
5470
|
fetch(url, options) {
|
|
5224
5471
|
const fetchFn = getFetch();
|