@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.
package/dist/index.mjs CHANGED
@@ -1,4 +1,6 @@
1
- import { isArray, isString, isNullish, isFormData, hasOwnProperty, isNumber, isNull, PostHogPersistedProperty, isUndefined, isFunction, includes, stripLeadingDollar, isObject, isEmptyObject, isBoolean, trim, clampToRange, PostHogCore, getFetch, isEmptyString } from '@posthog/core';
1
+ import { isArray, isNullish, isFormData, hasOwnProperty, isString, isNull, isNumber, PostHogPersistedProperty, isUndefined, isFile, isFunction, includes, stripLeadingDollar, isObject, isEmptyObject, trim, isBoolean, clampToRange, BucketedRateLimiter, PostHogCore, getFetch, isEmptyString } from '@posthog/core';
2
+ import { record } from '@rrweb/record';
3
+ import { strFromU8, gzipSync, strToU8 } from 'fflate';
2
4
 
3
5
  const breaker = {};
4
6
  const ArrayProto = Array.prototype;
@@ -8,7 +10,7 @@ const win = typeof window !== 'undefined' ? window : undefined;
8
10
  const global = typeof globalThis !== 'undefined' ? globalThis : win;
9
11
  const navigator$1 = global?.navigator;
10
12
  const document = global?.document;
11
- const location = global?.location;
13
+ const location$1 = global?.location;
12
14
  global?.fetch;
13
15
  global?.XMLHttpRequest && 'withCredentials' in new global.XMLHttpRequest() ? global.XMLHttpRequest : undefined;
14
16
  global?.AbortController;
@@ -195,10 +197,13 @@ const EVENT_TIMERS_KEY = '__timers';
195
197
  const AUTOCAPTURE_DISABLED_SERVER_SIDE = '$autocapture_disabled_server_side';
196
198
  const HEATMAPS_ENABLED_SERVER_SIDE = '$heatmaps_enabled_server_side';
197
199
  const ERROR_TRACKING_SUPPRESSION_RULES = '$error_tracking_suppression_rules';
200
+ const SESSION_RECORDING_REMOTE_CONFIG = '$session_recording_remote_config';
198
201
  // @deprecated can be removed along with eager loaded replay
199
202
  const SESSION_RECORDING_ENABLED_SERVER_SIDE = '$session_recording_enabled_server_side';
200
203
  const SESSION_ID = '$sesid';
201
204
  const SESSION_RECORDING_IS_SAMPLED = '$session_is_sampled';
205
+ const SESSION_RECORDING_URL_TRIGGER_ACTIVATED_SESSION = '$session_recording_url_trigger_activated_session';
206
+ const SESSION_RECORDING_EVENT_TRIGGER_ACTIVATED_SESSION = '$session_recording_event_trigger_activated_session';
202
207
  const ENABLED_FEATURE_FLAGS = '$enabled_feature_flags';
203
208
  const PERSISTENCE_EARLY_ACCESS_FEATURES = '$early_access_features';
204
209
  const PERSISTENCE_FEATURE_FLAG_DETAILS = '$feature_flag_details';
@@ -223,7 +228,7 @@ PostHogPersistedProperty.Queue, PostHogPersistedProperty.FeatureFlagDetails, Pos
223
228
 
224
229
  /* eslint-disable no-console */
225
230
  const PREFIX = '[Leanbase]';
