@leanbase-giangnd/js 0.3.2 → 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.2";
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
  }
@@ -4104,7 +4124,7 @@ class LazyLoadedSessionRecording {
4104
4124
  this._eventTriggerMatching = new EventTriggerMatching(this._instance);
4105
4125
  this._buffer = this._clearBuffer();
4106
4126
  if (this._sessionIdleThresholdMilliseconds >= this._sessionManager.sessionTimeoutMs) {
4107
- 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`);
4108
4128
  }
4109
4129
  }
4110
4130
  get _masking() {
@@ -4178,25 +4198,62 @@ class LazyLoadedSessionRecording {
4178
4198
  payloadHostDenyList: networkPayloadCapture_server_side?.payloadHostDenyList
4179
4199
  };
4180
4200
  }
4181
- _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() {
4182
4233
  const plugins = [];
4234
+ if (!win) {
4235
+ return plugins;
4236
+ }
4183
4237
  if (this._isConsoleLogCaptureEnabled) {
4184
- 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
+ }
4185
4242
  }
4186
4243
  if (this._networkPayloadCapture) {
4187
4244
  const canRecordNetwork = !isLocalhost() || this._forceAllowLocalhostNetworkCapture;
4188
4245
  if (canRecordNetwork) {
4189
- const assignableWindow = globalThis;
4190
- const networkFactory = assignableWindow.__PosthogExtensions__?.rrwebPlugins?.getRecordNetworkPlugin?.();
4191
- if (typeof networkFactory === 'function') {
4192
- plugins.push(networkFactory(buildNetworkRequestOptions(this._instance.config, this._networkPayloadCapture)));
4193
- } else {
4194
- 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);
4195
4249
  }
4196
4250
  } else {
4197
- logger.info('NetworkCapture not started because we are on localhost.');
4251
+ this._debug('NetworkCapture not started because we are on localhost.');
4198
4252
  }
4199
4253
  }
4254
+ if (plugins.length > 0) {
4255
+ this._debug('Replay plugins loaded', plugins.map(p => p.name));
4256
+ }
4200
4257
  return plugins;
4201
4258
  }
4202
4259
  _maskUrl(url) {
@@ -4225,7 +4282,7 @@ class LazyLoadedSessionRecording {
4225
4282
  rrwebMethod: queuedRRWebEvent.rrwebMethod
4226
4283
  });
4227
4284
  } else {
4228
- logger.warn('could not emit queued rrweb event.', e, queuedRRWebEvent);
4285
+ logger$1.warn('could not emit queued rrweb event.', e, queuedRRWebEvent);
4229
4286
  }
4230
4287
  return false;
4231
4288
  }
@@ -4337,7 +4394,7 @@ class LazyLoadedSessionRecording {
4337
4394
  this._urlTriggerMatching.urlBlocked = true;
4338
4395
  // Clear the snapshot timer since we don't want new snapshots while paused
4339
4396
  clearInterval(this._fullSnapshotTimer);
4340
- logger.info('recording paused due to URL blocker');
4397
+ logger$1.info('recording paused due to URL blocker');
4341
4398
  this._tryAddCustomEvent('recording paused', {
4342
4399
  reason: 'url blocker'
4343
4400
  });
@@ -4356,7 +4413,7 @@ class LazyLoadedSessionRecording {
4356
4413
  this._tryAddCustomEvent('recording resumed', {
4357
4414
  reason: 'left blocked url'
4358
4415
  });
4359
- logger.info('recording resumed');
4416
+ logger$1.info('recording resumed');
4360
4417
  }
4361
4418
  _activateTrigger(triggerType) {
4362
4419
  if (!this.isStarted || !this._recording || !this._isFullyReady) {
@@ -4389,7 +4446,7 @@ class LazyLoadedSessionRecording {
4389
4446
  this._isFullyReady = false;
4390
4447
  const config = this._remoteConfig;
4391
4448
  if (!config) {
4392
- 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');
4393
4450
  return;
4394
4451
  }
4395
4452
  // We want to ensure the sessionManager is reset if necessary on loading the recorder
@@ -4472,7 +4529,7 @@ class LazyLoadedSessionRecording {
4472
4529
  });
4473
4530
  }
4474
4531
  } catch (e) {
4475
- logger.error('Could not add $pageview to rrweb session', e);
4532
+ logger$1.error('Could not add $pageview to rrweb session', e);
4476
4533
  }
4477
4534
  });
4478
4535
  }
@@ -4517,17 +4574,30 @@ class LazyLoadedSessionRecording {
4517
4574
  this._stopRrweb = undefined;
4518
4575
  this._isFullyReady = false;
4519
4576
  this._hasReportedRecordingInitialized = false;
4520
- logger.info('stopped');
4577
+ logger$1.info('stopped');
4521
4578
  }
4522
4579
  _snapshotIngestionUrl() {
4523
4580
  const endpointFor = this._instance?.requestRouter?.endpointFor;
4524
- 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) {
4525
4592
  return null;
4526
4593
  }
4527
4594
  try {
4528
- return endpointFor('api', this._endpoint);
4595
+ // eslint-disable-next-line compat/compat
4596
+ return new URL(this._endpoint, host).href;
4529
4597
  } catch {
4530
- 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}`;
4531
4601
  }
4532
4602
  }
