@leanbase-giangnd/js 0.3.1 → 0.4.0
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 +836 -85
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +59 -43
- package/dist/index.mjs +837 -86
- package/dist/index.mjs.map +1 -1
- package/dist/leanbase.iife.js +2577 -510
- package/dist/leanbase.iife.js.map +1 -1
- package/package.json +2 -1
- package/src/extensions/replay/external/lazy-loaded-session-recorder.ts +104 -17
- package/src/extensions/replay/session-recording.ts +9 -6
- package/src/leanbase.ts +4 -1
- package/src/utils/request-router.ts +39 -0
- package/src/version.ts +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isArray, isNullish, isFormData, hasOwnProperty, isString, isNull, isNumber, PostHogPersistedProperty, isUndefined, isFunction, includes, stripLeadingDollar, isObject, isEmptyObject, trim, isBoolean, clampToRange, BucketedRateLimiter, PostHogCore, getFetch, isEmptyString } from '@posthog/core';
|
|
1
|
+
import { isArray, isNullish, isFormData, hasOwnProperty, isString, isNull, isNumber, PostHogPersistedProperty, isUndefined, isFile, isFunction, includes, stripLeadingDollar, isObject, isEmptyObject, trim, isBoolean, clampToRange, BucketedRateLimiter, PostHogCore, getFetch, isEmptyString } from '@posthog/core';
|
|
2
2
|
import { strFromU8, gzipSync, strToU8 } from 'fflate';
|
|
3
3
|
|
|
4
4
|
const breaker = {};
|
|
@@ -229,7 +229,7 @@ PostHogPersistedProperty.Queue, PostHogPersistedProperty.FeatureFlagDetails, Pos
|
|
|
229
229
|
|
|
230
230
|
/* eslint-disable no-console */
|
|
231
231
|
const PREFIX = '[Leanbase]';
|
|
232
|
-
const logger$
|
|
232
|
+
const logger$3 = {
|
|
233
233
|
info: (...args) => {
|
|
234
234
|
if (typeof console !== 'undefined') {
|
|
235
235
|
console.log(PREFIX, ...args);
|
|
@@ -527,7 +527,7 @@ function chooseCookieDomain(hostname, cross_subdomain) {
|
|
|
527
527
|
if (!matchedSubDomain) {
|
|
528
528
|
const originalMatch = originalCookieDomainFn(hostname);
|
|
529
529
|
if (originalMatch !== matchedSubDomain) {
|
|
530
|
-
logger$
|
|
530
|
+
logger$3.info('Warning: cookie subdomain discovery mismatch', originalMatch, matchedSubDomain);
|
|
531
531
|
}
|
|
532
532
|
matchedSubDomain = originalMatch;
|
|
533
533
|
}
|
|
@@ -539,7 +539,7 @@ function chooseCookieDomain(hostname, cross_subdomain) {
|
|
|
539
539
|
const cookieStore = {
|
|
540
540
|
_is_supported: () => !!document,
|
|
541
541
|
_error: function (msg) {
|
|
542
|
-
logger$
|
|
542
|
+
logger$3.error('cookieStore error: ' + msg);
|
|
543
543
|
},
|
|
544
544
|
_get: function (name) {
|
|
545
545
|
if (!document) {
|
|
@@ -588,7 +588,7 @@ const cookieStore = {
|
|
|
588
588
|
const new_cookie_val = name + '=' + encodeURIComponent(JSON.stringify(value)) + expires + '; SameSite=Lax; path=/' + cdomain + secure;
|
|
589
589
|
// 4096 bytes is the size at which some browsers (e.g. firefox) will not store a cookie, warn slightly before that
|
|
590
590
|
if (new_cookie_val.length > 4096 * 0.9) {
|
|
591
|
-
logger$
|
|
591
|
+
logger$3.warn('cookieStore warning: large cookie, len=' + new_cookie_val.length);
|
|
592
592
|
}
|
|
593
593
|
document.cookie = new_cookie_val;
|
|
594
594
|
return new_cookie_val;
|
|
@@ -630,13 +630,13 @@ const localStore = {
|
|
|
630
630
|
supported = false;
|
|
631
631
|
}
|
|
632
632
|
if (!supported) {
|
|
633
|
-
logger$
|
|
633
|
+
logger$3.error('localStorage unsupported; falling back to cookie store');
|
|
634
634
|
}
|
|
635
635
|
_localStorage_supported = supported;
|
|
636
636
|
return supported;
|
|
637
637
|
},
|
|
638
638
|
_error: function (msg) {
|
|
639
|
-
logger$
|
|
639
|
+
logger$3.error('localStorage error: ' + msg);
|
|
640
640
|
},
|
|
641
641
|
_get: function (name) {
|
|
642
642
|
try {
|
|
@@ -722,7 +722,7 @@ const memoryStore = {
|
|
|
722
722
|
return true;
|
|
723
723
|
},
|
|
724
724
|
_error: function (msg) {
|
|
725
|
-
logger$
|
|
725
|
+
logger$3.error('memoryStorage error: ' + msg);
|
|
726
726
|
},
|
|
727
727
|
_get: function (name) {
|
|
728
728
|
return memoryStorage[name] || null;
|
|
@@ -763,7 +763,7 @@ const sessionStore = {
|
|
|
763
763
|
return sessionStorageSupported;
|
|
764
764
|
},
|
|
765
765
|
_error: function (msg) {
|
|
766
|
-
logger$
|
|
766
|
+
logger$3.error('sessionStorage error: ', msg);
|
|
767
767
|
},
|
|
768
768
|
_get: function (name) {
|
|
769
769
|
try {
|
|
@@ -812,6 +812,21 @@ const convertToURL = url => {
|
|
|
812
812
|
location.href = url;
|
|
813
813
|
return location;
|
|
814
814
|
};
|
|
815
|
+
const formDataToQuery = function (formdata, arg_separator = '&') {
|
|
816
|
+
let use_val;
|
|
817
|
+
let use_key;
|
|
818
|
+
const tph_arr = [];
|
|
819
|
+
each(formdata, function (val, key) {
|
|
820
|
+
// the key might be literally the string undefined for e.g. if {undefined: 'something'}
|
|
821
|
+
if (isUndefined(val) || isUndefined(key) || key === 'undefined') {
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
use_val = encodeURIComponent(isFile(val) ? val.name : val.toString());
|
|
825
|
+
use_key = encodeURIComponent(key);
|
|
826
|
+
tph_arr[tph_arr.length] = use_key + '=' + use_val;
|
|
827
|
+
});
|
|
828
|
+
return tph_arr.join(arg_separator);
|
|
829
|
+
};
|
|
815
830
|
// NOTE: Once we get rid of IE11/op_mini we can start using URLSearchParams
|
|
816
831
|
const getQueryParam = function (url, param) {
|
|
817
832
|
const withoutHash = url.split('#')[0] || '';
|
|
@@ -835,7 +850,7 @@ const getQueryParam = function (url, param) {
|
|
|
835
850
|
try {
|
|
836
851
|
result = decodeURIComponent(result);
|
|
837
852
|
} catch {
|
|
838
|
-
logger$
|
|
853
|
+
logger$3.error('Skipping decoding for malformed query param: ' + result);
|
|
839
854
|
}
|
|
840
855
|
return result.replace(/\+/g, ' ');
|
|
841
856
|
}
|
|
@@ -1169,7 +1184,7 @@ const detectDeviceType = function (user_agent) {
|
|
|
1169
1184
|
}
|
|
1170
1185
|
};
|
|
1171
1186
|
|
|
1172
|
-
var version = "0.
|
|
1187
|
+
var version = "0.4.0";
|
|
1173
1188
|
var packageInfo = {
|
|
1174
1189
|
version: version};
|
|
1175
1190
|
|
|
@@ -1434,7 +1449,7 @@ class LeanbasePersistence {
|
|
|
1434
1449
|
this._storage = this._buildStorage(config);
|
|
1435
1450
|
this.load();
|
|
1436
1451
|
if (config.debug) {
|
|
1437
|
-
logger$
|
|
1452
|
+
logger$3.info('Persistence loaded', config['persistence'], {
|
|
1438
1453
|
...this.props
|
|
1439
1454
|
});
|
|
1440
1455
|
}
|
|
@@ -1450,7 +1465,7 @@ class LeanbasePersistence {
|
|
|
1450
1465
|
}
|
|
1451
1466
|
_buildStorage(config) {
|
|
1452
1467
|
if (CASE_INSENSITIVE_PERSISTENCE_TYPES.indexOf(config['persistence'].toLowerCase()) === -1) {
|
|
1453
|
-
logger$
|
|
1468
|
+
logger$3.info('Unknown persistence type ' + config['persistence'] + '; falling back to localStorage+cookie');
|
|
1454
1469
|
config['persistence'] = 'localStorage+cookie';
|
|
1455
1470
|
}
|
|
1456
1471
|
let store;
|
|
@@ -2086,7 +2101,7 @@ function getNestedSpanText(target) {
|
|
|
2086
2101
|
text = `${text} ${getNestedSpanText(child)}`.trim();
|
|
2087
2102
|
}
|
|
2088
2103
|
} catch (e) {
|
|
2089
|
-
logger$
|
|
2104
|
+
logger$3.error('[AutoCapture]', e);
|
|
2090
2105
|
}
|
|
2091
2106
|
}
|
|
2092
2107
|
});
|
|
@@ -2388,7 +2403,7 @@ class Autocapture {
|
|
|
2388
2403
|
}
|
|
2389
2404
|
_addDomEventHandlers() {
|
|
2390
2405
|
if (!this.isBrowserSupported()) {
|
|
2391
|
-
logger$
|
|
2406
|
+
logger$3.info('Disabling Automatic Event Collection because this browser is not supported');
|
|
2392
2407
|
return;
|
|
2393
2408
|
}
|
|
2394
2409
|
if (!win || !document) {
|
|
@@ -2399,7 +2414,7 @@ class Autocapture {
|
|
|
2399
2414
|
try {
|
|
2400
2415
|
this._captureEvent(e);
|
|
2401
2416
|
} catch (error) {
|
|
2402
|
-
logger$
|
|
2417
|
+
logger$3.error('Failed to capture event', error);
|
|
2403
2418
|
}
|
|
2404
2419
|
};
|
|
2405
2420
|
addEventListener(document, 'submit', handler, {
|
|
@@ -2584,7 +2599,7 @@ class SessionIdManager {
|
|
|
2584
2599
|
this._windowIdGenerator = windowIdGenerator || uuidv7;
|
|
2585
2600
|
const persistenceName = this._config['persistence_name'] || this._config['token'];
|
|
2586
2601
|
const desiredTimeout = this._config['session_idle_timeout_seconds'] || DEFAULT_SESSION_IDLE_TIMEOUT_SECONDS;
|
|
2587
|
-
this._sessionTimeoutMs = clampToRange(desiredTimeout, MIN_SESSION_IDLE_TIMEOUT_SECONDS, MAX_SESSION_IDLE_TIMEOUT_SECONDS, logger$
|
|
2602
|
+
this._sessionTimeoutMs = clampToRange(desiredTimeout, MIN_SESSION_IDLE_TIMEOUT_SECONDS, MAX_SESSION_IDLE_TIMEOUT_SECONDS, logger$3, DEFAULT_SESSION_IDLE_TIMEOUT_SECONDS) * 1000;
|
|
2588
2603
|
instance.register({
|
|
2589
2604
|
$configured_session_timeout_ms: this._sessionTimeoutMs
|
|
2590
2605
|
});
|
|
@@ -2611,7 +2626,7 @@ class SessionIdManager {
|
|
|
2611
2626
|
const sessionStartTimestamp = uuid7ToTimestampMs(this._config.bootstrap.sessionID);
|
|
2612
2627
|
this._setSessionId(this._config.bootstrap.sessionID, new Date().getTime(), sessionStartTimestamp);
|
|
2613
2628
|
} catch (e) {
|
|
2614
|
-
logger$
|
|
2629
|
+
logger$3.error('Invalid sessionID in bootstrap', e);
|
|
2615
2630
|
}
|
|
2616
2631
|
}
|
|
2617
2632
|
this._listenToReloadWindow();
|
|
@@ -2752,7 +2767,7 @@ class SessionIdManager {
|
|
|
2752
2767
|
if (noSessionId || activityTimeout || sessionPastMaximumLength) {
|
|
2753
2768
|
sessionId = this._sessionIdGenerator();
|
|
2754
2769
|
windowId = this._windowIdGenerator();
|
|
2755
|
-
logger$
|
|
2770
|
+
logger$3.info('new session ID generated', {
|
|
2756
2771
|
sessionId,
|
|
2757
2772
|
windowId,
|
|
2758
2773
|
changeReason: {
|
|
@@ -2941,10 +2956,10 @@ class PageViewManager {
|
|
|
2941
2956
|
lastContentY = Math.ceil(lastContentY);
|
|
2942
2957
|
maxContentY = Math.ceil(maxContentY);
|
|
2943
2958
|
// if the maximum scroll height is near 0, then the percentage is 1
|
|
2944
|
-
const lastScrollPercentage = maxScrollHeight <= 1 ? 1 : clampToRange(lastScrollY / maxScrollHeight, 0, 1, logger$
|
|
2945
|
-
const maxScrollPercentage = maxScrollHeight <= 1 ? 1 : clampToRange(maxScrollY / maxScrollHeight, 0, 1, logger$
|
|
2946
|
-
const lastContentPercentage = maxContentHeight <= 1 ? 1 : clampToRange(lastContentY / maxContentHeight, 0, 1, logger$
|
|
2947
|
-
const maxContentPercentage = maxContentHeight <= 1 ? 1 : clampToRange(maxContentY / maxContentHeight, 0, 1, logger$
|
|
2959
|
+
const lastScrollPercentage = maxScrollHeight <= 1 ? 1 : clampToRange(lastScrollY / maxScrollHeight, 0, 1, logger$3);
|
|
2960
|
+
const maxScrollPercentage = maxScrollHeight <= 1 ? 1 : clampToRange(maxScrollY / maxScrollHeight, 0, 1, logger$3);
|
|
2961
|
+
const lastContentPercentage = maxContentHeight <= 1 ? 1 : clampToRange(lastContentY / maxContentHeight, 0, 1, logger$3);
|
|
2962
|
+
const maxContentPercentage = maxContentHeight <= 1 ? 1 : clampToRange(maxContentY / maxContentHeight, 0, 1, logger$3);
|
|
2948
2963
|
properties = extend(properties, {
|
|
2949
2964
|
$prev_pageview_last_scroll: lastScrollY,
|
|
2950
2965
|
$prev_pageview_last_scroll_percentage: lastScrollPercentage,
|
|
@@ -3209,17 +3224,17 @@ var CanvasContext;
|
|
|
3209
3224
|
|
|
3210
3225
|
const _createLogger = prefix => {
|
|
3211
3226
|
return {
|
|
3212
|
-
info: (...args) => logger$
|
|
3213
|
-
warn: (...args) => logger$
|
|
3214
|
-
error: (...args) => logger$
|
|
3215
|
-
critical: (...args) => logger$
|
|
3227
|
+
info: (...args) => logger$3.info(prefix, ...args),
|
|
3228
|
+
warn: (...args) => logger$3.warn(prefix, ...args),
|
|
3229
|
+
error: (...args) => logger$3.error(prefix, ...args),
|
|
3230
|
+
critical: (...args) => logger$3.critical(prefix, ...args),
|
|
3216
3231
|
uninitializedWarning: methodName => {
|
|
3217
|
-
logger$
|
|
3232
|
+
logger$3.error(prefix, `You must initialize Leanbase before calling ${methodName}`);
|
|
3218
3233
|
},
|
|
3219
3234
|
createLogger: additionalPrefix => _createLogger(`${prefix} ${additionalPrefix}`)
|
|
3220
3235
|
};
|
|
3221
3236
|
};
|
|
3222
|
-
const logger$
|
|
3237
|
+
const logger$2 = _createLogger('[Leanbase]');
|
|
3223
3238
|
const createLogger = _createLogger;
|
|
3224
3239
|
|
|
3225
3240
|
const LOGGER_PREFIX$2 = '[SessionRecording]';
|
|
@@ -3293,7 +3308,7 @@ function enforcePayloadSizeLimit(payload, headers, limit, description) {
|
|
|
3293
3308
|
// people can have arbitrarily large payloads on their site, but we don't want to ingest them
|
|
3294
3309
|
const limitPayloadSize = options => {
|
|
3295
3310
|
// the smallest of 1MB or the specified limit if there is one
|
|
3296
|
-
const limit = Math.min(1000000, options.payloadSizeLimitBytes);
|
|
3311
|
+
const limit = Math.min(1000000, options.payloadSizeLimitBytes ?? 1000000);
|
|
3297
3312
|
return data => {
|
|
3298
3313
|
if (data?.requestBody) {
|
|
3299
3314
|
data.requestBody = enforcePayloadSizeLimit(data.requestBody, data.requestHeaders, limit, 'Request');
|
|
@@ -3351,7 +3366,7 @@ const buildNetworkRequestOptions = (instanceConfig, remoteNetworkOptions = {}) =
|
|
|
3351
3366
|
const enforcedCleaningFn = d => payloadLimiter(ignorePostHogPaths(removeAuthorizationHeader(d), instanceConfig.host || ''));
|
|
3352
3367
|
const hasDeprecatedMaskFunction = isFunction(sessionRecordingConfig.maskNetworkRequestFn);
|
|
3353
3368
|
if (hasDeprecatedMaskFunction && isFunction(sessionRecordingConfig.maskCapturedNetworkRequestFn)) {
|
|
3354
|
-
logger$
|
|
3369
|
+
logger$2.warn('Both `maskNetworkRequestFn` and `maskCapturedNetworkRequestFn` are defined. `maskNetworkRequestFn` will be ignored.');
|
|
3355
3370
|
}
|
|
3356
3371
|
if (hasDeprecatedMaskFunction) {
|
|
3357
3372
|
sessionRecordingConfig.maskCapturedNetworkRequestFn = data => {
|
|
@@ -3798,7 +3813,7 @@ class MutationThrottler {
|
|
|
3798
3813
|
refillInterval: 1000,
|
|
3799
3814
|
// one second
|
|
3800
3815
|
_onBucketRateLimited: this._onNodeRateLimited,
|
|
3801
|
-
_logger: logger$
|
|
3816
|
+
_logger: logger$2
|
|
3802
3817
|
});
|
|
3803
3818
|
}
|
|
3804
3819
|
reset() {
|
|
@@ -3822,7 +3837,7 @@ function simpleHash(str) {
|
|
|
3822
3837
|
* receives percent as a number between 0 and 1
|
|
3823
3838
|
*/
|
|
3824
3839
|
function sampleOnProperty(prop, percent) {
|
|
3825
|
-
return simpleHash(prop) % 100 < clampToRange(percent * 100, 0, 100, logger$
|
|
3840
|
+
return simpleHash(prop) % 100 < clampToRange(percent * 100, 0, 100, logger$2);
|
|
3826
3841
|
}
|
|
3827
3842
|
|
|
3828
3843
|
/* eslint-disable posthog-js/no-direct-function-check */
|
|
@@ -3853,7 +3868,7 @@ const RECORDING_MAX_EVENT_SIZE = ONE_KB * ONE_KB * 0.9; // ~1mb (with some wiggl
|
|
|
3853
3868
|
const RECORDING_BUFFER_TIMEOUT = 2000; // 2 seconds
|
|
3854
3869
|
const SESSION_RECORDING_BATCH_KEY = 'recordings';
|
|
3855
3870
|
const LOGGER_PREFIX$1 = '[SessionRecording]';
|
|
3856
|
-
const logger = createLogger(LOGGER_PREFIX$1);
|
|
3871
|
+
const logger$1 = createLogger(LOGGER_PREFIX$1);
|
|
3857
3872
|
const ACTIVE_SOURCES = [IncrementalSource.MouseMove, IncrementalSource.MouseInteraction, IncrementalSource.Scroll, IncrementalSource.ViewportResize, IncrementalSource.Input, IncrementalSource.TouchMove, IncrementalSource.MediaInteraction, IncrementalSource.Drag];
|
|
3858
3873
|
const newQueuedEvent = rrwebMethod => ({
|
|
3859
3874
|
rrwebMethod,
|
|
@@ -3902,7 +3917,7 @@ async function loadRRWeb() {
|
|
|
3902
3917
|
return rr;
|
|
3903
3918
|
}
|
|
3904
3919
|
} catch (e) {
|
|
3905
|
-
logger.error('could not dynamically load rrweb', e);
|
|
3920
|
+
logger$1.error('could not dynamically load rrweb', e);
|
|
3906
3921
|
}
|
|
3907
3922
|
return null;
|
|
3908
3923
|
}
|
|
@@ -3949,7 +3964,7 @@ function compressEvent(event) {
|
|
|
3949
3964
|
};
|
|
3950
3965
|
}
|
|
3951
3966
|
} catch (e) {
|
|
3952
|
-
logger.error('could not compress event - will use uncompressed event', e);
|
|
3967
|
+
logger$1.error('could not compress event - will use uncompressed event', e);
|
|
3953
3968
|
}
|
|
3954
3969
|
return event;
|
|
3955
3970
|
}
|
|
@@ -3986,6 +4001,11 @@ function splitBuffer(buffer, sizeLimit = SEVEN_MEGABYTES) {
|
|
|
3986
4001
|
}
|
|
3987
4002
|
}
|
|
3988
4003
|
class LazyLoadedSessionRecording {
|
|
4004
|
+
_debug(...args) {
|
|
4005
|
+
if (this._instance?.config?.debug) {
|
|
4006
|
+
logger$1.info(...args);
|
|
4007
|
+
}
|
|
4008
|
+
}
|
|
3989
4009
|
get sessionId() {
|
|
3990
4010
|
return this._sessionId;
|
|
3991
4011
|
}
|
|
@@ -4000,9 +4020,9 @@ class LazyLoadedSessionRecording {
|
|
|
4000
4020
|
if (!this._loggedPermanentlyDisabled) {
|
|
4001
4021
|
this._loggedPermanentlyDisabled = true;
|
|
4002
4022
|
if (error) {
|
|
4003
|
-
logger.error(`replay disabled: ${reason}`, error);
|
|
4023
|
+
logger$1.error(`replay disabled: ${reason}`, error);
|
|
4004
4024
|
} else {
|
|
4005
|
-
logger.error(`replay disabled: ${reason}`);
|
|
4025
|
+
logger$1.error(`replay disabled: ${reason}`);
|
|
4006
4026
|
}
|
|
4007
4027
|
}
|
|
4008
4028
|
}
|
|
@@ -4039,6 +4059,7 @@ class LazyLoadedSessionRecording {
|
|
|
4039
4059
|
this._stopRrweb = undefined;
|
|
4040
4060
|
this._permanentlyDisabled = false;
|
|
4041
4061
|
this._loggedPermanentlyDisabled = false;
|
|
4062
|
+
this._hasReportedRecordingInitialized = false;
|
|
4042
4063
|
this._lastActivityTimestamp = Date.now();
|
|
4043
4064
|
/**
|
|
4044
4065
|
* and a queue - that contains rrweb events that we want to send to rrweb, but rrweb wasn't able to accept them yet
|
|
@@ -4101,7 +4122,7 @@ class LazyLoadedSessionRecording {
|
|
|
4101
4122
|
this._eventTriggerMatching = new EventTriggerMatching(this._instance);
|
|
4102
4123
|
this._buffer = this._clearBuffer();
|
|
4103
4124
|
if (this._sessionIdleThresholdMilliseconds >= this._sessionManager.sessionTimeoutMs) {
|
|
4104
|
-
logger.warn(`session_idle_threshold_ms (${this._sessionIdleThresholdMilliseconds}) is greater than the session timeout (${this._sessionManager.sessionTimeoutMs}). Session will never be detected as idle`);
|
|
4125
|
+
logger$1.warn(`session_idle_threshold_ms (${this._sessionIdleThresholdMilliseconds}) is greater than the session timeout (${this._sessionManager.sessionTimeoutMs}). Session will never be detected as idle`);
|
|
4105
4126
|
}
|
|
4106
4127
|
}
|
|
4107
4128
|
get _masking() {
|
|
@@ -4175,25 +4196,62 @@ class LazyLoadedSessionRecording {
|
|
|
4175
4196
|
payloadHostDenyList: networkPayloadCapture_server_side?.payloadHostDenyList
|
|
4176
4197
|
};
|
|
4177
4198
|
}
|
|
4178
|
-
|
|
4199
|
+
async _loadConsolePlugin() {
|
|
4200
|
+
try {
|
|
4201
|
+
const mod = await import('@rrweb/rrweb-plugin-console-record');
|
|
4202
|
+
const factory = mod?.getRecordConsolePlugin ?? mod?.default?.getRecordConsolePlugin;
|
|
4203
|
+
if (typeof factory === 'function') {
|
|
4204
|
+
const plugin = factory();
|
|
4205
|
+
this._debug('Console plugin loaded');
|
|
4206
|
+
return plugin;
|
|
4207
|
+
}
|
|
4208
|
+
logger$1.warn('console plugin factory unavailable after import');
|
|
4209
|
+
} catch (e) {
|
|
4210
|
+
logger$1.warn('could not load console plugin', e);
|
|
4211
|
+
}
|
|
4212
|
+
return null;
|
|
4213
|
+
}
|
|
4214
|
+
async _loadNetworkPlugin(networkPayloadCapture) {
|
|
4215
|
+
try {
|
|
4216
|
+
const mod = await Promise.resolve().then(function () { return networkPlugin; });
|
|
4217
|
+
const factory = mod?.getRecordNetworkPlugin ?? mod?.default?.getRecordNetworkPlugin;
|
|
4218
|
+
if (typeof factory === 'function') {
|
|
4219
|
+
const options = buildNetworkRequestOptions(this._instance.config, networkPayloadCapture);
|
|
4220
|
+
const plugin = factory(options);
|
|
4221
|
+
this._debug('Network plugin loaded');
|
|
4222
|
+
return plugin;
|
|
4223
|
+
}
|
|
4224
|
+
logger$1.warn('network plugin factory unavailable after import');
|
|
4225
|
+
} catch (e) {
|
|
4226
|
+
logger$1.warn('could not load network plugin', e);
|
|
4227
|
+
}
|
|
4228
|
+
return null;
|
|
4229
|
+
}
|
|
4230
|
+
async _gatherRRWebPlugins() {
|
|
4179
4231
|
const plugins = [];
|
|
4232
|
+
if (!win) {
|
|
4233
|
+
return plugins;
|
|
4234
|
+
}
|
|
4180
4235
|
if (this._isConsoleLogCaptureEnabled) {
|
|
4181
|
-
|
|
4236
|
+
const consolePlugin = await this._loadConsolePlugin();
|
|
4237
|
+
if (consolePlugin) {
|
|
4238
|
+
plugins.push(consolePlugin);
|
|
4239
|
+
}
|
|
4182
4240
|
}
|
|
4183
4241
|
if (this._networkPayloadCapture) {
|
|
4184
4242
|
const canRecordNetwork = !isLocalhost() || this._forceAllowLocalhostNetworkCapture;
|
|
4185
4243
|
if (canRecordNetwork) {
|
|
4186
|
-
const
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
plugins.push(networkFactory(buildNetworkRequestOptions(this._instance.config, this._networkPayloadCapture)));
|
|
4190
|
-
} else {
|
|
4191
|
-
logger.info('Network plugin factory not available yet; skipping network plugin');
|
|
4244
|
+
const networkPlugin = await this._loadNetworkPlugin(this._networkPayloadCapture);
|
|
4245
|
+
if (networkPlugin) {
|
|
4246
|
+
plugins.push(networkPlugin);
|
|
4192
4247
|
}
|
|
4193
4248
|
} else {
|
|
4194
|
-
|
|
4249
|
+
this._debug('NetworkCapture not started because we are on localhost.');
|
|
4195
4250
|
}
|
|
4196
4251
|
}
|
|
4252
|
+
if (plugins.length > 0) {
|
|
4253
|
+
this._debug('Replay plugins loaded', plugins.map(p => p.name));
|
|
4254
|
+
}
|
|
4197
4255
|
return plugins;
|
|
4198
4256
|
}
|
|
4199
4257
|
_maskUrl(url) {
|
|
@@ -4222,7 +4280,7 @@ class LazyLoadedSessionRecording {
|
|
|
4222
4280
|
rrwebMethod: queuedRRWebEvent.rrwebMethod
|
|
4223
4281
|
});
|
|
4224
4282
|
} else {
|
|
4225
|
-
logger.warn('could not emit queued rrweb event.', e, queuedRRWebEvent);
|
|
4283
|
+
logger$1.warn('could not emit queued rrweb event.', e, queuedRRWebEvent);
|
|
4226
4284
|
}
|
|
4227
4285
|
return false;
|
|
4228
4286
|
}
|
|
@@ -4334,7 +4392,7 @@ class LazyLoadedSessionRecording {
|
|
|
4334
4392
|
this._urlTriggerMatching.urlBlocked = true;
|
|
4335
4393
|
// Clear the snapshot timer since we don't want new snapshots while paused
|
|
4336
4394
|
clearInterval(this._fullSnapshotTimer);
|
|
4337
|
-
logger.info('recording paused due to URL blocker');
|
|
4395
|
+
logger$1.info('recording paused due to URL blocker');
|
|
4338
4396
|
this._tryAddCustomEvent('recording paused', {
|
|
4339
4397
|
reason: 'url blocker'
|
|
4340
4398
|
});
|
|
@@ -4353,7 +4411,7 @@ class LazyLoadedSessionRecording {
|
|
|
4353
4411
|
this._tryAddCustomEvent('recording resumed', {
|
|
4354
4412
|
reason: 'left blocked url'
|
|
4355
4413
|
});
|
|
4356
|
-
logger.info('recording resumed');
|
|
4414
|
+
logger$1.info('recording resumed');
|
|
4357
4415
|
}
|
|
4358
4416
|
_activateTrigger(triggerType) {
|
|
4359
4417
|
if (!this.isStarted || !this._recording || !this._isFullyReady) {
|
|
@@ -4386,7 +4444,7 @@ class LazyLoadedSessionRecording {
|
|
|
4386
4444
|
this._isFullyReady = false;
|
|
4387
4445
|
const config = this._remoteConfig;
|
|
4388
4446
|
if (!config) {
|
|
4389
|
-
logger.info('remote config must be stored in persistence before recording can start');
|
|
4447
|
+
logger$1.info('remote config must be stored in persistence before recording can start');
|
|
4390
4448
|
return;
|
|
4391
4449
|
}
|
|
4392
4450
|
// We want to ensure the sessionManager is reset if necessary on loading the recorder
|
|
@@ -4469,12 +4527,18 @@ class LazyLoadedSessionRecording {
|
|
|
4469
4527
|
});
|
|
4470
4528
|
}
|
|
4471
4529
|
} catch (e) {
|
|
4472
|
-
logger.error('Could not add $pageview to rrweb session', e);
|
|
4530
|
+
logger$1.error('Could not add $pageview to rrweb session', e);
|
|
4473
4531
|
}
|
|
4474
4532
|
});
|
|
4475
4533
|
}
|
|
4476
4534
|
if (this.status === ACTIVE) {
|
|
4477
|
-
|
|
4535
|
+
const reason = startReason || 'recording_initialized';
|
|
4536
|
+
if (reason !== 'recording_initialized' || !this._hasReportedRecordingInitialized) {
|
|
4537
|
+
if (reason === 'recording_initialized') {
|
|
4538
|
+
this._hasReportedRecordingInitialized = true;
|
|
4539
|
+
}
|
|
4540
|
+
this._reportStarted(reason);
|
|
4541
|
+
}
|
|
4478
4542
|
}
|
|
4479
4543
|
}
|
|
4480
4544
|
stop() {
|
|
@@ -4507,17 +4571,31 @@ class LazyLoadedSessionRecording {
|
|
|
4507
4571
|
this._recording = undefined;
|
|
4508
4572
|
this._stopRrweb = undefined;
|
|
4509
4573
|
this._isFullyReady = false;
|
|
4510
|
-
|
|
4574
|
+
this._hasReportedRecordingInitialized = false;
|
|
4575
|
+
logger$1.info('stopped');
|
|
4511
4576
|
}
|
|
4512
4577
|
_snapshotIngestionUrl() {
|
|
4513
4578
|
const endpointFor = this._instance?.requestRouter?.endpointFor;
|
|
4514
|
-
|
|
4579
|
+
// Prefer requestRouter (parity with Browser SDK)
|
|
4580
|
+
if (typeof endpointFor === 'function') {
|
|
4581
|
+
try {
|
|
4582
|
+
return endpointFor('api', this._endpoint);
|
|
4583
|
+
} catch {
|
|
4584
|
+
return null;
|
|
4585
|
+
}
|
|
4586
|
+
}
|
|
4587
|
+
// Fallback: construct from host/api_host if requestRouter is unavailable (older IIFE builds)
|
|
4588
|
+
const host = (this._instance.config.api_host || this._instance.config.host || '').trim();
|
|
4589
|
+
if (!host) {
|
|
4515
4590
|
return null;
|
|
4516
4591
|
}
|
|
4517
4592
|
try {
|
|
4518
|
-
|
|
4593
|
+
// eslint-disable-next-line compat/compat
|
|
4594
|
+
return new URL(this._endpoint, host).href;
|
|
4519
4595
|
} catch {
|
|
4520
|
-
|
|
4596
|
+
const normalizedHost = host.endsWith('/') ? host.slice(0, -1) : host;
|
|
4597
|
+
const normalizedEndpoint = this._endpoint.startsWith('/') ? this._endpoint : `/${this._endpoint}`;
|
|
4598
|
+
return `${normalizedHost}${normalizedEndpoint}`;
|
|
4521
4599
|
}
|
|
4522
4600
|
}
|
|
4523
4601
|
_canCaptureSnapshots() {
|
|
@@ -4602,7 +4680,7 @@ class LazyLoadedSessionRecording {
|
|
|
4602
4680
|
}
|
|
4603
4681
|
this._captureSnapshotBuffered(properties);
|
|
4604
4682
|
} catch (e) {
|
|
4605
|
-
logger.error('error processing rrweb event', e);
|
|
4683
|
+
logger$1.error('error processing rrweb event', e);
|
|
4606
4684
|
}
|
|
4607
4685
|
}
|
|
4608
4686
|
get status() {
|
|
@@ -4693,7 +4771,7 @@ class LazyLoadedSessionRecording {
|
|
|
4693
4771
|
if (!this._canCaptureSnapshots()) {
|
|
4694
4772
|
if (!this._loggedMissingEndpointFor) {
|
|
4695
4773
|
this._loggedMissingEndpointFor = true;
|
|
4696
|
-
logger.warn('snapshot capture skipped because requestRouter.endpointFor is unavailable');
|
|
4774
|
+
logger$1.warn('snapshot capture skipped because requestRouter.endpointFor is unavailable');
|
|
4697
4775
|
}
|
|
4698
4776
|
this._flushBufferTimer = setTimeout(() => {
|
|
4699
4777
|
this._flushBuffer();
|
|
@@ -4736,7 +4814,7 @@ class LazyLoadedSessionRecording {
|
|
|
4736
4814
|
if (!url) {
|
|
4737
4815
|
if (!this._loggedMissingEndpointFor) {
|
|
4738
4816
|
this._loggedMissingEndpointFor = true;
|
|
4739
|
-
logger.warn('snapshot capture skipped because requestRouter.endpointFor is unavailable');
|
|
4817
|
+
logger$1.warn('snapshot capture skipped because requestRouter.endpointFor is unavailable');
|
|
4740
4818
|
}
|
|
4741
4819
|
return;
|
|
4742
4820
|
}
|
|
@@ -4749,7 +4827,7 @@ class LazyLoadedSessionRecording {
|
|
|
4749
4827
|
skip_client_rate_limiting: true
|
|
4750
4828
|
});
|
|
4751
4829
|
} catch (e) {
|
|
4752
|
-
logger.error('failed to capture snapshot', e);
|
|
4830
|
+
logger$1.error('failed to capture snapshot', e);
|
|
4753
4831
|
}
|
|
4754
4832
|
}
|
|
4755
4833
|
_snapshotUrl() {
|
|
@@ -4806,7 +4884,12 @@ class LazyLoadedSessionRecording {
|
|
|
4806
4884
|
this._instance.registerForSession({
|
|
4807
4885
|
$session_recording_start_reason: startReason
|
|
4808
4886
|
});
|
|
4809
|
-
|
|
4887
|
+
const message = startReason.replace('_', ' ');
|
|
4888
|
+
if (typeof tagPayload === 'undefined') {
|
|
4889
|
+
logger$1.info(message);
|
|
4890
|
+
} else {
|
|
4891
|
+
logger$1.info(message, tagPayload);
|
|
4892
|
+
}
|
|
4810
4893
|
if (!includes(['recording_initialized', 'session_id_changed'], startReason)) {
|
|
4811
4894
|
this._tryAddCustomEvent(startReason, tagPayload);
|
|
4812
4895
|
}
|
|
@@ -4906,7 +4989,7 @@ class LazyLoadedSessionRecording {
|
|
|
4906
4989
|
if (shouldSample) {
|
|
4907
4990
|
this._reportStarted(SAMPLED);
|
|
4908
4991
|
} else {
|
|
4909
|
-
logger.warn(`Sample rate (${currentSampleRate}) has determined that this sessionId (${sessionId}) will not be sent to the server.`);
|
|
4992
|
+
logger$1.warn(`Sample rate (${currentSampleRate}) has determined that this sessionId (${sessionId}) will not be sent to the server.`);
|
|
4910
4993
|
}
|
|
4911
4994
|
this._tryAddCustomEvent('samplingDecisionMade', {
|
|
4912
4995
|
sampleRate: currentSampleRate,
|
|
@@ -4929,7 +5012,7 @@ class LazyLoadedSessionRecording {
|
|
|
4929
5012
|
this._activateTrigger('event');
|
|
4930
5013
|
}
|
|
4931
5014
|
} catch (e) {
|
|
4932
|
-
logger.error('Could not activate event trigger', e);
|
|
5015
|
+
logger$1.error('Could not activate event trigger', e);
|
|
4933
5016
|
}
|
|
4934
5017
|
});
|
|
4935
5018
|
}
|
|
@@ -5011,7 +5094,7 @@ class LazyLoadedSessionRecording {
|
|
|
5011
5094
|
this._disablePermanently('rrweb record function unavailable');
|
|
5012
5095
|
return;
|
|
5013
5096
|
}
|
|
5014
|
-
const activePlugins = this._gatherRRWebPlugins();
|
|
5097
|
+
const activePlugins = await this._gatherRRWebPlugins();
|
|
5015
5098
|
let stopHandler;
|
|
5016
5099
|
try {
|
|
5017
5100
|
stopHandler = rrwebRecord({
|
|
@@ -5020,7 +5103,7 @@ class LazyLoadedSessionRecording {
|
|
|
5020
5103
|
this.onRRwebEmit(event);
|
|
5021
5104
|
} catch (e) {
|
|
5022
5105
|
// never throw from rrweb emit handler
|
|
5023
|
-
logger.error('error in rrweb emit handler', e);
|
|
5106
|
+
logger$1.error('error in rrweb emit handler', e);
|
|
5024
5107
|
}
|
|
5025
5108
|
},
|
|
5026
5109
|
plugins: activePlugins,
|
|
@@ -5045,7 +5128,7 @@ class LazyLoadedSessionRecording {
|
|
|
5045
5128
|
bucketSize: this._instance.config.session_recording?.__mutationThrottlerBucketSize,
|
|
5046
5129
|
onBlockedNode: (id, node) => {
|
|
5047
5130
|
const message = `Too many mutations on node '${id}'. Rate limiting. This could be due to SVG animations or something similar`;
|
|
5048
|
-
logger.info(message, {
|
|
5131
|
+
logger$1.info(message, {
|
|
5049
5132
|
node: node
|
|
5050
5133
|
});
|
|
5051
5134
|
this.log(LOGGER_PREFIX$1 + ' ' + message, 'warn');
|
|
@@ -5072,11 +5155,16 @@ class LazyLoadedSessionRecording {
|
|
|
5072
5155
|
/* eslint-disable posthog-js/no-direct-function-check */
|
|
5073
5156
|
const LOGGER_PREFIX = '[SessionRecording]';
|
|
5074
5157
|
const log = {
|
|
5075
|
-
info: (...args) => logger$
|
|
5076
|
-
warn: (...args) => logger$
|
|
5077
|
-
error: (...args) => logger$
|
|
5158
|
+
info: (...args) => logger$3.info(LOGGER_PREFIX, ...args),
|
|
5159
|
+
warn: (...args) => logger$3.warn(LOGGER_PREFIX, ...args),
|
|
5160
|
+
error: (...args) => logger$3.error(LOGGER_PREFIX, ...args)
|
|
5078
5161
|
};
|
|
5079
5162
|
class SessionRecording {
|
|
5163
|
+
_debug(...args) {
|
|
5164
|
+
if (this._instance?.config?.debug) {
|
|
5165
|
+
log.info(...args);
|
|
5166
|
+
}
|
|
5167
|
+
}
|
|
5080
5168
|
get started() {
|
|
5081
5169
|
return !!this._lazyLoadedSessionRecording?.isStarted;
|
|
5082
5170
|
}
|
|
@@ -5114,8 +5202,10 @@ class SessionRecording {
|
|
|
5114
5202
|
}
|
|
5115
5203
|
const canRunReplay = !isUndefined(Object.assign) && !isUndefined(Array.from);
|
|
5116
5204
|
if (this._isRecordingEnabled && canRunReplay) {
|
|
5205
|
+
this._debug('Session replay enabled; starting recorder');
|
|
5117
5206
|
this._lazyLoadAndStart(startReason);
|
|
5118
5207
|
} else {
|
|
5208
|
+
this._debug('Session replay disabled; stopping recorder');
|
|
5119
5209
|
this.stopRecording();
|
|
5120
5210
|
}
|
|
5121
5211
|
}
|
|
@@ -5187,18 +5277,15 @@ class SessionRecording {
|
|
|
5187
5277
|
log.info('skipping remote config with no sessionRecording', response);
|
|
5188
5278
|
return;
|
|
5189
5279
|
}
|
|
5190
|
-
this._receivedFlags = true;
|
|
5191
|
-
if (response.sessionRecording === false) {
|
|
5192
|
-
return;
|
|
5193
|
-
}
|
|
5194
5280
|
this._persistRemoteConfig(response);
|
|
5281
|
+
this._receivedFlags = true;
|
|
5195
5282
|
this.startIfEnabledOrStop();
|
|
5196
5283
|
}
|
|
5197
5284
|
log(message, level = 'log') {
|
|
5198
5285
|
if (this._lazyLoadedSessionRecording?.log) {
|
|
5199
5286
|
this._lazyLoadedSessionRecording.log(message, level);
|
|
5200
5287
|
} else {
|
|
5201
|
-
logger$
|
|
5288
|
+
logger$3.warn('log called before recorder was ready');
|
|
5202
5289
|
}
|
|
5203
5290
|
}
|
|
5204
5291
|
get _scriptName() {
|
|
@@ -5221,10 +5308,10 @@ class SessionRecording {
|
|
|
5221
5308
|
try {
|
|
5222
5309
|
const maybePromise = this._lazyLoadedSessionRecording.start(startReason);
|
|
5223
5310
|
if (maybePromise && typeof maybePromise.catch === 'function') {
|
|
5224
|
-
maybePromise.catch(e => logger$
|
|
5311
|
+
maybePromise.catch(e => logger$3.error('error starting session recording', e));
|
|
5225
5312
|
}
|
|
5226
5313
|
} catch (e) {
|
|
5227
|
-
logger$
|
|
5314
|
+
logger$3.error('error starting session recording', e);
|
|
5228
5315
|
}
|
|
5229
5316
|
return;
|
|
5230
5317
|
}
|
|
@@ -5237,10 +5324,10 @@ class SessionRecording {
|
|
|
5237
5324
|
try {
|
|
5238
5325
|
const maybePromise = this._lazyLoadedSessionRecording.start(startReason);
|
|
5239
5326
|
if (maybePromise && typeof maybePromise.catch === 'function') {
|
|
5240
|
-
maybePromise.catch(e => logger$
|
|
5327
|
+
maybePromise.catch(e => logger$3.error('error starting session recording', e));
|
|
5241
5328
|
}
|
|
5242
5329
|
} catch (e) {
|
|
5243
|
-
logger$
|
|
5330
|
+
logger$3.error('error starting session recording', e);
|
|
5244
5331
|
}
|
|
5245
5332
|
}
|
|
5246
5333
|
/**
|
|
@@ -5273,6 +5360,39 @@ class SessionRecording {
|
|
|
5273
5360
|
}
|
|
5274
5361
|
}
|
|
5275
5362
|
|
|
5363
|
+
/**
|
|
5364
|
+
* Leanbase-local version of PostHog's RequestRouter.
|
|
5365
|
+
*
|
|
5366
|
+
* Browser SDK always has a requestRouter instance; Leanbase IIFE needs this too so
|
|
5367
|
+
* features like Session Replay can construct ingestion URLs.
|
|
5368
|
+
*/
|
|
5369
|
+
class RequestRouter {
|
|
5370
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
5371
|
+
constructor(instance) {
|
|
5372
|
+
this.instance = instance;
|
|
5373
|
+
}
|
|
5374
|
+
get apiHost() {
|
|
5375
|
+
const configured = (this.instance.config.api_host || this.instance.config.host || '').trim();
|
|
5376
|
+
return configured.replace(/\/$/, '');
|
|
5377
|
+
}
|
|
5378
|
+
get uiHost() {
|
|
5379
|
+
const configured = this.instance.config.ui_host?.trim().replace(/\/$/, '');
|
|
5380
|
+
return configured || undefined;
|
|
5381
|
+
}
|
|
5382
|
+
endpointFor(target, path = '') {
|
|
5383
|
+
if (path) {
|
|
5384
|
+
path = path[0] === '/' ? path : `/${path}`;
|
|
5385
|
+
}
|
|
5386
|
+
if (target === 'ui') {
|
|
5387
|
+
const host = this.uiHost || this.apiHost;
|
|
5388
|
+
return host + path;
|
|
5389
|
+
}
|
|
5390
|
+
// Leanbase doesn't currently do region-based routing; default to apiHost.
|
|
5391
|
+
// Browser's router has special handling for assets; we keep parity in interface, not domains.
|
|
5392
|
+
return this.apiHost + path;
|
|
5393
|
+
}
|
|
5394
|
+
}
|
|
5395
|
+
|
|
5276
5396
|
const defaultConfig = () => ({
|
|
5277
5397
|
host: 'https://i.leanbase.co',
|
|
5278
5398
|
token: '',
|
|
@@ -5330,6 +5450,8 @@ class Leanbase extends PostHogCore {
|
|
|
5330
5450
|
}));
|
|
5331
5451
|
this.isLoaded = true;
|
|
5332
5452
|
this.persistence = new LeanbasePersistence(this.config);
|
|
5453
|
+
// Browser SDK always has a requestRouter; session replay relies on it for $snapshot ingestion URLs.
|
|
5454
|
+
this.requestRouter = new RequestRouter(this);
|
|
5333
5455
|
if (this.config.cookieless_mode !== 'always') {
|
|
5334
5456
|
this.sessionManager = new SessionIdManager(this);
|
|
5335
5457
|
this.sessionPropsManager = new SessionPropsManager(this, this.sessionManager, this.persistence);
|
|
@@ -5362,12 +5484,12 @@ class Leanbase extends PostHogCore {
|
|
|
5362
5484
|
}, 1);
|
|
5363
5485
|
}
|
|
5364
5486
|
const triggerRemoteConfigLoad = reason => {
|
|
5365
|
-
logger$
|
|
5487
|
+
logger$3.info(`remote config load triggered via ${reason}`);
|
|
5366
5488
|
void this.loadRemoteConfig();
|
|
5367
5489
|
};
|
|
5368
5490
|
if (document) {
|
|
5369
5491
|
if (document.readyState === 'loading') {
|
|
5370
|
-
logger$
|
|
5492
|
+
logger$3.info('remote config load deferred until DOMContentLoaded');
|
|
5371
5493
|
const onDomReady = () => {
|
|
5372
5494
|
document?.removeEventListener('DOMContentLoaded', onDomReady);
|
|
5373
5495
|
triggerRemoteConfigLoad('dom');
|
|
@@ -5460,7 +5582,7 @@ class Leanbase extends PostHogCore {
|
|
|
5460
5582
|
try {
|
|
5461
5583
|
this.sessionRecording.startIfEnabledOrStop();
|
|
5462
5584
|
} catch (e) {
|
|
5463
|
-
logger$
|
|
5585
|
+
logger$3.error('Failed to start session recording', e);
|
|
5464
5586
|
}
|
|
5465
5587
|
}
|
|
5466
5588
|
fetch(url, options) {
|
|
@@ -5526,7 +5648,7 @@ class Leanbase extends PostHogCore {
|
|
|
5526
5648
|
};
|
|
5527
5649
|
properties['distinct_id'] = persistenceProps.distinct_id;
|
|
5528
5650
|
if (!(isString(properties['distinct_id']) || isNumber(properties['distinct_id'])) || isEmptyString(properties['distinct_id'])) {
|
|
5529
|
-
logger$
|
|
5651
|
+
logger$3.error('Invalid distinct_id for replay event. This indicates a bug in your implementation');
|
|
5530
5652
|
}
|
|
5531
5653
|
return properties;
|
|
5532
5654
|
}
|
|
@@ -5601,11 +5723,11 @@ class Leanbase extends PostHogCore {
|
|
|
5601
5723
|
return;
|
|
5602
5724
|
}
|
|
5603
5725
|
if (isUndefined(event) || !isString(event)) {
|
|
5604
|
-
logger$
|
|
5726
|
+
logger$3.error('No event name provided to posthog.capture');
|
|
5605
5727
|
return;
|
|
5606
5728
|
}
|
|
5607
5729
|
if (properties?.$current_url && !isString(properties?.$current_url)) {
|
|
5608
|
-
logger$
|
|
5730
|
+
logger$3.error('Invalid `$current_url` property provided to `posthog.capture`. Input must be a string. Ignoring provided value.');
|
|
5609
5731
|
delete properties?.$current_url;
|
|
5610
5732
|
}
|
|
5611
5733
|
this.sessionPersistence.update_search_keyword();
|
|
@@ -5657,5 +5779,634 @@ class Leanbase extends PostHogCore {
|
|
|
5657
5779
|
}
|
|
5658
5780
|
}
|
|
5659
5781
|
|
|
5782
|
+
function patch(source, name, replacement) {
|
|
5783
|
+
try {
|
|
5784
|
+
if (!(name in source)) {
|
|
5785
|
+
return () => {
|
|
5786
|
+
//
|
|
5787
|
+
};
|
|
5788
|
+
}
|
|
5789
|
+
const original = source[name];
|
|
5790
|
+
const wrapped = replacement(original);
|
|
5791
|
+
if (isFunction(wrapped)) {
|
|
5792
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
5793
|
+
wrapped.prototype = wrapped.prototype || {};
|
|
5794
|
+
Object.defineProperties(wrapped, {
|
|
5795
|
+
__posthog_wrapped__: {
|
|
5796
|
+
enumerable: false,
|
|
5797
|
+
value: true
|
|
5798
|
+
}
|
|
5799
|
+
});
|
|
5800
|
+
}
|
|
5801
|
+
source[name] = wrapped;
|
|
5802
|
+
return () => {
|
|
5803
|
+
source[name] = original;
|
|
5804
|
+
};
|
|
5805
|
+
} catch {
|
|
5806
|
+
return () => {
|
|
5807
|
+
//
|
|
5808
|
+
};
|
|
5809
|
+
}
|
|
5810
|
+
}
|
|
5811
|
+
|
|
5812
|
+
function hostnameFromURL(url) {
|
|
5813
|
+
try {
|
|
5814
|
+
if (typeof url === 'string') {
|
|
5815
|
+
return new URL(url).hostname;
|
|
5816
|
+
}
|
|
5817
|
+
if ('url' in url) {
|
|
5818
|
+
return new URL(url.url).hostname;
|
|
5819
|
+
}
|
|
5820
|
+
return url.hostname;
|
|
5821
|
+
} catch {
|
|
5822
|
+
return null;
|
|
5823
|
+
}
|
|
5824
|
+
}
|
|
5825
|
+
function isHostOnDenyList(url, options) {
|
|
5826
|
+
const hostname = hostnameFromURL(url);
|
|
5827
|
+
const defaultNotDenied = {
|
|
5828
|
+
hostname,
|
|
5829
|
+
isHostDenied: false
|
|
5830
|
+
};
|
|
5831
|
+
if (!options.payloadHostDenyList?.length || !hostname?.trim().length) {
|
|
5832
|
+
return defaultNotDenied;
|
|
5833
|
+
}
|
|
5834
|
+
for (const deny of options.payloadHostDenyList) {
|
|
5835
|
+
if (hostname.endsWith(deny)) {
|
|
5836
|
+
return {
|
|
5837
|
+
hostname,
|
|
5838
|
+
isHostDenied: true
|
|
5839
|
+
};
|
|
5840
|
+
}
|
|
5841
|
+
}
|
|
5842
|
+
return defaultNotDenied;
|
|
5843
|
+
}
|
|
5844
|
+
|
|
5845
|
+
/// <reference lib="dom" />
|
|
5846
|
+
const logger = createLogger('[Recorder]');
|
|
5847
|
+
const isNavigationTiming = entry => entry.entryType === 'navigation';
|
|
5848
|
+
const isResourceTiming = entry => entry.entryType === 'resource';
|
|
5849
|
+
function findLast(array, predicate) {
|
|
5850
|
+
const length = array.length;
|
|
5851
|
+
for (let i = length - 1; i >= 0; i -= 1) {
|
|
5852
|
+
if (predicate(array[i])) {
|
|
5853
|
+
return array[i];
|
|
5854
|
+
}
|
|
5855
|
+
}
|
|
5856
|
+
return undefined;
|
|
5857
|
+
}
|
|
5858
|
+
function isDocument(value) {
|
|
5859
|
+
return !!value && typeof value === 'object' && 'nodeType' in value && value.nodeType === 9;
|
|
5860
|
+
}
|
|
5861
|
+
function initPerformanceObserver(cb, win, options) {
|
|
5862
|
+
// if we are only observing timings then we could have a single observer for all types, with buffer true,
|
|
5863
|
+
// but we are going to filter by initiatorType _if we are wrapping fetch and xhr as the wrapped functions
|
|
5864
|
+
// will deal with those.
|
|
5865
|
+
// so we have a block which captures requests from before fetch/xhr is wrapped
|
|
5866
|
+
// these are marked `isInitial` so playback can display them differently if needed
|
|
5867
|
+
// they will never have method/status/headers/body because they are pre-wrapping that provides that
|
|
5868
|
+
if (options.recordInitialRequests) {
|
|
5869
|
+
const initialPerformanceEntries = win.performance.getEntries().filter(entry => isNavigationTiming(entry) || isResourceTiming(entry) && options.initiatorTypes.includes(entry.initiatorType));
|
|
5870
|
+
cb({
|
|
5871
|
+
requests: initialPerformanceEntries.flatMap(entry => prepareRequest({
|
|
5872
|
+
entry,
|
|
5873
|
+
method: undefined,
|
|
5874
|
+
status: undefined,
|
|
5875
|
+
networkRequest: {},
|
|
5876
|
+
isInitial: true
|
|
5877
|
+
})),
|
|
5878
|
+
isInitial: true
|
|
5879
|
+
});
|
|
5880
|
+
}
|
|
5881
|
+
const observer = new win.PerformanceObserver(entries => {
|
|
5882
|
+
// if recordBody or recordHeaders is true then we don't want to record fetch or xhr here
|
|
5883
|
+
// as the wrapped functions will do that. Otherwise, this filter becomes a noop
|
|
5884
|
+
// because we do want to record them here
|
|
5885
|
+
const wrappedInitiatorFilter = entry => options.recordBody || options.recordHeaders ? entry.initiatorType !== 'xmlhttprequest' && entry.initiatorType !== 'fetch' : true;
|
|
5886
|
+
const performanceEntries = entries.getEntries().filter(entry => isNavigationTiming(entry) || isResourceTiming(entry) && options.initiatorTypes.includes(entry.initiatorType) &&
|
|
5887
|
+
// TODO if we are _only_ capturing timing we don't want to filter initiator here
|
|
5888
|
+
wrappedInitiatorFilter(entry));
|
|
5889
|
+
cb({
|
|
5890
|
+
requests: performanceEntries.flatMap(entry => prepareRequest({
|
|
5891
|
+
entry,
|
|
5892
|
+
method: undefined,
|
|
5893
|
+
status: undefined,
|
|
5894
|
+
networkRequest: {}
|
|
5895
|
+
}))
|
|
5896
|
+
});
|
|
5897
|
+
});
|
|
5898
|
+
// compat checked earlier
|
|
5899
|
+
// eslint-disable-next-line compat/compat
|
|
5900
|
+
const entryTypes = PerformanceObserver.supportedEntryTypes.filter(x => options.performanceEntryTypeToObserve.includes(x));
|
|
5901
|
+
// initial records are gathered above, so we don't need to observe and buffer each type separately
|
|
5902
|
+
observer.observe({
|
|
5903
|
+
entryTypes
|
|
5904
|
+
});
|
|
5905
|
+
return () => {
|
|
5906
|
+
observer.disconnect();
|
|
5907
|
+
};
|
|
5908
|
+
}
|
|
5909
|
+
function shouldRecordHeaders(type, recordHeaders) {
|
|
5910
|
+
return !!recordHeaders && (isBoolean(recordHeaders) || recordHeaders[type]);
|
|
5911
|
+
}
|
|
5912
|
+
function shouldRecordBody({
|
|
5913
|
+
type,
|
|
5914
|
+
recordBody,
|
|
5915
|
+
headers,
|
|
5916
|
+
url
|
|
5917
|
+
}) {
|
|
5918
|
+
function matchesContentType(contentTypes) {
|
|
5919
|
+
const contentTypeHeader = Object.keys(headers).find(key => key.toLowerCase() === 'content-type');
|
|
5920
|
+
const contentType = contentTypeHeader && headers[contentTypeHeader];
|
|
5921
|
+
return contentTypes.some(ct => contentType?.includes(ct));
|
|
5922
|
+
}
|
|
5923
|
+
/**
|
|
5924
|
+
* particularly in canvas applications we see many requests to blob URLs
|
|
5925
|
+
* e.g. blob:https://video_url
|
|
5926
|
+
* these blob/object URLs are local to the browser, we can never capture that body
|
|
5927
|
+
* so we can just return false here
|
|
5928
|
+
*/
|
|
5929
|
+
function isBlobURL(url) {
|
|
5930
|
+
try {
|
|
5931
|
+
if (typeof url === 'string') {
|
|
5932
|
+
return url.startsWith('blob:');
|
|
5933
|
+
}
|
|
5934
|
+
if (url instanceof URL) {
|
|
5935
|
+
return url.protocol === 'blob:';
|
|
5936
|
+
}
|
|
5937
|
+
if (url instanceof Request) {
|
|
5938
|
+
return isBlobURL(url.url);
|
|
5939
|
+
}
|
|
5940
|
+
return false;
|
|
5941
|
+
} catch {
|
|
5942
|
+
return false;
|
|
5943
|
+
}
|
|
5944
|
+
}
|
|
5945
|
+
if (!recordBody) return false;
|
|
5946
|
+
if (isBlobURL(url)) return false;
|
|
5947
|
+
if (isBoolean(recordBody)) return true;
|
|
5948
|
+
if (isArray(recordBody)) return matchesContentType(recordBody);
|
|
5949
|
+
const recordBodyType = recordBody[type];
|
|
5950
|
+
if (isBoolean(recordBodyType)) return recordBodyType;
|
|
5951
|
+
return matchesContentType(recordBodyType);
|
|
5952
|
+
}
|
|
5953
|
+
async function getRequestPerformanceEntry(win, initiatorType, url, start, end, attempt = 0) {
|
|
5954
|
+
if (attempt > 10) {
|
|
5955
|
+
logger.warn('Failed to get performance entry for request', {
|
|
5956
|
+
url,
|
|
5957
|
+
initiatorType
|
|
5958
|
+
});
|
|
5959
|
+
return null;
|
|
5960
|
+
}
|
|
5961
|
+
const urlPerformanceEntries = win.performance.getEntriesByName(url);
|
|
5962
|
+
const performanceEntry = findLast(urlPerformanceEntries, entry => isResourceTiming(entry) && entry.initiatorType === initiatorType && (isUndefined(start) || entry.startTime >= start) && (isUndefined(end) || entry.startTime <= end));
|
|
5963
|
+
if (!performanceEntry) {
|
|
5964
|
+
await new Promise(resolve => setTimeout(resolve, 50 * attempt));
|
|
5965
|
+
return getRequestPerformanceEntry(win, initiatorType, url, start, end, attempt + 1);
|
|
5966
|
+
}
|
|
5967
|
+
return performanceEntry;
|
|
5968
|
+
}
|
|
5969
|
+
/**
|
|
5970
|
+
* According to MDN https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/response
|
|
5971
|
+
* xhr response is typed as any but can be an ArrayBuffer, a Blob, a Document, a JavaScript object,
|
|
5972
|
+
* or a string, depending on the value of XMLHttpRequest.responseType, that contains the response entity body.
|
|
5973
|
+
*
|
|
5974
|
+
* XHR request body is Document | XMLHttpRequestBodyInit | null | undefined
|
|
5975
|
+
*/
|
|
5976
|
+
function _tryReadXHRBody({
|
|
5977
|
+
body,
|
|
5978
|
+
options,
|
|
5979
|
+
url
|
|
5980
|
+
}) {
|
|
5981
|
+
if (isNullish(body)) {
|
|
5982
|
+
return null;
|
|
5983
|
+
}
|
|
5984
|
+
const {
|
|
5985
|
+
hostname,
|
|
5986
|
+
isHostDenied
|
|
5987
|
+
} = isHostOnDenyList(url, options);
|
|
5988
|
+
if (isHostDenied) {
|
|
5989
|
+
return hostname + ' is in deny list';
|
|
5990
|
+
}
|
|
5991
|
+
if (isString(body)) {
|
|
5992
|
+
return body;
|
|
5993
|
+
}
|
|
5994
|
+
if (isDocument(body)) {
|
|
5995
|
+
return body.textContent;
|
|
5996
|
+
}
|
|
5997
|
+
if (isFormData(body)) {
|
|
5998
|
+
return formDataToQuery(body);
|
|
5999
|
+
}
|
|
6000
|
+
if (isObject(body)) {
|
|
6001
|
+
try {
|
|
6002
|
+
return JSON.stringify(body);
|
|
6003
|
+
} catch {
|
|
6004
|
+
return '[SessionReplay] Failed to stringify response object';
|
|
6005
|
+
}
|
|
6006
|
+
}
|
|
6007
|
+
return '[SessionReplay] Cannot read body of type ' + toString.call(body);
|
|
6008
|
+
}
|
|
6009
|
+
function initXhrObserver(cb, win, options) {
|
|
6010
|
+
if (!options.initiatorTypes.includes('xmlhttprequest')) {
|
|
6011
|
+
return () => {
|
|
6012
|
+
//
|
|
6013
|
+
};
|
|
6014
|
+
}
|
|
6015
|
+
const recordRequestHeaders = shouldRecordHeaders('request', options.recordHeaders);
|
|
6016
|
+
const recordResponseHeaders = shouldRecordHeaders('response', options.recordHeaders);
|
|
6017
|
+
const restorePatch = patch(win.XMLHttpRequest.prototype, 'open',
|
|
6018
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
6019
|
+
// @ts-ignore
|
|
6020
|
+
originalOpen => {
|
|
6021
|
+
return function (method, url, async = true, username, password) {
|
|
6022
|
+
// because this function is returned in its actual context `this` _is_ an XMLHttpRequest
|
|
6023
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
6024
|
+
// @ts-ignore
|
|
6025
|
+
const xhr = this;
|
|
6026
|
+
// check IE earlier than this, we only initialize if Request is present
|
|
6027
|
+
// eslint-disable-next-line compat/compat
|
|
6028
|
+
const req = new Request(url);
|
|
6029
|
+
const networkRequest = {};
|
|
6030
|
+
let start;
|
|
6031
|
+
let end;
|
|
6032
|
+
const requestHeaders = {};
|
|
6033
|
+
const originalSetRequestHeader = xhr.setRequestHeader.bind(xhr);
|
|
6034
|
+
xhr.setRequestHeader = (header, value) => {
|
|
6035
|
+
requestHeaders[header] = value;
|
|
6036
|
+
return originalSetRequestHeader(header, value);
|
|
6037
|
+
};
|
|
6038
|
+
if (recordRequestHeaders) {
|
|
6039
|
+
networkRequest.requestHeaders = requestHeaders;
|
|
6040
|
+
}
|
|
6041
|
+
const originalSend = xhr.send.bind(xhr);
|
|
6042
|
+
xhr.send = body => {
|
|
6043
|
+
if (shouldRecordBody({
|
|
6044
|
+
type: 'request',
|
|
6045
|
+
headers: requestHeaders,
|
|
6046
|
+
url,
|
|
6047
|
+
recordBody: options.recordBody
|
|
6048
|
+
})) {
|
|
6049
|
+
networkRequest.requestBody = _tryReadXHRBody({
|
|
6050
|
+
body,
|
|
6051
|
+
options,
|
|
6052
|
+
url
|
|
6053
|
+
});
|
|
6054
|
+
}
|
|
6055
|
+
start = win.performance.now();
|
|
6056
|
+
return originalSend(body);
|
|
6057
|
+
};
|
|
6058
|
+
const readyStateListener = () => {
|
|
6059
|
+
if (xhr.readyState !== xhr.DONE) {
|
|
6060
|
+
return;
|
|
6061
|
+
}
|
|
6062
|
+
// Clean up the listener immediately when done to prevent memory leaks
|
|
6063
|
+
xhr.removeEventListener('readystatechange', readyStateListener);
|
|
6064
|
+
end = win.performance.now();
|
|
6065
|
+
const responseHeaders = {};
|
|
6066
|
+
const rawHeaders = xhr.getAllResponseHeaders();
|
|
6067
|
+
const headers = rawHeaders.trim().split(/[\r\n]+/);
|
|
6068
|
+
headers.forEach(line => {
|
|
6069
|
+
const parts = line.split(': ');
|
|
6070
|
+
const header = parts.shift();
|
|
6071
|
+
const value = parts.join(': ');
|
|
6072
|
+
if (header) {
|
|
6073
|
+
responseHeaders[header] = value;
|
|
6074
|
+
}
|
|
6075
|
+
});
|
|
6076
|
+
if (recordResponseHeaders) {
|
|
6077
|
+
networkRequest.responseHeaders = responseHeaders;
|
|
6078
|
+
}
|
|
6079
|
+
if (shouldRecordBody({
|
|
6080
|
+
type: 'response',
|
|
6081
|
+
headers: responseHeaders,
|
|
6082
|
+
url,
|
|
6083
|
+
recordBody: options.recordBody
|
|
6084
|
+
})) {
|
|
6085
|
+
networkRequest.responseBody = _tryReadXHRBody({
|
|
6086
|
+
body: xhr.response,
|
|
6087
|
+
options,
|
|
6088
|
+
url
|
|
6089
|
+
});
|
|
6090
|
+
}
|
|
6091
|
+
getRequestPerformanceEntry(win, 'xmlhttprequest', req.url, start, end).then(entry => {
|
|
6092
|
+
const requests = prepareRequest({
|
|
6093
|
+
entry,
|
|
6094
|
+
method: method,
|
|
6095
|
+
status: xhr?.status,
|
|
6096
|
+
networkRequest,
|
|
6097
|
+
start,
|
|
6098
|
+
end,
|
|
6099
|
+
url: url.toString(),
|
|
6100
|
+
initiatorType: 'xmlhttprequest'
|
|
6101
|
+
});
|
|
6102
|
+
cb({
|
|
6103
|
+
requests
|
|
6104
|
+
});
|
|
6105
|
+
}).catch(() => {
|
|
6106
|
+
//
|
|
6107
|
+
});
|
|
6108
|
+
};
|
|
6109
|
+
// This is very tricky code, and making it passive won't bring many performance benefits,
|
|
6110
|
+
// so let's ignore the rule here.
|
|
6111
|
+
// eslint-disable-next-line posthog-js/no-add-event-listener
|
|
6112
|
+
xhr.addEventListener('readystatechange', readyStateListener);
|
|
6113
|
+
originalOpen.call(xhr, method, url, async, username, password);
|
|
6114
|
+
};
|
|
6115
|
+
});
|
|
6116
|
+
return () => {
|
|
6117
|
+
restorePatch();
|
|
6118
|
+
};
|
|
6119
|
+
}
|
|
6120
|
+
/**
|
|
6121
|
+
* Check if this PerformanceEntry is either a PerformanceResourceTiming or a PerformanceNavigationTiming
|
|
6122
|
+
* NB PerformanceNavigationTiming extends PerformanceResourceTiming
|
|
6123
|
+
* Here we don't care which interface it implements as both expose `serverTimings`
|
|
6124
|
+
*/
|
|
6125
|
+
const exposesServerTiming = event => !isNull(event) && (event.entryType === 'navigation' || event.entryType === 'resource');
|
|
6126
|
+
function prepareRequest({
|
|
6127
|
+
entry,
|
|
6128
|
+
method,
|
|
6129
|
+
status,
|
|
6130
|
+
networkRequest,
|
|
6131
|
+
isInitial,
|
|
6132
|
+
start,
|
|
6133
|
+
end,
|
|
6134
|
+
url,
|
|
6135
|
+
initiatorType
|
|
6136
|
+
}) {
|
|
6137
|
+
start = entry ? entry.startTime : start;
|
|
6138
|
+
end = entry ? entry.responseEnd : end;
|
|
6139
|
+
// kudos to sentry javascript sdk for excellent background on why to use Date.now() here
|
|
6140
|
+
// https://github.com/getsentry/sentry-javascript/blob/e856e40b6e71a73252e788cd42b5260f81c9c88e/packages/utils/src/time.ts#L70
|
|
6141
|
+
// can't start observer if performance.now() is not available
|
|
6142
|
+
// eslint-disable-next-line compat/compat
|
|
6143
|
+
const timeOrigin = Math.floor(Date.now() - performance.now());
|
|
6144
|
+
// clickhouse can't ingest timestamps that are floats
|
|
6145
|
+
// (in this case representing fractions of a millisecond we don't care about anyway)
|
|
6146
|
+
// use timeOrigin if we really can't gather a start time
|
|
6147
|
+
const timestamp = Math.floor(timeOrigin + (start || 0));
|
|
6148
|
+
const entryJSON = entry ? entry.toJSON() : {
|
|
6149
|
+
name: url
|
|
6150
|
+
};
|
|
6151
|
+
const requests = [{
|
|
6152
|
+
...entryJSON,
|
|
6153
|
+
startTime: isUndefined(start) ? undefined : Math.round(start),
|
|
6154
|
+
endTime: isUndefined(end) ? undefined : Math.round(end),
|
|
6155
|
+
timeOrigin,
|
|
6156
|
+
timestamp,
|
|
6157
|
+
method: method,
|
|
6158
|
+
initiatorType: initiatorType ? initiatorType : entry ? entry.initiatorType : undefined,
|
|
6159
|
+
status,
|
|
6160
|
+
requestHeaders: networkRequest.requestHeaders,
|
|
6161
|
+
requestBody: networkRequest.requestBody,
|
|
6162
|
+
responseHeaders: networkRequest.responseHeaders,
|
|
6163
|
+
responseBody: networkRequest.responseBody,
|
|
6164
|
+
isInitial
|
|
6165
|
+
}];
|
|
6166
|
+
if (exposesServerTiming(entry)) {
|
|
6167
|
+
for (const timing of entry.serverTiming || []) {
|
|
6168
|
+
requests.push({
|
|
6169
|
+
timeOrigin,
|
|
6170
|
+
timestamp,
|
|
6171
|
+
startTime: Math.round(entry.startTime),
|
|
6172
|
+
name: timing.name,
|
|
6173
|
+
duration: timing.duration,
|
|
6174
|
+
// the spec has a closed list of possible types
|
|
6175
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/PerformanceEntry/entryType
|
|
6176
|
+
// but, we need to know this was a server timing so that we know to
|
|
6177
|
+
// match it to the appropriate navigation or resource timing
|
|
6178
|
+
// that matching will have to be on timestamp and $current_url
|
|
6179
|
+
entryType: 'serverTiming'
|
|
6180
|
+
});
|
|
6181
|
+
}
|
|
6182
|
+
}
|
|
6183
|
+
return requests;
|
|
6184
|
+
}
|
|
6185
|
+
const contentTypePrefixDenyList = ['video/', 'audio/'];
|
|
6186
|
+
function _checkForCannotReadResponseBody({
|
|
6187
|
+
r,
|
|
6188
|
+
options,
|
|
6189
|
+
url
|
|
6190
|
+
}) {
|
|
6191
|
+
if (r.headers.get('Transfer-Encoding') === 'chunked') {
|
|
6192
|
+
return 'Chunked Transfer-Encoding is not supported';
|
|
6193
|
+
}
|
|
6194
|
+
// `get` and `has` are case-insensitive
|
|
6195
|
+
// but return the header value with the casing that was supplied
|
|
6196
|
+
const contentType = r.headers.get('Content-Type')?.toLowerCase();
|
|
6197
|
+
const contentTypeIsDenied = contentTypePrefixDenyList.some(prefix => contentType?.startsWith(prefix));
|
|
6198
|
+
if (contentType && contentTypeIsDenied) {
|
|
6199
|
+
return `Content-Type ${contentType} is not supported`;
|
|
6200
|
+
}
|
|
6201
|
+
const {
|
|
6202
|
+
hostname,
|
|
6203
|
+
isHostDenied
|
|
6204
|
+
} = isHostOnDenyList(url, options);
|
|
6205
|
+
if (isHostDenied) {
|
|
6206
|
+
return hostname + ' is in deny list';
|
|
6207
|
+
}
|
|
6208
|
+
return null;
|
|
6209
|
+
}
|
|
6210
|
+
function _tryReadBody(r) {
|
|
6211
|
+
// there are now already multiple places where we're using Promise...
|
|
6212
|
+
// eslint-disable-next-line compat/compat
|
|
6213
|
+
return new Promise((resolve, reject) => {
|
|
6214
|
+
const timeout = setTimeout(() => resolve('[SessionReplay] Timeout while trying to read body'), 500);
|
|
6215
|
+
try {
|
|
6216
|
+
r.clone().text().then(txt => resolve(txt), reason => reject(reason)).finally(() => clearTimeout(timeout));
|
|
6217
|
+
} catch {
|
|
6218
|
+
clearTimeout(timeout);
|
|
6219
|
+
resolve('[SessionReplay] Failed to read body');
|
|
6220
|
+
}
|
|
6221
|
+
});
|
|
6222
|
+
}
|
|
6223
|
+
async function _tryReadRequestBody({
|
|
6224
|
+
r,
|
|
6225
|
+
options,
|
|
6226
|
+
url
|
|
6227
|
+
}) {
|
|
6228
|
+
const {
|
|
6229
|
+
hostname,
|
|
6230
|
+
isHostDenied
|
|
6231
|
+
} = isHostOnDenyList(url, options);
|
|
6232
|
+
if (isHostDenied) {
|
|
6233
|
+
return Promise.resolve(hostname + ' is in deny list');
|
|
6234
|
+
}
|
|
6235
|
+
return _tryReadBody(r);
|
|
6236
|
+
}
|
|
6237
|
+
async function _tryReadResponseBody({
|
|
6238
|
+
r,
|
|
6239
|
+
options,
|
|
6240
|
+
url
|
|
6241
|
+
}) {
|
|
6242
|
+
const cannotReadBodyReason = _checkForCannotReadResponseBody({
|
|
6243
|
+
r,
|
|
6244
|
+
options,
|
|
6245
|
+
url
|
|
6246
|
+
});
|
|
6247
|
+
if (!isNull(cannotReadBodyReason)) {
|
|
6248
|
+
return Promise.resolve(cannotReadBodyReason);
|
|
6249
|
+
}
|
|
6250
|
+
return _tryReadBody(r);
|
|
6251
|
+
}
|
|
6252
|
+
function initFetchObserver(cb, win, options) {
|
|
6253
|
+
if (!options.initiatorTypes.includes('fetch')) {
|
|
6254
|
+
return () => {
|
|
6255
|
+
//
|
|
6256
|
+
};
|
|
6257
|
+
}
|
|
6258
|
+
const recordRequestHeaders = shouldRecordHeaders('request', options.recordHeaders);
|
|
6259
|
+
const recordResponseHeaders = shouldRecordHeaders('response', options.recordHeaders);
|
|
6260
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
6261
|
+
// @ts-ignore
|
|
6262
|
+
const restorePatch = patch(win, 'fetch', originalFetch => {
|
|
6263
|
+
return async function (url, init) {
|
|
6264
|
+
// check IE earlier than this, we only initialize if Request is present
|
|
6265
|
+
// eslint-disable-next-line compat/compat
|
|
6266
|
+
const req = new Request(url, init);
|
|
6267
|
+
let res;
|
|
6268
|
+
const networkRequest = {};
|
|
6269
|
+
let start;
|
|
6270
|
+
let end;
|
|
6271
|
+
try {
|
|
6272
|
+
const requestHeaders = {};
|
|
6273
|
+
req.headers.forEach((value, header) => {
|
|
6274
|
+
requestHeaders[header] = value;
|
|
6275
|
+
});
|
|
6276
|
+
if (recordRequestHeaders) {
|
|
6277
|
+
networkRequest.requestHeaders = requestHeaders;
|
|
6278
|
+
}
|
|
6279
|
+
if (shouldRecordBody({
|
|
6280
|
+
type: 'request',
|
|
6281
|
+
headers: requestHeaders,
|
|
6282
|
+
url,
|
|
6283
|
+
recordBody: options.recordBody
|
|
6284
|
+
})) {
|
|
6285
|
+
networkRequest.requestBody = await _tryReadRequestBody({
|
|
6286
|
+
r: req,
|
|
6287
|
+
options,
|
|
6288
|
+
url
|
|
6289
|
+
});
|
|
6290
|
+
}
|
|
6291
|
+
start = win.performance.now();
|
|
6292
|
+
res = await originalFetch(req);
|
|
6293
|
+
end = win.performance.now();
|
|
6294
|
+
const responseHeaders = {};
|
|
6295
|
+
res.headers.forEach((value, header) => {
|
|
6296
|
+
responseHeaders[header] = value;
|
|
6297
|
+
});
|
|
6298
|
+
if (recordResponseHeaders) {
|
|
6299
|
+
networkRequest.responseHeaders = responseHeaders;
|
|
6300
|
+
}
|
|
6301
|
+
if (shouldRecordBody({
|
|
6302
|
+
type: 'response',
|
|
6303
|
+
headers: responseHeaders,
|
|
6304
|
+
url,
|
|
6305
|
+
recordBody: options.recordBody
|
|
6306
|
+
})) {
|
|
6307
|
+
networkRequest.responseBody = await _tryReadResponseBody({
|
|
6308
|
+
r: res,
|
|
6309
|
+
options,
|
|
6310
|
+
url
|
|
6311
|
+
});
|
|
6312
|
+
}
|
|
6313
|
+
return res;
|
|
6314
|
+
} finally {
|
|
6315
|
+
getRequestPerformanceEntry(win, 'fetch', req.url, start, end).then(entry => {
|
|
6316
|
+
const requests = prepareRequest({
|
|
6317
|
+
entry,
|
|
6318
|
+
method: req.method,
|
|
6319
|
+
status: res?.status,
|
|
6320
|
+
networkRequest,
|
|
6321
|
+
start,
|
|
6322
|
+
end,
|
|
6323
|
+
url: req.url,
|
|
6324
|
+
initiatorType: 'fetch'
|
|
6325
|
+
});
|
|
6326
|
+
cb({
|
|
6327
|
+
requests
|
|
6328
|
+
});
|
|
6329
|
+
}).catch(() => {
|
|
6330
|
+
//
|
|
6331
|
+
});
|
|
6332
|
+
}
|
|
6333
|
+
};
|
|
6334
|
+
});
|
|
6335
|
+
return () => {
|
|
6336
|
+
restorePatch();
|
|
6337
|
+
};
|
|
6338
|
+
}
|
|
6339
|
+
let initialisedHandler = null;
|
|
6340
|
+
function initNetworkObserver(callback, win,
|
|
6341
|
+
// top window or in an iframe
|
|
6342
|
+
options) {
|
|
6343
|
+
if (!('performance' in win)) {
|
|
6344
|
+
return () => {
|
|
6345
|
+
//
|
|
6346
|
+
};
|
|
6347
|
+
}
|
|
6348
|
+
if (initialisedHandler) {
|
|
6349
|
+
logger.warn('Network observer already initialised, doing nothing');
|
|
6350
|
+
return () => {
|
|
6351
|
+
// the first caller should already have this handler and will be responsible for teardown
|
|
6352
|
+
};
|
|
6353
|
+
}
|
|
6354
|
+
const networkOptions = options ? Object.assign({}, defaultNetworkOptions, options) : defaultNetworkOptions;
|
|
6355
|
+
const cb = data => {
|
|
6356
|
+
const requests = [];
|
|
6357
|
+
data.requests.forEach(request => {
|
|
6358
|
+
const maskedRequest = networkOptions.maskRequestFn(request);
|
|
6359
|
+
if (maskedRequest) {
|
|
6360
|
+
requests.push(maskedRequest);
|
|
6361
|
+
}
|
|
6362
|
+
});
|
|
6363
|
+
if (requests.length > 0) {
|
|
6364
|
+
callback({
|
|
6365
|
+
...data,
|
|
6366
|
+
requests
|
|
6367
|
+
});
|
|
6368
|
+
}
|
|
6369
|
+
};
|
|
6370
|
+
const performanceObserver = initPerformanceObserver(cb, win, networkOptions);
|
|
6371
|
+
// only wrap fetch and xhr if headers or body are being recorded
|
|
6372
|
+
let xhrObserver = () => {};
|
|
6373
|
+
let fetchObserver = () => {};
|
|
6374
|
+
if (networkOptions.recordHeaders || networkOptions.recordBody) {
|
|
6375
|
+
xhrObserver = initXhrObserver(cb, win, networkOptions);
|
|
6376
|
+
fetchObserver = initFetchObserver(cb, win, networkOptions);
|
|
6377
|
+
}
|
|
6378
|
+
const teardown = () => {
|
|
6379
|
+
performanceObserver();
|
|
6380
|
+
xhrObserver();
|
|
6381
|
+
fetchObserver();
|
|
6382
|
+
// allow future observers to initialize after cleanup
|
|
6383
|
+
initialisedHandler = null;
|
|
6384
|
+
};
|
|
6385
|
+
initialisedHandler = teardown;
|
|
6386
|
+
return teardown;
|
|
6387
|
+
}
|
|
6388
|
+
// use the plugin name so that when this functionality is adopted into rrweb
|
|
6389
|
+
// we can remove this plugin and use the core functionality with the same data
|
|
6390
|
+
const NETWORK_PLUGIN_NAME = 'rrweb/network@1';
|
|
6391
|
+
// TODO how should this be typed?
|
|
6392
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
6393
|
+
// @ts-ignore
|
|
6394
|
+
const getRecordNetworkPlugin = options => {
|
|
6395
|
+
return {
|
|
6396
|
+
name: NETWORK_PLUGIN_NAME,
|
|
6397
|
+
observer: initNetworkObserver,
|
|
6398
|
+
options: options
|
|
6399
|
+
};
|
|
6400
|
+
};
|
|
6401
|
+
// rrweb/networ@1 ends
|
|
6402
|
+
|
|
6403
|
+
var networkPlugin = /*#__PURE__*/Object.freeze({
|
|
6404
|
+
__proto__: null,
|
|
6405
|
+
NETWORK_PLUGIN_NAME: NETWORK_PLUGIN_NAME,
|
|
6406
|
+
findLast: findLast,
|
|
6407
|
+
getRecordNetworkPlugin: getRecordNetworkPlugin,
|
|
6408
|
+
shouldRecordBody: shouldRecordBody
|
|
6409
|
+
});
|
|
6410
|
+
|
|
5660
6411
|
export { Leanbase };
|
|
5661
6412
|
//# sourceMappingURL=index.mjs.map
|