226
- const logger = {
231
+ const logger$3 = {
227
232
  info: (...args) => {
228
233
  if (typeof console !== 'undefined') {
229
234
  console.log(PREFIX, ...args);
@@ -521,7 +526,7 @@ function chooseCookieDomain(hostname, cross_subdomain) {
521
526
  if (!matchedSubDomain) {
522
527
  const originalMatch = originalCookieDomainFn(hostname);
523
528
  if (originalMatch !== matchedSubDomain) {
524
- logger.info('Warning: cookie subdomain discovery mismatch', originalMatch, matchedSubDomain);
529
+ logger$3.info('Warning: cookie subdomain discovery mismatch', originalMatch, matchedSubDomain);
525
530
  }
526
531
  matchedSubDomain = originalMatch;
527
532
  }
@@ -533,7 +538,7 @@ function chooseCookieDomain(hostname, cross_subdomain) {
533
538
  const cookieStore = {
534
539
  _is_supported: () => !!document,
535
540
  _error: function (msg) {
536
- logger.error('cookieStore error: ' + msg);
541
+ logger$3.error('cookieStore error: ' + msg);
537
542
  },
538
543
  _get: function (name) {
539
544
  if (!document) {
@@ -582,7 +587,7 @@ const cookieStore = {
582
587
  const new_cookie_val = name + '=' + encodeURIComponent(JSON.stringify(value)) + expires + '; SameSite=Lax; path=/' + cdomain + secure;
583
588
  // 4096 bytes is the size at which some browsers (e.g. firefox) will not store a cookie, warn slightly before that
584
589
  if (new_cookie_val.length > 4096 * 0.9) {
585
- logger.warn('cookieStore warning: large cookie, len=' + new_cookie_val.length);
590
+ logger$3.warn('cookieStore warning: large cookie, len=' + new_cookie_val.length);
586
591
  }
587
592
  document.cookie = new_cookie_val;
588
593
  return new_cookie_val;
@@ -624,13 +629,13 @@ const localStore = {
624
629
  supported = false;
625
630
  }
626
631
  if (!supported) {
627
- logger.error('localStorage unsupported; falling back to cookie store');
632
+ logger$3.error('localStorage unsupported; falling back to cookie store');
628
633
  }
629
634
  _localStorage_supported = supported;
630
635
  return supported;
631
636
  },
632
637
  _error: function (msg) {
633
- logger.error('localStorage error: ' + msg);
638
+ logger$3.error('localStorage error: ' + msg);
634
639
  },
635
640
  _get: function (name) {
636
641
  try {
@@ -716,7 +721,7 @@ const memoryStore = {
716
721
  return true;
717
722
  },
718
723
  _error: function (msg) {
719
- logger.error('memoryStorage error: ' + msg);
724
+ logger$3.error('memoryStorage error: ' + msg);
720
725
  },
721
726
  _get: function (name) {
722
727
  return memoryStorage[name] || null;
@@ -757,7 +762,7 @@ const sessionStore = {
757
762
  return sessionStorageSupported;
758
763
  },
759
764
  _error: function (msg) {
760
- logger.error('sessionStorage error: ', msg);
765
+ logger$3.error('sessionStorage error: ', msg);
761
766
  },
762
767
  _get: function (name) {
763
768
  try {
@@ -791,6 +796,7 @@ const sessionStore = {
791
796
  }
792
797
  };
793
798
 
799
+ const localDomains = ['localhost', '127.0.0.1'];
794
800
  /**
795
801
  * IE11 doesn't support `new URL`
796
802
  * so we can create an anchor element and use that to parse the URL
@@ -805,6 +811,21 @@ const convertToURL = url => {
805
811
  location.href = url;
806
812
  return location;
807
813
  };
814
+ const formDataToQuery = function (formdata, arg_separator = '&') {
815
+ let use_val;
816
+ let use_key;
817
+ const tph_arr = [];
818
+ each(formdata, function (val, key) {
819
+ // the key might be literally the string undefined for e.g. if {undefined: 'something'}
820
+ if (isUndefined(val) || isUndefined(key) || key === 'undefined') {
821
+ return;
822
+ }
823
+ use_val = encodeURIComponent(isFile(val) ? val.name : val.toString());
824
+ use_key = encodeURIComponent(key);
825
+ tph_arr[tph_arr.length] = use_key + '=' + use_val;
826
+ });
827
+ return tph_arr.join(arg_separator);
828
+ };
808
829
  // NOTE: Once we get rid of IE11/op_mini we can start using URLSearchParams
809
830
  const getQueryParam = function (url, param) {
810
831
  const withoutHash = url.split('#')[0] || '';
@@ -828,7 +849,7 @@ const getQueryParam = function (url, param) {
828
849
  try {
829
850
  result = decodeURIComponent(result);
830
851
  } catch {
831
- logger.error('Skipping decoding for malformed query param: ' + result);
852
+ logger$3.error('Skipping decoding for malformed query param: ' + result);
832
853
  }
833
854
  return result.replace(/\+/g, ' ');
834
855
  }
@@ -867,6 +888,9 @@ const maskQueryParams = function (url, maskedParams, mask) {
867
888
  }
868
889
  return result;
869
890
  };
891
+ const isLocalhost = () => {
892
+ return localDomains.includes(location.hostname);
893
+ };
870
894
 
871
895
  /**
872
896
  * this device detection code is (at time of writing) about 3% of the size of the entire library
@@ -1159,7 +1183,7 @@ const detectDeviceType = function (user_agent) {
1159
1183
  }
1160
1184
  };
1161
1185
 
1162
- var version = "0.0.7";
1186
+ var version = "0.1.1";
1163
1187
  var packageInfo = {
1164
1188
  version: version};
1165
1189
 
@@ -1309,7 +1333,7 @@ function getReferrerInfo() {
1309
1333
  }
1310
1334
  function getPersonInfo(maskPersonalDataProperties, customPersonalDataProperties) {
1311
1335
  const paramsToMask = maskPersonalDataProperties ? extendArray([], PERSONAL_DATA_CAMPAIGN_PARAMS, customPersonalDataProperties || []) : [];
1312
- const url = location?.href.substring(0, 1000);
1336
+ const url = location$1?.href.substring(0, 1000);
1313
1337
  // we're being a bit more economical with bytes here because this is stored in the cookie
1314
1338
  return {
1315
1339
  r: getReferrer().substring(0, 1000),
@@ -1377,9 +1401,9 @@ function getEventProperties(maskPersonalDataProperties, customPersonalDataProper
1377
1401
  $timezone: getTimezone(),
1378
1402
  $timezone_offset: getTimezoneOffset()
1379
1403
  }), {
1380
- $current_url: maskQueryParams(location?.href, paramsToMask, MASKED),
1381
- $host: location?.host,
1382
- $pathname: location?.pathname,
1404
+ $current_url: maskQueryParams(location$1?.href, paramsToMask, MASKED),
1405
+ $host: location$1?.host,
1406
+ $pathname: location$1?.pathname,
1383
1407
  $raw_user_agent: userAgent.length > 1000 ? userAgent.substring(0, 997) + '...' : userAgent,
1384
1408
  $browser_version: detectBrowserVersion(userAgent, navigator.vendor),
1385
1409
  $browser_language: getBrowserLanguage(),
@@ -1424,7 +1448,7 @@ class LeanbasePersistence {
1424
1448
  this._storage = this._buildStorage(config);
1425
1449
  this.load();
1426
1450
  if (config.debug) {
1427
- logger.info('Persistence loaded', config['persistence'], {
1451
+ logger$3.info('Persistence loaded', config['persistence'], {
1428
1452
  ...this.props
1429
1453
  });
1430
1454
  }
@@ -1440,7 +1464,7 @@ class LeanbasePersistence {
1440
1464
  }
1441
1465
  _buildStorage(config) {
1442
1466
  if (CASE_INSENSITIVE_PERSISTENCE_TYPES.indexOf(config['persistence'].toLowerCase()) === -1) {
1443
- logger.info('Unknown persistence type ' + config['persistence'] + '; falling back to localStorage+cookie');
1467
+ logger$3.info('Unknown persistence type ' + config['persistence'] + '; falling back to localStorage+cookie');
1444
1468
  config['persistence'] = 'localStorage+cookie';
1445
1469
  }
1446
1470
  let store;
@@ -2076,7 +2100,7 @@ function getNestedSpanText(target) {
2076
2100
  text = `${text} ${getNestedSpanText(child)}`.trim();
2077
2101
  }
2078
2102
  } catch (e) {
2079
- logger.error('[AutoCapture]', e);
2103
+ logger$3.error('[AutoCapture]', e);
2080
2104
  }
2081
2105
  }
2082
2106
  });
@@ -2378,7 +2402,7 @@ class Autocapture {
2378
2402
  }
2379
2403
  _addDomEventHandlers() {
2380
2404
  if (!this.isBrowserSupported()) {
2381
- logger.info('Disabling Automatic Event Collection because this browser is not supported');
2405
+ logger$3.info('Disabling Automatic Event Collection because this browser is not supported');
2382
2406
  return;
2383
2407
  }
2384
2408
  if (!win || !document) {
@@ -2389,7 +2413,7 @@ class Autocapture {
2389
2413
  try {
2390
2414
  this._captureEvent(e);
2391
2415
  } catch (error) {
2392
- logger.error('Failed to capture event', error);
2416
+ logger$3.error('Failed to capture event', error);
2393
2417
  }
2394
2418
  };
2395
2419
  addEventListener(document, 'submit', handler, {
@@ -2574,7 +2598,7 @@ class SessionIdManager {
2574
2598
  this._windowIdGenerator = windowIdGenerator || uuidv7;
2575
2599
  const persistenceName = this._config['persistence_name'] || this._config['token'];
2576
2600
  const desiredTimeout = this._config['session_idle_timeout_seconds'] || DEFAULT_SESSION_IDLE_TIMEOUT_SECONDS;
2577
- this._sessionTimeoutMs = clampToRange(desiredTimeout, MIN_SESSION_IDLE_TIMEOUT_SECONDS, MAX_SESSION_IDLE_TIMEOUT_SECONDS, logger, DEFAULT_SESSION_IDLE_TIMEOUT_SECONDS) * 1000;
2601
+ this._sessionTimeoutMs = clampToRange(desiredTimeout, MIN_SESSION_IDLE_TIMEOUT_SECONDS, MAX_SESSION_IDLE_TIMEOUT_SECONDS, logger$3, DEFAULT_SESSION_IDLE_TIMEOUT_SECONDS) * 1000;
2578
2602
  instance.register({
2579
2603
  $configured_session_timeout_ms: this._sessionTimeoutMs
2580
2604
  });
@@ -2601,7 +2625,7 @@ class SessionIdManager {
2601
2625
  const sessionStartTimestamp = uuid7ToTimestampMs(this._config.bootstrap.sessionID);
2602
2626
  this._setSessionId(this._config.bootstrap.sessionID, new Date().getTime(), sessionStartTimestamp);
2603
2627
  } catch (e) {
2604
- logger.error('Invalid sessionID in bootstrap', e);
2628
+ logger$3.error('Invalid sessionID in bootstrap', e);
2605
2629
  }
2606
2630
  }
2607
2631
  this._listenToReloadWindow();
@@ -2742,7 +2766,7 @@ class SessionIdManager {
2742
2766
  if (noSessionId || activityTimeout || sessionPastMaximumLength) {
2743
2767
  sessionId = this._sessionIdGenerator();
2744
2768
  windowId = this._windowIdGenerator();
2745
- logger.info('new session ID generated', {
2769
+ logger$3.info('new session ID generated', {
2746
2770
  sessionId,
2747
2771
  windowId,
2748
2772
  changeReason: {
@@ -2871,67 +2895,6 @@ class SessionPropsManager {
2871
2895
  }
2872
2896
  }
2873
2897
 
2874
- var RequestRouterRegion;
2875
- (function (RequestRouterRegion) {
2876
- RequestRouterRegion["US"] = "us";
2877
- RequestRouterRegion["EU"] = "eu";
2878
- RequestRouterRegion["CUSTOM"] = "custom";
2879
- })(RequestRouterRegion || (RequestRouterRegion = {}));
2880
- const ingestionDomain = 'i.posthog.com';
2881
- class RequestRouter {
2882
- constructor(instance) {
2883
- this._regionCache = {};
2884
- this.instance = instance;
2885
- }
2886
- get apiHost() {
2887
- const host = this.instance.config.api_host.trim().replace(/\/$/, '');
2888
- if (host === 'https://app.posthog.com') {
2889
- return 'https://us.i.posthog.com';
2890
- }
2891
- return host;
2892
- }
2893
- get uiHost() {
2894
- let host = this.instance.config.ui_host?.replace(/\/$/, '');
2895
- if (!host) {
2896
- host = this.apiHost.replace(`.${ingestionDomain}`, '.posthog.com');
2897
- }
2898
- if (host === 'https://app.posthog.com') {
2899
- return 'https://us.posthog.com';
2900
- }
2901
- return host;
2902
- }
2903
- get region() {
2904
- if (!this._regionCache[this.apiHost]) {
2905
- if (/https:\/\/(app|us|us-assets)(\\.i)?\\.posthog\\.com/i.test(this.apiHost)) {
2906
- this._regionCache[this.apiHost] = RequestRouterRegion.US;
2907
- } else if (/https:\/\/(eu|eu-assets)(\\.i)?\\.posthog\\.com/i.test(this.apiHost)) {
2908
- this._regionCache[this.apiHost] = RequestRouterRegion.EU;
2909
- } else {
2910
- this._regionCache[this.apiHost] = RequestRouterRegion.CUSTOM;
2911
- }
2912
- }
2913
- return this._regionCache[this.apiHost];
2914
- }
2915
- endpointFor(target, path = '') {
2916
- if (path) {
2917
- path = path[0] === '/' ? path : `/${path}`;
2918
- }
2919
- if (target === 'ui') {
2920
- return this.uiHost + path;
2921
- }
2922
- if (this.region === RequestRouterRegion.CUSTOM) {
2923
- return this.apiHost + path;
2924
- }
2925
- const suffix = ingestionDomain + path;
2926
- switch (target) {
2927
- case 'assets':
2928
- return `https://${this.region}-assets.${suffix}`;
2929
- case 'api':
2930
- return `https://${this.region}.${suffix}`;
2931
- }
2932
- }
2933
- }
2934
-
2935
2898
  // This keeps track of the PageView state (such as the previous PageView's path, timestamp, id, and scroll properties).
2936
2899
  // We store the state in memory, which means that for non-SPA sites, the state will be lost on page reload. This means
2937
2900
  // that non-SPA sites should always send a $pageleave event on any navigation, before the page unloads. For SPA sites,
@@ -2992,10 +2955,10 @@ class PageViewManager {
2992
2955
  lastContentY = Math.ceil(lastContentY);
2993
2956
  maxContentY = Math.ceil(maxContentY);
2994
2957
  // if the maximum scroll height is near 0, then the percentage is 1
2995
- const lastScrollPercentage = maxScrollHeight <= 1 ? 1 : clampToRange(lastScrollY / maxScrollHeight, 0, 1, logger);
2996
- const maxScrollPercentage = maxScrollHeight <= 1 ? 1 : clampToRange(maxScrollY / maxScrollHeight, 0, 1, logger);
2997
- const lastContentPercentage = maxContentHeight <= 1 ? 1 : clampToRange(lastContentY / maxContentHeight, 0, 1, logger);
2998
- const maxContentPercentage = maxContentHeight <= 1 ? 1 : clampToRange(maxContentY / maxContentHeight, 0, 1, logger);
2958
+ const lastScrollPercentage = maxScrollHeight <= 1 ? 1 : clampToRange(lastScrollY / maxScrollHeight, 0, 1, logger$3);
2959
+ const maxScrollPercentage = maxScrollHeight <= 1 ? 1 : clampToRange(maxScrollY / maxScrollHeight, 0, 1, logger$3);
2960
+ const lastContentPercentage = maxContentHeight <= 1 ? 1 : clampToRange(lastContentY / maxContentHeight, 0, 1, logger$3);
2961
+ const maxContentPercentage = maxContentHeight <= 1 ? 1 : clampToRange(maxContentY / maxContentHeight, 0, 1, logger$3);
2999
2962
  properties = extend(properties, {
3000
2963
  $prev_pageview_last_scroll: lastScrollY,
3001
2964
  $prev_pageview_last_scroll_percentage: lastScrollPercentage,
@@ -3158,206 +3121,2762 @@ const isLikelyBot = function (navigator, customBlockedUserAgents) {
3158
3121
  // It would be very bad if posthog-js caused a permission prompt to appear on every page load.
3159
3122
  };
3160
3123
 
3161
- const defaultConfig = () => ({
3162
- host: 'https://i.leanbase.co',
3163
- api_host: 'https://i.leanbase.co',
3164
- token: '',
3165
- autocapture: true,
3166
- rageclick: true,
3167
- persistence: 'localStorage+cookie',
3168
- capture_pageview: 'history_change',
3169
- capture_pageleave: 'if_capture_pageview',
3170
- persistence_name: '',
3171
- mask_all_element_attributes: false,
3172
- cookie_expiration: 365,
3173
- cross_subdomain_cookie: isCrossDomainCookie(document?.location),
3174
- custom_campaign_params: [],
3175
- custom_personal_data_properties: [],
3176
- disable_persistence: false,
3177
- mask_personal_data_properties: false,
3178
- secure_cookie: window?.location?.protocol === 'https:',
3179
- mask_all_text: false,
3180
- bootstrap: {},
3181
- session_idle_timeout_seconds: 30 * 60,
3182
- save_campaign_params: true,
3183
- save_referrer: true,
3184
- opt_out_useragent_filter: false,
3185
- properties_string_max_length: 65535,
3186
- loaded: () => {},
3187
- session_recording: {}
3188
- });
3189
- class Leanbase extends PostHogCore {
3190
- constructor(token, config) {
3191
- const mergedConfig = extend(defaultConfig(), config || {}, {
3192
- token
3193
- });
3194
- super(token, mergedConfig);
3195
- this.personProcessingSetOncePropertiesSent = false;
3196
- this.isLoaded = false;
3197
- this.config = mergedConfig;
3198
- this.visibilityStateListener = null;
3199
- this.initialPageviewCaptured = false;
3200
- this.scrollManager = new ScrollManager(this);
3201
- this.pageViewManager = new PageViewManager(this);
3202
- this.requestRouter = new RequestRouter(this);
3203
- this.init(token, mergedConfig);
3204
- }
3205
- init(token, config) {
3206
- this.setConfig(extend(defaultConfig(), config, {
3207
- token
3208
- }));
3209
- this.isLoaded = true;
3210
- this.persistence = new LeanbasePersistence(this.config);
3211
- this.replayAutocapture = new Autocapture(this);
3212
- this.replayAutocapture.startIfEnabled();
3213
- // Initialize session manager and props before session recording (matches browser behavior)
3214
- if (this.config.cookieless_mode !== 'always') {
3215
- if (!this.sessionManager) {
3216
- this.sessionManager = new SessionIdManager(this);
3217
- this.sessionPropsManager = new SessionPropsManager(this, this.sessionManager, this.persistence);
3218
- }
3219
- // runtime require to lazy-load replay code; allowed for browser parity
3220
- // @ts-expect-error - runtime import only available in browser build
3221
- const {
3222
- SessionRecording
3223
- } = require('./extensions/replay/session-recording'); // eslint-disable-line @typescript-eslint/no-require-imports
3224
- this.sessionRecording = new SessionRecording(this);
3225
- this.sessionRecording.startIfEnabledOrStop();
3226
- }
3227
- if (this.config.preloadFeatureFlags !== false) {
3228
- this.reloadFeatureFlags();
3124
+ const _createLogger = prefix => {
3125
+ return {
3126
+ info: (...args) => logger$3.info(prefix, ...args),
3127
+ warn: (...args) => logger$3.warn(prefix, ...args),
3128
+ error: (...args) => logger$3.error(prefix, ...args),
3129
+ critical: (...args) => logger$3.critical(prefix, ...args),
3130
+ uninitializedWarning: methodName => {
3131
+ logger$3.error(prefix, `You must initialize Leanbase before calling ${methodName}`);
3132
+ },
3133
+ createLogger: additionalPrefix => _createLogger(`${prefix} ${additionalPrefix}`)
3134
+ };
3135
+ };
3136
+ const logger$2 = _createLogger('[Leanbase]');
3137
+ const createLogger = _createLogger;
3138
+
3139
+ function patch(source, name, replacement) {
3140
+ try {
3141
+ if (!(name in source)) {
3142
+ return () => {
3143
+ //
3144
+ };
3229
3145
  }
3230
- this.config.loaded?.(this);
3231
- if (this.config.capture_pageview) {
3232
- setTimeout(() => {
3233
- if (this.config.cookieless_mode === 'always') {
3234
- this.captureInitialPageview();
3146
+ const original = source[name];
3147
+ const wrapped = replacement(original);
3148
+ if (isFunction(wrapped)) {
3149
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
3150
+ wrapped.prototype = wrapped.prototype || {};
3151
+ Object.defineProperties(wrapped, {
3152
+ __posthog_wrapped__: {
3153
+ enumerable: false,
3154
+ value: true
3235
3155
  }
3236
- }, 1);
3156
+ });
3237
3157
  }
3238
- addEventListener(document, 'DOMContentLoaded', () => {
3239
- this.loadRemoteConfig();
3240
- });
3241
- addEventListener(window, 'onpagehide' in self ? 'pagehide' : 'unload', this.capturePageLeave.bind(this), {
3242
- passive: false
3243
- });
3158
+ source[name] = wrapped;
3159
+ return () => {
3160
+ source[name] = original;
3161
+ };
3162
+ } catch {
3163
+ return () => {
3164
+ //
3165
+ };
3244
3166
  }
3245
- captureInitialPageview() {
3246
- if (!document) {
3247
- return;
3248
- }
3249
- if (document.visibilityState !== 'visible') {
3250
- if (!this.visibilityStateListener) {
3251
- this.visibilityStateListener = this.captureInitialPageview.bind(this);
3252
- addEventListener(document, 'visibilitychange', this.visibilityStateListener);
3253
- }
3254
- return;
3167
+ }
3168
+
3169
+ function hostnameFromURL(url) {
3170
+ try {
3171
+ if (typeof url === 'string') {
3172
+ return new URL(url).hostname;
3255
3173
  }
3256
- if (!this.initialPageviewCaptured) {
3257
- this.initialPageviewCaptured = true;
3258
- this.capture('$pageview', {
3259
- title: document.title
3260
- });
3261
- if (this.visibilityStateListener) {
3262
- document.removeEventListener('visibilitychange', this.visibilityStateListener);
3263
- this.visibilityStateListener = null;
3264
- }
3174
+ if ('url' in url) {
3175
+ return new URL(url.url).hostname;
3265
3176
  }
3177
+ return url.hostname;
3178
+ } catch {
3179
+ return null;
3266
3180
  }
3267
- capturePageLeave() {
3268
- const {
3269
- capture_pageleave,
3270
- capture_pageview
3271
- } = this.config;
3272
- if (capture_pageleave === true || capture_pageleave === 'if_capture_pageview' && (capture_pageview === true || capture_pageview === 'history_change')) {
3273
- this.capture('$pageleave');
3181
+ }
3182
+ function isHostOnDenyList(url, options) {
3183
+ const hostname = hostnameFromURL(url);
3184
+ const defaultNotDenied = {
3185
+ hostname,
3186
+ isHostDenied: false
3187
+ };
3188
+ if (!options.payloadHostDenyList?.length || !hostname?.trim().length) {
3189
+ return defaultNotDenied;
3190
+ }
3191
+ for (const deny of options.payloadHostDenyList) {
3192
+ if (hostname.endsWith(deny)) {
3193
+ return {
3194
+ hostname,
3195
+ isHostDenied: true
3196
+ };
3274
3197
  }
3275
3198
  }
3276
- async loadRemoteConfig() {
3277
- if (!this.isRemoteConfigLoaded) {
3278
- const remoteConfig = await this.reloadRemoteConfigAsync();
3279
- if (remoteConfig) {
3280
- this.onRemoteConfig(remoteConfig);
3199
+ return defaultNotDenied;
3200
+ }
3201
+
3202
+ const LOGGER_PREFIX$2 = '[SessionRecording]';
3203
+ const REDACTED = 'redacted';
3204
+ const defaultNetworkOptions = {
3205
+ initiatorTypes: ['audio', 'beacon', 'body', 'css', 'early-hints', 'embed', 'fetch', 'frame', 'iframe', 'image', 'img', 'input', 'link', 'navigation', 'object', 'ping', 'script', 'track', 'video', 'xmlhttprequest'],
3206
+ maskRequestFn: data => data,
3207
+ recordHeaders: false,
3208
+ recordBody: false,
3209
+ recordInitialRequests: false,
3210
+ recordPerformance: false,
3211
+ performanceEntryTypeToObserve: [
3212
+ // 'event', // This is too noisy as it covers all browser events
3213
+ 'first-input',
3214
+ // 'mark', // Mark is used too liberally. We would need to filter for specific marks
3215
+ // 'measure', // Measure is used too liberally. We would need to filter for specific measures
3216
+ 'navigation', 'paint', 'resource'],
3217
+ payloadSizeLimitBytes: 1000000,
3218
+ payloadHostDenyList: ['.lr-ingest.io', '.ingest.sentry.io', '.clarity.ms',
3219
+ // NB no leading dot here
3220
+ 'analytics.google.com', 'bam.nr-data.net']
3221
+ };
3222
+ 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'];
3223
+ const PAYLOAD_CONTENT_DENY_LIST = ['password', 'secret', 'passwd', 'api_key', 'apikey', 'auth', 'credentials', 'mysql_pwd', 'privatekey', 'private_key', 'token'];
3224
+ // we always remove headers on the deny list because we never want to capture this sensitive data
3225
+ const removeAuthorizationHeader = data => {
3226
+ const headers = data.requestHeaders;
3227
+ if (!isNullish(headers)) {
3228
+ const mutableHeaders = isArray(headers) ? Object.fromEntries(headers) : headers;
3229
+ each(Object.keys(mutableHeaders ?? {}), header => {
3230
+ if (HEADER_DENY_LIST.includes(header.toLowerCase())) {
3231
+ mutableHeaders[header] = REDACTED;
3281
3232
  }
3282
- }
3233
+ });
3234
+ data.requestHeaders = mutableHeaders;
3283
3235
  }
3284
- onRemoteConfig(config) {
3285
- if (!(document && document.body)) {
3286
- setTimeout(() => {
3287
- this.onRemoteConfig(config);
3288
- }, 500);
3289
- return;
3290
- }
3291
- this.isRemoteConfigLoaded = true;
3292
- this.replayAutocapture?.onRemoteConfig(config);
3236
+ return data;
3237
+ };
3238
+ const POSTHOG_PATHS_TO_IGNORE = ['/s/', '/e/', '/i/'];
3239
+ // want to ignore posthog paths when capturing requests, or we can get trapped in a loop
3240
+ // because calls to PostHog would be reported using a call to PostHog which would be reported....
3241
+ const ignorePostHogPaths = (data, apiHostConfig) => {
3242
+ const url = convertToURL(data.name);
3243
+ const host = apiHostConfig || '';
3244
+ let replaceValue = host.indexOf('http') === 0 ? convertToURL(host)?.pathname : host;
3245
+ if (replaceValue === '/') {
3246
+ replaceValue = '';
3247
+ }
3248
+ const pathname = url?.pathname.replace(replaceValue || '', '');
3249
+ if (url && pathname && POSTHOG_PATHS_TO_IGNORE.some(path => pathname.indexOf(path) === 0)) {
3250
+ return undefined;
3293
3251
  }
3294
- fetch(url, options) {
3295
- const fetchFn = getFetch();
3296
- if (!fetchFn) {
3297
- return Promise.reject(new Error('Fetch API is not available in this environment.'));
3252
+ return data;
3253
+ };
3254
+ function estimateBytes(payload) {
3255
+ return new Blob([payload]).size;
3256
+ }
3257
+ function enforcePayloadSizeLimit(payload, headers, limit, description) {
3258
+ if (isNullish(payload)) {
3259
+ return payload;
3260
+ }
3261
+ let requestContentLength = headers?.['content-length'] || estimateBytes(payload);
3262
+ if (isString(requestContentLength)) {
3263
+ requestContentLength = parseInt(requestContentLength);
3264
+ }
3265
+ if (requestContentLength > limit) {
3266
+ return LOGGER_PREFIX$2 + ` ${description} body too large to record (${requestContentLength} bytes)`;
3267
+ }
3268
+ return payload;
3269
+ }
3270
+ // people can have arbitrarily large payloads on their site, but we don't want to ingest them
3271
+ const limitPayloadSize = options => {
3272
+ // the smallest of 1MB or the specified limit if there is one
3273
+ const limit = Math.min(1000000, options.payloadSizeLimitBytes ?? 1000000);
3274
+ return data => {
3275
+ if (data?.requestBody) {
3276
+ data.requestBody = enforcePayloadSizeLimit(data.requestBody, data.requestHeaders, limit, 'Request');
3277
+ }
3278
+ if (data?.responseBody) {
3279
+ data.responseBody = enforcePayloadSizeLimit(data.responseBody, data.responseHeaders, limit, 'Response');
3280
+ }
3281
+ return data;
3282
+ };
3283
+ };
3284
+ function scrubPayload(payload, label) {
3285
+ if (isNullish(payload)) {
3286
+ return payload;
3287
+ }
3288
+ let scrubbed = payload;
3289
+ if (!shouldCaptureValue(scrubbed, false)) {
3290
+ scrubbed = LOGGER_PREFIX$2 + ' ' + label + ' body ' + REDACTED;
3291
+ }
3292
+ each(PAYLOAD_CONTENT_DENY_LIST, text => {
3293
+ if (scrubbed?.length && scrubbed?.indexOf(text) !== -1) {
3294
+ scrubbed = LOGGER_PREFIX$2 + ' ' + label + ' body ' + REDACTED + ' as might contain: ' + text;
3298
3295
  }
3299
- return fetchFn(url, options);
3296
+ });
3297
+ return scrubbed;
3298
+ }
3299
+ function scrubPayloads(capturedRequest) {
3300
+ if (isUndefined(capturedRequest)) {
3301
+ return undefined;
3300
3302
  }
3301
- setConfig(config) {
3302
- const oldConfig = {
3303
- ...this.config
3303
+ capturedRequest.requestBody = scrubPayload(capturedRequest.requestBody, 'Request');
3304
+ capturedRequest.responseBody = scrubPayload(capturedRequest.responseBody, 'Response');
3305
+ return capturedRequest;
3306
+ }
3307
+ /**
3308
+ * whether a maskRequestFn is provided or not,
3309
+ * we ensure that we remove the denied header from requests
3310
+ * we _never_ want to record that header by accident
3311
+ * if someone complains then we'll add an opt-in to let them override it
3312
+ */
3313
+ const buildNetworkRequestOptions = (instanceConfig, remoteNetworkOptions = {}) => {
3314
+ const remoteOptions = remoteNetworkOptions || {};
3315
+ const config = {
3316
+ payloadSizeLimitBytes: defaultNetworkOptions.payloadSizeLimitBytes,
3317
+ performanceEntryTypeToObserve: [...defaultNetworkOptions.performanceEntryTypeToObserve],
3318
+ payloadHostDenyList: [...(remoteOptions.payloadHostDenyList || []), ...defaultNetworkOptions.payloadHostDenyList]
3319
+ };
3320
+ // client can always disable despite remote options
3321
+ const sessionRecordingConfig = instanceConfig.session_recording || {};
3322
+ const capturePerformanceConfig = instanceConfig.capture_performance;
3323
+ const userPerformanceOptIn = isBoolean(capturePerformanceConfig) ? capturePerformanceConfig : !!capturePerformanceConfig?.network_timing;
3324
+ const canRecordHeaders = sessionRecordingConfig.recordHeaders === true && !!remoteOptions.recordHeaders;
3325
+ const canRecordBody = sessionRecordingConfig.recordBody === true && !!remoteOptions.recordBody;
3326
+ const canRecordPerformance = userPerformanceOptIn && !!remoteOptions.recordPerformance;
3327
+ const payloadLimiter = limitPayloadSize(config);
3328
+ const enforcedCleaningFn = d => payloadLimiter(ignorePostHogPaths(removeAuthorizationHeader(d), instanceConfig.host || ''));
3329
+ const hasDeprecatedMaskFunction = isFunction(sessionRecordingConfig.maskNetworkRequestFn);
3330
+ if (hasDeprecatedMaskFunction && isFunction(sessionRecordingConfig.maskCapturedNetworkRequestFn)) {
3331
+ logger$2.warn('Both `maskNetworkRequestFn` and `maskCapturedNetworkRequestFn` are defined. `maskNetworkRequestFn` will be ignored.');
3332
+ }
3333
+ if (hasDeprecatedMaskFunction) {
3334
+ sessionRecordingConfig.maskCapturedNetworkRequestFn = data => {
3335
+ const cleanedURL = sessionRecordingConfig.maskNetworkRequestFn({
3336
+ url: data.name
3337
+ });
3338
+ return {
3339
+ ...data,
3340
+ name: cleanedURL?.url
3341
+ };
3304
3342
  };
3305
- if (isObject(config)) {
3306
- extend(this.config, config);
3307
- this.persistence?.update_config(this.config, oldConfig);
3308
- this.replayAutocapture?.startIfEnabled();
3309
- }
3310
- const isTempStorage = this.config.persistence === 'sessionStorage' || this.config.persistence === 'memory';
3311
- this.sessionPersistence = isTempStorage ? this.persistence : new LeanbasePersistence({
3312
- ...this.config,
3313
- persistence: 'sessionStorage'
3343
+ }
3344
+ config.maskRequestFn = isFunction(sessionRecordingConfig.maskCapturedNetworkRequestFn) ? data => {
3345
+ const cleanedRequest = enforcedCleaningFn(data);
3346
+ return cleanedRequest ? sessionRecordingConfig.maskCapturedNetworkRequestFn?.(cleanedRequest) ?? undefined : undefined;
3347
+ } : data => scrubPayloads(enforcedCleaningFn(data));
3348
+ return {
3349
+ ...defaultNetworkOptions,
3350
+ ...config,
3351
+ recordHeaders: canRecordHeaders,
3352
+ recordBody: canRecordBody,
3353
+ recordPerformance: canRecordPerformance,
3354
+ recordInitialRequests: canRecordPerformance
3355
+ };
3356
+ };
3357
+
3358
+ /// <reference lib="dom" />
3359
+ const logger$1 = createLogger('[Recorder]');
3360
+ const isNavigationTiming = entry => entry.entryType === 'navigation';
3361
+ const isResourceTiming = entry => entry.entryType === 'resource';
3362
+ function findLast(array, predicate) {
3363
+ const length = array.length;
3364
+ for (let i = length - 1; i >= 0; i -= 1) {
3365
+ if (predicate(array[i])) {
3366
+ return array[i];
3367
+ }
3368
+ }
3369
+ return undefined;
3370
+ }
3371
+ function isDocument(value) {
3372
+ return !!value && typeof value === 'object' && 'nodeType' in value && value.nodeType === 9;
3373
+ }
3374
+ function initPerformanceObserver(cb, win, options) {
3375
+ // if we are only observing timings then we could have a single observer for all types, with buffer true,
3376
+ // but we are going to filter by initiatorType _if we are wrapping fetch and xhr as the wrapped functions
3377
+ // will deal with those.
3378
+ // so we have a block which captures requests from before fetch/xhr is wrapped
3379
+ // these are marked `isInitial` so playback can display them differently if needed
3380
+ // they will never have method/status/headers/body because they are pre-wrapping that provides that
3381
+ if (options.recordInitialRequests) {
3382
+ const initialPerformanceEntries = win.performance.getEntries().filter(entry => isNavigationTiming(entry) || isResourceTiming(entry) && options.initiatorTypes.includes(entry.initiatorType));
3383
+ cb({
3384
+ requests: initialPerformanceEntries.flatMap(entry => prepareRequest({
3385
+ entry,
3386
+ method: undefined,
3387
+ status: undefined,
3388
+ networkRequest: {},
3389
+ isInitial: true
3390
+ })),
3391
+ isInitial: true
3314
3392
  });
3315
3393
  }
3316
- getLibraryId() {
3317
- return 'leanbase';
3394
+ const observer = new win.PerformanceObserver(entries => {
3395
+ // if recordBody or recordHeaders is true then we don't want to record fetch or xhr here
3396
+ // as the wrapped functions will do that. Otherwise, this filter becomes a noop
3397
+ // because we do want to record them here
3398
+ const wrappedInitiatorFilter = entry => options.recordBody || options.recordHeaders ? entry.initiatorType !== 'xmlhttprequest' && entry.initiatorType !== 'fetch' : true;
3399
+ const performanceEntries = entries.getEntries().filter(entry => isNavigationTiming(entry) || isResourceTiming(entry) && options.initiatorTypes.includes(entry.initiatorType) &&
3400
+ // TODO if we are _only_ capturing timing we don't want to filter initiator here
3401
+ wrappedInitiatorFilter(entry));
3402
+ cb({
3403
+ requests: performanceEntries.flatMap(entry => prepareRequest({
3404
+ entry,
3405
+ method: undefined,
3406
+ status: undefined,
3407
+ networkRequest: {}
3408
+ }))
3409
+ });
3410
+ });
3411
+ // compat checked earlier
3412
+ // eslint-disable-next-line compat/compat
3413
+ const entryTypes = PerformanceObserver.supportedEntryTypes.filter(x => options.performanceEntryTypeToObserve.includes(x));
3414
+ // initial records are gathered above, so we don't need to observe and buffer each type separately
3415
+ observer.observe({
3416
+ entryTypes
3417
+ });
3418
+ return () => {
3419
+ observer.disconnect();
3420
+ };
3421
+ }
3422
+ function shouldRecordHeaders(type, recordHeaders) {
3423
+ return !!recordHeaders && (isBoolean(recordHeaders) || recordHeaders[type]);
3424
+ }
3425
+ function shouldRecordBody({
3426
+ type,
3427
+ recordBody,
3428
+ headers,
3429
+ url
3430
+ }) {
3431
+ function matchesContentType(contentTypes) {
3432
+ const contentTypeHeader = Object.keys(headers).find(key => key.toLowerCase() === 'content-type');
3433
+ const contentType = contentTypeHeader && headers[contentTypeHeader];
3434
+ return contentTypes.some(ct => contentType?.includes(ct));
3318
3435
  }
3319
- getLibraryVersion() {
3320
- return Config.LIB_VERSION;
3436
+ /**
3437
+ * particularly in canvas applications we see many requests to blob URLs
3438
+ * e.g. blob:https://video_url
3439
+ * these blob/object URLs are local to the browser, we can never capture that body
3440
+ * so we can just return false here
3441
+ */
3442
+ function isBlobURL(url) {
3443
+ try {
3444
+ if (typeof url === 'string') {
3445
+ return url.startsWith('blob:');
3446
+ }
3447
+ if (url instanceof URL) {
3448
+ return url.protocol === 'blob:';
3449
+ }
3450
+ if (url instanceof Request) {
3451
+ return isBlobURL(url.url);
3452
+ }
3453
+ return false;
3454
+ } catch {
3455
+ return false;
3456
+ }
3321
3457
  }
3322
- getCustomUserAgent() {
3323
- return;
3458
+ if (!recordBody) return false;
3459
+ if (isBlobURL(url)) return false;
3460
+ if (isBoolean(recordBody)) return true;
3461
+ if (isArray(recordBody)) return matchesContentType(recordBody);
3462
+ const recordBodyType = recordBody[type];
3463
+ if (isBoolean(recordBodyType)) return recordBodyType;
3464
+ return matchesContentType(recordBodyType);
3465
+ }
3466
+ async function getRequestPerformanceEntry(win, initiatorType, url, start, end, attempt = 0) {
3467
+ if (attempt > 10) {
3468
+ logger$1.warn('Failed to get performance entry for request', {
3469
+ url,
3470
+ initiatorType
3471
+ });
3472
+ return null;
3324
3473
  }
3325
- getPersistedProperty(key) {
3326
- return this.persistence?.get_property(key);
3474
+ const urlPerformanceEntries = win.performance.getEntriesByName(url);
3475
+ const performanceEntry = findLast(urlPerformanceEntries, entry => isResourceTiming(entry) && entry.initiatorType === initiatorType && (isUndefined(start) || entry.startTime >= start) && (isUndefined(end) || entry.startTime <= end));
3476
+ if (!performanceEntry) {
3477
+ await new Promise(resolve => setTimeout(resolve, 50 * attempt));
3478
+ return getRequestPerformanceEntry(win, initiatorType, url, start, end, attempt + 1);
3327
3479
  }
3328
- setPersistedProperty(key, value) {
3329
- this.persistence?.set_property(key, value);
3480
+ return performanceEntry;
3481
+ }
3482
+ /**
3483
+ * According to MDN https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/response
3484
+ * xhr response is typed as any but can be an ArrayBuffer, a Blob, a Document, a JavaScript object,
3485
+ * or a string, depending on the value of XMLHttpRequest.responseType, that contains the response entity body.
3486
+ *
3487
+ * XHR request body is Document | XMLHttpRequestBodyInit | null | undefined
3488
+ */
3489
+ function _tryReadXHRBody({
3490
+ body,
3491
+ options,
3492
+ url
3493
+ }) {
3494
+ if (isNullish(body)) {
3495
+ return null;
3330
3496
  }
3331
- // Backwards-compatible aliases expected by replay/browser code
3332
- get_property(key) {
3333
- return this.persistence?.get_property(key);
3497
+ const {
3498
+ hostname,
3499
+ isHostDenied
3500
+ } = isHostOnDenyList(url, options);
3501
+ if (isHostDenied) {
3502
+ return hostname + ' is in deny list';
3334
3503
  }
3335
- set_property(key, value) {
3336
- this.persistence?.set_property(key, value);
3504
+ if (isString(body)) {
3505
+ return body;
3337
3506
  }
3338
- register_for_session(properties) {
3339
- // PostHogCore may expose registerForSession; call it if available
3340
- if (isFunction(this.registerForSession)) {
3341
- this.registerForSession(properties);
3342
- return;
3343
- }
3344
- // fallback: store properties in sessionPersistence
3345
- if (this.sessionPersistence) {
3346
- Object.keys(properties).forEach(k => this.sessionPersistence?.set_property(k, properties[k]));
3347
- }
3507
+ if (isDocument(body)) {
3508
+ return body.textContent;
3348
3509
  }
3349
- unregister_for_session(property) {
3350
- if (isFunction(this.unregisterForSession)) {
3351
- this.unregisterForSession(property);
3352
- return;
3353
- }
3354
- if (this.sessionPersistence) {
3355
- this.sessionPersistence.set_property(property, null);
3356
- }
3510
+ if (isFormData(body)) {
3511
+ return formDataToQuery(body);
3357
3512
  }
3358
- calculateEventProperties(eventName, eventProperties, timestamp, uuid, readOnly) {
3359
- if (!this.persistence || !this.sessionPersistence) {
3360
- return eventProperties;
3513
+ if (isObject(body)) {
3514
+ try {
3515
+ return JSON.stringify(body);
3516
+ } catch {
3517
+ return '[SessionReplay] Failed to stringify response object';
3518
+ }
3519
+ }
3520
+ return '[SessionReplay] Cannot read body of type ' + toString.call(body);
3521
+ }
3522
+ function initXhrObserver(cb, win, options) {
3523
+ if (!options.initiatorTypes.includes('xmlhttprequest')) {
3524
+ return () => {
3525
+ //
3526
+ };
3527
+ }
3528
+ const recordRequestHeaders = shouldRecordHeaders('request', options.recordHeaders);
3529
+ const recordResponseHeaders = shouldRecordHeaders('response', options.recordHeaders);
3530
+ const restorePatch = patch(win.XMLHttpRequest.prototype, 'open',
3531
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
3532
+ // @ts-ignore
3533
+ originalOpen => {
3534
+ return function (method, url, async = true, username, password) {
3535
+ // because this function is returned in its actual context `this` _is_ an XMLHttpRequest
3536
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
3537
+ // @ts-ignore
3538
+ const xhr = this;
3539
+ // check IE earlier than this, we only initialize if Request is present
3540
+ // eslint-disable-next-line compat/compat
3541
+ const req = new Request(url);
3542
+ const networkRequest = {};
3543
+ let start;
3544
+ let end;
3545
+ const requestHeaders = {};
3546
+ const originalSetRequestHeader = xhr.setRequestHeader.bind(xhr);
3547
+ xhr.setRequestHeader = (header, value) => {
3548
+ requestHeaders[header] = value;
3549
+ return originalSetRequestHeader(header, value);
3550
+ };
3551
+ if (recordRequestHeaders) {
3552
+ networkRequest.requestHeaders = requestHeaders;
3553
+ }
3554
+ const originalSend = xhr.send.bind(xhr);
3555
+ xhr.send = body => {
3556
+ if (shouldRecordBody({
3557
+ type: 'request',
3558
+ headers: requestHeaders,
3559
+ url,
3560
+ recordBody: options.recordBody
3561
+ })) {
3562
+ networkRequest.requestBody = _tryReadXHRBody({
3563
+ body,
3564
+ options,
3565
+ url
3566
+ });
3567
+ }
3568
+ start = win.performance.now();
3569
+ return originalSend(body);
3570
+ };
3571
+ const readyStateListener = () => {
3572
+ if (xhr.readyState !== xhr.DONE) {
3573
+ return;
3574
+ }
3575
+ // Clean up the listener immediately when done to prevent memory leaks
3576
+ xhr.removeEventListener('readystatechange', readyStateListener);
3577
+ end = win.performance.now();
3578
+ const responseHeaders = {};
3579
+ const rawHeaders = xhr.getAllResponseHeaders();
3580
+ const headers = rawHeaders.trim().split(/[\r\n]+/);
3581
+ headers.forEach(line => {
3582
+ const parts = line.split(': ');
3583
+ const header = parts.shift();
3584
+ const value = parts.join(': ');
3585
+ if (header) {
3586
+ responseHeaders[header] = value;
3587
+ }
3588
+ });
3589
+ if (recordResponseHeaders) {
3590
+ networkRequest.responseHeaders = responseHeaders;
3591
+ }
3592
+ if (shouldRecordBody({
3593
+ type: 'response',
3594
+ headers: responseHeaders,
3595
+ url,
3596
+ recordBody: options.recordBody
3597
+ })) {
3598
+ networkRequest.responseBody = _tryReadXHRBody({
3599
+ body: xhr.response,
3600
+ options,
3601
+ url
3602
+ });
3603
+ }
3604
+ getRequestPerformanceEntry(win, 'xmlhttprequest', req.url, start, end).then(entry => {
3605
+ const requests = prepareRequest({
3606
+ entry,
3607
+ method: method,
3608
+ status: xhr?.status,
3609
+ networkRequest,
3610
+ start,
3611
+ end,
3612
+ url: url.toString(),
3613
+ initiatorType: 'xmlhttprequest'
3614
+ });
3615
+ cb({
3616
+ requests
3617
+ });
3618
+ }).catch(() => {
3619
+ //
3620
+ });
3621
+ };
3622
+ // This is very tricky code, and making it passive won't bring many performance benefits,
3623
+ // so let's ignore the rule here.
3624
+ // eslint-disable-next-line posthog-js/no-add-event-listener
3625
+ xhr.addEventListener('readystatechange', readyStateListener);
3626
+ originalOpen.call(xhr, method, url, async, username, password);
3627
+ };
3628
+ });
3629
+ return () => {
3630
+ restorePatch();
3631
+ };
3632
+ }
3633
+ /**
3634
+ * Check if this PerformanceEntry is either a PerformanceResourceTiming or a PerformanceNavigationTiming
3635
+ * NB PerformanceNavigationTiming extends PerformanceResourceTiming
3636
+ * Here we don't care which interface it implements as both expose `serverTimings`
3637
+ */
3638
+ const exposesServerTiming = event => !isNull(event) && (event.entryType === 'navigation' || event.entryType === 'resource');
3639
+ function prepareRequest({
3640
+ entry,
3641
+ method,
3642
+ status,
3643
+ networkRequest,
3644
+ isInitial,
3645
+ start,
3646
+ end,
3647
+ url,
3648
+ initiatorType
3649
+ }) {
3650
+ start = entry ? entry.startTime : start;
3651
+ end = entry ? entry.responseEnd : end;
3652
+ // kudos to sentry javascript sdk for excellent background on why to use Date.now() here
3653
+ // https://github.com/getsentry/sentry-javascript/blob/e856e40b6e71a73252e788cd42b5260f81c9c88e/packages/utils/src/time.ts#L70
3654
+ // can't start observer if performance.now() is not available
3655
+ // eslint-disable-next-line compat/compat
3656
+ const timeOrigin = Math.floor(Date.now() - performance.now());
3657
+ // clickhouse can't ingest timestamps that are floats
3658
+ // (in this case representing fractions of a millisecond we don't care about anyway)
3659
+ // use timeOrigin if we really can't gather a start time
3660
+ const timestamp = Math.floor(timeOrigin + (start || 0));
3661
+ const entryJSON = entry ? entry.toJSON() : {
3662
+ name: url
3663
+ };
3664
+ const requests = [{
3665
+ ...entryJSON,
3666
+ startTime: isUndefined(start) ? undefined : Math.round(start),
3667
+ endTime: isUndefined(end) ? undefined : Math.round(end),
3668
+ timeOrigin,
3669
+ timestamp,
3670
+ method: method,
3671
+ initiatorType: initiatorType ? initiatorType : entry ? entry.initiatorType : undefined,
3672
+ status,
3673
+ requestHeaders: networkRequest.requestHeaders,
3674
+ requestBody: networkRequest.requestBody,
3675
+ responseHeaders: networkRequest.responseHeaders,
3676
+ responseBody: networkRequest.responseBody,
3677
+ isInitial
3678
+ }];
3679
+ if (exposesServerTiming(entry)) {
3680
+ for (const timing of entry.serverTiming || []) {
3681
+ requests.push({
3682
+ timeOrigin,
3683
+ timestamp,
3684
+ startTime: Math.round(entry.startTime),
3685
+ name: timing.name,
3686
+ duration: timing.duration,
3687
+ // the spec has a closed list of possible types
3688
+ // https://developer.mozilla.org/en-US/docs/Web/API/PerformanceEntry/entryType
3689
+ // but, we need to know this was a server timing so that we know to
3690
+ // match it to the appropriate navigation or resource timing
3691
+ // that matching will have to be on timestamp and $current_url
3692
+ entryType: 'serverTiming'
3693
+ });
3694
+ }
3695
+ }
3696
+ return requests;
3697
+ }
3698
+ const contentTypePrefixDenyList = ['video/', 'audio/'];
3699
+ function _checkForCannotReadResponseBody({
3700
+ r,
3701
+ options,
3702
+ url
3703
+ }) {
3704
+ if (r.headers.get('Transfer-Encoding') === 'chunked') {
3705
+ return 'Chunked Transfer-Encoding is not supported';
3706
+ }
3707
+ // `get` and `has` are case-insensitive
3708
+ // but return the header value with the casing that was supplied
3709
+ const contentType = r.headers.get('Content-Type')?.toLowerCase();
3710
+ const contentTypeIsDenied = contentTypePrefixDenyList.some(prefix => contentType?.startsWith(prefix));
3711
+ if (contentType && contentTypeIsDenied) {
3712
+ return `Content-Type ${contentType} is not supported`;
3713
+ }
3714
+ const {
3715
+ hostname,
3716
+ isHostDenied
3717
+ } = isHostOnDenyList(url, options);
3718
+ if (isHostDenied) {
3719
+ return hostname + ' is in deny list';
3720
+ }
3721
+ return null;
3722
+ }
3723
+ function _tryReadBody(r) {
3724
+ // there are now already multiple places where we're using Promise...
3725
+ // eslint-disable-next-line compat/compat
3726
+ return new Promise((resolve, reject) => {
3727
+ const timeout = setTimeout(() => resolve('[SessionReplay] Timeout while trying to read body'), 500);
3728
+ try {
3729
+ r.clone().text().then(txt => resolve(txt), reason => reject(reason)).finally(() => clearTimeout(timeout));
3730
+ } catch {
3731
+ clearTimeout(timeout);
3732
+ resolve('[SessionReplay] Failed to read body');
3733
+ }
3734
+ });
3735
+ }
3736
+ async function _tryReadRequestBody({
3737
+ r,
3738
+ options,
3739
+ url
3740
+ }) {
3741
+ const {
3742
+ hostname,
3743
+ isHostDenied
3744
+ } = isHostOnDenyList(url, options);
3745
+ if (isHostDenied) {
3746
+ return Promise.resolve(hostname + ' is in deny list');
3747
+ }
3748
+ return _tryReadBody(r);
3749
+ }
3750
+ async function _tryReadResponseBody({
3751
+ r,
3752
+ options,
3753
+ url
3754
+ }) {
3755
+ const cannotReadBodyReason = _checkForCannotReadResponseBody({
3756
+ r,
3757
+ options,
3758
+ url
3759
+ });
3760
+ if (!isNull(cannotReadBodyReason)) {
3761
+ return Promise.resolve(cannotReadBodyReason);
3762
+ }
3763
+ return _tryReadBody(r);
3764
+ }
3765
+ function initFetchObserver(cb, win, options) {
3766
+ if (!options.initiatorTypes.includes('fetch')) {
3767
+ return () => {
3768
+ //
3769
+ };
3770
+ }
3771
+ const recordRequestHeaders = shouldRecordHeaders('request', options.recordHeaders);
3772
+ const recordResponseHeaders = shouldRecordHeaders('response', options.recordHeaders);
3773
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
3774
+ // @ts-ignore
3775
+ const restorePatch = patch(win, 'fetch', originalFetch => {
3776
+ return async function (url, init) {
3777
+ // check IE earlier than this, we only initialize if Request is present
3778
+ // eslint-disable-next-line compat/compat
3779
+ const req = new Request(url, init);
3780
+ let res;
3781
+ const networkRequest = {};
3782
+ let start;
3783
+ let end;
3784
+ try {
3785
+ const requestHeaders = {};
3786
+ req.headers.forEach((value, header) => {
3787
+ requestHeaders[header] = value;
3788
+ });
3789
+ if (recordRequestHeaders) {
3790
+ networkRequest.requestHeaders = requestHeaders;
3791
+ }
3792
+ if (shouldRecordBody({
3793
+ type: 'request',
3794
+ headers: requestHeaders,
3795
+ url,
3796
+ recordBody: options.recordBody
3797
+ })) {
3798
+ networkRequest.requestBody = await _tryReadRequestBody({
3799
+ r: req,
3800
+ options,
3801
+ url
3802
+ });
3803
+ }
3804
+ start = win.performance.now();
3805
+ res = await originalFetch(req);
3806
+ end = win.performance.now();
3807
+ const responseHeaders = {};
3808
+ res.headers.forEach((value, header) => {
3809
+ responseHeaders[header] = value;
3810
+ });
3811
+ if (recordResponseHeaders) {
3812
+ networkRequest.responseHeaders = responseHeaders;
3813
+ }
3814
+ if (shouldRecordBody({
3815
+ type: 'response',
3816
+ headers: responseHeaders,
3817
+ url,
3818
+ recordBody: options.recordBody
3819
+ })) {
3820
+ networkRequest.responseBody = await _tryReadResponseBody({
3821
+ r: res,
3822
+ options,
3823
+ url
3824
+ });
3825
+ }
3826
+ return res;
3827
+ } finally {
3828
+ getRequestPerformanceEntry(win, 'fetch', req.url, start, end).then(entry => {
3829
+ const requests = prepareRequest({
3830
+ entry,
3831
+ method: req.method,
3832
+ status: res?.status,
3833
+ networkRequest,
3834
+ start,
3835
+ end,
3836
+ url: req.url,
3837
+ initiatorType: 'fetch'
3838
+ });
3839
+ cb({
3840
+ requests
3841
+ });
3842
+ }).catch(() => {
3843
+ //
3844
+ });
3845
+ }
3846
+ };
3847
+ });
3848
+ return () => {
3849
+ restorePatch();
3850
+ };
3851
+ }
3852
+ let initialisedHandler = null;
3853
+ function initNetworkObserver(callback, win,
3854
+ // top window or in an iframe
3855
+ options) {
3856
+ if (!('performance' in win)) {
3857
+ return () => {
3858
+ //
3859
+ };
3860
+ }
3861
+ if (initialisedHandler) {
3862
+ logger$1.warn('Network observer already initialised, doing nothing');
3863
+ return () => {
3864
+ // the first caller should already have this handler and will be responsible for teardown
3865
+ };
3866
+ }
3867
+ const networkOptions = options ? Object.assign({}, defaultNetworkOptions, options) : defaultNetworkOptions;
3868
+ const cb = data => {
3869
+ const requests = [];
3870
+ data.requests.forEach(request => {
3871
+ const maskedRequest = networkOptions.maskRequestFn(request);
3872
+ if (maskedRequest) {
3873
+ requests.push(maskedRequest);
3874
+ }
3875
+ });
3876
+ if (requests.length > 0) {
3877
+ callback({
3878
+ ...data,
3879
+ requests
3880
+ });
3881
+ }
3882
+ };
3883
+ const performanceObserver = initPerformanceObserver(cb, win, networkOptions);
3884
+ // only wrap fetch and xhr if headers or body are being recorded
3885
+ let xhrObserver = () => {};
3886
+ let fetchObserver = () => {};
3887
+ if (networkOptions.recordHeaders || networkOptions.recordBody) {
3888
+ xhrObserver = initXhrObserver(cb, win, networkOptions);
3889
+ fetchObserver = initFetchObserver(cb, win, networkOptions);
3890
+ }
3891
+ const teardown = () => {
3892
+ performanceObserver();
3893
+ xhrObserver();
3894
+ fetchObserver();
3895
+ // allow future observers to initialize after cleanup
3896
+ initialisedHandler = null;
3897
+ };
3898
+ initialisedHandler = teardown;
3899
+ return teardown;
3900
+ }
3901
+ // use the plugin name so that when this functionality is adopted into rrweb
3902
+ // we can remove this plugin and use the core functionality with the same data
3903
+ const NETWORK_PLUGIN_NAME = 'rrweb/network@1';
3904
+ // TODO how should this be typed?
3905
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
3906
+ // @ts-ignore
3907
+ const getRecordNetworkPlugin = options => {
3908
+ return {
3909
+ name: NETWORK_PLUGIN_NAME,
3910
+ observer: initNetworkObserver,
3911
+ options: options
3912
+ };
3913
+ };
3914
+ // rrweb/networ@1 ends
3915
+
3916
+ // Use a safe global target (prefer `win`, fallback to globalThis)
3917
+ const _target = win ?? globalThis;
3918
+ _target.__PosthogExtensions__ = _target.__PosthogExtensions__ || {};
3919
+ // Expose rrweb.record under the same contract
3920
+ _target.__PosthogExtensions__.rrweb = _target.__PosthogExtensions__.rrweb || {
3921
+ record: record
3922
+ };
3923
+ // Provide initSessionRecording if not present — return a new LazyLoadedSessionRecording when called
3924
+ _target.__PosthogExtensions__.initSessionRecording = _target.__PosthogExtensions__.initSessionRecording || (instance => {
3925
+ return new LazyLoadedSessionRecording(instance);
3926
+ });
3927
+ // Provide a no-op loadExternalDependency that calls the callback immediately (since rrweb is bundled)
3928
+ _target.__PosthogExtensions__.loadExternalDependency = _target.__PosthogExtensions__.loadExternalDependency || ((instance, scriptName, cb) => {
3929
+ if (cb) cb(undefined);
3930
+ });
3931
+ // Provide rrwebPlugins object with network plugin factory if not present
3932
+ _target.__PosthogExtensions__.rrwebPlugins = _target.__PosthogExtensions__.rrwebPlugins || {};
3933
+ _target.__PosthogExtensions__.rrwebPlugins.getRecordNetworkPlugin = _target.__PosthogExtensions__.rrwebPlugins.getRecordNetworkPlugin || (() => getRecordNetworkPlugin);
3934
+
3935
+ // Type definitions copied from @rrweb/types@2.0.0-alpha.17 and rrweb-snapshot@2.0.0-alpha.17
3936
+ // Both packages are MIT licensed: https://github.com/rrweb-io/rrweb
3937
+ //
3938
+ // These types are copied here to avoid requiring users to install peer dependencies
3939
+ // solely for TypeScript type information.
3940
+ //
3941
+ // Original sources:
3942
+ // - @rrweb/types: https://github.com/rrweb-io/rrweb/tree/main/packages/@rrweb/types
3943
+ // - rrweb-snapshot: https://github.com/rrweb-io/rrweb/tree/main/packages/rrweb-snapshot
3944
+ var NodeType;
3945
+ (function (NodeType) {
3946
+ NodeType[NodeType["Document"] = 0] = "Document";
3947
+ NodeType[NodeType["DocumentType"] = 1] = "DocumentType";
3948
+ NodeType[NodeType["Element"] = 2] = "Element";
3949
+ NodeType[NodeType["Text"] = 3] = "Text";
3950
+ NodeType[NodeType["CDATA"] = 4] = "CDATA";
3951
+ NodeType[NodeType["Comment"] = 5] = "Comment";
3952
+ })(NodeType || (NodeType = {}));
3953
+ var EventType;
3954
+ (function (EventType) {
3955
+ EventType[EventType["DomContentLoaded"] = 0] = "DomContentLoaded";
3956
+ EventType[EventType["Load"] = 1] = "Load";
3957
+ EventType[EventType["FullSnapshot"] = 2] = "FullSnapshot";
3958
+ EventType[EventType["IncrementalSnapshot"] = 3] = "IncrementalSnapshot";
3959
+ EventType[EventType["Meta"] = 4] = "Meta";
3960
+ EventType[EventType["Custom"] = 5] = "Custom";
3961
+ EventType[EventType["Plugin"] = 6] = "Plugin";
3962
+ })(EventType || (EventType = {}));
3963
+ var IncrementalSource;
3964
+ (function (IncrementalSource) {
3965
+ IncrementalSource[IncrementalSource["Mutation"] = 0] = "Mutation";
3966
+ IncrementalSource[IncrementalSource["MouseMove"] = 1] = "MouseMove";
3967
+ IncrementalSource[IncrementalSource["MouseInteraction"] = 2] = "MouseInteraction";
3968
+ IncrementalSource[IncrementalSource["Scroll"] = 3] = "Scroll";
3969
+ IncrementalSource[IncrementalSource["ViewportResize"] = 4] = "ViewportResize";
3970
+ IncrementalSource[IncrementalSource["Input"] = 5] = "Input";
3971
+ IncrementalSource[IncrementalSource["TouchMove"] = 6] = "TouchMove";
3972
+ IncrementalSource[IncrementalSource["MediaInteraction"] = 7] = "MediaInteraction";
3973
+ IncrementalSource[IncrementalSource["StyleSheetRule"] = 8] = "StyleSheetRule";
3974
+ IncrementalSource[IncrementalSource["CanvasMutation"] = 9] = "CanvasMutation";
3975
+ IncrementalSource[IncrementalSource["Font"] = 10] = "Font";
3976
+ IncrementalSource[IncrementalSource["Log"] = 11] = "Log";
3977
+ IncrementalSource[IncrementalSource["Drag"] = 12] = "Drag";
3978
+ IncrementalSource[IncrementalSource["StyleDeclaration"] = 13] = "StyleDeclaration";
3979
+ IncrementalSource[IncrementalSource["Selection"] = 14] = "Selection";
3980
+ IncrementalSource[IncrementalSource["AdoptedStyleSheet"] = 15] = "AdoptedStyleSheet";
3981
+ IncrementalSource[IncrementalSource["CustomElement"] = 16] = "CustomElement";
3982
+ })(IncrementalSource || (IncrementalSource = {}));
3983
+ var MouseInteractions;
3984
+ (function (MouseInteractions) {
3985
+ MouseInteractions[MouseInteractions["MouseUp"] = 0] = "MouseUp";
3986
+ MouseInteractions[MouseInteractions["MouseDown"] = 1] = "MouseDown";
3987
+ MouseInteractions[MouseInteractions["Click"] = 2] = "Click";
3988
+ MouseInteractions[MouseInteractions["ContextMenu"] = 3] = "ContextMenu";
3989
+ MouseInteractions[MouseInteractions["DblClick"] = 4] = "DblClick";
3990
+ MouseInteractions[MouseInteractions["Focus"] = 5] = "Focus";
3991
+ MouseInteractions[MouseInteractions["Blur"] = 6] = "Blur";
3992
+ MouseInteractions[MouseInteractions["TouchStart"] = 7] = "TouchStart";
3993
+ MouseInteractions[MouseInteractions["TouchMove_Departed"] = 8] = "TouchMove_Departed";
3994
+ MouseInteractions[MouseInteractions["TouchEnd"] = 9] = "TouchEnd";
3995
+ MouseInteractions[MouseInteractions["TouchCancel"] = 10] = "TouchCancel";
3996
+ })(MouseInteractions || (MouseInteractions = {}));
3997
+ var PointerTypes;
3998
+ (function (PointerTypes) {
3999
+ PointerTypes[PointerTypes["Mouse"] = 0] = "Mouse";
4000
+ PointerTypes[PointerTypes["Pen"] = 1] = "Pen";
4001
+ PointerTypes[PointerTypes["Touch"] = 2] = "Touch";
4002
+ })(PointerTypes || (PointerTypes = {}));
4003
+ var MediaInteractions;
4004
+ (function (MediaInteractions) {
4005
+ MediaInteractions[MediaInteractions["Play"] = 0] = "Play";
4006
+ MediaInteractions[MediaInteractions["Pause"] = 1] = "Pause";
4007
+ MediaInteractions[MediaInteractions["Seeked"] = 2] = "Seeked";
4008
+ MediaInteractions[MediaInteractions["VolumeChange"] = 3] = "VolumeChange";
4009
+ MediaInteractions[MediaInteractions["RateChange"] = 4] = "RateChange";
4010
+ })(MediaInteractions || (MediaInteractions = {}));
4011
+ var CanvasContext;
4012
+ (function (CanvasContext) {
4013
+ CanvasContext[CanvasContext["2D"] = 0] = "2D";
4014
+ CanvasContext[CanvasContext["WebGL"] = 1] = "WebGL";
4015
+ CanvasContext[CanvasContext["WebGL2"] = 2] = "WebGL2";
4016
+ })(CanvasContext || (CanvasContext = {}));
4017
+
4018
+ const DISABLED = 'disabled';
4019
+ const SAMPLED = 'sampled';
4020
+ const ACTIVE = 'active';
4021
+ const BUFFERING = 'buffering';
4022
+ const PAUSED = 'paused';
4023
+ const LAZY_LOADING = 'lazy_loading';
4024
+ const TRIGGER = 'trigger';
4025
+ const TRIGGER_ACTIVATED = TRIGGER + '_activated';
4026
+ const TRIGGER_PENDING = TRIGGER + '_pending';
4027
+ const TRIGGER_DISABLED = TRIGGER + '_' + DISABLED;
4028
+ function sessionRecordingUrlTriggerMatches(url, triggers) {
4029
+ return triggers.some(trigger => {
4030
+ switch (trigger.matching) {
4031
+ case 'regex':
4032
+ return new RegExp(trigger.url).test(url);
4033
+ default:
4034
+ return false;
4035
+ }
4036
+ });
4037
+ }
4038
+ class OrTriggerMatching {
4039
+ constructor(_matchers) {
4040
+ this._matchers = _matchers;
4041
+ }
4042
+ triggerStatus(sessionId) {
4043
+ const statuses = this._matchers.map(m => m.triggerStatus(sessionId));
4044
+ if (statuses.includes(TRIGGER_ACTIVATED)) {
4045
+ return TRIGGER_ACTIVATED;
4046
+ }
4047
+ if (statuses.includes(TRIGGER_PENDING)) {
4048
+ return TRIGGER_PENDING;
4049
+ }
4050
+ return TRIGGER_DISABLED;
4051
+ }
4052
+ stop() {
4053
+ this._matchers.forEach(m => m.stop());
4054
+ }
4055
+ }
4056
+ class AndTriggerMatching {
4057
+ constructor(_matchers) {
4058
+ this._matchers = _matchers;
4059
+ }
4060
+ triggerStatus(sessionId) {
4061
+ const statuses = new Set();
4062
+ for (const matcher of this._matchers) {
4063
+ statuses.add(matcher.triggerStatus(sessionId));
4064
+ }
4065
+ // trigger_disabled means no config
4066
+ statuses.delete(TRIGGER_DISABLED);
4067
+ switch (statuses.size) {
4068
+ case 0:
4069
+ return TRIGGER_DISABLED;
4070
+ case 1:
4071
+ return Array.from(statuses)[0];
4072
+ default:
4073
+ return TRIGGER_PENDING;
4074
+ }
4075
+ }
4076
+ stop() {
4077
+ this._matchers.forEach(m => m.stop());
4078
+ }
4079
+ }
4080
+ class PendingTriggerMatching {
4081
+ triggerStatus() {
4082
+ return TRIGGER_PENDING;
4083
+ }
4084
+ stop() {
4085
+ // no-op
4086
+ }
4087
+ }
4088
+ const isEagerLoadedConfig = x => {
4089
+ return 'sessionRecording' in x;
4090
+ };
4091
+ class URLTriggerMatching {
4092
+ constructor(_instance) {
4093
+ this._instance = _instance;
4094
+ this._urlTriggers = [];
4095
+ this._urlBlocklist = [];
4096
+ this.urlBlocked = false;
4097
+ }
4098
+ onConfig(config) {
4099
+ this._urlTriggers = (isEagerLoadedConfig(config) ? isObject(config.sessionRecording) ? config.sessionRecording?.urlTriggers : [] : config?.urlTriggers) || [];
4100
+ this._urlBlocklist = (isEagerLoadedConfig(config) ? isObject(config.sessionRecording) ? config.sessionRecording?.urlBlocklist : [] : config?.urlBlocklist) || [];
4101
+ }
4102
+ /**
4103
+ * @deprecated Use onConfig instead
4104
+ */
4105
+ onRemoteConfig(response) {
4106
+ this.onConfig(response);
4107
+ }
4108
+ _urlTriggerStatus(sessionId) {
4109
+ if (this._urlTriggers.length === 0) {
4110
+ return TRIGGER_DISABLED;
4111
+ }
4112
+ const currentTriggerSession = this._instance?.get_property(SESSION_RECORDING_URL_TRIGGER_ACTIVATED_SESSION);
4113
+ return currentTriggerSession === sessionId ? TRIGGER_ACTIVATED : TRIGGER_PENDING;
4114
+ }
4115
+ triggerStatus(sessionId) {
4116
+ const urlTriggerStatus = this._urlTriggerStatus(sessionId);
4117
+ const eitherIsActivated = urlTriggerStatus === TRIGGER_ACTIVATED;
4118
+ const eitherIsPending = urlTriggerStatus === TRIGGER_PENDING;
4119
+ const result = eitherIsActivated ? TRIGGER_ACTIVATED : eitherIsPending ? TRIGGER_PENDING : TRIGGER_DISABLED;
4120
+ this._instance.registerForSession({
4121
+ $sdk_debug_replay_url_trigger_status: result
4122
+ });
4123
+ return result;
4124
+ }
4125
+ checkUrlTriggerConditions(onPause, onResume, onActivate) {
4126
+ if (typeof win === 'undefined' || !win.location.href) {
4127
+ return;
4128
+ }
4129
+ const url = win.location.href;
4130
+ const wasBlocked = this.urlBlocked;
4131
+ const isNowBlocked = sessionRecordingUrlTriggerMatches(url, this._urlBlocklist);
4132
+ if (wasBlocked && isNowBlocked) {
4133
+ // if the url is blocked and was already blocked, do nothing
4134
+ return;
4135
+ } else if (isNowBlocked && !wasBlocked) {
4136
+ onPause();
4137
+ } else if (!isNowBlocked && wasBlocked) {
4138
+ onResume();
4139
+ }
4140
+ if (sessionRecordingUrlTriggerMatches(url, this._urlTriggers)) {
4141
+ onActivate('url');
4142
+ }
4143
+ }
4144
+ stop() {
4145
+ // no-op
4146
+ }
4147
+ }
4148
+ class LinkedFlagMatching {
4149
+ constructor(_instance) {
4150
+ this._instance = _instance;
4151
+ this.linkedFlag = null;
4152
+ this.linkedFlagSeen = false;
4153
+ this._flagListenerCleanup = () => {};
4154
+ }
4155
+ triggerStatus() {
4156
+ let result = TRIGGER_PENDING;
4157
+ if (isNullish(this.linkedFlag)) {
4158
+ result = TRIGGER_DISABLED;
4159
+ }
4160
+ if (this.linkedFlagSeen) {
4161
+ result = TRIGGER_ACTIVATED;
4162
+ }
4163
+ this._instance.registerForSession({
4164
+ $sdk_debug_replay_linked_flag_trigger_status: result
4165
+ });
4166
+ return result;
4167
+ }
4168
+ onConfig(config, onStarted) {
4169
+ this.linkedFlag = (isEagerLoadedConfig(config) ? isObject(config.sessionRecording) ? config.sessionRecording?.linkedFlag : null : config?.linkedFlag) || null;
4170
+ if (!isNullish(this.linkedFlag) && !this.linkedFlagSeen) {
4171
+ const linkedFlag = isString(this.linkedFlag) ? this.linkedFlag : this.linkedFlag.flag;
4172
+ const linkedVariant = isString(this.linkedFlag) ? null : this.linkedFlag.variant;
4173
+ this._flagListenerCleanup = this._instance.onFeatureFlags(flags => {
4174
+ const flagIsPresent = isObject(flags) && linkedFlag in flags;
4175
+ let linkedFlagMatches = false;
4176
+ if (flagIsPresent) {
4177
+ const variantForFlagKey = flags[linkedFlag];
4178
+ if (isBoolean(variantForFlagKey)) {
4179
+ linkedFlagMatches = variantForFlagKey === true;
4180
+ } else if (linkedVariant) {
4181
+ linkedFlagMatches = variantForFlagKey === linkedVariant;
4182
+ } else {
4183
+ // then this is a variant flag and we want to match any string
4184
+ linkedFlagMatches = !!variantForFlagKey;
4185
+ }
4186
+ }
4187
+ this.linkedFlagSeen = linkedFlagMatches;
4188
+ if (linkedFlagMatches) {
4189
+ onStarted(linkedFlag, linkedVariant);
4190
+ }
4191
+ });
4192
+ }
4193
+ }
4194
+ /**
4195
+ * @deprecated Use onConfig instead
4196
+ */
4197
+ onRemoteConfig(response, onStarted) {
4198
+ this.onConfig(response, onStarted);
4199
+ }
4200
+ stop() {
4201
+ this._flagListenerCleanup();
4202
+ }
4203
+ }
4204
+ class EventTriggerMatching {
4205
+ constructor(_instance) {
4206
+ this._instance = _instance;
4207
+ this._eventTriggers = [];
4208
+ }
4209
+ onConfig(config) {
4210
+ this._eventTriggers = (isEagerLoadedConfig(config) ? isObject(config.sessionRecording) ? config.sessionRecording?.eventTriggers : [] : config?.eventTriggers) || [];
4211
+ }
4212
+ /**
4213
+ * @deprecated Use onConfig instead
4214
+ */
4215
+ onRemoteConfig(response) {
4216
+ this.onConfig(response);
4217
+ }
4218
+ _eventTriggerStatus(sessionId) {
4219
+ if (this._eventTriggers.length === 0) {
4220
+ return TRIGGER_DISABLED;
4221
+ }
4222
+ const currentTriggerSession = this._instance?.get_property(SESSION_RECORDING_EVENT_TRIGGER_ACTIVATED_SESSION);
4223
+ return currentTriggerSession === sessionId ? TRIGGER_ACTIVATED : TRIGGER_PENDING;
4224
+ }
4225
+ triggerStatus(sessionId) {
4226
+ const eventTriggerStatus = this._eventTriggerStatus(sessionId);
4227
+ const result = eventTriggerStatus === TRIGGER_ACTIVATED ? TRIGGER_ACTIVATED : eventTriggerStatus === TRIGGER_PENDING ? TRIGGER_PENDING : TRIGGER_DISABLED;
4228
+ this._instance.registerForSession({
4229
+ $sdk_debug_replay_event_trigger_status: result
4230
+ });
4231
+ return result;
4232
+ }
4233
+ stop() {
4234
+ // no-op
4235
+ }
4236
+ }
4237
+ // we need a no-op matcher before we can lazy-load the other matches, since all matchers wait on remote config anyway
4238
+ function nullMatchSessionRecordingStatus(triggersStatus) {
4239
+ if (!triggersStatus.isRecordingEnabled) {
4240
+ return DISABLED;
4241
+ }
4242
+ return BUFFERING;
4243
+ }
4244
+ function anyMatchSessionRecordingStatus(triggersStatus) {
4245
+ if (!triggersStatus.receivedFlags) {
4246
+ return BUFFERING;
4247
+ }
4248
+ if (!triggersStatus.isRecordingEnabled) {
4249
+ return DISABLED;
4250
+ }
4251
+ if (triggersStatus.urlTriggerMatching.urlBlocked) {
4252
+ return PAUSED;
4253
+ }
4254
+ const sampledActive = triggersStatus.isSampled === true;
4255
+ const triggerMatches = new OrTriggerMatching([triggersStatus.eventTriggerMatching, triggersStatus.urlTriggerMatching, triggersStatus.linkedFlagMatching]).triggerStatus(triggersStatus.sessionId);
4256
+ if (sampledActive) {
4257
+ return SAMPLED;
4258
+ }
4259
+ if (triggerMatches === TRIGGER_ACTIVATED) {
4260
+ return ACTIVE;
4261
+ }
4262
+ if (triggerMatches === TRIGGER_PENDING) {
4263
+ // even if sampled active is false, we should still be buffering
4264
+ // since a pending trigger could override it
4265
+ return BUFFERING;
4266
+ }
4267
+ // if sampling is set and the session is already decided to not be sampled
4268
+ // then we should never be active
4269
+ if (triggersStatus.isSampled === false) {
4270
+ return DISABLED;
4271
+ }
4272
+ return ACTIVE;
4273
+ }
4274
+ function allMatchSessionRecordingStatus(triggersStatus) {
4275
+ if (!triggersStatus.receivedFlags) {
4276
+ return BUFFERING;
4277
+ }
4278
+ if (!triggersStatus.isRecordingEnabled) {
4279
+ return DISABLED;
4280
+ }
4281
+ if (triggersStatus.urlTriggerMatching.urlBlocked) {
4282
+ return PAUSED;
4283
+ }
4284
+ const andTriggerMatch = new AndTriggerMatching([triggersStatus.eventTriggerMatching, triggersStatus.urlTriggerMatching, triggersStatus.linkedFlagMatching]);
4285
+ const currentTriggerStatus = andTriggerMatch.triggerStatus(triggersStatus.sessionId);
4286
+ const hasTriggersConfigured = currentTriggerStatus !== TRIGGER_DISABLED;
4287
+ const hasSamplingConfigured = isBoolean(triggersStatus.isSampled);
4288
+ if (hasTriggersConfigured && currentTriggerStatus === TRIGGER_PENDING) {
4289
+ return BUFFERING;
4290
+ }
4291
+ if (hasTriggersConfigured && currentTriggerStatus === TRIGGER_DISABLED) {
4292
+ return DISABLED;
4293
+ }
4294
+ // sampling can't ever cause buffering, it's always determined right away or not configured
4295
+ if (hasSamplingConfigured && !triggersStatus.isSampled) {
4296
+ return DISABLED;
4297
+ }
4298
+ // If sampling is configured and set to true, return sampled
4299
+ if (triggersStatus.isSampled === true) {
4300
+ return SAMPLED;
4301
+ }
4302
+ return ACTIVE;
4303
+ }
4304
+
4305
+ // taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value#circular_references
4306
+ function circularReferenceReplacer() {
4307
+ const ancestors = [];
4308
+ return function (_key, value) {
4309
+ if (isObject(value)) {
4310
+ // `this` is the object that value is contained in,
4311
+ // i.e., its direct parent.
4312
+ while (ancestors.length > 0 && ancestors[ancestors.length - 1] !== this) {
4313
+ ancestors.pop();
4314
+ }
4315
+ if (ancestors.includes(value)) {
4316
+ return '[Circular]';
4317
+ }
4318
+ ancestors.push(value);
4319
+ return value;
4320
+ } else {
4321
+ return value;
4322
+ }
4323
+ };
4324
+ }
4325
+ function estimateSize(sizeable) {
4326
+ return JSON.stringify(sizeable, circularReferenceReplacer())?.length || 0;
4327
+ }
4328
+ const INCREMENTAL_SNAPSHOT_EVENT_TYPE = 3;
4329
+ const PLUGIN_EVENT_TYPE = 6;
4330
+ const MUTATION_SOURCE_TYPE = 0;
4331
+ const CONSOLE_LOG_PLUGIN_NAME = 'rrweb/console@1'; // The name of the rr-web plugin that emits console logs
4332
+ // Console logs can be really large. This function truncates large logs
4333
+ // It's a simple function that just truncates long strings.
4334
+ // TODO: Ideally this function would have better handling of objects + lists,
4335
+ // so they could still be rendered in a pretty way after truncation.
4336
+ function truncateLargeConsoleLogs(_event) {
4337
+ const event = _event;
4338
+ const MAX_STRING_SIZE = 2000; // Maximum number of characters allowed in a string
4339
+ const MAX_STRINGS_PER_LOG = 10; // A log can consist of multiple strings (e.g. consol.log('string1', 'string2'))
4340
+ if (event && isObject(event) && event.type === PLUGIN_EVENT_TYPE && isObject(event.data) && event.data.plugin === CONSOLE_LOG_PLUGIN_NAME) {
4341
+ // Note: event.data.payload.payload comes from rr-web, and is an array of strings
4342
+ if (event.data.payload.payload.length > MAX_STRINGS_PER_LOG) {
4343
+ event.data.payload.payload = event.data.payload.payload.slice(0, MAX_STRINGS_PER_LOG);
4344
+ event.data.payload.payload.push('...[truncated]');
4345
+ }
4346
+ const updatedPayload = [];
4347
+ for (let i = 0; i < event.data.payload.payload.length; i++) {
4348
+ if (event.data.payload.payload[i] &&
4349
+ // Value can be null
4350
+ event.data.payload.payload[i].length > MAX_STRING_SIZE) {
4351
+ updatedPayload.push(event.data.payload.payload[i].slice(0, MAX_STRING_SIZE) + '...[truncated]');
4352
+ } else {
4353
+ updatedPayload.push(event.data.payload.payload[i]);
4354
+ }
4355
+ }
4356
+ event.data.payload.payload = updatedPayload;
4357
+ // Return original type
4358
+ return _event;
4359
+ }
4360
+ return _event;
4361
+ }
4362
+
4363
+ class MutationThrottler {
4364
+ constructor(_rrweb, _options = {}) {
4365
+ this._rrweb = _rrweb;
4366
+ this._options = _options;
4367
+ this._loggedTracker = {};
4368
+ this._onNodeRateLimited = key => {
4369
+ if (!this._loggedTracker[key]) {
4370
+ this._loggedTracker[key] = true;
4371
+ const node = this._getNode(key);
4372
+ this._options.onBlockedNode?.(key, node);
4373
+ }
4374
+ };
4375
+ this._getNodeOrRelevantParent = id => {
4376
+ // For some nodes we know they are part of a larger tree such as an SVG.
4377
+ // For those we want to block the entire node, not just the specific attribute
4378
+ const node = this._getNode(id);
4379
+ // Check if the node is an Element and then find the closest parent that is an SVG
4380
+ if (node?.nodeName !== 'svg' && node instanceof Element) {
4381
+ const closestSVG = node.closest('svg');
4382
+ if (closestSVG) {
4383
+ return [this._rrweb.mirror.getId(closestSVG), closestSVG];
4384
+ }
4385
+ }
4386
+ return [id, node];
4387
+ };
4388
+ this._getNode = id => this._rrweb.mirror.getNode(id);
4389
+ this._numberOfChanges = data => {
4390
+ return (data.removes?.length ?? 0) + (data.attributes?.length ?? 0) + (data.texts?.length ?? 0) + (data.adds?.length ?? 0);
4391
+ };
4392
+ this.throttleMutations = event => {
4393
+ if (event.type !== INCREMENTAL_SNAPSHOT_EVENT_TYPE || event.data.source !== MUTATION_SOURCE_TYPE) {
4394
+ return event;
4395
+ }
4396
+ const data = event.data;
4397
+ const initialMutationCount = this._numberOfChanges(data);
4398
+ if (data.attributes) {
4399
+ // Most problematic mutations come from attrs where the style or minor properties are changed rapidly
4400
+ data.attributes = data.attributes.filter(attr => {
4401
+ const [nodeId] = this._getNodeOrRelevantParent(attr.id);
4402
+ const isRateLimited = this._rateLimiter.consumeRateLimit(nodeId);
4403
+ if (isRateLimited) {
4404
+ return false;
4405
+ }
4406
+ return attr;
4407
+ });
4408
+ }
4409
+ // Check if every part of the mutation is empty in which case there is nothing to do
4410
+ const mutationCount = this._numberOfChanges(data);
4411
+ if (mutationCount === 0 && initialMutationCount !== mutationCount) {
4412
+ // If we have modified the mutation count and the remaining count is 0, then we don't need the event.
4413
+ return;
4414
+ }
4415
+ return event;
4416
+ };
4417
+ const configuredBucketSize = this._options.bucketSize ?? 100;
4418
+ const effectiveBucketSize = Math.max(configuredBucketSize - 1, 1);
4419
+ this._rateLimiter = new BucketedRateLimiter({
4420
+ bucketSize: effectiveBucketSize,
4421
+ refillRate: this._options.refillRate ?? 10,
4422
+ refillInterval: 1000,
4423
+ // one second
4424
+ _onBucketRateLimited: this._onNodeRateLimited,
4425
+ _logger: logger$2
4426
+ });
4427
+ }
4428
+ reset() {
4429
+ this._loggedTracker = {};
4430
+ }
4431
+ stop() {
4432
+ this._rateLimiter.stop();
4433
+ this.reset();
4434
+ }
4435
+ }
4436
+
4437
+ function simpleHash(str) {
4438
+ let hash = 0;
4439
+ for (let i = 0; i < str.length; i++) {
4440
+ hash = (hash << 5) - hash + str.charCodeAt(i); // (hash * 31) + char code
4441
+ hash |= 0; // Convert to 32bit integer
4442
+ }
4443
+ return Math.abs(hash);
4444
+ }
4445
+ /*
4446
+ * receives percent as a number between 0 and 1
4447
+ */
4448
+ function sampleOnProperty(prop, percent) {
4449
+ return simpleHash(prop) % 100 < clampToRange(percent * 100, 0, 100, logger$2);
4450
+ }
4451
+
4452
+ const BASE_ENDPOINT = '/s/';
4453
+ const DEFAULT_CANVAS_QUALITY = 0.4;
4454
+ const DEFAULT_CANVAS_FPS = 4;
4455
+ const MAX_CANVAS_FPS = 12;
4456
+ const MAX_CANVAS_QUALITY = 1;
4457
+ const TWO_SECONDS = 2000;
4458
+ const ONE_KB = 1024;
4459
+ const ONE_MINUTE = 1000 * 60;
4460
+ const FIVE_MINUTES = ONE_MINUTE * 5;
4461
+ const RECORDING_IDLE_THRESHOLD_MS = FIVE_MINUTES;
4462
+ const RECORDING_MAX_EVENT_SIZE = ONE_KB * ONE_KB * 0.9; // ~1mb (with some wiggle room)
4463
+ const RECORDING_BUFFER_TIMEOUT = 2000; // 2 seconds
4464
+ const SESSION_RECORDING_BATCH_KEY = 'recordings';
4465
+ const LOGGER_PREFIX$1 = '[SessionRecording]';
4466
+ const logger = createLogger(LOGGER_PREFIX$1);
4467
+ const ACTIVE_SOURCES = [IncrementalSource.MouseMove, IncrementalSource.MouseInteraction, IncrementalSource.Scroll, IncrementalSource.ViewportResize, IncrementalSource.Input, IncrementalSource.TouchMove, IncrementalSource.MediaInteraction, IncrementalSource.Drag];
4468
+ const newQueuedEvent = rrwebMethod => ({
4469
+ rrwebMethod,
4470
+ enqueuedAt: Date.now(),
4471
+ attempt: 1
4472
+ });
4473
+ function getRRWebRecord() {
4474
+ try {
4475
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4476
+ const ext = globalThis.__PosthogExtensions__;
4477
+ if (ext && ext.rrweb && ext.rrweb.record) {
4478
+ return ext.rrweb.record;
4479
+ }
4480
+ } catch {
4481
+ // ignore
4482
+ }
4483
+ return record;
4484
+ }
4485
+ function gzipToString(data) {
4486
+ return strFromU8(gzipSync(strToU8(JSON.stringify(data))), true);
4487
+ }
4488
+ /**
4489
+ * rrweb's packer takes an event and returns a string or the reverse on `unpack`.
4490
+ * but we want to be able to inspect metadata during ingestion.
4491
+ * and don't want to compress the entire event,
4492
+ * so we have a custom packer that only compresses part of some events
4493
+ */
4494
+ function compressEvent(event) {
4495
+ try {
4496
+ if (event.type === EventType.FullSnapshot) {
4497
+ return {
4498
+ ...event,
4499
+ data: gzipToString(event.data),
4500
+ cv: '2024-10'
4501
+ };
4502
+ }
4503
+ if (event.type === EventType.IncrementalSnapshot && event.data.source === IncrementalSource.Mutation) {
4504
+ return {
4505
+ ...event,
4506
+ cv: '2024-10',
4507
+ data: {
4508
+ ...event.data,
4509
+ texts: gzipToString(event.data.texts),
4510
+ attributes: gzipToString(event.data.attributes),
4511
+ removes: gzipToString(event.data.removes),
4512
+ adds: gzipToString(event.data.adds)
4513
+ }
4514
+ };
4515
+ }
4516
+ if (event.type === EventType.IncrementalSnapshot && event.data.source === IncrementalSource.StyleSheetRule) {
4517
+ return {
4518
+ ...event,
4519
+ cv: '2024-10',
4520
+ data: {
4521
+ ...event.data,
4522
+ adds: event.data.adds ? gzipToString(event.data.adds) : undefined,
4523
+ removes: event.data.removes ? gzipToString(event.data.removes) : undefined
4524
+ }
4525
+ };
4526
+ }
4527
+ } catch (e) {
4528
+ logger.error('could not compress event - will use uncompressed event', e);
4529
+ }
4530
+ return event;
4531
+ }
4532
+ function isSessionIdleEvent(e) {
4533
+ return e.type === EventType.Custom && e.data.tag === 'sessionIdle';
4534
+ }
4535
+ /** When we put the recording into a paused state, we add a custom event.
4536
+ * However, in the paused state, events are dropped and never make it to the buffer,
4537
+ * so we need to manually let this one through */
4538
+ function isRecordingPausedEvent(e) {
4539
+ return e.type === EventType.Custom && e.data.tag === 'recording paused';
4540
+ }
4541
+ const SEVEN_MEGABYTES = 1024 * 1024 * 7 * 0.9; // ~7mb (with some wiggle room)
4542
+ // recursively splits large buffers into smaller ones
4543
+ // uses a pretty high size limit to avoid splitting too much
4544
+ function splitBuffer(buffer, sizeLimit = SEVEN_MEGABYTES) {
4545
+ if (buffer.size >= sizeLimit && buffer.data.length > 1) {
4546
+ const half = Math.floor(buffer.data.length / 2);
4547
+ const firstHalf = buffer.data.slice(0, half);
4548
+ const secondHalf = buffer.data.slice(half);
4549
+ return [splitBuffer({
4550
+ size: estimateSize(firstHalf),
4551
+ data: firstHalf,
4552
+ sessionId: buffer.sessionId,
4553
+ windowId: buffer.windowId
4554
+ }), splitBuffer({
4555
+ size: estimateSize(secondHalf),
4556
+ data: secondHalf,
4557
+ sessionId: buffer.sessionId,
4558
+ windowId: buffer.windowId
4559
+ })].flatMap(x => x);
4560
+ } else {
4561
+ return [buffer];
4562
+ }
4563
+ }
4564
+ class LazyLoadedSessionRecording {
4565
+ get sessionId() {
4566
+ return this._sessionId;
4567
+ }
4568
+ get _sessionManager() {
4569
+ if (!this._instance.sessionManager) {
4570
+ throw new Error(LOGGER_PREFIX$1 + ' must be started with a valid sessionManager.');
4571
+ }
4572
+ return this._instance.sessionManager;
4573
+ }
4574
+ get _sessionIdleThresholdMilliseconds() {
4575
+ return this._instance.config.session_recording?.session_idle_threshold_ms || RECORDING_IDLE_THRESHOLD_MS;
4576
+ }
4577
+ get _isSampled() {
4578
+ const currentValue = this._instance.get_property(SESSION_RECORDING_IS_SAMPLED);
4579
+ // originally we would store `true` or `false` or nothing,
4580
+ // but that would mean sometimes we would carry on recording on session id change
4581
+ return isBoolean(currentValue) ? currentValue : isString(currentValue) ? currentValue === this.sessionId : null;
4582
+ }
4583
+ get _sampleRate() {
4584
+ const rate = this._remoteConfig?.sampleRate;
4585
+ return isNumber(rate) ? rate : null;
4586
+ }
4587
+ get _minimumDuration() {
4588
+ const duration = this._remoteConfig?.minimumDurationMilliseconds;
4589
+ return isNumber(duration) ? duration : null;
4590
+ }
4591
+ constructor(_instance) {
4592
+ this._instance = _instance;
4593
+ this._endpoint = BASE_ENDPOINT;
4594
+ /**
4595
+ * Util to help developers working on this feature manually override
4596
+ */
4597
+ this._forceAllowLocalhostNetworkCapture = false;
4598
+ this._stopRrweb = undefined;
4599
+ this._lastActivityTimestamp = Date.now();
4600
+ /**
4601
+ * and a queue - that contains rrweb events that we want to send to rrweb, but rrweb wasn't able to accept them yet
4602
+ */
4603
+ this._queuedRRWebEvents = [];
4604
+ this._isIdle = 'unknown';
4605
+ // we need to be able to check the state of the event and url triggers separately
4606
+ // as we make some decisions based on them without referencing LinkedFlag etc
4607
+ this._triggerMatching = new PendingTriggerMatching();
4608
+ this._removePageViewCaptureHook = undefined;
4609
+ this._removeEventTriggerCaptureHook = undefined;
4610
+ this._statusMatcher = nullMatchSessionRecordingStatus;
4611
+ this._onSessionIdListener = undefined;
4612
+ this._onSessionIdleResetForcedListener = undefined;
4613
+ this._samplingSessionListener = undefined;
4614
+ this._forceIdleSessionIdListener = undefined;
4615
+ this._onSessionIdCallback = (sessionId, windowId, changeReason) => {
4616
+ if (changeReason) {
4617
+ this._tryAddCustomEvent('$session_id_change', {
4618
+ sessionId,
4619
+ windowId,
4620
+ changeReason
4621
+ });
4622
+ this._clearConditionalRecordingPersistence();
4623
+ if (!this._stopRrweb) {
4624
+ this.start('session_id_changed');
4625
+ }
4626
+ if (isNumber(this._sampleRate) && isNullish(this._samplingSessionListener)) {
4627
+ this._makeSamplingDecision(sessionId);
4628
+ }
4629
+ }
4630
+ };
4631
+ this._onBeforeUnload = () => {
4632
+ this._flushBuffer();
4633
+ };
4634
+ this._onOffline = () => {
4635
+ this._tryAddCustomEvent('browser offline', {});
4636
+ };
4637
+ this._onOnline = () => {
4638
+ this._tryAddCustomEvent('browser online', {});
4639
+ };
4640
+ this._onVisibilityChange = () => {
4641
+ if (document?.visibilityState) {
4642
+ const label = 'window ' + document.visibilityState;
4643
+ this._tryAddCustomEvent(label, {});
4644
+ }
4645
+ };
4646
+ // we know there's a sessionManager, so don't need to start without a session id
4647
+ const {
4648
+ sessionId,
4649
+ windowId
4650
+ } = this._sessionManager.checkAndGetSessionAndWindowId();
4651
+ this._sessionId = sessionId;
4652
+ this._windowId = windowId;
4653
+ this._linkedFlagMatching = new LinkedFlagMatching(this._instance);
4654
+ this._urlTriggerMatching = new URLTriggerMatching(this._instance);
4655
+ this._eventTriggerMatching = new EventTriggerMatching(this._instance);
4656
+ this._buffer = this._clearBuffer();
4657
+ if (this._sessionIdleThresholdMilliseconds >= this._sessionManager.sessionTimeoutMs) {
4658
+ logger.warn(`session_idle_threshold_ms (${this._sessionIdleThresholdMilliseconds}) is greater than the session timeout (${this._sessionManager.sessionTimeoutMs}). Session will never be detected as idle`);
4659
+ }
4660
+ }
4661
+ get _masking() {
4662
+ const masking_server_side = this._remoteConfig?.masking;
4663
+ const masking_client_side = {
4664
+ maskAllInputs: this._instance.config.session_recording?.maskAllInputs,
4665
+ maskTextSelector: this._instance.config.session_recording?.maskTextSelector,
4666
+ blockSelector: this._instance.config.session_recording?.blockSelector
4667
+ };
4668
+ const maskAllInputs = masking_client_side?.maskAllInputs ?? masking_server_side?.maskAllInputs;
4669
+ const maskTextSelector = masking_client_side?.maskTextSelector ?? masking_server_side?.maskTextSelector;
4670
+ const blockSelector = masking_client_side?.blockSelector ?? masking_server_side?.blockSelector;
4671
+ return !isUndefined(maskAllInputs) || !isUndefined(maskTextSelector) || !isUndefined(blockSelector) ? {
4672
+ maskAllInputs: maskAllInputs ?? true,
4673
+ maskTextSelector,
4674
+ blockSelector
4675
+ } : undefined;
4676
+ }
4677
+ get _canvasRecording() {
4678
+ const canvasRecording_client_side = this._instance.config.session_recording?.captureCanvas;
4679
+ const canvasRecording_server_side = this._remoteConfig?.canvasRecording;
4680
+ const enabled = canvasRecording_client_side?.recordCanvas ?? canvasRecording_server_side?.enabled ?? false;
4681
+ const fps = canvasRecording_client_side?.canvasFps ?? canvasRecording_server_side?.fps ?? DEFAULT_CANVAS_FPS;
4682
+ let quality = canvasRecording_client_side?.canvasQuality ?? canvasRecording_server_side?.quality ?? DEFAULT_CANVAS_QUALITY;
4683
+ if (typeof quality === 'string') {
4684
+ const parsed = parseFloat(quality);
4685
+ quality = isNaN(parsed) ? 0.4 : parsed;
4686
+ }
4687
+ return {
4688
+ enabled,
4689
+ fps: clampToRange(fps, 0, MAX_CANVAS_FPS, createLogger('canvas recording fps'), DEFAULT_CANVAS_FPS),
4690
+ quality: clampToRange(quality, 0, MAX_CANVAS_QUALITY, createLogger('canvas recording quality'), DEFAULT_CANVAS_QUALITY)
4691
+ };
4692
+ }
4693
+ get _isConsoleLogCaptureEnabled() {
4694
+ const enabled_server_side = !!this._remoteConfig?.consoleLogRecordingEnabled;
4695
+ const enabled_client_side = this._instance.config.enable_recording_console_log;
4696
+ return enabled_client_side ?? enabled_server_side;
4697
+ }
4698
+ // network payload capture config has three parts
4699
+ // each can be configured server side or client side
4700
+ get _networkPayloadCapture() {
4701
+ const networkPayloadCapture_server_side = this._remoteConfig?.networkPayloadCapture;
4702
+ const networkPayloadCapture_client_side = {
4703
+ recordHeaders: this._instance.config.session_recording?.recordHeaders,
4704
+ recordBody: this._instance.config.session_recording?.recordBody
4705
+ };
4706
+ const headersOptIn = networkPayloadCapture_client_side?.recordHeaders === true;
4707
+ const bodyOptIn = networkPayloadCapture_client_side?.recordBody === true;
4708
+ const clientPerformanceConfig = this._instance.config.capture_performance;
4709
+ const clientPerformanceOptIn = isObject(clientPerformanceConfig) ? !!clientPerformanceConfig.network_timing : !!clientPerformanceConfig;
4710
+ const serverAllowsHeaders = networkPayloadCapture_server_side?.recordHeaders ?? true;
4711
+ const serverAllowsBody = networkPayloadCapture_server_side?.recordBody ?? true;
4712
+ const capturePerfResponse = networkPayloadCapture_server_side?.capturePerformance;
4713
+ const serverAllowsPerformance = (() => {
4714
+ if (isObject(capturePerfResponse)) {
4715
+ return !!capturePerfResponse.network_timing;
4716
+ }
4717
+ return capturePerfResponse ?? true;
4718
+ })();
4719
+ const headersEnabled = headersOptIn && serverAllowsHeaders;
4720
+ const bodyEnabled = bodyOptIn && serverAllowsBody;
4721
+ const networkTimingEnabled = clientPerformanceOptIn && serverAllowsPerformance;
4722
+ if (!headersEnabled && !bodyEnabled && !networkTimingEnabled) {
4723
+ return undefined;
4724
+ }
4725
+ return {
4726
+ recordHeaders: headersEnabled,
4727
+ recordBody: bodyEnabled,
4728
+ recordPerformance: networkTimingEnabled,
4729
+ payloadHostDenyList: networkPayloadCapture_server_side?.payloadHostDenyList
4730
+ };
4731
+ }
4732
+ _gatherRRWebPlugins() {
4733
+ const plugins = [];
4734
+ if (this._isConsoleLogCaptureEnabled) {
4735
+ logger.info('Console log capture requested but console plugin is not bundled in this build yet.');
4736
+ }
4737
+ if (this._networkPayloadCapture) {
4738
+ const canRecordNetwork = !isLocalhost() || this._forceAllowLocalhostNetworkCapture;
4739
+ if (canRecordNetwork) {
4740
+ plugins.push(getRecordNetworkPlugin(buildNetworkRequestOptions(this._instance.config, this._networkPayloadCapture)));
4741
+ } else {
4742
+ logger.info('NetworkCapture not started because we are on localhost.');
4743
+ }
4744
+ }
4745
+ return plugins;
4746
+ }
4747
+ _maskUrl(url) {
4748
+ const userSessionRecordingOptions = this._instance.config.session_recording || {};
4749
+ if (userSessionRecordingOptions.maskNetworkRequestFn) {
4750
+ let networkRequest = {
4751
+ url
4752
+ };
4753
+ // TODO we should deprecate this and use the same function for this masking and the rrweb/network plugin
4754
+ // TODO or deprecate this and provide a new clearer name so this would be `maskURLPerformanceFn` or similar
4755
+ networkRequest = userSessionRecordingOptions.maskNetworkRequestFn(networkRequest);
4756
+ return networkRequest?.url;
4757
+ }
4758
+ return url;
4759
+ }
4760
+ _tryRRWebMethod(queuedRRWebEvent) {
4761
+ try {
4762
+ queuedRRWebEvent.rrwebMethod();
4763
+ return true;
4764
+ } catch (e) {
4765
+ // Sometimes a race can occur where the recorder is not fully started yet
4766
+ if (this._queuedRRWebEvents.length < 10) {
4767
+ this._queuedRRWebEvents.push({
4768
+ enqueuedAt: queuedRRWebEvent.enqueuedAt || Date.now(),
4769
+ attempt: queuedRRWebEvent.attempt + 1,
4770
+ rrwebMethod: queuedRRWebEvent.rrwebMethod
4771
+ });
4772
+ } else {
4773
+ logger.warn('could not emit queued rrweb event.', e, queuedRRWebEvent);
4774
+ }
4775
+ return false;
4776
+ }
4777
+ }
4778
+ _tryAddCustomEvent(tag, payload) {
4779
+ return this._tryRRWebMethod(newQueuedEvent(() => getRRWebRecord().addCustomEvent(tag, payload)));
4780
+ }
4781
+ _pageViewFallBack() {
4782
+ try {
4783
+ if (this._instance.config.capture_pageview || !win) {
4784
+ return;
4785
+ }
4786
+ // Strip hash parameters from URL since they often aren't helpful
4787
+ // Use URL constructor for proper parsing to handle edge cases
4788
+ // recording doesn't run in IE11, so we don't need compat here
4789
+ // eslint-disable-next-line compat/compat
4790
+ const url = new URL(win.location.href);
4791
+ const hrefWithoutHash = url.origin + url.pathname + url.search;
4792
+ const currentUrl = this._maskUrl(hrefWithoutHash);
4793
+ if (this._lastHref !== currentUrl) {
4794
+ this._lastHref = currentUrl;
4795
+ this._tryAddCustomEvent('$url_changed', {
4796
+ href: currentUrl
4797
+ });
4798
+ }
4799
+ } catch {
4800
+ // If URL processing fails, don't capture anything
4801
+ }
4802
+ }
4803
+ _processQueuedEvents() {
4804
+ if (this._queuedRRWebEvents.length) {
4805
+ // if rrweb isn't ready to accept events earlier, then we queued them up.
4806
+ // now that `emit` has been called rrweb should be ready to accept them.
4807
+ // so, before we process this event, we try our queued events _once_ each
4808
+ // we don't want to risk queuing more things and never exiting this loop!
4809
+ // if they fail here, they'll be pushed into a new queue
4810
+ // and tried on the next loop.
4811
+ // there is a risk of this queue growing in an uncontrolled manner.
4812
+ // so its length is limited elsewhere
4813
+ // for now this is to help us ensure we can capture events that happen
4814
+ // and try to identify more about when it is failing
4815
+ const itemsToProcess = [...this._queuedRRWebEvents];
4816
+ this._queuedRRWebEvents = [];
4817
+ itemsToProcess.forEach(queuedRRWebEvent => {
4818
+ if (Date.now() - queuedRRWebEvent.enqueuedAt <= TWO_SECONDS) {
4819
+ this._tryRRWebMethod(queuedRRWebEvent);
4820
+ }
4821
+ });
4822
+ }
4823
+ }
4824
+ _tryTakeFullSnapshot() {
4825
+ return this._tryRRWebMethod(newQueuedEvent(() => getRRWebRecord().takeFullSnapshot()));
4826
+ }
4827
+ get _fullSnapshotIntervalMillis() {
4828
+ if (this._triggerMatching.triggerStatus(this.sessionId) === TRIGGER_PENDING && !['sampled', 'active'].includes(this.status)) {
4829
+ return ONE_MINUTE;
4830
+ }
4831
+ return this._instance.config.session_recording?.full_snapshot_interval_millis ?? FIVE_MINUTES;
4832
+ }
4833
+ _scheduleFullSnapshot() {
4834
+ if (this._fullSnapshotTimer) {
4835
+ clearInterval(this._fullSnapshotTimer);
4836
+ }
4837
+ // we don't schedule snapshots while idle
4838
+ if (this._isIdle === true) {
4839
+ return;
4840
+ }
4841
+ const interval = this._fullSnapshotIntervalMillis;
4842
+ if (!interval) {
4843
+ return;
4844
+ }
4845
+ this._fullSnapshotTimer = setInterval(() => {
4846
+ this._tryTakeFullSnapshot();
4847
+ }, interval);
4848
+ }
4849
+ _pauseRecording() {
4850
+ // we check _urlBlocked not status, since more than one thing can affect status
4851
+ if (this._urlTriggerMatching.urlBlocked) {
4852
+ return;
4853
+ }
4854
+ // we can't flush the buffer here since someone might be starting on a blocked page.
4855
+ // and we need to be sure that we don't record that page,
4856
+ // so we might not get the below custom event, but events will report the paused status.
4857
+ // which will allow debugging of sessions that start on blocked pages
4858
+ this._urlTriggerMatching.urlBlocked = true;
4859
+ // Clear the snapshot timer since we don't want new snapshots while paused
4860
+ clearInterval(this._fullSnapshotTimer);
4861
+ logger.info('recording paused due to URL blocker');
4862
+ this._tryAddCustomEvent('recording paused', {
4863
+ reason: 'url blocker'
4864
+ });
4865
+ }
4866
+ _resumeRecording() {
4867
+ // we check _urlBlocked not status, since more than one thing can affect status
4868
+ if (!this._urlTriggerMatching.urlBlocked) {
4869
+ return;
4870
+ }
4871
+ this._urlTriggerMatching.urlBlocked = false;
4872
+ this._tryTakeFullSnapshot();
4873
+ this._scheduleFullSnapshot();
4874
+ this._tryAddCustomEvent('recording resumed', {
4875
+ reason: 'left blocked url'
4876
+ });
4877
+ logger.info('recording resumed');
4878
+ }
4879
+ _activateTrigger(triggerType) {
4880
+ if (this._triggerMatching.triggerStatus(this.sessionId) === TRIGGER_PENDING) {
4881
+ // status is stored separately for URL and event triggers
4882
+ this._instance?.persistence?.register({
4883
+ [triggerType === 'url' ? SESSION_RECORDING_URL_TRIGGER_ACTIVATED_SESSION : SESSION_RECORDING_EVENT_TRIGGER_ACTIVATED_SESSION]: this._sessionId
4884
+ });
4885
+ this._flushBuffer();
4886
+ this._reportStarted(triggerType + '_trigger_matched');
4887
+ }
4888
+ }
4889
+ get isStarted() {
4890
+ return !!this._stopRrweb;
4891
+ }
4892
+ get _remoteConfig() {
4893
+ const persistedConfig = this._instance.get_property(SESSION_RECORDING_REMOTE_CONFIG);
4894
+ if (!persistedConfig) {
4895
+ return undefined;
4896
+ }
4897
+ const parsedConfig = isObject(persistedConfig) ? persistedConfig : JSON.parse(persistedConfig);
4898
+ return parsedConfig;
4899
+ }
4900
+ start(startReason) {
4901
+ const config = this._remoteConfig;
4902
+ if (!config) {
4903
+ logger.info('remote config must be stored in persistence before recording can start');
4904
+ return;
4905
+ }
4906
+ // We want to ensure the sessionManager is reset if necessary on loading the recorder
4907
+ this._sessionManager.checkAndGetSessionAndWindowId();
4908
+ if (config?.endpoint) {
4909
+ this._endpoint = config?.endpoint;
4910
+ }
4911
+ if (config?.triggerMatchType === 'any') {
4912
+ this._statusMatcher = anyMatchSessionRecordingStatus;
4913
+ this._triggerMatching = new OrTriggerMatching([this._eventTriggerMatching, this._urlTriggerMatching]);
4914
+ } else {
4915
+ // either the setting is "ALL"
4916
+ // or we default to the most restrictive
4917
+ this._statusMatcher = allMatchSessionRecordingStatus;
4918
+ this._triggerMatching = new AndTriggerMatching([this._eventTriggerMatching, this._urlTriggerMatching]);
4919
+ }
4920
+ this._instance.registerForSession({
4921
+ $sdk_debug_replay_remote_trigger_matching_config: config?.triggerMatchType ?? null
4922
+ });
4923
+ this._urlTriggerMatching.onConfig(config);
4924
+ this._eventTriggerMatching.onConfig(config);
4925
+ this._removeEventTriggerCaptureHook?.();
4926
+ this._addEventTriggerListener();
4927
+ this._linkedFlagMatching.onConfig(config, (flag, variant) => {
4928
+ this._reportStarted('linked_flag_matched', {
4929
+ flag,
4930
+ variant
4931
+ });
4932
+ });
4933
+ this._makeSamplingDecision(this.sessionId);
4934
+ this._startRecorder();
4935
+ // calling addEventListener multiple times is safe and will not add duplicates
4936
+ addEventListener(win, 'beforeunload', this._onBeforeUnload);
4937
+ addEventListener(win, 'offline', this._onOffline);
4938
+ addEventListener(win, 'online', this._onOnline);
4939
+ addEventListener(win, 'visibilitychange', this._onVisibilityChange);
4940
+ if (!this._onSessionIdListener) {
4941
+ this._onSessionIdListener = this._sessionManager.onSessionId(this._onSessionIdCallback);
4942
+ }
4943
+ if (!this._onSessionIdleResetForcedListener) {
4944
+ this._onSessionIdleResetForcedListener = this._sessionManager.on('forcedIdleReset', () => {
4945
+ // a session was forced to reset due to idle timeout and lack of activity
4946
+ this._clearConditionalRecordingPersistence();
4947
+ this._isIdle = 'unknown';
4948
+ this.stop();
4949
+ // then we want a session id listener to restart the recording when a new session starts
4950
+ this._forceIdleSessionIdListener = this._sessionManager.onSessionId((sessionId, windowId, changeReason) => {
4951
+ // this should first unregister itself
4952
+ this._forceIdleSessionIdListener?.();
4953
+ this._forceIdleSessionIdListener = undefined;
4954
+ this._onSessionIdCallback(sessionId, windowId, changeReason);
4955
+ });
4956
+ });
4957
+ }
4958
+ if (isNullish(this._removePageViewCaptureHook)) {
4959
+ // :TRICKY: rrweb does not capture navigation within SPA-s, so hook into our $pageview events to get access to all events.
4960
+ // Dropping the initial event is fine (it's always captured by rrweb).
4961
+ this._removePageViewCaptureHook = this._instance.on('eventCaptured', event => {
4962
+ // If anything could go wrong here,
4963
+ // it has the potential to block the main loop,
4964
+ // so we catch all errors.
4965
+ try {
4966
+ if (event.event === '$pageview') {
4967
+ const href = event?.properties.$current_url ? this._maskUrl(event?.properties.$current_url) : '';
4968
+ if (!href) {
4969
+ return;
4970
+ }
4971
+ this._tryAddCustomEvent('$pageview', {
4972
+ href
4973
+ });
4974
+ }
4975
+ } catch (e) {
4976
+ logger.error('Could not add $pageview to rrweb session', e);
4977
+ }
4978
+ });
4979
+ }
4980
+ if (this.status === ACTIVE) {
4981
+ this._reportStarted(startReason || 'recording_initialized');
4982
+ }
4983
+ }
4984
+ stop() {
4985
+ win?.removeEventListener('beforeunload', this._onBeforeUnload);
4986
+ win?.removeEventListener('offline', this._onOffline);
4987
+ win?.removeEventListener('online', this._onOnline);
4988
+ win?.removeEventListener('visibilitychange', this._onVisibilityChange);
4989
+ this._clearBuffer();
4990
+ clearInterval(this._fullSnapshotTimer);
4991
+ this._clearFlushBufferTimer();
4992
+ this._removePageViewCaptureHook?.();
4993
+ this._removePageViewCaptureHook = undefined;
4994
+ this._removeEventTriggerCaptureHook?.();
4995
+ this._removeEventTriggerCaptureHook = undefined;
4996
+ this._onSessionIdListener?.();
4997
+ this._onSessionIdListener = undefined;
4998
+ this._onSessionIdleResetForcedListener?.();
4999
+ this._onSessionIdleResetForcedListener = undefined;
5000
+ this._samplingSessionListener?.();
5001
+ this._samplingSessionListener = undefined;
5002
+ this._forceIdleSessionIdListener?.();
5003
+ this._forceIdleSessionIdListener = undefined;
5004
+ this._eventTriggerMatching.stop();
5005
+ this._urlTriggerMatching.stop();
5006
+ this._linkedFlagMatching.stop();
5007
+ this._mutationThrottler?.stop();
5008
+ // Clear any queued rrweb events to prevent memory leaks from closures
5009
+ this._queuedRRWebEvents = [];
5010
+ this._stopRrweb?.();
5011
+ this._stopRrweb = undefined;
5012
+ logger.info('stopped');
5013
+ }
5014
+ onRRwebEmit(rawEvent) {
5015
+ this._processQueuedEvents();
5016
+ if (!rawEvent || !isObject(rawEvent)) {
5017
+ return;
5018
+ }
5019
+ if (rawEvent.type === EventType.Meta) {
5020
+ const href = this._maskUrl(rawEvent.data.href);
5021
+ this._lastHref = href;
5022
+ if (!href) {
5023
+ return;
5024
+ }
5025
+ rawEvent.data.href = href;
5026
+ } else {
5027
+ this._pageViewFallBack();
5028
+ }
5029
+ // Check if the URL matches any trigger patterns
5030
+ this._urlTriggerMatching.checkUrlTriggerConditions(() => this._pauseRecording(), () => this._resumeRecording(), triggerType => this._activateTrigger(triggerType));
5031
+ // always have to check if the URL is blocked really early,
5032
+ // or you risk getting stuck in a loop
5033
+ if (this._urlTriggerMatching.urlBlocked && !isRecordingPausedEvent(rawEvent)) {
5034
+ return;
5035
+ }
5036
+ // we're processing a full snapshot, so we should reset the timer
5037
+ if (rawEvent.type === EventType.FullSnapshot) {
5038
+ this._scheduleFullSnapshot();
5039
+ // Full snapshots reset rrweb's node IDs, so clear any logged node tracking
5040
+ this._mutationThrottler?.reset();
5041
+ }
5042
+ // Clear the buffer if waiting for a trigger and only keep data from after the current full snapshot
5043
+ // we always start trigger pending so need to wait for flags before we know if we're really pending
5044
+ if (rawEvent.type === EventType.FullSnapshot && this._triggerMatching.triggerStatus(this.sessionId) === TRIGGER_PENDING) {
5045
+ this._clearBufferBeforeMostRecentMeta();
5046
+ }
5047
+ const throttledEvent = this._mutationThrottler ? this._mutationThrottler.throttleMutations(rawEvent) : rawEvent;
5048
+ if (!throttledEvent) {
5049
+ return;
5050
+ }
5051
+ // TODO: Re-add ensureMaxMessageSize once we are confident in it
5052
+ const event = truncateLargeConsoleLogs(throttledEvent);
5053
+ this._updateWindowAndSessionIds(event);
5054
+ // When in an idle state we keep recording but don't capture the events,
5055
+ // we don't want to return early if idle is 'unknown'
5056
+ if (this._isIdle === true && !isSessionIdleEvent(event)) {
5057
+ return;
5058
+ }
5059
+ if (isSessionIdleEvent(event)) {
5060
+ // session idle events have a timestamp when rrweb sees them
5061
+ // which can artificially lengthen a session
5062
+ // we know when we detected it based on the payload and can correct the timestamp
5063
+ const payload = event.data.payload;
5064
+ if (payload) {
5065
+ const lastActivity = payload.lastActivityTimestamp;
5066
+ const threshold = payload.threshold;
5067
+ event.timestamp = lastActivity + threshold;
5068
+ }
5069
+ }
5070
+ const eventToSend = this._instance.config.session_recording?.compress_events ?? true ? compressEvent(event) : event;
5071
+ const size = estimateSize(eventToSend);
5072
+ const properties = {
5073
+ $snapshot_bytes: size,
5074
+ $snapshot_data: eventToSend,
5075
+ $session_id: this._sessionId,
5076
+ $window_id: this._windowId
5077
+ };
5078
+ if (this.status === DISABLED) {
5079
+ this._clearBuffer();
5080
+ return;
5081
+ }
5082
+ this._captureSnapshotBuffered(properties);
5083
+ }
5084
+ get status() {
5085
+ return this._statusMatcher({
5086
+ // can't get here without recording being enabled...
5087
+ receivedFlags: true,
5088
+ isRecordingEnabled: true,
5089
+ // things that do still vary
5090
+ isSampled: this._isSampled,
5091
+ urlTriggerMatching: this._urlTriggerMatching,
5092
+ eventTriggerMatching: this._eventTriggerMatching,
5093
+ linkedFlagMatching: this._linkedFlagMatching,
5094
+ sessionId: this.sessionId
5095
+ });
5096
+ }
5097
+ log(message, level = 'log') {
5098
+ this._instance.sessionRecording?.onRRwebEmit({
5099
+ type: 6,
5100
+ data: {
5101
+ plugin: 'rrweb/console@1',
5102
+ payload: {
5103
+ level,
5104
+ trace: [],
5105
+ // Even though it is a string, we stringify it as that's what rrweb expects
5106
+ payload: [JSON.stringify(message)]
5107
+ }
5108
+ },
5109
+ timestamp: Date.now()
5110
+ });
5111
+ }
5112
+ overrideLinkedFlag() {
5113
+ this._linkedFlagMatching.linkedFlagSeen = true;
5114
+ this._tryTakeFullSnapshot();
5115
+ this._reportStarted('linked_flag_overridden');
5116
+ }
5117
+ /**
5118
+ * this ignores the sampling config and (if other conditions are met) causes capture to start
5119
+ *
5120
+ * It is not usual to call this directly,
5121
+ * instead call `posthog.startSessionRecording({sampling: true})`
5122
+ * */
5123
+ overrideSampling() {
5124
+ this._instance.persistence?.register({
5125
+ // short-circuits the `makeSamplingDecision` function in the session recording module
5126
+ [SESSION_RECORDING_IS_SAMPLED]: this.sessionId
5127
+ });
5128
+ this._tryTakeFullSnapshot();
5129
+ this._reportStarted('sampling_overridden');
5130
+ }
5131
+ /**
5132
+ * this ignores the URL/Event trigger config and (if other conditions are met) causes capture to start
5133
+ *
5134
+ * It is not usual to call this directly,
5135
+ * instead call `posthog.startSessionRecording({trigger: 'url' | 'event'})`
5136
+ * */
5137
+ overrideTrigger(triggerType) {
5138
+ this._activateTrigger(triggerType);
5139
+ }
5140
+ _clearFlushBufferTimer() {
5141
+ if (this._flushBufferTimer) {
5142
+ clearTimeout(this._flushBufferTimer);
5143
+ this._flushBufferTimer = undefined;
5144
+ }
5145
+ }
5146
+ _flushBuffer() {
5147
+ this._clearFlushBufferTimer();
5148
+ const minimumDuration = this._minimumDuration;
5149
+ const sessionDuration = this._sessionDuration;
5150
+ // if we have old data in the buffer but the session has rotated, then the
5151
+ // session duration might be negative. In that case we want to flush the buffer
5152
+ const isPositiveSessionDuration = isNumber(sessionDuration) && sessionDuration >= 0;
5153
+ const isBelowMinimumDuration = isNumber(minimumDuration) && isPositiveSessionDuration && sessionDuration < minimumDuration;
5154
+ if (this.status === BUFFERING || this.status === PAUSED || this.status === DISABLED || isBelowMinimumDuration) {
5155
+ this._flushBufferTimer = setTimeout(() => {
5156
+ this._flushBuffer();
5157
+ }, RECORDING_BUFFER_TIMEOUT);
5158
+ return this._buffer;
5159
+ }
5160
+ if (this._buffer.data.length > 0) {
5161
+ const snapshotEvents = splitBuffer(this._buffer);
5162
+ snapshotEvents.forEach(snapshotBuffer => {
5163
+ this._captureSnapshot({
5164
+ $snapshot_bytes: snapshotBuffer.size,
5165
+ $snapshot_data: snapshotBuffer.data,
5166
+ $session_id: snapshotBuffer.sessionId,
5167
+ $window_id: snapshotBuffer.windowId,
5168
+ $lib: 'web',
5169
+ $lib_version: Config.LIB_VERSION
5170
+ });
5171
+ });
5172
+ }
5173
+ // buffer is empty, we clear it in case the session id has changed
5174
+ return this._clearBuffer();
5175
+ }
5176
+ _captureSnapshotBuffered(properties) {
5177
+ const additionalBytes = 2 + (this._buffer?.data.length || 0); // 2 bytes for the array brackets and 1 byte for each comma
5178
+ if (!this._isIdle && (
5179
+ // we never want to flush when idle
5180
+ this._buffer.size + properties.$snapshot_bytes + additionalBytes > RECORDING_MAX_EVENT_SIZE || this._buffer.sessionId !== this._sessionId)) {
5181
+ this._buffer = this._flushBuffer();
5182
+ }
5183
+ this._buffer.size += properties.$snapshot_bytes;
5184
+ this._buffer.data.push(properties.$snapshot_data);
5185
+ if (!this._flushBufferTimer && !this._isIdle) {
5186
+ this._flushBufferTimer = setTimeout(() => {
5187
+ this._flushBuffer();
5188
+ }, RECORDING_BUFFER_TIMEOUT);
5189
+ }
5190
+ }
5191
+ _captureSnapshot(properties) {
5192
+ // :TRICKY: Make sure we batch these requests, use a custom endpoint and don't truncate the strings.
5193
+ this._instance.capture('$snapshot', properties, {
5194
+ _url: this._instance.requestRouter.endpointFor('api', this._endpoint),
5195
+ _noTruncate: true,
5196
+ _batchKey: SESSION_RECORDING_BATCH_KEY,
5197
+ skip_client_rate_limiting: true
5198
+ });
5199
+ }
5200
+ _snapshotUrl() {
5201
+ const host = this._instance.config.host || '';
5202
+ try {
5203
+ // eslint-disable-next-line compat/compat
5204
+ return new URL(this._endpoint, host).href;
5205
+ } catch {
5206
+ const normalizedHost = host.endsWith('/') ? host.slice(0, -1) : host;
5207
+ const normalizedEndpoint = this._endpoint.startsWith('/') ? this._endpoint.slice(1) : this._endpoint;
5208
+ return `${normalizedHost}/${normalizedEndpoint}`;
5209
+ }
5210
+ }
5211
+ get _sessionDuration() {
5212
+ const mostRecentSnapshot = this._buffer?.data[this._buffer?.data.length - 1];
5213
+ const {
5214
+ sessionStartTimestamp
5215
+ } = this._sessionManager.checkAndGetSessionAndWindowId(true);
5216
+ return mostRecentSnapshot ? mostRecentSnapshot.timestamp - sessionStartTimestamp : null;
5217
+ }
5218
+ _clearBufferBeforeMostRecentMeta() {
5219
+ if (!this._buffer || this._buffer.data.length === 0) {
5220
+ return this._clearBuffer();
5221
+ }
5222
+ // Find the last meta event index by iterating backwards
5223
+ let lastMetaIndex = -1;
5224
+ for (let i = this._buffer.data.length - 1; i >= 0; i--) {
5225
+ if (this._buffer.data[i].type === EventType.Meta) {
5226
+ lastMetaIndex = i;
5227
+ break;
5228
+ }
5229
+ }
5230
+ if (lastMetaIndex >= 0) {
5231
+ this._buffer.data = this._buffer.data.slice(lastMetaIndex);
5232
+ this._buffer.size = this._buffer.data.reduce((acc, curr) => acc + estimateSize(curr), 0);
5233
+ return this._buffer;
5234
+ } else {
5235
+ return this._clearBuffer();
5236
+ }
5237
+ }
5238
+ _clearBuffer() {
5239
+ this._buffer = {
5240
+ size: 0,
5241
+ data: [],
5242
+ sessionId: this._sessionId,
5243
+ windowId: this._windowId
5244
+ };
5245
+ return this._buffer;
5246
+ }
5247
+ _reportStarted(startReason, tagPayload) {
5248
+ this._instance.registerForSession({
5249
+ $session_recording_start_reason: startReason
5250
+ });
5251
+ logger.info(startReason.replace('_', ' '), tagPayload);
5252
+ if (!includes(['recording_initialized', 'session_id_changed'], startReason)) {
5253
+ this._tryAddCustomEvent(startReason, tagPayload);
5254
+ }
5255
+ }
5256
+ _isInteractiveEvent(event) {
5257
+ return event.type === INCREMENTAL_SNAPSHOT_EVENT_TYPE && ACTIVE_SOURCES.indexOf(event.data?.source) !== -1;
5258
+ }
5259
+ _updateWindowAndSessionIds(event) {
5260
+ // Some recording events are triggered by non-user events (e.g. "X minutes ago" text updating on the screen).
5261
+ // We don't want to extend the session or trigger a new session in these cases. These events are designated by event
5262
+ // type -> incremental update, and source -> mutation.
5263
+ const isUserInteraction = this._isInteractiveEvent(event);
5264
+ if (!isUserInteraction && !this._isIdle) {
5265
+ // We check if the lastActivityTimestamp is old enough to go idle
5266
+ const timeSinceLastActivity = event.timestamp - this._lastActivityTimestamp;
5267
+ if (timeSinceLastActivity > this._sessionIdleThresholdMilliseconds) {
5268
+ // we mark as idle right away,
5269
+ // or else we get multiple idle events
5270
+ // if there are lots of non-user activity events being emitted
5271
+ this._isIdle = true;
5272
+ // don't take full snapshots while idle
5273
+ clearInterval(this._fullSnapshotTimer);
5274
+ this._tryAddCustomEvent('sessionIdle', {
5275
+ eventTimestamp: event.timestamp,
5276
+ lastActivityTimestamp: this._lastActivityTimestamp,
5277
+ threshold: this._sessionIdleThresholdMilliseconds,
5278
+ bufferLength: this._buffer.data.length,
5279
+ bufferSize: this._buffer.size
5280
+ });
5281
+ // proactively flush the buffer in case the session is idle for a long time
5282
+ this._flushBuffer();
5283
+ }
5284
+ }
5285
+ let returningFromIdle = false;
5286
+ if (isUserInteraction) {
5287
+ this._lastActivityTimestamp = event.timestamp;
5288
+ if (this._isIdle) {
5289
+ const idleWasUnknown = this._isIdle === 'unknown';
5290
+ // Remove the idle state
5291
+ this._isIdle = false;
5292
+ // if the idle state was unknown, we don't want to add an event, since we're just in bootup
5293
+ // whereas if it was true, we know we've been idle for a while, and we can mark ourselves as returning from idle
5294
+ if (!idleWasUnknown) {
5295
+ this._tryAddCustomEvent('sessionNoLongerIdle', {
5296
+ reason: 'user activity',
5297
+ type: event.type
5298
+ });
5299
+ returningFromIdle = true;
5300
+ }
5301
+ }
5302
+ }
5303
+ if (this._isIdle) {
5304
+ return;
5305
+ }
5306
+ // We only want to extend the session if it is an interactive event.
5307
+ const {
5308
+ windowId,
5309
+ sessionId
5310
+ } = this._sessionManager.checkAndGetSessionAndWindowId(!isUserInteraction, event.timestamp);
5311
+ const sessionIdChanged = this._sessionId !== sessionId;
5312
+ const windowIdChanged = this._windowId !== windowId;
5313
+ this._windowId = windowId;
5314
+ this._sessionId = sessionId;
5315
+ if (sessionIdChanged || windowIdChanged) {
5316
+ this.stop();
5317
+ this.start('session_id_changed');
5318
+ } else if (returningFromIdle) {
5319
+ this._scheduleFullSnapshot();
5320
+ }
5321
+ }
5322
+ _clearConditionalRecordingPersistence() {
5323
+ this._instance?.persistence?.unregister(SESSION_RECORDING_EVENT_TRIGGER_ACTIVATED_SESSION);
5324
+ this._instance?.persistence?.unregister(SESSION_RECORDING_URL_TRIGGER_ACTIVATED_SESSION);
5325
+ this._instance?.persistence?.unregister(SESSION_RECORDING_IS_SAMPLED);
5326
+ }
5327
+ _makeSamplingDecision(sessionId) {
5328
+ const sessionIdChanged = this._sessionId !== sessionId;
5329
+ // capture the current sample rate
5330
+ // because it is re-used multiple times
5331
+ // and the bundler won't minimize any of the references
5332
+ const currentSampleRate = this._sampleRate;
5333
+ if (!isNumber(currentSampleRate)) {
5334
+ this._instance.persistence?.unregister(SESSION_RECORDING_IS_SAMPLED);
5335
+ return;
5336
+ }
5337
+ const storedIsSampled = this._isSampled;
5338
+ /**
5339
+ * if we get this far, then we should make a sampling decision.
5340
+ * When the session id changes or there is no stored sampling decision for this session id
5341
+ * then we should make a new decision.
5342
+ *
5343
+ * Otherwise, we should use the stored decision.
5344
+ */
5345
+ const makeDecision = sessionIdChanged || !isBoolean(storedIsSampled);
5346
+ const shouldSample = makeDecision ? sampleOnProperty(sessionId, currentSampleRate) : storedIsSampled;
5347
+ if (makeDecision) {
5348
+ if (shouldSample) {
5349
+ this._reportStarted(SAMPLED);
5350
+ } else {
5351
+ logger.warn(`Sample rate (${currentSampleRate}) has determined that this sessionId (${sessionId}) will not be sent to the server.`);
5352
+ }
5353
+ this._tryAddCustomEvent('samplingDecisionMade', {
5354
+ sampleRate: currentSampleRate,
5355
+ isSampled: shouldSample
5356
+ });
5357
+ }
5358
+ this._instance.persistence?.register({
5359
+ [SESSION_RECORDING_IS_SAMPLED]: shouldSample ? sessionId : false
5360
+ });
5361
+ }
5362
+ _addEventTriggerListener() {
5363
+ if (this._eventTriggerMatching._eventTriggers.length === 0 || !isNullish(this._removeEventTriggerCaptureHook)) {
5364
+ return;
5365
+ }
5366
+ this._removeEventTriggerCaptureHook = this._instance.on('eventCaptured', event => {
5367
+ // If anything could go wrong here, it has the potential to block the main loop,
5368
+ // so we catch all errors.
5369
+ try {
5370
+ if (this._eventTriggerMatching._eventTriggers.includes(event.event)) {
5371
+ this._activateTrigger('event');
5372
+ }
5373
+ } catch (e) {
5374
+ logger.error('Could not activate event trigger', e);
5375
+ }
5376
+ });
5377
+ }
5378
+ get sdkDebugProperties() {
5379
+ const {
5380
+ sessionStartTimestamp
5381
+ } = this._sessionManager.checkAndGetSessionAndWindowId(true);
5382
+ return {
5383
+ $recording_status: this.status,
5384
+ $sdk_debug_replay_internal_buffer_length: this._buffer.data.length,
5385
+ $sdk_debug_replay_internal_buffer_size: this._buffer.size,
5386
+ $sdk_debug_current_session_duration: this._sessionDuration,
5387
+ $sdk_debug_session_start: sessionStartTimestamp
5388
+ };
5389
+ }
5390
+ _startRecorder() {
5391
+ if (this._stopRrweb) {
5392
+ return;
5393
+ }
5394
+ // rrweb config info: https://github.com/rrweb-io/rrweb/blob/7d5d0033258d6c29599fb08412202d9a2c7b9413/src/record/index.ts#L28
5395
+ const sessionRecordingOptions = {
5396
+ // a limited set of the rrweb config options that we expose to our users.
5397
+ // see https://github.com/rrweb-io/rrweb/blob/master/guide.md
5398
+ blockClass: 'ph-no-capture',
5399
+ blockSelector: undefined,
5400
+ ignoreClass: 'ph-ignore-input',
5401
+ maskTextClass: 'ph-mask',
5402
+ maskTextSelector: undefined,
5403
+ maskTextFn: undefined,
5404
+ maskAllInputs: true,
5405
+ maskInputOptions: {
5406
+ password: true
5407
+ },
5408
+ maskInputFn: undefined,
5409
+ slimDOMOptions: {},
5410
+ collectFonts: false,
5411
+ inlineStylesheet: true,
5412
+ recordCrossOriginIframes: false
5413
+ };
5414
+ // only allows user to set our allowlisted options
5415
+ const userSessionRecordingOptions = this._instance.config.session_recording;
5416
+ for (const [key, value] of Object.entries(userSessionRecordingOptions || {})) {
5417
+ if (key in sessionRecordingOptions) {
5418
+ if (key === 'maskInputOptions') {
5419
+ // ensure password config is set if not included
5420
+ sessionRecordingOptions.maskInputOptions = {
5421
+ password: true,
5422
+ ...value
5423
+ };
5424
+ } else {
5425
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
5426
+ // @ts-ignore
5427
+ sessionRecordingOptions[key] = value;
5428
+ }
5429
+ }
5430
+ }
5431
+ if (this._canvasRecording && this._canvasRecording.enabled) {
5432
+ sessionRecordingOptions.recordCanvas = true;
5433
+ sessionRecordingOptions.sampling = {
5434
+ canvas: this._canvasRecording.fps
5435
+ };
5436
+ sessionRecordingOptions.dataURLOptions = {
5437
+ type: 'image/webp',
5438
+ quality: this._canvasRecording.quality
5439
+ };
5440
+ }
5441
+ if (this._masking) {
5442
+ sessionRecordingOptions.maskAllInputs = this._masking.maskAllInputs ?? true;
5443
+ sessionRecordingOptions.maskTextSelector = this._masking.maskTextSelector ?? undefined;
5444
+ sessionRecordingOptions.blockSelector = this._masking.blockSelector ?? undefined;
5445
+ }
5446
+ const rrwebRecord = getRRWebRecord();
5447
+ if (!rrwebRecord) {
5448
+ logger.error('_startRecorder was called but rrwebRecord is not available. This indicates something has gone wrong.');
5449
+ return;
5450
+ }
5451
+ this._mutationThrottler = this._mutationThrottler ?? new MutationThrottler(rrwebRecord, {
5452
+ refillRate: this._instance.config.session_recording?.__mutationThrottlerRefillRate,
5453
+ bucketSize: this._instance.config.session_recording?.__mutationThrottlerBucketSize,
5454
+ onBlockedNode: (id, node) => {
5455
+ const message = `Too many mutations on node '${id}'. Rate limiting. This could be due to SVG animations or something similar`;
5456
+ logger.info(message, {
5457
+ node: node
5458
+ });
5459
+ this.log(LOGGER_PREFIX$1 + ' ' + message, 'warn');
5460
+ }
5461
+ });
5462
+ const activePlugins = this._gatherRRWebPlugins();
5463
+ this._stopRrweb = rrwebRecord({
5464
+ emit: event => {
5465
+ this.onRRwebEmit(event);
5466
+ },
5467
+ plugins: activePlugins,
5468
+ ...sessionRecordingOptions
5469
+ });
5470
+ // We reset the last activity timestamp, resetting the idle timer
5471
+ this._lastActivityTimestamp = Date.now();
5472
+ // stay unknown if we're not sure if we're idle or not
5473
+ this._isIdle = isBoolean(this._isIdle) ? this._isIdle : 'unknown';
5474
+ this.tryAddCustomEvent('$remote_config_received', this._remoteConfig);
5475
+ this._tryAddCustomEvent('$session_options', {
5476
+ sessionRecordingOptions,
5477
+ activePlugins: activePlugins.map(p => p?.name)
5478
+ });
5479
+ this._tryAddCustomEvent('$posthog_config', {
5480
+ config: this._instance.config
5481
+ });
5482
+ }
5483
+ tryAddCustomEvent(tag, payload) {
5484
+ return this._tryAddCustomEvent(tag, payload);
5485
+ }
5486
+ }
5487
+
5488
+ const LOGGER_PREFIX = '[SessionRecording]';
5489
+ const log = {
5490
+ info: (...args) => logger$3.info(LOGGER_PREFIX, ...args),
5491
+ warn: (...args) => logger$3.warn(LOGGER_PREFIX, ...args),
5492
+ error: (...args) => logger$3.error(LOGGER_PREFIX, ...args)
5493
+ };
5494
+ class SessionRecording {
5495
+ get started() {
5496
+ return !!this._lazyLoadedSessionRecording?.isStarted;
5497
+ }
5498
+ /**
5499
+ * defaults to buffering mode until a flags response is received
5500
+ * once a flags response is received status can be disabled, active or sampled
5501
+ */
5502
+ get status() {
5503
+ if (this._lazyLoadedSessionRecording) {
5504
+ return this._lazyLoadedSessionRecording.status;
5505
+ }
5506
+ if (this._receivedFlags && !this._isRecordingEnabled) {
5507
+ return DISABLED;
5508
+ }
5509
+ return LAZY_LOADING;
5510
+ }
5511
+ constructor(_instance) {
5512
+ this._instance = _instance;
5513
+ this._forceAllowLocalhostNetworkCapture = false;
5514
+ this._receivedFlags = false;
5515
+ this._serverRecordingEnabled = false;
5516
+ this._persistFlagsOnSessionListener = undefined;
5517
+ if (!this._instance.sessionManager) {
5518
+ log.error('started without valid sessionManager');
5519
+ throw new Error(LOGGER_PREFIX + ' started without valid sessionManager. This is a bug.');
5520
+ }
5521
+ if (this._instance.config.cookieless_mode === 'always') {
5522
+ throw new Error(LOGGER_PREFIX + ' cannot be used with cookieless_mode="always"');
5523
+ }
5524
+ }
5525
+ get _isRecordingEnabled() {
5526
+ const enabled_server_side = !!this._instance.get_property(SESSION_RECORDING_REMOTE_CONFIG)?.enabled;
5527
+ const enabled_client_side = !this._instance.config.disable_session_recording;
5528
+ const isDisabled = this._instance.config.disable_session_recording || this._instance.consent?.isOptedOut?.();
5529
+ return win && enabled_server_side && enabled_client_side && !isDisabled;
5530
+ }
5531
+ startIfEnabledOrStop(startReason) {
5532
+ if (this._isRecordingEnabled && this._lazyLoadedSessionRecording?.isStarted) {
5533
+ return;
5534
+ }
5535
+ const canRunReplay = !isUndefined(Object.assign) && !isUndefined(Array.from);
5536
+ if (this._isRecordingEnabled && canRunReplay) {
5537
+ this._lazyLoadAndStart(startReason);
5538
+ log.info('starting');
5539
+ } else {
5540
+ this.stopRecording();
5541
+ }
5542
+ }
5543
+ /**
5544
+ * session recording waits until it receives remote config before loading the script
5545
+ * this is to ensure we can control the script name remotely
5546
+ * and because we wait until we have local and remote config to determine if we should start at all
5547
+ * if start is called and there is no remote config then we wait until there is
5548
+ */
5549
+ _lazyLoadAndStart(startReason) {
5550
+ // by checking `_isRecordingEnabled` here we know that
5551
+ // we have stored remote config and client config to read
5552
+ // replay waits for both local and remote config before starting
5553
+ if (!this._isRecordingEnabled) {
5554
+ return;
5555
+ }
5556
+ this._onScriptLoaded(startReason);
5557
+ }
5558
+ stopRecording() {
5559
+ this._persistFlagsOnSessionListener?.();
5560
+ this._persistFlagsOnSessionListener = undefined;
5561
+ this._lazyLoadedSessionRecording?.stop();
5562
+ }
5563
+ _resetSampling() {
5564
+ this._instance.persistence?.unregister(SESSION_RECORDING_IS_SAMPLED);
5565
+ }
5566
+ _persistRemoteConfig(response) {
5567
+ if (this._instance.persistence) {
5568
+ const persistence = this._instance.persistence;
5569
+ const persistResponse = () => {
5570
+ const sessionRecordingConfigResponse = response.sessionRecording === false ? undefined : response.sessionRecording;
5571
+ const receivedSampleRate = sessionRecordingConfigResponse?.sampleRate;
5572
+ const parsedSampleRate = isNullish(receivedSampleRate) ? null : parseFloat(receivedSampleRate);
5573
+ if (isNullish(parsedSampleRate)) {
5574
+ this._resetSampling();
5575
+ }
5576
+ const receivedMinimumDuration = sessionRecordingConfigResponse?.minimumDurationMilliseconds;
5577
+ persistence.register({
5578
+ [SESSION_RECORDING_REMOTE_CONFIG]: {
5579
+ enabled: !!sessionRecordingConfigResponse,
5580
+ ...sessionRecordingConfigResponse,
5581
+ networkPayloadCapture: {
5582
+ capturePerformance: response.capturePerformance,
5583
+ ...sessionRecordingConfigResponse?.networkPayloadCapture
5584
+ },
5585
+ canvasRecording: {
5586
+ enabled: sessionRecordingConfigResponse?.recordCanvas,
5587
+ fps: sessionRecordingConfigResponse?.canvasFps,
5588
+ quality: sessionRecordingConfigResponse?.canvasQuality
5589
+ },
5590
+ sampleRate: parsedSampleRate,
5591
+ minimumDurationMilliseconds: isUndefined(receivedMinimumDuration) ? null : receivedMinimumDuration,
5592
+ endpoint: sessionRecordingConfigResponse?.endpoint,
5593
+ triggerMatchType: sessionRecordingConfigResponse?.triggerMatchType,
5594
+ masking: sessionRecordingConfigResponse?.masking,
5595
+ urlTriggers: sessionRecordingConfigResponse?.urlTriggers
5596
+ }
5597
+ });
5598
+ };
5599
+ persistResponse();
5600
+ // in case we see multiple flags responses, we should only use the response from the most recent one
5601
+ this._persistFlagsOnSessionListener?.();
5602
+ // we 100% know there is a session manager by this point
5603
+ this._persistFlagsOnSessionListener = this._instance.sessionManager?.onSessionId(persistResponse);
5604
+ }
5605
+ }
5606
+ _clearRemoteConfig() {
5607
+ this._instance.persistence?.unregister(SESSION_RECORDING_REMOTE_CONFIG);
5608
+ this._resetSampling();
5609
+ }
5610
+ onRemoteConfig(response) {
5611
+ if (!('sessionRecording' in response)) {
5612
+ // if sessionRecording is not in the response, we do nothing
5613
+ log.info('skipping remote config with no sessionRecording', response);
5614
+ return;
5615
+ }
5616
+ this._receivedFlags = true;
5617
+ if (response.sessionRecording === false) {
5618
+ this._serverRecordingEnabled = false;
5619
+ this._clearRemoteConfig();
5620
+ this.stopRecording();
5621
+ return;
5622
+ }
5623
+ this._serverRecordingEnabled = true;
5624
+ this._persistRemoteConfig(response);
5625
+ this.startIfEnabledOrStop();
5626
+ }
5627
+ log(message, level = 'log') {
5628
+ if (this._lazyLoadedSessionRecording?.log) {
5629
+ this._lazyLoadedSessionRecording.log(message, level);
5630
+ } else {
5631
+ logger$3.warn('log called before recorder was ready');
5632
+ }
5633
+ }
5634
+ _onScriptLoaded(startReason) {
5635
+ if (!this._lazyLoadedSessionRecording) {
5636
+ this._lazyLoadedSessionRecording = new LazyLoadedSessionRecording(this._instance);
5637
+ this._lazyLoadedSessionRecording._forceAllowLocalhostNetworkCapture = this._forceAllowLocalhostNetworkCapture;
5638
+ }
5639
+ this._lazyLoadedSessionRecording.start(startReason);
5640
+ }
5641
+ /**
5642
+ * this is maintained on the public API only because it has always been on the public API
5643
+ * if you are calling this directly you are certainly doing something wrong
5644
+ * @deprecated
5645
+ */
5646
+ onRRwebEmit(rawEvent) {
5647
+ this._lazyLoadedSessionRecording?.onRRwebEmit?.(rawEvent);
5648
+ }
5649
+ /**
5650
+ * this ignores the linked flag config and (if other conditions are met) causes capture to start
5651
+ *
5652
+ * It is not usual to call this directly,
5653
+ * instead call `posthog.startSessionRecording({linked_flag: true})`
5654
+ * */
5655
+ overrideLinkedFlag() {
5656
+ // TODO what if this gets called before lazy loading is done
5657
+ this._lazyLoadedSessionRecording?.overrideLinkedFlag();
5658
+ }
5659
+ /**
5660
+ * this ignores the sampling config and (if other conditions are met) causes capture to start
5661
+ *
5662
+ * It is not usual to call this directly,
5663
+ * instead call `posthog.startSessionRecording({sampling: true})`
5664
+ * */
5665
+ overrideSampling() {
5666
+ // TODO what if this gets called before lazy loading is done
5667
+ this._lazyLoadedSessionRecording?.overrideSampling();
5668
+ }
5669
+ /**
5670
+ * this ignores the URL/Event trigger config and (if other conditions are met) causes capture to start
5671
+ *
5672
+ * It is not usual to call this directly,
5673
+ * instead call `posthog.startSessionRecording({trigger: 'url' | 'event'})`
5674
+ * */
5675
+ overrideTrigger(triggerType) {
5676
+ // TODO what if this gets called before lazy loading is done
5677
+ this._lazyLoadedSessionRecording?.overrideTrigger(triggerType);
5678
+ }
5679
+ /*
5680
+ * whenever we capture an event, we add these properties to the event
5681
+ * these are used to debug issues with the session recording
5682
+ * when looking at the event feed for a session
5683
+ */
5684
+ get sdkDebugProperties() {
5685
+ return this._lazyLoadedSessionRecording?.sdkDebugProperties || {
5686
+ $recording_status: this.status
5687
+ };
5688
+ }
5689
+ /**
5690
+ * This adds a custom event to the session recording
5691
+ *
5692
+ * It is not intended for arbitrary public use - playback only displays known custom events
5693
+ * And is exposed on the public interface only so that other parts of the SDK are able to use it
5694
+ *
5695
+ * if you are calling this from client code, you're probably looking for `posthog.capture('$custom_event', {...})`
5696
+ */
5697
+ tryAddCustomEvent(tag, payload) {
5698
+ return !!this._lazyLoadedSessionRecording?.tryAddCustomEvent(tag, payload);
5699
+ }
5700
+ }
5701
+
5702
+ const defaultConfig = () => ({
5703
+ host: 'https://i.leanbase.co',
5704
+ token: '',
5705
+ autocapture: true,
5706
+ rageclick: true,
5707
+ disable_session_recording: false,
5708
+ session_recording: {
5709
+ // Force-enable session recording locally unless explicitly disabled via config
5710
+ forceClientRecording: true
5711
+ },
5712
+ enable_recording_console_log: undefined,
5713
+ persistence: 'localStorage+cookie',
5714
+ capture_pageview: 'history_change',
5715
+ capture_pageleave: 'if_capture_pageview',
5716
+ persistence_name: '',
5717
+ mask_all_element_attributes: false,
5718
+ cookie_expiration: 365,
5719
+ cross_subdomain_cookie: isCrossDomainCookie(document?.location),
5720
+ custom_campaign_params: [],
5721
+ custom_personal_data_properties: [],
5722
+ disable_persistence: false,
5723
+ mask_personal_data_properties: false,
5724
+ secure_cookie: window?.location?.protocol === 'https:',
5725
+ mask_all_text: false,
5726
+ bootstrap: {},
5727
+ session_idle_timeout_seconds: 30 * 60,
5728
+ save_campaign_params: true,
5729
+ save_referrer: true,
5730
+ opt_out_useragent_filter: false,
5731
+ properties_string_max_length: 65535,
5732
+ loaded: () => {}
5733
+ });
5734
+ class Leanbase extends PostHogCore {
5735
+ constructor(token, config) {
5736
+ const mergedConfig = extend(defaultConfig(), config || {}, {
5737
+ token
5738
+ });
5739
+ super(token, mergedConfig);
5740
+ this.personProcessingSetOncePropertiesSent = false;
5741
+ this.isLoaded = false;
5742
+ this.config = mergedConfig;
5743
+ this.visibilityStateListener = null;
5744
+ this.initialPageviewCaptured = false;
5745
+ this.scrollManager = new ScrollManager(this);
5746
+ this.pageViewManager = new PageViewManager(this);
5747
+ this.init(token, mergedConfig);
5748
+ }
5749
+ init(token, config) {
5750
+ this.setConfig(extend(defaultConfig(), config, {
5751
+ token
5752
+ }));
5753
+ this.isLoaded = true;
5754
+ this.persistence = new LeanbasePersistence(this.config);
5755
+ if (this.config.cookieless_mode !== 'always') {
5756
+ this.sessionManager = new SessionIdManager(this);
5757
+ this.sessionPropsManager = new SessionPropsManager(this, this.sessionManager, this.persistence);
5758
+ }
5759
+ this.replayAutocapture = new Autocapture(this);
5760
+ this.replayAutocapture.startIfEnabled();
5761
+ if (this.sessionManager && this.config.cookieless_mode !== 'always') {
5762
+ this.sessionRecording = new SessionRecording(this);
5763
+ this.sessionRecording.startIfEnabledOrStop();
5764
+ }
5765
+ if (this.config.preloadFeatureFlags !== false) {
5766
+ this.reloadFeatureFlags();
5767
+ }
5768
+ this.config.loaded?.(this);
5769
+ if (this.config.capture_pageview) {
5770
+ setTimeout(() => {
5771
+ if (this.config.cookieless_mode === 'always') {
5772
+ this.captureInitialPageview();
5773
+ }
5774
+ }, 1);
5775
+ }
5776
+ addEventListener(document, 'DOMContentLoaded', () => {
5777
+ this.loadRemoteConfig();
5778
+ });
5779
+ addEventListener(window, 'onpagehide' in self ? 'pagehide' : 'unload', this.capturePageLeave.bind(this), {
5780
+ passive: false
5781
+ });
5782
+ }
5783
+ captureInitialPageview() {
5784
+ if (!document) {
5785
+ return;
5786
+ }
5787
+ if (document.visibilityState !== 'visible') {
5788
+ if (!this.visibilityStateListener) {
5789
+ this.visibilityStateListener = this.captureInitialPageview.bind(this);
5790
+ addEventListener(document, 'visibilitychange', this.visibilityStateListener);
5791
+ }
5792
+ return;
5793
+ }
5794
+ if (!this.initialPageviewCaptured) {
5795
+ this.initialPageviewCaptured = true;
5796
+ this.capture('$pageview', {
5797
+ title: document.title
5798
+ });
5799
+ if (this.visibilityStateListener) {
5800
+ document.removeEventListener('visibilitychange', this.visibilityStateListener);
5801
+ this.visibilityStateListener = null;
5802
+ }
5803
+ }
5804
+ }
5805
+ capturePageLeave() {
5806
+ const {
5807
+ capture_pageleave,
5808
+ capture_pageview
5809
+ } = this.config;
5810
+ if (capture_pageleave === true || capture_pageleave === 'if_capture_pageview' && (capture_pageview === true || capture_pageview === 'history_change')) {
5811
+ this.capture('$pageleave');
5812
+ }
5813
+ }
5814
+ async loadRemoteConfig() {
5815
+ if (!this.isRemoteConfigLoaded) {
5816
+ const remoteConfig = await this.reloadRemoteConfigAsync();
5817
+ if (remoteConfig) {
5818
+ this.onRemoteConfig(remoteConfig);
5819
+ }
5820
+ }
5821
+ }
5822
+ onRemoteConfig(config) {
5823
+ if (!(document && document.body)) {
5824
+ setTimeout(() => {
5825
+ this.onRemoteConfig(config);
5826
+ }, 500);
5827
+ return;
5828
+ }
5829
+ this.isRemoteConfigLoaded = true;
5830
+ this.replayAutocapture?.onRemoteConfig(config);
5831
+ this.sessionRecording?.onRemoteConfig(config);
5832
+ }
5833
+ fetch(url, options) {
5834
+ const fetchFn = getFetch();
5835
+ if (!fetchFn) {
5836
+ return Promise.reject(new Error('Fetch API is not available in this environment.'));
5837
+ }
5838
+ return fetchFn(url, options);
5839
+ }
5840
+ setConfig(config) {
5841
+ const oldConfig = {
5842
+ ...this.config
5843
+ };
5844
+ if (isObject(config)) {
5845
+ extend(this.config, config);
5846
+ if (!this.config.api_host && this.config.host) {
5847
+ this.config.api_host = this.config.host;
5848
+ }
5849
+ this.persistence?.update_config(this.config, oldConfig);
5850
+ this.replayAutocapture?.startIfEnabled();
5851
+ this.sessionRecording?.startIfEnabledOrStop();
5852
+ }
5853
+ const isTempStorage = this.config.persistence === 'sessionStorage' || this.config.persistence === 'memory';
5854
+ this.sessionPersistence = isTempStorage ? this.persistence : new LeanbasePersistence({
5855
+ ...this.config,
5856
+ persistence: 'sessionStorage'
5857
+ });
5858
+ }
5859
+ getLibraryId() {
5860
+ return 'leanbase';
5861
+ }
5862
+ getLibraryVersion() {
5863
+ return Config.LIB_VERSION;
5864
+ }
5865
+ getCustomUserAgent() {
5866
+ return;
5867
+ }
5868
+ getPersistedProperty(key) {
5869
+ return this.persistence?.get_property(key);
5870
+ }
5871
+ get_property(key) {
5872
+ return this.persistence?.get_property(key);
5873
+ }
5874
+ setPersistedProperty(key, value) {
5875
+ this.persistence?.set_property(key, value);
5876
+ }
5877
+ calculateEventProperties(eventName, eventProperties, timestamp, uuid, readOnly) {
5878
+ if (!this.persistence || !this.sessionPersistence) {
5879
+ return eventProperties;
3361
5880
  }
3362
5881
  timestamp = timestamp || new Date();
3363
5882
  const startTimestamp = readOnly ? undefined : this.persistence?.remove_event_timer(eventName);
@@ -3374,7 +5893,7 @@ class Leanbase extends PostHogCore {
3374
5893
  };
3375
5894
  properties['distinct_id'] = persistenceProps.distinct_id;
3376
5895
  if (!(isString(properties['distinct_id']) || isNumber(properties['distinct_id'])) || isEmptyString(properties['distinct_id'])) {
3377
- logger.error('Invalid distinct_id for replay event. This indicates a bug in your implementation');
5896
+ logger$3.error('Invalid distinct_id for replay event. This indicates a bug in your implementation');
3378
5897
  }
3379
5898
  return properties;
3380
5899
  }
@@ -3390,6 +5909,13 @@ class Leanbase extends PostHogCore {
3390
5909
  if (this.sessionPropsManager) {
3391
5910
  extend(properties, this.sessionPropsManager.getSessionProps());
3392
5911
  }
5912
+ try {
5913
+ if (this.sessionRecording) {
5914
+ extend(properties, this.sessionRecording.sdkDebugProperties);
5915
+ }
5916
+ } catch (e) {
5917
+ properties['$sdk_debug_error_capturing_properties'] = String(e);
5918
+ }
3393
5919
  let pageviewProperties = this.pageViewManager.doEvent();
3394
5920
  if (eventName === '$pageview' && !readOnly) {
3395
5921
  pageviewProperties = this.pageViewManager.doPageView(timestamp, uuid);
@@ -3442,11 +5968,11 @@ class Leanbase extends PostHogCore {
3442
5968
  return;
3443
5969
  }
3444
5970
  if (isUndefined(event) || !isString(event)) {
3445
- logger.error('No event name provided to posthog.capture');
5971
+ logger$3.error('No event name provided to posthog.capture');
3446
5972
  return;
3447
5973
  }
3448
5974
  if (properties?.$current_url && !isString(properties?.$current_url)) {
3449
- logger.error('Invalid `$current_url` property provided to `posthog.capture`. Input must be a string. Ignoring provided value.');
5975
+ logger$3.error('Invalid `$current_url` property provided to `posthog.capture`. Input must be a string. Ignoring provided value.');
3450
5976
  delete properties?.$current_url;
3451
5977
  }
3452
5978
  this.sessionPersistence.update_search_keyword();