@leanbase-giangnd/js 0.0.7 → 0.1.1

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.
@@ -1,4 +1,4 @@
1
- var leanbase = (function () {
1
+ var leanbase = (function (record) {
2
2
  'use strict';
3
3
 
4
4
  const normalizeFlagsResponse = (flagsResponse)=>{
@@ -329,6 +329,7 @@ var leanbase = (function () {
329
329
  const isNumber = (x)=>'[object Number]' == type_utils_toString.call(x);
330
330
  const isBoolean = (x)=>'[object Boolean]' === type_utils_toString.call(x);
331
331
  const isFormData = (x)=>x instanceof FormData;
332
+ const isFile = (x)=>x instanceof File;
332
333
  const isPlainError = (x)=>x instanceof Error;
333
334
 
334
335
  function clampToRange(value, min, max, logger, fallbackValue) {
@@ -348,6 +349,48 @@ var leanbase = (function () {
348
349
  return clampToRange(fallbackValue || max, min, max, logger);
349
350
  }
350
351
 
352
+ class BucketedRateLimiter {
353
+ constructor(_options){
354
+ this._options = _options;
355
+ this._buckets = {};
356
+ this._refillBuckets = ()=>{
357
+ Object.keys(this._buckets).forEach((key)=>{
358
+ const newTokens = this._getBucket(key) + this._refillRate;
359
+ if (newTokens >= this._bucketSize) delete this._buckets[key];
360
+ else this._setBucket(key, newTokens);
361
+ });
362
+ };
363
+ this._getBucket = (key)=>this._buckets[String(key)];
364
+ this._setBucket = (key, value)=>{
365
+ this._buckets[String(key)] = value;
366
+ };
367
+ this.consumeRateLimit = (key)=>{
368
+ let tokens = this._getBucket(key);
369
+ if (void 0 === tokens) tokens = this._bucketSize;
370
+ if (tokens <= 0) {
371
+ this._onBucketRateLimited?.(key);
372
+ return true;
373
+ }
374
+ tokens -= 1;
375
+ this._setBucket(key, tokens);
376
+ return false;
377
+ };
378
+ this._onBucketRateLimited = this._options._onBucketRateLimited;
379
+ this._bucketSize = clampToRange(this._options.bucketSize, 0, 100, this._options._logger);
380
+ this._refillRate = clampToRange(this._options.refillRate, 0, this._bucketSize, this._options._logger);
381
+ this._refillInterval = clampToRange(this._options.refillInterval, 0, 86400000, this._options._logger);
382
+ this._removeInterval = setInterval(()=>{
383
+ this._refillBuckets();
384
+ }, this._refillInterval);
385
+ }
386
+ stop() {
387
+ if (this._removeInterval) {
388
+ clearInterval(this._removeInterval);
389
+ this._removeInterval = void 0;
390
+ }
391
+ }
392
+ }
393
+
351
394
  class PromiseQueue {
352
395
  add(promise) {
353
396
  const promiseUUID = uuidv7$1();
@@ -465,7 +508,7 @@ var leanbase = (function () {
465
508
  };
466
509
  return lockedMethods;
467
510
  }
468
- const _createLogger = (prefix, maybeCall, consoleLike)=>{
511
+ const _createLogger$1 = (prefix, maybeCall, consoleLike)=>{
469
512
  function _log(level, ...args) {
470
513
  maybeCall(()=>{
471
514
  const consoleMethod = consoleLike[level];
@@ -485,12 +528,12 @@ var leanbase = (function () {
485
528
  critical: (...args)=>{
486
529
  consoleLike['error'](prefix, ...args);
487
530
  },
488
- createLogger: (additionalPrefix)=>_createLogger(`${prefix} ${additionalPrefix}`, maybeCall, consoleLike)
531
+ createLogger: (additionalPrefix)=>_createLogger$1(`${prefix} ${additionalPrefix}`, maybeCall, consoleLike)
489
532
  };
490
533
  return logger;
491
534
  };
492
- function createLogger(prefix, maybeCall) {
493
- return _createLogger(prefix, maybeCall, createConsole());
535
+ function createLogger$1(prefix, maybeCall) {
536
+ return _createLogger$1(prefix, maybeCall, createConsole());
494
537
  }
495
538
 
496
539
  class PostHogFetchHttpError extends Error {
@@ -569,7 +612,7 @@ var leanbase = (function () {
569
612
  this.evaluationEnvironments = options?.evaluationEnvironments;
570
613
  this._initPromise = Promise.resolve();
571
614
  this._isInitialized = true;
572
- this._logger = createLogger('[PostHog]', this.logMsgIfDebug.bind(this));
615
+ this._logger = createLogger$1('[PostHog]', this.logMsgIfDebug.bind(this));
573
616
  this.disableCompression = !isGzipSupported() || (options?.disableCompression ?? false);
574
617
  }
575
618
  logMsgIfDebug(fn) {
@@ -1647,7 +1690,7 @@ var leanbase = (function () {
1647
1690
  const global = typeof globalThis !== 'undefined' ? globalThis : win;
1648
1691
  const navigator$1 = global?.navigator;
1649
1692
  const document = global?.document;
1650
- const location = global?.location;
1693
+ const location$1 = global?.location;
1651
1694
  global?.fetch;
1652
1695
  global?.XMLHttpRequest && 'withCredentials' in new global.XMLHttpRequest() ? global.XMLHttpRequest : undefined;
1653
1696
  global?.AbortController;
@@ -1834,10 +1877,13 @@ var leanbase = (function () {
1834
1877
  const AUTOCAPTURE_DISABLED_SERVER_SIDE = '$autocapture_disabled_server_side';
1835
1878
  const HEATMAPS_ENABLED_SERVER_SIDE = '$heatmaps_enabled_server_side';
1836
1879
  const ERROR_TRACKING_SUPPRESSION_RULES = '$error_tracking_suppression_rules';
1880
+ const SESSION_RECORDING_REMOTE_CONFIG = '$session_recording_remote_config';
1837
1881
  // @deprecated can be removed along with eager loaded replay
1838
1882
  const SESSION_RECORDING_ENABLED_SERVER_SIDE = '$session_recording_enabled_server_side';
1839
1883
  const SESSION_ID = '$sesid';
1840
1884
  const SESSION_RECORDING_IS_SAMPLED = '$session_is_sampled';
1885
+ const SESSION_RECORDING_URL_TRIGGER_ACTIVATED_SESSION = '$session_recording_url_trigger_activated_session';
1886
+ const SESSION_RECORDING_EVENT_TRIGGER_ACTIVATED_SESSION = '$session_recording_event_trigger_activated_session';
1841
1887
  const ENABLED_FEATURE_FLAGS = '$enabled_feature_flags';
1842
1888
  const PERSISTENCE_EARLY_ACCESS_FEATURES = '$early_access_features';
1843
1889
  const PERSISTENCE_FEATURE_FLAG_DETAILS = '$feature_flag_details';
@@ -1862,7 +1908,7 @@ var leanbase = (function () {
1862
1908
 
1863
1909
  /* eslint-disable no-console */
1864
1910
  const PREFIX = '[Leanbase]';
1865
- const logger = {
1911
+ const logger$3 = {
1866
1912
  info: (...args) => {
1867
1913
  if (typeof console !== 'undefined') {
1868
1914
  console.log(PREFIX, ...args);
@@ -2160,7 +2206,7 @@ var leanbase = (function () {
2160
2206
  if (!matchedSubDomain) {
2161
2207
  const originalMatch = originalCookieDomainFn(hostname);
2162
2208
  if (originalMatch !== matchedSubDomain) {
2163
- logger.info('Warning: cookie subdomain discovery mismatch', originalMatch, matchedSubDomain);
2209
+ logger$3.info('Warning: cookie subdomain discovery mismatch', originalMatch, matchedSubDomain);
2164
2210
  }
2165
2211
  matchedSubDomain = originalMatch;
2166
2212
  }
@@ -2172,7 +2218,7 @@ var leanbase = (function () {
2172
2218
  const cookieStore = {
2173
2219
  _is_supported: () => !!document,
2174
2220
  _error: function (msg) {
2175
- logger.error('cookieStore error: ' + msg);
2221
+ logger$3.error('cookieStore error: ' + msg);
2176
2222
  },
2177
2223
  _get: function (name) {
2178
2224
  if (!document) {
@@ -2221,7 +2267,7 @@ var leanbase = (function () {
2221
2267
  const new_cookie_val = name + '=' + encodeURIComponent(JSON.stringify(value)) + expires + '; SameSite=Lax; path=/' + cdomain + secure;
2222
2268
  // 4096 bytes is the size at which some browsers (e.g. firefox) will not store a cookie, warn slightly before that
2223
2269
  if (new_cookie_val.length > 4096 * 0.9) {
2224
- logger.warn('cookieStore warning: large cookie, len=' + new_cookie_val.length);
2270
+ logger$3.warn('cookieStore warning: large cookie, len=' + new_cookie_val.length);
2225
2271
  }
2226
2272
  document.cookie = new_cookie_val;
2227
2273
  return new_cookie_val;
@@ -2263,13 +2309,13 @@ var leanbase = (function () {
2263
2309
  supported = false;
2264
2310
  }
2265
2311
  if (!supported) {
2266
- logger.error('localStorage unsupported; falling back to cookie store');
2312
+ logger$3.error('localStorage unsupported; falling back to cookie store');
2267
2313
  }
2268
2314
  _localStorage_supported = supported;
2269
2315
  return supported;
2270
2316
  },
2271
2317
  _error: function (msg) {
2272
- logger.error('localStorage error: ' + msg);
2318
+ logger$3.error('localStorage error: ' + msg);
2273
2319
  },
2274
2320
  _get: function (name) {
2275
2321
  try {
@@ -2355,7 +2401,7 @@ var leanbase = (function () {
2355
2401
  return true;
2356
2402
  },
2357
2403
  _error: function (msg) {
2358
- logger.error('memoryStorage error: ' + msg);
2404
+ logger$3.error('memoryStorage error: ' + msg);
2359
2405
  },
2360
2406
  _get: function (name) {
2361
2407
  return memoryStorage[name] || null;
@@ -2396,7 +2442,7 @@ var leanbase = (function () {
2396
2442
  return sessionStorageSupported;
2397
2443
  },
2398
2444
  _error: function (msg) {
2399
- logger.error('sessionStorage error: ', msg);
2445
+ logger$3.error('sessionStorage error: ', msg);
2400
2446
  },
2401
2447
  _get: function (name) {
2402
2448
  try {
@@ -2430,6 +2476,7 @@ var leanbase = (function () {
2430
2476
  }
2431
2477
  };
2432
2478
 
2479
+ const localDomains = ['localhost', '127.0.0.1'];
2433
2480
  /**
2434
2481
  * IE11 doesn't support `new URL`
2435
2482
  * so we can create an anchor element and use that to parse the URL
@@ -2444,6 +2491,21 @@ var leanbase = (function () {
2444
2491
  location.href = url;
2445
2492
  return location;
2446
2493
  };
2494
+ const formDataToQuery = function (formdata, arg_separator = '&') {
2495
+ let use_val;
2496
+ let use_key;
2497
+ const tph_arr = [];
2498
+ each(formdata, function (val, key) {
2499
+ // the key might be literally the string undefined for e.g. if {undefined: 'something'}
2500
+ if (isUndefined(val) || isUndefined(key) || key === 'undefined') {
2501
+ return;
2502
+ }
2503
+ use_val = encodeURIComponent(isFile(val) ? val.name : val.toString());
2504
+ use_key = encodeURIComponent(key);
2505
+ tph_arr[tph_arr.length] = use_key + '=' + use_val;
2506
+ });
2507
+ return tph_arr.join(arg_separator);
2508
+ };
2447
2509
  // NOTE: Once we get rid of IE11/op_mini we can start using URLSearchParams
2448
2510
  const getQueryParam = function (url, param) {
2449
2511
  const withoutHash = url.split('#')[0] || '';
@@ -2467,7 +2529,7 @@ var leanbase = (function () {
2467
2529
  try {
2468
2530
  result = decodeURIComponent(result);
2469
2531
  } catch {
2470
- logger.error('Skipping decoding for malformed query param: ' + result);
2532
+ logger$3.error('Skipping decoding for malformed query param: ' + result);
2471
2533
  }
2472
2534
  return result.replace(/\+/g, ' ');
2473
2535
  }
@@ -2506,6 +2568,9 @@ var leanbase = (function () {
2506
2568
  }
2507
2569
  return result;
2508
2570
  };
2571
+ const isLocalhost = () => {
2572
+ return localDomains.includes(location.hostname);
2573
+ };
2509
2574
 
2510
2575
  /**
2511
2576
  * this device detection code is (at time of writing) about 3% of the size of the entire library
@@ -2798,7 +2863,7 @@ var leanbase = (function () {
2798
2863
  }
2799
2864
  };
2800
2865
 
2801
- var version = "0.0.7";
2866
+ var version = "0.1.1";
2802
2867
  var packageInfo = {
2803
2868
  version: version};
2804
2869
 
@@ -2948,7 +3013,7 @@ var leanbase = (function () {
2948
3013
  }
2949
3014
  function getPersonInfo(maskPersonalDataProperties, customPersonalDataProperties) {
2950
3015
  const paramsToMask = maskPersonalDataProperties ? extendArray([], PERSONAL_DATA_CAMPAIGN_PARAMS, customPersonalDataProperties || []) : [];
2951
- const url = location?.href.substring(0, 1000);
3016
+ const url = location$1?.href.substring(0, 1000);
2952
3017
  // we're being a bit more economical with bytes here because this is stored in the cookie
2953
3018
  return {
2954
3019
  r: getReferrer().substring(0, 1000),
@@ -3016,9 +3081,9 @@ var leanbase = (function () {
3016
3081
  $timezone: getTimezone(),
3017
3082
  $timezone_offset: getTimezoneOffset()
3018
3083
  }), {
3019
- $current_url: maskQueryParams(location?.href, paramsToMask, MASKED),
3020
- $host: location?.host,
3021
- $pathname: location?.pathname,
3084
+ $current_url: maskQueryParams(location$1?.href, paramsToMask, MASKED),
3085
+ $host: location$1?.host,
3086
+ $pathname: location$1?.pathname,
3022
3087
  $raw_user_agent: userAgent.length > 1000 ? userAgent.substring(0, 997) + '...' : userAgent,
3023
3088
  $browser_version: detectBrowserVersion(userAgent, navigator.vendor),
3024
3089
  $browser_language: getBrowserLanguage(),
@@ -3063,7 +3128,7 @@ var leanbase = (function () {
3063
3128
  this._storage = this._buildStorage(config);
3064
3129
  this.load();
3065
3130
  if (config.debug) {
3066
- logger.info('Persistence loaded', config['persistence'], {
3131
+ logger$3.info('Persistence loaded', config['persistence'], {
3067
3132
  ...this.props
3068
3133
  });
3069
3134
  }
@@ -3079,7 +3144,7 @@ var leanbase = (function () {
3079
3144
  }
3080
3145
  _buildStorage(config) {
3081
3146
  if (CASE_INSENSITIVE_PERSISTENCE_TYPES.indexOf(config['persistence'].toLowerCase()) === -1) {
3082
- logger.info('Unknown persistence type ' + config['persistence'] + '; falling back to localStorage+cookie');
3147
+ logger$3.info('Unknown persistence type ' + config['persistence'] + '; falling back to localStorage+cookie');
3083
3148
  config['persistence'] = 'localStorage+cookie';
3084
3149
  }
3085
3150
  let store;
@@ -3715,7 +3780,7 @@ var leanbase = (function () {
3715
3780
  text = `${text} ${getNestedSpanText(child)}`.trim();
3716
3781
  }
3717
3782
  } catch (e) {
3718
- logger.error('[AutoCapture]', e);
3783
+ logger$3.error('[AutoCapture]', e);
3719
3784
  }
3720
3785
  }
3721
3786
  });
@@ -4017,7 +4082,7 @@ var leanbase = (function () {
4017
4082
  }
4018
4083
  _addDomEventHandlers() {
4019
4084
  if (!this.isBrowserSupported()) {
4020
- logger.info('Disabling Automatic Event Collection because this browser is not supported');
4085
+ logger$3.info('Disabling Automatic Event Collection because this browser is not supported');
4021
4086
  return;
4022
4087
  }
4023
4088
  if (!win || !document) {
@@ -4028,7 +4093,7 @@ var leanbase = (function () {
4028
4093
  try {
4029
4094
  this._captureEvent(e);
4030
4095
  } catch (error) {
4031
- logger.error('Failed to capture event', error);
4096
+ logger$3.error('Failed to capture event', error);
4032
4097
  }
4033
4098
  };
4034
4099
  addEventListener(document, 'submit', handler, {
@@ -4213,7 +4278,7 @@ var leanbase = (function () {
4213
4278
  this._windowIdGenerator = windowIdGenerator || uuidv7;
4214
4279
  const persistenceName = this._config['persistence_name'] || this._config['token'];
4215
4280
  const desiredTimeout = this._config['session_idle_timeout_seconds'] || DEFAULT_SESSION_IDLE_TIMEOUT_SECONDS;
4216
- this._sessionTimeoutMs = clampToRange(desiredTimeout, MIN_SESSION_IDLE_TIMEOUT_SECONDS, MAX_SESSION_IDLE_TIMEOUT_SECONDS, logger, DEFAULT_SESSION_IDLE_TIMEOUT_SECONDS) * 1000;
4281
+ this._sessionTimeoutMs = clampToRange(desiredTimeout, MIN_SESSION_IDLE_TIMEOUT_SECONDS, MAX_SESSION_IDLE_TIMEOUT_SECONDS, logger$3, DEFAULT_SESSION_IDLE_TIMEOUT_SECONDS) * 1000;
4217
4282
  instance.register({
4218
4283
  $configured_session_timeout_ms: this._sessionTimeoutMs
4219
4284
  });
@@ -4240,7 +4305,7 @@ var leanbase = (function () {
4240
4305
  const sessionStartTimestamp = uuid7ToTimestampMs(this._config.bootstrap.sessionID);
4241
4306
  this._setSessionId(this._config.bootstrap.sessionID, new Date().getTime(), sessionStartTimestamp);
4242
4307
  } catch (e) {
4243
- logger.error('Invalid sessionID in bootstrap', e);
4308
+ logger$3.error('Invalid sessionID in bootstrap', e);
4244
4309
  }
4245
4310
  }
4246
4311
  this._listenToReloadWindow();
@@ -4381,7 +4446,7 @@ var leanbase = (function () {
4381
4446
  if (noSessionId || activityTimeout || sessionPastMaximumLength) {
4382
4447
  sessionId = this._sessionIdGenerator();
4383
4448
  windowId = this._windowIdGenerator();
4384
- logger.info('new session ID generated', {
4449
+ logger$3.info('new session ID generated', {
4385
4450
  sessionId,
4386
4451
  windowId,
4387
4452
  changeReason: {
@@ -4510,67 +4575,6 @@ var leanbase = (function () {
4510
4575
  }
4511
4576
  }
4512
4577
 
4513
- var RequestRouterRegion;
4514
- (function (RequestRouterRegion) {
4515
- RequestRouterRegion["US"] = "us";
4516
- RequestRouterRegion["EU"] = "eu";
4517
- RequestRouterRegion["CUSTOM"] = "custom";
4518
- })(RequestRouterRegion || (RequestRouterRegion = {}));
4519
- const ingestionDomain = 'i.posthog.com';
4520
- class RequestRouter {
4521
- constructor(instance) {
4522
- this._regionCache = {};
4523
- this.instance = instance;
4524
- }
4525
- get apiHost() {
4526
- const host = this.instance.config.api_host.trim().replace(/\/$/, '');
4527
- if (host === 'https://app.posthog.com') {
4528
- return 'https://us.i.posthog.com';
4529
- }
4530
- return host;
4531
- }
4532
- get uiHost() {
4533
- let host = this.instance.config.ui_host?.replace(/\/$/, '');
4534
- if (!host) {
4535
- host = this.apiHost.replace(`.${ingestionDomain}`, '.posthog.com');
4536
- }
4537
- if (host === 'https://app.posthog.com') {
4538
- return 'https://us.posthog.com';
4539
- }
4540
- return host;
4541
- }
4542
- get region() {
4543
- if (!this._regionCache[this.apiHost]) {
4544
- if (/https:\/\/(app|us|us-assets)(\\.i)?\\.posthog\\.com/i.test(this.apiHost)) {
4545
- this._regionCache[this.apiHost] = RequestRouterRegion.US;
4546
- } else if (/https:\/\/(eu|eu-assets)(\\.i)?\\.posthog\\.com/i.test(this.apiHost)) {
4547
- this._regionCache[this.apiHost] = RequestRouterRegion.EU;
4548
- } else {
4549
- this._regionCache[this.apiHost] = RequestRouterRegion.CUSTOM;
4550
- }
4551
- }
4552
- return this._regionCache[this.apiHost];
4553
- }
4554
- endpointFor(target, path = '') {
4555
- if (path) {
4556
- path = path[0] === '/' ? path : `/${path}`;
4557
- }
4558
- if (target === 'ui') {
4559
- return this.uiHost + path;
4560
- }
4561
- if (this.region === RequestRouterRegion.CUSTOM) {
4562
- return this.apiHost + path;
4563
- }
4564
- const suffix = ingestionDomain + path;
4565
- switch (target) {
4566
- case 'assets':
4567
- return `https://${this.region}-assets.${suffix}`;
4568
- case 'api':
4569
- return `https://${this.region}.${suffix}`;
4570
- }
4571
- }
4572
- }
4573
-
4574
4578
  // This keeps track of the PageView state (such as the previous PageView's path, timestamp, id, and scroll properties).
4575
4579
  // We store the state in memory, which means that for non-SPA sites, the state will be lost on page reload. This means
4576
4580
  // that non-SPA sites should always send a $pageleave event on any navigation, before the page unloads. For SPA sites,
@@ -4631,10 +4635,10 @@ var leanbase = (function () {
4631
4635
  lastContentY = Math.ceil(lastContentY);
4632
4636
  maxContentY = Math.ceil(maxContentY);
4633
4637
  // if the maximum scroll height is near 0, then the percentage is 1
4634
- const lastScrollPercentage = maxScrollHeight <= 1 ? 1 : clampToRange(lastScrollY / maxScrollHeight, 0, 1, logger);
4635
- const maxScrollPercentage = maxScrollHeight <= 1 ? 1 : clampToRange(maxScrollY / maxScrollHeight, 0, 1, logger);
4636
- const lastContentPercentage = maxContentHeight <= 1 ? 1 : clampToRange(lastContentY / maxContentHeight, 0, 1, logger);
4637
- const maxContentPercentage = maxContentHeight <= 1 ? 1 : clampToRange(maxContentY / maxContentHeight, 0, 1, logger);
4638
+ const lastScrollPercentage = maxScrollHeight <= 1 ? 1 : clampToRange(lastScrollY / maxScrollHeight, 0, 1, logger$3);
4639
+ const maxScrollPercentage = maxScrollHeight <= 1 ? 1 : clampToRange(maxScrollY / maxScrollHeight, 0, 1, logger$3);
4640
+ const lastContentPercentage = maxContentHeight <= 1 ? 1 : clampToRange(lastContentY / maxContentHeight, 0, 1, logger$3);
4641
+ const maxContentPercentage = maxContentHeight <= 1 ? 1 : clampToRange(maxContentY / maxContentHeight, 0, 1, logger$3);
4638
4642
  properties = extend(properties, {
4639
4643
  $prev_pageview_last_scroll: lastScrollY,
4640
4644
  $prev_pageview_last_scroll_percentage: lastScrollPercentage,
@@ -4797,208 +4801,3348 @@ var leanbase = (function () {
4797
4801
  // It would be very bad if posthog-js caused a permission prompt to appear on every page load.
4798
4802
  };
4799
4803
 
4800
- const defaultConfig = () => ({
4801
- host: 'https://i.leanbase.co',
4802
- api_host: 'https://i.leanbase.co',
4803
- token: '',
4804
- autocapture: true,
4805
- rageclick: true,
4806
- persistence: 'localStorage+cookie',
4807
- capture_pageview: 'history_change',
4808
- capture_pageleave: 'if_capture_pageview',
4809
- persistence_name: '',
4810
- mask_all_element_attributes: false,
4811
- cookie_expiration: 365,
4812
- cross_subdomain_cookie: isCrossDomainCookie(document?.location),
4813
- custom_campaign_params: [],
4814
- custom_personal_data_properties: [],
4815
- disable_persistence: false,
4816
- mask_personal_data_properties: false,
4817
- secure_cookie: window?.location?.protocol === 'https:',
4818
- mask_all_text: false,
4819
- bootstrap: {},
4820
- session_idle_timeout_seconds: 30 * 60,
4821
- save_campaign_params: true,
4822
- save_referrer: true,
4823
- opt_out_useragent_filter: false,
4824
- properties_string_max_length: 65535,
4825
- loaded: () => {},
4826
- session_recording: {}
4827
- });
4828
- class Leanbase extends PostHogCore {
4829
- constructor(token, config) {
4830
- const mergedConfig = extend(defaultConfig(), config || {}, {
4831
- token
4832
- });
4833
- super(token, mergedConfig);
4834
- this.personProcessingSetOncePropertiesSent = false;
4835
- this.isLoaded = false;
4836
- this.config = mergedConfig;
4837
- this.visibilityStateListener = null;
4838
- this.initialPageviewCaptured = false;
4839
- this.scrollManager = new ScrollManager(this);
4840
- this.pageViewManager = new PageViewManager(this);
4841
- this.requestRouter = new RequestRouter(this);
4842
- this.init(token, mergedConfig);
4843
- }
4844
- init(token, config) {
4845
- this.setConfig(extend(defaultConfig(), config, {
4846
- token
4847
- }));
4848
- this.isLoaded = true;
4849
- this.persistence = new LeanbasePersistence(this.config);
4850
- this.replayAutocapture = new Autocapture(this);
4851
- this.replayAutocapture.startIfEnabled();
4852
- // Initialize session manager and props before session recording (matches browser behavior)
4853
- if (this.config.cookieless_mode !== 'always') {
4854
- if (!this.sessionManager) {
4855
- this.sessionManager = new SessionIdManager(this);
4856
- this.sessionPropsManager = new SessionPropsManager(this, this.sessionManager, this.persistence);
4857
- }
4858
- // runtime require to lazy-load replay code; allowed for browser parity
4859
- // @ts-expect-error - runtime import only available in browser build
4860
- const {
4861
- SessionRecording
4862
- } = require('./extensions/replay/session-recording'); // eslint-disable-line @typescript-eslint/no-require-imports
4863
- this.sessionRecording = new SessionRecording(this);
4864
- this.sessionRecording.startIfEnabledOrStop();
4865
- }
4866
- if (this.config.preloadFeatureFlags !== false) {
4867
- this.reloadFeatureFlags();
4804
+ const _createLogger = prefix => {
4805
+ return {
4806
+ info: (...args) => logger$3.info(prefix, ...args),
4807
+ warn: (...args) => logger$3.warn(prefix, ...args),
4808
+ error: (...args) => logger$3.error(prefix, ...args),
4809
+ critical: (...args) => logger$3.critical(prefix, ...args),
4810
+ uninitializedWarning: methodName => {
4811
+ logger$3.error(prefix, `You must initialize Leanbase before calling ${methodName}`);
4812
+ },
4813
+ createLogger: additionalPrefix => _createLogger(`${prefix} ${additionalPrefix}`)
4814
+ };
4815
+ };
4816
+ const logger$2 = _createLogger('[Leanbase]');
4817
+ const createLogger = _createLogger;
4818
+
4819
+ function patch(source, name, replacement) {
4820
+ try {
4821
+ if (!(name in source)) {
4822
+ return () => {
4823
+ //
4824
+ };
4868
4825
  }
4869
- this.config.loaded?.(this);
4870
- if (this.config.capture_pageview) {
4871
- setTimeout(() => {
4872
- if (this.config.cookieless_mode === 'always') {
4873
- this.captureInitialPageview();
4826
+ const original = source[name];
4827
+ const wrapped = replacement(original);
4828
+ if (isFunction(wrapped)) {
4829
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
4830
+ wrapped.prototype = wrapped.prototype || {};
4831
+ Object.defineProperties(wrapped, {
4832
+ __posthog_wrapped__: {
4833
+ enumerable: false,
4834
+ value: true
4874
4835
  }
4875
- }, 1);
4836
+ });
4876
4837
  }
4877
- addEventListener(document, 'DOMContentLoaded', () => {
4878
- this.loadRemoteConfig();
4879
- });
4880
- addEventListener(window, 'onpagehide' in self ? 'pagehide' : 'unload', this.capturePageLeave.bind(this), {
4881
- passive: false
4882
- });
4838
+ source[name] = wrapped;
4839
+ return () => {
4840
+ source[name] = original;
4841
+ };
4842
+ } catch {
4843
+ return () => {
4844
+ //
4845
+ };
4883
4846
  }
4884
- captureInitialPageview() {
4885
- if (!document) {
4886
- return;
4887
- }
4888
- if (document.visibilityState !== 'visible') {
4889
- if (!this.visibilityStateListener) {
4890
- this.visibilityStateListener = this.captureInitialPageview.bind(this);
4891
- addEventListener(document, 'visibilitychange', this.visibilityStateListener);
4892
- }
4893
- return;
4847
+ }
4848
+
4849
+ function hostnameFromURL(url) {
4850
+ try {
4851
+ if (typeof url === 'string') {
4852
+ return new URL(url).hostname;
4894
4853
  }
4895
- if (!this.initialPageviewCaptured) {
4896
- this.initialPageviewCaptured = true;
4897
- this.capture('$pageview', {
4898
- title: document.title
4899
- });
4900
- if (this.visibilityStateListener) {
4901
- document.removeEventListener('visibilitychange', this.visibilityStateListener);
4902
- this.visibilityStateListener = null;
4903
- }
4854
+ if ('url' in url) {
4855
+ return new URL(url.url).hostname;
4904
4856
  }
4857
+ return url.hostname;
4858
+ } catch {
4859
+ return null;
4905
4860
  }
4906
- capturePageLeave() {
4907
- const {
4908
- capture_pageleave,
4909
- capture_pageview
4910
- } = this.config;
4911
- if (capture_pageleave === true || capture_pageleave === 'if_capture_pageview' && (capture_pageview === true || capture_pageview === 'history_change')) {
4912
- this.capture('$pageleave');
4861
+ }
4862
+ function isHostOnDenyList(url, options) {
4863
+ const hostname = hostnameFromURL(url);
4864
+ const defaultNotDenied = {
4865
+ hostname,
4866
+ isHostDenied: false
4867
+ };
4868
+ if (!options.payloadHostDenyList?.length || !hostname?.trim().length) {
4869
+ return defaultNotDenied;
4870
+ }
4871
+ for (const deny of options.payloadHostDenyList) {
4872
+ if (hostname.endsWith(deny)) {
4873
+ return {
4874
+ hostname,
4875
+ isHostDenied: true
4876
+ };
4913
4877
  }
4914
4878
  }
4915
- async loadRemoteConfig() {
4916
- if (!this.isRemoteConfigLoaded) {
4917
- const remoteConfig = await this.reloadRemoteConfigAsync();
4918
- if (remoteConfig) {
4919
- this.onRemoteConfig(remoteConfig);
4879
+ return defaultNotDenied;
4880
+ }
4881
+
4882
+ const LOGGER_PREFIX$2 = '[SessionRecording]';
4883
+ const REDACTED = 'redacted';
4884
+ const defaultNetworkOptions = {
4885
+ initiatorTypes: ['audio', 'beacon', 'body', 'css', 'early-hints', 'embed', 'fetch', 'frame', 'iframe', 'image', 'img', 'input', 'link', 'navigation', 'object', 'ping', 'script', 'track', 'video', 'xmlhttprequest'],
4886
+ maskRequestFn: data => data,
4887
+ recordHeaders: false,
4888
+ recordBody: false,
4889
+ recordInitialRequests: false,
4890
+ recordPerformance: false,
4891
+ performanceEntryTypeToObserve: [
4892
+ // 'event', // This is too noisy as it covers all browser events
4893
+ 'first-input',
4894
+ // 'mark', // Mark is used too liberally. We would need to filter for specific marks
4895
+ // 'measure', // Measure is used too liberally. We would need to filter for specific measures
4896
+ 'navigation', 'paint', 'resource'],
4897
+ payloadSizeLimitBytes: 1000000,
4898
+ payloadHostDenyList: ['.lr-ingest.io', '.ingest.sentry.io', '.clarity.ms',
4899
+ // NB no leading dot here
4900
+ 'analytics.google.com', 'bam.nr-data.net']
4901
+ };
4902
+ const HEADER_DENY_LIST = ['authorization', 'x-forwarded-for', 'authorization', 'cookie', 'set-cookie', 'x-api-key', 'x-real-ip', 'remote-addr', 'forwarded', 'proxy-authorization', 'x-csrf-token', 'x-csrftoken', 'x-xsrf-token'];
4903
+ const PAYLOAD_CONTENT_DENY_LIST = ['password', 'secret', 'passwd', 'api_key', 'apikey', 'auth', 'credentials', 'mysql_pwd', 'privatekey', 'private_key', 'token'];
4904
+ // we always remove headers on the deny list because we never want to capture this sensitive data
4905
+ const removeAuthorizationHeader = data => {
4906
+ const headers = data.requestHeaders;
4907
+ if (!isNullish(headers)) {
4908
+ const mutableHeaders = isArray(headers) ? Object.fromEntries(headers) : headers;
4909
+ each(Object.keys(mutableHeaders ?? {}), header => {
4910
+ if (HEADER_DENY_LIST.includes(header.toLowerCase())) {
4911
+ mutableHeaders[header] = REDACTED;
4920
4912
  }
4921
- }
4913
+ });
4914
+ data.requestHeaders = mutableHeaders;
4922
4915
  }
4923
- onRemoteConfig(config) {
4924
- if (!(document && document.body)) {
4925
- setTimeout(() => {
4926
- this.onRemoteConfig(config);
4927
- }, 500);
4928
- return;
4929
- }
4930
- this.isRemoteConfigLoaded = true;
4931
- this.replayAutocapture?.onRemoteConfig(config);
4916
+ return data;
4917
+ };
4918
+ const POSTHOG_PATHS_TO_IGNORE = ['/s/', '/e/', '/i/'];
4919
+ // want to ignore posthog paths when capturing requests, or we can get trapped in a loop
4920
+ // because calls to PostHog would be reported using a call to PostHog which would be reported....
4921
+ const ignorePostHogPaths = (data, apiHostConfig) => {
4922
+ const url = convertToURL(data.name);
4923
+ const host = apiHostConfig || '';
4924
+ let replaceValue = host.indexOf('http') === 0 ? convertToURL(host)?.pathname : host;
4925
+ if (replaceValue === '/') {
4926
+ replaceValue = '';
4927
+ }
4928
+ const pathname = url?.pathname.replace(replaceValue || '', '');
4929
+ if (url && pathname && POSTHOG_PATHS_TO_IGNORE.some(path => pathname.indexOf(path) === 0)) {
4930
+ return undefined;
4932
4931
  }
4933
- fetch(url, options) {
4934
- const fetchFn = getFetch();
4935
- if (!fetchFn) {
4936
- return Promise.reject(new Error('Fetch API is not available in this environment.'));
4937
- }
4938
- return fetchFn(url, options);
4932
+ return data;
4933
+ };
4934
+ function estimateBytes(payload) {
4935
+ return new Blob([payload]).size;
4936
+ }
4937
+ function enforcePayloadSizeLimit(payload, headers, limit, description) {
4938
+ if (isNullish(payload)) {
4939
+ return payload;
4939
4940
  }
4940
- setConfig(config) {
4941
- const oldConfig = {
4942
- ...this.config
4943
- };
4944
- if (isObject(config)) {
4945
- extend(this.config, config);
4946
- this.persistence?.update_config(this.config, oldConfig);
4947
- this.replayAutocapture?.startIfEnabled();
4948
- }
4949
- const isTempStorage = this.config.persistence === 'sessionStorage' || this.config.persistence === 'memory';
4950
- this.sessionPersistence = isTempStorage ? this.persistence : new LeanbasePersistence({
4951
- ...this.config,
4952
- persistence: 'sessionStorage'
4953
- });
4941
+ let requestContentLength = headers?.['content-length'] || estimateBytes(payload);
4942
+ if (isString(requestContentLength)) {
4943
+ requestContentLength = parseInt(requestContentLength);
4954
4944
  }
4955
- getLibraryId() {
4956
- return 'leanbase';
4945
+ if (requestContentLength > limit) {
4946
+ return LOGGER_PREFIX$2 + ` ${description} body too large to record (${requestContentLength} bytes)`;
4957
4947
  }
4958
- getLibraryVersion() {
4959
- return Config.LIB_VERSION;
4948
+ return payload;
4949
+ }
4950
+ // people can have arbitrarily large payloads on their site, but we don't want to ingest them
4951
+ const limitPayloadSize = options => {
4952
+ // the smallest of 1MB or the specified limit if there is one
4953
+ const limit = Math.min(1000000, options.payloadSizeLimitBytes ?? 1000000);
4954
+ return data => {
4955
+ if (data?.requestBody) {
4956
+ data.requestBody = enforcePayloadSizeLimit(data.requestBody, data.requestHeaders, limit, 'Request');
4957
+ }
4958
+ if (data?.responseBody) {
4959
+ data.responseBody = enforcePayloadSizeLimit(data.responseBody, data.responseHeaders, limit, 'Response');
4960
+ }
4961
+ return data;
4962
+ };
4963
+ };
4964
+ function scrubPayload(payload, label) {
4965
+ if (isNullish(payload)) {
4966
+ return payload;
4960
4967
  }
4961
- getCustomUserAgent() {
4962
- return;
4968
+ let scrubbed = payload;
4969
+ if (!shouldCaptureValue(scrubbed, false)) {
4970
+ scrubbed = LOGGER_PREFIX$2 + ' ' + label + ' body ' + REDACTED;
4963
4971
  }
4964
- getPersistedProperty(key) {
4965
- return this.persistence?.get_property(key);
4972
+ each(PAYLOAD_CONTENT_DENY_LIST, text => {
4973
+ if (scrubbed?.length && scrubbed?.indexOf(text) !== -1) {
4974
+ scrubbed = LOGGER_PREFIX$2 + ' ' + label + ' body ' + REDACTED + ' as might contain: ' + text;
4975
+ }
4976
+ });
4977
+ return scrubbed;
4978
+ }
4979
+ function scrubPayloads(capturedRequest) {
4980
+ if (isUndefined(capturedRequest)) {
4981
+ return undefined;
4966
4982
  }
4967
- setPersistedProperty(key, value) {
4968
- this.persistence?.set_property(key, value);
4983
+ capturedRequest.requestBody = scrubPayload(capturedRequest.requestBody, 'Request');
4984
+ capturedRequest.responseBody = scrubPayload(capturedRequest.responseBody, 'Response');
4985
+ return capturedRequest;
4986
+ }
4987
+ /**
4988
+ * whether a maskRequestFn is provided or not,
4989
+ * we ensure that we remove the denied header from requests
4990
+ * we _never_ want to record that header by accident
4991
+ * if someone complains then we'll add an opt-in to let them override it
4992
+ */
4993
+ const buildNetworkRequestOptions = (instanceConfig, remoteNetworkOptions = {}) => {
4994
+ const remoteOptions = remoteNetworkOptions || {};
4995
+ const config = {
4996
+ payloadSizeLimitBytes: defaultNetworkOptions.payloadSizeLimitBytes,
4997
+ performanceEntryTypeToObserve: [...defaultNetworkOptions.performanceEntryTypeToObserve],
4998
+ payloadHostDenyList: [...(remoteOptions.payloadHostDenyList || []), ...defaultNetworkOptions.payloadHostDenyList]
4999
+ };
5000
+ // client can always disable despite remote options
5001
+ const sessionRecordingConfig = instanceConfig.session_recording || {};
5002
+ const capturePerformanceConfig = instanceConfig.capture_performance;
5003
+ const userPerformanceOptIn = isBoolean(capturePerformanceConfig) ? capturePerformanceConfig : !!capturePerformanceConfig?.network_timing;
5004
+ const canRecordHeaders = sessionRecordingConfig.recordHeaders === true && !!remoteOptions.recordHeaders;
5005
+ const canRecordBody = sessionRecordingConfig.recordBody === true && !!remoteOptions.recordBody;
5006
+ const canRecordPerformance = userPerformanceOptIn && !!remoteOptions.recordPerformance;
5007
+ const payloadLimiter = limitPayloadSize(config);
5008
+ const enforcedCleaningFn = d => payloadLimiter(ignorePostHogPaths(removeAuthorizationHeader(d), instanceConfig.host || ''));
5009
+ const hasDeprecatedMaskFunction = isFunction(sessionRecordingConfig.maskNetworkRequestFn);
5010
+ if (hasDeprecatedMaskFunction && isFunction(sessionRecordingConfig.maskCapturedNetworkRequestFn)) {
5011
+ logger$2.warn('Both `maskNetworkRequestFn` and `maskCapturedNetworkRequestFn` are defined. `maskNetworkRequestFn` will be ignored.');
5012
+ }
5013
+ if (hasDeprecatedMaskFunction) {
5014
+ sessionRecordingConfig.maskCapturedNetworkRequestFn = data => {
5015
+ const cleanedURL = sessionRecordingConfig.maskNetworkRequestFn({
5016
+ url: data.name
5017
+ });
5018
+ return {
5019
+ ...data,
5020
+ name: cleanedURL?.url
5021
+ };
5022
+ };
4969
5023
  }
4970
- // Backwards-compatible aliases expected by replay/browser code
4971
- get_property(key) {
4972
- return this.persistence?.get_property(key);
5024
+ config.maskRequestFn = isFunction(sessionRecordingConfig.maskCapturedNetworkRequestFn) ? data => {
5025
+ const cleanedRequest = enforcedCleaningFn(data);
5026
+ return cleanedRequest ? sessionRecordingConfig.maskCapturedNetworkRequestFn?.(cleanedRequest) ?? undefined : undefined;
5027
+ } : data => scrubPayloads(enforcedCleaningFn(data));
5028
+ return {
5029
+ ...defaultNetworkOptions,
5030
+ ...config,
5031
+ recordHeaders: canRecordHeaders,
5032
+ recordBody: canRecordBody,
5033
+ recordPerformance: canRecordPerformance,
5034
+ recordInitialRequests: canRecordPerformance
5035
+ };
5036
+ };
5037
+
5038
+ /// <reference lib="dom" />
5039
+ const logger$1 = createLogger('[Recorder]');
5040
+ const isNavigationTiming = entry => entry.entryType === 'navigation';
5041
+ const isResourceTiming = entry => entry.entryType === 'resource';
5042
+ function findLast(array, predicate) {
5043
+ const length = array.length;
5044
+ for (let i = length - 1; i >= 0; i -= 1) {
5045
+ if (predicate(array[i])) {
5046
+ return array[i];
5047
+ }
5048
+ }
5049
+ return undefined;
5050
+ }
5051
+ function isDocument(value) {
5052
+ return !!value && typeof value === 'object' && 'nodeType' in value && value.nodeType === 9;
5053
+ }
5054
+ function initPerformanceObserver(cb, win, options) {
5055
+ // if we are only observing timings then we could have a single observer for all types, with buffer true,
5056
+ // but we are going to filter by initiatorType _if we are wrapping fetch and xhr as the wrapped functions
5057
+ // will deal with those.
5058
+ // so we have a block which captures requests from before fetch/xhr is wrapped
5059
+ // these are marked `isInitial` so playback can display them differently if needed
5060
+ // they will never have method/status/headers/body because they are pre-wrapping that provides that
5061
+ if (options.recordInitialRequests) {
5062
+ const initialPerformanceEntries = win.performance.getEntries().filter(entry => isNavigationTiming(entry) || isResourceTiming(entry) && options.initiatorTypes.includes(entry.initiatorType));
5063
+ cb({
5064
+ requests: initialPerformanceEntries.flatMap(entry => prepareRequest({
5065
+ entry,
5066
+ method: undefined,
5067
+ status: undefined,
5068
+ networkRequest: {},
5069
+ isInitial: true
5070
+ })),
5071
+ isInitial: true
5072
+ });
4973
5073
  }
4974
- set_property(key, value) {
4975
- this.persistence?.set_property(key, value);
5074
+ const observer = new win.PerformanceObserver(entries => {
5075
+ // if recordBody or recordHeaders is true then we don't want to record fetch or xhr here
5076
+ // as the wrapped functions will do that. Otherwise, this filter becomes a noop
5077
+ // because we do want to record them here
5078
+ const wrappedInitiatorFilter = entry => options.recordBody || options.recordHeaders ? entry.initiatorType !== 'xmlhttprequest' && entry.initiatorType !== 'fetch' : true;
5079
+ const performanceEntries = entries.getEntries().filter(entry => isNavigationTiming(entry) || isResourceTiming(entry) && options.initiatorTypes.includes(entry.initiatorType) &&
5080
+ // TODO if we are _only_ capturing timing we don't want to filter initiator here
5081
+ wrappedInitiatorFilter(entry));
5082
+ cb({
5083
+ requests: performanceEntries.flatMap(entry => prepareRequest({
5084
+ entry,
5085
+ method: undefined,
5086
+ status: undefined,
5087
+ networkRequest: {}
5088
+ }))
5089
+ });
5090
+ });
5091
+ // compat checked earlier
5092
+ // eslint-disable-next-line compat/compat
5093
+ const entryTypes = PerformanceObserver.supportedEntryTypes.filter(x => options.performanceEntryTypeToObserve.includes(x));
5094
+ // initial records are gathered above, so we don't need to observe and buffer each type separately
5095
+ observer.observe({
5096
+ entryTypes
5097
+ });
5098
+ return () => {
5099
+ observer.disconnect();
5100
+ };
5101
+ }
5102
+ function shouldRecordHeaders(type, recordHeaders) {
5103
+ return !!recordHeaders && (isBoolean(recordHeaders) || recordHeaders[type]);
5104
+ }
5105
+ function shouldRecordBody({
5106
+ type,
5107
+ recordBody,
5108
+ headers,
5109
+ url
5110
+ }) {
5111
+ function matchesContentType(contentTypes) {
5112
+ const contentTypeHeader = Object.keys(headers).find(key => key.toLowerCase() === 'content-type');
5113
+ const contentType = contentTypeHeader && headers[contentTypeHeader];
5114
+ return contentTypes.some(ct => contentType?.includes(ct));
4976
5115
  }
4977
- register_for_session(properties) {
4978
- // PostHogCore may expose registerForSession; call it if available
4979
- if (isFunction(this.registerForSession)) {
4980
- this.registerForSession(properties);
4981
- return;
4982
- }
4983
- // fallback: store properties in sessionPersistence
4984
- if (this.sessionPersistence) {
4985
- Object.keys(properties).forEach(k => this.sessionPersistence?.set_property(k, properties[k]));
5116
+ /**
5117
+ * particularly in canvas applications we see many requests to blob URLs
5118
+ * e.g. blob:https://video_url
5119
+ * these blob/object URLs are local to the browser, we can never capture that body
5120
+ * so we can just return false here
5121
+ */
5122
+ function isBlobURL(url) {
5123
+ try {
5124
+ if (typeof url === 'string') {
5125
+ return url.startsWith('blob:');
5126
+ }
5127
+ if (url instanceof URL) {
5128
+ return url.protocol === 'blob:';
5129
+ }
5130
+ if (url instanceof Request) {
5131
+ return isBlobURL(url.url);
5132
+ }
5133
+ return false;
5134
+ } catch {
5135
+ return false;
4986
5136
  }
4987
5137
  }
4988
- unregister_for_session(property) {
4989
- if (isFunction(this.unregisterForSession)) {
4990
- this.unregisterForSession(property);
4991
- return;
4992
- }
4993
- if (this.sessionPersistence) {
4994
- this.sessionPersistence.set_property(property, null);
4995
- }
5138
+ if (!recordBody) return false;
5139
+ if (isBlobURL(url)) return false;
5140
+ if (isBoolean(recordBody)) return true;
5141
+ if (isArray(recordBody)) return matchesContentType(recordBody);
5142
+ const recordBodyType = recordBody[type];
5143
+ if (isBoolean(recordBodyType)) return recordBodyType;
5144
+ return matchesContentType(recordBodyType);
5145
+ }
5146
+ async function getRequestPerformanceEntry(win, initiatorType, url, start, end, attempt = 0) {
5147
+ if (attempt > 10) {
5148
+ logger$1.warn('Failed to get performance entry for request', {
5149
+ url,
5150
+ initiatorType
5151
+ });
5152
+ return null;
4996
5153
  }
4997
- calculateEventProperties(eventName, eventProperties, timestamp, uuid, readOnly) {
4998
- if (!this.persistence || !this.sessionPersistence) {
4999
- return eventProperties;
5000
- }
5001
- timestamp = timestamp || new Date();
5154
+ const urlPerformanceEntries = win.performance.getEntriesByName(url);
5155
+ const performanceEntry = findLast(urlPerformanceEntries, entry => isResourceTiming(entry) && entry.initiatorType === initiatorType && (isUndefined(start) || entry.startTime >= start) && (isUndefined(end) || entry.startTime <= end));
5156
+ if (!performanceEntry) {
5157
+ await new Promise(resolve => setTimeout(resolve, 50 * attempt));
5158
+ return getRequestPerformanceEntry(win, initiatorType, url, start, end, attempt + 1);
5159
+ }
5160
+ return performanceEntry;
5161
+ }
5162
+ /**
5163
+ * According to MDN https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/response
5164
+ * xhr response is typed as any but can be an ArrayBuffer, a Blob, a Document, a JavaScript object,
5165
+ * or a string, depending on the value of XMLHttpRequest.responseType, that contains the response entity body.
5166
+ *
5167
+ * XHR request body is Document | XMLHttpRequestBodyInit | null | undefined
5168
+ */
5169
+ function _tryReadXHRBody({
5170
+ body,
5171
+ options,
5172
+ url
5173
+ }) {
5174
+ if (isNullish(body)) {
5175
+ return null;
5176
+ }
5177
+ const {
5178
+ hostname,
5179
+ isHostDenied
5180
+ } = isHostOnDenyList(url, options);
5181
+ if (isHostDenied) {
5182
+ return hostname + ' is in deny list';
5183
+ }
5184
+ if (isString(body)) {
5185
+ return body;
5186
+ }
5187
+ if (isDocument(body)) {
5188
+ return body.textContent;
5189
+ }
5190
+ if (isFormData(body)) {
5191
+ return formDataToQuery(body);
5192
+ }
5193
+ if (isObject(body)) {
5194
+ try {
5195
+ return JSON.stringify(body);
5196
+ } catch {
5197
+ return '[SessionReplay] Failed to stringify response object';
5198
+ }
5199
+ }
5200
+ return '[SessionReplay] Cannot read body of type ' + toString.call(body);
5201
+ }
5202
+ function initXhrObserver(cb, win, options) {
5203
+ if (!options.initiatorTypes.includes('xmlhttprequest')) {
5204
+ return () => {
5205
+ //
5206
+ };
5207
+ }
5208
+ const recordRequestHeaders = shouldRecordHeaders('request', options.recordHeaders);
5209
+ const recordResponseHeaders = shouldRecordHeaders('response', options.recordHeaders);
5210
+ const restorePatch = patch(win.XMLHttpRequest.prototype, 'open',
5211
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
5212
+ // @ts-ignore
5213
+ originalOpen => {
5214
+ return function (method, url, async = true, username, password) {
5215
+ // because this function is returned in its actual context `this` _is_ an XMLHttpRequest
5216
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
5217
+ // @ts-ignore
5218
+ const xhr = this;
5219
+ // check IE earlier than this, we only initialize if Request is present
5220
+ // eslint-disable-next-line compat/compat
5221
+ const req = new Request(url);
5222
+ const networkRequest = {};
5223
+ let start;
5224
+ let end;
5225
+ const requestHeaders = {};
5226
+ const originalSetRequestHeader = xhr.setRequestHeader.bind(xhr);
5227
+ xhr.setRequestHeader = (header, value) => {
5228
+ requestHeaders[header] = value;
5229
+ return originalSetRequestHeader(header, value);
5230
+ };
5231
+ if (recordRequestHeaders) {
5232
+ networkRequest.requestHeaders = requestHeaders;
5233
+ }
5234
+ const originalSend = xhr.send.bind(xhr);
5235
+ xhr.send = body => {
5236
+ if (shouldRecordBody({
5237
+ type: 'request',
5238
+ headers: requestHeaders,
5239
+ url,
5240
+ recordBody: options.recordBody
5241
+ })) {
5242
+ networkRequest.requestBody = _tryReadXHRBody({
5243
+ body,
5244
+ options,
5245
+ url
5246
+ });
5247
+ }
5248
+ start = win.performance.now();
5249
+ return originalSend(body);
5250
+ };
5251
+ const readyStateListener = () => {
5252
+ if (xhr.readyState !== xhr.DONE) {
5253
+ return;
5254
+ }
5255
+ // Clean up the listener immediately when done to prevent memory leaks
5256
+ xhr.removeEventListener('readystatechange', readyStateListener);
5257
+ end = win.performance.now();
5258
+ const responseHeaders = {};
5259
+ const rawHeaders = xhr.getAllResponseHeaders();
5260
+ const headers = rawHeaders.trim().split(/[\r\n]+/);
5261
+ headers.forEach(line => {
5262
+ const parts = line.split(': ');
5263
+ const header = parts.shift();
5264
+ const value = parts.join(': ');
5265
+ if (header) {
5266
+ responseHeaders[header] = value;
5267
+ }
5268
+ });
5269
+ if (recordResponseHeaders) {
5270
+ networkRequest.responseHeaders = responseHeaders;
5271
+ }
5272
+ if (shouldRecordBody({
5273
+ type: 'response',
5274
+ headers: responseHeaders,
5275
+ url,
5276
+ recordBody: options.recordBody
5277
+ })) {
5278
+ networkRequest.responseBody = _tryReadXHRBody({
5279
+ body: xhr.response,
5280
+ options,
5281
+ url
5282
+ });
5283
+ }
5284
+ getRequestPerformanceEntry(win, 'xmlhttprequest', req.url, start, end).then(entry => {
5285
+ const requests = prepareRequest({
5286
+ entry,
5287
+ method: method,
5288
+ status: xhr?.status,
5289
+ networkRequest,
5290
+ start,
5291
+ end,
5292
+ url: url.toString(),
5293
+ initiatorType: 'xmlhttprequest'
5294
+ });
5295
+ cb({
5296
+ requests
5297
+ });
5298
+ }).catch(() => {
5299
+ //
5300
+ });
5301
+ };
5302
+ // This is very tricky code, and making it passive won't bring many performance benefits,
5303
+ // so let's ignore the rule here.
5304
+ // eslint-disable-next-line posthog-js/no-add-event-listener
5305
+ xhr.addEventListener('readystatechange', readyStateListener);
5306
+ originalOpen.call(xhr, method, url, async, username, password);
5307
+ };
5308
+ });
5309
+ return () => {
5310
+ restorePatch();
5311
+ };
5312
+ }
5313
+ /**
5314
+ * Check if this PerformanceEntry is either a PerformanceResourceTiming or a PerformanceNavigationTiming
5315
+ * NB PerformanceNavigationTiming extends PerformanceResourceTiming
5316
+ * Here we don't care which interface it implements as both expose `serverTimings`
5317
+ */
5318
+ const exposesServerTiming = event => !isNull(event) && (event.entryType === 'navigation' || event.entryType === 'resource');
5319
+ function prepareRequest({
5320
+ entry,
5321
+ method,
5322
+ status,
5323
+ networkRequest,
5324
+ isInitial,
5325
+ start,
5326
+ end,
5327
+ url,
5328
+ initiatorType
5329
+ }) {
5330
+ start = entry ? entry.startTime : start;
5331
+ end = entry ? entry.responseEnd : end;
5332
+ // kudos to sentry javascript sdk for excellent background on why to use Date.now() here
5333
+ // https://github.com/getsentry/sentry-javascript/blob/e856e40b6e71a73252e788cd42b5260f81c9c88e/packages/utils/src/time.ts#L70
5334
+ // can't start observer if performance.now() is not available
5335
+ // eslint-disable-next-line compat/compat
5336
+ const timeOrigin = Math.floor(Date.now() - performance.now());
5337
+ // clickhouse can't ingest timestamps that are floats
5338
+ // (in this case representing fractions of a millisecond we don't care about anyway)
5339
+ // use timeOrigin if we really can't gather a start time
5340
+ const timestamp = Math.floor(timeOrigin + (start || 0));
5341
+ const entryJSON = entry ? entry.toJSON() : {
5342
+ name: url
5343
+ };
5344
+ const requests = [{
5345
+ ...entryJSON,
5346
+ startTime: isUndefined(start) ? undefined : Math.round(start),
5347
+ endTime: isUndefined(end) ? undefined : Math.round(end),
5348
+ timeOrigin,
5349
+ timestamp,
5350
+ method: method,
5351
+ initiatorType: initiatorType ? initiatorType : entry ? entry.initiatorType : undefined,
5352
+ status,
5353
+ requestHeaders: networkRequest.requestHeaders,
5354
+ requestBody: networkRequest.requestBody,
5355
+ responseHeaders: networkRequest.responseHeaders,
5356
+ responseBody: networkRequest.responseBody,
5357
+ isInitial
5358
+ }];
5359
+ if (exposesServerTiming(entry)) {
5360
+ for (const timing of entry.serverTiming || []) {
5361
+ requests.push({
5362
+ timeOrigin,
5363
+ timestamp,
5364
+ startTime: Math.round(entry.startTime),
5365
+ name: timing.name,
5366
+ duration: timing.duration,
5367
+ // the spec has a closed list of possible types
5368
+ // https://developer.mozilla.org/en-US/docs/Web/API/PerformanceEntry/entryType
5369
+ // but, we need to know this was a server timing so that we know to
5370
+ // match it to the appropriate navigation or resource timing
5371
+ // that matching will have to be on timestamp and $current_url
5372
+ entryType: 'serverTiming'
5373
+ });
5374
+ }
5375
+ }
5376
+ return requests;
5377
+ }
5378
+ const contentTypePrefixDenyList = ['video/', 'audio/'];
5379
+ function _checkForCannotReadResponseBody({
5380
+ r,
5381
+ options,
5382
+ url
5383
+ }) {
5384
+ if (r.headers.get('Transfer-Encoding') === 'chunked') {
5385
+ return 'Chunked Transfer-Encoding is not supported';
5386
+ }
5387
+ // `get` and `has` are case-insensitive
5388
+ // but return the header value with the casing that was supplied
5389
+ const contentType = r.headers.get('Content-Type')?.toLowerCase();
5390
+ const contentTypeIsDenied = contentTypePrefixDenyList.some(prefix => contentType?.startsWith(prefix));
5391
+ if (contentType && contentTypeIsDenied) {
5392
+ return `Content-Type ${contentType} is not supported`;
5393
+ }
5394
+ const {
5395
+ hostname,
5396
+ isHostDenied
5397
+ } = isHostOnDenyList(url, options);
5398
+ if (isHostDenied) {
5399
+ return hostname + ' is in deny list';
5400
+ }
5401
+ return null;
5402
+ }
5403
+ function _tryReadBody(r) {
5404
+ // there are now already multiple places where we're using Promise...
5405
+ // eslint-disable-next-line compat/compat
5406
+ return new Promise((resolve, reject) => {
5407
+ const timeout = setTimeout(() => resolve('[SessionReplay] Timeout while trying to read body'), 500);
5408
+ try {
5409
+ r.clone().text().then(txt => resolve(txt), reason => reject(reason)).finally(() => clearTimeout(timeout));
5410
+ } catch {
5411
+ clearTimeout(timeout);
5412
+ resolve('[SessionReplay] Failed to read body');
5413
+ }
5414
+ });
5415
+ }
5416
+ async function _tryReadRequestBody({
5417
+ r,
5418
+ options,
5419
+ url
5420
+ }) {
5421
+ const {
5422
+ hostname,
5423
+ isHostDenied
5424
+ } = isHostOnDenyList(url, options);
5425
+ if (isHostDenied) {
5426
+ return Promise.resolve(hostname + ' is in deny list');
5427
+ }
5428
+ return _tryReadBody(r);
5429
+ }
5430
+ async function _tryReadResponseBody({
5431
+ r,
5432
+ options,
5433
+ url
5434
+ }) {
5435
+ const cannotReadBodyReason = _checkForCannotReadResponseBody({
5436
+ r,
5437
+ options,
5438
+ url
5439
+ });
5440
+ if (!isNull(cannotReadBodyReason)) {
5441
+ return Promise.resolve(cannotReadBodyReason);
5442
+ }
5443
+ return _tryReadBody(r);
5444
+ }
5445
+ function initFetchObserver(cb, win, options) {
5446
+ if (!options.initiatorTypes.includes('fetch')) {
5447
+ return () => {
5448
+ //
5449
+ };
5450
+ }
5451
+ const recordRequestHeaders = shouldRecordHeaders('request', options.recordHeaders);
5452
+ const recordResponseHeaders = shouldRecordHeaders('response', options.recordHeaders);
5453
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
5454
+ // @ts-ignore
5455
+ const restorePatch = patch(win, 'fetch', originalFetch => {
5456
+ return async function (url, init) {
5457
+ // check IE earlier than this, we only initialize if Request is present
5458
+ // eslint-disable-next-line compat/compat
5459
+ const req = new Request(url, init);
5460
+ let res;
5461
+ const networkRequest = {};
5462
+ let start;
5463
+ let end;
5464
+ try {
5465
+ const requestHeaders = {};
5466
+ req.headers.forEach((value, header) => {
5467
+ requestHeaders[header] = value;
5468
+ });
5469
+ if (recordRequestHeaders) {
5470
+ networkRequest.requestHeaders = requestHeaders;
5471
+ }
5472
+ if (shouldRecordBody({
5473
+ type: 'request',
5474
+ headers: requestHeaders,
5475
+ url,
5476
+ recordBody: options.recordBody
5477
+ })) {
5478
+ networkRequest.requestBody = await _tryReadRequestBody({
5479
+ r: req,
5480
+ options,
5481
+ url
5482
+ });
5483
+ }
5484
+ start = win.performance.now();
5485
+ res = await originalFetch(req);
5486
+ end = win.performance.now();
5487
+ const responseHeaders = {};
5488
+ res.headers.forEach((value, header) => {
5489
+ responseHeaders[header] = value;
5490
+ });
5491
+ if (recordResponseHeaders) {
5492
+ networkRequest.responseHeaders = responseHeaders;
5493
+ }
5494
+ if (shouldRecordBody({
5495
+ type: 'response',
5496
+ headers: responseHeaders,
5497
+ url,
5498
+ recordBody: options.recordBody
5499
+ })) {
5500
+ networkRequest.responseBody = await _tryReadResponseBody({
5501
+ r: res,
5502
+ options,
5503
+ url
5504
+ });
5505
+ }
5506
+ return res;
5507
+ } finally {
5508
+ getRequestPerformanceEntry(win, 'fetch', req.url, start, end).then(entry => {
5509
+ const requests = prepareRequest({
5510
+ entry,
5511
+ method: req.method,
5512
+ status: res?.status,
5513
+ networkRequest,
5514
+ start,
5515
+ end,
5516
+ url: req.url,
5517
+ initiatorType: 'fetch'
5518
+ });
5519
+ cb({
5520
+ requests
5521
+ });
5522
+ }).catch(() => {
5523
+ //
5524
+ });
5525
+ }
5526
+ };
5527
+ });
5528
+ return () => {
5529
+ restorePatch();
5530
+ };
5531
+ }
5532
+ let initialisedHandler = null;
5533
+ function initNetworkObserver(callback, win,
5534
+ // top window or in an iframe
5535
+ options) {
5536
+ if (!('performance' in win)) {
5537
+ return () => {
5538
+ //
5539
+ };
5540
+ }
5541
+ if (initialisedHandler) {
5542
+ logger$1.warn('Network observer already initialised, doing nothing');
5543
+ return () => {
5544
+ // the first caller should already have this handler and will be responsible for teardown
5545
+ };
5546
+ }
5547
+ const networkOptions = options ? Object.assign({}, defaultNetworkOptions, options) : defaultNetworkOptions;
5548
+ const cb = data => {
5549
+ const requests = [];
5550
+ data.requests.forEach(request => {
5551
+ const maskedRequest = networkOptions.maskRequestFn(request);
5552
+ if (maskedRequest) {
5553
+ requests.push(maskedRequest);
5554
+ }
5555
+ });
5556
+ if (requests.length > 0) {
5557
+ callback({
5558
+ ...data,
5559
+ requests
5560
+ });
5561
+ }
5562
+ };
5563
+ const performanceObserver = initPerformanceObserver(cb, win, networkOptions);
5564
+ // only wrap fetch and xhr if headers or body are being recorded
5565
+ let xhrObserver = () => {};
5566
+ let fetchObserver = () => {};
5567
+ if (networkOptions.recordHeaders || networkOptions.recordBody) {
5568
+ xhrObserver = initXhrObserver(cb, win, networkOptions);
5569
+ fetchObserver = initFetchObserver(cb, win, networkOptions);
5570
+ }
5571
+ const teardown = () => {
5572
+ performanceObserver();
5573
+ xhrObserver();
5574
+ fetchObserver();
5575
+ // allow future observers to initialize after cleanup
5576
+ initialisedHandler = null;
5577
+ };
5578
+ initialisedHandler = teardown;
5579
+ return teardown;
5580
+ }
5581
+ // use the plugin name so that when this functionality is adopted into rrweb
5582
+ // we can remove this plugin and use the core functionality with the same data
5583
+ const NETWORK_PLUGIN_NAME = 'rrweb/network@1';
5584
+ // TODO how should this be typed?
5585
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
5586
+ // @ts-ignore
5587
+ const getRecordNetworkPlugin = options => {
5588
+ return {
5589
+ name: NETWORK_PLUGIN_NAME,
5590
+ observer: initNetworkObserver,
5591
+ options: options
5592
+ };
5593
+ };
5594
+ // rrweb/networ@1 ends
5595
+
5596
+ // Use a safe global target (prefer `win`, fallback to globalThis)
5597
+ const _target = win ?? globalThis;
5598
+ _target.__PosthogExtensions__ = _target.__PosthogExtensions__ || {};
5599
+ // Expose rrweb.record under the same contract
5600
+ _target.__PosthogExtensions__.rrweb = _target.__PosthogExtensions__.rrweb || {
5601
+ record: record.record
5602
+ };
5603
+ // Provide initSessionRecording if not present — return a new LazyLoadedSessionRecording when called
5604
+ _target.__PosthogExtensions__.initSessionRecording = _target.__PosthogExtensions__.initSessionRecording || (instance => {
5605
+ return new LazyLoadedSessionRecording(instance);
5606
+ });
5607
+ // Provide a no-op loadExternalDependency that calls the callback immediately (since rrweb is bundled)
5608
+ _target.__PosthogExtensions__.loadExternalDependency = _target.__PosthogExtensions__.loadExternalDependency || ((instance, scriptName, cb) => {
5609
+ if (cb) cb(undefined);
5610
+ });
5611
+ // Provide rrwebPlugins object with network plugin factory if not present
5612
+ _target.__PosthogExtensions__.rrwebPlugins = _target.__PosthogExtensions__.rrwebPlugins || {};
5613
+ _target.__PosthogExtensions__.rrwebPlugins.getRecordNetworkPlugin = _target.__PosthogExtensions__.rrwebPlugins.getRecordNetworkPlugin || (() => getRecordNetworkPlugin);
5614
+
5615
+ // Type definitions copied from @rrweb/types@2.0.0-alpha.17 and rrweb-snapshot@2.0.0-alpha.17
5616
+ // Both packages are MIT licensed: https://github.com/rrweb-io/rrweb
5617
+ //
5618
+ // These types are copied here to avoid requiring users to install peer dependencies
5619
+ // solely for TypeScript type information.
5620
+ //
5621
+ // Original sources:
5622
+ // - @rrweb/types: https://github.com/rrweb-io/rrweb/tree/main/packages/@rrweb/types
5623
+ // - rrweb-snapshot: https://github.com/rrweb-io/rrweb/tree/main/packages/rrweb-snapshot
5624
+ var NodeType;
5625
+ (function (NodeType) {
5626
+ NodeType[NodeType["Document"] = 0] = "Document";
5627
+ NodeType[NodeType["DocumentType"] = 1] = "DocumentType";
5628
+ NodeType[NodeType["Element"] = 2] = "Element";
5629
+ NodeType[NodeType["Text"] = 3] = "Text";
5630
+ NodeType[NodeType["CDATA"] = 4] = "CDATA";
5631
+ NodeType[NodeType["Comment"] = 5] = "Comment";
5632
+ })(NodeType || (NodeType = {}));
5633
+ var EventType;
5634
+ (function (EventType) {
5635
+ EventType[EventType["DomContentLoaded"] = 0] = "DomContentLoaded";
5636
+ EventType[EventType["Load"] = 1] = "Load";
5637
+ EventType[EventType["FullSnapshot"] = 2] = "FullSnapshot";
5638
+ EventType[EventType["IncrementalSnapshot"] = 3] = "IncrementalSnapshot";
5639
+ EventType[EventType["Meta"] = 4] = "Meta";
5640
+ EventType[EventType["Custom"] = 5] = "Custom";
5641
+ EventType[EventType["Plugin"] = 6] = "Plugin";
5642
+ })(EventType || (EventType = {}));
5643
+ var IncrementalSource;
5644
+ (function (IncrementalSource) {
5645
+ IncrementalSource[IncrementalSource["Mutation"] = 0] = "Mutation";
5646
+ IncrementalSource[IncrementalSource["MouseMove"] = 1] = "MouseMove";
5647
+ IncrementalSource[IncrementalSource["MouseInteraction"] = 2] = "MouseInteraction";
5648
+ IncrementalSource[IncrementalSource["Scroll"] = 3] = "Scroll";
5649
+ IncrementalSource[IncrementalSource["ViewportResize"] = 4] = "ViewportResize";
5650
+ IncrementalSource[IncrementalSource["Input"] = 5] = "Input";
5651
+ IncrementalSource[IncrementalSource["TouchMove"] = 6] = "TouchMove";
5652
+ IncrementalSource[IncrementalSource["MediaInteraction"] = 7] = "MediaInteraction";
5653
+ IncrementalSource[IncrementalSource["StyleSheetRule"] = 8] = "StyleSheetRule";
5654
+ IncrementalSource[IncrementalSource["CanvasMutation"] = 9] = "CanvasMutation";
5655
+ IncrementalSource[IncrementalSource["Font"] = 10] = "Font";
5656
+ IncrementalSource[IncrementalSource["Log"] = 11] = "Log";
5657
+ IncrementalSource[IncrementalSource["Drag"] = 12] = "Drag";
5658
+ IncrementalSource[IncrementalSource["StyleDeclaration"] = 13] = "StyleDeclaration";
5659
+ IncrementalSource[IncrementalSource["Selection"] = 14] = "Selection";
5660
+ IncrementalSource[IncrementalSource["AdoptedStyleSheet"] = 15] = "AdoptedStyleSheet";
5661
+ IncrementalSource[IncrementalSource["CustomElement"] = 16] = "CustomElement";
5662
+ })(IncrementalSource || (IncrementalSource = {}));
5663
+ var MouseInteractions;
5664
+ (function (MouseInteractions) {
5665
+ MouseInteractions[MouseInteractions["MouseUp"] = 0] = "MouseUp";
5666
+ MouseInteractions[MouseInteractions["MouseDown"] = 1] = "MouseDown";
5667
+ MouseInteractions[MouseInteractions["Click"] = 2] = "Click";
5668
+ MouseInteractions[MouseInteractions["ContextMenu"] = 3] = "ContextMenu";
5669
+ MouseInteractions[MouseInteractions["DblClick"] = 4] = "DblClick";
5670
+ MouseInteractions[MouseInteractions["Focus"] = 5] = "Focus";
5671
+ MouseInteractions[MouseInteractions["Blur"] = 6] = "Blur";
5672
+ MouseInteractions[MouseInteractions["TouchStart"] = 7] = "TouchStart";
5673
+ MouseInteractions[MouseInteractions["TouchMove_Departed"] = 8] = "TouchMove_Departed";
5674
+ MouseInteractions[MouseInteractions["TouchEnd"] = 9] = "TouchEnd";
5675
+ MouseInteractions[MouseInteractions["TouchCancel"] = 10] = "TouchCancel";
5676
+ })(MouseInteractions || (MouseInteractions = {}));
5677
+ var PointerTypes;
5678
+ (function (PointerTypes) {
5679
+ PointerTypes[PointerTypes["Mouse"] = 0] = "Mouse";
5680
+ PointerTypes[PointerTypes["Pen"] = 1] = "Pen";
5681
+ PointerTypes[PointerTypes["Touch"] = 2] = "Touch";
5682
+ })(PointerTypes || (PointerTypes = {}));
5683
+ var MediaInteractions;
5684
+ (function (MediaInteractions) {
5685
+ MediaInteractions[MediaInteractions["Play"] = 0] = "Play";
5686
+ MediaInteractions[MediaInteractions["Pause"] = 1] = "Pause";
5687
+ MediaInteractions[MediaInteractions["Seeked"] = 2] = "Seeked";
5688
+ MediaInteractions[MediaInteractions["VolumeChange"] = 3] = "VolumeChange";
5689
+ MediaInteractions[MediaInteractions["RateChange"] = 4] = "RateChange";
5690
+ })(MediaInteractions || (MediaInteractions = {}));
5691
+ var CanvasContext;
5692
+ (function (CanvasContext) {
5693
+ CanvasContext[CanvasContext["2D"] = 0] = "2D";
5694
+ CanvasContext[CanvasContext["WebGL"] = 1] = "WebGL";
5695
+ CanvasContext[CanvasContext["WebGL2"] = 2] = "WebGL2";
5696
+ })(CanvasContext || (CanvasContext = {}));
5697
+
5698
+ const DISABLED = 'disabled';
5699
+ const SAMPLED = 'sampled';
5700
+ const ACTIVE = 'active';
5701
+ const BUFFERING = 'buffering';
5702
+ const PAUSED = 'paused';
5703
+ const LAZY_LOADING = 'lazy_loading';
5704
+ const TRIGGER = 'trigger';
5705
+ const TRIGGER_ACTIVATED = TRIGGER + '_activated';
5706
+ const TRIGGER_PENDING = TRIGGER + '_pending';
5707
+ const TRIGGER_DISABLED = TRIGGER + '_' + DISABLED;
5708
+ function sessionRecordingUrlTriggerMatches(url, triggers) {
5709
+ return triggers.some(trigger => {
5710
+ switch (trigger.matching) {
5711
+ case 'regex':
5712
+ return new RegExp(trigger.url).test(url);
5713
+ default:
5714
+ return false;
5715
+ }
5716
+ });
5717
+ }
5718
+ class OrTriggerMatching {
5719
+ constructor(_matchers) {
5720
+ this._matchers = _matchers;
5721
+ }
5722
+ triggerStatus(sessionId) {
5723
+ const statuses = this._matchers.map(m => m.triggerStatus(sessionId));
5724
+ if (statuses.includes(TRIGGER_ACTIVATED)) {
5725
+ return TRIGGER_ACTIVATED;
5726
+ }
5727
+ if (statuses.includes(TRIGGER_PENDING)) {
5728
+ return TRIGGER_PENDING;
5729
+ }
5730
+ return TRIGGER_DISABLED;
5731
+ }
5732
+ stop() {
5733
+ this._matchers.forEach(m => m.stop());
5734
+ }
5735
+ }
5736
+ class AndTriggerMatching {
5737
+ constructor(_matchers) {
5738
+ this._matchers = _matchers;
5739
+ }
5740
+ triggerStatus(sessionId) {
5741
+ const statuses = new Set();
5742
+ for (const matcher of this._matchers) {
5743
+ statuses.add(matcher.triggerStatus(sessionId));
5744
+ }
5745
+ // trigger_disabled means no config
5746
+ statuses.delete(TRIGGER_DISABLED);
5747
+ switch (statuses.size) {
5748
+ case 0:
5749
+ return TRIGGER_DISABLED;
5750
+ case 1:
5751
+ return Array.from(statuses)[0];
5752
+ default:
5753
+ return TRIGGER_PENDING;
5754
+ }
5755
+ }
5756
+ stop() {
5757
+ this._matchers.forEach(m => m.stop());
5758
+ }
5759
+ }
5760
+ class PendingTriggerMatching {
5761
+ triggerStatus() {
5762
+ return TRIGGER_PENDING;
5763
+ }
5764
+ stop() {
5765
+ // no-op
5766
+ }
5767
+ }
5768
+ const isEagerLoadedConfig = x => {
5769
+ return 'sessionRecording' in x;
5770
+ };
5771
+ class URLTriggerMatching {
5772
+ constructor(_instance) {
5773
+ this._instance = _instance;
5774
+ this._urlTriggers = [];
5775
+ this._urlBlocklist = [];
5776
+ this.urlBlocked = false;
5777
+ }
5778
+ onConfig(config) {
5779
+ this._urlTriggers = (isEagerLoadedConfig(config) ? isObject(config.sessionRecording) ? config.sessionRecording?.urlTriggers : [] : config?.urlTriggers) || [];
5780
+ this._urlBlocklist = (isEagerLoadedConfig(config) ? isObject(config.sessionRecording) ? config.sessionRecording?.urlBlocklist : [] : config?.urlBlocklist) || [];
5781
+ }
5782
+ /**
5783
+ * @deprecated Use onConfig instead
5784
+ */
5785
+ onRemoteConfig(response) {
5786
+ this.onConfig(response);
5787
+ }
5788
+ _urlTriggerStatus(sessionId) {
5789
+ if (this._urlTriggers.length === 0) {
5790
+ return TRIGGER_DISABLED;
5791
+ }
5792
+ const currentTriggerSession = this._instance?.get_property(SESSION_RECORDING_URL_TRIGGER_ACTIVATED_SESSION);
5793
+ return currentTriggerSession === sessionId ? TRIGGER_ACTIVATED : TRIGGER_PENDING;
5794
+ }
5795
+ triggerStatus(sessionId) {
5796
+ const urlTriggerStatus = this._urlTriggerStatus(sessionId);
5797
+ const eitherIsActivated = urlTriggerStatus === TRIGGER_ACTIVATED;
5798
+ const eitherIsPending = urlTriggerStatus === TRIGGER_PENDING;
5799
+ const result = eitherIsActivated ? TRIGGER_ACTIVATED : eitherIsPending ? TRIGGER_PENDING : TRIGGER_DISABLED;
5800
+ this._instance.registerForSession({
5801
+ $sdk_debug_replay_url_trigger_status: result
5802
+ });
5803
+ return result;
5804
+ }
5805
+ checkUrlTriggerConditions(onPause, onResume, onActivate) {
5806
+ if (typeof win === 'undefined' || !win.location.href) {
5807
+ return;
5808
+ }
5809
+ const url = win.location.href;
5810
+ const wasBlocked = this.urlBlocked;
5811
+ const isNowBlocked = sessionRecordingUrlTriggerMatches(url, this._urlBlocklist);
5812
+ if (wasBlocked && isNowBlocked) {
5813
+ // if the url is blocked and was already blocked, do nothing
5814
+ return;
5815
+ } else if (isNowBlocked && !wasBlocked) {
5816
+ onPause();
5817
+ } else if (!isNowBlocked && wasBlocked) {
5818
+ onResume();
5819
+ }
5820
+ if (sessionRecordingUrlTriggerMatches(url, this._urlTriggers)) {
5821
+ onActivate('url');
5822
+ }
5823
+ }
5824
+ stop() {
5825
+ // no-op
5826
+ }
5827
+ }
5828
+ class LinkedFlagMatching {
5829
+ constructor(_instance) {
5830
+ this._instance = _instance;
5831
+ this.linkedFlag = null;
5832
+ this.linkedFlagSeen = false;
5833
+ this._flagListenerCleanup = () => {};
5834
+ }
5835
+ triggerStatus() {
5836
+ let result = TRIGGER_PENDING;
5837
+ if (isNullish(this.linkedFlag)) {
5838
+ result = TRIGGER_DISABLED;
5839
+ }
5840
+ if (this.linkedFlagSeen) {
5841
+ result = TRIGGER_ACTIVATED;
5842
+ }
5843
+ this._instance.registerForSession({
5844
+ $sdk_debug_replay_linked_flag_trigger_status: result
5845
+ });
5846
+ return result;
5847
+ }
5848
+ onConfig(config, onStarted) {
5849
+ this.linkedFlag = (isEagerLoadedConfig(config) ? isObject(config.sessionRecording) ? config.sessionRecording?.linkedFlag : null : config?.linkedFlag) || null;
5850
+ if (!isNullish(this.linkedFlag) && !this.linkedFlagSeen) {
5851
+ const linkedFlag = isString(this.linkedFlag) ? this.linkedFlag : this.linkedFlag.flag;
5852
+ const linkedVariant = isString(this.linkedFlag) ? null : this.linkedFlag.variant;
5853
+ this._flagListenerCleanup = this._instance.onFeatureFlags(flags => {
5854
+ const flagIsPresent = isObject(flags) && linkedFlag in flags;
5855
+ let linkedFlagMatches = false;
5856
+ if (flagIsPresent) {
5857
+ const variantForFlagKey = flags[linkedFlag];
5858
+ if (isBoolean(variantForFlagKey)) {
5859
+ linkedFlagMatches = variantForFlagKey === true;
5860
+ } else if (linkedVariant) {
5861
+ linkedFlagMatches = variantForFlagKey === linkedVariant;
5862
+ } else {
5863
+ // then this is a variant flag and we want to match any string
5864
+ linkedFlagMatches = !!variantForFlagKey;
5865
+ }
5866
+ }
5867
+ this.linkedFlagSeen = linkedFlagMatches;
5868
+ if (linkedFlagMatches) {
5869
+ onStarted(linkedFlag, linkedVariant);
5870
+ }
5871
+ });
5872
+ }
5873
+ }
5874
+ /**
5875
+ * @deprecated Use onConfig instead
5876
+ */
5877
+ onRemoteConfig(response, onStarted) {
5878
+ this.onConfig(response, onStarted);
5879
+ }
5880
+ stop() {
5881
+ this._flagListenerCleanup();
5882
+ }
5883
+ }
5884
+ class EventTriggerMatching {
5885
+ constructor(_instance) {
5886
+ this._instance = _instance;
5887
+ this._eventTriggers = [];
5888
+ }
5889
+ onConfig(config) {
5890
+ this._eventTriggers = (isEagerLoadedConfig(config) ? isObject(config.sessionRecording) ? config.sessionRecording?.eventTriggers : [] : config?.eventTriggers) || [];
5891
+ }
5892
+ /**
5893
+ * @deprecated Use onConfig instead
5894
+ */
5895
+ onRemoteConfig(response) {
5896
+ this.onConfig(response);
5897
+ }
5898
+ _eventTriggerStatus(sessionId) {
5899
+ if (this._eventTriggers.length === 0) {
5900
+ return TRIGGER_DISABLED;
5901
+ }
5902
+ const currentTriggerSession = this._instance?.get_property(SESSION_RECORDING_EVENT_TRIGGER_ACTIVATED_SESSION);
5903
+ return currentTriggerSession === sessionId ? TRIGGER_ACTIVATED : TRIGGER_PENDING;
5904
+ }
5905
+ triggerStatus(sessionId) {
5906
+ const eventTriggerStatus = this._eventTriggerStatus(sessionId);
5907
+ const result = eventTriggerStatus === TRIGGER_ACTIVATED ? TRIGGER_ACTIVATED : eventTriggerStatus === TRIGGER_PENDING ? TRIGGER_PENDING : TRIGGER_DISABLED;
5908
+ this._instance.registerForSession({
5909
+ $sdk_debug_replay_event_trigger_status: result
5910
+ });
5911
+ return result;
5912
+ }
5913
+ stop() {
5914
+ // no-op
5915
+ }
5916
+ }
5917
+ // we need a no-op matcher before we can lazy-load the other matches, since all matchers wait on remote config anyway
5918
+ function nullMatchSessionRecordingStatus(triggersStatus) {
5919
+ if (!triggersStatus.isRecordingEnabled) {
5920
+ return DISABLED;
5921
+ }
5922
+ return BUFFERING;
5923
+ }
5924
+ function anyMatchSessionRecordingStatus(triggersStatus) {
5925
+ if (!triggersStatus.receivedFlags) {
5926
+ return BUFFERING;
5927
+ }
5928
+ if (!triggersStatus.isRecordingEnabled) {
5929
+ return DISABLED;
5930
+ }
5931
+ if (triggersStatus.urlTriggerMatching.urlBlocked) {
5932
+ return PAUSED;
5933
+ }
5934
+ const sampledActive = triggersStatus.isSampled === true;
5935
+ const triggerMatches = new OrTriggerMatching([triggersStatus.eventTriggerMatching, triggersStatus.urlTriggerMatching, triggersStatus.linkedFlagMatching]).triggerStatus(triggersStatus.sessionId);
5936
+ if (sampledActive) {
5937
+ return SAMPLED;
5938
+ }
5939
+ if (triggerMatches === TRIGGER_ACTIVATED) {
5940
+ return ACTIVE;
5941
+ }
5942
+ if (triggerMatches === TRIGGER_PENDING) {
5943
+ // even if sampled active is false, we should still be buffering
5944
+ // since a pending trigger could override it
5945
+ return BUFFERING;
5946
+ }
5947
+ // if sampling is set and the session is already decided to not be sampled
5948
+ // then we should never be active
5949
+ if (triggersStatus.isSampled === false) {
5950
+ return DISABLED;
5951
+ }
5952
+ return ACTIVE;
5953
+ }
5954
+ function allMatchSessionRecordingStatus(triggersStatus) {
5955
+ if (!triggersStatus.receivedFlags) {
5956
+ return BUFFERING;
5957
+ }
5958
+ if (!triggersStatus.isRecordingEnabled) {
5959
+ return DISABLED;
5960
+ }
5961
+ if (triggersStatus.urlTriggerMatching.urlBlocked) {
5962
+ return PAUSED;
5963
+ }
5964
+ const andTriggerMatch = new AndTriggerMatching([triggersStatus.eventTriggerMatching, triggersStatus.urlTriggerMatching, triggersStatus.linkedFlagMatching]);
5965
+ const currentTriggerStatus = andTriggerMatch.triggerStatus(triggersStatus.sessionId);
5966
+ const hasTriggersConfigured = currentTriggerStatus !== TRIGGER_DISABLED;
5967
+ const hasSamplingConfigured = isBoolean(triggersStatus.isSampled);
5968
+ if (hasTriggersConfigured && currentTriggerStatus === TRIGGER_PENDING) {
5969
+ return BUFFERING;
5970
+ }
5971
+ if (hasTriggersConfigured && currentTriggerStatus === TRIGGER_DISABLED) {
5972
+ return DISABLED;
5973
+ }
5974
+ // sampling can't ever cause buffering, it's always determined right away or not configured
5975
+ if (hasSamplingConfigured && !triggersStatus.isSampled) {
5976
+ return DISABLED;
5977
+ }
5978
+ // If sampling is configured and set to true, return sampled
5979
+ if (triggersStatus.isSampled === true) {
5980
+ return SAMPLED;
5981
+ }
5982
+ return ACTIVE;
5983
+ }
5984
+
5985
+ // taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value#circular_references
5986
+ function circularReferenceReplacer() {
5987
+ const ancestors = [];
5988
+ return function (_key, value) {
5989
+ if (isObject(value)) {
5990
+ // `this` is the object that value is contained in,
5991
+ // i.e., its direct parent.
5992
+ while (ancestors.length > 0 && ancestors[ancestors.length - 1] !== this) {
5993
+ ancestors.pop();
5994
+ }
5995
+ if (ancestors.includes(value)) {
5996
+ return '[Circular]';
5997
+ }
5998
+ ancestors.push(value);
5999
+ return value;
6000
+ } else {
6001
+ return value;
6002
+ }
6003
+ };
6004
+ }
6005
+ function estimateSize(sizeable) {
6006
+ return JSON.stringify(sizeable, circularReferenceReplacer())?.length || 0;
6007
+ }
6008
+ const INCREMENTAL_SNAPSHOT_EVENT_TYPE = 3;
6009
+ const PLUGIN_EVENT_TYPE = 6;
6010
+ const MUTATION_SOURCE_TYPE = 0;
6011
+ const CONSOLE_LOG_PLUGIN_NAME = 'rrweb/console@1'; // The name of the rr-web plugin that emits console logs
6012
+ // Console logs can be really large. This function truncates large logs
6013
+ // It's a simple function that just truncates long strings.
6014
+ // TODO: Ideally this function would have better handling of objects + lists,
6015
+ // so they could still be rendered in a pretty way after truncation.
6016
+ function truncateLargeConsoleLogs(_event) {
6017
+ const event = _event;
6018
+ const MAX_STRING_SIZE = 2000; // Maximum number of characters allowed in a string
6019
+ const MAX_STRINGS_PER_LOG = 10; // A log can consist of multiple strings (e.g. consol.log('string1', 'string2'))
6020
+ if (event && isObject(event) && event.type === PLUGIN_EVENT_TYPE && isObject(event.data) && event.data.plugin === CONSOLE_LOG_PLUGIN_NAME) {
6021
+ // Note: event.data.payload.payload comes from rr-web, and is an array of strings
6022
+ if (event.data.payload.payload.length > MAX_STRINGS_PER_LOG) {
6023
+ event.data.payload.payload = event.data.payload.payload.slice(0, MAX_STRINGS_PER_LOG);
6024
+ event.data.payload.payload.push('...[truncated]');
6025
+ }
6026
+ const updatedPayload = [];
6027
+ for (let i = 0; i < event.data.payload.payload.length; i++) {
6028
+ if (event.data.payload.payload[i] &&
6029
+ // Value can be null
6030
+ event.data.payload.payload[i].length > MAX_STRING_SIZE) {
6031
+ updatedPayload.push(event.data.payload.payload[i].slice(0, MAX_STRING_SIZE) + '...[truncated]');
6032
+ } else {
6033
+ updatedPayload.push(event.data.payload.payload[i]);
6034
+ }
6035
+ }
6036
+ event.data.payload.payload = updatedPayload;
6037
+ // Return original type
6038
+ return _event;
6039
+ }
6040
+ return _event;
6041
+ }
6042
+
6043
+ // DEFLATE is a complex format; to read this code, you should probably check the RFC first:
6044
+ // https://tools.ietf.org/html/rfc1951
6045
+ // You may also wish to take a look at the guide I made about this program:
6046
+ // https://gist.github.com/101arrowz/253f31eb5abc3d9275ab943003ffecad
6047
+ // Much of the following code is similar to that of UZIP.js:
6048
+ // https://github.com/photopea/UZIP.js
6049
+ // Many optimizations have been made, so the bundle size is ultimately smaller but performance is similar.
6050
+ // Sometimes 0 will appear where -1 would be more appropriate. This is because using a uint
6051
+ // is better for memory in most engines (I *think*).
6052
+ // Mediocre shim
6053
+ var Worker;
6054
+ try {
6055
+ Worker = require('worker_threads').Worker;
6056
+ }
6057
+ catch (e) {
6058
+ }
6059
+
6060
+ // aliases for shorter compressed code (most minifers don't do this)
6061
+ var u8 = Uint8Array, u16 = Uint16Array, u32 = Uint32Array;
6062
+ // fixed length extra bits
6063
+ var fleb = new u8([0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, /* unused */ 0, 0, /* impossible */ 0]);
6064
+ // fixed distance extra bits
6065
+ // see fleb note
6066
+ var fdeb = new u8([0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, /* unused */ 0, 0]);
6067
+ // code length index map
6068
+ var clim = new u8([16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]);
6069
+ // get base, reverse index map from extra bits
6070
+ var freb = function (eb, start) {
6071
+ var b = new u16(31);
6072
+ for (var i = 0; i < 31; ++i) {
6073
+ b[i] = start += 1 << eb[i - 1];
6074
+ }
6075
+ // numbers here are at max 18 bits
6076
+ var r = new u32(b[30]);
6077
+ for (var i = 1; i < 30; ++i) {
6078
+ for (var j = b[i]; j < b[i + 1]; ++j) {
6079
+ r[j] = ((j - b[i]) << 5) | i;
6080
+ }
6081
+ }
6082
+ return [b, r];
6083
+ };
6084
+ var _a = freb(fleb, 2), fl = _a[0], revfl = _a[1];
6085
+ // we can ignore the fact that the other numbers are wrong; they never happen anyway
6086
+ fl[28] = 258, revfl[258] = 28;
6087
+ var _b = freb(fdeb, 0), revfd = _b[1];
6088
+ // map of value to reverse (assuming 16 bits)
6089
+ var rev = new u16(32768);
6090
+ for (var i = 0; i < 32768; ++i) {
6091
+ // reverse table algorithm from SO
6092
+ var x = ((i & 0xAAAA) >>> 1) | ((i & 0x5555) << 1);
6093
+ x = ((x & 0xCCCC) >>> 2) | ((x & 0x3333) << 2);
6094
+ x = ((x & 0xF0F0) >>> 4) | ((x & 0x0F0F) << 4);
6095
+ rev[i] = (((x & 0xFF00) >>> 8) | ((x & 0x00FF) << 8)) >>> 1;
6096
+ }
6097
+ // create huffman tree from u8 "map": index -> code length for code index
6098
+ // mb (max bits) must be at most 15
6099
+ // TODO: optimize/split up?
6100
+ var hMap = (function (cd, mb, r) {
6101
+ var s = cd.length;
6102
+ // index
6103
+ var i = 0;
6104
+ // u16 "map": index -> # of codes with bit length = index
6105
+ var l = new u16(mb);
6106
+ // length of cd must be 288 (total # of codes)
6107
+ for (; i < s; ++i)
6108
+ ++l[cd[i] - 1];
6109
+ // u16 "map": index -> minimum code for bit length = index
6110
+ var le = new u16(mb);
6111
+ for (i = 0; i < mb; ++i) {
6112
+ le[i] = (le[i - 1] + l[i - 1]) << 1;
6113
+ }
6114
+ var co;
6115
+ if (r) {
6116
+ // u16 "map": index -> number of actual bits, symbol for code
6117
+ co = new u16(1 << mb);
6118
+ // bits to remove for reverser
6119
+ var rvb = 15 - mb;
6120
+ for (i = 0; i < s; ++i) {
6121
+ // ignore 0 lengths
6122
+ if (cd[i]) {
6123
+ // num encoding both symbol and bits read
6124
+ var sv = (i << 4) | cd[i];
6125
+ // free bits
6126
+ var r_1 = mb - cd[i];
6127
+ // start value
6128
+ var v = le[cd[i] - 1]++ << r_1;
6129
+ // m is end value
6130
+ for (var m = v | ((1 << r_1) - 1); v <= m; ++v) {
6131
+ // every 16 bit value starting with the code yields the same result
6132
+ co[rev[v] >>> rvb] = sv;
6133
+ }
6134
+ }
6135
+ }
6136
+ }
6137
+ else {
6138
+ co = new u16(s);
6139
+ for (i = 0; i < s; ++i)
6140
+ co[i] = rev[le[cd[i] - 1]++] >>> (15 - cd[i]);
6141
+ }
6142
+ return co;
6143
+ });
6144
+ // fixed length tree
6145
+ var flt = new u8(288);
6146
+ for (var i = 0; i < 144; ++i)
6147
+ flt[i] = 8;
6148
+ for (var i = 144; i < 256; ++i)
6149
+ flt[i] = 9;
6150
+ for (var i = 256; i < 280; ++i)
6151
+ flt[i] = 7;
6152
+ for (var i = 280; i < 288; ++i)
6153
+ flt[i] = 8;
6154
+ // fixed distance tree
6155
+ var fdt = new u8(32);
6156
+ for (var i = 0; i < 32; ++i)
6157
+ fdt[i] = 5;
6158
+ // fixed length map
6159
+ var flm = /*#__PURE__*/ hMap(flt, 9, 0);
6160
+ // fixed distance map
6161
+ var fdm = /*#__PURE__*/ hMap(fdt, 5, 0);
6162
+ // get end of byte
6163
+ var shft = function (p) { return ((p / 8) >> 0) + (p & 7 && 1); };
6164
+ // typed array slice - allows garbage collector to free original reference,
6165
+ // while being more compatible than .slice
6166
+ var slc = function (v, s, e) {
6167
+ if (e == null || e > v.length)
6168
+ e = v.length;
6169
+ // can't use .constructor in case user-supplied
6170
+ var n = new (v instanceof u16 ? u16 : v instanceof u32 ? u32 : u8)(e - s);
6171
+ n.set(v.subarray(s, e));
6172
+ return n;
6173
+ };
6174
+ // starting at p, write the minimum number of bits that can hold v to d
6175
+ var wbits = function (d, p, v) {
6176
+ v <<= p & 7;
6177
+ var o = (p / 8) >> 0;
6178
+ d[o] |= v;
6179
+ d[o + 1] |= v >>> 8;
6180
+ };
6181
+ // starting at p, write the minimum number of bits (>8) that can hold v to d
6182
+ var wbits16 = function (d, p, v) {
6183
+ v <<= p & 7;
6184
+ var o = (p / 8) >> 0;
6185
+ d[o] |= v;
6186
+ d[o + 1] |= v >>> 8;
6187
+ d[o + 2] |= v >>> 16;
6188
+ };
6189
+ // creates code lengths from a frequency table
6190
+ var hTree = function (d, mb) {
6191
+ // Need extra info to make a tree
6192
+ var t = [];
6193
+ for (var i = 0; i < d.length; ++i) {
6194
+ if (d[i])
6195
+ t.push({ s: i, f: d[i] });
6196
+ }
6197
+ var s = t.length;
6198
+ var t2 = t.slice();
6199
+ if (!s)
6200
+ return [new u8(0), 0];
6201
+ if (s == 1) {
6202
+ var v = new u8(t[0].s + 1);
6203
+ v[t[0].s] = 1;
6204
+ return [v, 1];
6205
+ }
6206
+ t.sort(function (a, b) { return a.f - b.f; });
6207
+ // after i2 reaches last ind, will be stopped
6208
+ // freq must be greater than largest possible number of symbols
6209
+ t.push({ s: -1, f: 25001 });
6210
+ var l = t[0], r = t[1], i0 = 0, i1 = 1, i2 = 2;
6211
+ t[0] = { s: -1, f: l.f + r.f, l: l, r: r };
6212
+ // efficient algorithm from UZIP.js
6213
+ // i0 is lookbehind, i2 is lookahead - after processing two low-freq
6214
+ // symbols that combined have high freq, will start processing i2 (high-freq,
6215
+ // non-composite) symbols instead
6216
+ // see https://reddit.com/r/photopea/comments/ikekht/uzipjs_questions/
6217
+ while (i1 != s - 1) {
6218
+ l = t[t[i0].f < t[i2].f ? i0++ : i2++];
6219
+ r = t[i0 != i1 && t[i0].f < t[i2].f ? i0++ : i2++];
6220
+ t[i1++] = { s: -1, f: l.f + r.f, l: l, r: r };
6221
+ }
6222
+ var maxSym = t2[0].s;
6223
+ for (var i = 1; i < s; ++i) {
6224
+ if (t2[i].s > maxSym)
6225
+ maxSym = t2[i].s;
6226
+ }
6227
+ // code lengths
6228
+ var tr = new u16(maxSym + 1);
6229
+ // max bits in tree
6230
+ var mbt = ln(t[i1 - 1], tr, 0);
6231
+ if (mbt > mb) {
6232
+ // more algorithms from UZIP.js
6233
+ // TODO: find out how this code works (debt)
6234
+ // ind debt
6235
+ var i = 0, dt = 0;
6236
+ // left cost
6237
+ var lft = mbt - mb, cst = 1 << lft;
6238
+ t2.sort(function (a, b) { return tr[b.s] - tr[a.s] || a.f - b.f; });
6239
+ for (; i < s; ++i) {
6240
+ var i2_1 = t2[i].s;
6241
+ if (tr[i2_1] > mb) {
6242
+ dt += cst - (1 << (mbt - tr[i2_1]));
6243
+ tr[i2_1] = mb;
6244
+ }
6245
+ else
6246
+ break;
6247
+ }
6248
+ dt >>>= lft;
6249
+ while (dt > 0) {
6250
+ var i2_2 = t2[i].s;
6251
+ if (tr[i2_2] < mb)
6252
+ dt -= 1 << (mb - tr[i2_2]++ - 1);
6253
+ else
6254
+ ++i;
6255
+ }
6256
+ for (; i >= 0 && dt; --i) {
6257
+ var i2_3 = t2[i].s;
6258
+ if (tr[i2_3] == mb) {
6259
+ --tr[i2_3];
6260
+ ++dt;
6261
+ }
6262
+ }
6263
+ mbt = mb;
6264
+ }
6265
+ return [new u8(tr), mbt];
6266
+ };
6267
+ // get the max length and assign length codes
6268
+ var ln = function (n, l, d) {
6269
+ return n.s == -1
6270
+ ? Math.max(ln(n.l, l, d + 1), ln(n.r, l, d + 1))
6271
+ : (l[n.s] = d);
6272
+ };
6273
+ // length codes generation
6274
+ var lc = function (c) {
6275
+ var s = c.length;
6276
+ // Note that the semicolon was intentional
6277
+ while (s && !c[--s])
6278
+ ;
6279
+ var cl = new u16(++s);
6280
+ // ind num streak
6281
+ var cli = 0, cln = c[0], cls = 1;
6282
+ var w = function (v) { cl[cli++] = v; };
6283
+ for (var i = 1; i <= s; ++i) {
6284
+ if (c[i] == cln && i != s)
6285
+ ++cls;
6286
+ else {
6287
+ if (!cln && cls > 2) {
6288
+ for (; cls > 138; cls -= 138)
6289
+ w(32754);
6290
+ if (cls > 2) {
6291
+ w(cls > 10 ? ((cls - 11) << 5) | 28690 : ((cls - 3) << 5) | 12305);
6292
+ cls = 0;
6293
+ }
6294
+ }
6295
+ else if (cls > 3) {
6296
+ w(cln), --cls;
6297
+ for (; cls > 6; cls -= 6)
6298
+ w(8304);
6299
+ if (cls > 2)
6300
+ w(((cls - 3) << 5) | 8208), cls = 0;
6301
+ }
6302
+ while (cls--)
6303
+ w(cln);
6304
+ cls = 1;
6305
+ cln = c[i];
6306
+ }
6307
+ }
6308
+ return [cl.subarray(0, cli), s];
6309
+ };
6310
+ // calculate the length of output from tree, code lengths
6311
+ var clen = function (cf, cl) {
6312
+ var l = 0;
6313
+ for (var i = 0; i < cl.length; ++i)
6314
+ l += cf[i] * cl[i];
6315
+ return l;
6316
+ };
6317
+ // writes a fixed block
6318
+ // returns the new bit pos
6319
+ var wfblk = function (out, pos, dat) {
6320
+ // no need to write 00 as type: TypedArray defaults to 0
6321
+ var s = dat.length;
6322
+ var o = shft(pos + 2);
6323
+ out[o] = s & 255;
6324
+ out[o + 1] = s >>> 8;
6325
+ out[o + 2] = out[o] ^ 255;
6326
+ out[o + 3] = out[o + 1] ^ 255;
6327
+ for (var i = 0; i < s; ++i)
6328
+ out[o + i + 4] = dat[i];
6329
+ return (o + 4 + s) * 8;
6330
+ };
6331
+ // writes a block
6332
+ var wblk = function (dat, out, final, syms, lf, df, eb, li, bs, bl, p) {
6333
+ wbits(out, p++, final);
6334
+ ++lf[256];
6335
+ var _a = hTree(lf, 15), dlt = _a[0], mlb = _a[1];
6336
+ var _b = hTree(df, 15), ddt = _b[0], mdb = _b[1];
6337
+ var _c = lc(dlt), lclt = _c[0], nlc = _c[1];
6338
+ var _d = lc(ddt), lcdt = _d[0], ndc = _d[1];
6339
+ var lcfreq = new u16(19);
6340
+ for (var i = 0; i < lclt.length; ++i)
6341
+ lcfreq[lclt[i] & 31]++;
6342
+ for (var i = 0; i < lcdt.length; ++i)
6343
+ lcfreq[lcdt[i] & 31]++;
6344
+ var _e = hTree(lcfreq, 7), lct = _e[0], mlcb = _e[1];
6345
+ var nlcc = 19;
6346
+ for (; nlcc > 4 && !lct[clim[nlcc - 1]]; --nlcc)
6347
+ ;
6348
+ var flen = (bl + 5) << 3;
6349
+ var ftlen = clen(lf, flt) + clen(df, fdt) + eb;
6350
+ var dtlen = clen(lf, dlt) + clen(df, ddt) + eb + 14 + 3 * nlcc + clen(lcfreq, lct) + (2 * lcfreq[16] + 3 * lcfreq[17] + 7 * lcfreq[18]);
6351
+ if (flen <= ftlen && flen <= dtlen)
6352
+ return wfblk(out, p, dat.subarray(bs, bs + bl));
6353
+ var lm, ll, dm, dl;
6354
+ wbits(out, p, 1 + (dtlen < ftlen)), p += 2;
6355
+ if (dtlen < ftlen) {
6356
+ lm = hMap(dlt, mlb, 0), ll = dlt, dm = hMap(ddt, mdb, 0), dl = ddt;
6357
+ var llm = hMap(lct, mlcb, 0);
6358
+ wbits(out, p, nlc - 257);
6359
+ wbits(out, p + 5, ndc - 1);
6360
+ wbits(out, p + 10, nlcc - 4);
6361
+ p += 14;
6362
+ for (var i = 0; i < nlcc; ++i)
6363
+ wbits(out, p + 3 * i, lct[clim[i]]);
6364
+ p += 3 * nlcc;
6365
+ var lcts = [lclt, lcdt];
6366
+ for (var it = 0; it < 2; ++it) {
6367
+ var clct = lcts[it];
6368
+ for (var i = 0; i < clct.length; ++i) {
6369
+ var len = clct[i] & 31;
6370
+ wbits(out, p, llm[len]), p += lct[len];
6371
+ if (len > 15)
6372
+ wbits(out, p, (clct[i] >>> 5) & 127), p += clct[i] >>> 12;
6373
+ }
6374
+ }
6375
+ }
6376
+ else {
6377
+ lm = flm, ll = flt, dm = fdm, dl = fdt;
6378
+ }
6379
+ for (var i = 0; i < li; ++i) {
6380
+ if (syms[i] > 255) {
6381
+ var len = (syms[i] >>> 18) & 31;
6382
+ wbits16(out, p, lm[len + 257]), p += ll[len + 257];
6383
+ if (len > 7)
6384
+ wbits(out, p, (syms[i] >>> 23) & 31), p += fleb[len];
6385
+ var dst = syms[i] & 31;
6386
+ wbits16(out, p, dm[dst]), p += dl[dst];
6387
+ if (dst > 3)
6388
+ wbits16(out, p, (syms[i] >>> 5) & 8191), p += fdeb[dst];
6389
+ }
6390
+ else {
6391
+ wbits16(out, p, lm[syms[i]]), p += ll[syms[i]];
6392
+ }
6393
+ }
6394
+ wbits16(out, p, lm[256]);
6395
+ return p + ll[256];
6396
+ };
6397
+ // deflate options (nice << 13) | chain
6398
+ var deo = /*#__PURE__*/ new u32([65540, 131080, 131088, 131104, 262176, 1048704, 1048832, 2114560, 2117632]);
6399
+ // compresses data into a raw DEFLATE buffer
6400
+ var dflt = function (dat, lvl, plvl, pre, post, lst) {
6401
+ var s = dat.length;
6402
+ var o = new u8(pre + s + 5 * (1 + Math.floor(s / 7000)) + post);
6403
+ // writing to this writes to the output buffer
6404
+ var w = o.subarray(pre, o.length - post);
6405
+ var pos = 0;
6406
+ if (!lvl || s < 8) {
6407
+ for (var i = 0; i <= s; i += 65535) {
6408
+ // end
6409
+ var e = i + 65535;
6410
+ if (e < s) {
6411
+ // write full block
6412
+ pos = wfblk(w, pos, dat.subarray(i, e));
6413
+ }
6414
+ else {
6415
+ // write final block
6416
+ w[i] = lst;
6417
+ pos = wfblk(w, pos, dat.subarray(i, s));
6418
+ }
6419
+ }
6420
+ }
6421
+ else {
6422
+ var opt = deo[lvl - 1];
6423
+ var n = opt >>> 13, c = opt & 8191;
6424
+ var msk_1 = (1 << plvl) - 1;
6425
+ // prev 2-byte val map curr 2-byte val map
6426
+ var prev = new u16(32768), head = new u16(msk_1 + 1);
6427
+ var bs1_1 = Math.ceil(plvl / 3), bs2_1 = 2 * bs1_1;
6428
+ var hsh = function (i) { return (dat[i] ^ (dat[i + 1] << bs1_1) ^ (dat[i + 2] << bs2_1)) & msk_1; };
6429
+ // 24576 is an arbitrary number of maximum symbols per block
6430
+ // 424 buffer for last block
6431
+ var syms = new u32(25000);
6432
+ // length/literal freq distance freq
6433
+ var lf = new u16(288), df = new u16(32);
6434
+ // l/lcnt exbits index l/lind waitdx bitpos
6435
+ var lc_1 = 0, eb = 0, i = 0, li = 0, wi = 0, bs = 0;
6436
+ for (; i < s; ++i) {
6437
+ // hash value
6438
+ var hv = hsh(i);
6439
+ // index mod 32768
6440
+ var imod = i & 32767;
6441
+ // previous index with this value
6442
+ var pimod = head[hv];
6443
+ prev[imod] = pimod;
6444
+ head[hv] = imod;
6445
+ // We always should modify head and prev, but only add symbols if
6446
+ // this data is not yet processed ("wait" for wait index)
6447
+ if (wi <= i) {
6448
+ // bytes remaining
6449
+ var rem = s - i;
6450
+ if ((lc_1 > 7000 || li > 24576) && rem > 423) {
6451
+ pos = wblk(dat, w, 0, syms, lf, df, eb, li, bs, i - bs, pos);
6452
+ li = lc_1 = eb = 0, bs = i;
6453
+ for (var j = 0; j < 286; ++j)
6454
+ lf[j] = 0;
6455
+ for (var j = 0; j < 30; ++j)
6456
+ df[j] = 0;
6457
+ }
6458
+ // len dist chain
6459
+ var l = 2, d = 0, ch_1 = c, dif = (imod - pimod) & 32767;
6460
+ if (rem > 2 && hv == hsh(i - dif)) {
6461
+ var maxn = Math.min(n, rem) - 1;
6462
+ var maxd = Math.min(32767, i);
6463
+ // max possible length
6464
+ // not capped at dif because decompressors implement "rolling" index population
6465
+ var ml = Math.min(258, rem);
6466
+ while (dif <= maxd && --ch_1 && imod != pimod) {
6467
+ if (dat[i + l] == dat[i + l - dif]) {
6468
+ var nl = 0;
6469
+ for (; nl < ml && dat[i + nl] == dat[i + nl - dif]; ++nl)
6470
+ ;
6471
+ if (nl > l) {
6472
+ l = nl, d = dif;
6473
+ // break out early when we reach "nice" (we are satisfied enough)
6474
+ if (nl > maxn)
6475
+ break;
6476
+ // now, find the rarest 2-byte sequence within this
6477
+ // length of literals and search for that instead.
6478
+ // Much faster than just using the start
6479
+ var mmd = Math.min(dif, nl - 2);
6480
+ var md = 0;
6481
+ for (var j = 0; j < mmd; ++j) {
6482
+ var ti = (i - dif + j + 32768) & 32767;
6483
+ var pti = prev[ti];
6484
+ var cd = (ti - pti + 32768) & 32767;
6485
+ if (cd > md)
6486
+ md = cd, pimod = ti;
6487
+ }
6488
+ }
6489
+ }
6490
+ // check the previous match
6491
+ imod = pimod, pimod = prev[imod];
6492
+ dif += (imod - pimod + 32768) & 32767;
6493
+ }
6494
+ }
6495
+ // d will be nonzero only when a match was found
6496
+ if (d) {
6497
+ // store both dist and len data in one Uint32
6498
+ // Make sure this is recognized as a len/dist with 28th bit (2^28)
6499
+ syms[li++] = 268435456 | (revfl[l] << 18) | revfd[d];
6500
+ var lin = revfl[l] & 31, din = revfd[d] & 31;
6501
+ eb += fleb[lin] + fdeb[din];
6502
+ ++lf[257 + lin];
6503
+ ++df[din];
6504
+ wi = i + l;
6505
+ ++lc_1;
6506
+ }
6507
+ else {
6508
+ syms[li++] = dat[i];
6509
+ ++lf[dat[i]];
6510
+ }
6511
+ }
6512
+ }
6513
+ pos = wblk(dat, w, lst, syms, lf, df, eb, li, bs, i - bs, pos);
6514
+ }
6515
+ return slc(o, 0, pre + shft(pos) + post);
6516
+ };
6517
+ // CRC32 table
6518
+ var crct = /*#__PURE__*/ (function () {
6519
+ var t = new u32(256);
6520
+ for (var i = 0; i < 256; ++i) {
6521
+ var c = i, k = 9;
6522
+ while (--k)
6523
+ c = ((c & 1) && 0xEDB88320) ^ (c >>> 1);
6524
+ t[i] = c;
6525
+ }
6526
+ return t;
6527
+ })();
6528
+ // CRC32
6529
+ var crc = function () {
6530
+ var c = 0xFFFFFFFF;
6531
+ return {
6532
+ p: function (d) {
6533
+ // closures have awful performance
6534
+ var cr = c;
6535
+ for (var i = 0; i < d.length; ++i)
6536
+ cr = crct[(cr & 255) ^ d[i]] ^ (cr >>> 8);
6537
+ c = cr;
6538
+ },
6539
+ d: function () { return c ^ 0xFFFFFFFF; }
6540
+ };
6541
+ };
6542
+ // deflate with opts
6543
+ var dopt = function (dat, opt, pre, post, st) {
6544
+ return dflt(dat, opt.level == null ? 6 : opt.level, opt.mem == null ? Math.ceil(Math.max(8, Math.min(13, Math.log(dat.length))) * 1.5) : (12 + opt.mem), pre, post, true);
6545
+ };
6546
+ // write bytes
6547
+ var wbytes = function (d, b, v) {
6548
+ for (; v; ++b)
6549
+ d[b] = v, v >>>= 8;
6550
+ };
6551
+ // gzip header
6552
+ var gzh = function (c, o) {
6553
+ var fn = o.filename;
6554
+ c[0] = 31, c[1] = 139, c[2] = 8, c[8] = o.level < 2 ? 4 : o.level == 9 ? 2 : 0, c[9] = 3; // assume Unix
6555
+ if (o.mtime != 0)
6556
+ wbytes(c, 4, Math.floor(new Date(o.mtime || Date.now()) / 1000));
6557
+ if (fn) {
6558
+ c[3] = 8;
6559
+ for (var i = 0; i <= fn.length; ++i)
6560
+ c[i + 10] = fn.charCodeAt(i);
6561
+ }
6562
+ };
6563
+ // gzip header length
6564
+ var gzhl = function (o) { return 10 + ((o.filename && (o.filename.length + 1)) || 0); };
6565
+ /**
6566
+ * Compresses data with GZIP
6567
+ * @param data The data to compress
6568
+ * @param opts The compression options
6569
+ * @returns The gzipped version of the data
6570
+ */
6571
+ function gzipSync(data, opts) {
6572
+ if (opts === void 0) { opts = {}; }
6573
+ var c = crc(), l = data.length;
6574
+ c.p(data);
6575
+ var d = dopt(data, opts, gzhl(opts), 8), s = d.length;
6576
+ return gzh(d, opts), wbytes(d, s - 8, c.d()), wbytes(d, s - 4, l), d;
6577
+ }
6578
+ /**
6579
+ * Converts a string into a Uint8Array for use with compression/decompression methods
6580
+ * @param str The string to encode
6581
+ * @param latin1 Whether or not to interpret the data as Latin-1. This should
6582
+ * not need to be true unless decoding a binary string.
6583
+ * @returns The string encoded in UTF-8/Latin-1 binary
6584
+ */
6585
+ function strToU8(str, latin1) {
6586
+ var l = str.length;
6587
+ if (typeof TextEncoder != 'undefined')
6588
+ return new TextEncoder().encode(str);
6589
+ var ar = new u8(str.length + (str.length >>> 1));
6590
+ var ai = 0;
6591
+ var w = function (v) { ar[ai++] = v; };
6592
+ for (var i = 0; i < l; ++i) {
6593
+ if (ai + 5 > ar.length) {
6594
+ var n = new u8(ai + 8 + ((l - i) << 1));
6595
+ n.set(ar);
6596
+ ar = n;
6597
+ }
6598
+ var c = str.charCodeAt(i);
6599
+ if (c < 128 || latin1)
6600
+ w(c);
6601
+ else if (c < 2048)
6602
+ w(192 | (c >>> 6)), w(128 | (c & 63));
6603
+ else if (c > 55295 && c < 57344)
6604
+ c = 65536 + (c & 1023 << 10) | (str.charCodeAt(++i) & 1023),
6605
+ w(240 | (c >>> 18)), w(128 | ((c >>> 12) & 63)), w(128 | ((c >>> 6) & 63)), w(128 | (c & 63));
6606
+ else
6607
+ w(224 | (c >>> 12)), w(128 | ((c >>> 6) & 63)), w(128 | (c & 63));
6608
+ }
6609
+ return slc(ar, 0, ai);
6610
+ }
6611
+ /**
6612
+ * Converts a Uint8Array to a string
6613
+ * @param dat The data to decode to string
6614
+ * @param latin1 Whether or not to interpret the data as Latin-1. This should
6615
+ * not need to be true unless encoding to binary string.
6616
+ * @returns The original UTF-8/Latin-1 string
6617
+ */
6618
+ function strFromU8(dat, latin1) {
6619
+ var r = '';
6620
+ for (var i = 0; i < dat.length;) {
6621
+ var c = dat[i++];
6622
+ r += String.fromCharCode(c);
6623
+ }
6624
+ return r;
6625
+ }
6626
+
6627
+ class MutationThrottler {
6628
+ constructor(_rrweb, _options = {}) {
6629
+ this._rrweb = _rrweb;
6630
+ this._options = _options;
6631
+ this._loggedTracker = {};
6632
+ this._onNodeRateLimited = key => {
6633
+ if (!this._loggedTracker[key]) {
6634
+ this._loggedTracker[key] = true;
6635
+ const node = this._getNode(key);
6636
+ this._options.onBlockedNode?.(key, node);
6637
+ }
6638
+ };
6639
+ this._getNodeOrRelevantParent = id => {
6640
+ // For some nodes we know they are part of a larger tree such as an SVG.
6641
+ // For those we want to block the entire node, not just the specific attribute
6642
+ const node = this._getNode(id);
6643
+ // Check if the node is an Element and then find the closest parent that is an SVG
6644
+ if (node?.nodeName !== 'svg' && node instanceof Element) {
6645
+ const closestSVG = node.closest('svg');
6646
+ if (closestSVG) {
6647
+ return [this._rrweb.mirror.getId(closestSVG), closestSVG];
6648
+ }
6649
+ }
6650
+ return [id, node];
6651
+ };
6652
+ this._getNode = id => this._rrweb.mirror.getNode(id);
6653
+ this._numberOfChanges = data => {
6654
+ return (data.removes?.length ?? 0) + (data.attributes?.length ?? 0) + (data.texts?.length ?? 0) + (data.adds?.length ?? 0);
6655
+ };
6656
+ this.throttleMutations = event => {
6657
+ if (event.type !== INCREMENTAL_SNAPSHOT_EVENT_TYPE || event.data.source !== MUTATION_SOURCE_TYPE) {
6658
+ return event;
6659
+ }
6660
+ const data = event.data;
6661
+ const initialMutationCount = this._numberOfChanges(data);
6662
+ if (data.attributes) {
6663
+ // Most problematic mutations come from attrs where the style or minor properties are changed rapidly
6664
+ data.attributes = data.attributes.filter(attr => {
6665
+ const [nodeId] = this._getNodeOrRelevantParent(attr.id);
6666
+ const isRateLimited = this._rateLimiter.consumeRateLimit(nodeId);
6667
+ if (isRateLimited) {
6668
+ return false;
6669
+ }
6670
+ return attr;
6671
+ });
6672
+ }
6673
+ // Check if every part of the mutation is empty in which case there is nothing to do
6674
+ const mutationCount = this._numberOfChanges(data);
6675
+ if (mutationCount === 0 && initialMutationCount !== mutationCount) {
6676
+ // If we have modified the mutation count and the remaining count is 0, then we don't need the event.
6677
+ return;
6678
+ }
6679
+ return event;
6680
+ };
6681
+ const configuredBucketSize = this._options.bucketSize ?? 100;
6682
+ const effectiveBucketSize = Math.max(configuredBucketSize - 1, 1);
6683
+ this._rateLimiter = new BucketedRateLimiter({
6684
+ bucketSize: effectiveBucketSize,
6685
+ refillRate: this._options.refillRate ?? 10,
6686
+ refillInterval: 1000,
6687
+ // one second
6688
+ _onBucketRateLimited: this._onNodeRateLimited,
6689
+ _logger: logger$2
6690
+ });
6691
+ }
6692
+ reset() {
6693
+ this._loggedTracker = {};
6694
+ }
6695
+ stop() {
6696
+ this._rateLimiter.stop();
6697
+ this.reset();
6698
+ }
6699
+ }
6700
+
6701
+ function simpleHash(str) {
6702
+ let hash = 0;
6703
+ for (let i = 0; i < str.length; i++) {
6704
+ hash = (hash << 5) - hash + str.charCodeAt(i); // (hash * 31) + char code
6705
+ hash |= 0; // Convert to 32bit integer
6706
+ }
6707
+ return Math.abs(hash);
6708
+ }
6709
+ /*
6710
+ * receives percent as a number between 0 and 1
6711
+ */
6712
+ function sampleOnProperty(prop, percent) {
6713
+ return simpleHash(prop) % 100 < clampToRange(percent * 100, 0, 100, logger$2);
6714
+ }
6715
+
6716
+ const BASE_ENDPOINT = '/s/';
6717
+ const DEFAULT_CANVAS_QUALITY = 0.4;
6718
+ const DEFAULT_CANVAS_FPS = 4;
6719
+ const MAX_CANVAS_FPS = 12;
6720
+ const MAX_CANVAS_QUALITY = 1;
6721
+ const TWO_SECONDS = 2000;
6722
+ const ONE_KB = 1024;
6723
+ const ONE_MINUTE = 1000 * 60;
6724
+ const FIVE_MINUTES = ONE_MINUTE * 5;
6725
+ const RECORDING_IDLE_THRESHOLD_MS = FIVE_MINUTES;
6726
+ const RECORDING_MAX_EVENT_SIZE = ONE_KB * ONE_KB * 0.9; // ~1mb (with some wiggle room)
6727
+ const RECORDING_BUFFER_TIMEOUT = 2000; // 2 seconds
6728
+ const SESSION_RECORDING_BATCH_KEY = 'recordings';
6729
+ const LOGGER_PREFIX$1 = '[SessionRecording]';
6730
+ const logger = createLogger(LOGGER_PREFIX$1);
6731
+ const ACTIVE_SOURCES = [IncrementalSource.MouseMove, IncrementalSource.MouseInteraction, IncrementalSource.Scroll, IncrementalSource.ViewportResize, IncrementalSource.Input, IncrementalSource.TouchMove, IncrementalSource.MediaInteraction, IncrementalSource.Drag];
6732
+ const newQueuedEvent = rrwebMethod => ({
6733
+ rrwebMethod,
6734
+ enqueuedAt: Date.now(),
6735
+ attempt: 1
6736
+ });
6737
+ function getRRWebRecord() {
6738
+ try {
6739
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6740
+ const ext = globalThis.__PosthogExtensions__;
6741
+ if (ext && ext.rrweb && ext.rrweb.record) {
6742
+ return ext.rrweb.record;
6743
+ }
6744
+ } catch {
6745
+ // ignore
6746
+ }
6747
+ return record.record;
6748
+ }
6749
+ function gzipToString(data) {
6750
+ return strFromU8(gzipSync(strToU8(JSON.stringify(data))));
6751
+ }
6752
+ /**
6753
+ * rrweb's packer takes an event and returns a string or the reverse on `unpack`.
6754
+ * but we want to be able to inspect metadata during ingestion.
6755
+ * and don't want to compress the entire event,
6756
+ * so we have a custom packer that only compresses part of some events
6757
+ */
6758
+ function compressEvent(event) {
6759
+ try {
6760
+ if (event.type === EventType.FullSnapshot) {
6761
+ return {
6762
+ ...event,
6763
+ data: gzipToString(event.data),
6764
+ cv: '2024-10'
6765
+ };
6766
+ }
6767
+ if (event.type === EventType.IncrementalSnapshot && event.data.source === IncrementalSource.Mutation) {
6768
+ return {
6769
+ ...event,
6770
+ cv: '2024-10',
6771
+ data: {
6772
+ ...event.data,
6773
+ texts: gzipToString(event.data.texts),
6774
+ attributes: gzipToString(event.data.attributes),
6775
+ removes: gzipToString(event.data.removes),
6776
+ adds: gzipToString(event.data.adds)
6777
+ }
6778
+ };
6779
+ }
6780
+ if (event.type === EventType.IncrementalSnapshot && event.data.source === IncrementalSource.StyleSheetRule) {
6781
+ return {
6782
+ ...event,
6783
+ cv: '2024-10',
6784
+ data: {
6785
+ ...event.data,
6786
+ adds: event.data.adds ? gzipToString(event.data.adds) : undefined,
6787
+ removes: event.data.removes ? gzipToString(event.data.removes) : undefined
6788
+ }
6789
+ };
6790
+ }
6791
+ } catch (e) {
6792
+ logger.error('could not compress event - will use uncompressed event', e);
6793
+ }
6794
+ return event;
6795
+ }
6796
+ function isSessionIdleEvent(e) {
6797
+ return e.type === EventType.Custom && e.data.tag === 'sessionIdle';
6798
+ }
6799
+ /** When we put the recording into a paused state, we add a custom event.
6800
+ * However, in the paused state, events are dropped and never make it to the buffer,
6801
+ * so we need to manually let this one through */
6802
+ function isRecordingPausedEvent(e) {
6803
+ return e.type === EventType.Custom && e.data.tag === 'recording paused';
6804
+ }
6805
+ const SEVEN_MEGABYTES = 1024 * 1024 * 7 * 0.9; // ~7mb (with some wiggle room)
6806
+ // recursively splits large buffers into smaller ones
6807
+ // uses a pretty high size limit to avoid splitting too much
6808
+ function splitBuffer(buffer, sizeLimit = SEVEN_MEGABYTES) {
6809
+ if (buffer.size >= sizeLimit && buffer.data.length > 1) {
6810
+ const half = Math.floor(buffer.data.length / 2);
6811
+ const firstHalf = buffer.data.slice(0, half);
6812
+ const secondHalf = buffer.data.slice(half);
6813
+ return [splitBuffer({
6814
+ size: estimateSize(firstHalf),
6815
+ data: firstHalf,
6816
+ sessionId: buffer.sessionId,
6817
+ windowId: buffer.windowId
6818
+ }), splitBuffer({
6819
+ size: estimateSize(secondHalf),
6820
+ data: secondHalf,
6821
+ sessionId: buffer.sessionId,
6822
+ windowId: buffer.windowId
6823
+ })].flatMap(x => x);
6824
+ } else {
6825
+ return [buffer];
6826
+ }
6827
+ }
6828
+ class LazyLoadedSessionRecording {
6829
+ get sessionId() {
6830
+ return this._sessionId;
6831
+ }
6832
+ get _sessionManager() {
6833
+ if (!this._instance.sessionManager) {
6834
+ throw new Error(LOGGER_PREFIX$1 + ' must be started with a valid sessionManager.');
6835
+ }
6836
+ return this._instance.sessionManager;
6837
+ }
6838
+ get _sessionIdleThresholdMilliseconds() {
6839
+ return this._instance.config.session_recording?.session_idle_threshold_ms || RECORDING_IDLE_THRESHOLD_MS;
6840
+ }
6841
+ get _isSampled() {
6842
+ const currentValue = this._instance.get_property(SESSION_RECORDING_IS_SAMPLED);
6843
+ // originally we would store `true` or `false` or nothing,
6844
+ // but that would mean sometimes we would carry on recording on session id change
6845
+ return isBoolean(currentValue) ? currentValue : isString(currentValue) ? currentValue === this.sessionId : null;
6846
+ }
6847
+ get _sampleRate() {
6848
+ const rate = this._remoteConfig?.sampleRate;
6849
+ return isNumber(rate) ? rate : null;
6850
+ }
6851
+ get _minimumDuration() {
6852
+ const duration = this._remoteConfig?.minimumDurationMilliseconds;
6853
+ return isNumber(duration) ? duration : null;
6854
+ }
6855
+ constructor(_instance) {
6856
+ this._instance = _instance;
6857
+ this._endpoint = BASE_ENDPOINT;
6858
+ /**
6859
+ * Util to help developers working on this feature manually override
6860
+ */
6861
+ this._forceAllowLocalhostNetworkCapture = false;
6862
+ this._stopRrweb = undefined;
6863
+ this._lastActivityTimestamp = Date.now();
6864
+ /**
6865
+ * and a queue - that contains rrweb events that we want to send to rrweb, but rrweb wasn't able to accept them yet
6866
+ */
6867
+ this._queuedRRWebEvents = [];
6868
+ this._isIdle = 'unknown';
6869
+ // we need to be able to check the state of the event and url triggers separately
6870
+ // as we make some decisions based on them without referencing LinkedFlag etc
6871
+ this._triggerMatching = new PendingTriggerMatching();
6872
+ this._removePageViewCaptureHook = undefined;
6873
+ this._removeEventTriggerCaptureHook = undefined;
6874
+ this._statusMatcher = nullMatchSessionRecordingStatus;
6875
+ this._onSessionIdListener = undefined;
6876
+ this._onSessionIdleResetForcedListener = undefined;
6877
+ this._samplingSessionListener = undefined;
6878
+ this._forceIdleSessionIdListener = undefined;
6879
+ this._onSessionIdCallback = (sessionId, windowId, changeReason) => {
6880
+ if (changeReason) {
6881
+ this._tryAddCustomEvent('$session_id_change', {
6882
+ sessionId,
6883
+ windowId,
6884
+ changeReason
6885
+ });
6886
+ this._clearConditionalRecordingPersistence();
6887
+ if (!this._stopRrweb) {
6888
+ this.start('session_id_changed');
6889
+ }
6890
+ if (isNumber(this._sampleRate) && isNullish(this._samplingSessionListener)) {
6891
+ this._makeSamplingDecision(sessionId);
6892
+ }
6893
+ }
6894
+ };
6895
+ this._onBeforeUnload = () => {
6896
+ this._flushBuffer();
6897
+ };
6898
+ this._onOffline = () => {
6899
+ this._tryAddCustomEvent('browser offline', {});
6900
+ };
6901
+ this._onOnline = () => {
6902
+ this._tryAddCustomEvent('browser online', {});
6903
+ };
6904
+ this._onVisibilityChange = () => {
6905
+ if (document?.visibilityState) {
6906
+ const label = 'window ' + document.visibilityState;
6907
+ this._tryAddCustomEvent(label, {});
6908
+ }
6909
+ };
6910
+ // we know there's a sessionManager, so don't need to start without a session id
6911
+ const {
6912
+ sessionId,
6913
+ windowId
6914
+ } = this._sessionManager.checkAndGetSessionAndWindowId();
6915
+ this._sessionId = sessionId;
6916
+ this._windowId = windowId;
6917
+ this._linkedFlagMatching = new LinkedFlagMatching(this._instance);
6918
+ this._urlTriggerMatching = new URLTriggerMatching(this._instance);
6919
+ this._eventTriggerMatching = new EventTriggerMatching(this._instance);
6920
+ this._buffer = this._clearBuffer();
6921
+ if (this._sessionIdleThresholdMilliseconds >= this._sessionManager.sessionTimeoutMs) {
6922
+ logger.warn(`session_idle_threshold_ms (${this._sessionIdleThresholdMilliseconds}) is greater than the session timeout (${this._sessionManager.sessionTimeoutMs}). Session will never be detected as idle`);
6923
+ }
6924
+ }
6925
+ get _masking() {
6926
+ const masking_server_side = this._remoteConfig?.masking;
6927
+ const masking_client_side = {
6928
+ maskAllInputs: this._instance.config.session_recording?.maskAllInputs,
6929
+ maskTextSelector: this._instance.config.session_recording?.maskTextSelector,
6930
+ blockSelector: this._instance.config.session_recording?.blockSelector
6931
+ };
6932
+ const maskAllInputs = masking_client_side?.maskAllInputs ?? masking_server_side?.maskAllInputs;
6933
+ const maskTextSelector = masking_client_side?.maskTextSelector ?? masking_server_side?.maskTextSelector;
6934
+ const blockSelector = masking_client_side?.blockSelector ?? masking_server_side?.blockSelector;
6935
+ return !isUndefined(maskAllInputs) || !isUndefined(maskTextSelector) || !isUndefined(blockSelector) ? {
6936
+ maskAllInputs: maskAllInputs ?? true,
6937
+ maskTextSelector,
6938
+ blockSelector
6939
+ } : undefined;
6940
+ }
6941
+ get _canvasRecording() {
6942
+ const canvasRecording_client_side = this._instance.config.session_recording?.captureCanvas;
6943
+ const canvasRecording_server_side = this._remoteConfig?.canvasRecording;
6944
+ const enabled = canvasRecording_client_side?.recordCanvas ?? canvasRecording_server_side?.enabled ?? false;
6945
+ const fps = canvasRecording_client_side?.canvasFps ?? canvasRecording_server_side?.fps ?? DEFAULT_CANVAS_FPS;
6946
+ let quality = canvasRecording_client_side?.canvasQuality ?? canvasRecording_server_side?.quality ?? DEFAULT_CANVAS_QUALITY;
6947
+ if (typeof quality === 'string') {
6948
+ const parsed = parseFloat(quality);
6949
+ quality = isNaN(parsed) ? 0.4 : parsed;
6950
+ }
6951
+ return {
6952
+ enabled,
6953
+ fps: clampToRange(fps, 0, MAX_CANVAS_FPS, createLogger('canvas recording fps'), DEFAULT_CANVAS_FPS),
6954
+ quality: clampToRange(quality, 0, MAX_CANVAS_QUALITY, createLogger('canvas recording quality'), DEFAULT_CANVAS_QUALITY)
6955
+ };
6956
+ }
6957
+ get _isConsoleLogCaptureEnabled() {
6958
+ const enabled_server_side = !!this._remoteConfig?.consoleLogRecordingEnabled;
6959
+ const enabled_client_side = this._instance.config.enable_recording_console_log;
6960
+ return enabled_client_side ?? enabled_server_side;
6961
+ }
6962
+ // network payload capture config has three parts
6963
+ // each can be configured server side or client side
6964
+ get _networkPayloadCapture() {
6965
+ const networkPayloadCapture_server_side = this._remoteConfig?.networkPayloadCapture;
6966
+ const networkPayloadCapture_client_side = {
6967
+ recordHeaders: this._instance.config.session_recording?.recordHeaders,
6968
+ recordBody: this._instance.config.session_recording?.recordBody
6969
+ };
6970
+ const headersOptIn = networkPayloadCapture_client_side?.recordHeaders === true;
6971
+ const bodyOptIn = networkPayloadCapture_client_side?.recordBody === true;
6972
+ const clientPerformanceConfig = this._instance.config.capture_performance;
6973
+ const clientPerformanceOptIn = isObject(clientPerformanceConfig) ? !!clientPerformanceConfig.network_timing : !!clientPerformanceConfig;
6974
+ const serverAllowsHeaders = networkPayloadCapture_server_side?.recordHeaders ?? true;
6975
+ const serverAllowsBody = networkPayloadCapture_server_side?.recordBody ?? true;
6976
+ const capturePerfResponse = networkPayloadCapture_server_side?.capturePerformance;
6977
+ const serverAllowsPerformance = (() => {
6978
+ if (isObject(capturePerfResponse)) {
6979
+ return !!capturePerfResponse.network_timing;
6980
+ }
6981
+ return capturePerfResponse ?? true;
6982
+ })();
6983
+ const headersEnabled = headersOptIn && serverAllowsHeaders;
6984
+ const bodyEnabled = bodyOptIn && serverAllowsBody;
6985
+ const networkTimingEnabled = clientPerformanceOptIn && serverAllowsPerformance;
6986
+ if (!headersEnabled && !bodyEnabled && !networkTimingEnabled) {
6987
+ return undefined;
6988
+ }
6989
+ return {
6990
+ recordHeaders: headersEnabled,
6991
+ recordBody: bodyEnabled,
6992
+ recordPerformance: networkTimingEnabled,
6993
+ payloadHostDenyList: networkPayloadCapture_server_side?.payloadHostDenyList
6994
+ };
6995
+ }
6996
+ _gatherRRWebPlugins() {
6997
+ const plugins = [];
6998
+ if (this._isConsoleLogCaptureEnabled) {
6999
+ logger.info('Console log capture requested but console plugin is not bundled in this build yet.');
7000
+ }
7001
+ if (this._networkPayloadCapture) {
7002
+ const canRecordNetwork = !isLocalhost() || this._forceAllowLocalhostNetworkCapture;
7003
+ if (canRecordNetwork) {
7004
+ plugins.push(getRecordNetworkPlugin(buildNetworkRequestOptions(this._instance.config, this._networkPayloadCapture)));
7005
+ } else {
7006
+ logger.info('NetworkCapture not started because we are on localhost.');
7007
+ }
7008
+ }
7009
+ return plugins;
7010
+ }
7011
+ _maskUrl(url) {
7012
+ const userSessionRecordingOptions = this._instance.config.session_recording || {};
7013
+ if (userSessionRecordingOptions.maskNetworkRequestFn) {
7014
+ let networkRequest = {
7015
+ url
7016
+ };
7017
+ // TODO we should deprecate this and use the same function for this masking and the rrweb/network plugin
7018
+ // TODO or deprecate this and provide a new clearer name so this would be `maskURLPerformanceFn` or similar
7019
+ networkRequest = userSessionRecordingOptions.maskNetworkRequestFn(networkRequest);
7020
+ return networkRequest?.url;
7021
+ }
7022
+ return url;
7023
+ }
7024
+ _tryRRWebMethod(queuedRRWebEvent) {
7025
+ try {
7026
+ queuedRRWebEvent.rrwebMethod();
7027
+ return true;
7028
+ } catch (e) {
7029
+ // Sometimes a race can occur where the recorder is not fully started yet
7030
+ if (this._queuedRRWebEvents.length < 10) {
7031
+ this._queuedRRWebEvents.push({
7032
+ enqueuedAt: queuedRRWebEvent.enqueuedAt || Date.now(),
7033
+ attempt: queuedRRWebEvent.attempt + 1,
7034
+ rrwebMethod: queuedRRWebEvent.rrwebMethod
7035
+ });
7036
+ } else {
7037
+ logger.warn('could not emit queued rrweb event.', e, queuedRRWebEvent);
7038
+ }
7039
+ return false;
7040
+ }
7041
+ }
7042
+ _tryAddCustomEvent(tag, payload) {
7043
+ return this._tryRRWebMethod(newQueuedEvent(() => getRRWebRecord().addCustomEvent(tag, payload)));
7044
+ }
7045
+ _pageViewFallBack() {
7046
+ try {
7047
+ if (this._instance.config.capture_pageview || !win) {
7048
+ return;
7049
+ }
7050
+ // Strip hash parameters from URL since they often aren't helpful
7051
+ // Use URL constructor for proper parsing to handle edge cases
7052
+ // recording doesn't run in IE11, so we don't need compat here
7053
+ // eslint-disable-next-line compat/compat
7054
+ const url = new URL(win.location.href);
7055
+ const hrefWithoutHash = url.origin + url.pathname + url.search;
7056
+ const currentUrl = this._maskUrl(hrefWithoutHash);
7057
+ if (this._lastHref !== currentUrl) {
7058
+ this._lastHref = currentUrl;
7059
+ this._tryAddCustomEvent('$url_changed', {
7060
+ href: currentUrl
7061
+ });
7062
+ }
7063
+ } catch {
7064
+ // If URL processing fails, don't capture anything
7065
+ }
7066
+ }
7067
+ _processQueuedEvents() {
7068
+ if (this._queuedRRWebEvents.length) {
7069
+ // if rrweb isn't ready to accept events earlier, then we queued them up.
7070
+ // now that `emit` has been called rrweb should be ready to accept them.
7071
+ // so, before we process this event, we try our queued events _once_ each
7072
+ // we don't want to risk queuing more things and never exiting this loop!
7073
+ // if they fail here, they'll be pushed into a new queue
7074
+ // and tried on the next loop.
7075
+ // there is a risk of this queue growing in an uncontrolled manner.
7076
+ // so its length is limited elsewhere
7077
+ // for now this is to help us ensure we can capture events that happen
7078
+ // and try to identify more about when it is failing
7079
+ const itemsToProcess = [...this._queuedRRWebEvents];
7080
+ this._queuedRRWebEvents = [];
7081
+ itemsToProcess.forEach(queuedRRWebEvent => {
7082
+ if (Date.now() - queuedRRWebEvent.enqueuedAt <= TWO_SECONDS) {
7083
+ this._tryRRWebMethod(queuedRRWebEvent);
7084
+ }
7085
+ });
7086
+ }
7087
+ }
7088
+ _tryTakeFullSnapshot() {
7089
+ return this._tryRRWebMethod(newQueuedEvent(() => getRRWebRecord().takeFullSnapshot()));
7090
+ }
7091
+ get _fullSnapshotIntervalMillis() {
7092
+ if (this._triggerMatching.triggerStatus(this.sessionId) === TRIGGER_PENDING && !['sampled', 'active'].includes(this.status)) {
7093
+ return ONE_MINUTE;
7094
+ }
7095
+ return this._instance.config.session_recording?.full_snapshot_interval_millis ?? FIVE_MINUTES;
7096
+ }
7097
+ _scheduleFullSnapshot() {
7098
+ if (this._fullSnapshotTimer) {
7099
+ clearInterval(this._fullSnapshotTimer);
7100
+ }
7101
+ // we don't schedule snapshots while idle
7102
+ if (this._isIdle === true) {
7103
+ return;
7104
+ }
7105
+ const interval = this._fullSnapshotIntervalMillis;
7106
+ if (!interval) {
7107
+ return;
7108
+ }
7109
+ this._fullSnapshotTimer = setInterval(() => {
7110
+ this._tryTakeFullSnapshot();
7111
+ }, interval);
7112
+ }
7113
+ _pauseRecording() {
7114
+ // we check _urlBlocked not status, since more than one thing can affect status
7115
+ if (this._urlTriggerMatching.urlBlocked) {
7116
+ return;
7117
+ }
7118
+ // we can't flush the buffer here since someone might be starting on a blocked page.
7119
+ // and we need to be sure that we don't record that page,
7120
+ // so we might not get the below custom event, but events will report the paused status.
7121
+ // which will allow debugging of sessions that start on blocked pages
7122
+ this._urlTriggerMatching.urlBlocked = true;
7123
+ // Clear the snapshot timer since we don't want new snapshots while paused
7124
+ clearInterval(this._fullSnapshotTimer);
7125
+ logger.info('recording paused due to URL blocker');
7126
+ this._tryAddCustomEvent('recording paused', {
7127
+ reason: 'url blocker'
7128
+ });
7129
+ }
7130
+ _resumeRecording() {
7131
+ // we check _urlBlocked not status, since more than one thing can affect status
7132
+ if (!this._urlTriggerMatching.urlBlocked) {
7133
+ return;
7134
+ }
7135
+ this._urlTriggerMatching.urlBlocked = false;
7136
+ this._tryTakeFullSnapshot();
7137
+ this._scheduleFullSnapshot();
7138
+ this._tryAddCustomEvent('recording resumed', {
7139
+ reason: 'left blocked url'
7140
+ });
7141
+ logger.info('recording resumed');
7142
+ }
7143
+ _activateTrigger(triggerType) {
7144
+ if (this._triggerMatching.triggerStatus(this.sessionId) === TRIGGER_PENDING) {
7145
+ // status is stored separately for URL and event triggers
7146
+ this._instance?.persistence?.register({
7147
+ [triggerType === 'url' ? SESSION_RECORDING_URL_TRIGGER_ACTIVATED_SESSION : SESSION_RECORDING_EVENT_TRIGGER_ACTIVATED_SESSION]: this._sessionId
7148
+ });
7149
+ this._flushBuffer();
7150
+ this._reportStarted(triggerType + '_trigger_matched');
7151
+ }
7152
+ }
7153
+ get isStarted() {
7154
+ return !!this._stopRrweb;
7155
+ }
7156
+ get _remoteConfig() {
7157
+ const persistedConfig = this._instance.get_property(SESSION_RECORDING_REMOTE_CONFIG);
7158
+ if (!persistedConfig) {
7159
+ return undefined;
7160
+ }
7161
+ const parsedConfig = isObject(persistedConfig) ? persistedConfig : JSON.parse(persistedConfig);
7162
+ return parsedConfig;
7163
+ }
7164
+ start(startReason) {
7165
+ const config = this._remoteConfig;
7166
+ if (!config) {
7167
+ logger.info('remote config must be stored in persistence before recording can start');
7168
+ return;
7169
+ }
7170
+ // We want to ensure the sessionManager is reset if necessary on loading the recorder
7171
+ this._sessionManager.checkAndGetSessionAndWindowId();
7172
+ if (config?.endpoint) {
7173
+ this._endpoint = config?.endpoint;
7174
+ }
7175
+ if (config?.triggerMatchType === 'any') {
7176
+ this._statusMatcher = anyMatchSessionRecordingStatus;
7177
+ this._triggerMatching = new OrTriggerMatching([this._eventTriggerMatching, this._urlTriggerMatching]);
7178
+ } else {
7179
+ // either the setting is "ALL"
7180
+ // or we default to the most restrictive
7181
+ this._statusMatcher = allMatchSessionRecordingStatus;
7182
+ this._triggerMatching = new AndTriggerMatching([this._eventTriggerMatching, this._urlTriggerMatching]);
7183
+ }
7184
+ this._instance.registerForSession({
7185
+ $sdk_debug_replay_remote_trigger_matching_config: config?.triggerMatchType ?? null
7186
+ });
7187
+ this._urlTriggerMatching.onConfig(config);
7188
+ this._eventTriggerMatching.onConfig(config);
7189
+ this._removeEventTriggerCaptureHook?.();
7190
+ this._addEventTriggerListener();
7191
+ this._linkedFlagMatching.onConfig(config, (flag, variant) => {
7192
+ this._reportStarted('linked_flag_matched', {
7193
+ flag,
7194
+ variant
7195
+ });
7196
+ });
7197
+ this._makeSamplingDecision(this.sessionId);
7198
+ this._startRecorder();
7199
+ // calling addEventListener multiple times is safe and will not add duplicates
7200
+ addEventListener(win, 'beforeunload', this._onBeforeUnload);
7201
+ addEventListener(win, 'offline', this._onOffline);
7202
+ addEventListener(win, 'online', this._onOnline);
7203
+ addEventListener(win, 'visibilitychange', this._onVisibilityChange);
7204
+ if (!this._onSessionIdListener) {
7205
+ this._onSessionIdListener = this._sessionManager.onSessionId(this._onSessionIdCallback);
7206
+ }
7207
+ if (!this._onSessionIdleResetForcedListener) {
7208
+ this._onSessionIdleResetForcedListener = this._sessionManager.on('forcedIdleReset', () => {
7209
+ // a session was forced to reset due to idle timeout and lack of activity
7210
+ this._clearConditionalRecordingPersistence();
7211
+ this._isIdle = 'unknown';
7212
+ this.stop();
7213
+ // then we want a session id listener to restart the recording when a new session starts
7214
+ this._forceIdleSessionIdListener = this._sessionManager.onSessionId((sessionId, windowId, changeReason) => {
7215
+ // this should first unregister itself
7216
+ this._forceIdleSessionIdListener?.();
7217
+ this._forceIdleSessionIdListener = undefined;
7218
+ this._onSessionIdCallback(sessionId, windowId, changeReason);
7219
+ });
7220
+ });
7221
+ }
7222
+ if (isNullish(this._removePageViewCaptureHook)) {
7223
+ // :TRICKY: rrweb does not capture navigation within SPA-s, so hook into our $pageview events to get access to all events.
7224
+ // Dropping the initial event is fine (it's always captured by rrweb).
7225
+ this._removePageViewCaptureHook = this._instance.on('eventCaptured', event => {
7226
+ // If anything could go wrong here,
7227
+ // it has the potential to block the main loop,
7228
+ // so we catch all errors.
7229
+ try {
7230
+ if (event.event === '$pageview') {
7231
+ const href = event?.properties.$current_url ? this._maskUrl(event?.properties.$current_url) : '';
7232
+ if (!href) {
7233
+ return;
7234
+ }
7235
+ this._tryAddCustomEvent('$pageview', {
7236
+ href
7237
+ });
7238
+ }
7239
+ } catch (e) {
7240
+ logger.error('Could not add $pageview to rrweb session', e);
7241
+ }
7242
+ });
7243
+ }
7244
+ if (this.status === ACTIVE) {
7245
+ this._reportStarted(startReason || 'recording_initialized');
7246
+ }
7247
+ }
7248
+ stop() {
7249
+ win?.removeEventListener('beforeunload', this._onBeforeUnload);
7250
+ win?.removeEventListener('offline', this._onOffline);
7251
+ win?.removeEventListener('online', this._onOnline);
7252
+ win?.removeEventListener('visibilitychange', this._onVisibilityChange);
7253
+ this._clearBuffer();
7254
+ clearInterval(this._fullSnapshotTimer);
7255
+ this._clearFlushBufferTimer();
7256
+ this._removePageViewCaptureHook?.();
7257
+ this._removePageViewCaptureHook = undefined;
7258
+ this._removeEventTriggerCaptureHook?.();
7259
+ this._removeEventTriggerCaptureHook = undefined;
7260
+ this._onSessionIdListener?.();
7261
+ this._onSessionIdListener = undefined;
7262
+ this._onSessionIdleResetForcedListener?.();
7263
+ this._onSessionIdleResetForcedListener = undefined;
7264
+ this._samplingSessionListener?.();
7265
+ this._samplingSessionListener = undefined;
7266
+ this._forceIdleSessionIdListener?.();
7267
+ this._forceIdleSessionIdListener = undefined;
7268
+ this._eventTriggerMatching.stop();
7269
+ this._urlTriggerMatching.stop();
7270
+ this._linkedFlagMatching.stop();
7271
+ this._mutationThrottler?.stop();
7272
+ // Clear any queued rrweb events to prevent memory leaks from closures
7273
+ this._queuedRRWebEvents = [];
7274
+ this._stopRrweb?.();
7275
+ this._stopRrweb = undefined;
7276
+ logger.info('stopped');
7277
+ }
7278
+ onRRwebEmit(rawEvent) {
7279
+ this._processQueuedEvents();
7280
+ if (!rawEvent || !isObject(rawEvent)) {
7281
+ return;
7282
+ }
7283
+ if (rawEvent.type === EventType.Meta) {
7284
+ const href = this._maskUrl(rawEvent.data.href);
7285
+ this._lastHref = href;
7286
+ if (!href) {
7287
+ return;
7288
+ }
7289
+ rawEvent.data.href = href;
7290
+ } else {
7291
+ this._pageViewFallBack();
7292
+ }
7293
+ // Check if the URL matches any trigger patterns
7294
+ this._urlTriggerMatching.checkUrlTriggerConditions(() => this._pauseRecording(), () => this._resumeRecording(), triggerType => this._activateTrigger(triggerType));
7295
+ // always have to check if the URL is blocked really early,
7296
+ // or you risk getting stuck in a loop
7297
+ if (this._urlTriggerMatching.urlBlocked && !isRecordingPausedEvent(rawEvent)) {
7298
+ return;
7299
+ }
7300
+ // we're processing a full snapshot, so we should reset the timer
7301
+ if (rawEvent.type === EventType.FullSnapshot) {
7302
+ this._scheduleFullSnapshot();
7303
+ // Full snapshots reset rrweb's node IDs, so clear any logged node tracking
7304
+ this._mutationThrottler?.reset();
7305
+ }
7306
+ // Clear the buffer if waiting for a trigger and only keep data from after the current full snapshot
7307
+ // we always start trigger pending so need to wait for flags before we know if we're really pending
7308
+ if (rawEvent.type === EventType.FullSnapshot && this._triggerMatching.triggerStatus(this.sessionId) === TRIGGER_PENDING) {
7309
+ this._clearBufferBeforeMostRecentMeta();
7310
+ }
7311
+ const throttledEvent = this._mutationThrottler ? this._mutationThrottler.throttleMutations(rawEvent) : rawEvent;
7312
+ if (!throttledEvent) {
7313
+ return;
7314
+ }
7315
+ // TODO: Re-add ensureMaxMessageSize once we are confident in it
7316
+ const event = truncateLargeConsoleLogs(throttledEvent);
7317
+ this._updateWindowAndSessionIds(event);
7318
+ // When in an idle state we keep recording but don't capture the events,
7319
+ // we don't want to return early if idle is 'unknown'
7320
+ if (this._isIdle === true && !isSessionIdleEvent(event)) {
7321
+ return;
7322
+ }
7323
+ if (isSessionIdleEvent(event)) {
7324
+ // session idle events have a timestamp when rrweb sees them
7325
+ // which can artificially lengthen a session
7326
+ // we know when we detected it based on the payload and can correct the timestamp
7327
+ const payload = event.data.payload;
7328
+ if (payload) {
7329
+ const lastActivity = payload.lastActivityTimestamp;
7330
+ const threshold = payload.threshold;
7331
+ event.timestamp = lastActivity + threshold;
7332
+ }
7333
+ }
7334
+ const eventToSend = this._instance.config.session_recording?.compress_events ?? true ? compressEvent(event) : event;
7335
+ const size = estimateSize(eventToSend);
7336
+ const properties = {
7337
+ $snapshot_bytes: size,
7338
+ $snapshot_data: eventToSend,
7339
+ $session_id: this._sessionId,
7340
+ $window_id: this._windowId
7341
+ };
7342
+ if (this.status === DISABLED) {
7343
+ this._clearBuffer();
7344
+ return;
7345
+ }
7346
+ this._captureSnapshotBuffered(properties);
7347
+ }
7348
+ get status() {
7349
+ return this._statusMatcher({
7350
+ // can't get here without recording being enabled...
7351
+ receivedFlags: true,
7352
+ isRecordingEnabled: true,
7353
+ // things that do still vary
7354
+ isSampled: this._isSampled,
7355
+ urlTriggerMatching: this._urlTriggerMatching,
7356
+ eventTriggerMatching: this._eventTriggerMatching,
7357
+ linkedFlagMatching: this._linkedFlagMatching,
7358
+ sessionId: this.sessionId
7359
+ });
7360
+ }
7361
+ log(message, level = 'log') {
7362
+ this._instance.sessionRecording?.onRRwebEmit({
7363
+ type: 6,
7364
+ data: {
7365
+ plugin: 'rrweb/console@1',
7366
+ payload: {
7367
+ level,
7368
+ trace: [],
7369
+ // Even though it is a string, we stringify it as that's what rrweb expects
7370
+ payload: [JSON.stringify(message)]
7371
+ }
7372
+ },
7373
+ timestamp: Date.now()
7374
+ });
7375
+ }
7376
+ overrideLinkedFlag() {
7377
+ this._linkedFlagMatching.linkedFlagSeen = true;
7378
+ this._tryTakeFullSnapshot();
7379
+ this._reportStarted('linked_flag_overridden');
7380
+ }
7381
+ /**
7382
+ * this ignores the sampling config and (if other conditions are met) causes capture to start
7383
+ *
7384
+ * It is not usual to call this directly,
7385
+ * instead call `posthog.startSessionRecording({sampling: true})`
7386
+ * */
7387
+ overrideSampling() {
7388
+ this._instance.persistence?.register({
7389
+ // short-circuits the `makeSamplingDecision` function in the session recording module
7390
+ [SESSION_RECORDING_IS_SAMPLED]: this.sessionId
7391
+ });
7392
+ this._tryTakeFullSnapshot();
7393
+ this._reportStarted('sampling_overridden');
7394
+ }
7395
+ /**
7396
+ * this ignores the URL/Event trigger config and (if other conditions are met) causes capture to start
7397
+ *
7398
+ * It is not usual to call this directly,
7399
+ * instead call `posthog.startSessionRecording({trigger: 'url' | 'event'})`
7400
+ * */
7401
+ overrideTrigger(triggerType) {
7402
+ this._activateTrigger(triggerType);
7403
+ }
7404
+ _clearFlushBufferTimer() {
7405
+ if (this._flushBufferTimer) {
7406
+ clearTimeout(this._flushBufferTimer);
7407
+ this._flushBufferTimer = undefined;
7408
+ }
7409
+ }
7410
+ _flushBuffer() {
7411
+ this._clearFlushBufferTimer();
7412
+ const minimumDuration = this._minimumDuration;
7413
+ const sessionDuration = this._sessionDuration;
7414
+ // if we have old data in the buffer but the session has rotated, then the
7415
+ // session duration might be negative. In that case we want to flush the buffer
7416
+ const isPositiveSessionDuration = isNumber(sessionDuration) && sessionDuration >= 0;
7417
+ const isBelowMinimumDuration = isNumber(minimumDuration) && isPositiveSessionDuration && sessionDuration < minimumDuration;
7418
+ if (this.status === BUFFERING || this.status === PAUSED || this.status === DISABLED || isBelowMinimumDuration) {
7419
+ this._flushBufferTimer = setTimeout(() => {
7420
+ this._flushBuffer();
7421
+ }, RECORDING_BUFFER_TIMEOUT);
7422
+ return this._buffer;
7423
+ }
7424
+ if (this._buffer.data.length > 0) {
7425
+ const snapshotEvents = splitBuffer(this._buffer);
7426
+ snapshotEvents.forEach(snapshotBuffer => {
7427
+ this._captureSnapshot({
7428
+ $snapshot_bytes: snapshotBuffer.size,
7429
+ $snapshot_data: snapshotBuffer.data,
7430
+ $session_id: snapshotBuffer.sessionId,
7431
+ $window_id: snapshotBuffer.windowId,
7432
+ $lib: 'web',
7433
+ $lib_version: Config.LIB_VERSION
7434
+ });
7435
+ });
7436
+ }
7437
+ // buffer is empty, we clear it in case the session id has changed
7438
+ return this._clearBuffer();
7439
+ }
7440
+ _captureSnapshotBuffered(properties) {
7441
+ const additionalBytes = 2 + (this._buffer?.data.length || 0); // 2 bytes for the array brackets and 1 byte for each comma
7442
+ if (!this._isIdle && (
7443
+ // we never want to flush when idle
7444
+ this._buffer.size + properties.$snapshot_bytes + additionalBytes > RECORDING_MAX_EVENT_SIZE || this._buffer.sessionId !== this._sessionId)) {
7445
+ this._buffer = this._flushBuffer();
7446
+ }
7447
+ this._buffer.size += properties.$snapshot_bytes;
7448
+ this._buffer.data.push(properties.$snapshot_data);
7449
+ if (!this._flushBufferTimer && !this._isIdle) {
7450
+ this._flushBufferTimer = setTimeout(() => {
7451
+ this._flushBuffer();
7452
+ }, RECORDING_BUFFER_TIMEOUT);
7453
+ }
7454
+ }
7455
+ _captureSnapshot(properties) {
7456
+ // :TRICKY: Make sure we batch these requests, use a custom endpoint and don't truncate the strings.
7457
+ this._instance.capture('$snapshot', properties, {
7458
+ _url: this._instance.requestRouter.endpointFor('api', this._endpoint),
7459
+ _noTruncate: true,
7460
+ _batchKey: SESSION_RECORDING_BATCH_KEY,
7461
+ skip_client_rate_limiting: true
7462
+ });
7463
+ }
7464
+ _snapshotUrl() {
7465
+ const host = this._instance.config.host || '';
7466
+ try {
7467
+ // eslint-disable-next-line compat/compat
7468
+ return new URL(this._endpoint, host).href;
7469
+ } catch {
7470
+ const normalizedHost = host.endsWith('/') ? host.slice(0, -1) : host;
7471
+ const normalizedEndpoint = this._endpoint.startsWith('/') ? this._endpoint.slice(1) : this._endpoint;
7472
+ return `${normalizedHost}/${normalizedEndpoint}`;
7473
+ }
7474
+ }
7475
+ get _sessionDuration() {
7476
+ const mostRecentSnapshot = this._buffer?.data[this._buffer?.data.length - 1];
7477
+ const {
7478
+ sessionStartTimestamp
7479
+ } = this._sessionManager.checkAndGetSessionAndWindowId(true);
7480
+ return mostRecentSnapshot ? mostRecentSnapshot.timestamp - sessionStartTimestamp : null;
7481
+ }
7482
+ _clearBufferBeforeMostRecentMeta() {
7483
+ if (!this._buffer || this._buffer.data.length === 0) {
7484
+ return this._clearBuffer();
7485
+ }
7486
+ // Find the last meta event index by iterating backwards
7487
+ let lastMetaIndex = -1;
7488
+ for (let i = this._buffer.data.length - 1; i >= 0; i--) {
7489
+ if (this._buffer.data[i].type === EventType.Meta) {
7490
+ lastMetaIndex = i;
7491
+ break;
7492
+ }
7493
+ }
7494
+ if (lastMetaIndex >= 0) {
7495
+ this._buffer.data = this._buffer.data.slice(lastMetaIndex);
7496
+ this._buffer.size = this._buffer.data.reduce((acc, curr) => acc + estimateSize(curr), 0);
7497
+ return this._buffer;
7498
+ } else {
7499
+ return this._clearBuffer();
7500
+ }
7501
+ }
7502
+ _clearBuffer() {
7503
+ this._buffer = {
7504
+ size: 0,
7505
+ data: [],
7506
+ sessionId: this._sessionId,
7507
+ windowId: this._windowId
7508
+ };
7509
+ return this._buffer;
7510
+ }
7511
+ _reportStarted(startReason, tagPayload) {
7512
+ this._instance.registerForSession({
7513
+ $session_recording_start_reason: startReason
7514
+ });
7515
+ logger.info(startReason.replace('_', ' '), tagPayload);
7516
+ if (!includes(['recording_initialized', 'session_id_changed'], startReason)) {
7517
+ this._tryAddCustomEvent(startReason, tagPayload);
7518
+ }
7519
+ }
7520
+ _isInteractiveEvent(event) {
7521
+ return event.type === INCREMENTAL_SNAPSHOT_EVENT_TYPE && ACTIVE_SOURCES.indexOf(event.data?.source) !== -1;
7522
+ }
7523
+ _updateWindowAndSessionIds(event) {
7524
+ // Some recording events are triggered by non-user events (e.g. "X minutes ago" text updating on the screen).
7525
+ // We don't want to extend the session or trigger a new session in these cases. These events are designated by event
7526
+ // type -> incremental update, and source -> mutation.
7527
+ const isUserInteraction = this._isInteractiveEvent(event);
7528
+ if (!isUserInteraction && !this._isIdle) {
7529
+ // We check if the lastActivityTimestamp is old enough to go idle
7530
+ const timeSinceLastActivity = event.timestamp - this._lastActivityTimestamp;
7531
+ if (timeSinceLastActivity > this._sessionIdleThresholdMilliseconds) {
7532
+ // we mark as idle right away,
7533
+ // or else we get multiple idle events
7534
+ // if there are lots of non-user activity events being emitted
7535
+ this._isIdle = true;
7536
+ // don't take full snapshots while idle
7537
+ clearInterval(this._fullSnapshotTimer);
7538
+ this._tryAddCustomEvent('sessionIdle', {
7539
+ eventTimestamp: event.timestamp,
7540
+ lastActivityTimestamp: this._lastActivityTimestamp,
7541
+ threshold: this._sessionIdleThresholdMilliseconds,
7542
+ bufferLength: this._buffer.data.length,
7543
+ bufferSize: this._buffer.size
7544
+ });
7545
+ // proactively flush the buffer in case the session is idle for a long time
7546
+ this._flushBuffer();
7547
+ }
7548
+ }
7549
+ let returningFromIdle = false;
7550
+ if (isUserInteraction) {
7551
+ this._lastActivityTimestamp = event.timestamp;
7552
+ if (this._isIdle) {
7553
+ const idleWasUnknown = this._isIdle === 'unknown';
7554
+ // Remove the idle state
7555
+ this._isIdle = false;
7556
+ // if the idle state was unknown, we don't want to add an event, since we're just in bootup
7557
+ // whereas if it was true, we know we've been idle for a while, and we can mark ourselves as returning from idle
7558
+ if (!idleWasUnknown) {
7559
+ this._tryAddCustomEvent('sessionNoLongerIdle', {
7560
+ reason: 'user activity',
7561
+ type: event.type
7562
+ });
7563
+ returningFromIdle = true;
7564
+ }
7565
+ }
7566
+ }
7567
+ if (this._isIdle) {
7568
+ return;
7569
+ }
7570
+ // We only want to extend the session if it is an interactive event.
7571
+ const {
7572
+ windowId,
7573
+ sessionId
7574
+ } = this._sessionManager.checkAndGetSessionAndWindowId(!isUserInteraction, event.timestamp);
7575
+ const sessionIdChanged = this._sessionId !== sessionId;
7576
+ const windowIdChanged = this._windowId !== windowId;
7577
+ this._windowId = windowId;
7578
+ this._sessionId = sessionId;
7579
+ if (sessionIdChanged || windowIdChanged) {
7580
+ this.stop();
7581
+ this.start('session_id_changed');
7582
+ } else if (returningFromIdle) {
7583
+ this._scheduleFullSnapshot();
7584
+ }
7585
+ }
7586
+ _clearConditionalRecordingPersistence() {
7587
+ this._instance?.persistence?.unregister(SESSION_RECORDING_EVENT_TRIGGER_ACTIVATED_SESSION);
7588
+ this._instance?.persistence?.unregister(SESSION_RECORDING_URL_TRIGGER_ACTIVATED_SESSION);
7589
+ this._instance?.persistence?.unregister(SESSION_RECORDING_IS_SAMPLED);
7590
+ }
7591
+ _makeSamplingDecision(sessionId) {
7592
+ const sessionIdChanged = this._sessionId !== sessionId;
7593
+ // capture the current sample rate
7594
+ // because it is re-used multiple times
7595
+ // and the bundler won't minimize any of the references
7596
+ const currentSampleRate = this._sampleRate;
7597
+ if (!isNumber(currentSampleRate)) {
7598
+ this._instance.persistence?.unregister(SESSION_RECORDING_IS_SAMPLED);
7599
+ return;
7600
+ }
7601
+ const storedIsSampled = this._isSampled;
7602
+ /**
7603
+ * if we get this far, then we should make a sampling decision.
7604
+ * When the session id changes or there is no stored sampling decision for this session id
7605
+ * then we should make a new decision.
7606
+ *
7607
+ * Otherwise, we should use the stored decision.
7608
+ */
7609
+ const makeDecision = sessionIdChanged || !isBoolean(storedIsSampled);
7610
+ const shouldSample = makeDecision ? sampleOnProperty(sessionId, currentSampleRate) : storedIsSampled;
7611
+ if (makeDecision) {
7612
+ if (shouldSample) {
7613
+ this._reportStarted(SAMPLED);
7614
+ } else {
7615
+ logger.warn(`Sample rate (${currentSampleRate}) has determined that this sessionId (${sessionId}) will not be sent to the server.`);
7616
+ }
7617
+ this._tryAddCustomEvent('samplingDecisionMade', {
7618
+ sampleRate: currentSampleRate,
7619
+ isSampled: shouldSample
7620
+ });
7621
+ }
7622
+ this._instance.persistence?.register({
7623
+ [SESSION_RECORDING_IS_SAMPLED]: shouldSample ? sessionId : false
7624
+ });
7625
+ }
7626
+ _addEventTriggerListener() {
7627
+ if (this._eventTriggerMatching._eventTriggers.length === 0 || !isNullish(this._removeEventTriggerCaptureHook)) {
7628
+ return;
7629
+ }
7630
+ this._removeEventTriggerCaptureHook = this._instance.on('eventCaptured', event => {
7631
+ // If anything could go wrong here, it has the potential to block the main loop,
7632
+ // so we catch all errors.
7633
+ try {
7634
+ if (this._eventTriggerMatching._eventTriggers.includes(event.event)) {
7635
+ this._activateTrigger('event');
7636
+ }
7637
+ } catch (e) {
7638
+ logger.error('Could not activate event trigger', e);
7639
+ }
7640
+ });
7641
+ }
7642
+ get sdkDebugProperties() {
7643
+ const {
7644
+ sessionStartTimestamp
7645
+ } = this._sessionManager.checkAndGetSessionAndWindowId(true);
7646
+ return {
7647
+ $recording_status: this.status,
7648
+ $sdk_debug_replay_internal_buffer_length: this._buffer.data.length,
7649
+ $sdk_debug_replay_internal_buffer_size: this._buffer.size,
7650
+ $sdk_debug_current_session_duration: this._sessionDuration,
7651
+ $sdk_debug_session_start: sessionStartTimestamp
7652
+ };
7653
+ }
7654
+ _startRecorder() {
7655
+ if (this._stopRrweb) {
7656
+ return;
7657
+ }
7658
+ // rrweb config info: https://github.com/rrweb-io/rrweb/blob/7d5d0033258d6c29599fb08412202d9a2c7b9413/src/record/index.ts#L28
7659
+ const sessionRecordingOptions = {
7660
+ // a limited set of the rrweb config options that we expose to our users.
7661
+ // see https://github.com/rrweb-io/rrweb/blob/master/guide.md
7662
+ blockClass: 'ph-no-capture',
7663
+ blockSelector: undefined,
7664
+ ignoreClass: 'ph-ignore-input',
7665
+ maskTextClass: 'ph-mask',
7666
+ maskTextSelector: undefined,
7667
+ maskTextFn: undefined,
7668
+ maskAllInputs: true,
7669
+ maskInputOptions: {
7670
+ password: true
7671
+ },
7672
+ maskInputFn: undefined,
7673
+ slimDOMOptions: {},
7674
+ collectFonts: false,
7675
+ inlineStylesheet: true,
7676
+ recordCrossOriginIframes: false
7677
+ };
7678
+ // only allows user to set our allowlisted options
7679
+ const userSessionRecordingOptions = this._instance.config.session_recording;
7680
+ for (const [key, value] of Object.entries(userSessionRecordingOptions || {})) {
7681
+ if (key in sessionRecordingOptions) {
7682
+ if (key === 'maskInputOptions') {
7683
+ // ensure password config is set if not included
7684
+ sessionRecordingOptions.maskInputOptions = {
7685
+ password: true,
7686
+ ...value
7687
+ };
7688
+ } else {
7689
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
7690
+ // @ts-ignore
7691
+ sessionRecordingOptions[key] = value;
7692
+ }
7693
+ }
7694
+ }
7695
+ if (this._canvasRecording && this._canvasRecording.enabled) {
7696
+ sessionRecordingOptions.recordCanvas = true;
7697
+ sessionRecordingOptions.sampling = {
7698
+ canvas: this._canvasRecording.fps
7699
+ };
7700
+ sessionRecordingOptions.dataURLOptions = {
7701
+ type: 'image/webp',
7702
+ quality: this._canvasRecording.quality
7703
+ };
7704
+ }
7705
+ if (this._masking) {
7706
+ sessionRecordingOptions.maskAllInputs = this._masking.maskAllInputs ?? true;
7707
+ sessionRecordingOptions.maskTextSelector = this._masking.maskTextSelector ?? undefined;
7708
+ sessionRecordingOptions.blockSelector = this._masking.blockSelector ?? undefined;
7709
+ }
7710
+ const rrwebRecord = getRRWebRecord();
7711
+ if (!rrwebRecord) {
7712
+ logger.error('_startRecorder was called but rrwebRecord is not available. This indicates something has gone wrong.');
7713
+ return;
7714
+ }
7715
+ this._mutationThrottler = this._mutationThrottler ?? new MutationThrottler(rrwebRecord, {
7716
+ refillRate: this._instance.config.session_recording?.__mutationThrottlerRefillRate,
7717
+ bucketSize: this._instance.config.session_recording?.__mutationThrottlerBucketSize,
7718
+ onBlockedNode: (id, node) => {
7719
+ const message = `Too many mutations on node '${id}'. Rate limiting. This could be due to SVG animations or something similar`;
7720
+ logger.info(message, {
7721
+ node: node
7722
+ });
7723
+ this.log(LOGGER_PREFIX$1 + ' ' + message, 'warn');
7724
+ }
7725
+ });
7726
+ const activePlugins = this._gatherRRWebPlugins();
7727
+ this._stopRrweb = rrwebRecord({
7728
+ emit: event => {
7729
+ this.onRRwebEmit(event);
7730
+ },
7731
+ plugins: activePlugins,
7732
+ ...sessionRecordingOptions
7733
+ });
7734
+ // We reset the last activity timestamp, resetting the idle timer
7735
+ this._lastActivityTimestamp = Date.now();
7736
+ // stay unknown if we're not sure if we're idle or not
7737
+ this._isIdle = isBoolean(this._isIdle) ? this._isIdle : 'unknown';
7738
+ this.tryAddCustomEvent('$remote_config_received', this._remoteConfig);
7739
+ this._tryAddCustomEvent('$session_options', {
7740
+ sessionRecordingOptions,
7741
+ activePlugins: activePlugins.map(p => p?.name)
7742
+ });
7743
+ this._tryAddCustomEvent('$posthog_config', {
7744
+ config: this._instance.config
7745
+ });
7746
+ }
7747
+ tryAddCustomEvent(tag, payload) {
7748
+ return this._tryAddCustomEvent(tag, payload);
7749
+ }
7750
+ }
7751
+
7752
+ const LOGGER_PREFIX = '[SessionRecording]';
7753
+ const log = {
7754
+ info: (...args) => logger$3.info(LOGGER_PREFIX, ...args),
7755
+ warn: (...args) => logger$3.warn(LOGGER_PREFIX, ...args),
7756
+ error: (...args) => logger$3.error(LOGGER_PREFIX, ...args)
7757
+ };
7758
+ class SessionRecording {
7759
+ get started() {
7760
+ return !!this._lazyLoadedSessionRecording?.isStarted;
7761
+ }
7762
+ /**
7763
+ * defaults to buffering mode until a flags response is received
7764
+ * once a flags response is received status can be disabled, active or sampled
7765
+ */
7766
+ get status() {
7767
+ if (this._lazyLoadedSessionRecording) {
7768
+ return this._lazyLoadedSessionRecording.status;
7769
+ }
7770
+ if (this._receivedFlags && !this._isRecordingEnabled) {
7771
+ return DISABLED;
7772
+ }
7773
+ return LAZY_LOADING;
7774
+ }
7775
+ constructor(_instance) {
7776
+ this._instance = _instance;
7777
+ this._forceAllowLocalhostNetworkCapture = false;
7778
+ this._receivedFlags = false;
7779
+ this._serverRecordingEnabled = false;
7780
+ this._persistFlagsOnSessionListener = undefined;
7781
+ if (!this._instance.sessionManager) {
7782
+ log.error('started without valid sessionManager');
7783
+ throw new Error(LOGGER_PREFIX + ' started without valid sessionManager. This is a bug.');
7784
+ }
7785
+ if (this._instance.config.cookieless_mode === 'always') {
7786
+ throw new Error(LOGGER_PREFIX + ' cannot be used with cookieless_mode="always"');
7787
+ }
7788
+ }
7789
+ get _isRecordingEnabled() {
7790
+ const enabled_server_side = !!this._instance.get_property(SESSION_RECORDING_REMOTE_CONFIG)?.enabled;
7791
+ const enabled_client_side = !this._instance.config.disable_session_recording;
7792
+ const isDisabled = this._instance.config.disable_session_recording || this._instance.consent?.isOptedOut?.();
7793
+ return win && enabled_server_side && enabled_client_side && !isDisabled;
7794
+ }
7795
+ startIfEnabledOrStop(startReason) {
7796
+ if (this._isRecordingEnabled && this._lazyLoadedSessionRecording?.isStarted) {
7797
+ return;
7798
+ }
7799
+ const canRunReplay = !isUndefined(Object.assign) && !isUndefined(Array.from);
7800
+ if (this._isRecordingEnabled && canRunReplay) {
7801
+ this._lazyLoadAndStart(startReason);
7802
+ log.info('starting');
7803
+ } else {
7804
+ this.stopRecording();
7805
+ }
7806
+ }
7807
+ /**
7808
+ * session recording waits until it receives remote config before loading the script
7809
+ * this is to ensure we can control the script name remotely
7810
+ * and because we wait until we have local and remote config to determine if we should start at all
7811
+ * if start is called and there is no remote config then we wait until there is
7812
+ */
7813
+ _lazyLoadAndStart(startReason) {
7814
+ // by checking `_isRecordingEnabled` here we know that
7815
+ // we have stored remote config and client config to read
7816
+ // replay waits for both local and remote config before starting
7817
+ if (!this._isRecordingEnabled) {
7818
+ return;
7819
+ }
7820
+ this._onScriptLoaded(startReason);
7821
+ }
7822
+ stopRecording() {
7823
+ this._persistFlagsOnSessionListener?.();
7824
+ this._persistFlagsOnSessionListener = undefined;
7825
+ this._lazyLoadedSessionRecording?.stop();
7826
+ }
7827
+ _resetSampling() {
7828
+ this._instance.persistence?.unregister(SESSION_RECORDING_IS_SAMPLED);
7829
+ }
7830
+ _persistRemoteConfig(response) {
7831
+ if (this._instance.persistence) {
7832
+ const persistence = this._instance.persistence;
7833
+ const persistResponse = () => {
7834
+ const sessionRecordingConfigResponse = response.sessionRecording === false ? undefined : response.sessionRecording;
7835
+ const receivedSampleRate = sessionRecordingConfigResponse?.sampleRate;
7836
+ const parsedSampleRate = isNullish(receivedSampleRate) ? null : parseFloat(receivedSampleRate);
7837
+ if (isNullish(parsedSampleRate)) {
7838
+ this._resetSampling();
7839
+ }
7840
+ const receivedMinimumDuration = sessionRecordingConfigResponse?.minimumDurationMilliseconds;
7841
+ persistence.register({
7842
+ [SESSION_RECORDING_REMOTE_CONFIG]: {
7843
+ enabled: !!sessionRecordingConfigResponse,
7844
+ ...sessionRecordingConfigResponse,
7845
+ networkPayloadCapture: {
7846
+ capturePerformance: response.capturePerformance,
7847
+ ...sessionRecordingConfigResponse?.networkPayloadCapture
7848
+ },
7849
+ canvasRecording: {
7850
+ enabled: sessionRecordingConfigResponse?.recordCanvas,
7851
+ fps: sessionRecordingConfigResponse?.canvasFps,
7852
+ quality: sessionRecordingConfigResponse?.canvasQuality
7853
+ },
7854
+ sampleRate: parsedSampleRate,
7855
+ minimumDurationMilliseconds: isUndefined(receivedMinimumDuration) ? null : receivedMinimumDuration,
7856
+ endpoint: sessionRecordingConfigResponse?.endpoint,
7857
+ triggerMatchType: sessionRecordingConfigResponse?.triggerMatchType,
7858
+ masking: sessionRecordingConfigResponse?.masking,
7859
+ urlTriggers: sessionRecordingConfigResponse?.urlTriggers
7860
+ }
7861
+ });
7862
+ };
7863
+ persistResponse();
7864
+ // in case we see multiple flags responses, we should only use the response from the most recent one
7865
+ this._persistFlagsOnSessionListener?.();
7866
+ // we 100% know there is a session manager by this point
7867
+ this._persistFlagsOnSessionListener = this._instance.sessionManager?.onSessionId(persistResponse);
7868
+ }
7869
+ }
7870
+ _clearRemoteConfig() {
7871
+ this._instance.persistence?.unregister(SESSION_RECORDING_REMOTE_CONFIG);
7872
+ this._resetSampling();
7873
+ }
7874
+ onRemoteConfig(response) {
7875
+ if (!('sessionRecording' in response)) {
7876
+ // if sessionRecording is not in the response, we do nothing
7877
+ log.info('skipping remote config with no sessionRecording', response);
7878
+ return;
7879
+ }
7880
+ this._receivedFlags = true;
7881
+ if (response.sessionRecording === false) {
7882
+ this._serverRecordingEnabled = false;
7883
+ this._clearRemoteConfig();
7884
+ this.stopRecording();
7885
+ return;
7886
+ }
7887
+ this._serverRecordingEnabled = true;
7888
+ this._persistRemoteConfig(response);
7889
+ this.startIfEnabledOrStop();
7890
+ }
7891
+ log(message, level = 'log') {
7892
+ if (this._lazyLoadedSessionRecording?.log) {
7893
+ this._lazyLoadedSessionRecording.log(message, level);
7894
+ } else {
7895
+ logger$3.warn('log called before recorder was ready');
7896
+ }
7897
+ }
7898
+ _onScriptLoaded(startReason) {
7899
+ if (!this._lazyLoadedSessionRecording) {
7900
+ this._lazyLoadedSessionRecording = new LazyLoadedSessionRecording(this._instance);
7901
+ this._lazyLoadedSessionRecording._forceAllowLocalhostNetworkCapture = this._forceAllowLocalhostNetworkCapture;
7902
+ }
7903
+ this._lazyLoadedSessionRecording.start(startReason);
7904
+ }
7905
+ /**
7906
+ * this is maintained on the public API only because it has always been on the public API
7907
+ * if you are calling this directly you are certainly doing something wrong
7908
+ * @deprecated
7909
+ */
7910
+ onRRwebEmit(rawEvent) {
7911
+ this._lazyLoadedSessionRecording?.onRRwebEmit?.(rawEvent);
7912
+ }
7913
+ /**
7914
+ * this ignores the linked flag config and (if other conditions are met) causes capture to start
7915
+ *
7916
+ * It is not usual to call this directly,
7917
+ * instead call `posthog.startSessionRecording({linked_flag: true})`
7918
+ * */
7919
+ overrideLinkedFlag() {
7920
+ // TODO what if this gets called before lazy loading is done
7921
+ this._lazyLoadedSessionRecording?.overrideLinkedFlag();
7922
+ }
7923
+ /**
7924
+ * this ignores the sampling config and (if other conditions are met) causes capture to start
7925
+ *
7926
+ * It is not usual to call this directly,
7927
+ * instead call `posthog.startSessionRecording({sampling: true})`
7928
+ * */
7929
+ overrideSampling() {
7930
+ // TODO what if this gets called before lazy loading is done
7931
+ this._lazyLoadedSessionRecording?.overrideSampling();
7932
+ }
7933
+ /**
7934
+ * this ignores the URL/Event trigger config and (if other conditions are met) causes capture to start
7935
+ *
7936
+ * It is not usual to call this directly,
7937
+ * instead call `posthog.startSessionRecording({trigger: 'url' | 'event'})`
7938
+ * */
7939
+ overrideTrigger(triggerType) {
7940
+ // TODO what if this gets called before lazy loading is done
7941
+ this._lazyLoadedSessionRecording?.overrideTrigger(triggerType);
7942
+ }
7943
+ /*
7944
+ * whenever we capture an event, we add these properties to the event
7945
+ * these are used to debug issues with the session recording
7946
+ * when looking at the event feed for a session
7947
+ */
7948
+ get sdkDebugProperties() {
7949
+ return this._lazyLoadedSessionRecording?.sdkDebugProperties || {
7950
+ $recording_status: this.status
7951
+ };
7952
+ }
7953
+ /**
7954
+ * This adds a custom event to the session recording
7955
+ *
7956
+ * It is not intended for arbitrary public use - playback only displays known custom events
7957
+ * And is exposed on the public interface only so that other parts of the SDK are able to use it
7958
+ *
7959
+ * if you are calling this from client code, you're probably looking for `posthog.capture('$custom_event', {...})`
7960
+ */
7961
+ tryAddCustomEvent(tag, payload) {
7962
+ return !!this._lazyLoadedSessionRecording?.tryAddCustomEvent(tag, payload);
7963
+ }
7964
+ }
7965
+
7966
+ const defaultConfig = () => ({
7967
+ host: 'https://i.leanbase.co',
7968
+ token: '',
7969
+ autocapture: true,
7970
+ rageclick: true,
7971
+ disable_session_recording: false,
7972
+ session_recording: {
7973
+ // Force-enable session recording locally unless explicitly disabled via config
7974
+ forceClientRecording: true
7975
+ },
7976
+ enable_recording_console_log: undefined,
7977
+ persistence: 'localStorage+cookie',
7978
+ capture_pageview: 'history_change',
7979
+ capture_pageleave: 'if_capture_pageview',
7980
+ persistence_name: '',
7981
+ mask_all_element_attributes: false,
7982
+ cookie_expiration: 365,
7983
+ cross_subdomain_cookie: isCrossDomainCookie(document?.location),
7984
+ custom_campaign_params: [],
7985
+ custom_personal_data_properties: [],
7986
+ disable_persistence: false,
7987
+ mask_personal_data_properties: false,
7988
+ secure_cookie: window?.location?.protocol === 'https:',
7989
+ mask_all_text: false,
7990
+ bootstrap: {},
7991
+ session_idle_timeout_seconds: 30 * 60,
7992
+ save_campaign_params: true,
7993
+ save_referrer: true,
7994
+ opt_out_useragent_filter: false,
7995
+ properties_string_max_length: 65535,
7996
+ loaded: () => {}
7997
+ });
7998
+ class Leanbase extends PostHogCore {
7999
+ constructor(token, config) {
8000
+ const mergedConfig = extend(defaultConfig(), config || {}, {
8001
+ token
8002
+ });
8003
+ super(token, mergedConfig);
8004
+ this.personProcessingSetOncePropertiesSent = false;
8005
+ this.isLoaded = false;
8006
+ this.config = mergedConfig;
8007
+ this.visibilityStateListener = null;
8008
+ this.initialPageviewCaptured = false;
8009
+ this.scrollManager = new ScrollManager(this);
8010
+ this.pageViewManager = new PageViewManager(this);
8011
+ this.init(token, mergedConfig);
8012
+ }
8013
+ init(token, config) {
8014
+ this.setConfig(extend(defaultConfig(), config, {
8015
+ token
8016
+ }));
8017
+ this.isLoaded = true;
8018
+ this.persistence = new LeanbasePersistence(this.config);
8019
+ if (this.config.cookieless_mode !== 'always') {
8020
+ this.sessionManager = new SessionIdManager(this);
8021
+ this.sessionPropsManager = new SessionPropsManager(this, this.sessionManager, this.persistence);
8022
+ }
8023
+ this.replayAutocapture = new Autocapture(this);
8024
+ this.replayAutocapture.startIfEnabled();
8025
+ if (this.sessionManager && this.config.cookieless_mode !== 'always') {
8026
+ this.sessionRecording = new SessionRecording(this);
8027
+ this.sessionRecording.startIfEnabledOrStop();
8028
+ }
8029
+ if (this.config.preloadFeatureFlags !== false) {
8030
+ this.reloadFeatureFlags();
8031
+ }
8032
+ this.config.loaded?.(this);
8033
+ if (this.config.capture_pageview) {
8034
+ setTimeout(() => {
8035
+ if (this.config.cookieless_mode === 'always') {
8036
+ this.captureInitialPageview();
8037
+ }
8038
+ }, 1);
8039
+ }
8040
+ addEventListener(document, 'DOMContentLoaded', () => {
8041
+ this.loadRemoteConfig();
8042
+ });
8043
+ addEventListener(window, 'onpagehide' in self ? 'pagehide' : 'unload', this.capturePageLeave.bind(this), {
8044
+ passive: false
8045
+ });
8046
+ }
8047
+ captureInitialPageview() {
8048
+ if (!document) {
8049
+ return;
8050
+ }
8051
+ if (document.visibilityState !== 'visible') {
8052
+ if (!this.visibilityStateListener) {
8053
+ this.visibilityStateListener = this.captureInitialPageview.bind(this);
8054
+ addEventListener(document, 'visibilitychange', this.visibilityStateListener);
8055
+ }
8056
+ return;
8057
+ }
8058
+ if (!this.initialPageviewCaptured) {
8059
+ this.initialPageviewCaptured = true;
8060
+ this.capture('$pageview', {
8061
+ title: document.title
8062
+ });
8063
+ if (this.visibilityStateListener) {
8064
+ document.removeEventListener('visibilitychange', this.visibilityStateListener);
8065
+ this.visibilityStateListener = null;
8066
+ }
8067
+ }
8068
+ }
8069
+ capturePageLeave() {
8070
+ const {
8071
+ capture_pageleave,
8072
+ capture_pageview
8073
+ } = this.config;
8074
+ if (capture_pageleave === true || capture_pageleave === 'if_capture_pageview' && (capture_pageview === true || capture_pageview === 'history_change')) {
8075
+ this.capture('$pageleave');
8076
+ }
8077
+ }
8078
+ async loadRemoteConfig() {
8079
+ if (!this.isRemoteConfigLoaded) {
8080
+ const remoteConfig = await this.reloadRemoteConfigAsync();
8081
+ if (remoteConfig) {
8082
+ this.onRemoteConfig(remoteConfig);
8083
+ }
8084
+ }
8085
+ }
8086
+ onRemoteConfig(config) {
8087
+ if (!(document && document.body)) {
8088
+ setTimeout(() => {
8089
+ this.onRemoteConfig(config);
8090
+ }, 500);
8091
+ return;
8092
+ }
8093
+ this.isRemoteConfigLoaded = true;
8094
+ this.replayAutocapture?.onRemoteConfig(config);
8095
+ this.sessionRecording?.onRemoteConfig(config);
8096
+ }
8097
+ fetch(url, options) {
8098
+ const fetchFn = getFetch();
8099
+ if (!fetchFn) {
8100
+ return Promise.reject(new Error('Fetch API is not available in this environment.'));
8101
+ }
8102
+ return fetchFn(url, options);
8103
+ }
8104
+ setConfig(config) {
8105
+ const oldConfig = {
8106
+ ...this.config
8107
+ };
8108
+ if (isObject(config)) {
8109
+ extend(this.config, config);
8110
+ if (!this.config.api_host && this.config.host) {
8111
+ this.config.api_host = this.config.host;
8112
+ }
8113
+ this.persistence?.update_config(this.config, oldConfig);
8114
+ this.replayAutocapture?.startIfEnabled();
8115
+ this.sessionRecording?.startIfEnabledOrStop();
8116
+ }
8117
+ const isTempStorage = this.config.persistence === 'sessionStorage' || this.config.persistence === 'memory';
8118
+ this.sessionPersistence = isTempStorage ? this.persistence : new LeanbasePersistence({
8119
+ ...this.config,
8120
+ persistence: 'sessionStorage'
8121
+ });
8122
+ }
8123
+ getLibraryId() {
8124
+ return 'leanbase';
8125
+ }
8126
+ getLibraryVersion() {
8127
+ return Config.LIB_VERSION;
8128
+ }
8129
+ getCustomUserAgent() {
8130
+ return;
8131
+ }
8132
+ getPersistedProperty(key) {
8133
+ return this.persistence?.get_property(key);
8134
+ }
8135
+ get_property(key) {
8136
+ return this.persistence?.get_property(key);
8137
+ }
8138
+ setPersistedProperty(key, value) {
8139
+ this.persistence?.set_property(key, value);
8140
+ }
8141
+ calculateEventProperties(eventName, eventProperties, timestamp, uuid, readOnly) {
8142
+ if (!this.persistence || !this.sessionPersistence) {
8143
+ return eventProperties;
8144
+ }
8145
+ timestamp = timestamp || new Date();
5002
8146
  const startTimestamp = readOnly ? undefined : this.persistence?.remove_event_timer(eventName);
5003
8147
  let properties = {
5004
8148
  ...eventProperties
@@ -5013,7 +8157,7 @@ var leanbase = (function () {
5013
8157
  };
5014
8158
  properties['distinct_id'] = persistenceProps.distinct_id;
5015
8159
  if (!(isString(properties['distinct_id']) || isNumber(properties['distinct_id'])) || isEmptyString(properties['distinct_id'])) {
5016
- logger.error('Invalid distinct_id for replay event. This indicates a bug in your implementation');
8160
+ logger$3.error('Invalid distinct_id for replay event. This indicates a bug in your implementation');
5017
8161
  }
5018
8162
  return properties;
5019
8163
  }
@@ -5029,6 +8173,13 @@ var leanbase = (function () {
5029
8173
  if (this.sessionPropsManager) {
5030
8174
  extend(properties, this.sessionPropsManager.getSessionProps());
5031
8175
  }
8176
+ try {
8177
+ if (this.sessionRecording) {
8178
+ extend(properties, this.sessionRecording.sdkDebugProperties);
8179
+ }
8180
+ } catch (e) {
8181
+ properties['$sdk_debug_error_capturing_properties'] = String(e);
8182
+ }
5032
8183
  let pageviewProperties = this.pageViewManager.doEvent();
5033
8184
  if (eventName === '$pageview' && !readOnly) {
5034
8185
  pageviewProperties = this.pageViewManager.doPageView(timestamp, uuid);
@@ -5081,11 +8232,11 @@ var leanbase = (function () {
5081
8232
  return;
5082
8233
  }
5083
8234
  if (isUndefined(event) || !isString(event)) {
5084
- logger.error('No event name provided to posthog.capture');
8235
+ logger$3.error('No event name provided to posthog.capture');
5085
8236
  return;
5086
8237
  }
5087
8238
  if (properties?.$current_url && !isString(properties?.$current_url)) {
5088
- logger.error('Invalid `$current_url` property provided to `posthog.capture`. Input must be a string. Ignoring provided value.');
8239
+ logger$3.error('Invalid `$current_url` property provided to `posthog.capture`. Input must be a string. Ignoring provided value.');
5089
8240
  delete properties?.$current_url;
5090
8241
  }
5091
8242
  this.sessionPersistence.update_search_keyword();
@@ -5221,5 +8372,5 @@ var leanbase = (function () {
5221
8372
 
5222
8373
  return api;
5223
8374
 
5224
- })();
8375
+ })(record);
5225
8376
  //# sourceMappingURL=leanbase.iife.js.map