4533
4603
  _canCaptureSnapshots() {
@@ -4612,7 +4682,7 @@ class LazyLoadedSessionRecording {
4612
4682
  }
4613
4683
  this._captureSnapshotBuffered(properties);
4614
4684
  } catch (e) {
4615
- logger.error('error processing rrweb event', e);
4685
+ logger$1.error('error processing rrweb event', e);
4616
4686
  }
4617
4687
  }
4618
4688
  get status() {
@@ -4703,7 +4773,7 @@ class LazyLoadedSessionRecording {
4703
4773
  if (!this._canCaptureSnapshots()) {
4704
4774
  if (!this._loggedMissingEndpointFor) {
4705
4775
  this._loggedMissingEndpointFor = true;
4706
- logger.warn('snapshot capture skipped because requestRouter.endpointFor is unavailable');
4776
+ logger$1.warn('snapshot capture skipped because requestRouter.endpointFor is unavailable');
4707
4777
  }
4708
4778
  this._flushBufferTimer = setTimeout(() => {
4709
4779
  this._flushBuffer();
@@ -4746,7 +4816,7 @@ class LazyLoadedSessionRecording {
4746
4816
  if (!url) {
4747
4817
  if (!this._loggedMissingEndpointFor) {
4748
4818
  this._loggedMissingEndpointFor = true;
4749
- logger.warn('snapshot capture skipped because requestRouter.endpointFor is unavailable');
4819
+ logger$1.warn('snapshot capture skipped because requestRouter.endpointFor is unavailable');
4750
4820
  }
4751
4821
  return;
4752
4822
  }
@@ -4759,7 +4829,7 @@ class LazyLoadedSessionRecording {
4759
4829
  skip_client_rate_limiting: true
4760
4830
  });
4761
4831
  } catch (e) {
4762
- logger.error('failed to capture snapshot', e);
4832
+ logger$1.error('failed to capture snapshot', e);
4763
4833
  }
4764
4834
  }
4765
4835
  _snapshotUrl() {
@@ -4818,9 +4888,9 @@ class LazyLoadedSessionRecording {
4818
4888
  });
4819
4889
  const message = startReason.replace('_', ' ');
4820
4890
  if (typeof tagPayload === 'undefined') {
4821
- logger.info(message);
4891
+ logger$1.info(message);
4822
4892
  } else {
4823
- logger.info(message, tagPayload);
4893
+ logger$1.info(message, tagPayload);
4824
4894
  }
4825
4895
  if (!core.includes(['recording_initialized', 'session_id_changed'], startReason)) {
4826
4896
  this._tryAddCustomEvent(startReason, tagPayload);
@@ -4921,7 +4991,7 @@ class LazyLoadedSessionRecording {
4921
4991
  if (shouldSample) {
4922
4992
  this._reportStarted(SAMPLED);
4923
4993
  } else {
4924
- 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.`);
4925
4995
  }
4926
4996
  this._tryAddCustomEvent('samplingDecisionMade', {
4927
4997
  sampleRate: currentSampleRate,
@@ -4944,7 +5014,7 @@ class LazyLoadedSessionRecording {
4944
5014
  this._activateTrigger('event');
4945
5015
  }
4946
5016
  } catch (e) {
4947
- logger.error('Could not activate event trigger', e);
5017
+ logger$1.error('Could not activate event trigger', e);
4948
5018
  }
4949
5019
  });
4950
5020
  }
@@ -5026,7 +5096,7 @@ class LazyLoadedSessionRecording {
5026
5096
  this._disablePermanently('rrweb record function unavailable');
5027
5097
  return;
5028
5098
  }
5029
- const activePlugins = this._gatherRRWebPlugins();
5099
+ const activePlugins = await this._gatherRRWebPlugins();
5030
5100
  let stopHandler;
5031
5101
  try {
5032
5102
  stopHandler = rrwebRecord({
@@ -5035,7 +5105,7 @@ class LazyLoadedSessionRecording {
5035
5105
  this.onRRwebEmit(event);
5036
5106
  } catch (e) {
5037
5107
  // never throw from rrweb emit handler
5038
- logger.error('error in rrweb emit handler', e);
5108
+ logger$1.error('error in rrweb emit handler', e);
5039
5109
  }
5040
5110
  },
5041
5111
  plugins: activePlugins,
@@ -5060,7 +5130,7 @@ class LazyLoadedSessionRecording {
5060
5130
  bucketSize: this._instance.config.session_recording?.__mutationThrottlerBucketSize,
5061
5131
  onBlockedNode: (id, node) => {
5062
5132
  const message = `Too many mutations on node '${id}'. Rate limiting. This could be due to SVG animations or something similar`;
5063
- logger.info(message, {
5133
+ logger$1.info(message, {
5064
5134
  node: node
5065
5135
  });
5066
5136
  this.log(LOGGER_PREFIX$1 + ' ' + message, 'warn');
@@ -5087,11 +5157,16 @@ class LazyLoadedSessionRecording {
5087
5157
  /* eslint-disable posthog-js/no-direct-function-check */
5088
5158
  const LOGGER_PREFIX = '[SessionRecording]';
5089
5159
  const log = {
5090
- info: (...args) => logger$2.info(LOGGER_PREFIX, ...args),
5091
- warn: (...args) => logger$2.warn(LOGGER_PREFIX, ...args),
5092
- 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)
5093
5163
  };
5094
5164
  class SessionRecording {
5165
+ _debug(...args) {
5166
+ if (this._instance?.config?.debug) {
5167
+ log.info(...args);
5168
+ }
5169
+ }
5095
5170
  get started() {
5096
5171
  return !!this._lazyLoadedSessionRecording?.isStarted;
5097
5172
  }
@@ -5129,8 +5204,10 @@ class SessionRecording {
5129
5204
  }
5130
5205
  const canRunReplay = !core.isUndefined(Object.assign) && !core.isUndefined(Array.from);
5131
5206
  if (this._isRecordingEnabled && canRunReplay) {
5207
+ this._debug('Session replay enabled; starting recorder');
5132
5208
  this._lazyLoadAndStart(startReason);
5133
5209
  } else {
5210
+ this._debug('Session replay disabled; stopping recorder');
5134
5211
  this.stopRecording();
5135
5212
  }
5136
5213
  }
@@ -5202,18 +5279,15 @@ class SessionRecording {
5202
5279
  log.info('skipping remote config with no sessionRecording', response);
5203
5280
  return;
5204
5281
  }
5205
- this._receivedFlags = true;
5206
- if (response.sessionRecording === false) {
5207
- return;
5208
- }
5209
5282
  this._persistRemoteConfig(response);
5283
+ this._receivedFlags = true;
5210
5284
  this.startIfEnabledOrStop();
5211
5285
  }
5212
5286
  log(message, level = 'log') {
5213
5287
  if (this._lazyLoadedSessionRecording?.log) {
5214
5288
  this._lazyLoadedSessionRecording.log(message, level);
5215
5289
  } else {
5216
- logger$2.warn('log called before recorder was ready');
5290
+ logger$3.warn('log called before recorder was ready');
5217
5291
  }
5218
5292
  }
5219
5293
  get _scriptName() {
@@ -5236,10 +5310,10 @@ class SessionRecording {
5236
5310
  try {
5237
5311
  const maybePromise = this._lazyLoadedSessionRecording.start(startReason);
5238
5312
  if (maybePromise && typeof maybePromise.catch === 'function') {
5239
- maybePromise.catch(e => logger$2.error('error starting session recording', e));
5313
+ maybePromise.catch(e => logger$3.error('error starting session recording', e));
5240
5314
  }
5241
5315
  } catch (e) {
5242
- logger$2.error('error starting session recording', e);
5316
+ logger$3.error('error starting session recording', e);
5243
5317
  }
5244
5318
  return;
5245
5319
  }
@@ -5252,10 +5326,10 @@ class SessionRecording {
5252
5326
  try {
5253
5327
  const maybePromise = this._lazyLoadedSessionRecording.start(startReason);
5254
5328
  if (maybePromise && typeof maybePromise.catch === 'function') {
5255
- maybePromise.catch(e => logger$2.error('error starting session recording', e));
5329
+ maybePromise.catch(e => logger$3.error('error starting session recording', e));
5256
5330
  }
5257
5331
  } catch (e) {
5258
- logger$2.error('error starting session recording', e);
5332
+ logger$3.error('error starting session recording', e);
5259
5333
  }
5260
5334
  }
5261
5335
  /**
@@ -5412,12 +5486,12 @@ class Leanbase extends core.PostHogCore {
5412
5486
  }, 1);
5413
5487
  }
5414
5488
  const triggerRemoteConfigLoad = reason => {
5415
- logger$2.info(`remote config load triggered via ${reason}`);
5489
+ logger$3.info(`remote config load triggered via ${reason}`);
5416
5490
  void this.loadRemoteConfig();
5417
5491
  };
5418
5492
  if (document) {
5419
5493
  if (document.readyState === 'loading') {
5420
- logger$2.info('remote config load deferred until DOMContentLoaded');
5494
+ logger$3.info('remote config load deferred until DOMContentLoaded');
5421
5495
  const onDomReady = () => {
5422
5496
  document?.removeEventListener('DOMContentLoaded', onDomReady);
5423
5497
  triggerRemoteConfigLoad('dom');
@@ -5510,7 +5584,7 @@ class Leanbase extends core.PostHogCore {
5510
5584
  try {
5511
5585
  this.sessionRecording.startIfEnabledOrStop();
5512
5586
  } catch (e) {
5513
- logger$2.error('Failed to start session recording', e);
5587
+ logger$3.error('Failed to start session recording', e);
5514
5588
  }
5515
5589
  }
5516
5590
  fetch(url, options) {
@@ -5576,7 +5650,7 @@ class Leanbase extends core.PostHogCore {
5576
5650
  };
5577
5651
  properties['distinct_id'] = persistenceProps.distinct_id;
5578
5652
  if (!(core.isString(properties['distinct_id']) || core.isNumber(properties['distinct_id'])) || core.isEmptyString(properties['distinct_id'])) {
5579
- 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');
5580
5654
  }
5581
5655
  return properties;
5582
5656
  }
@@ -5651,11 +5725,11 @@ class Leanbase extends core.PostHogCore {
5651
5725
  return;
5652
5726
  }
5653
5727
  if (core.isUndefined(event) || !core.isString(event)) {
5654
- logger$2.error('No event name provided to posthog.capture');
5728
+ logger$3.error('No event name provided to posthog.capture');
5655
5729
  return;
5656
5730
  }
5657
5731
  if (properties?.$current_url && !core.isString(properties?.$current_url)) {
5658
- 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.');
5659
5733
  delete properties?.$current_url;
5660
5734
  }
5661
5735
  this.sessionPersistence.update_search_keyword();
@@ -5707,5 +5781,634 @@ class Leanbase extends core.PostHogCore {
5707
5781
  }
5708
5782
  }
5709
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
+
5710
6413
  exports.Leanbase = Leanbase;
5711
6414
  //# sourceMappingURL=index.cjs.map