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