@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.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: any;
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): any;
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.1.5";
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: 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 record;
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
- const rrwebRecord = getRRWebRecord();
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
- this._stopRrweb = rrwebRecord({
4854
- emit: event => {
4855
- this.onRRwebEmit(event);
4856
- },
4857
- plugins: activePlugins,
4858
- ...sessionRecordingOptions
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
- this._lazyLoadedSessionRecording.start(startReason);
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
- addEventListener(document, 'DOMContentLoaded', () => {
5167
- this.loadRemoteConfig();
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 (!this.isRemoteConfigLoaded) {
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();