@leanbase-giangnd/js 0.3.2 → 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 +788 -85
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.mjs +789 -86
- package/dist/index.mjs.map +1 -1
- package/dist/leanbase.iife.js +2529 -510
- package/dist/leanbase.iife.js.map +1 -1
- package/package.json +2 -1
- package/src/extensions/replay/external/lazy-loaded-session-recorder.ts +89 -15
- package/src/extensions/replay/session-recording.ts +9 -6
- 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
|
}
|
|
@@ -4102,7 +4122,7 @@ class LazyLoadedSessionRecording {
|
|
|
4102
4122
|
this._eventTriggerMatching = new EventTriggerMatching(this._instance);
|
|
4103
4123
|
this._buffer = this._clearBuffer();
|
|
4104
4124
|
if (this._sessionIdleThresholdMilliseconds >= this._sessionManager.sessionTimeoutMs) {
|
|
4105
|
-
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`);
|
|
4106
4126
|
}
|
|
4107
4127
|
}
|
|
4108
4128
|
get _masking() {
|
|
@@ -4176,25 +4196,62 @@ class LazyLoadedSessionRecording {
|
|
|
4176
4196
|
payloadHostDenyList: networkPayloadCapture_server_side?.payloadHostDenyList
|
|
4177
4197
|
};
|
|
4178
4198
|
}
|
|
4179
|
-
|
|
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() {
|
|
4180
4231
|
const plugins = [];
|
|
4232
|
+
if (!win) {
|
|
4233
|
+
return plugins;
|
|
4234
|
+
}
|
|
4181
4235
|
if (this._isConsoleLogCaptureEnabled) {
|
|
4182
|
-
|
|
4236
|
+
const consolePlugin = await this._loadConsolePlugin();
|
|
4237
|
+
if (consolePlugin) {
|
|
4238
|
+
plugins.push(consolePlugin);
|
|
4239
|
+
}
|
|
4183
4240
|
}
|
|
4184
4241
|
if (this._networkPayloadCapture) {
|
|
4185
4242
|
const canRecordNetwork = !isLocalhost() || this._forceAllowLocalhostNetworkCapture;
|
|
4186
4243
|
if (canRecordNetwork) {
|
|
4187
|
-
const
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
plugins.push(networkFactory(buildNetworkRequestOptions(this._instance.config, this._networkPayloadCapture)));
|
|
4191
|
-
} else {
|
|
4192
|
-
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);
|
|
4193
4247
|
}
|
|
4194
4248
|
} else {
|
|
4195
|
-
|
|
4249
|
+
this._debug('NetworkCapture not started because we are on localhost.');
|
|
4196
4250
|
}
|
|
4197
4251
|
}
|
|
4252
|
+
if (plugins.length > 0) {
|
|
4253
|
+
this._debug('Replay plugins loaded', plugins.map(p => p.name));
|
|
4254
|
+
}
|
|
4198
4255
|
return plugins;
|
|
4199
4256
|
}
|
|
4200
4257
|
_maskUrl(url) {
|
|
@@ -4223,7 +4280,7 @@ class LazyLoadedSessionRecording {
|
|
|
4223
4280
|
rrwebMethod: queuedRRWebEvent.rrwebMethod
|
|
4224
4281
|
});
|
|
4225
4282
|
} else {
|
|
4226
|
-
logger.warn('could not emit queued rrweb event.', e, queuedRRWebEvent);
|
|
4283
|
+
logger$1.warn('could not emit queued rrweb event.', e, queuedRRWebEvent);
|
|
4227
4284
|
}
|
|
4228
4285
|
return false;
|
|
4229
4286
|
}
|
|
@@ -4335,7 +4392,7 @@ class LazyLoadedSessionRecording {
|
|
|
4335
4392
|
this._urlTriggerMatching.urlBlocked = true;
|
|
4336
4393
|
// Clear the snapshot timer since we don't want new snapshots while paused
|
|
4337
4394
|
clearInterval(this._fullSnapshotTimer);
|
|
4338
|
-
logger.info('recording paused due to URL blocker');
|
|
4395
|
+
logger$1.info('recording paused due to URL blocker');
|
|
4339
4396
|
this._tryAddCustomEvent('recording paused', {
|
|
4340
4397
|
reason: 'url blocker'
|
|
4341
4398
|
});
|
|
@@ -4354,7 +4411,7 @@ class LazyLoadedSessionRecording {
|
|
|
4354
4411
|
this._tryAddCustomEvent('recording resumed', {
|
|
4355
4412
|
reason: 'left blocked url'
|
|
4356
4413
|
});
|
|
4357
|
-
logger.info('recording resumed');
|
|
4414
|
+
logger$1.info('recording resumed');
|
|
4358
4415
|
}
|
|
4359
4416
|
_activateTrigger(triggerType) {
|
|
4360
4417
|
if (!this.isStarted || !this._recording || !this._isFullyReady) {
|
|
@@ -4387,7 +4444,7 @@ class LazyLoadedSessionRecording {
|
|
|
4387
4444
|
this._isFullyReady = false;
|
|
4388
4445
|
const config = this._remoteConfig;
|
|
4389
4446
|
if (!config) {
|
|
4390
|
-
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');
|
|
4391
4448
|
return;
|
|
4392
4449
|
}
|
|
4393
4450
|
// We want to ensure the sessionManager is reset if necessary on loading the recorder
|
|
@@ -4470,7 +4527,7 @@ class LazyLoadedSessionRecording {
|
|
|
4470
4527
|
});
|
|
4471
4528
|
}
|
|
4472
4529
|
} catch (e) {
|
|
4473
|
-
logger.error('Could not add $pageview to rrweb session', e);
|
|
4530
|
+
logger$1.error('Could not add $pageview to rrweb session', e);
|
|
4474
4531
|
}
|
|
4475
4532
|
});
|
|
4476
4533
|
}
|
|
@@ -4515,17 +4572,30 @@ class LazyLoadedSessionRecording {
|
|
|
4515
4572
|
this._stopRrweb = undefined;
|
|
4516
4573
|
this._isFullyReady = false;
|
|
4517
4574
|
this._hasReportedRecordingInitialized = false;
|
|
4518
|
-
logger.info('stopped');
|
|
4575
|
+
logger$1.info('stopped');
|
|
4519
4576
|
}
|
|
4520
4577
|
_snapshotIngestionUrl() {
|
|
4521
4578
|
const endpointFor = this._instance?.requestRouter?.endpointFor;
|
|
4522
|
-
|
|
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) {
|
|
4523
4590
|
return null;
|
|
4524
4591
|
}
|
|
4525
4592
|
try {
|
|
4526
|
-
|
|
4593
|
+
// eslint-disable-next-line compat/compat
|
|
4594
|
+
return new URL(this._endpoint, host).href;
|
|
4527
4595
|
} catch {
|
|
4528
|
-
|
|
4596
|
+
const normalizedHost = host.endsWith('/') ? host.slice(0, -1) : host;
|
|
4597
|
+
const normalizedEndpoint = this._endpoint.startsWith('/') ? this._endpoint : `/${this._endpoint}`;
|
|
4598
|
+
return `${normalizedHost}${normalizedEndpoint}`;
|
|
4529
4599
|
}
|
|
4530
4600
|
}
|
|
4531
4601
|
_canCaptureSnapshots() {
|
|
@@ -4610,7 +4680,7 @@ class LazyLoadedSessionRecording {
|
|
|
4610
4680
|
}
|
|
4611
4681
|
this._captureSnapshotBuffered(properties);
|
|
4612
4682
|
} catch (e) {
|
|
4613
|
-
logger.error('error processing rrweb event', e);
|
|
4683
|
+
logger$1.error('error processing rrweb event', e);
|
|
4614
4684
|
}
|
|
4615
4685
|
}
|
|
4616
4686
|
get status() {
|
|
@@ -4701,7 +4771,7 @@ class LazyLoadedSessionRecording {
|
|
|
4701
4771
|
if (!this._canCaptureSnapshots()) {
|
|
4702
4772
|
if (!this._loggedMissingEndpointFor) {
|
|
4703
4773
|
this._loggedMissingEndpointFor = true;
|
|
4704
|
-
logger.warn('snapshot capture skipped because requestRouter.endpointFor is unavailable');
|
|
4774
|
+
logger$1.warn('snapshot capture skipped because requestRouter.endpointFor is unavailable');
|
|
4705
4775
|
}
|
|
4706
4776
|
this._flushBufferTimer = setTimeout(() => {
|
|
4707
4777
|
this._flushBuffer();
|
|
@@ -4744,7 +4814,7 @@ class LazyLoadedSessionRecording {
|
|
|
4744
4814
|
if (!url) {
|
|
4745
4815
|
if (!this._loggedMissingEndpointFor) {
|
|
4746
4816
|
this._loggedMissingEndpointFor = true;
|
|
4747
|
-
logger.warn('snapshot capture skipped because requestRouter.endpointFor is unavailable');
|
|
4817
|
+
logger$1.warn('snapshot capture skipped because requestRouter.endpointFor is unavailable');
|
|
4748
4818
|
}
|
|
4749
4819
|
return;
|
|
4750
4820
|
}
|
|
@@ -4757,7 +4827,7 @@ class LazyLoadedSessionRecording {
|
|
|
4757
4827
|
skip_client_rate_limiting: true
|
|
4758
4828
|
});
|
|
4759
4829
|
} catch (e) {
|
|
4760
|
-
logger.error('failed to capture snapshot', e);
|
|
4830
|
+
logger$1.error('failed to capture snapshot', e);
|
|
4761
4831
|
}
|
|
4762
4832
|
}
|
|
4763
4833
|
_snapshotUrl() {
|
|
@@ -4816,9 +4886,9 @@ class LazyLoadedSessionRecording {
|
|
|
4816
4886
|
});
|
|
4817
4887
|
const message = startReason.replace('_', ' ');
|
|
4818
4888
|
if (typeof tagPayload === 'undefined') {
|
|
4819
|
-
logger.info(message);
|
|
4889
|
+
logger$1.info(message);
|
|
4820
4890
|
} else {
|
|
4821
|
-
logger.info(message, tagPayload);
|
|
4891
|
+
logger$1.info(message, tagPayload);
|
|
4822
4892
|
}
|
|
4823
4893
|
if (!includes(['recording_initialized', 'session_id_changed'], startReason)) {
|
|
4824
4894
|
this._tryAddCustomEvent(startReason, tagPayload);
|
|
@@ -4919,7 +4989,7 @@ class LazyLoadedSessionRecording {
|
|
|
4919
4989
|
if (shouldSample) {
|
|
4920
4990
|
this._reportStarted(SAMPLED);
|
|
4921
4991
|
} else {
|
|
4922
|
-
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.`);
|
|
4923
4993
|
}
|
|
4924
4994
|
this._tryAddCustomEvent('samplingDecisionMade', {
|
|
4925
4995
|
sampleRate: currentSampleRate,
|
|
@@ -4942,7 +5012,7 @@ class LazyLoadedSessionRecording {
|
|
|
4942
5012
|
this._activateTrigger('event');
|
|
4943
5013
|
}
|
|
4944
5014
|
} catch (e) {
|
|
4945
|
-
logger.error('Could not activate event trigger', e);
|
|
5015
|
+
logger$1.error('Could not activate event trigger', e);
|
|
4946
5016
|
}
|
|
4947
5017
|
});
|
|
4948
5018
|
}
|
|
@@ -5024,7 +5094,7 @@ class LazyLoadedSessionRecording {
|
|
|
5024
5094
|
this._disablePermanently('rrweb record function unavailable');
|
|
5025
5095
|
return;
|
|
5026
5096
|
}
|
|
5027
|
-
const activePlugins = this._gatherRRWebPlugins();
|
|
5097
|
+
const activePlugins = await this._gatherRRWebPlugins();
|
|
5028
5098
|
let stopHandler;
|
|
5029
5099
|
try {
|
|
5030
5100
|
stopHandler = rrwebRecord({
|
|
@@ -5033,7 +5103,7 @@ class LazyLoadedSessionRecording {
|
|
|
5033
5103
|
this.onRRwebEmit(event);
|
|
5034
5104
|
} catch (e) {
|
|
5035
5105
|
// never throw from rrweb emit handler
|
|
5036
|
-
logger.error('error in rrweb emit handler', e);
|
|
5106
|
+
logger$1.error('error in rrweb emit handler', e);
|
|
5037
5107
|
}
|
|
5038
5108
|
},
|
|
5039
5109
|
plugins: activePlugins,
|
|
@@ -5058,7 +5128,7 @@ class LazyLoadedSessionRecording {
|
|
|
5058
5128
|
bucketSize: this._instance.config.session_recording?.__mutationThrottlerBucketSize,
|
|
5059
5129
|
onBlockedNode: (id, node) => {
|
|
5060
5130
|
const message = `Too many mutations on node '${id}'. Rate limiting. This could be due to SVG animations or something similar`;
|
|
5061
|
-
logger.info(message, {
|
|
5131
|
+
logger$1.info(message, {
|
|
5062
5132
|
node: node
|
|
5063
5133
|
});
|
|
5064
5134
|
this.log(LOGGER_PREFIX$1 + ' ' + message, 'warn');
|
|
@@ -5085,11 +5155,16 @@ class LazyLoadedSessionRecording {
|
|
|
5085
5155
|
/* eslint-disable posthog-js/no-direct-function-check */
|
|
5086
5156
|
const LOGGER_PREFIX = '[SessionRecording]';
|
|
5087
5157
|
const log = {
|
|
5088
|
-
info: (...args) => logger$
|
|
5089
|
-
warn: (...args) => logger$
|
|
5090
|
-
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)
|
|
5091
5161
|
};
|
|
5092
5162
|
class SessionRecording {
|
|
5163
|
+
_debug(...args) {
|
|
5164
|
+
if (this._instance?.config?.debug) {
|
|
5165
|
+
log.info(...args);
|
|
5166
|
+
}
|
|
5167
|
+
}
|
|
5093
5168
|
get started() {
|
|
5094
5169
|
return !!this._lazyLoadedSessionRecording?.isStarted;
|
|
5095
5170
|
}
|
|
@@ -5127,8 +5202,10 @@ class SessionRecording {
|
|
|
5127
5202
|
}
|
|
5128
5203
|
const canRunReplay = !isUndefined(Object.assign) && !isUndefined(Array.from);
|
|
5129
5204
|
if (this._isRecordingEnabled && canRunReplay) {
|
|
5205
|
+
this._debug('Session replay enabled; starting recorder');
|
|
5130
5206
|
this._lazyLoadAndStart(startReason);
|
|
5131
5207
|
} else {
|
|
5208
|
+
this._debug('Session replay disabled; stopping recorder');
|
|
5132
5209
|
this.stopRecording();
|
|
5133
5210
|
}
|
|
5134
5211
|
}
|
|
@@ -5200,18 +5277,15 @@ class SessionRecording {
|
|
|
5200
5277
|
log.info('skipping remote config with no sessionRecording', response);
|
|
5201
5278
|
return;
|
|
5202
5279
|
}
|
|
5203
|
-
this._receivedFlags = true;
|
|
5204
|
-
if (response.sessionRecording === false) {
|
|
5205
|
-
return;
|
|
5206
|
-
}
|
|
5207
5280
|
this._persistRemoteConfig(response);
|
|
5281
|
+
this._receivedFlags = true;
|
|
5208
5282
|
this.startIfEnabledOrStop();
|
|
5209
5283
|
}
|
|
5210
5284
|
log(message, level = 'log') {
|
|
5211
5285
|
if (this._lazyLoadedSessionRecording?.log) {
|
|
5212
5286
|
this._lazyLoadedSessionRecording.log(message, level);
|
|
5213
5287
|
} else {
|
|
5214
|
-
logger$
|
|
5288
|
+
logger$3.warn('log called before recorder was ready');
|
|
5215
5289
|
}
|
|
5216
5290
|
}
|
|
5217
5291
|
get _scriptName() {
|
|
@@ -5234,10 +5308,10 @@ class SessionRecording {
|
|
|
5234
5308
|
try {
|
|
5235
5309
|
const maybePromise = this._lazyLoadedSessionRecording.start(startReason);
|
|
5236
5310
|
if (maybePromise && typeof maybePromise.catch === 'function') {
|
|
5237
|
-
maybePromise.catch(e => logger$
|
|
5311
|
+
maybePromise.catch(e => logger$3.error('error starting session recording', e));
|
|
5238
5312
|
}
|
|
5239
5313
|
} catch (e) {
|
|
5240
|
-
logger$
|
|
5314
|
+
logger$3.error('error starting session recording', e);
|
|
5241
5315
|
}
|
|
5242
5316
|
return;
|
|
5243
5317
|
}
|
|
@@ -5250,10 +5324,10 @@ class SessionRecording {
|
|
|
5250
5324
|
try {
|
|
5251
5325
|
const maybePromise = this._lazyLoadedSessionRecording.start(startReason);
|
|
5252
5326
|
if (maybePromise && typeof maybePromise.catch === 'function') {
|
|
5253
|
-
maybePromise.catch(e => logger$
|
|
5327
|
+
maybePromise.catch(e => logger$3.error('error starting session recording', e));
|
|
5254
5328
|
}
|
|
5255
5329
|
} catch (e) {
|
|
5256
|
-
logger$
|
|
5330
|
+
logger$3.error('error starting session recording', e);
|
|
5257
5331
|
}
|
|
5258
5332
|
}
|
|
5259
5333
|
/**
|
|
@@ -5410,12 +5484,12 @@ class Leanbase extends PostHogCore {
|
|
|
5410
5484
|
}, 1);
|
|
5411
5485
|
}
|
|
5412
5486
|
const triggerRemoteConfigLoad = reason => {
|
|
5413
|
-
logger$
|
|
5487
|
+
logger$3.info(`remote config load triggered via ${reason}`);
|
|
5414
5488
|
void this.loadRemoteConfig();
|
|
5415
5489
|
};
|
|
5416
5490
|
if (document) {
|
|
5417
5491
|
if (document.readyState === 'loading') {
|
|
5418
|
-
logger$
|
|
5492
|
+
logger$3.info('remote config load deferred until DOMContentLoaded');
|
|
5419
5493
|
const onDomReady = () => {
|
|
5420
5494
|
document?.removeEventListener('DOMContentLoaded', onDomReady);
|
|
5421
5495
|
triggerRemoteConfigLoad('dom');
|
|
@@ -5508,7 +5582,7 @@ class Leanbase extends PostHogCore {
|
|
|
5508
5582
|
try {
|
|
5509
5583
|
this.sessionRecording.startIfEnabledOrStop();
|
|
5510
5584
|
} catch (e) {
|
|
5511
|
-
logger$
|
|
5585
|
+
logger$3.error('Failed to start session recording', e);
|
|
5512
5586
|
}
|
|
5513
5587
|
}
|
|
5514
5588
|
fetch(url, options) {
|
|
@@ -5574,7 +5648,7 @@ class Leanbase extends PostHogCore {
|
|
|
5574
5648
|
};
|
|
5575
5649
|
properties['distinct_id'] = persistenceProps.distinct_id;
|
|
5576
5650
|
if (!(isString(properties['distinct_id']) || isNumber(properties['distinct_id'])) || isEmptyString(properties['distinct_id'])) {
|
|
5577
|
-
logger$
|
|
5651
|
+
logger$3.error('Invalid distinct_id for replay event. This indicates a bug in your implementation');
|
|
5578
5652
|
}
|
|
5579
5653
|
return properties;
|
|
5580
5654
|
}
|
|
@@ -5649,11 +5723,11 @@ class Leanbase extends PostHogCore {
|
|
|
5649
5723
|
return;
|
|
5650
5724
|
}
|
|
5651
5725
|
if (isUndefined(event) || !isString(event)) {
|
|
5652
|
-
logger$
|
|
5726
|
+
logger$3.error('No event name provided to posthog.capture');
|
|
5653
5727
|
return;
|
|
5654
5728
|
}
|
|
5655
5729
|
if (properties?.$current_url && !isString(properties?.$current_url)) {
|
|
5656
|
-
logger$
|
|
5730
|
+
logger$3.error('Invalid `$current_url` property provided to `posthog.capture`. Input must be a string. Ignoring provided value.');
|
|
5657
5731
|
delete properties?.$current_url;
|
|
5658
5732
|
}
|
|
5659
5733
|
this.sessionPersistence.update_search_keyword();
|
|
@@ -5705,5 +5779,634 @@ class Leanbase extends PostHogCore {
|
|
|
5705
5779
|
}
|
|
5706
5780
|
}
|
|
5707
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
|
+
|
|
5708
6411
|
export { Leanbase };
|
|
5709
6412
|
//# sourceMappingURL=index.mjs.map
|