@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.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.2";
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
  }
@@ -4102,7 +4122,7 @@ class LazyLoadedSessionRecording {
4102
4122
  this._eventTriggerMatching = new EventTriggerMatching(this._instance);
4103
4123
  this._buffer = this._clearBuffer();
4104
4124
  if (this._sessionIdleThresholdMilliseconds >= this._sessionManager.sessionTimeoutMs) {
4105
- 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`);
4106
4126
  }
4107
4127
  }
4108
4128
  get _masking() {
@@ -4176,25 +4196,62 @@ class LazyLoadedSessionRecording {
4176
4196
  payloadHostDenyList: networkPayloadCapture_server_side?.payloadHostDenyList
4177
4197
  };
4178
4198
  }
4179
- _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() {
4180
4231
  const plugins = [];
4232
+ if (!win) {
4233
+ return plugins;
4234
+ }
4181
4235
  if (this._isConsoleLogCaptureEnabled) {
4182
- 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
+ }
4183
4240
  }
4184
4241
  if (this._networkPayloadCapture) {
4185
4242
  const canRecordNetwork = !isLocalhost() || this._forceAllowLocalhostNetworkCapture;
4186
4243
  if (canRecordNetwork) {
4187
- const assignableWindow = globalThis;
4188
- const networkFactory = assignableWindow.__PosthogExtensions__?.rrwebPlugins?.getRecordNetworkPlugin?.();
4189
- if (typeof networkFactory === 'function') {
4190
- plugins.push(networkFactory(buildNetworkRequestOptions(this._instance.config, this._networkPayloadCapture)));
4191
- } else {
4192
- 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);
4193
4247
  }
4194
4248
  } else {
4195
- logger.info('NetworkCapture not started because we are on localhost.');
4249
+ this._debug('NetworkCapture not started because we are on localhost.');
4196
4250
  }
4197
4251
  }
4252
+ if (plugins.length > 0) {
4253
+ this._debug('Replay plugins loaded', plugins.map(p => p.name));
4254
+ }
4198
4255
  return plugins;
4199
4256
  }
4200
4257
  _maskUrl(url) {
@@ -4223,7 +4280,7 @@ class LazyLoadedSessionRecording {
4223
4280
  rrwebMethod: queuedRRWebEvent.rrwebMethod
4224
4281
  });
4225
4282
  } else {
4226
- logger.warn('could not emit queued rrweb event.', e, queuedRRWebEvent);
4283
+ logger$1.warn('could not emit queued rrweb event.', e, queuedRRWebEvent);
4227
4284
  }
4228
4285
  return false;
4229
4286
  }
@@ -4335,7 +4392,7 @@ class LazyLoadedSessionRecording {
4335
4392
  this._urlTriggerMatching.urlBlocked = true;
4336
4393
  // Clear the snapshot timer since we don't want new snapshots while paused
4337
4394
  clearInterval(this._fullSnapshotTimer);
4338
- logger.info('recording paused due to URL blocker');
4395
+ logger$1.info('recording paused due to URL blocker');
4339
4396
  this._tryAddCustomEvent('recording paused', {
4340
4397
  reason: 'url blocker'
4341
4398
  });
@@ -4354,7 +4411,7 @@ class LazyLoadedSessionRecording {
4354
4411
  this._tryAddCustomEvent('recording resumed', {
4355
4412
  reason: 'left blocked url'
4356
4413
  });
4357
- logger.info('recording resumed');
4414
+ logger$1.info('recording resumed');
4358
4415
  }
4359
4416
  _activateTrigger(triggerType) {
4360
4417
  if (!this.isStarted || !this._recording || !this._isFullyReady) {
@@ -4387,7 +4444,7 @@ class LazyLoadedSessionRecording {
4387
4444
  this._isFullyReady = false;
4388
4445
  const config = this._remoteConfig;
4389
4446
  if (!config) {
4390
- 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');
4391
4448
  return;
4392
4449
  }
4393
4450
  // We want to ensure the sessionManager is reset if necessary on loading the recorder
@@ -4470,7 +4527,7 @@ class LazyLoadedSessionRecording {
4470
4527
  });
4471
4528
  }
4472
4529
  } catch (e) {
4473
- logger.error('Could not add $pageview to rrweb session', e);
4530
+ logger$1.error('Could not add $pageview to rrweb session', e);
4474
4531
  }
4475
4532
  });
4476
4533
  }
@@ -4515,17 +4572,30 @@ class LazyLoadedSessionRecording {
4515
4572
  this._stopRrweb = undefined;
4516
4573
  this._isFullyReady = false;
4517
4574
  this._hasReportedRecordingInitialized = false;
4518
- logger.info('stopped');
4575
+ logger$1.info('stopped');
4519
4576
  }
4520
4577
  _snapshotIngestionUrl() {
4521
4578
  const endpointFor = this._instance?.requestRouter?.endpointFor;
4522
- 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) {
4523
4590
  return null;
4524
4591
  }
4525
4592
  try {
4526
- return endpointFor('api', this._endpoint);
4593
+ // eslint-disable-next-line compat/compat
4594
+ return new URL(this._endpoint, host).href;
4527
4595
  } catch {
4528
- 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}`;
4529
4599
  }
4530
4600
  }
