@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 CHANGED
@@ -1,7 +1,6 @@
1
1
  'use strict';
2
2
 
3
3
  var core = require('@posthog/core');
4
- var record = require('@rrweb/record');
5
4
  var fflate = require('fflate');
6
5
 
7
6
  const breaker = {};
@@ -17,6 +16,8 @@ global?.fetch;
17
16
  global?.XMLHttpRequest && 'withCredentials' in new global.XMLHttpRequest() ? global.XMLHttpRequest : undefined;
18
17
  global?.AbortController;
19
18
  const userAgent = navigator$1?.userAgent;
19
+ // assignableWindow mirrors browser package's assignableWindow for extension loading shims
20
+ const assignableWindow = win ?? {};
20
21
  function eachArray(obj, iterator, thisArg) {
21
22
  if (core.isArray(obj)) {
22
23
  if (nativeForEach && obj.forEach === nativeForEach) {
@@ -1170,7 +1171,7 @@ const detectDeviceType = function (user_agent) {
1170
1171
  }
1171
1172
  };
1172
1173
 
1173
- var version = "0.1.5";
1174
+ var version = "0.2.3";
1174
1175
  var packageInfo = {
1175
1176
  version: version};
1176
1177
 
@@ -3108,12 +3109,101 @@ const isLikelyBot = function (navigator, customBlockedUserAgents) {
3108
3109
  // It would be very bad if posthog-js caused a permission prompt to appear on every page load.
3109
3110
  };
3110
3111
 
3112
+ /* eslint-disable @typescript-eslint/no-unused-vars */
3113
+ // We avoid importing '@rrweb/record' at module load time to prevent IIFE builds
3114
+ // from requiring a top-level global. Instead, expose a lazy proxy that will
3115
+ // dynamically import the module the first time it's used.
3116
+ let _cachedRRWeb = null;
3117
+ async function _loadRRWebModule() {
3118
+ if (_cachedRRWeb) return _cachedRRWeb;
3119
+ try {
3120
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3121
+ const mod = await import('@rrweb/record');
3122
+ _cachedRRWeb = mod;
3123
+ return _cachedRRWeb;
3124
+ } catch (e) {
3125
+ return null;
3126
+ }
3127
+ }
3128
+ // queue for method calls before rrweb loads
3129
+ const _queuedCalls = [];
3130
+ // Create a proxy function that delegates to the real rrweb.record when called
3131
+ const rrwebRecordProxy = function (...args) {
3132
+ let realStop;
3133
+ let calledReal = false;
3134
+ // Start loading asynchronously and call the real record when available
3135
+ void (async () => {
3136
+ const mod = await _loadRRWebModule();
3137
+ const real = mod && (mod.record ?? mod.default?.record);
3138
+ if (real) {
3139
+ try {
3140
+ calledReal = true;
3141
+ realStop = real(...args);
3142
+ // flush any queued calls that were waiting for rrweb
3143
+ while (_queuedCalls.length) {
3144
+ try {
3145
+ const fn = _queuedCalls.shift();
3146
+ fn();
3147
+ } catch (e) {
3148
+ // ignore
3149
+ }
3150
+ }
3151
+ } catch (e) {
3152
+ // ignore
3153
+ }
3154
+ }
3155
+ })();
3156
+ // return a stop function that will call the real stop when available
3157
+ return () => {
3158
+ if (realStop) {
3159
+ try {
3160
+ realStop();
3161
+ } catch (e) {
3162
+ // ignore
3163
+ }
3164
+ } else if (!calledReal) {
3165
+ // If rrweb hasn't been initialised yet, queue a stop request that will
3166
+ // call the real stop once available.
3167
+ _queuedCalls.push(() => {
3168
+ try {
3169
+ realStop?.();
3170
+ } catch (e) {
3171
+ // ignore
3172
+ }
3173
+ });
3174
+ }
3175
+ };
3176
+ };
3177
+ // methods that can be called on the rrweb.record object - queue until real module is available
3178
+ rrwebRecordProxy.addCustomEvent = function (tag, payload) {
3179
+ const call = () => {
3180
+ try {
3181
+ const real = _cachedRRWeb && (_cachedRRWeb.record ?? _cachedRRWeb.default?.record);
3182
+ real?.addCustomEvent?.(tag, payload);
3183
+ } catch (e) {
3184
+ // ignore
3185
+ }
3186
+ };
3187
+ if (_cachedRRWeb) call();else _queuedCalls.push(call);
3188
+ };
3189
+ rrwebRecordProxy.takeFullSnapshot = function () {
3190
+ const call = () => {
3191
+ try {
3192
+ const real = _cachedRRWeb && (_cachedRRWeb.record ?? _cachedRRWeb.default?.record);
3193
+ real?.takeFullSnapshot?.();
3194
+ } catch (e) {
3195
+ // ignore
3196
+ }
3197
+ };
3198
+ if (_cachedRRWeb) call();else _queuedCalls.push(call);
3199
+ };
3111
3200
  // Use a safe global target (prefer `win`, fallback to globalThis)
3112
3201
  const _target = win ?? globalThis;
3113
3202
  _target.__PosthogExtensions__ = _target.__PosthogExtensions__ || {};
3114
- // Expose rrweb.record under the same contract
3203
+ // Expose rrweb.record under the same contract. We provide a lazy proxy so
3204
+ // builds that execute this file don't require rrweb at module evaluation time.
3115
3205
  _target.__PosthogExtensions__.rrweb = _target.__PosthogExtensions__.rrweb || {
3116
- record: record.record
3206
+ record: rrwebRecordProxy
3117
3207
  };
3118
3208
  // Provide initSessionRecording if not present — return a new LazyLoadedSessionRecording when called
3119
3209
  _target.__PosthogExtensions__.initSessionRecording = _target.__PosthogExtensions__.initSessionRecording || (instance => {
@@ -3866,7 +3956,41 @@ function getRRWebRecord() {
3866
3956
  } catch {
3867
3957
  // ignore
3868
3958
  }
3869
- return record.record;
3959
+ // If we've previously loaded rrweb via dynamic import, return the cached reference
3960
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3961
+ const cached = getRRWebRecord._cachedRRWebRecord;
3962
+ return cached;
3963
+ }
3964
+ async function loadRRWeb() {
3965
+ try {
3966
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3967
+ const ext = globalThis.__PosthogExtensions__;
3968
+ if (ext && ext.rrweb && ext.rrweb.record) {
3969
+ ;
3970
+ getRRWebRecord._cachedRRWebRecord = ext.rrweb.record;
3971
+ return ext.rrweb.record;
3972
+ }
3973
+ // If already cached, return it
3974
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3975
+ const already = getRRWebRecord._cachedRRWebRecord;
3976
+ if (already) {
3977
+ return already;
3978
+ }
3979
+ // Dynamic import - let the bundler (IIFE build) include rrweb in the bundle or allow lazy-load
3980
+ // Note: we intentionally use a dynamic import so rrweb is not referenced at the module top-level
3981
+ // which would cause IIFE builds to assume a global is present at script execution.
3982
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3983
+ const mod = await import('@rrweb/record');
3984
+ const rr = mod && (mod.record ?? (mod.default && mod.default.record));
3985
+ if (rr) {
3986
+ ;
3987
+ getRRWebRecord._cachedRRWebRecord = rr;
3988
+ return rr;
3989
+ }
3990
+ } catch (e) {
3991
+ logger.error('could not dynamically load rrweb', e);
3992
+ }
3993
+ return null;
3870
3994
  }
3871
3995
  function gzipToString(data) {
3872
3996
  return fflate.strFromU8(fflate.gzipSync(fflate.strToU8(JSON.stringify(data))), true);
@@ -4289,7 +4413,7 @@ class LazyLoadedSessionRecording {
4289
4413
  const parsedConfig = core.isObject(persistedConfig) ? persistedConfig : JSON.parse(persistedConfig);
4290
4414
  return parsedConfig;
4291
4415
  }
4292
- start(startReason) {
4416
+ async start(startReason) {
4293
4417
  const config = this._remoteConfig;
4294
4418
  if (!config) {
4295
4419
  logger.info('remote config must be stored in persistence before recording can start');
@@ -4323,7 +4447,12 @@ class LazyLoadedSessionRecording {
4323
4447
  });
4324
4448
  });
4325
4449
  this._makeSamplingDecision(this.sessionId);
4326
- this._startRecorder();
4450
+ await this._startRecorder();
4451
+ // If rrweb failed to load/start, do not proceed further.
4452
+ // This prevents installing listeners that assume rrweb is active.
4453
+ if (!this.isStarted) {
4454
+ return;
4455
+ }
4327
4456
  // calling addEventListener multiple times is safe and will not add duplicates
4328
4457
  addEventListener(win, 'beforeunload', this._onBeforeUnload);
4329
4458
  addEventListener(win, 'offline', this._onOffline);
@@ -4779,7 +4908,7 @@ class LazyLoadedSessionRecording {
4779
4908
  $sdk_debug_session_start: sessionStartTimestamp
4780
4909
  };
4781
4910
  }
4782
- _startRecorder() {
4911
+ async _startRecorder() {
4783
4912
  if (this._stopRrweb) {
4784
4913
  return;
4785
4914
  }
@@ -4835,7 +4964,12 @@ class LazyLoadedSessionRecording {
4835
4964
  sessionRecordingOptions.maskTextSelector = this._masking.maskTextSelector ?? undefined;
4836
4965
  sessionRecordingOptions.blockSelector = this._masking.blockSelector ?? undefined;
4837
4966
  }
4838
- const rrwebRecord = getRRWebRecord();
4967
+ // Ensure rrweb is loaded (either via global extension or dynamic import)
4968
+ let rrwebRecord = getRRWebRecord();
4969
+ if (!rrwebRecord) {
4970
+ const loaded = await loadRRWeb();
4971
+ rrwebRecord = loaded ?? undefined;
4972
+ }
4839
4973
  if (!rrwebRecord) {
4840
4974
  logger.error('_startRecorder was called but rrwebRecord is not available. This indicates something has gone wrong.');
4841
4975
  return;
@@ -4852,13 +4986,19 @@ class LazyLoadedSessionRecording {
4852
4986
  }
4853
4987
  });
4854
4988
  const activePlugins = this._gatherRRWebPlugins();
4855
- this._stopRrweb = rrwebRecord({
4856
- emit: event => {
4857
- this.onRRwebEmit(event);
4858
- },
4859
- plugins: activePlugins,
4860
- ...sessionRecordingOptions
4861
- });
4989
+ try {
4990
+ this._stopRrweb = rrwebRecord({
4991
+ emit: event => {
4992
+ this.onRRwebEmit(event);
4993
+ },
4994
+ plugins: activePlugins,
4995
+ ...sessionRecordingOptions
4996
+ });
4997
+ } catch (e) {
4998
+ logger.error('failed to start rrweb recorder', e);
4999
+ this._stopRrweb = undefined;
5000
+ return;
5001
+ }
4862
5002
  // We reset the last activity timestamp, resetting the idle timer
4863
5003
  this._lastActivityTimestamp = Date.now();
4864
5004
  // stay unknown if we're not sure if we're idle or not
@@ -4877,6 +5017,7 @@ class LazyLoadedSessionRecording {
4877
5017
  }
4878
5018
  }
4879
5019
 
5020
+ /* eslint-disable posthog-js/no-direct-function-check */
4880
5021
  const LOGGER_PREFIX = '[SessionRecording]';
4881
5022
  const log = {
4882
5023
  info: (...args) => logger$2.info(LOGGER_PREFIX, ...args),
@@ -4945,6 +5086,18 @@ class SessionRecording {
4945
5086
  if (!this._isRecordingEnabled) {
4946
5087
  return;
4947
5088
  }
5089
+ // If extensions provide a loader, use it. Otherwise fallback to the local _onScriptLoaded which
5090
+ // will create the local LazyLoadedSessionRecording (so tests that mock it work correctly).
5091
+ const loader = assignableWindow.__PosthogExtensions__?.loadExternalDependency;
5092
+ if (typeof loader === 'function') {
5093
+ loader(this._instance, this._scriptName, err => {
5094
+ if (err) {
5095
+ return log.error('could not load recorder', err);
5096
+ }
5097
+ this._onScriptLoaded(startReason);
5098
+ });
5099
+ return;
5100
+ }
4948
5101
  this._onScriptLoaded(startReason);
4949
5102
  }
4950
5103
  stopRecording() {
@@ -5023,12 +5176,47 @@ class SessionRecording {
5023
5176
  logger$2.warn('log called before recorder was ready');
5024
5177
  }
5025
5178
  }
5179
+ get _scriptName() {
5180
+ const remoteConfig = this._instance?.persistence?.get_property(SESSION_RECORDING_REMOTE_CONFIG);
5181
+ return remoteConfig?.scriptConfig?.script || 'lazy-recorder';
5182
+ }
5026
5183
  _onScriptLoaded(startReason) {
5184
+ // If extensions provide an init function, use it. Otherwise, fall back to the local LazyLoadedSessionRecording
5185
+ if (assignableWindow.__PosthogExtensions__?.initSessionRecording) {
5186
+ if (!this._lazyLoadedSessionRecording) {
5187
+ const maybeRecording = assignableWindow.__PosthogExtensions__?.initSessionRecording(this._instance);
5188
+ if (maybeRecording && typeof maybeRecording.start === 'function') {
5189
+ this._lazyLoadedSessionRecording = maybeRecording;
5190
+ this._lazyLoadedSessionRecording._forceAllowLocalhostNetworkCapture = this._forceAllowLocalhostNetworkCapture;
5191
+ } else {
5192
+ log.warn('initSessionRecording was present but did not return a recorder instance; falling back to local recorder');
5193
+ }
5194
+ }
5195
+ if (this._lazyLoadedSessionRecording) {
5196
+ try {
5197
+ const maybePromise = this._lazyLoadedSessionRecording.start(startReason);
5198
+ if (maybePromise && typeof maybePromise.catch === 'function') {
5199
+ maybePromise.catch(e => logger$2.error('error starting session recording', e));
5200
+ }
5201
+ } catch (e) {
5202
+ logger$2.error('error starting session recording', e);
5203
+ }
5204
+ return;
5205
+ }
5206
+ }
5027
5207
  if (!this._lazyLoadedSessionRecording) {
5028
5208
  this._lazyLoadedSessionRecording = new LazyLoadedSessionRecording(this._instance);
5029
5209
  this._lazyLoadedSessionRecording._forceAllowLocalhostNetworkCapture = this._forceAllowLocalhostNetworkCapture;
5030
5210
  }
5031
- this._lazyLoadedSessionRecording.start(startReason);
5211
+ // start may perform a dynamic import; handle both sync and Promise returns
5212
+ try {
5213
+ const maybePromise = this._lazyLoadedSessionRecording.start(startReason);
5214
+ if (maybePromise && typeof maybePromise.catch === 'function') {
5215
+ maybePromise.catch(e => logger$2.error('error starting session recording', e));
5216
+ }
5217
+ } catch (e) {
5218
+ logger$2.error('error starting session recording', e);
5219
+ }
5032
5220
  }
5033
5221
  /**
5034
5222
  * this is maintained on the public API only because it has always been on the public API
@@ -5129,6 +5317,10 @@ class Leanbase extends core.PostHogCore {
5129
5317
  token
5130
5318
  });
5131
5319
  super(token, mergedConfig);
5320
+ this._remoteConfigLoadAttempted = false;
5321
+ this._remoteConfigResolved = false;
5322
+ this._featureFlagsResolved = false;
5323
+ this._maybeStartedSessionRecording = false;
5132
5324
  this.personProcessingSetOncePropertiesSent = false;
5133
5325
  this.isLoaded = false;
5134
5326
  this.config = mergedConfig;
@@ -5152,10 +5344,20 @@ class Leanbase extends core.PostHogCore {
5152
5344
  this.replayAutocapture.startIfEnabled();
5153
5345
  if (this.sessionManager && this.config.cookieless_mode !== 'always') {
5154
5346
  this.sessionRecording = new SessionRecording(this);
5155
- this.sessionRecording.startIfEnabledOrStop();
5156
5347
  }
5348
+ // Start session recording only once flags + remote config have been resolved.
5349
+ // This matches the PostHog browser SDK where replay activation is driven by remote config and flags.
5157
5350
  if (this.config.preloadFeatureFlags !== false) {
5158
- this.reloadFeatureFlags();
5351
+ this.reloadFeatureFlags({
5352
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
5353
+ cb: _err => {
5354
+ this._featureFlagsResolved = true;
5355
+ this._maybeStartSessionRecording();
5356
+ }
5357
+ });
5358
+ } else {
5359
+ // If feature flags preload is explicitly disabled, treat this requirement as satisfied.
5360
+ this._featureFlagsResolved = true;
5159
5361
  }
5160
5362
  this.config.loaded?.(this);
5161
5363
  if (this.config.capture_pageview) {
@@ -5165,9 +5367,26 @@ class Leanbase extends core.PostHogCore {
5165
5367
  }
5166
5368
  }, 1);
5167
5369
  }
5168
- addEventListener(document, 'DOMContentLoaded', () => {
5169
- this.loadRemoteConfig();
5170
- });
5370
+ const triggerRemoteConfigLoad = reason => {
5371
+ logger$2.info(`remote config load triggered via ${reason}`);
5372
+ void this.loadRemoteConfig();
5373
+ };
5374
+ if (document) {
5375
+ if (document.readyState === 'loading') {
5376
+ logger$2.info('remote config load deferred until DOMContentLoaded');
5377
+ const onDomReady = () => {
5378
+ document?.removeEventListener('DOMContentLoaded', onDomReady);
5379
+ triggerRemoteConfigLoad('dom');
5380
+ };
5381
+ addEventListener(document, 'DOMContentLoaded', onDomReady, {
5382
+ once: true
5383
+ });
5384
+ } else {
5385
+ triggerRemoteConfigLoad('immediate');
5386
+ }
5387
+ } else {
5388
+ triggerRemoteConfigLoad('no-document');
5389
+ }
5171
5390
  addEventListener(window, 'onpagehide' in self ? 'pagehide' : 'unload', this.capturePageLeave.bind(this), {
5172
5391
  passive: false
5173
5392
  });
@@ -5204,11 +5423,19 @@ class Leanbase extends core.PostHogCore {
5204
5423
  }
5205
5424
  }
5206
5425
  async loadRemoteConfig() {
5207
- if (!this.isRemoteConfigLoaded) {
5426
+ if (this._remoteConfigLoadAttempted) {
5427
+ return;
5428
+ }
5429
+ this._remoteConfigLoadAttempted = true;
5430
+ try {
5208
5431
  const remoteConfig = await this.reloadRemoteConfigAsync();
5209
5432
  if (remoteConfig) {
5210
5433
  this.onRemoteConfig(remoteConfig);
5211
5434
  }
5435
+ } finally {
5436
+ // Regardless of success/failure, we consider remote config "resolved" so replay isn't blocked forever.
5437
+ this._remoteConfigResolved = true;
5438
+ this._maybeStartSessionRecording();
5212
5439
  }
5213
5440
  }
5214
5441
  onRemoteConfig(config) {
@@ -5221,6 +5448,26 @@ class Leanbase extends core.PostHogCore {
5221
5448
  this.isRemoteConfigLoaded = true;
5222
5449
  this.replayAutocapture?.onRemoteConfig(config);
5223
5450
  this.sessionRecording?.onRemoteConfig(config);
5451
+ // Remote config has been applied; allow replay start if flags are also ready.
5452
+ this._remoteConfigResolved = true;
5453
+ this._maybeStartSessionRecording();
5454
+ }
5455
+ _maybeStartSessionRecording() {
5456
+ if (this._maybeStartedSessionRecording) {
5457
+ return;
5458
+ }
5459
+ if (!this.sessionRecording) {
5460
+ return;
5461
+ }
5462
+ if (!this._featureFlagsResolved || !this._remoteConfigResolved) {
5463
+ return;
5464
+ }
5465
+ this._maybeStartedSessionRecording = true;
5466
+ try {
5467
+ this.sessionRecording.startIfEnabledOrStop();
5468
+ } catch (e) {
5469
+ logger$2.error('Failed to start session recording', e);
5470
+ }
5224
5471
  }
5225
5472
  fetch(url, options) {
5226
5473
  const fetchFn = core.getFetch();