@leanbase-giangnd/js 0.3.1 → 0.4.0

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
@@ -231,7 +231,7 @@ core.PostHogPersistedProperty.Queue, core.PostHogPersistedProperty.FeatureFlagDe
231
231
 
232
232
  /* eslint-disable no-console */
233
233
  const PREFIX = '[Leanbase]';
234
- const logger$2 = {
234
+ const logger$3 = {
235
235
  info: (...args) => {
236
236
  if (typeof console !== 'undefined') {
237
237
  console.log(PREFIX, ...args);
@@ -529,7 +529,7 @@ function chooseCookieDomain(hostname, cross_subdomain) {
529
529
  if (!matchedSubDomain) {
530
530
  const originalMatch = originalCookieDomainFn(hostname);
531
531
  if (originalMatch !== matchedSubDomain) {
532
- logger$2.info('Warning: cookie subdomain discovery mismatch', originalMatch, matchedSubDomain);
532
+ logger$3.info('Warning: cookie subdomain discovery mismatch', originalMatch, matchedSubDomain);
533
533
  }
534
534
  matchedSubDomain = originalMatch;
535
535
  }
@@ -541,7 +541,7 @@ function chooseCookieDomain(hostname, cross_subdomain) {
541
541
  const cookieStore = {
542
542
  _is_supported: () => !!document,
543
543
  _error: function (msg) {
544
- logger$2.error('cookieStore error: ' + msg);
544
+ logger$3.error('cookieStore error: ' + msg);
545
545
  },
546
546
  _get: function (name) {
547
547
  if (!document) {
@@ -590,7 +590,7 @@ const cookieStore = {
590
590
  const new_cookie_val = name + '=' + encodeURIComponent(JSON.stringify(value)) + expires + '; SameSite=Lax; path=/' + cdomain + secure;
591
591
  // 4096 bytes is the size at which some browsers (e.g. firefox) will not store a cookie, warn slightly before that
592
592
  if (new_cookie_val.length > 4096 * 0.9) {
593
- logger$2.warn('cookieStore warning: large cookie, len=' + new_cookie_val.length);
593
+ logger$3.warn('cookieStore warning: large cookie, len=' + new_cookie_val.length);
594
594
  }
595
595
  document.cookie = new_cookie_val;
596
596
  return new_cookie_val;
@@ -632,13 +632,13 @@ const localStore = {
632
632
  supported = false;
633
633
  }
634
634
  if (!supported) {
635
- logger$2.error('localStorage unsupported; falling back to cookie store');
635
+ logger$3.error('localStorage unsupported; falling back to cookie store');
636
636
  }
637
637
  _localStorage_supported = supported;
638
638
  return supported;
639
639
  },
640
640
  _error: function (msg) {
641
- logger$2.error('localStorage error: ' + msg);
641
+ logger$3.error('localStorage error: ' + msg);
642
642
  },
643
643
  _get: function (name) {
644
644
  try {
@@ -724,7 +724,7 @@ const memoryStore = {
724
724
  return true;
725
725
  },
726
726
  _error: function (msg) {
727
- logger$2.error('memoryStorage error: ' + msg);
727
+ logger$3.error('memoryStorage error: ' + msg);
728
728
  },
729
729
  _get: function (name) {
730
730
  return memoryStorage[name] || null;
@@ -765,7 +765,7 @@ const sessionStore = {
765
765
  return sessionStorageSupported;
766
766
  },
767
767
  _error: function (msg) {
768
- logger$2.error('sessionStorage error: ', msg);
768
+ logger$3.error('sessionStorage error: ', msg);
769
769
  },
770
770
  _get: function (name) {
771
771
  try {
@@ -814,6 +814,21 @@ const convertToURL = url => {
814
814
  location.href = url;
815
815
  return location;
816
816
  };
817
+ const formDataToQuery = function (formdata, arg_separator = '&') {
818
+ let use_val;
819
+ let use_key;
820
+ const tph_arr = [];
821
+ each(formdata, function (val, key) {
822
+ // the key might be literally the string undefined for e.g. if {undefined: 'something'}
823
+ if (core.isUndefined(val) || core.isUndefined(key) || key === 'undefined') {
824
+ return;
825
+ }
826
+ use_val = encodeURIComponent(core.isFile(val) ? val.name : val.toString());
827
+ use_key = encodeURIComponent(key);
828
+ tph_arr[tph_arr.length] = use_key + '=' + use_val;
829
+ });
830
+ return tph_arr.join(arg_separator);
831
+ };
817
832
  // NOTE: Once we get rid of IE11/op_mini we can start using URLSearchParams
818
833
  const getQueryParam = function (url, param) {
819
834
  const withoutHash = url.split('#')[0] || '';
@@ -837,7 +852,7 @@ const getQueryParam = function (url, param) {
837
852
  try {
838
853
  result = decodeURIComponent(result);
839
854
  } catch {
840
- logger$2.error('Skipping decoding for malformed query param: ' + result);
855
+ logger$3.error('Skipping decoding for malformed query param: ' + result);
841
856
  }
842
857
  return result.replace(/\+/g, ' ');
843
858
  }
@@ -1171,7 +1186,7 @@ const detectDeviceType = function (user_agent) {
1171
1186
  }
1172
1187
  };
1173
1188
 
1174
- var version = "0.3.1";
1189
+ var version = "0.4.0";
1175
1190
  var packageInfo = {
1176
1191
  version: version};
1177
1192
 
@@ -1436,7 +1451,7 @@ class LeanbasePersistence {
1436
1451
  this._storage = this._buildStorage(config);
1437
1452
  this.load();
1438
1453
  if (config.debug) {
1439
- logger$2.info('Persistence loaded', config['persistence'], {
1454
+ logger$3.info('Persistence loaded', config['persistence'], {
1440
1455
  ...this.props
1441
1456
  });
1442
1457
  }
@@ -1452,7 +1467,7 @@ class LeanbasePersistence {
1452
1467
  }
1453
1468
  _buildStorage(config) {
1454
1469
  if (CASE_INSENSITIVE_PERSISTENCE_TYPES.indexOf(config['persistence'].toLowerCase()) === -1) {
1455
- logger$2.info('Unknown persistence type ' + config['persistence'] + '; falling back to localStorage+cookie');
1470
+ logger$3.info('Unknown persistence type ' + config['persistence'] + '; falling back to localStorage+cookie');
1456
1471
  config['persistence'] = 'localStorage+cookie';
1457
1472
  }
1458
1473
  let store;
@@ -2088,7 +2103,7 @@ function getNestedSpanText(target) {
2088
2103
  text = `${text} ${getNestedSpanText(child)}`.trim();
2089
2104
  }
2090
2105
  } catch (e) {
2091
- logger$2.error('[AutoCapture]', e);
2106
+ logger$3.error('[AutoCapture]', e);
2092
2107
  }
2093
2108
  }
2094
2109
  });
@@ -2390,7 +2405,7 @@ class Autocapture {
2390
2405
  }
2391
2406
  _addDomEventHandlers() {
2392
2407
  if (!this.isBrowserSupported()) {
2393
- logger$2.info('Disabling Automatic Event Collection because this browser is not supported');
2408
+ logger$3.info('Disabling Automatic Event Collection because this browser is not supported');
2394
2409
  return;
2395
2410
  }
2396
2411
  if (!win || !document) {
@@ -2401,7 +2416,7 @@ class Autocapture {
2401
2416
  try {
2402
2417
  this._captureEvent(e);
2403
2418
  } catch (error) {
2404
- logger$2.error('Failed to capture event', error);
2419
+ logger$3.error('Failed to capture event', error);
2405
2420
  }
2406
2421
  };
2407
2422
  addEventListener(document, 'submit', handler, {
@@ -2586,7 +2601,7 @@ class SessionIdManager {
2586
2601
  this._windowIdGenerator = windowIdGenerator || uuidv7;
2587
2602
  const persistenceName = this._config['persistence_name'] || this._config['token'];
2588
2603
  const desiredTimeout = this._config['session_idle_timeout_seconds'] || DEFAULT_SESSION_IDLE_TIMEOUT_SECONDS;
2589
- this._sessionTimeoutMs = core.clampToRange(desiredTimeout, MIN_SESSION_IDLE_TIMEOUT_SECONDS, MAX_SESSION_IDLE_TIMEOUT_SECONDS, logger$2, DEFAULT_SESSION_IDLE_TIMEOUT_SECONDS) * 1000;
2604
+ this._sessionTimeoutMs = core.clampToRange(desiredTimeout, MIN_SESSION_IDLE_TIMEOUT_SECONDS, MAX_SESSION_IDLE_TIMEOUT_SECONDS, logger$3, DEFAULT_SESSION_IDLE_TIMEOUT_SECONDS) * 1000;
2590
2605
  instance.register({
2591
2606
  $configured_session_timeout_ms: this._sessionTimeoutMs
2592
2607
  });
@@ -2613,7 +2628,7 @@ class SessionIdManager {
2613
2628
  const sessionStartTimestamp = uuid7ToTimestampMs(this._config.bootstrap.sessionID);
2614
2629
  this._setSessionId(this._config.bootstrap.sessionID, new Date().getTime(), sessionStartTimestamp);
2615
2630
  } catch (e) {
2616
- logger$2.error('Invalid sessionID in bootstrap', e);
2631
+ logger$3.error('Invalid sessionID in bootstrap', e);
2617
2632
  }
2618
2633
  }
2619
2634
  this._listenToReloadWindow();
@@ -2754,7 +2769,7 @@ class SessionIdManager {
2754
2769
  if (noSessionId || activityTimeout || sessionPastMaximumLength) {
2755
2770
  sessionId = this._sessionIdGenerator();
2756
2771
  windowId = this._windowIdGenerator();
2757
- logger$2.info('new session ID generated', {
2772
+ logger$3.info('new session ID generated', {
2758
2773
  sessionId,
2759
2774
  windowId,
2760
2775
  changeReason: {
@@ -2943,10 +2958,10 @@ class PageViewManager {
2943
2958
  lastContentY = Math.ceil(lastContentY);
2944
2959
  maxContentY = Math.ceil(maxContentY);
2945
2960
  // if the maximum scroll height is near 0, then the percentage is 1
2946
- const lastScrollPercentage = maxScrollHeight <= 1 ? 1 : core.clampToRange(lastScrollY / maxScrollHeight, 0, 1, logger$2);
2947
- const maxScrollPercentage = maxScrollHeight <= 1 ? 1 : core.clampToRange(maxScrollY / maxScrollHeight, 0, 1, logger$2);
2948
- const lastContentPercentage = maxContentHeight <= 1 ? 1 : core.clampToRange(lastContentY / maxContentHeight, 0, 1, logger$2);
2949
- const maxContentPercentage = maxContentHeight <= 1 ? 1 : core.clampToRange(maxContentY / maxContentHeight, 0, 1, logger$2);
2961
+ const lastScrollPercentage = maxScrollHeight <= 1 ? 1 : core.clampToRange(lastScrollY / maxScrollHeight, 0, 1, logger$3);
2962
+ const maxScrollPercentage = maxScrollHeight <= 1 ? 1 : core.clampToRange(maxScrollY / maxScrollHeight, 0, 1, logger$3);
2963
+ const lastContentPercentage = maxContentHeight <= 1 ? 1 : core.clampToRange(lastContentY / maxContentHeight, 0, 1, logger$3);
2964
+ const maxContentPercentage = maxContentHeight <= 1 ? 1 : core.clampToRange(maxContentY / maxContentHeight, 0, 1, logger$3);
2950
2965
  properties = extend(properties, {
2951
2966
  $prev_pageview_last_scroll: lastScrollY,
2952
2967
  $prev_pageview_last_scroll_percentage: lastScrollPercentage,
@@ -3211,17 +3226,17 @@ var CanvasContext;
3211
3226
 
3212
3227
  const _createLogger = prefix => {
3213
3228
  return {
3214
- info: (...args) => logger$2.info(prefix, ...args),
3215
- warn: (...args) => logger$2.warn(prefix, ...args),
3216
- error: (...args) => logger$2.error(prefix, ...args),
3217
- critical: (...args) => logger$2.critical(prefix, ...args),
3229
+ info: (...args) => logger$3.info(prefix, ...args),
3230
+ warn: (...args) => logger$3.warn(prefix, ...args),
3231
+ error: (...args) => logger$3.error(prefix, ...args),
3232
+ critical: (...args) => logger$3.critical(prefix, ...args),
3218
3233
  uninitializedWarning: methodName => {
3219
- logger$2.error(prefix, `You must initialize Leanbase before calling ${methodName}`);
3234
+ logger$3.error(prefix, `You must initialize Leanbase before calling ${methodName}`);
3220
3235
  },
3221
3236
  createLogger: additionalPrefix => _createLogger(`${prefix} ${additionalPrefix}`)
3222
3237
  };
3223
3238
  };
3224
- const logger$1 = _createLogger('[Leanbase]');
3239
+ const logger$2 = _createLogger('[Leanbase]');
3225
3240
  const createLogger = _createLogger;
3226
3241
 
3227
3242
  const LOGGER_PREFIX$2 = '[SessionRecording]';
@@ -3295,7 +3310,7 @@ function enforcePayloadSizeLimit(payload, headers, limit, description) {
3295
3310
  // people can have arbitrarily large payloads on their site, but we don't want to ingest them
3296
3311
  const limitPayloadSize = options => {
3297
3312
  // the smallest of 1MB or the specified limit if there is one
3298
- const limit = Math.min(1000000, options.payloadSizeLimitBytes);
3313
+ const limit = Math.min(1000000, options.payloadSizeLimitBytes ?? 1000000);
3299
3314
  return data => {
3300
3315
  if (data?.requestBody) {
3301
3316
  data.requestBody = enforcePayloadSizeLimit(data.requestBody, data.requestHeaders, limit, 'Request');
@@ -3353,7 +3368,7 @@ const buildNetworkRequestOptions = (instanceConfig, remoteNetworkOptions = {}) =
3353
3368
  const enforcedCleaningFn = d => payloadLimiter(ignorePostHogPaths(removeAuthorizationHeader(d), instanceConfig.host || ''));
3354
3369
  const hasDeprecatedMaskFunction = core.isFunction(sessionRecordingConfig.maskNetworkRequestFn);
3355
3370
  if (hasDeprecatedMaskFunction && core.isFunction(sessionRecordingConfig.maskCapturedNetworkRequestFn)) {
3356
- logger$1.warn('Both `maskNetworkRequestFn` and `maskCapturedNetworkRequestFn` are defined. `maskNetworkRequestFn` will be ignored.');
3371
+ logger$2.warn('Both `maskNetworkRequestFn` and `maskCapturedNetworkRequestFn` are defined. `maskNetworkRequestFn` will be ignored.');
3357
3372
  }
3358
3373
  if (hasDeprecatedMaskFunction) {
3359
3374
  sessionRecordingConfig.maskCapturedNetworkRequestFn = data => {
@@ -3800,7 +3815,7 @@ class MutationThrottler {
3800
3815
  refillInterval: 1000,
3801
3816
  // one second
3802
3817
  _onBucketRateLimited: this._onNodeRateLimited,
3803
- _logger: logger$1
3818
+ _logger: logger$2
3804
3819
  });
3805
3820
  }
3806
3821
  reset() {
@@ -3824,7 +3839,7 @@ function simpleHash(str) {
3824
3839
  * receives percent as a number between 0 and 1
3825
3840
  */
3826
3841
  function sampleOnProperty(prop, percent) {
3827
- return simpleHash(prop) % 100 < core.clampToRange(percent * 100, 0, 100, logger$1);
3842
+ return simpleHash(prop) % 100 < core.clampToRange(percent * 100, 0, 100, logger$2);
3828
3843
  }
3829
3844
 
3830
3845
  /* eslint-disable posthog-js/no-direct-function-check */
@@ -3855,7 +3870,7 @@ const RECORDING_MAX_EVENT_SIZE = ONE_KB * ONE_KB * 0.9; // ~1mb (with some wiggl
3855
3870
  const RECORDING_BUFFER_TIMEOUT = 2000; // 2 seconds
3856
3871
  const SESSION_RECORDING_BATCH_KEY = 'recordings';
3857
3872
  const LOGGER_PREFIX$1 = '[SessionRecording]';
3858
- const logger = createLogger(LOGGER_PREFIX$1);
3873
+ const logger$1 = createLogger(LOGGER_PREFIX$1);
3859
3874
  const ACTIVE_SOURCES = [IncrementalSource.MouseMove, IncrementalSource.MouseInteraction, IncrementalSource.Scroll, IncrementalSource.ViewportResize, IncrementalSource.Input, IncrementalSource.TouchMove, IncrementalSource.MediaInteraction, IncrementalSource.Drag];
3860
3875
  const newQueuedEvent = rrwebMethod => ({
3861
3876
  rrwebMethod,
@@ -3904,7 +3919,7 @@ async function loadRRWeb() {
3904
3919
  return rr;
3905
3920
  }
3906
3921
  } catch (e) {
3907
- logger.error('could not dynamically load rrweb', e);
3922
+ logger$1.error('could not dynamically load rrweb', e);
3908
3923
  }
3909
3924
  return null;
3910
3925
  }
@@ -3951,7 +3966,7 @@ function compressEvent(event) {
3951
3966
  };
3952
3967
  }
3953
3968
  } catch (e) {
3954
- logger.error('could not compress event - will use uncompressed event', e);
3969
+ logger$1.error('could not compress event - will use uncompressed event', e);
3955
3970
  }
3956
3971
  return event;
3957
3972
  }
@@ -3988,6 +4003,11 @@ function splitBuffer(buffer, sizeLimit = SEVEN_MEGABYTES) {
3988
4003
  }
3989
4004
  }
3990
4005
  class LazyLoadedSessionRecording {
4006
+ _debug(...args) {
4007
+ if (this._instance?.config?.debug) {
4008
+ logger$1.info(...args);
4009
+ }
4010
+ }
3991
4011
  get sessionId() {
3992
4012
  return this._sessionId;
3993
4013
  }
@@ -4002,9 +4022,9 @@ class LazyLoadedSessionRecording {
4002
4022
  if (!this._loggedPermanentlyDisabled) {
4003
4023
  this._loggedPermanentlyDisabled = true;
4004
4024
  if (error) {
4005
- logger.error(`replay disabled: ${reason}`, error);
4025
+ logger$1.error(`replay disabled: ${reason}`, error);
4006
4026
  } else {
4007
- logger.error(`replay disabled: ${reason}`);
4027
+ logger$1.error(`replay disabled: ${reason}`);
4008
4028
  }
4009
4029
  }
4010
4030
  }
@@ -4041,6 +4061,7 @@ class LazyLoadedSessionRecording {
4041
4061
  this._stopRrweb = undefined;
4042
4062
  this._permanentlyDisabled = false;
4043
4063
  this._loggedPermanentlyDisabled = false;
4064
+ this._hasReportedRecordingInitialized = false;
4044
4065
  this._lastActivityTimestamp = Date.now();
4045
4066
  /**
4046
4067
  * and a queue - that contains rrweb events that we want to send to rrweb, but rrweb wasn't able to accept them yet
@@ -4103,7 +4124,7 @@ class LazyLoadedSessionRecording {
4103
4124
  this._eventTriggerMatching = new EventTriggerMatching(this._instance);
4104
4125
  this._buffer = this._clearBuffer();
4105
4126
  if (this._sessionIdleThresholdMilliseconds >= this._sessionManager.sessionTimeoutMs) {
4106
- logger.warn(`session_idle_threshold_ms (${this._sessionIdleThresholdMilliseconds}) is greater than the session timeout (${this._sessionManager.sessionTimeoutMs}). Session will never be detected as idle`);
4127
+ logger$1.warn(`session_idle_threshold_ms (${this._sessionIdleThresholdMilliseconds}) is greater than the session timeout (${this._sessionManager.sessionTimeoutMs}). Session will never be detected as idle`);
4107
4128
  }
4108
4129
  }
4109
4130
  get _masking() {
@@ -4177,25 +4198,62 @@ class LazyLoadedSessionRecording {
4177
4198
  payloadHostDenyList: networkPayloadCapture_server_side?.payloadHostDenyList
4178
4199
  };
4179
4200
  }
4180
- _gatherRRWebPlugins() {
4201
+ async _loadConsolePlugin() {
4202
+ try {
4203
+ const mod = await import('@rrweb/rrweb-plugin-console-record');
4204
+ const factory = mod?.getRecordConsolePlugin ?? mod?.default?.getRecordConsolePlugin;
4205
+ if (typeof factory === 'function') {
4206
+ const plugin = factory();
4207
+ this._debug('Console plugin loaded');
4208
+ return plugin;
4209
+ }
4210
+ logger$1.warn('console plugin factory unavailable after import');
4211
+ } catch (e) {
4212
+ logger$1.warn('could not load console plugin', e);
4213
+ }
4214
+ return null;
4215
+ }
4216
+ async _loadNetworkPlugin(networkPayloadCapture) {
4217
+ try {
4218
+ const mod = await Promise.resolve().then(function () { return networkPlugin; });
4219
+ const factory = mod?.getRecordNetworkPlugin ?? mod?.default?.getRecordNetworkPlugin;
4220
+ if (typeof factory === 'function') {
4221
+ const options = buildNetworkRequestOptions(this._instance.config, networkPayloadCapture);
4222
+ const plugin = factory(options);
4223
+ this._debug('Network plugin loaded');
4224
+ return plugin;
4225
+ }
4226
+ logger$1.warn('network plugin factory unavailable after import');
4227
+ } catch (e) {
4228
+ logger$1.warn('could not load network plugin', e);
4229
+ }
4230
+ return null;
4231
+ }
4232
+ async _gatherRRWebPlugins() {
4181
4233
  const plugins = [];
4234
+ if (!win) {
4235
+ return plugins;
4236
+ }
4182
4237
  if (this._isConsoleLogCaptureEnabled) {
4183
- logger.info('Console log capture requested but console plugin is not bundled in this build yet.');
4238
+ const consolePlugin = await this._loadConsolePlugin();
4239
+ if (consolePlugin) {
4240
+ plugins.push(consolePlugin);
4241
+ }
4184
4242
  }
4185
4243
  if (this._networkPayloadCapture) {
4186
4244
  const canRecordNetwork = !isLocalhost() || this._forceAllowLocalhostNetworkCapture;
4187
4245
  if (canRecordNetwork) {
4188
- const assignableWindow = globalThis;
4189
- const networkFactory = assignableWindow.__PosthogExtensions__?.rrwebPlugins?.getRecordNetworkPlugin?.();
4190
- if (typeof networkFactory === 'function') {
4191
- plugins.push(networkFactory(buildNetworkRequestOptions(this._instance.config, this._networkPayloadCapture)));
4192
- } else {
4193
- logger.info('Network plugin factory not available yet; skipping network plugin');
4246
+ const networkPlugin = await this._loadNetworkPlugin(this._networkPayloadCapture);
4247
+ if (networkPlugin) {
4248
+ plugins.push(networkPlugin);
4194
4249
  }
4195
4250
  } else {
4196
- logger.info('NetworkCapture not started because we are on localhost.');
4251
+ this._debug('NetworkCapture not started because we are on localhost.');
4197
4252
  }
4198
4253
  }
4254
+ if (plugins.length > 0) {
4255
+ this._debug('Replay plugins loaded', plugins.map(p => p.name));
4256
+ }
4199
4257
  return plugins;
4200
4258
  }
4201
4259
  _maskUrl(url) {
@@ -4224,7 +4282,7 @@ class LazyLoadedSessionRecording {
4224
4282
  rrwebMethod: queuedRRWebEvent.rrwebMethod
4225
4283
  });
4226
4284
  } else {
4227
- logger.warn('could not emit queued rrweb event.', e, queuedRRWebEvent);
4285
+ logger$1.warn('could not emit queued rrweb event.', e, queuedRRWebEvent);
4228
4286
  }
4229
4287
  return false;
4230
4288
  }
@@ -4336,7 +4394,7 @@ class LazyLoadedSessionRecording {
4336
4394
  this._urlTriggerMatching.urlBlocked = true;
4337
4395
  // Clear the snapshot timer since we don't want new snapshots while paused
4338
4396
  clearInterval(this._fullSnapshotTimer);
4339
- logger.info('recording paused due to URL blocker');
4397
+ logger$1.info('recording paused due to URL blocker');
4340
4398
  this._tryAddCustomEvent('recording paused', {
4341
4399
  reason: 'url blocker'
4342
4400
  });
@@ -4355,7 +4413,7 @@ class LazyLoadedSessionRecording {
4355
4413
  this._tryAddCustomEvent('recording resumed', {
4356
4414
  reason: 'left blocked url'
4357
4415
  });
4358
- logger.info('recording resumed');
4416
+ logger$1.info('recording resumed');
4359
4417
  }
4360
4418
  _activateTrigger(triggerType) {
4361
4419
  if (!this.isStarted || !this._recording || !this._isFullyReady) {
@@ -4388,7 +4446,7 @@ class LazyLoadedSessionRecording {
4388
4446
  this._isFullyReady = false;
4389
4447
  const config = this._remoteConfig;
4390
4448
  if (!config) {
4391
- logger.info('remote config must be stored in persistence before recording can start');
4449
+ logger$1.info('remote config must be stored in persistence before recording can start');
4392
4450
  return;
4393
4451
  }
4394
4452
  // We want to ensure the sessionManager is reset if necessary on loading the recorder
@@ -4471,12 +4529,18 @@ class LazyLoadedSessionRecording {
4471
4529
  });
4472
4530
  }
4473
4531
  } catch (e) {
4474
- logger.error('Could not add $pageview to rrweb session', e);
4532
+ logger$1.error('Could not add $pageview to rrweb session', e);
4475
4533
  }
4476
4534
  });
4477
4535
  }
4478
4536
  if (this.status === ACTIVE) {
4479
- this._reportStarted(startReason || 'recording_initialized');
4537
+ const reason = startReason || 'recording_initialized';
4538
+ if (reason !== 'recording_initialized' || !this._hasReportedRecordingInitialized) {
4539
+ if (reason === 'recording_initialized') {
4540
+ this._hasReportedRecordingInitialized = true;
4541
+ }
4542
+ this._reportStarted(reason);
4543
+ }
4480
4544
  }
4481
4545
  }
4482
4546
  stop() {
@@ -4509,17 +4573,31 @@ class LazyLoadedSessionRecording {
4509
4573
  this._recording = undefined;
4510
4574
  this._stopRrweb = undefined;
4511
4575
  this._isFullyReady = false;
4512
- logger.info('stopped');
4576
+ this._hasReportedRecordingInitialized = false;
4577
+ logger$1.info('stopped');
4513
4578
  }
4514
4579
  _snapshotIngestionUrl() {
4515
4580
  const endpointFor = this._instance?.requestRouter?.endpointFor;
4516
- if (typeof endpointFor !== 'function') {
4581
+ // Prefer requestRouter (parity with Browser SDK)
4582
+ if (typeof endpointFor === 'function') {
4583
+ try {
4584
+ return endpointFor('api', this._endpoint);
4585
+ } catch {
4586
+ return null;
4587
+ }
4588
+ }
4589
+ // Fallback: construct from host/api_host if requestRouter is unavailable (older IIFE builds)
4590
+ const host = (this._instance.config.api_host || this._instance.config.host || '').trim();
4591
+ if (!host) {
4517
4592
  return null;
4518
4593
  }
4519
4594
  try {
4520
- return endpointFor('api', this._endpoint);
4595
+ // eslint-disable-next-line compat/compat
4596
+ return new URL(this._endpoint, host).href;
4521
4597
  } catch {
4522
- return null;
4598
+ const normalizedHost = host.endsWith('/') ? host.slice(0, -1) : host;
4599
+ const normalizedEndpoint = this._endpoint.startsWith('/') ? this._endpoint : `/${this._endpoint}`;
4600
+ return `${normalizedHost}${normalizedEndpoint}`;
4523
4601
  }
4524
4602
  }
4525
4603
  _canCaptureSnapshots() {
@@ -4604,7 +4682,7 @@ class LazyLoadedSessionRecording {
4604
4682
  }
4605
4683
  this._captureSnapshotBuffered(properties);
4606
4684
  } catch (e) {
4607
- logger.error('error processing rrweb event', e);
4685
+ logger$1.error('error processing rrweb event', e);
4608
4686
  }
4609
4687
  }
4610
4688
  get status() {
@@ -4695,7 +4773,7 @@ class LazyLoadedSessionRecording {
4695
4773
  if (!this._canCaptureSnapshots()) {
4696
4774
  if (!this._loggedMissingEndpointFor) {
4697
4775
  this._loggedMissingEndpointFor = true;
4698
- logger.warn('snapshot capture skipped because requestRouter.endpointFor is unavailable');
4776
+ logger$1.warn('snapshot capture skipped because requestRouter.endpointFor is unavailable');
4699
4777
  }
4700
4778
  this._flushBufferTimer = setTimeout(() => {
4701
4779
  this._flushBuffer();
@@ -4738,7 +4816,7 @@ class LazyLoadedSessionRecording {
4738
4816
  if (!url) {
4739
4817
  if (!this._loggedMissingEndpointFor) {
4740
4818
  this._loggedMissingEndpointFor = true;
4741
- logger.warn('snapshot capture skipped because requestRouter.endpointFor is unavailable');
4819
+ logger$1.warn('snapshot capture skipped because requestRouter.endpointFor is unavailable');
4742
4820
  }
4743
4821
  return;
4744
4822
  }
@@ -4751,7 +4829,7 @@ class LazyLoadedSessionRecording {
4751
4829
  skip_client_rate_limiting: true
4752
4830
  });
4753
4831
  } catch (e) {
4754
- logger.error('failed to capture snapshot', e);
4832
+ logger$1.error('failed to capture snapshot', e);
4755
4833
  }
4756
4834
  }
4757
4835
  _snapshotUrl() {
@@ -4808,7 +4886,12 @@ class LazyLoadedSessionRecording {
4808
4886
  this._instance.registerForSession({
4809
4887
  $session_recording_start_reason: startReason
4810
4888
  });
4811
- logger.info(startReason.replace('_', ' '), tagPayload);
4889
+ const message = startReason.replace('_', ' ');
4890
+ if (typeof tagPayload === 'undefined') {
4891
+ logger$1.info(message);
4892
+ } else {
4893
+ logger$1.info(message, tagPayload);
4894
+ }
4812
4895
  if (!core.includes(['recording_initialized', 'session_id_changed'], startReason)) {
4813
4896
  this._tryAddCustomEvent(startReason, tagPayload);
4814
4897
  }
@@ -4908,7 +4991,7 @@ class LazyLoadedSessionRecording {
4908
4991
  if (shouldSample) {
4909
4992
  this._reportStarted(SAMPLED);
4910
4993
  } else {
4911
- logger.warn(`Sample rate (${currentSampleRate}) has determined that this sessionId (${sessionId}) will not be sent to the server.`);
4994
+ logger$1.warn(`Sample rate (${currentSampleRate}) has determined that this sessionId (${sessionId}) will not be sent to the server.`);
4912
4995
  }
4913
4996
  this._tryAddCustomEvent('samplingDecisionMade', {
4914
4997
  sampleRate: currentSampleRate,
@@ -4931,7 +5014,7 @@ class LazyLoadedSessionRecording {
4931
5014
  this._activateTrigger('event');
4932
5015
  }
4933
5016
  } catch (e) {
4934
- logger.error('Could not activate event trigger', e);
5017
+ logger$1.error('Could not activate event trigger', e);
4935
5018
  }
4936
5019
  });
4937
5020
  }
@@ -5013,7 +5096,7 @@ class LazyLoadedSessionRecording {
5013
5096
  this._disablePermanently('rrweb record function unavailable');
5014
5097
  return;
5015
5098
  }
5016
- const activePlugins = this._gatherRRWebPlugins();
5099
+ const activePlugins = await this._gatherRRWebPlugins();
5017
5100
  let stopHandler;
5018
5101
  try {
5019
5102
  stopHandler = rrwebRecord({
@@ -5022,7 +5105,7 @@ class LazyLoadedSessionRecording {
5022
5105
  this.onRRwebEmit(event);
5023
5106
  } catch (e) {
5024
5107
  // never throw from rrweb emit handler
5025
- logger.error('error in rrweb emit handler', e);
5108
+ logger$1.error('error in rrweb emit handler', e);
5026
5109
  }
5027
5110
  },
5028
5111
  plugins: activePlugins,
@@ -5047,7 +5130,7 @@ class LazyLoadedSessionRecording {
5047
5130
  bucketSize: this._instance.config.session_recording?.__mutationThrottlerBucketSize,
5048
5131
  onBlockedNode: (id, node) => {
5049
5132
  const message = `Too many mutations on node '${id}'. Rate limiting. This could be due to SVG animations or something similar`;
5050
- logger.info(message, {
5133
+ logger$1.info(message, {
5051
5134
  node: node
5052
5135
  });
5053
5136
  this.log(LOGGER_PREFIX$1 + ' ' + message, 'warn');
@@ -5074,11 +5157,16 @@ class LazyLoadedSessionRecording {
5074
5157
  /* eslint-disable posthog-js/no-direct-function-check */
5075
5158
  const LOGGER_PREFIX = '[SessionRecording]';
5076
5159
  const log = {
5077
- info: (...args) => logger$2.info(LOGGER_PREFIX, ...args),
5078
- warn: (...args) => logger$2.warn(LOGGER_PREFIX, ...args),
5079
- error: (...args) => logger$2.error(LOGGER_PREFIX, ...args)
5160
+ info: (...args) => logger$3.info(LOGGER_PREFIX, ...args),
5161
+ warn: (...args) => logger$3.warn(LOGGER_PREFIX, ...args),
5162
+ error: (...args) => logger$3.error(LOGGER_PREFIX, ...args)
5080
5163
  };
5081
5164
  class SessionRecording {
5165
+ _debug(...args) {
5166
+ if (this._instance?.config?.debug) {
5167
+ log.info(...args);
5168
+ }
5169
+ }
5082
5170
  get started() {
5083
5171
  return !!this._lazyLoadedSessionRecording?.isStarted;
5084
5172
  }
@@ -5116,8 +5204,10 @@ class SessionRecording {
5116
5204
  }
5117
5205
  const canRunReplay = !core.isUndefined(Object.assign) && !core.isUndefined(Array.from);
5118
5206
  if (this._isRecordingEnabled && canRunReplay) {
5207
+ this._debug('Session replay enabled; starting recorder');
5119
5208
  this._lazyLoadAndStart(startReason);
5120
5209
  } else {
5210
+ this._debug('Session replay disabled; stopping recorder');
5121
5211
  this.stopRecording();
5122
5212
  }
5123
5213
  }
@@ -5189,18 +5279,15 @@ class SessionRecording {
5189
5279
  log.info('skipping remote config with no sessionRecording', response);
5190
5280
  return;
5191
5281
  }
5192
- this._receivedFlags = true;
5193
- if (response.sessionRecording === false) {
5194
- return;
5195
- }
5196
5282
  this._persistRemoteConfig(response);
5283
+ this._receivedFlags = true;
5197
5284
  this.startIfEnabledOrStop();
5198
5285
  }
5199
5286
  log(message, level = 'log') {
5200
5287
  if (this._lazyLoadedSessionRecording?.log) {
5201
5288
  this._lazyLoadedSessionRecording.log(message, level);
5202
5289
  } else {
5203
- logger$2.warn('log called before recorder was ready');
5290
+ logger$3.warn('log called before recorder was ready');
5204
5291
  }
5205
5292
  }
5206
5293
  get _scriptName() {
@@ -5223,10 +5310,10 @@ class SessionRecording {
5223
5310
  try {
5224
5311
  const maybePromise = this._lazyLoadedSessionRecording.start(startReason);
5225
5312
  if (maybePromise && typeof maybePromise.catch === 'function') {
5226
- maybePromise.catch(e => logger$2.error('error starting session recording', e));
5313
+ maybePromise.catch(e => logger$3.error('error starting session recording', e));
5227
5314
  }
5228
5315
  } catch (e) {
5229
- logger$2.error('error starting session recording', e);
5316
+ logger$3.error('error starting session recording', e);
5230
5317
  }
5231
5318
  return;
5232
5319
  }
@@ -5239,10 +5326,10 @@ class SessionRecording {
5239
5326
  try {
5240
5327
  const maybePromise = this._lazyLoadedSessionRecording.start(startReason);
5241
5328
  if (maybePromise && typeof maybePromise.catch === 'function') {
5242
- maybePromise.catch(e => logger$2.error('error starting session recording', e));
5329
+ maybePromise.catch(e => logger$3.error('error starting session recording', e));
5243
5330
  }
5244
5331
  } catch (e) {
5245
- logger$2.error('error starting session recording', e);
5332
+ logger$3.error('error starting session recording', e);
5246
5333
  }
5247
5334
  }
5248
5335
  /**
@@ -5275,6 +5362,39 @@ class SessionRecording {
5275
5362
  }
5276
5363
  }
5277
5364
 
5365
+ /**
5366
+ * Leanbase-local version of PostHog's RequestRouter.
5367
+ *
5368
+ * Browser SDK always has a requestRouter instance; Leanbase IIFE needs this too so
5369
+ * features like Session Replay can construct ingestion URLs.
5370
+ */
5371
+ class RequestRouter {
5372
+ // eslint-disable-next-line @typescript-eslint/naming-convention
5373
+ constructor(instance) {
5374
+ this.instance = instance;
5375
+ }
5376
+ get apiHost() {
5377
+ const configured = (this.instance.config.api_host || this.instance.config.host || '').trim();
5378
+ return configured.replace(/\/$/, '');
5379
+ }
5380
+ get uiHost() {
5381
+ const configured = this.instance.config.ui_host?.trim().replace(/\/$/, '');
5382
+ return configured || undefined;
5383
+ }
5384
+ endpointFor(target, path = '') {
5385
+ if (path) {
5386
+ path = path[0] === '/' ? path : `/${path}`;
5387
+ }
5388
+ if (target === 'ui') {
5389
+ const host = this.uiHost || this.apiHost;
5390
+ return host + path;
5391
+ }
5392
+ // Leanbase doesn't currently do region-based routing; default to apiHost.
5393
+ // Browser's router has special handling for assets; we keep parity in interface, not domains.
5394
+ return this.apiHost + path;
5395
+ }
5396
+ }
5397
+
5278
5398
  const defaultConfig = () => ({
5279
5399
  host: 'https://i.leanbase.co',
5280
5400
  token: '',
@@ -5332,6 +5452,8 @@ class Leanbase extends core.PostHogCore {
5332
5452
  }));
5333
5453
  this.isLoaded = true;
5334
5454
  this.persistence = new LeanbasePersistence(this.config);
5455
+ // Browser SDK always has a requestRouter; session replay relies on it for $snapshot ingestion URLs.
5456
+ this.requestRouter = new RequestRouter(this);
5335
5457
  if (this.config.cookieless_mode !== 'always') {
5336
5458
  this.sessionManager = new SessionIdManager(this);
5337
5459
  this.sessionPropsManager = new SessionPropsManager(this, this.sessionManager, this.persistence);
@@ -5364,12 +5486,12 @@ class Leanbase extends core.PostHogCore {
5364
5486
  }, 1);
5365
5487
  }
5366
5488
  const triggerRemoteConfigLoad = reason => {
5367
- logger$2.info(`remote config load triggered via ${reason}`);
5489
+ logger$3.info(`remote config load triggered via ${reason}`);
5368
5490
  void this.loadRemoteConfig();
5369
5491
  };
5370
5492
  if (document) {
5371
5493
  if (document.readyState === 'loading') {
5372
- logger$2.info('remote config load deferred until DOMContentLoaded');
5494
+ logger$3.info('remote config load deferred until DOMContentLoaded');
5373
5495
  const onDomReady = () => {
5374
5496
  document?.removeEventListener('DOMContentLoaded', onDomReady);
5375
5497
  triggerRemoteConfigLoad('dom');
@@ -5462,7 +5584,7 @@ class Leanbase extends core.PostHogCore {
5462
5584
  try {
5463
5585
  this.sessionRecording.startIfEnabledOrStop();
5464
5586
  } catch (e) {
5465
- logger$2.error('Failed to start session recording', e);
5587
+ logger$3.error('Failed to start session recording', e);
5466
5588
  }
5467
5589
  }
5468
5590
  fetch(url, options) {
@@ -5528,7 +5650,7 @@ class Leanbase extends core.PostHogCore {
5528
5650
  };
5529
5651
  properties['distinct_id'] = persistenceProps.distinct_id;
5530
5652
  if (!(core.isString(properties['distinct_id']) || core.isNumber(properties['distinct_id'])) || core.isEmptyString(properties['distinct_id'])) {
5531
- logger$2.error('Invalid distinct_id for replay event. This indicates a bug in your implementation');
5653
+ logger$3.error('Invalid distinct_id for replay event. This indicates a bug in your implementation');
5532
5654
  }
5533
5655
  return properties;
5534
5656
  }
@@ -5603,11 +5725,11 @@ class Leanbase extends core.PostHogCore {
5603
5725
  return;
5604
5726
  }
5605
5727
  if (core.isUndefined(event) || !core.isString(event)) {
5606
- logger$2.error('No event name provided to posthog.capture');
5728
+ logger$3.error('No event name provided to posthog.capture');
5607
5729
  return;
5608
5730
  }
5609
5731
  if (properties?.$current_url && !core.isString(properties?.$current_url)) {
5610
- logger$2.error('Invalid `$current_url` property provided to `posthog.capture`. Input must be a string. Ignoring provided value.');
5732
+ logger$3.error('Invalid `$current_url` property provided to `posthog.capture`. Input must be a string. Ignoring provided value.');
5611
5733
  delete properties?.$current_url;
5612
5734
  }
5613
5735
  this.sessionPersistence.update_search_keyword();
@@ -5659,5 +5781,634 @@ class Leanbase extends core.PostHogCore {
5659
5781
  }
5660
5782
  }
5661
5783
 
5784
+ function patch(source, name, replacement) {
5785
+ try {
5786
+ if (!(name in source)) {
5787
+ return () => {
5788
+ //
5789
+ };
5790
+ }
5791
+ const original = source[name];
5792
+ const wrapped = replacement(original);
5793
+ if (core.isFunction(wrapped)) {
5794
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
5795
+ wrapped.prototype = wrapped.prototype || {};
5796
+ Object.defineProperties(wrapped, {
5797
+ __posthog_wrapped__: {
5798
+ enumerable: false,
5799
+ value: true
5800
+ }
5801
+ });
5802
+ }
5803
+ source[name] = wrapped;
5804
+ return () => {
5805
+ source[name] = original;
5806
+ };
5807
+ } catch {
5808
+ return () => {
5809
+ //
5810
+ };
5811
+ }
5812
+ }
5813
+
5814
+ function hostnameFromURL(url) {
5815
+ try {
5816
+ if (typeof url === 'string') {
5817
+ return new URL(url).hostname;
5818
+ }
5819
+ if ('url' in url) {
5820
+ return new URL(url.url).hostname;
5821
+ }
5822
+ return url.hostname;
5823
+ } catch {
5824
+ return null;
5825
+ }
5826
+ }
5827
+ function isHostOnDenyList(url, options) {
5828
+ const hostname = hostnameFromURL(url);
5829
+ const defaultNotDenied = {
5830
+ hostname,
5831
+ isHostDenied: false
5832
+ };
5833
+ if (!options.payloadHostDenyList?.length || !hostname?.trim().length) {
5834
+ return defaultNotDenied;
5835
+ }
5836
+ for (const deny of options.payloadHostDenyList) {
5837
+ if (hostname.endsWith(deny)) {
5838
+ return {
5839
+ hostname,
5840
+ isHostDenied: true
5841
+ };
5842
+ }
5843
+ }
5844
+ return defaultNotDenied;
5845
+ }
5846
+
5847
+ /// <reference lib="dom" />
5848
+ const logger = createLogger('[Recorder]');
5849
+ const isNavigationTiming = entry => entry.entryType === 'navigation';
5850
+ const isResourceTiming = entry => entry.entryType === 'resource';
5851
+ function findLast(array, predicate) {
5852
+ const length = array.length;
5853
+ for (let i = length - 1; i >= 0; i -= 1) {
5854
+ if (predicate(array[i])) {
5855
+ return array[i];
5856
+ }
5857
+ }
5858
+ return undefined;
5859
+ }
5860
+ function isDocument(value) {
5861
+ return !!value && typeof value === 'object' && 'nodeType' in value && value.nodeType === 9;
5862
+ }
5863
+ function initPerformanceObserver(cb, win, options) {
5864
+ // if we are only observing timings then we could have a single observer for all types, with buffer true,
5865
+ // but we are going to filter by initiatorType _if we are wrapping fetch and xhr as the wrapped functions
5866
+ // will deal with those.
5867
+ // so we have a block which captures requests from before fetch/xhr is wrapped
5868
+ // these are marked `isInitial` so playback can display them differently if needed
5869
+ // they will never have method/status/headers/body because they are pre-wrapping that provides that
5870
+ if (options.recordInitialRequests) {
5871
+ const initialPerformanceEntries = win.performance.getEntries().filter(entry => isNavigationTiming(entry) || isResourceTiming(entry) && options.initiatorTypes.includes(entry.initiatorType));
5872
+ cb({
5873
+ requests: initialPerformanceEntries.flatMap(entry => prepareRequest({
5874
+ entry,
5875
+ method: undefined,
5876
+ status: undefined,
5877
+ networkRequest: {},
5878
+ isInitial: true
5879
+ })),
5880
+ isInitial: true
5881
+ });
5882
+ }
5883
+ const observer = new win.PerformanceObserver(entries => {
5884
+ // if recordBody or recordHeaders is true then we don't want to record fetch or xhr here
5885
+ // as the wrapped functions will do that. Otherwise, this filter becomes a noop
5886
+ // because we do want to record them here
5887
+ const wrappedInitiatorFilter = entry => options.recordBody || options.recordHeaders ? entry.initiatorType !== 'xmlhttprequest' && entry.initiatorType !== 'fetch' : true;
5888
+ const performanceEntries = entries.getEntries().filter(entry => isNavigationTiming(entry) || isResourceTiming(entry) && options.initiatorTypes.includes(entry.initiatorType) &&
5889
+ // TODO if we are _only_ capturing timing we don't want to filter initiator here
5890
+ wrappedInitiatorFilter(entry));
5891
+ cb({
5892
+ requests: performanceEntries.flatMap(entry => prepareRequest({
5893
+ entry,
5894
+ method: undefined,
5895
+ status: undefined,
5896
+ networkRequest: {}
5897
+ }))
5898
+ });
5899
+ });
5900
+ // compat checked earlier
5901
+ // eslint-disable-next-line compat/compat
5902
+ const entryTypes = PerformanceObserver.supportedEntryTypes.filter(x => options.performanceEntryTypeToObserve.includes(x));
5903
+ // initial records are gathered above, so we don't need to observe and buffer each type separately
5904
+ observer.observe({
5905
+ entryTypes
5906
+ });
5907
+ return () => {
5908
+ observer.disconnect();
5909
+ };
5910
+ }
5911
+ function shouldRecordHeaders(type, recordHeaders) {
5912
+ return !!recordHeaders && (core.isBoolean(recordHeaders) || recordHeaders[type]);
5913
+ }
5914
+ function shouldRecordBody({
5915
+ type,
5916
+ recordBody,
5917
+ headers,
5918
+ url
5919
+ }) {
5920
+ function matchesContentType(contentTypes) {
5921
+ const contentTypeHeader = Object.keys(headers).find(key => key.toLowerCase() === 'content-type');
5922
+ const contentType = contentTypeHeader && headers[contentTypeHeader];
5923
+ return contentTypes.some(ct => contentType?.includes(ct));
5924
+ }
5925
+ /**
5926
+ * particularly in canvas applications we see many requests to blob URLs
5927
+ * e.g. blob:https://video_url
5928
+ * these blob/object URLs are local to the browser, we can never capture that body
5929
+ * so we can just return false here
5930
+ */
5931
+ function isBlobURL(url) {
5932
+ try {
5933
+ if (typeof url === 'string') {
5934
+ return url.startsWith('blob:');
5935
+ }
5936
+ if (url instanceof URL) {
5937
+ return url.protocol === 'blob:';
5938
+ }
5939
+ if (url instanceof Request) {
5940
+ return isBlobURL(url.url);
5941
+ }
5942
+ return false;
5943
+ } catch {
5944
+ return false;
5945
+ }
5946
+ }
5947
+ if (!recordBody) return false;
5948
+ if (isBlobURL(url)) return false;
5949
+ if (core.isBoolean(recordBody)) return true;
5950
+ if (core.isArray(recordBody)) return matchesContentType(recordBody);
5951
+ const recordBodyType = recordBody[type];
5952
+ if (core.isBoolean(recordBodyType)) return recordBodyType;
5953
+ return matchesContentType(recordBodyType);
5954
+ }
5955
+ async function getRequestPerformanceEntry(win, initiatorType, url, start, end, attempt = 0) {
5956
+ if (attempt > 10) {
5957
+ logger.warn('Failed to get performance entry for request', {
5958
+ url,
5959
+ initiatorType
5960
+ });
5961
+ return null;
5962
+ }
5963
+ const urlPerformanceEntries = win.performance.getEntriesByName(url);
5964
+ const performanceEntry = findLast(urlPerformanceEntries, entry => isResourceTiming(entry) && entry.initiatorType === initiatorType && (core.isUndefined(start) || entry.startTime >= start) && (core.isUndefined(end) || entry.startTime <= end));
5965
+ if (!performanceEntry) {
5966
+ await new Promise(resolve => setTimeout(resolve, 50 * attempt));
5967
+ return getRequestPerformanceEntry(win, initiatorType, url, start, end, attempt + 1);
5968
+ }
5969
+ return performanceEntry;
5970
+ }
5971
+ /**
5972
+ * According to MDN https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/response
5973
+ * xhr response is typed as any but can be an ArrayBuffer, a Blob, a Document, a JavaScript object,
5974
+ * or a string, depending on the value of XMLHttpRequest.responseType, that contains the response entity body.
5975
+ *
5976
+ * XHR request body is Document | XMLHttpRequestBodyInit | null | undefined
5977
+ */
5978
+ function _tryReadXHRBody({
5979
+ body,
5980
+ options,
5981
+ url
5982
+ }) {
5983
+ if (core.isNullish(body)) {
5984
+ return null;
5985
+ }
5986
+ const {
5987
+ hostname,
5988
+ isHostDenied
5989
+ } = isHostOnDenyList(url, options);
5990
+ if (isHostDenied) {
5991
+ return hostname + ' is in deny list';
5992
+ }
5993
+ if (core.isString(body)) {
5994
+ return body;
5995
+ }
5996
+ if (isDocument(body)) {
5997
+ return body.textContent;
5998
+ }
5999
+ if (core.isFormData(body)) {
6000
+ return formDataToQuery(body);
6001
+ }
6002
+ if (core.isObject(body)) {
6003
+ try {
6004
+ return JSON.stringify(body);
6005
+ } catch {
6006
+ return '[SessionReplay] Failed to stringify response object';
6007
+ }
6008
+ }
6009
+ return '[SessionReplay] Cannot read body of type ' + toString.call(body);
6010
+ }
6011
+ function initXhrObserver(cb, win, options) {
6012
+ if (!options.initiatorTypes.includes('xmlhttprequest')) {
6013
+ return () => {
6014
+ //
6015
+ };
6016
+ }
6017
+ const recordRequestHeaders = shouldRecordHeaders('request', options.recordHeaders);
6018
+ const recordResponseHeaders = shouldRecordHeaders('response', options.recordHeaders);
6019
+ const restorePatch = patch(win.XMLHttpRequest.prototype, 'open',
6020
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
6021
+ // @ts-ignore
6022
+ originalOpen => {
6023
+ return function (method, url, async = true, username, password) {
6024
+ // because this function is returned in its actual context `this` _is_ an XMLHttpRequest
6025
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
6026
+ // @ts-ignore
6027
+ const xhr = this;
6028
+ // check IE earlier than this, we only initialize if Request is present
6029
+ // eslint-disable-next-line compat/compat
6030
+ const req = new Request(url);
6031
+ const networkRequest = {};
6032
+ let start;
6033
+ let end;
6034
+ const requestHeaders = {};
6035
+ const originalSetRequestHeader = xhr.setRequestHeader.bind(xhr);
6036
+ xhr.setRequestHeader = (header, value) => {
6037
+ requestHeaders[header] = value;
6038
+ return originalSetRequestHeader(header, value);
6039
+ };
6040
+ if (recordRequestHeaders) {
6041
+ networkRequest.requestHeaders = requestHeaders;
6042
+ }
6043
+ const originalSend = xhr.send.bind(xhr);
6044
+ xhr.send = body => {
6045
+ if (shouldRecordBody({
6046
+ type: 'request',
6047
+ headers: requestHeaders,
6048
+ url,
6049
+ recordBody: options.recordBody
6050
+ })) {
6051
+ networkRequest.requestBody = _tryReadXHRBody({
6052
+ body,
6053
+ options,
6054
+ url
6055
+ });
6056
+ }
6057
+ start = win.performance.now();
6058
+ return originalSend(body);
6059
+ };
6060
+ const readyStateListener = () => {
6061
+ if (xhr.readyState !== xhr.DONE) {
6062
+ return;
6063
+ }
6064
+ // Clean up the listener immediately when done to prevent memory leaks
6065
+ xhr.removeEventListener('readystatechange', readyStateListener);
6066
+ end = win.performance.now();
6067
+ const responseHeaders = {};
6068
+ const rawHeaders = xhr.getAllResponseHeaders();
6069
+ const headers = rawHeaders.trim().split(/[\r\n]+/);
6070
+ headers.forEach(line => {
6071
+ const parts = line.split(': ');
6072
+ const header = parts.shift();
6073
+ const value = parts.join(': ');
6074
+ if (header) {
6075
+ responseHeaders[header] = value;
6076
+ }
6077
+ });
6078
+ if (recordResponseHeaders) {
6079
+ networkRequest.responseHeaders = responseHeaders;
6080
+ }
6081
+ if (shouldRecordBody({
6082
+ type: 'response',
6083
+ headers: responseHeaders,
6084
+ url,
6085
+ recordBody: options.recordBody
6086
+ })) {
6087
+ networkRequest.responseBody = _tryReadXHRBody({
6088
+ body: xhr.response,
6089
+ options,
6090
+ url
6091
+ });
6092
+ }
6093
+ getRequestPerformanceEntry(win, 'xmlhttprequest', req.url, start, end).then(entry => {
6094
+ const requests = prepareRequest({
6095
+ entry,
6096
+ method: method,
6097
+ status: xhr?.status,
6098
+ networkRequest,
6099
+ start,
6100
+ end,
6101
+ url: url.toString(),
6102
+ initiatorType: 'xmlhttprequest'
6103
+ });
6104
+ cb({
6105
+ requests
6106
+ });
6107
+ }).catch(() => {
6108
+ //
6109
+ });
6110
+ };
6111
+ // This is very tricky code, and making it passive won't bring many performance benefits,
6112
+ // so let's ignore the rule here.
6113
+ // eslint-disable-next-line posthog-js/no-add-event-listener
6114
+ xhr.addEventListener('readystatechange', readyStateListener);
6115
+ originalOpen.call(xhr, method, url, async, username, password);
6116
+ };
6117
+ });
6118
+ return () => {
6119
+ restorePatch();
6120
+ };
6121
+ }
6122
+ /**
6123
+ * Check if this PerformanceEntry is either a PerformanceResourceTiming or a PerformanceNavigationTiming
6124
+ * NB PerformanceNavigationTiming extends PerformanceResourceTiming
6125
+ * Here we don't care which interface it implements as both expose `serverTimings`
6126
+ */
6127
+ const exposesServerTiming = event => !core.isNull(event) && (event.entryType === 'navigation' || event.entryType === 'resource');
6128
+ function prepareRequest({
6129
+ entry,
6130
+ method,
6131
+ status,
6132
+ networkRequest,
6133
+ isInitial,
6134
+ start,
6135
+ end,
6136
+ url,
6137
+ initiatorType
6138
+ }) {
6139
+ start = entry ? entry.startTime : start;
6140
+ end = entry ? entry.responseEnd : end;
6141
+ // kudos to sentry javascript sdk for excellent background on why to use Date.now() here
6142
+ // https://github.com/getsentry/sentry-javascript/blob/e856e40b6e71a73252e788cd42b5260f81c9c88e/packages/utils/src/time.ts#L70
6143
+ // can't start observer if performance.now() is not available
6144
+ // eslint-disable-next-line compat/compat
6145
+ const timeOrigin = Math.floor(Date.now() - performance.now());
6146
+ // clickhouse can't ingest timestamps that are floats
6147
+ // (in this case representing fractions of a millisecond we don't care about anyway)
6148
+ // use timeOrigin if we really can't gather a start time
6149
+ const timestamp = Math.floor(timeOrigin + (start || 0));
6150
+ const entryJSON = entry ? entry.toJSON() : {
6151
+ name: url
6152
+ };
6153
+ const requests = [{
6154
+ ...entryJSON,
6155
+ startTime: core.isUndefined(start) ? undefined : Math.round(start),
6156
+ endTime: core.isUndefined(end) ? undefined : Math.round(end),
6157
+ timeOrigin,
6158
+ timestamp,
6159
+ method: method,
6160
+ initiatorType: initiatorType ? initiatorType : entry ? entry.initiatorType : undefined,
6161
+ status,
6162
+ requestHeaders: networkRequest.requestHeaders,
6163
+ requestBody: networkRequest.requestBody,
6164
+ responseHeaders: networkRequest.responseHeaders,
6165
+ responseBody: networkRequest.responseBody,
6166
+ isInitial
6167
+ }];
6168
+ if (exposesServerTiming(entry)) {
6169
+ for (const timing of entry.serverTiming || []) {
6170
+ requests.push({
6171
+ timeOrigin,
6172
+ timestamp,
6173
+ startTime: Math.round(entry.startTime),
6174
+ name: timing.name,
6175
+ duration: timing.duration,
6176
+ // the spec has a closed list of possible types
6177
+ // https://developer.mozilla.org/en-US/docs/Web/API/PerformanceEntry/entryType
6178
+ // but, we need to know this was a server timing so that we know to
6179
+ // match it to the appropriate navigation or resource timing
6180
+ // that matching will have to be on timestamp and $current_url
6181
+ entryType: 'serverTiming'
6182
+ });
6183
+ }
6184
+ }
6185
+ return requests;
6186
+ }
6187
+ const contentTypePrefixDenyList = ['video/', 'audio/'];
6188
+ function _checkForCannotReadResponseBody({
6189
+ r,
6190
+ options,
6191
+ url
6192
+ }) {
6193
+ if (r.headers.get('Transfer-Encoding') === 'chunked') {
6194
+ return 'Chunked Transfer-Encoding is not supported';
6195
+ }
6196
+ // `get` and `has` are case-insensitive
6197
+ // but return the header value with the casing that was supplied
6198
+ const contentType = r.headers.get('Content-Type')?.toLowerCase();
6199
+ const contentTypeIsDenied = contentTypePrefixDenyList.some(prefix => contentType?.startsWith(prefix));
6200
+ if (contentType && contentTypeIsDenied) {
6201
+ return `Content-Type ${contentType} is not supported`;
6202
+ }
6203
+ const {
6204
+ hostname,
6205
+ isHostDenied
6206
+ } = isHostOnDenyList(url, options);
6207
+ if (isHostDenied) {
6208
+ return hostname + ' is in deny list';
6209
+ }
6210
+ return null;
6211
+ }
6212
+ function _tryReadBody(r) {
6213
+ // there are now already multiple places where we're using Promise...
6214
+ // eslint-disable-next-line compat/compat
6215
+ return new Promise((resolve, reject) => {
6216
+ const timeout = setTimeout(() => resolve('[SessionReplay] Timeout while trying to read body'), 500);
6217
+ try {
6218
+ r.clone().text().then(txt => resolve(txt), reason => reject(reason)).finally(() => clearTimeout(timeout));
6219
+ } catch {
6220
+ clearTimeout(timeout);
6221
+ resolve('[SessionReplay] Failed to read body');
6222
+ }
6223
+ });
6224
+ }
6225
+ async function _tryReadRequestBody({
6226
+ r,
6227
+ options,
6228
+ url
6229
+ }) {
6230
+ const {
6231
+ hostname,
6232
+ isHostDenied
6233
+ } = isHostOnDenyList(url, options);
6234
+ if (isHostDenied) {
6235
+ return Promise.resolve(hostname + ' is in deny list');
6236
+ }
6237
+ return _tryReadBody(r);
6238
+ }
6239
+ async function _tryReadResponseBody({
6240
+ r,
6241
+ options,
6242
+ url
6243
+ }) {
6244
+ const cannotReadBodyReason = _checkForCannotReadResponseBody({
6245
+ r,
6246
+ options,
6247
+ url
6248
+ });
6249
+ if (!core.isNull(cannotReadBodyReason)) {
6250
+ return Promise.resolve(cannotReadBodyReason);
6251
+ }
6252
+ return _tryReadBody(r);
6253
+ }
6254
+ function initFetchObserver(cb, win, options) {
6255
+ if (!options.initiatorTypes.includes('fetch')) {
6256
+ return () => {
6257
+ //
6258
+ };
6259
+ }
6260
+ const recordRequestHeaders = shouldRecordHeaders('request', options.recordHeaders);
6261
+ const recordResponseHeaders = shouldRecordHeaders('response', options.recordHeaders);
6262
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
6263
+ // @ts-ignore
6264
+ const restorePatch = patch(win, 'fetch', originalFetch => {
6265
+ return async function (url, init) {
6266
+ // check IE earlier than this, we only initialize if Request is present
6267
+ // eslint-disable-next-line compat/compat
6268
+ const req = new Request(url, init);
6269
+ let res;
6270
+ const networkRequest = {};
6271
+ let start;
6272
+ let end;
6273
+ try {
6274
+ const requestHeaders = {};
6275
+ req.headers.forEach((value, header) => {
6276
+ requestHeaders[header] = value;
6277
+ });
6278
+ if (recordRequestHeaders) {
6279
+ networkRequest.requestHeaders = requestHeaders;
6280
+ }
6281
+ if (shouldRecordBody({
6282
+ type: 'request',
6283
+ headers: requestHeaders,
6284
+ url,
6285
+ recordBody: options.recordBody
6286
+ })) {
6287
+ networkRequest.requestBody = await _tryReadRequestBody({
6288
+ r: req,
6289
+ options,
6290
+ url
6291
+ });
6292
+ }
6293
+ start = win.performance.now();
6294
+ res = await originalFetch(req);
6295
+ end = win.performance.now();
6296
+ const responseHeaders = {};
6297
+ res.headers.forEach((value, header) => {
6298
+ responseHeaders[header] = value;
6299
+ });
6300
+ if (recordResponseHeaders) {
6301
+ networkRequest.responseHeaders = responseHeaders;
6302
+ }
6303
+ if (shouldRecordBody({
6304
+ type: 'response',
6305
+ headers: responseHeaders,
6306
+ url,
6307
+ recordBody: options.recordBody
6308
+ })) {
6309
+ networkRequest.responseBody = await _tryReadResponseBody({
6310
+ r: res,
6311
+ options,
6312
+ url
6313
+ });
6314
+ }
6315
+ return res;
6316
+ } finally {
6317
+ getRequestPerformanceEntry(win, 'fetch', req.url, start, end).then(entry => {
6318
+ const requests = prepareRequest({
6319
+ entry,
6320
+ method: req.method,
6321
+ status: res?.status,
6322
+ networkRequest,
6323
+ start,
6324
+ end,
6325
+ url: req.url,
6326
+ initiatorType: 'fetch'
6327
+ });
6328
+ cb({
6329
+ requests
6330
+ });
6331
+ }).catch(() => {
6332
+ //
6333
+ });
6334
+ }
6335
+ };
6336
+ });
6337
+ return () => {
6338
+ restorePatch();
6339
+ };
6340
+ }
6341
+ let initialisedHandler = null;
6342
+ function initNetworkObserver(callback, win,
6343
+ // top window or in an iframe
6344
+ options) {
6345
+ if (!('performance' in win)) {
6346
+ return () => {
6347
+ //
6348
+ };
6349
+ }
6350
+ if (initialisedHandler) {
6351
+ logger.warn('Network observer already initialised, doing nothing');
6352
+ return () => {
6353
+ // the first caller should already have this handler and will be responsible for teardown
6354
+ };
6355
+ }
6356
+ const networkOptions = options ? Object.assign({}, defaultNetworkOptions, options) : defaultNetworkOptions;
6357
+ const cb = data => {
6358
+ const requests = [];
6359
+ data.requests.forEach(request => {
6360
+ const maskedRequest = networkOptions.maskRequestFn(request);
6361
+ if (maskedRequest) {
6362
+ requests.push(maskedRequest);
6363
+ }
6364
+ });
6365
+ if (requests.length > 0) {
6366
+ callback({
6367
+ ...data,
6368
+ requests
6369
+ });
6370
+ }
6371
+ };
6372
+ const performanceObserver = initPerformanceObserver(cb, win, networkOptions);
6373
+ // only wrap fetch and xhr if headers or body are being recorded
6374
+ let xhrObserver = () => {};
6375
+ let fetchObserver = () => {};
6376
+ if (networkOptions.recordHeaders || networkOptions.recordBody) {
6377
+ xhrObserver = initXhrObserver(cb, win, networkOptions);
6378
+ fetchObserver = initFetchObserver(cb, win, networkOptions);
6379
+ }
6380
+ const teardown = () => {
6381
+ performanceObserver();
6382
+ xhrObserver();
6383
+ fetchObserver();
6384
+ // allow future observers to initialize after cleanup
6385
+ initialisedHandler = null;
6386
+ };
6387
+ initialisedHandler = teardown;
6388
+ return teardown;
6389
+ }
6390
+ // use the plugin name so that when this functionality is adopted into rrweb
6391
+ // we can remove this plugin and use the core functionality with the same data
6392
+ const NETWORK_PLUGIN_NAME = 'rrweb/network@1';
6393
+ // TODO how should this be typed?
6394
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
6395
+ // @ts-ignore
6396
+ const getRecordNetworkPlugin = options => {
6397
+ return {
6398
+ name: NETWORK_PLUGIN_NAME,
6399
+ observer: initNetworkObserver,
6400
+ options: options
6401
+ };
6402
+ };
6403
+ // rrweb/networ@1 ends
6404
+
6405
+ var networkPlugin = /*#__PURE__*/Object.freeze({
6406
+ __proto__: null,
6407
+ NETWORK_PLUGIN_NAME: NETWORK_PLUGIN_NAME,
6408
+ findLast: findLast,
6409
+ getRecordNetworkPlugin: getRecordNetworkPlugin,
6410
+ shouldRecordBody: shouldRecordBody
6411
+ });
6412
+
5662
6413
  exports.Leanbase = Leanbase;
5663
6414
  //# sourceMappingURL=index.cjs.map