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