@leanbase-giangnd/js 0.1.2 → 0.2.2

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,7 +1,6 @@
1
1
  'use strict';
2
2
 
3
3
  var core = require('@posthog/core');
4
- var record = require('@rrweb/record');
5
4
  var fflate = require('fflate');
6
5
 
7
6
  const breaker = {};
@@ -17,6 +16,8 @@ global?.fetch;
17
16
  global?.XMLHttpRequest && 'withCredentials' in new global.XMLHttpRequest() ? global.XMLHttpRequest : undefined;
18
17
  global?.AbortController;
19
18
  const userAgent = navigator$1?.userAgent;
19
+ // assignableWindow mirrors browser package's assignableWindow for extension loading shims
20
+ const assignableWindow = win ?? {};
20
21
  function eachArray(obj, iterator, thisArg) {
21
22
  if (core.isArray(obj)) {
22
23
  if (nativeForEach && obj.forEach === nativeForEach) {
@@ -230,7 +231,7 @@ core.PostHogPersistedProperty.Queue, core.PostHogPersistedProperty.FeatureFlagDe
230
231
 
231
232
  /* eslint-disable no-console */
232
233
  const PREFIX = '[Leanbase]';
233
- const logger$3 = {
234
+ const logger$2 = {
234
235
  info: (...args) => {
235
236
  if (typeof console !== 'undefined') {
236
237
  console.log(PREFIX, ...args);
@@ -528,7 +529,7 @@ function chooseCookieDomain(hostname, cross_subdomain) {
528
529
  if (!matchedSubDomain) {
529
530
  const originalMatch = originalCookieDomainFn(hostname);
530
531
  if (originalMatch !== matchedSubDomain) {
531
- logger$3.info('Warning: cookie subdomain discovery mismatch', originalMatch, matchedSubDomain);
532
+ logger$2.info('Warning: cookie subdomain discovery mismatch', originalMatch, matchedSubDomain);
532
533
  }
533
534
  matchedSubDomain = originalMatch;
534
535
  }
@@ -540,7 +541,7 @@ function chooseCookieDomain(hostname, cross_subdomain) {
540
541
  const cookieStore = {
541
542
  _is_supported: () => !!document,
542
543
  _error: function (msg) {
543
- logger$3.error('cookieStore error: ' + msg);
544
+ logger$2.error('cookieStore error: ' + msg);
544
545
  },
545
546
  _get: function (name) {
546
547
  if (!document) {
@@ -589,7 +590,7 @@ const cookieStore = {
589
590
  const new_cookie_val = name + '=' + encodeURIComponent(JSON.stringify(value)) + expires + '; SameSite=Lax; path=/' + cdomain + secure;
590
591
  // 4096 bytes is the size at which some browsers (e.g. firefox) will not store a cookie, warn slightly before that
591
592
  if (new_cookie_val.length > 4096 * 0.9) {
592
- logger$3.warn('cookieStore warning: large cookie, len=' + new_cookie_val.length);
593
+ logger$2.warn('cookieStore warning: large cookie, len=' + new_cookie_val.length);
593
594
  }
594
595
  document.cookie = new_cookie_val;
595
596
  return new_cookie_val;
@@ -631,13 +632,13 @@ const localStore = {
631
632
  supported = false;
632
633
  }
633
634
  if (!supported) {
634
- logger$3.error('localStorage unsupported; falling back to cookie store');
635
+ logger$2.error('localStorage unsupported; falling back to cookie store');
635
636
  }
636
637
  _localStorage_supported = supported;
637
638
  return supported;
638
639
  },
639
640
  _error: function (msg) {
640
- logger$3.error('localStorage error: ' + msg);
641
+ logger$2.error('localStorage error: ' + msg);
641
642
  },
642
643
  _get: function (name) {
643
644
  try {
@@ -723,7 +724,7 @@ const memoryStore = {
723
724
  return true;
724
725
  },
725
726
  _error: function (msg) {
726
- logger$3.error('memoryStorage error: ' + msg);
727
+ logger$2.error('memoryStorage error: ' + msg);
727
728
  },
728
729
  _get: function (name) {
729
730
  return memoryStorage[name] || null;
@@ -764,7 +765,7 @@ const sessionStore = {
764
765
  return sessionStorageSupported;
765
766
  },
766
767
  _error: function (msg) {
767
- logger$3.error('sessionStorage error: ', msg);
768
+ logger$2.error('sessionStorage error: ', msg);
768
769
  },
769
770
  _get: function (name) {
770
771
  try {
@@ -813,21 +814,6 @@ const convertToURL = url => {
813
814
  location.href = url;
814
815
  return location;
815
816
  };
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
817
  // NOTE: Once we get rid of IE11/op_mini we can start using URLSearchParams
832
818
  const getQueryParam = function (url, param) {
833
819
  const withoutHash = url.split('#')[0] || '';
@@ -851,7 +837,7 @@ const getQueryParam = function (url, param) {
851
837
  try {
852
838
  result = decodeURIComponent(result);
853
839
  } catch {
854
- logger$3.error('Skipping decoding for malformed query param: ' + result);
840
+ logger$2.error('Skipping decoding for malformed query param: ' + result);
855
841
  }
856
842
  return result.replace(/\+/g, ' ');
857
843
  }
@@ -1185,7 +1171,7 @@ const detectDeviceType = function (user_agent) {
1185
1171
  }
1186
1172
  };
1187
1173
 
1188
- var version = "0.1.2";
1174
+ var version = "0.2.2";
1189
1175
  var packageInfo = {
1190
1176
  version: version};
1191
1177
 
@@ -1450,7 +1436,7 @@ class LeanbasePersistence {
1450
1436
  this._storage = this._buildStorage(config);
1451
1437
  this.load();
1452
1438
  if (config.debug) {
1453
- logger$3.info('Persistence loaded', config['persistence'], {
1439
+ logger$2.info('Persistence loaded', config['persistence'], {
1454
1440
  ...this.props
1455
1441
  });
1456
1442
  }
@@ -1466,7 +1452,7 @@ class LeanbasePersistence {
1466
1452
  }
1467
1453
  _buildStorage(config) {
1468
1454
  if (CASE_INSENSITIVE_PERSISTENCE_TYPES.indexOf(config['persistence'].toLowerCase()) === -1) {
1469
- logger$3.info('Unknown persistence type ' + config['persistence'] + '; falling back to localStorage+cookie');
1455
+ logger$2.info('Unknown persistence type ' + config['persistence'] + '; falling back to localStorage+cookie');
1470
1456
  config['persistence'] = 'localStorage+cookie';
1471
1457
  }
1472
1458
  let store;
@@ -2102,7 +2088,7 @@ function getNestedSpanText(target) {
2102
2088
  text = `${text} ${getNestedSpanText(child)}`.trim();
2103
2089
  }
2104
2090
  } catch (e) {
2105
- logger$3.error('[AutoCapture]', e);
2091
+ logger$2.error('[AutoCapture]', e);
2106
2092
  }
2107
2093
  }
2108
2094
  });
@@ -2404,7 +2390,7 @@ class Autocapture {
2404
2390
  }
2405
2391
  _addDomEventHandlers() {
2406
2392
  if (!this.isBrowserSupported()) {
2407
- logger$3.info('Disabling Automatic Event Collection because this browser is not supported');
2393
+ logger$2.info('Disabling Automatic Event Collection because this browser is not supported');
2408
2394
  return;
2409
2395
  }
2410
2396
  if (!win || !document) {
@@ -2415,7 +2401,7 @@ class Autocapture {
2415
2401
  try {
2416
2402
  this._captureEvent(e);
2417
2403
  } catch (error) {
2418
- logger$3.error('Failed to capture event', error);
2404
+ logger$2.error('Failed to capture event', error);
2419
2405
  }
2420
2406
  };
2421
2407
  addEventListener(document, 'submit', handler, {
@@ -2600,7 +2586,7 @@ class SessionIdManager {
2600
2586
  this._windowIdGenerator = windowIdGenerator || uuidv7;
2601
2587
  const persistenceName = this._config['persistence_name'] || this._config['token'];
2602
2588
  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;
2589
+ this._sessionTimeoutMs = core.clampToRange(desiredTimeout, MIN_SESSION_IDLE_TIMEOUT_SECONDS, MAX_SESSION_IDLE_TIMEOUT_SECONDS, logger$2, DEFAULT_SESSION_IDLE_TIMEOUT_SECONDS) * 1000;
2604
2590
  instance.register({
2605
2591
  $configured_session_timeout_ms: this._sessionTimeoutMs
2606
2592
  });
@@ -2627,7 +2613,7 @@ class SessionIdManager {
2627
2613
  const sessionStartTimestamp = uuid7ToTimestampMs(this._config.bootstrap.sessionID);
2628
2614
  this._setSessionId(this._config.bootstrap.sessionID, new Date().getTime(), sessionStartTimestamp);
2629
2615
  } catch (e) {
2630
- logger$3.error('Invalid sessionID in bootstrap', e);
2616
+ logger$2.error('Invalid sessionID in bootstrap', e);
2631
2617
  }
2632
2618
  }
2633
2619
  this._listenToReloadWindow();
@@ -2768,7 +2754,7 @@ class SessionIdManager {
2768
2754
  if (noSessionId || activityTimeout || sessionPastMaximumLength) {
2769
2755
  sessionId = this._sessionIdGenerator();
2770
2756
  windowId = this._windowIdGenerator();
2771
- logger$3.info('new session ID generated', {
2757
+ logger$2.info('new session ID generated', {
2772
2758
  sessionId,
2773
2759
  windowId,
2774
2760
  changeReason: {
@@ -2957,10 +2943,10 @@ class PageViewManager {
2957
2943
  lastContentY = Math.ceil(lastContentY);
2958
2944
  maxContentY = Math.ceil(maxContentY);
2959
2945
  // 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);
2946
+ const lastScrollPercentage = maxScrollHeight <= 1 ? 1 : core.clampToRange(lastScrollY / maxScrollHeight, 0, 1, logger$2);
2947
+ const maxScrollPercentage = maxScrollHeight <= 1 ? 1 : core.clampToRange(maxScrollY / maxScrollHeight, 0, 1, logger$2);
2948
+ const lastContentPercentage = maxContentHeight <= 1 ? 1 : core.clampToRange(lastContentY / maxContentHeight, 0, 1, logger$2);
2949
+ const maxContentPercentage = maxContentHeight <= 1 ? 1 : core.clampToRange(maxContentY / maxContentHeight, 0, 1, logger$2);
2964
2950
  properties = extend(properties, {
2965
2951
  $prev_pageview_last_scroll: lastScrollY,
2966
2952
  $prev_pageview_last_scroll_percentage: lastScrollPercentage,
@@ -3123,83 +3109,217 @@ const isLikelyBot = function (navigator, customBlockedUserAgents) {
3123
3109
  // It would be very bad if posthog-js caused a permission prompt to appear on every page load.
3124
3110
  };
3125
3111
 
3126
- const _createLogger = prefix => {
3127
- return {
3128
- info: (...args) => logger$3.info(prefix, ...args),
3129
- warn: (...args) => logger$3.warn(prefix, ...args),
3130
- error: (...args) => logger$3.error(prefix, ...args),
3131
- critical: (...args) => logger$3.critical(prefix, ...args),
3132
- uninitializedWarning: methodName => {
3133
- logger$3.error(prefix, `You must initialize Leanbase before calling ${methodName}`);
3134
- },
3135
- createLogger: additionalPrefix => _createLogger(`${prefix} ${additionalPrefix}`)
3136
- };
3137
- };
3138
- const logger$2 = _createLogger('[Leanbase]');
3139
- const createLogger = _createLogger;
3140
-
3141
- function patch(source, name, replacement) {
3112
+ /* eslint-disable @typescript-eslint/no-unused-vars */
3113
+ // We avoid importing '@rrweb/record' at module load time to prevent IIFE builds
3114
+ // from requiring a top-level global. Instead, expose a lazy proxy that will
3115
+ // dynamically import the module the first time it's used.
3116
+ let _cachedRRWeb = null;
3117
+ async function _loadRRWebModule() {
3118
+ if (_cachedRRWeb) return _cachedRRWeb;
3142
3119
  try {
3143
- if (!(name in source)) {
3144
- return () => {
3145
- //
3146
- };
3120
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3121
+ const mod = await import('@rrweb/record');
3122
+ _cachedRRWeb = mod;
3123
+ return _cachedRRWeb;
3124
+ } catch (e) {
3125
+ return null;
3126
+ }
3127
+ }
3128
+ // queue for method calls before rrweb loads
3129
+ const _queuedCalls = [];
3130
+ // Create a proxy function that delegates to the real rrweb.record when called
3131
+ const rrwebRecordProxy = function (...args) {
3132
+ let realStop;
3133
+ let calledReal = false;
3134
+ // Start loading asynchronously and call the real record when available
3135
+ void (async () => {
3136
+ const mod = await _loadRRWebModule();
3137
+ const real = mod && (mod.record ?? mod.default?.record);
3138
+ if (real) {
3139
+ try {
3140
+ calledReal = true;
3141
+ realStop = real(...args);
3142
+ // flush any queued calls that were waiting for rrweb
3143
+ while (_queuedCalls.length) {
3144
+ try {
3145
+ const fn = _queuedCalls.shift();
3146
+ fn();
3147
+ } catch (e) {
3148
+ // ignore
3149
+ }
3150
+ }
3151
+ } catch (e) {
3152
+ // ignore
3153
+ }
3147
3154
  }
3148
- const original = source[name];
3149
- const wrapped = replacement(original);
3150
- if (core.isFunction(wrapped)) {
3151
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
3152
- wrapped.prototype = wrapped.prototype || {};
3153
- Object.defineProperties(wrapped, {
3154
- __posthog_wrapped__: {
3155
- enumerable: false,
3156
- value: true
3155
+ })();
3156
+ // return a stop function that will call the real stop when available
3157
+ return () => {
3158
+ if (realStop) {
3159
+ try {
3160
+ realStop();
3161
+ } catch (e) {
3162
+ // ignore
3163
+ }
3164
+ } else if (!calledReal) {
3165
+ // If rrweb hasn't been initialised yet, queue a stop request that will
3166
+ // call the real stop once available.
3167
+ _queuedCalls.push(() => {
3168
+ try {
3169
+ realStop?.();
3170
+ } catch (e) {
3171
+ // ignore
3157
3172
  }
3158
3173
  });
3159
3174
  }
3160
- source[name] = wrapped;
3161
- return () => {
3162
- source[name] = original;
3163
- };
3164
- } catch {
3165
- return () => {
3166
- //
3167
- };
3168
- }
3169
- }
3170
-
3171
- function hostnameFromURL(url) {
3172
- try {
3173
- if (typeof url === 'string') {
3174
- return new URL(url).hostname;
3175
- }
3176
- if ('url' in url) {
3177
- return new URL(url.url).hostname;
3175
+ };
3176
+ };
3177
+ // methods that can be called on the rrweb.record object - queue until real module is available
3178
+ rrwebRecordProxy.addCustomEvent = function (tag, payload) {
3179
+ const call = () => {
3180
+ try {
3181
+ const real = _cachedRRWeb && (_cachedRRWeb.record ?? _cachedRRWeb.default?.record);
3182
+ real?.addCustomEvent?.(tag, payload);
3183
+ } catch (e) {
3184
+ // ignore
3178
3185
  }
3179
- return url.hostname;
3180
- } catch {
3181
- return null;
3182
- }
3183
- }
3184
- function isHostOnDenyList(url, options) {
3185
- const hostname = hostnameFromURL(url);
3186
- const defaultNotDenied = {
3187
- hostname,
3188
- isHostDenied: false
3189
3186
  };
3190
- if (!options.payloadHostDenyList?.length || !hostname?.trim().length) {
3191
- return defaultNotDenied;
3192
- }
3193
- for (const deny of options.payloadHostDenyList) {
3194
- if (hostname.endsWith(deny)) {
3195
- return {
3196
- hostname,
3197
- isHostDenied: true
3198
- };
3187
+ if (_cachedRRWeb) call();else _queuedCalls.push(call);
3188
+ };
3189
+ rrwebRecordProxy.takeFullSnapshot = function () {
3190
+ const call = () => {
3191
+ try {
3192
+ const real = _cachedRRWeb && (_cachedRRWeb.record ?? _cachedRRWeb.default?.record);
3193
+ real?.takeFullSnapshot?.();
3194
+ } catch (e) {
3195
+ // ignore
3199
3196
  }
3197
+ };
3198
+ if (_cachedRRWeb) call();else _queuedCalls.push(call);
3199
+ };
3200
+ // Use a safe global target (prefer `win`, fallback to globalThis)
3201
+ const _target = win ?? globalThis;
3202
+ _target.__PosthogExtensions__ = _target.__PosthogExtensions__ || {};
3203
+ // Expose rrweb.record under the same contract. We provide a lazy proxy so
3204
+ // builds that execute this file don't require rrweb at module evaluation time.
3205
+ _target.__PosthogExtensions__.rrweb = _target.__PosthogExtensions__.rrweb || {
3206
+ record: rrwebRecordProxy
3207
+ };
3208
+ // Provide initSessionRecording if not present — return a new LazyLoadedSessionRecording when called
3209
+ _target.__PosthogExtensions__.initSessionRecording = _target.__PosthogExtensions__.initSessionRecording || (instance => {
3210
+ const factory = _target.__PosthogExtensions__._initSessionRecordingFactory;
3211
+ if (factory) {
3212
+ return factory(instance);
3200
3213
  }
3201
- return defaultNotDenied;
3202
- }
3214
+ // If no factory is registered yet, return undefined — callers should handle lazy-loading.
3215
+ return undefined;
3216
+ });
3217
+ // Provide a no-op loadExternalDependency that calls the callback immediately (since rrweb is bundled)
3218
+ _target.__PosthogExtensions__.loadExternalDependency = _target.__PosthogExtensions__.loadExternalDependency || ((instance, scriptName, cb) => {
3219
+ if (cb) cb(undefined);
3220
+ });
3221
+ // Provide rrwebPlugins object with network plugin factory if not present
3222
+ _target.__PosthogExtensions__.rrwebPlugins = _target.__PosthogExtensions__.rrwebPlugins || {};
3223
+ // Default to undefined; the lazy-loaded recorder will register the real factory when it initializes.
3224
+ _target.__PosthogExtensions__.rrwebPlugins.getRecordNetworkPlugin = _target.__PosthogExtensions__.rrwebPlugins.getRecordNetworkPlugin || (() => undefined);
3225
+
3226
+ // Type definitions copied from @rrweb/types@2.0.0-alpha.17 and rrweb-snapshot@2.0.0-alpha.17
3227
+ // Both packages are MIT licensed: https://github.com/rrweb-io/rrweb
3228
+ //
3229
+ // These types are copied here to avoid requiring users to install peer dependencies
3230
+ // solely for TypeScript type information.
3231
+ //
3232
+ // Original sources:
3233
+ // - @rrweb/types: https://github.com/rrweb-io/rrweb/tree/main/packages/@rrweb/types
3234
+ // - rrweb-snapshot: https://github.com/rrweb-io/rrweb/tree/main/packages/rrweb-snapshot
3235
+ var NodeType;
3236
+ (function (NodeType) {
3237
+ NodeType[NodeType["Document"] = 0] = "Document";
3238
+ NodeType[NodeType["DocumentType"] = 1] = "DocumentType";
3239
+ NodeType[NodeType["Element"] = 2] = "Element";
3240
+ NodeType[NodeType["Text"] = 3] = "Text";
3241
+ NodeType[NodeType["CDATA"] = 4] = "CDATA";
3242
+ NodeType[NodeType["Comment"] = 5] = "Comment";
3243
+ })(NodeType || (NodeType = {}));
3244
+ var EventType;
3245
+ (function (EventType) {
3246
+ EventType[EventType["DomContentLoaded"] = 0] = "DomContentLoaded";
3247
+ EventType[EventType["Load"] = 1] = "Load";
3248
+ EventType[EventType["FullSnapshot"] = 2] = "FullSnapshot";
3249
+ EventType[EventType["IncrementalSnapshot"] = 3] = "IncrementalSnapshot";
3250
+ EventType[EventType["Meta"] = 4] = "Meta";
3251
+ EventType[EventType["Custom"] = 5] = "Custom";
3252
+ EventType[EventType["Plugin"] = 6] = "Plugin";
3253
+ })(EventType || (EventType = {}));
3254
+ var IncrementalSource;
3255
+ (function (IncrementalSource) {
3256
+ IncrementalSource[IncrementalSource["Mutation"] = 0] = "Mutation";
3257
+ IncrementalSource[IncrementalSource["MouseMove"] = 1] = "MouseMove";
3258
+ IncrementalSource[IncrementalSource["MouseInteraction"] = 2] = "MouseInteraction";
3259
+ IncrementalSource[IncrementalSource["Scroll"] = 3] = "Scroll";
3260
+ IncrementalSource[IncrementalSource["ViewportResize"] = 4] = "ViewportResize";
3261
+ IncrementalSource[IncrementalSource["Input"] = 5] = "Input";
3262
+ IncrementalSource[IncrementalSource["TouchMove"] = 6] = "TouchMove";
3263
+ IncrementalSource[IncrementalSource["MediaInteraction"] = 7] = "MediaInteraction";
3264
+ IncrementalSource[IncrementalSource["StyleSheetRule"] = 8] = "StyleSheetRule";
3265
+ IncrementalSource[IncrementalSource["CanvasMutation"] = 9] = "CanvasMutation";
3266
+ IncrementalSource[IncrementalSource["Font"] = 10] = "Font";
3267
+ IncrementalSource[IncrementalSource["Log"] = 11] = "Log";
3268
+ IncrementalSource[IncrementalSource["Drag"] = 12] = "Drag";
3269
+ IncrementalSource[IncrementalSource["StyleDeclaration"] = 13] = "StyleDeclaration";
3270
+ IncrementalSource[IncrementalSource["Selection"] = 14] = "Selection";
3271
+ IncrementalSource[IncrementalSource["AdoptedStyleSheet"] = 15] = "AdoptedStyleSheet";
3272
+ IncrementalSource[IncrementalSource["CustomElement"] = 16] = "CustomElement";
3273
+ })(IncrementalSource || (IncrementalSource = {}));
3274
+ var MouseInteractions;
3275
+ (function (MouseInteractions) {
3276
+ MouseInteractions[MouseInteractions["MouseUp"] = 0] = "MouseUp";
3277
+ MouseInteractions[MouseInteractions["MouseDown"] = 1] = "MouseDown";
3278
+ MouseInteractions[MouseInteractions["Click"] = 2] = "Click";
3279
+ MouseInteractions[MouseInteractions["ContextMenu"] = 3] = "ContextMenu";
3280
+ MouseInteractions[MouseInteractions["DblClick"] = 4] = "DblClick";
3281
+ MouseInteractions[MouseInteractions["Focus"] = 5] = "Focus";
3282
+ MouseInteractions[MouseInteractions["Blur"] = 6] = "Blur";
3283
+ MouseInteractions[MouseInteractions["TouchStart"] = 7] = "TouchStart";
3284
+ MouseInteractions[MouseInteractions["TouchMove_Departed"] = 8] = "TouchMove_Departed";
3285
+ MouseInteractions[MouseInteractions["TouchEnd"] = 9] = "TouchEnd";
3286
+ MouseInteractions[MouseInteractions["TouchCancel"] = 10] = "TouchCancel";
3287
+ })(MouseInteractions || (MouseInteractions = {}));
3288
+ var PointerTypes;
3289
+ (function (PointerTypes) {
3290
+ PointerTypes[PointerTypes["Mouse"] = 0] = "Mouse";
3291
+ PointerTypes[PointerTypes["Pen"] = 1] = "Pen";
3292
+ PointerTypes[PointerTypes["Touch"] = 2] = "Touch";
3293
+ })(PointerTypes || (PointerTypes = {}));
3294
+ var MediaInteractions;
3295
+ (function (MediaInteractions) {
3296
+ MediaInteractions[MediaInteractions["Play"] = 0] = "Play";
3297
+ MediaInteractions[MediaInteractions["Pause"] = 1] = "Pause";
3298
+ MediaInteractions[MediaInteractions["Seeked"] = 2] = "Seeked";
3299
+ MediaInteractions[MediaInteractions["VolumeChange"] = 3] = "VolumeChange";
3300
+ MediaInteractions[MediaInteractions["RateChange"] = 4] = "RateChange";
3301
+ })(MediaInteractions || (MediaInteractions = {}));
3302
+ var CanvasContext;
3303
+ (function (CanvasContext) {
3304
+ CanvasContext[CanvasContext["2D"] = 0] = "2D";
3305
+ CanvasContext[CanvasContext["WebGL"] = 1] = "WebGL";
3306
+ CanvasContext[CanvasContext["WebGL2"] = 2] = "WebGL2";
3307
+ })(CanvasContext || (CanvasContext = {}));
3308
+
3309
+ const _createLogger = prefix => {
3310
+ return {
3311
+ info: (...args) => logger$2.info(prefix, ...args),
3312
+ warn: (...args) => logger$2.warn(prefix, ...args),
3313
+ error: (...args) => logger$2.error(prefix, ...args),
3314
+ critical: (...args) => logger$2.critical(prefix, ...args),
3315
+ uninitializedWarning: methodName => {
3316
+ logger$2.error(prefix, `You must initialize Leanbase before calling ${methodName}`);
3317
+ },
3318
+ createLogger: additionalPrefix => _createLogger(`${prefix} ${additionalPrefix}`)
3319
+ };
3320
+ };
3321
+ const logger$1 = _createLogger('[Leanbase]');
3322
+ const createLogger = _createLogger;
3203
3323
 
3204
3324
  const LOGGER_PREFIX$2 = '[SessionRecording]';
3205
3325
  const REDACTED = 'redacted';
@@ -3272,7 +3392,7 @@ function enforcePayloadSizeLimit(payload, headers, limit, description) {
3272
3392
  // people can have arbitrarily large payloads on their site, but we don't want to ingest them
3273
3393
  const limitPayloadSize = options => {
3274
3394
  // the smallest of 1MB or the specified limit if there is one
3275
- const limit = Math.min(1000000, options.payloadSizeLimitBytes ?? 1000000);
3395
+ const limit = Math.min(1000000, options.payloadSizeLimitBytes);
3276
3396
  return data => {
3277
3397
  if (data?.requestBody) {
3278
3398
  data.requestBody = enforcePayloadSizeLimit(data.requestBody, data.requestHeaders, limit, 'Request');
@@ -3330,7 +3450,7 @@ const buildNetworkRequestOptions = (instanceConfig, remoteNetworkOptions = {}) =
3330
3450
  const enforcedCleaningFn = d => payloadLimiter(ignorePostHogPaths(removeAuthorizationHeader(d), instanceConfig.host || ''));
3331
3451
  const hasDeprecatedMaskFunction = core.isFunction(sessionRecordingConfig.maskNetworkRequestFn);
3332
3452
  if (hasDeprecatedMaskFunction && core.isFunction(sessionRecordingConfig.maskCapturedNetworkRequestFn)) {
3333
- logger$2.warn('Both `maskNetworkRequestFn` and `maskCapturedNetworkRequestFn` are defined. `maskNetworkRequestFn` will be ignored.');
3453
+ logger$1.warn('Both `maskNetworkRequestFn` and `maskCapturedNetworkRequestFn` are defined. `maskNetworkRequestFn` will be ignored.');
3334
3454
  }
3335
3455
  if (hasDeprecatedMaskFunction) {
3336
3456
  sessionRecordingConfig.maskCapturedNetworkRequestFn = data => {
@@ -3357,666 +3477,6 @@ const buildNetworkRequestOptions = (instanceConfig, remoteNetworkOptions = {}) =
3357
3477
  };
3358
3478
  };
3359
3479
 
3360
- /// <reference lib="dom" />
3361
- const logger$1 = createLogger('[Recorder]');
3362
- const isNavigationTiming = entry => entry.entryType === 'navigation';
3363
- const isResourceTiming = entry => entry.entryType === 'resource';
3364
- function findLast(array, predicate) {
3365
- const length = array.length;
3366
- for (let i = length - 1; i >= 0; i -= 1) {
3367
- if (predicate(array[i])) {
3368
- return array[i];
3369
- }
3370
- }
3371
- return undefined;
3372
- }
3373
- function isDocument(value) {
3374
- return !!value && typeof value === 'object' && 'nodeType' in value && value.nodeType === 9;
3375
- }
3376
- function initPerformanceObserver(cb, win, options) {
3377
- // if we are only observing timings then we could have a single observer for all types, with buffer true,
3378
- // but we are going to filter by initiatorType _if we are wrapping fetch and xhr as the wrapped functions
3379
- // will deal with those.
3380
- // so we have a block which captures requests from before fetch/xhr is wrapped
3381
- // these are marked `isInitial` so playback can display them differently if needed
3382
- // they will never have method/status/headers/body because they are pre-wrapping that provides that
3383
- if (options.recordInitialRequests) {
3384
- const initialPerformanceEntries = win.performance.getEntries().filter(entry => isNavigationTiming(entry) || isResourceTiming(entry) && options.initiatorTypes.includes(entry.initiatorType));
3385
- cb({
3386
- requests: initialPerformanceEntries.flatMap(entry => prepareRequest({
3387
- entry,
3388
- method: undefined,
3389
- status: undefined,
3390
- networkRequest: {},
3391
- isInitial: true
3392
- })),
3393
- isInitial: true
3394
- });
3395
- }
3396
- const observer = new win.PerformanceObserver(entries => {
3397
- // if recordBody or recordHeaders is true then we don't want to record fetch or xhr here
3398
- // as the wrapped functions will do that. Otherwise, this filter becomes a noop
3399
- // because we do want to record them here
3400
- const wrappedInitiatorFilter = entry => options.recordBody || options.recordHeaders ? entry.initiatorType !== 'xmlhttprequest' && entry.initiatorType !== 'fetch' : true;
3401
- const performanceEntries = entries.getEntries().filter(entry => isNavigationTiming(entry) || isResourceTiming(entry) && options.initiatorTypes.includes(entry.initiatorType) &&
3402
- // TODO if we are _only_ capturing timing we don't want to filter initiator here
3403
- wrappedInitiatorFilter(entry));
3404
- cb({
3405
- requests: performanceEntries.flatMap(entry => prepareRequest({
3406
- entry,
3407
- method: undefined,
3408
- status: undefined,
3409
- networkRequest: {}
3410
- }))
3411
- });
3412
- });
3413
- // compat checked earlier
3414
- // eslint-disable-next-line compat/compat
3415
- const entryTypes = PerformanceObserver.supportedEntryTypes.filter(x => options.performanceEntryTypeToObserve.includes(x));
3416
- // initial records are gathered above, so we don't need to observe and buffer each type separately
3417
- observer.observe({
3418
- entryTypes
3419
- });
3420
- return () => {
3421
- observer.disconnect();
3422
- };
3423
- }
3424
- function shouldRecordHeaders(type, recordHeaders) {
3425
- return !!recordHeaders && (core.isBoolean(recordHeaders) || recordHeaders[type]);
3426
- }
3427
- function shouldRecordBody({
3428
- type,
3429
- recordBody,
3430
- headers,
3431
- url
3432
- }) {
3433
- function matchesContentType(contentTypes) {
3434
- const contentTypeHeader = Object.keys(headers).find(key => key.toLowerCase() === 'content-type');
3435
- const contentType = contentTypeHeader && headers[contentTypeHeader];
3436
- return contentTypes.some(ct => contentType?.includes(ct));
3437
- }
3438
- /**
3439
- * particularly in canvas applications we see many requests to blob URLs
3440
- * e.g. blob:https://video_url
3441
- * these blob/object URLs are local to the browser, we can never capture that body
3442
- * so we can just return false here
3443
- */
3444
- function isBlobURL(url) {
3445
- try {
3446
- if (typeof url === 'string') {
3447
- return url.startsWith('blob:');
3448
- }
3449
- if (url instanceof URL) {
3450
- return url.protocol === 'blob:';
3451
- }
3452
- if (url instanceof Request) {
3453
- return isBlobURL(url.url);
3454
- }
3455
- return false;
3456
- } catch {
3457
- return false;
3458
- }
3459
- }
3460
- if (!recordBody) return false;
3461
- if (isBlobURL(url)) return false;
3462
- if (core.isBoolean(recordBody)) return true;
3463
- if (core.isArray(recordBody)) return matchesContentType(recordBody);
3464
- const recordBodyType = recordBody[type];
3465
- if (core.isBoolean(recordBodyType)) return recordBodyType;
3466
- return matchesContentType(recordBodyType);
3467
- }
3468
- async function getRequestPerformanceEntry(win, initiatorType, url, start, end, attempt = 0) {
3469
- if (attempt > 10) {
3470
- logger$1.warn('Failed to get performance entry for request', {
3471
- url,
3472
- initiatorType
3473
- });
3474
- return null;
3475
- }
3476
- const urlPerformanceEntries = win.performance.getEntriesByName(url);
3477
- const performanceEntry = findLast(urlPerformanceEntries, entry => isResourceTiming(entry) && entry.initiatorType === initiatorType && (core.isUndefined(start) || entry.startTime >= start) && (core.isUndefined(end) || entry.startTime <= end));
3478
- if (!performanceEntry) {
3479
- await new Promise(resolve => setTimeout(resolve, 50 * attempt));
3480
- return getRequestPerformanceEntry(win, initiatorType, url, start, end, attempt + 1);
3481
- }
3482
- return performanceEntry;
3483
- }
3484
- /**
3485
- * According to MDN https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/response
3486
- * xhr response is typed as any but can be an ArrayBuffer, a Blob, a Document, a JavaScript object,
3487
- * or a string, depending on the value of XMLHttpRequest.responseType, that contains the response entity body.
3488
- *
3489
- * XHR request body is Document | XMLHttpRequestBodyInit | null | undefined
3490
- */
3491
- function _tryReadXHRBody({
3492
- body,
3493
- options,
3494
- url
3495
- }) {
3496
- if (core.isNullish(body)) {
3497
- return null;
3498
- }
3499
- const {
3500
- hostname,
3501
- isHostDenied
3502
- } = isHostOnDenyList(url, options);
3503
- if (isHostDenied) {
3504
- return hostname + ' is in deny list';
3505
- }
3506
- if (core.isString(body)) {
3507
- return body;
3508
- }
3509
- if (isDocument(body)) {
3510
- return body.textContent;
3511
- }
3512
- if (core.isFormData(body)) {
3513
- return formDataToQuery(body);
3514
- }
3515
- if (core.isObject(body)) {
3516
- try {
3517
- return JSON.stringify(body);
3518
- } catch {
3519
- return '[SessionReplay] Failed to stringify response object';
3520
- }
3521
- }
3522
- return '[SessionReplay] Cannot read body of type ' + toString.call(body);
3523
- }
3524
- function initXhrObserver(cb, win, options) {
3525
- if (!options.initiatorTypes.includes('xmlhttprequest')) {
3526
- return () => {
3527
- //
3528
- };
3529
- }
3530
- const recordRequestHeaders = shouldRecordHeaders('request', options.recordHeaders);
3531
- const recordResponseHeaders = shouldRecordHeaders('response', options.recordHeaders);
3532
- const restorePatch = patch(win.XMLHttpRequest.prototype, 'open',
3533
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
3534
- // @ts-ignore
3535
- originalOpen => {
3536
- return function (method, url, async = true, username, password) {
3537
- // because this function is returned in its actual context `this` _is_ an XMLHttpRequest
3538
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
3539
- // @ts-ignore
3540
- const xhr = this;
3541
- // check IE earlier than this, we only initialize if Request is present
3542
- // eslint-disable-next-line compat/compat
3543
- const req = new Request(url);
3544
- const networkRequest = {};
3545
- let start;
3546
- let end;
3547
- const requestHeaders = {};
3548
- const originalSetRequestHeader = xhr.setRequestHeader.bind(xhr);
3549
- xhr.setRequestHeader = (header, value) => {
3550
- requestHeaders[header] = value;
3551
- return originalSetRequestHeader(header, value);
3552
- };
3553
- if (recordRequestHeaders) {
3554
- networkRequest.requestHeaders = requestHeaders;
3555
- }
3556
- const originalSend = xhr.send.bind(xhr);
3557
- xhr.send = body => {
3558
- if (shouldRecordBody({
3559
- type: 'request',
3560
- headers: requestHeaders,
3561
- url,
3562
- recordBody: options.recordBody
3563
- })) {
3564
- networkRequest.requestBody = _tryReadXHRBody({
3565
- body,
3566
- options,
3567
- url
3568
- });
3569
- }
3570
- start = win.performance.now();
3571
- return originalSend(body);
3572
- };
3573
- const readyStateListener = () => {
3574
- if (xhr.readyState !== xhr.DONE) {
3575
- return;
3576
- }
3577
- // Clean up the listener immediately when done to prevent memory leaks
3578
- xhr.removeEventListener('readystatechange', readyStateListener);
3579
- end = win.performance.now();
3580
- const responseHeaders = {};
3581
- const rawHeaders = xhr.getAllResponseHeaders();
3582
- const headers = rawHeaders.trim().split(/[\r\n]+/);
3583
- headers.forEach(line => {
3584
- const parts = line.split(': ');
3585
- const header = parts.shift();
3586
- const value = parts.join(': ');
3587
- if (header) {
3588
- responseHeaders[header] = value;
3589
- }
3590
- });
3591
- if (recordResponseHeaders) {
3592
- networkRequest.responseHeaders = responseHeaders;
3593
- }
3594
- if (shouldRecordBody({
3595
- type: 'response',
3596
- headers: responseHeaders,
3597
- url,
3598
- recordBody: options.recordBody
3599
- })) {
3600
- networkRequest.responseBody = _tryReadXHRBody({
3601
- body: xhr.response,
3602
- options,
3603
- url
3604
- });
3605
- }
3606
- getRequestPerformanceEntry(win, 'xmlhttprequest', req.url, start, end).then(entry => {
3607
- const requests = prepareRequest({
3608
- entry,
3609
- method: method,
3610
- status: xhr?.status,
3611
- networkRequest,
3612
- start,
3613
- end,
3614
- url: url.toString(),
3615
- initiatorType: 'xmlhttprequest'
3616
- });
3617
- cb({
3618
- requests
3619
- });
3620
- }).catch(() => {
3621
- //
3622
- });
3623
- };
3624
- // This is very tricky code, and making it passive won't bring many performance benefits,
3625
- // so let's ignore the rule here.
3626
- // eslint-disable-next-line posthog-js/no-add-event-listener
3627
- xhr.addEventListener('readystatechange', readyStateListener);
3628
- originalOpen.call(xhr, method, url, async, username, password);
3629
- };
3630
- });
3631
- return () => {
3632
- restorePatch();
3633
- };
3634
- }
3635
- /**
3636
- * Check if this PerformanceEntry is either a PerformanceResourceTiming or a PerformanceNavigationTiming
3637
- * NB PerformanceNavigationTiming extends PerformanceResourceTiming
3638
- * Here we don't care which interface it implements as both expose `serverTimings`
3639
- */
3640
- const exposesServerTiming = event => !core.isNull(event) && (event.entryType === 'navigation' || event.entryType === 'resource');
3641
- function prepareRequest({
3642
- entry,
3643
- method,
3644
- status,
3645
- networkRequest,
3646
- isInitial,
3647
- start,
3648
- end,
3649
- url,
3650
- initiatorType
3651
- }) {
3652
- start = entry ? entry.startTime : start;
3653
- end = entry ? entry.responseEnd : end;
3654
- // kudos to sentry javascript sdk for excellent background on why to use Date.now() here
3655
- // https://github.com/getsentry/sentry-javascript/blob/e856e40b6e71a73252e788cd42b5260f81c9c88e/packages/utils/src/time.ts#L70
3656
- // can't start observer if performance.now() is not available
3657
- // eslint-disable-next-line compat/compat
3658
- const timeOrigin = Math.floor(Date.now() - performance.now());
3659
- // clickhouse can't ingest timestamps that are floats
3660
- // (in this case representing fractions of a millisecond we don't care about anyway)
3661
- // use timeOrigin if we really can't gather a start time
3662
- const timestamp = Math.floor(timeOrigin + (start || 0));
3663
- const entryJSON = entry ? entry.toJSON() : {
3664
- name: url
3665
- };
3666
- const requests = [{
3667
- ...entryJSON,
3668
- startTime: core.isUndefined(start) ? undefined : Math.round(start),
3669
- endTime: core.isUndefined(end) ? undefined : Math.round(end),
3670
- timeOrigin,
3671
- timestamp,
3672
- method: method,
3673
- initiatorType: initiatorType ? initiatorType : entry ? entry.initiatorType : undefined,
3674
- status,
3675
- requestHeaders: networkRequest.requestHeaders,
3676
- requestBody: networkRequest.requestBody,
3677
- responseHeaders: networkRequest.responseHeaders,
3678
- responseBody: networkRequest.responseBody,
3679
- isInitial
3680
- }];
3681
- if (exposesServerTiming(entry)) {
3682
- for (const timing of entry.serverTiming || []) {
3683
- requests.push({
3684
- timeOrigin,
3685
- timestamp,
3686
- startTime: Math.round(entry.startTime),
3687
- name: timing.name,
3688
- duration: timing.duration,
3689
- // the spec has a closed list of possible types
3690
- // https://developer.mozilla.org/en-US/docs/Web/API/PerformanceEntry/entryType
3691
- // but, we need to know this was a server timing so that we know to
3692
- // match it to the appropriate navigation or resource timing
3693
- // that matching will have to be on timestamp and $current_url
3694
- entryType: 'serverTiming'
3695
- });
3696
- }
3697
- }
3698
- return requests;
3699
- }
3700
- const contentTypePrefixDenyList = ['video/', 'audio/'];
3701
- function _checkForCannotReadResponseBody({
3702
- r,
3703
- options,
3704
- url
3705
- }) {
3706
- if (r.headers.get('Transfer-Encoding') === 'chunked') {
3707
- return 'Chunked Transfer-Encoding is not supported';
3708
- }
3709
- // `get` and `has` are case-insensitive
3710
- // but return the header value with the casing that was supplied
3711
- const contentType = r.headers.get('Content-Type')?.toLowerCase();
3712
- const contentTypeIsDenied = contentTypePrefixDenyList.some(prefix => contentType?.startsWith(prefix));
3713
- if (contentType && contentTypeIsDenied) {
3714
- return `Content-Type ${contentType} is not supported`;
3715
- }
3716
- const {
3717
- hostname,
3718
- isHostDenied
3719
- } = isHostOnDenyList(url, options);
3720
- if (isHostDenied) {
3721
- return hostname + ' is in deny list';
3722
- }
3723
- return null;
3724
- }
3725
- function _tryReadBody(r) {
3726
- // there are now already multiple places where we're using Promise...
3727
- // eslint-disable-next-line compat/compat
3728
- return new Promise((resolve, reject) => {
3729
- const timeout = setTimeout(() => resolve('[SessionReplay] Timeout while trying to read body'), 500);
3730
- try {
3731
- r.clone().text().then(txt => resolve(txt), reason => reject(reason)).finally(() => clearTimeout(timeout));
3732
- } catch {
3733
- clearTimeout(timeout);
3734
- resolve('[SessionReplay] Failed to read body');
3735
- }
3736
- });
3737
- }
3738
- async function _tryReadRequestBody({
3739
- r,
3740
- options,
3741
- url
3742
- }) {
3743
- const {
3744
- hostname,
3745
- isHostDenied
3746
- } = isHostOnDenyList(url, options);
3747
- if (isHostDenied) {
3748
- return Promise.resolve(hostname + ' is in deny list');
3749
- }
3750
- return _tryReadBody(r);
3751
- }
3752
- async function _tryReadResponseBody({
3753
- r,
3754
- options,
3755
- url
3756
- }) {
3757
- const cannotReadBodyReason = _checkForCannotReadResponseBody({
3758
- r,
3759
- options,
3760
- url
3761
- });
3762
- if (!core.isNull(cannotReadBodyReason)) {
3763
- return Promise.resolve(cannotReadBodyReason);
3764
- }
3765
- return _tryReadBody(r);
3766
- }
3767
- function initFetchObserver(cb, win, options) {
3768
- if (!options.initiatorTypes.includes('fetch')) {
3769
- return () => {
3770
- //
3771
- };
3772
- }
3773
- const recordRequestHeaders = shouldRecordHeaders('request', options.recordHeaders);
3774
- const recordResponseHeaders = shouldRecordHeaders('response', options.recordHeaders);
3775
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
3776
- // @ts-ignore
3777
- const restorePatch = patch(win, 'fetch', originalFetch => {
3778
- return async function (url, init) {
3779
- // check IE earlier than this, we only initialize if Request is present
3780
- // eslint-disable-next-line compat/compat
3781
- const req = new Request(url, init);
3782
- let res;
3783
- const networkRequest = {};
3784
- let start;
3785
- let end;
3786
- try {
3787
- const requestHeaders = {};
3788
- req.headers.forEach((value, header) => {
3789
- requestHeaders[header] = value;
3790
- });
3791
- if (recordRequestHeaders) {
3792
- networkRequest.requestHeaders = requestHeaders;
3793
- }
3794
- if (shouldRecordBody({
3795
- type: 'request',
3796
- headers: requestHeaders,
3797
- url,
3798
- recordBody: options.recordBody
3799
- })) {
3800
- networkRequest.requestBody = await _tryReadRequestBody({
3801
- r: req,
3802
- options,
3803
- url
3804
- });
3805
- }
3806
- start = win.performance.now();
3807
- res = await originalFetch(req);
3808
- end = win.performance.now();
3809
- const responseHeaders = {};
3810
- res.headers.forEach((value, header) => {
3811
- responseHeaders[header] = value;
3812
- });
3813
- if (recordResponseHeaders) {
3814
- networkRequest.responseHeaders = responseHeaders;
3815
- }
3816
- if (shouldRecordBody({
3817
- type: 'response',
3818
- headers: responseHeaders,
3819
- url,
3820
- recordBody: options.recordBody
3821
- })) {
3822
- networkRequest.responseBody = await _tryReadResponseBody({
3823
- r: res,
3824
- options,
3825
- url
3826
- });
3827
- }
3828
- return res;
3829
- } finally {
3830
- getRequestPerformanceEntry(win, 'fetch', req.url, start, end).then(entry => {
3831
- const requests = prepareRequest({
3832
- entry,
3833
- method: req.method,
3834
- status: res?.status,
3835
- networkRequest,
3836
- start,
3837
- end,
3838
- url: req.url,
3839
- initiatorType: 'fetch'
3840
- });
3841
- cb({
3842
- requests
3843
- });
3844
- }).catch(() => {
3845
- //
3846
- });
3847
- }
3848
- };
3849
- });
3850
- return () => {
3851
- restorePatch();
3852
- };
3853
- }
3854
- let initialisedHandler = null;
3855
- function initNetworkObserver(callback, win,
3856
- // top window or in an iframe
3857
- options) {
3858
- if (!('performance' in win)) {
3859
- return () => {
3860
- //
3861
- };
3862
- }
3863
- if (initialisedHandler) {
3864
- logger$1.warn('Network observer already initialised, doing nothing');
3865
- return () => {
3866
- // the first caller should already have this handler and will be responsible for teardown
3867
- };
3868
- }
3869
- const networkOptions = options ? Object.assign({}, defaultNetworkOptions, options) : defaultNetworkOptions;
3870
- const cb = data => {
3871
- const requests = [];
3872
- data.requests.forEach(request => {
3873
- const maskedRequest = networkOptions.maskRequestFn(request);
3874
- if (maskedRequest) {
3875
- requests.push(maskedRequest);
3876
- }
3877
- });
3878
- if (requests.length > 0) {
3879
- callback({
3880
- ...data,
3881
- requests
3882
- });
3883
- }
3884
- };
3885
- const performanceObserver = initPerformanceObserver(cb, win, networkOptions);
3886
- // only wrap fetch and xhr if headers or body are being recorded
3887
- let xhrObserver = () => {};
3888
- let fetchObserver = () => {};
3889
- if (networkOptions.recordHeaders || networkOptions.recordBody) {
3890
- xhrObserver = initXhrObserver(cb, win, networkOptions);
3891
- fetchObserver = initFetchObserver(cb, win, networkOptions);
3892
- }
3893
- const teardown = () => {
3894
- performanceObserver();
3895
- xhrObserver();
3896
- fetchObserver();
3897
- // allow future observers to initialize after cleanup
3898
- initialisedHandler = null;
3899
- };
3900
- initialisedHandler = teardown;
3901
- return teardown;
3902
- }
3903
- // use the plugin name so that when this functionality is adopted into rrweb
3904
- // we can remove this plugin and use the core functionality with the same data
3905
- const NETWORK_PLUGIN_NAME = 'rrweb/network@1';
3906
- // TODO how should this be typed?
3907
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
3908
- // @ts-ignore
3909
- const getRecordNetworkPlugin = options => {
3910
- return {
3911
- name: NETWORK_PLUGIN_NAME,
3912
- observer: initNetworkObserver,
3913
- options: options
3914
- };
3915
- };
3916
- // rrweb/networ@1 ends
3917
-
3918
- // Use a safe global target (prefer `win`, fallback to globalThis)
3919
- const _target = win ?? globalThis;
3920
- _target.__PosthogExtensions__ = _target.__PosthogExtensions__ || {};
3921
- // Expose rrweb.record under the same contract
3922
- _target.__PosthogExtensions__.rrweb = _target.__PosthogExtensions__.rrweb || {
3923
- record: record.record
3924
- };
3925
- // Provide initSessionRecording if not present — return a new LazyLoadedSessionRecording when called
3926
- _target.__PosthogExtensions__.initSessionRecording = _target.__PosthogExtensions__.initSessionRecording || (instance => {
3927
- return new LazyLoadedSessionRecording(instance);
3928
- });
3929
- // Provide a no-op loadExternalDependency that calls the callback immediately (since rrweb is bundled)
3930
- _target.__PosthogExtensions__.loadExternalDependency = _target.__PosthogExtensions__.loadExternalDependency || ((instance, scriptName, cb) => {
3931
- if (cb) cb(undefined);
3932
- });
3933
- // Provide rrwebPlugins object with network plugin factory if not present
3934
- _target.__PosthogExtensions__.rrwebPlugins = _target.__PosthogExtensions__.rrwebPlugins || {};
3935
- _target.__PosthogExtensions__.rrwebPlugins.getRecordNetworkPlugin = _target.__PosthogExtensions__.rrwebPlugins.getRecordNetworkPlugin || (() => getRecordNetworkPlugin);
3936
-
3937
- // Type definitions copied from @rrweb/types@2.0.0-alpha.17 and rrweb-snapshot@2.0.0-alpha.17
3938
- // Both packages are MIT licensed: https://github.com/rrweb-io/rrweb
3939
- //
3940
- // These types are copied here to avoid requiring users to install peer dependencies
3941
- // solely for TypeScript type information.
3942
- //
3943
- // Original sources:
3944
- // - @rrweb/types: https://github.com/rrweb-io/rrweb/tree/main/packages/@rrweb/types
3945
- // - rrweb-snapshot: https://github.com/rrweb-io/rrweb/tree/main/packages/rrweb-snapshot
3946
- var NodeType;
3947
- (function (NodeType) {
3948
- NodeType[NodeType["Document"] = 0] = "Document";
3949
- NodeType[NodeType["DocumentType"] = 1] = "DocumentType";
3950
- NodeType[NodeType["Element"] = 2] = "Element";
3951
- NodeType[NodeType["Text"] = 3] = "Text";
3952
- NodeType[NodeType["CDATA"] = 4] = "CDATA";
3953
- NodeType[NodeType["Comment"] = 5] = "Comment";
3954
- })(NodeType || (NodeType = {}));
3955
- var EventType;
3956
- (function (EventType) {
3957
- EventType[EventType["DomContentLoaded"] = 0] = "DomContentLoaded";
3958
- EventType[EventType["Load"] = 1] = "Load";
3959
- EventType[EventType["FullSnapshot"] = 2] = "FullSnapshot";
3960
- EventType[EventType["IncrementalSnapshot"] = 3] = "IncrementalSnapshot";
3961
- EventType[EventType["Meta"] = 4] = "Meta";
3962
- EventType[EventType["Custom"] = 5] = "Custom";
3963
- EventType[EventType["Plugin"] = 6] = "Plugin";
3964
- })(EventType || (EventType = {}));
3965
- var IncrementalSource;
3966
- (function (IncrementalSource) {
3967
- IncrementalSource[IncrementalSource["Mutation"] = 0] = "Mutation";
3968
- IncrementalSource[IncrementalSource["MouseMove"] = 1] = "MouseMove";
3969
- IncrementalSource[IncrementalSource["MouseInteraction"] = 2] = "MouseInteraction";
3970
- IncrementalSource[IncrementalSource["Scroll"] = 3] = "Scroll";
3971
- IncrementalSource[IncrementalSource["ViewportResize"] = 4] = "ViewportResize";
3972
- IncrementalSource[IncrementalSource["Input"] = 5] = "Input";
3973
- IncrementalSource[IncrementalSource["TouchMove"] = 6] = "TouchMove";
3974
- IncrementalSource[IncrementalSource["MediaInteraction"] = 7] = "MediaInteraction";
3975
- IncrementalSource[IncrementalSource["StyleSheetRule"] = 8] = "StyleSheetRule";
3976
- IncrementalSource[IncrementalSource["CanvasMutation"] = 9] = "CanvasMutation";
3977
- IncrementalSource[IncrementalSource["Font"] = 10] = "Font";
3978
- IncrementalSource[IncrementalSource["Log"] = 11] = "Log";
3979
- IncrementalSource[IncrementalSource["Drag"] = 12] = "Drag";
3980
- IncrementalSource[IncrementalSource["StyleDeclaration"] = 13] = "StyleDeclaration";
3981
- IncrementalSource[IncrementalSource["Selection"] = 14] = "Selection";
3982
- IncrementalSource[IncrementalSource["AdoptedStyleSheet"] = 15] = "AdoptedStyleSheet";
3983
- IncrementalSource[IncrementalSource["CustomElement"] = 16] = "CustomElement";
3984
- })(IncrementalSource || (IncrementalSource = {}));
3985
- var MouseInteractions;
3986
- (function (MouseInteractions) {
3987
- MouseInteractions[MouseInteractions["MouseUp"] = 0] = "MouseUp";
3988
- MouseInteractions[MouseInteractions["MouseDown"] = 1] = "MouseDown";
3989
- MouseInteractions[MouseInteractions["Click"] = 2] = "Click";
3990
- MouseInteractions[MouseInteractions["ContextMenu"] = 3] = "ContextMenu";
3991
- MouseInteractions[MouseInteractions["DblClick"] = 4] = "DblClick";
3992
- MouseInteractions[MouseInteractions["Focus"] = 5] = "Focus";
3993
- MouseInteractions[MouseInteractions["Blur"] = 6] = "Blur";
3994
- MouseInteractions[MouseInteractions["TouchStart"] = 7] = "TouchStart";
3995
- MouseInteractions[MouseInteractions["TouchMove_Departed"] = 8] = "TouchMove_Departed";
3996
- MouseInteractions[MouseInteractions["TouchEnd"] = 9] = "TouchEnd";
3997
- MouseInteractions[MouseInteractions["TouchCancel"] = 10] = "TouchCancel";
3998
- })(MouseInteractions || (MouseInteractions = {}));
3999
- var PointerTypes;
4000
- (function (PointerTypes) {
4001
- PointerTypes[PointerTypes["Mouse"] = 0] = "Mouse";
4002
- PointerTypes[PointerTypes["Pen"] = 1] = "Pen";
4003
- PointerTypes[PointerTypes["Touch"] = 2] = "Touch";
4004
- })(PointerTypes || (PointerTypes = {}));
4005
- var MediaInteractions;
4006
- (function (MediaInteractions) {
4007
- MediaInteractions[MediaInteractions["Play"] = 0] = "Play";
4008
- MediaInteractions[MediaInteractions["Pause"] = 1] = "Pause";
4009
- MediaInteractions[MediaInteractions["Seeked"] = 2] = "Seeked";
4010
- MediaInteractions[MediaInteractions["VolumeChange"] = 3] = "VolumeChange";
4011
- MediaInteractions[MediaInteractions["RateChange"] = 4] = "RateChange";
4012
- })(MediaInteractions || (MediaInteractions = {}));
4013
- var CanvasContext;
4014
- (function (CanvasContext) {
4015
- CanvasContext[CanvasContext["2D"] = 0] = "2D";
4016
- CanvasContext[CanvasContext["WebGL"] = 1] = "WebGL";
4017
- CanvasContext[CanvasContext["WebGL2"] = 2] = "WebGL2";
4018
- })(CanvasContext || (CanvasContext = {}));
4019
-
4020
3480
  const DISABLED = 'disabled';
4021
3481
  const SAMPLED = 'sampled';
4022
3482
  const ACTIVE = 'active';
@@ -4424,7 +3884,7 @@ class MutationThrottler {
4424
3884
  refillInterval: 1000,
4425
3885
  // one second
4426
3886
  _onBucketRateLimited: this._onNodeRateLimited,
4427
- _logger: logger$2
3887
+ _logger: logger$1
4428
3888
  });
4429
3889
  }
4430
3890
  reset() {
@@ -4448,9 +3908,10 @@ function simpleHash(str) {
4448
3908
  * receives percent as a number between 0 and 1
4449
3909
  */
4450
3910
  function sampleOnProperty(prop, percent) {
4451
- return simpleHash(prop) % 100 < core.clampToRange(percent * 100, 0, 100, logger$2);
3911
+ return simpleHash(prop) % 100 < core.clampToRange(percent * 100, 0, 100, logger$1);
4452
3912
  }
4453
3913
 
3914
+ /* eslint-disable posthog-js/no-direct-function-check */
4454
3915
  const BASE_ENDPOINT = '/s/';
4455
3916
  const DEFAULT_CANVAS_QUALITY = 0.4;
4456
3917
  const DEFAULT_CANVAS_FPS = 4;
@@ -4461,6 +3922,19 @@ const ONE_KB = 1024;
4461
3922
  const ONE_MINUTE = 1000 * 60;
4462
3923
  const FIVE_MINUTES = ONE_MINUTE * 5;
4463
3924
  const RECORDING_IDLE_THRESHOLD_MS = FIVE_MINUTES;
3925
+ // Register a factory on the global extensions object so the extension shim can
3926
+ // instantiate a LazyLoadedSessionRecording without importing this module directly.
3927
+ try {
3928
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3929
+ const ext = globalThis.__PosthogExtensions__;
3930
+ if (ext) {
3931
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3932
+ ext._initSessionRecordingFactory = ext._initSessionRecordingFactory || (instance => new LazyLoadedSessionRecording(instance));
3933
+ }
3934
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
3935
+ } catch (e) {
3936
+ // ignore
3937
+ }
4464
3938
  const RECORDING_MAX_EVENT_SIZE = ONE_KB * ONE_KB * 0.9; // ~1mb (with some wiggle room)
4465
3939
  const RECORDING_BUFFER_TIMEOUT = 2000; // 2 seconds
4466
3940
  const SESSION_RECORDING_BATCH_KEY = 'recordings';
@@ -4482,7 +3956,41 @@ function getRRWebRecord() {
4482
3956
  } catch {
4483
3957
  // ignore
4484
3958
  }
4485
- return record.record;
3959
+ // If we've previously loaded rrweb via dynamic import, return the cached reference
3960
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3961
+ const cached = getRRWebRecord._cachedRRWebRecord;
3962
+ return cached;
3963
+ }
3964
+ async function loadRRWeb() {
3965
+ try {
3966
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3967
+ const ext = globalThis.__PosthogExtensions__;
3968
+ if (ext && ext.rrweb && ext.rrweb.record) {
3969
+ ;
3970
+ getRRWebRecord._cachedRRWebRecord = ext.rrweb.record;
3971
+ return ext.rrweb.record;
3972
+ }
3973
+ // If already cached, return it
3974
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3975
+ const already = getRRWebRecord._cachedRRWebRecord;
3976
+ if (already) {
3977
+ return already;
3978
+ }
3979
+ // Dynamic import - let the bundler (IIFE build) include rrweb in the bundle or allow lazy-load
3980
+ // Note: we intentionally use a dynamic import so rrweb is not referenced at the module top-level
3981
+ // which would cause IIFE builds to assume a global is present at script execution.
3982
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3983
+ const mod = await import('@rrweb/record');
3984
+ const rr = mod && (mod.record ?? (mod.default && mod.default.record));
3985
+ if (rr) {
3986
+ ;
3987
+ getRRWebRecord._cachedRRWebRecord = rr;
3988
+ return rr;
3989
+ }
3990
+ } catch (e) {
3991
+ logger.error('could not dynamically load rrweb', e);
3992
+ }
3993
+ return null;
4486
3994
  }
4487
3995
  function gzipToString(data) {
4488
3996
  return fflate.strFromU8(fflate.gzipSync(fflate.strToU8(JSON.stringify(data))), true);
@@ -4739,7 +4247,13 @@ class LazyLoadedSessionRecording {
4739
4247
  if (this._networkPayloadCapture) {
4740
4248
  const canRecordNetwork = !isLocalhost() || this._forceAllowLocalhostNetworkCapture;
4741
4249
  if (canRecordNetwork) {
4742
- plugins.push(getRecordNetworkPlugin(buildNetworkRequestOptions(this._instance.config, this._networkPayloadCapture)));
4250
+ const assignableWindow = globalThis;
4251
+ const networkFactory = assignableWindow.__PosthogExtensions__?.rrwebPlugins?.getRecordNetworkPlugin?.();
4252
+ if (typeof networkFactory === 'function') {
4253
+ plugins.push(networkFactory(buildNetworkRequestOptions(this._instance.config, this._networkPayloadCapture)));
4254
+ } else {
4255
+ logger.info('Network plugin factory not available yet; skipping network plugin');
4256
+ }
4743
4257
  } else {
4744
4258
  logger.info('NetworkCapture not started because we are on localhost.');
4745
4259
  }
@@ -4899,7 +4413,7 @@ class LazyLoadedSessionRecording {
4899
4413
  const parsedConfig = core.isObject(persistedConfig) ? persistedConfig : JSON.parse(persistedConfig);
4900
4414
  return parsedConfig;
4901
4415
  }
4902
- start(startReason) {
4416
+ async start(startReason) {
4903
4417
  const config = this._remoteConfig;
4904
4418
  if (!config) {
4905
4419
  logger.info('remote config must be stored in persistence before recording can start');
@@ -4933,7 +4447,7 @@ class LazyLoadedSessionRecording {
4933
4447
  });
4934
4448
  });
4935
4449
  this._makeSamplingDecision(this.sessionId);
4936
- this._startRecorder();
4450
+ await this._startRecorder();
4937
4451
  // calling addEventListener multiple times is safe and will not add duplicates
4938
4452
  addEventListener(win, 'beforeunload', this._onBeforeUnload);
4939
4453
  addEventListener(win, 'offline', this._onOffline);
@@ -5389,7 +4903,7 @@ class LazyLoadedSessionRecording {
5389
4903
  $sdk_debug_session_start: sessionStartTimestamp
5390
4904
  };
5391
4905
  }
5392
- _startRecorder() {
4906
+ async _startRecorder() {
5393
4907
  if (this._stopRrweb) {
5394
4908
  return;
5395
4909
  }
@@ -5445,7 +4959,12 @@ class LazyLoadedSessionRecording {
5445
4959
  sessionRecordingOptions.maskTextSelector = this._masking.maskTextSelector ?? undefined;
5446
4960
  sessionRecordingOptions.blockSelector = this._masking.blockSelector ?? undefined;
5447
4961
  }
5448
- const rrwebRecord = getRRWebRecord();
4962
+ // Ensure rrweb is loaded (either via global extension or dynamic import)
4963
+ let rrwebRecord = getRRWebRecord();
4964
+ if (!rrwebRecord) {
4965
+ const loaded = await loadRRWeb();
4966
+ rrwebRecord = loaded ?? undefined;
4967
+ }
5449
4968
  if (!rrwebRecord) {
5450
4969
  logger.error('_startRecorder was called but rrwebRecord is not available. This indicates something has gone wrong.');
5451
4970
  return;
@@ -5487,11 +5006,12 @@ class LazyLoadedSessionRecording {
5487
5006
  }
5488
5007
  }
5489
5008
 
5009
+ /* eslint-disable posthog-js/no-direct-function-check */
5490
5010
  const LOGGER_PREFIX = '[SessionRecording]';
5491
5011
  const log = {
5492
- info: (...args) => logger$3.info(LOGGER_PREFIX, ...args),
5493
- warn: (...args) => logger$3.warn(LOGGER_PREFIX, ...args),
5494
- error: (...args) => logger$3.error(LOGGER_PREFIX, ...args)
5012
+ info: (...args) => logger$2.info(LOGGER_PREFIX, ...args),
5013
+ warn: (...args) => logger$2.warn(LOGGER_PREFIX, ...args),
5014
+ error: (...args) => logger$2.error(LOGGER_PREFIX, ...args)
5495
5015
  };
5496
5016
  class SessionRecording {
5497
5017
  get started() {
@@ -5555,6 +5075,18 @@ class SessionRecording {
5555
5075
  if (!this._isRecordingEnabled) {
5556
5076
  return;
5557
5077
  }
5078
+ // If extensions provide a loader, use it. Otherwise fallback to the local _onScriptLoaded which
5079
+ // will create the local LazyLoadedSessionRecording (so tests that mock it work correctly).
5080
+ const loader = assignableWindow.__PosthogExtensions__?.loadExternalDependency;
5081
+ if (typeof loader === 'function') {
5082
+ loader(this._instance, this._scriptName, err => {
5083
+ if (err) {
5084
+ return log.error('could not load recorder', err);
5085
+ }
5086
+ this._onScriptLoaded(startReason);
5087
+ });
5088
+ return;
5089
+ }
5558
5090
  this._onScriptLoaded(startReason);
5559
5091
  }
5560
5092
  stopRecording() {
@@ -5630,15 +5162,43 @@ class SessionRecording {
5630
5162
  if (this._lazyLoadedSessionRecording?.log) {
5631
5163
  this._lazyLoadedSessionRecording.log(message, level);
5632
5164
  } else {
5633
- logger$3.warn('log called before recorder was ready');
5165
+ logger$2.warn('log called before recorder was ready');
5634
5166
  }
5635
5167
  }
5168
+ get _scriptName() {
5169
+ const remoteConfig = this._instance?.persistence?.get_property(SESSION_RECORDING_REMOTE_CONFIG);
5170
+ return remoteConfig?.scriptConfig?.script || 'lazy-recorder';
5171
+ }
5636
5172
  _onScriptLoaded(startReason) {
5173
+ // If extensions provide an init function, use it. Otherwise, fall back to the local LazyLoadedSessionRecording
5174
+ if (assignableWindow.__PosthogExtensions__?.initSessionRecording) {
5175
+ if (!this._lazyLoadedSessionRecording) {
5176
+ this._lazyLoadedSessionRecording = assignableWindow.__PosthogExtensions__?.initSessionRecording(this._instance);
5177
+ this._lazyLoadedSessionRecording._forceAllowLocalhostNetworkCapture = this._forceAllowLocalhostNetworkCapture;
5178
+ }
5179
+ try {
5180
+ const maybePromise = this._lazyLoadedSessionRecording.start(startReason);
5181
+ if (maybePromise && typeof maybePromise.catch === 'function') {
5182
+ maybePromise.catch(e => logger$2.error('error starting session recording', e));
5183
+ }
5184
+ } catch (e) {
5185
+ logger$2.error('error starting session recording', e);
5186
+ }
5187
+ return;
5188
+ }
5637
5189
  if (!this._lazyLoadedSessionRecording) {
5638
5190
  this._lazyLoadedSessionRecording = new LazyLoadedSessionRecording(this._instance);
5639
5191
  this._lazyLoadedSessionRecording._forceAllowLocalhostNetworkCapture = this._forceAllowLocalhostNetworkCapture;
5640
5192
  }
5641
- this._lazyLoadedSessionRecording.start(startReason);
5193
+ // start may perform a dynamic import; handle both sync and Promise returns
5194
+ try {
5195
+ const maybePromise = this._lazyLoadedSessionRecording.start(startReason);
5196
+ if (maybePromise && typeof maybePromise.catch === 'function') {
5197
+ maybePromise.catch(e => logger$2.error('error starting session recording', e));
5198
+ }
5199
+ } catch (e) {
5200
+ logger$2.error('error starting session recording', e);
5201
+ }
5642
5202
  }
5643
5203
  /**
5644
5204
  * this is maintained on the public API only because it has always been on the public API
@@ -5895,7 +5455,7 @@ class Leanbase extends core.PostHogCore {
5895
5455
  };
5896
5456
  properties['distinct_id'] = persistenceProps.distinct_id;
5897
5457
  if (!(core.isString(properties['distinct_id']) || core.isNumber(properties['distinct_id'])) || core.isEmptyString(properties['distinct_id'])) {
5898
- logger$3.error('Invalid distinct_id for replay event. This indicates a bug in your implementation');
5458
+ logger$2.error('Invalid distinct_id for replay event. This indicates a bug in your implementation');
5899
5459
  }
5900
5460
  return properties;
5901
5461
  }
@@ -5970,11 +5530,11 @@ class Leanbase extends core.PostHogCore {
5970
5530
  return;
5971
5531
  }
5972
5532
  if (core.isUndefined(event) || !core.isString(event)) {
5973
- logger$3.error('No event name provided to posthog.capture');
5533
+ logger$2.error('No event name provided to posthog.capture');
5974
5534
  return;
5975
5535
  }
5976
5536
  if (properties?.$current_url && !core.isString(properties?.$current_url)) {
5977
- logger$3.error('Invalid `$current_url` property provided to `posthog.capture`. Input must be a string. Ignoring provided value.');
5537
+ logger$2.error('Invalid `$current_url` property provided to `posthog.capture`. Input must be a string. Ignoring provided value.');
5978
5538
  delete properties?.$current_url;
5979
5539
  }
5980
5540
  this.sessionPersistence.update_search_keyword();