@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.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
|
}
|
|
@@ -4104,7 +4124,7 @@ class LazyLoadedSessionRecording {
|
|
|
4104
4124
|
this._eventTriggerMatching = new EventTriggerMatching(this._instance);
|
|
4105
4125
|
this._buffer = this._clearBuffer();
|
|
4106
4126
|
if (this._sessionIdleThresholdMilliseconds >= this._sessionManager.sessionTimeoutMs) {
|
|
4107
|
-
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`);
|
|
4108
4128
|
}
|
|
4109
4129
|
}
|
|
4110
4130
|
get _masking() {
|
|
@@ -4178,25 +4198,62 @@ class LazyLoadedSessionRecording {
|
|
|
4178
4198
|
payloadHostDenyList: networkPayloadCapture_server_side?.payloadHostDenyList
|
|
4179
4199
|
};
|
|
4180
4200
|
}
|
|
4181
|
-
|
|
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() {
|
|
4182
4233
|
const plugins = [];
|
|
4234
|
+
if (!win) {
|
|
4235
|
+
return plugins;
|
|
4236
|
+
}
|
|
4183
4237
|
if (this._isConsoleLogCaptureEnabled) {
|
|
4184
|
-
|
|
4238
|
+
const consolePlugin = await this._loadConsolePlugin();
|
|
4239
|
+
if (consolePlugin) {
|
|
4240
|
+
plugins.push(consolePlugin);
|
|
4241
|
+
}
|
|
4185
4242
|
}
|
|
4186
4243
|
if (this._networkPayloadCapture) {
|
|
4187
4244
|
const canRecordNetwork = !isLocalhost() || this._forceAllowLocalhostNetworkCapture;
|
|
4188
4245
|
if (canRecordNetwork) {
|
|
4189
|
-
const
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
plugins.push(networkFactory(buildNetworkRequestOptions(this._instance.config, this._networkPayloadCapture)));
|
|
4193
|
-
} else {
|
|
4194
|
-
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);
|
|
4195
4249
|
}
|
|
4196
4250
|
} else {
|
|
4197
|
-
|
|
4251
|
+
this._debug('NetworkCapture not started because we are on localhost.');
|
|
4198
4252
|
}
|
|
4199
4253
|
}
|
|
4254
|
+
if (plugins.length > 0) {
|
|
4255
|
+
this._debug('Replay plugins loaded', plugins.map(p => p.name));
|
|
4256
|
+
}
|
|
4200
4257
|
return plugins;
|
|
4201
4258
|
}
|
|
4202
4259
|
_maskUrl(url) {
|
|
@@ -4225,7 +4282,7 @@ class LazyLoadedSessionRecording {
|
|
|
4225
4282
|
rrwebMethod: queuedRRWebEvent.rrwebMethod
|
|
4226
4283
|
});
|
|
4227
4284
|
} else {
|
|
4228
|
-
logger.warn('could not emit queued rrweb event.', e, queuedRRWebEvent);
|
|
4285
|
+
logger$1.warn('could not emit queued rrweb event.', e, queuedRRWebEvent);
|
|
4229
4286
|
}
|
|
4230
4287
|
return false;
|
|
4231
4288
|
}
|
|
@@ -4337,7 +4394,7 @@ class LazyLoadedSessionRecording {
|
|
|
4337
4394
|
this._urlTriggerMatching.urlBlocked = true;
|
|
4338
4395
|
// Clear the snapshot timer since we don't want new snapshots while paused
|
|
4339
4396
|
clearInterval(this._fullSnapshotTimer);
|
|
4340
|
-
logger.info('recording paused due to URL blocker');
|
|
4397
|
+
logger$1.info('recording paused due to URL blocker');
|
|
4341
4398
|
this._tryAddCustomEvent('recording paused', {
|
|
4342
4399
|
reason: 'url blocker'
|
|
4343
4400
|
});
|
|
@@ -4356,7 +4413,7 @@ class LazyLoadedSessionRecording {
|
|
|
4356
4413
|
this._tryAddCustomEvent('recording resumed', {
|
|
4357
4414
|
reason: 'left blocked url'
|
|
4358
4415
|
});
|
|
4359
|
-
logger.info('recording resumed');
|
|
4416
|
+
logger$1.info('recording resumed');
|
|
4360
4417
|
}
|
|
4361
4418
|
_activateTrigger(triggerType) {
|
|
4362
4419
|
if (!this.isStarted || !this._recording || !this._isFullyReady) {
|
|
@@ -4389,7 +4446,7 @@ class LazyLoadedSessionRecording {
|
|
|
4389
4446
|
this._isFullyReady = false;
|
|
4390
4447
|
const config = this._remoteConfig;
|
|
4391
4448
|
if (!config) {
|
|
4392
|
-
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');
|
|
4393
4450
|
return;
|
|
4394
4451
|
}
|
|
4395
4452
|
// We want to ensure the sessionManager is reset if necessary on loading the recorder
|
|
@@ -4472,7 +4529,7 @@ class LazyLoadedSessionRecording {
|
|
|
4472
4529
|
});
|
|
4473
4530
|
}
|
|
4474
4531
|
} catch (e) {
|
|
4475
|
-
logger.error('Could not add $pageview to rrweb session', e);
|
|
4532
|
+
logger$1.error('Could not add $pageview to rrweb session', e);
|
|
4476
4533
|
}
|
|
4477
4534
|
});
|
|
4478
4535
|
}
|
|
@@ -4517,17 +4574,30 @@ class LazyLoadedSessionRecording {
|
|
|
4517
4574
|
this._stopRrweb = undefined;
|
|
4518
4575
|
this._isFullyReady = false;
|
|
4519
4576
|
this._hasReportedRecordingInitialized = false;
|
|
4520
|
-
logger.info('stopped');
|
|
4577
|
+
logger$1.info('stopped');
|
|
4521
4578
|
}
|
|
4522
4579
|
_snapshotIngestionUrl() {
|
|
4523
4580
|
const endpointFor = this._instance?.requestRouter?.endpointFor;
|
|
4524
|
-
|
|
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) {
|
|
4525
4592
|
return null;
|
|
4526
4593
|
}
|
|
4527
4594
|
try {
|
|
4528
|
-
|
|
4595
|
+
// eslint-disable-next-line compat/compat
|
|
4596
|
+
return new URL(this._endpoint, host).href;
|
|
4529
4597
|
} catch {
|
|
4530
|
-
|
|
4598
|
+
const normalizedHost = host.endsWith('/') ? host.slice(0, -1) : host;
|
|
4599
|
+
const normalizedEndpoint = this._endpoint.startsWith('/') ? this._endpoint : `/${this._endpoint}`;
|
|
4600
|
+
return `${normalizedHost}${normalizedEndpoint}`;
|
|
4531
4601
|
}
|
|
4532
4602
|
}
|
|
4533
4603
|
_canCaptureSnapshots() {
|
|
@@ -4612,7 +4682,7 @@ class LazyLoadedSessionRecording {
|
|
|
4612
4682
|
}
|
|
4613
4683
|
this._captureSnapshotBuffered(properties);
|
|
4614
4684
|
} catch (e) {
|
|
4615
|
-
logger.error('error processing rrweb event', e);
|
|
4685
|
+
logger$1.error('error processing rrweb event', e);
|
|
4616
4686
|
}
|
|
4617
4687
|
}
|
|
4618
4688
|
get status() {
|
|
@@ -4703,7 +4773,7 @@ class LazyLoadedSessionRecording {
|
|
|
4703
4773
|
if (!this._canCaptureSnapshots()) {
|
|
4704
4774
|
if (!this._loggedMissingEndpointFor) {
|
|
4705
4775
|
this._loggedMissingEndpointFor = true;
|
|
4706
|
-
logger.warn('snapshot capture skipped because requestRouter.endpointFor is unavailable');
|
|
4776
|
+
logger$1.warn('snapshot capture skipped because requestRouter.endpointFor is unavailable');
|
|
4707
4777
|
}
|
|
4708
4778
|
this._flushBufferTimer = setTimeout(() => {
|
|
4709
4779
|
this._flushBuffer();
|
|
@@ -4746,7 +4816,7 @@ class LazyLoadedSessionRecording {
|
|
|
4746
4816
|
if (!url) {
|
|
4747
4817
|
if (!this._loggedMissingEndpointFor) {
|
|
4748
4818
|
this._loggedMissingEndpointFor = true;
|
|
4749
|
-
logger.warn('snapshot capture skipped because requestRouter.endpointFor is unavailable');
|
|
4819
|
+
logger$1.warn('snapshot capture skipped because requestRouter.endpointFor is unavailable');
|
|
4750
4820
|
}
|
|
4751
4821
|
return;
|
|
4752
4822
|
}
|
|
@@ -4759,7 +4829,7 @@ class LazyLoadedSessionRecording {
|
|
|
4759
4829
|
skip_client_rate_limiting: true
|
|
4760
4830
|
});
|
|
4761
4831
|
} catch (e) {
|
|
4762
|
-
logger.error('failed to capture snapshot', e);
|
|
4832
|
+
logger$1.error('failed to capture snapshot', e);
|
|
4763
4833
|
}
|
|
4764
4834
|
}
|
|
4765
4835
|
_snapshotUrl() {
|
|
@@ -4818,9 +4888,9 @@ class LazyLoadedSessionRecording {
|
|
|
4818
4888
|
});
|
|
4819
4889
|
const message = startReason.replace('_', ' ');
|
|
4820
4890
|
if (typeof tagPayload === 'undefined') {
|
|
4821
|
-
logger.info(message);
|
|
4891
|
+
logger$1.info(message);
|
|
4822
4892
|
} else {
|
|
4823
|
-
logger.info(message, tagPayload);
|
|
4893
|
+
logger$1.info(message, tagPayload);
|
|
4824
4894
|
}
|
|
4825
4895
|
if (!core.includes(['recording_initialized', 'session_id_changed'], startReason)) {
|
|
4826
4896
|
this._tryAddCustomEvent(startReason, tagPayload);
|
|
@@ -4921,7 +4991,7 @@ class LazyLoadedSessionRecording {
|
|
|
4921
4991
|
if (shouldSample) {
|
|
4922
4992
|
this._reportStarted(SAMPLED);
|
|
4923
4993
|
} else {
|
|
4924
|
-
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.`);
|
|
4925
4995
|
}
|
|
4926
4996
|
this._tryAddCustomEvent('samplingDecisionMade', {
|
|
4927
4997
|
sampleRate: currentSampleRate,
|
|
@@ -4944,7 +5014,7 @@ class LazyLoadedSessionRecording {
|
|
|
4944
5014
|
this._activateTrigger('event');
|
|
4945
5015
|
}
|
|
4946
5016
|
} catch (e) {
|
|
4947
|
-
logger.error('Could not activate event trigger', e);
|
|
5017
|
+
logger$1.error('Could not activate event trigger', e);
|
|
4948
5018
|
}
|
|
4949
5019
|
});
|
|
4950
5020
|
}
|
|
@@ -5026,7 +5096,7 @@ class LazyLoadedSessionRecording {
|
|
|
5026
5096
|
this._disablePermanently('rrweb record function unavailable');
|
|
5027
5097
|
return;
|
|
5028
5098
|
}
|
|
5029
|
-
const activePlugins = this._gatherRRWebPlugins();
|
|
5099
|
+
const activePlugins = await this._gatherRRWebPlugins();
|
|
5030
5100
|
let stopHandler;
|
|
5031
5101
|
try {
|
|
5032
5102
|
stopHandler = rrwebRecord({
|
|
@@ -5035,7 +5105,7 @@ class LazyLoadedSessionRecording {
|
|
|
5035
5105
|
this.onRRwebEmit(event);
|
|
5036
5106
|
} catch (e) {
|
|
5037
5107
|
// never throw from rrweb emit handler
|
|
5038
|
-
logger.error('error in rrweb emit handler', e);
|
|
5108
|
+
logger$1.error('error in rrweb emit handler', e);
|
|
5039
5109
|
}
|
|
5040
5110
|
},
|
|
5041
5111
|
plugins: activePlugins,
|
|
@@ -5060,7 +5130,7 @@ class LazyLoadedSessionRecording {
|
|
|
5060
5130
|
bucketSize: this._instance.config.session_recording?.__mutationThrottlerBucketSize,
|
|
5061
5131
|
onBlockedNode: (id, node) => {
|
|
5062
5132
|
const message = `Too many mutations on node '${id}'. Rate limiting. This could be due to SVG animations or something similar`;
|
|
5063
|
-
logger.info(message, {
|
|
5133
|
+
logger$1.info(message, {
|
|
5064
5134
|
node: node
|
|
5065
5135
|
});
|
|
5066
5136
|
this.log(LOGGER_PREFIX$1 + ' ' + message, 'warn');
|
|
@@ -5087,11 +5157,16 @@ class LazyLoadedSessionRecording {
|
|
|
5087
5157
|
/* eslint-disable posthog-js/no-direct-function-check */
|
|
5088
5158
|
const LOGGER_PREFIX = '[SessionRecording]';
|
|
5089
5159
|
const log = {
|
|
5090
|
-
info: (...args) => logger$
|
|
5091
|
-
warn: (...args) => logger$
|
|
5092
|
-
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)
|
|
5093
5163
|
};
|
|
5094
5164
|
class SessionRecording {
|
|
5165
|
+
_debug(...args) {
|
|
5166
|
+
if (this._instance?.config?.debug) {
|
|
5167
|
+
log.info(...args);
|
|
5168
|
+
}
|
|
5169
|
+
}
|
|
5095
5170
|
get started() {
|
|
5096
5171
|
return !!this._lazyLoadedSessionRecording?.isStarted;
|
|
5097
5172
|
}
|
|
@@ -5129,8 +5204,10 @@ class SessionRecording {
|
|
|
5129
5204
|
}
|
|
5130
5205
|
const canRunReplay = !core.isUndefined(Object.assign) && !core.isUndefined(Array.from);
|
|
5131
5206
|
if (this._isRecordingEnabled && canRunReplay) {
|
|
5207
|
+
this._debug('Session replay enabled; starting recorder');
|
|
5132
5208
|
this._lazyLoadAndStart(startReason);
|
|
5133
5209
|
} else {
|
|
5210
|
+
this._debug('Session replay disabled; stopping recorder');
|
|
5134
5211
|
this.stopRecording();
|
|
5135
5212
|
}
|
|
5136
5213
|
}
|
|
@@ -5202,18 +5279,15 @@ class SessionRecording {
|
|
|
5202
5279
|
log.info('skipping remote config with no sessionRecording', response);
|
|
5203
5280
|
return;
|
|
5204
5281
|
}
|
|
5205
|
-
this._receivedFlags = true;
|
|
5206
|
-
if (response.sessionRecording === false) {
|
|
5207
|
-
return;
|
|
5208
|
-
}
|
|
5209
5282
|
this._persistRemoteConfig(response);
|
|
5283
|
+
this._receivedFlags = true;
|
|
5210
5284
|
this.startIfEnabledOrStop();
|
|
5211
5285
|
}
|
|
5212
5286
|
log(message, level = 'log') {
|
|
5213
5287
|
if (this._lazyLoadedSessionRecording?.log) {
|
|
5214
5288
|
this._lazyLoadedSessionRecording.log(message, level);
|
|
5215
5289
|
} else {
|
|
5216
|
-
logger$
|
|
5290
|
+
logger$3.warn('log called before recorder was ready');
|
|
5217
5291
|
}
|
|
5218
5292
|
}
|
|
5219
5293
|
get _scriptName() {
|
|
@@ -5236,10 +5310,10 @@ class SessionRecording {
|
|
|
5236
5310
|
try {
|
|
5237
5311
|
const maybePromise = this._lazyLoadedSessionRecording.start(startReason);
|
|
5238
5312
|
if (maybePromise && typeof maybePromise.catch === 'function') {
|
|
5239
|
-
maybePromise.catch(e => logger$
|
|
5313
|
+
maybePromise.catch(e => logger$3.error('error starting session recording', e));
|
|
5240
5314
|
}
|
|
5241
5315
|
} catch (e) {
|
|
5242
|
-
logger$
|
|
5316
|
+
logger$3.error('error starting session recording', e);
|
|
5243
5317
|
}
|
|
5244
5318
|
return;
|
|
5245
5319
|
}
|
|
@@ -5252,10 +5326,10 @@ class SessionRecording {
|
|
|
5252
5326
|
try {
|
|
5253
5327
|
const maybePromise = this._lazyLoadedSessionRecording.start(startReason);
|
|
5254
5328
|
if (maybePromise && typeof maybePromise.catch === 'function') {
|
|
5255
|
-
maybePromise.catch(e => logger$
|
|
5329
|
+
maybePromise.catch(e => logger$3.error('error starting session recording', e));
|
|
5256
5330
|
}
|
|
5257
5331
|
} catch (e) {
|
|
5258
|
-
logger$
|
|
5332
|
+
logger$3.error('error starting session recording', e);
|
|
5259
5333
|
}
|
|
5260
5334
|
}
|
|
5261
5335
|
/**
|
|
@@ -5412,12 +5486,12 @@ class Leanbase extends core.PostHogCore {
|
|
|
5412
5486
|
}, 1);
|
|
5413
5487
|
}
|
|
5414
5488
|
const triggerRemoteConfigLoad = reason => {
|
|
5415
|
-
logger$
|
|
5489
|
+
logger$3.info(`remote config load triggered via ${reason}`);
|
|
5416
5490
|
void this.loadRemoteConfig();
|
|
5417
5491
|
};
|
|
5418
5492
|
if (document) {
|
|
5419
5493
|
if (document.readyState === 'loading') {
|
|
5420
|
-
logger$
|
|
5494
|
+
logger$3.info('remote config load deferred until DOMContentLoaded');
|
|
5421
5495
|
const onDomReady = () => {
|
|
5422
5496
|
document?.removeEventListener('DOMContentLoaded', onDomReady);
|
|
5423
5497
|
triggerRemoteConfigLoad('dom');
|
|
@@ -5510,7 +5584,7 @@ class Leanbase extends core.PostHogCore {
|
|
|
5510
5584
|
try {
|
|
5511
5585
|
this.sessionRecording.startIfEnabledOrStop();
|
|
5512
5586
|
} catch (e) {
|
|
5513
|
-
logger$
|
|
5587
|
+
logger$3.error('Failed to start session recording', e);
|
|
5514
5588
|
}
|
|
5515
5589
|
}
|
|
5516
5590
|
fetch(url, options) {
|
|
@@ -5576,7 +5650,7 @@ class Leanbase extends core.PostHogCore {
|
|
|
5576
5650
|
};
|
|
5577
5651
|
properties['distinct_id'] = persistenceProps.distinct_id;
|
|
5578
5652
|
if (!(core.isString(properties['distinct_id']) || core.isNumber(properties['distinct_id'])) || core.isEmptyString(properties['distinct_id'])) {
|
|
5579
|
-
logger$
|
|
5653
|
+
logger$3.error('Invalid distinct_id for replay event. This indicates a bug in your implementation');
|
|
5580
5654
|
}
|
|
5581
5655
|
return properties;
|
|
5582
5656
|
}
|
|
@@ -5651,11 +5725,11 @@ class Leanbase extends core.PostHogCore {
|
|
|
5651
5725
|
return;
|
|
5652
5726
|
}
|
|
5653
5727
|
if (core.isUndefined(event) || !core.isString(event)) {
|
|
5654
|
-
logger$
|
|
5728
|
+
logger$3.error('No event name provided to posthog.capture');
|
|
5655
5729
|
return;
|
|
5656
5730
|
}
|
|
5657
5731
|
if (properties?.$current_url && !core.isString(properties?.$current_url)) {
|
|
5658
|
-
logger$
|
|
5732
|
+
logger$3.error('Invalid `$current_url` property provided to `posthog.capture`. Input must be a string. Ignoring provided value.');
|
|
5659
5733
|
delete properties?.$current_url;
|
|
5660
5734
|
}
|
|
5661
5735
|
this.sessionPersistence.update_search_keyword();
|
|
@@ -5707,5 +5781,634 @@ class Leanbase extends core.PostHogCore {
|
|
|
5707
5781
|
}
|
|
5708
5782
|
}
|
|
5709
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
|
+
|
|
5710
6413
|
exports.Leanbase = Leanbase;
|
|
5711
6414
|
//# sourceMappingURL=index.cjs.map
|