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