4531
4601
  _canCaptureSnapshots() {
@@ -4610,7 +4680,7 @@ class LazyLoadedSessionRecording {
4610
4680
  }
4611
4681
  this._captureSnapshotBuffered(properties);
4612
4682
  } catch (e) {
4613
- logger.error('error processing rrweb event', e);
4683
+ logger$1.error('error processing rrweb event', e);
4614
4684
  }
4615
4685
  }
4616
4686
  get status() {
@@ -4701,7 +4771,7 @@ class LazyLoadedSessionRecording {
4701
4771
  if (!this._canCaptureSnapshots()) {
4702
4772
  if (!this._loggedMissingEndpointFor) {
4703
4773
  this._loggedMissingEndpointFor = true;
4704
- logger.warn('snapshot capture skipped because requestRouter.endpointFor is unavailable');
4774
+ logger$1.warn('snapshot capture skipped because requestRouter.endpointFor is unavailable');
4705
4775
  }
4706
4776
  this._flushBufferTimer = setTimeout(() => {
4707
4777
  this._flushBuffer();
@@ -4744,7 +4814,7 @@ class LazyLoadedSessionRecording {
4744
4814
  if (!url) {
4745
4815
  if (!this._loggedMissingEndpointFor) {
4746
4816
  this._loggedMissingEndpointFor = true;
4747
- logger.warn('snapshot capture skipped because requestRouter.endpointFor is unavailable');
4817
+ logger$1.warn('snapshot capture skipped because requestRouter.endpointFor is unavailable');
4748
4818
  }
4749
4819
  return;
4750
4820
  }
@@ -4757,7 +4827,7 @@ class LazyLoadedSessionRecording {
4757
4827
  skip_client_rate_limiting: true
4758
4828
  });
4759
4829
  } catch (e) {
4760
- logger.error('failed to capture snapshot', e);
4830
+ logger$1.error('failed to capture snapshot', e);
4761
4831
  }
4762
4832
  }
4763
4833
  _snapshotUrl() {
@@ -4816,9 +4886,9 @@ class LazyLoadedSessionRecording {
4816
4886
  });
4817
4887
  const message = startReason.replace('_', ' ');
4818
4888
  if (typeof tagPayload === 'undefined') {
4819
- logger.info(message);
4889
+ logger$1.info(message);
4820
4890
  } else {
4821
- logger.info(message, tagPayload);
4891
+ logger$1.info(message, tagPayload);
4822
4892
  }
4823
4893
  if (!includes(['recording_initialized', 'session_id_changed'], startReason)) {
4824
4894
  this._tryAddCustomEvent(startReason, tagPayload);
@@ -4919,7 +4989,7 @@ class LazyLoadedSessionRecording {
4919
4989
  if (shouldSample) {
4920
4990
  this._reportStarted(SAMPLED);
4921
4991
  } else {
4922
- 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.`);
4923
4993
  }
4924
4994
  this._tryAddCustomEvent('samplingDecisionMade', {
4925
4995
  sampleRate: currentSampleRate,
@@ -4942,7 +5012,7 @@ class LazyLoadedSessionRecording {
4942
5012
  this._activateTrigger('event');
4943
5013
  }
4944
5014
  } catch (e) {
4945
- logger.error('Could not activate event trigger', e);
5015
+ logger$1.error('Could not activate event trigger', e);
4946
5016
  }
4947
5017
  });
4948
5018
  }
@@ -5024,7 +5094,7 @@ class LazyLoadedSessionRecording {
5024
5094
  this._disablePermanently('rrweb record function unavailable');
5025
5095
  return;
5026
5096
  }
5027
- const activePlugins = this._gatherRRWebPlugins();
5097
+ const activePlugins = await this._gatherRRWebPlugins();
5028
5098
  let stopHandler;
5029
5099
  try {
5030
5100
  stopHandler = rrwebRecord({
@@ -5033,7 +5103,7 @@ class LazyLoadedSessionRecording {
5033
5103
  this.onRRwebEmit(event);
5034
5104
  } catch (e) {
5035
5105
  // never throw from rrweb emit handler
5036
- logger.error('error in rrweb emit handler', e);
5106
+ logger$1.error('error in rrweb emit handler', e);
5037
5107
  }
5038
5108
  },
5039
5109
  plugins: activePlugins,
@@ -5058,7 +5128,7 @@ class LazyLoadedSessionRecording {
5058
5128
  bucketSize: this._instance.config.session_recording?.__mutationThrottlerBucketSize,
5059
5129
  onBlockedNode: (id, node) => {
5060
5130
  const message = `Too many mutations on node '${id}'. Rate limiting. This could be due to SVG animations or something similar`;
5061
- logger.info(message, {
5131
+ logger$1.info(message, {
5062
5132
  node: node
5063
5133
  });
5064
5134
  this.log(LOGGER_PREFIX$1 + ' ' + message, 'warn');
@@ -5085,11 +5155,16 @@ class LazyLoadedSessionRecording {
5085
5155
  /* eslint-disable posthog-js/no-direct-function-check */
5086
5156
  const LOGGER_PREFIX = '[SessionRecording]';
5087
5157
  const log = {
5088
- info: (...args) => logger$2.info(LOGGER_PREFIX, ...args),
5089
- warn: (...args) => logger$2.warn(LOGGER_PREFIX, ...args),
5090
- 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)
5091
5161
  };
5092
5162
  class SessionRecording {
5163
+ _debug(...args) {
5164
+ if (this._instance?.config?.debug) {
5165
+ log.info(...args);
5166
+ }
5167
+ }
5093
5168
  get started() {
5094
5169
  return !!this._lazyLoadedSessionRecording?.isStarted;
5095
5170
  }
@@ -5127,8 +5202,10 @@ class SessionRecording {
5127
5202
  }
5128
5203
  const canRunReplay = !isUndefined(Object.assign) && !isUndefined(Array.from);
5129
5204
  if (this._isRecordingEnabled && canRunReplay) {
5205
+ this._debug('Session replay enabled; starting recorder');
5130
5206
  this._lazyLoadAndStart(startReason);
5131
5207
  } else {
5208
+ this._debug('Session replay disabled; stopping recorder');
5132
5209
  this.stopRecording();
5133
5210
  }
5134
5211
  }
@@ -5200,18 +5277,15 @@ class SessionRecording {
5200
5277
  log.info('skipping remote config with no sessionRecording', response);
5201
5278
  return;
5202
5279
  }
5203
- this._receivedFlags = true;
5204
- if (response.sessionRecording === false) {
5205
- return;
5206
- }
5207
5280
  this._persistRemoteConfig(response);
5281
+ this._receivedFlags = true;
5208
5282
  this.startIfEnabledOrStop();
5209
5283
  }
5210
5284
  log(message, level = 'log') {
5211
5285
  if (this._lazyLoadedSessionRecording?.log) {
5212
5286
  this._lazyLoadedSessionRecording.log(message, level);
5213
5287
  } else {
5214
- logger$2.warn('log called before recorder was ready');
5288
+ logger$3.warn('log called before recorder was ready');
5215
5289
  }
5216
5290
  }
5217
5291
  get _scriptName() {
@@ -5234,10 +5308,10 @@ class SessionRecording {
5234
5308
  try {
5235
5309
  const maybePromise = this._lazyLoadedSessionRecording.start(startReason);
5236
5310
  if (maybePromise && typeof maybePromise.catch === 'function') {
5237
- maybePromise.catch(e => logger$2.error('error starting session recording', e));
5311
+ maybePromise.catch(e => logger$3.error('error starting session recording', e));
5238
5312
  }
5239
5313
  } catch (e) {
5240
- logger$2.error('error starting session recording', e);
5314
+ logger$3.error('error starting session recording', e);
5241
5315
  }
5242
5316
  return;
5243
5317
  }
@@ -5250,10 +5324,10 @@ class SessionRecording {
5250
5324
  try {
5251
5325
  const maybePromise = this._lazyLoadedSessionRecording.start(startReason);
5252
5326
  if (maybePromise && typeof maybePromise.catch === 'function') {
5253
- maybePromise.catch(e => logger$2.error('error starting session recording', e));
5327
+ maybePromise.catch(e => logger$3.error('error starting session recording', e));
5254
5328
  }
5255
5329
  } catch (e) {
5256
- logger$2.error('error starting session recording', e);
5330
+ logger$3.error('error starting session recording', e);
5257
5331
  }
5258
5332
  }
5259
5333
  /**
@@ -5410,12 +5484,12 @@ class Leanbase extends PostHogCore {
5410
5484
  }, 1);
5411
5485
  }
5412
5486
  const triggerRemoteConfigLoad = reason => {
5413
- logger$2.info(`remote config load triggered via ${reason}`);
5487
+ logger$3.info(`remote config load triggered via ${reason}`);
5414
5488
  void this.loadRemoteConfig();
5415
5489
  };
5416
5490
  if (document) {
5417
5491
  if (document.readyState === 'loading') {
5418
- logger$2.info('remote config load deferred until DOMContentLoaded');
5492
+ logger$3.info('remote config load deferred until DOMContentLoaded');
5419
5493
  const onDomReady = () => {
5420
5494
  document?.removeEventListener('DOMContentLoaded', onDomReady);
5421
5495
  triggerRemoteConfigLoad('dom');
@@ -5508,7 +5582,7 @@ class Leanbase extends PostHogCore {
5508
5582
  try {
5509
5583
  this.sessionRecording.startIfEnabledOrStop();
5510
5584
  } catch (e) {
5511
- logger$2.error('Failed to start session recording', e);
5585
+ logger$3.error('Failed to start session recording', e);
5512
5586
  }
5513
5587
  }
5514
5588
  fetch(url, options) {
@@ -5574,7 +5648,7 @@ class Leanbase extends PostHogCore {
5574
5648
  };
5575
5649
  properties['distinct_id'] = persistenceProps.distinct_id;
5576
5650
  if (!(isString(properties['distinct_id']) || isNumber(properties['distinct_id'])) || isEmptyString(properties['distinct_id'])) {
5577
- 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');
5578
5652
  }
5579
5653
  return properties;
5580
5654
  }
@@ -5649,11 +5723,11 @@ class Leanbase extends PostHogCore {
5649
5723
  return;
5650
5724
  }
5651
5725
  if (isUndefined(event) || !isString(event)) {
5652
- logger$2.error('No event name provided to posthog.capture');
5726
+ logger$3.error('No event name provided to posthog.capture');
5653
5727
  return;
5654
5728
  }
5655
5729
  if (properties?.$current_url && !isString(properties?.$current_url)) {
5656
- 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.');
5657
5731
  delete properties?.$current_url;
5658
5732
  }
5659
5733
  this.sessionPersistence.update_search_keyword();
@@ -5705,5 +5779,634 @@ class Leanbase extends PostHogCore {
5705
5779
  }
5706
5780
  }
5707
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
+
5708
6411
  export { Leanbase };
5709
6412
  //# sourceMappingURL=index.mjs.map