@leanbase-giangnd/js 0.0.4 → 0.0.7

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