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