"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SessionReplay = void 0;
var tslib_1 = require("tslib");
var analytics_client_common_1 = require("@amplitude/analytics-client-common");
var analytics_core_1 = require("@amplitude/analytics-core");
var rrweb_1 = require("@amplitude/rrweb");
var joined_config_1 = require("./config/joined-config");
var constants_1 = require("./constants");
var events_manager_1 = require("./events/events-manager");
var multi_manager_1 = require("./events/multi-manager");
var helpers_1 = require("./helpers");
var click_1 = require("./hooks/click");
var scroll_1 = require("./hooks/scroll");
var identifiers_1 = require("./identifiers");
var version_1 = require("./version");
var event_compressor_1 = require("./events/event-compressor");
var rrweb_plugin_console_record_1 = require("@amplitude/rrweb-plugin-console-record");
var logger_1 = require("./logger");
var observers_1 = require("./observers");
var SessionReplay = /** @class */ (function () {
    function SessionReplay() {
        var _this = this;
        this.name = '@amplitude/session-replay-browser';
        this.recordCancelCallback = null;
        this.eventCount = 0;
        // Visible for testing
        this.pageLeaveFns = [];
        this.teardownEventListeners = function (teardown) {
            var globalScope = (0, analytics_client_common_1.getGlobalScope)();
            if (globalScope) {
                globalScope.removeEventListener('blur', _this.blurListener);
                globalScope.removeEventListener('focus', _this.focusListener);
                !teardown && globalScope.addEventListener('blur', _this.blurListener);
                !teardown && globalScope.addEventListener('focus', _this.focusListener);
                // prefer pagehide to unload events, this is the standard going forward. it is not
                // 100% reliable, but is bfcache-compatible.
                if (globalScope.self && 'onpagehide' in globalScope.self) {
                    globalScope.removeEventListener('pagehide', _this.pageLeaveListener);
                    !teardown && globalScope.addEventListener('pagehide', _this.pageLeaveListener);
                }
                else {
                    // this has performance implications, but is the only way we can reliably send events
                    // in browser that don't support pagehide.
                    globalScope.removeEventListener('beforeunload', _this.pageLeaveListener);
                    !teardown && globalScope.addEventListener('beforeunload', _this.pageLeaveListener);
                }
            }
        };
        this.blurListener = function () {
            _this.sendEvents();
        };
        this.focusListener = function () {
            // Restart recording on focus to ensure that when user
            // switches tabs, we take a full snapshot
            _this.recordEvents();
        };
        /**
         * This is an instance member so that if init is called multiple times
         * it doesn't add another listener to the page leave event. This is to
         * prevent duplicate listener actions from firing.
         */
        this.pageLeaveListener = function (e) {
            _this.pageLeaveFns.forEach(function (fn) {
                fn(e);
            });
        };
        this.addCustomRRWebEvent = function (eventName, eventData, addStorageInfo) {
            if (eventData === void 0) { eventData = {}; }
            if (addStorageInfo === void 0) { addStorageInfo = true; }
            return tslib_1.__awaiter(_this, void 0, void 0, function () {
                var debugInfo, config, storageSizeData, e_1;
                return tslib_1.__generator(this, function (_a) {
                    switch (_a.label) {
                        case 0:
                            _a.trys.push([0, 3, , 4]);
                            debugInfo = undefined;
                            config = this.config;
                            if (!config) return [3 /*break*/, 2];
                            debugInfo = {
                                config: (0, helpers_1.getDebugConfig)(config),
                                version: version_1.VERSION,
                            };
                            if (!addStorageInfo) return [3 /*break*/, 2];
                            return [4 /*yield*/, (0, helpers_1.getStorageSize)()];
                        case 1:
                            storageSizeData = _a.sent();
                            debugInfo = tslib_1.__assign(tslib_1.__assign({}, storageSizeData), debugInfo);
                            _a.label = 2;
                        case 2:
                            // Check first to ensure we are recording
                            if (this.recordCancelCallback) {
                                rrweb_1.record.addCustomEvent(eventName, tslib_1.__assign(tslib_1.__assign({}, eventData), debugInfo));
                            }
                            else {
                                this.loggerProvider.debug("Not able to add custom replay capture event ".concat(eventName, " due to no ongoing recording."));
                            }
                            return [3 /*break*/, 4];
                        case 3:
                            e_1 = _a.sent();
                            this.loggerProvider.debug('Error while adding custom replay capture event: ', e_1);
                            return [3 /*break*/, 4];
                        case 4: return [2 /*return*/];
                    }
                });
            });
        };
        this.stopRecordingEvents = function () {
            var _a;
            try {
                _this.loggerProvider.log('Session Replay capture stopping.');
                _this.recordCancelCallback && _this.recordCancelCallback();
                _this.recordCancelCallback = null;
                (_a = _this.networkObservers) === null || _a === void 0 ? void 0 : _a.stop();
            }
            catch (error) {
                var typedError = error;
                _this.loggerProvider.warn("Error occurred while stopping replay capture: ".concat(typedError.toString()));
            }
        };
        this.loggerProvider = new logger_1.SafeLoggerProvider(new analytics_core_1.Logger());
    }
    SessionReplay.prototype.init = function (apiKey, options) {
        return (0, analytics_core_1.returnWrapper)(this._init(apiKey, options));
    };
    SessionReplay.prototype._init = function (apiKey, options) {
        var _a, _b, _c, _d, _e, _f;
        return tslib_1.__awaiter(this, void 0, void 0, function () {
            var _g, _h, scrollWatcher, managers, storeType, rrwebEventManager, error_1, typedError, payloadBatcher, interactionEventManager, error_2, typedError;
            return tslib_1.__generator(this, function (_j) {
                switch (_j.label) {
                    case 0:
                        this.loggerProvider = new logger_1.SafeLoggerProvider(options.loggerProvider || new analytics_core_1.Logger());
                        Object.prototype.hasOwnProperty.call(options, 'logLevel') &&
                            this.loggerProvider.enable(options.logLevel);
                        this.identifiers = new identifiers_1.SessionIdentifiers({ sessionId: options.sessionId, deviceId: options.deviceId });
                        _g = this;
                        return [4 /*yield*/, (0, joined_config_1.createSessionReplayJoinedConfigGenerator)(apiKey, options)];
                    case 1:
                        _g.joinedConfigGenerator = _j.sent();
                        _h = this;
                        return [4 /*yield*/, this.joinedConfigGenerator.generateJoinedConfig(this.identifiers.sessionId)];
                    case 2:
                        _h.config = _j.sent();
                        if (options.sessionId && ((_a = this.config.interactionConfig) === null || _a === void 0 ? void 0 : _a.enabled)) {
                            scrollWatcher = scroll_1.ScrollWatcher.default({
                                sessionId: options.sessionId,
                                type: 'interaction',
                            }, this.config);
                            this.pageLeaveFns = [scrollWatcher.send(this.getDeviceId.bind(this)).bind(scrollWatcher)];
                            this.scrollHook = scrollWatcher.hook.bind(scrollWatcher);
                        }
                        managers = [];
                        storeType = this.config.storeType;
                        if (storeType === 'idb' && !((_b = (0, analytics_client_common_1.getGlobalScope)()) === null || _b === void 0 ? void 0 : _b.indexedDB)) {
                            storeType = 'memory';
                            this.loggerProvider.warn('Could not use preferred indexedDB storage, reverting to in memory option.');
                        }
                        this.loggerProvider.log("Using ".concat(storeType, " for event storage."));
                        _j.label = 3;
                    case 3:
                        _j.trys.push([3, 5, , 6]);
                        return [4 /*yield*/, (0, events_manager_1.createEventsManager)({
                                config: this.config,
                                sessionId: this.identifiers.sessionId,
                                type: 'replay',
                                storeType: storeType,
                            })];
                    case 4:
                        rrwebEventManager = _j.sent();
                        managers.push({ name: 'replay', manager: rrwebEventManager });
                        return [3 /*break*/, 6];
                    case 5:
                        error_1 = _j.sent();
                        typedError = error_1;
                        this.loggerProvider.warn("Error occurred while creating replay events manager: ".concat(typedError.toString()));
                        return [3 /*break*/, 6];
                    case 6:
                        if (!((_c = this.config.interactionConfig) === null || _c === void 0 ? void 0 : _c.enabled)) return [3 /*break*/, 10];
                        payloadBatcher = this.config.interactionConfig.batch ? click_1.clickBatcher : click_1.clickNonBatcher;
                        _j.label = 7;
                    case 7:
                        _j.trys.push([7, 9, , 10]);
                        return [4 /*yield*/, (0, events_manager_1.createEventsManager)({
                                config: this.config,
                                sessionId: this.identifiers.sessionId,
                                type: 'interaction',
                                minInterval: (_d = this.config.interactionConfig.trackEveryNms) !== null && _d !== void 0 ? _d : constants_1.INTERACTION_MIN_INTERVAL,
                                maxInterval: constants_1.INTERACTION_MAX_INTERVAL,
                                payloadBatcher: payloadBatcher,
                                storeType: storeType,
                            })];
                    case 8:
                        interactionEventManager = _j.sent();
                        managers.push({ name: 'interaction', manager: interactionEventManager });
                        return [3 /*break*/, 10];
                    case 9:
                        error_2 = _j.sent();
                        typedError = error_2;
                        this.loggerProvider.warn("Error occurred while creating interaction events manager: ".concat(typedError.toString()));
                        return [3 /*break*/, 10];
                    case 10:
                        this.eventsManager = new (multi_manager_1.MultiEventManager.bind.apply(multi_manager_1.MultiEventManager, tslib_1.__spreadArray([void 0], tslib_1.__read(managers), false)))();
                        // To prevent too many threads.
                        if (this.eventCompressor) {
                            this.eventCompressor.terminate();
                        }
                        this.eventCompressor = new event_compressor_1.EventCompressor(this.eventsManager, this.config, this.getDeviceId());
                        // Initialize network observers if logging is enabled
                        if ((_f = (_e = this.config.loggingConfig) === null || _e === void 0 ? void 0 : _e.network) === null || _f === void 0 ? void 0 : _f.enabled) {
                            this.networkObservers = new observers_1.NetworkObservers();
                        }
                        this.loggerProvider.log('Installing @amplitude/session-replay-browser.');
                        this.teardownEventListeners(false);
                        this.initialize(true);
                        return [2 /*return*/];
                }
            });
        });
    };
    SessionReplay.prototype.setSessionId = function (sessionId, deviceId) {
        return (0, analytics_core_1.returnWrapper)(this.asyncSetSessionId(sessionId, deviceId));
    };
    SessionReplay.prototype.asyncSetSessionId = function (sessionId, deviceId) {
        return tslib_1.__awaiter(this, void 0, void 0, function () {
            var previousSessionId, deviceIdForReplayId, _a;
            return tslib_1.__generator(this, function (_b) {
                switch (_b.label) {
                    case 0:
                        previousSessionId = this.identifiers && this.identifiers.sessionId;
                        if (previousSessionId) {
                            this.sendEvents(previousSessionId);
                        }
                        deviceIdForReplayId = deviceId || this.getDeviceId();
                        this.identifiers = new identifiers_1.SessionIdentifiers({
                            sessionId: sessionId,
                            deviceId: deviceIdForReplayId,
                        });
                        if (!(this.joinedConfigGenerator && previousSessionId)) return [3 /*break*/, 2];
                        _a = this;
                        return [4 /*yield*/, this.joinedConfigGenerator.generateJoinedConfig(this.identifiers.sessionId)];
                    case 1:
                        _a.config = _b.sent();
                        _b.label = 2;
                    case 2:
                        this.recordEvents();
                        return [2 /*return*/];
                }
            });
        });
    };
    SessionReplay.prototype.getSessionReplayProperties = function () {
        var _a;
        var config = this.config;
        var identifiers = this.identifiers;
        if (!config || !identifiers) {
            this.loggerProvider.warn('Session replay init has not been called, cannot get session replay properties.');
            return {};
        }
        var shouldRecord = this.getShouldRecord();
        var eventProperties = {};
        if (shouldRecord) {
            eventProperties = (_a = {},
                _a[constants_1.DEFAULT_SESSION_REPLAY_PROPERTY] = identifiers.sessionReplayId ? identifiers.sessionReplayId : null,
                _a);
            if (config.debugMode) {
                eventProperties[constants_1.SESSION_REPLAY_DEBUG_PROPERTY] = JSON.stringify({
                    appHash: (0, helpers_1.generateHashCode)(config.apiKey).toString(),
                });
            }
        }
        void this.addCustomRRWebEvent(constants_1.CustomRRwebEvent.GET_SR_PROPS, {
            shouldRecord: shouldRecord,
            eventProperties: eventProperties,
        }, this.eventCount === 10);
        if (this.eventCount === 10) {
            this.eventCount = 0;
        }
        this.eventCount++;
        return eventProperties;
    };
    SessionReplay.prototype.sendEvents = function (sessionId) {
        var _a;
        var sessionIdToSend = sessionId || ((_a = this.identifiers) === null || _a === void 0 ? void 0 : _a.sessionId);
        var deviceId = this.getDeviceId();
        this.eventsManager &&
            sessionIdToSend &&
            deviceId &&
            this.eventsManager.sendCurrentSequenceEvents({ sessionId: sessionIdToSend, deviceId: deviceId });
    };
    SessionReplay.prototype.initialize = function (shouldSendStoredEvents) {
        var _a;
        if (shouldSendStoredEvents === void 0) { shouldSendStoredEvents = false; }
        if (!((_a = this.identifiers) === null || _a === void 0 ? void 0 : _a.sessionId)) {
            this.loggerProvider.log("Session is not being recorded due to lack of session id.");
            return;
        }
        var deviceId = this.getDeviceId();
        if (!deviceId) {
            this.loggerProvider.log("Session is not being recorded due to lack of device id.");
            return;
        }
        this.eventsManager && shouldSendStoredEvents && this.eventsManager.sendStoredEvents({ deviceId: deviceId });
        this.recordEvents();
    };
    SessionReplay.prototype.shouldOptOut = function () {
        var _a, _b;
        var identityStoreOptOut;
        if ((_a = this.config) === null || _a === void 0 ? void 0 : _a.instanceName) {
            var identityStore = (0, analytics_client_common_1.getAnalyticsConnector)(this.config.instanceName).identityStore;
            identityStoreOptOut = identityStore.getIdentity().optOut;
        }
        return identityStoreOptOut !== undefined ? identityStoreOptOut : (_b = this.config) === null || _b === void 0 ? void 0 : _b.optOut;
    };
    SessionReplay.prototype.getShouldRecord = function () {
        if (!this.identifiers || !this.config || !this.identifiers.sessionId) {
            this.loggerProvider.warn("Session is not being recorded due to lack of config, please call sessionReplay.init.");
            return false;
        }
        if (!this.config.captureEnabled) {
            this.loggerProvider.log("Session ".concat(this.identifiers.sessionId, " not being captured due to capture being disabled for project or because the remote config could not be fetched."));
            return false;
        }
        if (this.shouldOptOut()) {
            this.loggerProvider.log("Opting session ".concat(this.identifiers.sessionId, " out of recording due to optOut config."));
            return false;
        }
        var isInSample = (0, helpers_1.isSessionInSample)(this.identifiers.sessionId, this.config.sampleRate);
        if (!isInSample) {
            this.loggerProvider.log("Opting session ".concat(this.identifiers.sessionId, " out of recording due to sample rate."));
        }
        return isInSample;
    };
    SessionReplay.prototype.getBlockSelectors = function () {
        var _a, _b, _c;
        // For some reason, this defaults to empty array ([]) if undefined in the compiled script.
        // Empty arrays cause errors when being evaluated in Safari.
        // Force the selector to be undefined if it's an empty array.
        var blockSelector = (_c = (_b = (_a = this.config) === null || _a === void 0 ? void 0 : _a.privacyConfig) === null || _b === void 0 ? void 0 : _b.blockSelector) !== null && _c !== void 0 ? _c : [];
        if (blockSelector.length === 0) {
            return undefined;
        }
        return blockSelector;
    };
    SessionReplay.prototype.getMaskTextSelectors = function () {
        var _a, _b, _c, _d;
        if (((_b = (_a = this.config) === null || _a === void 0 ? void 0 : _a.privacyConfig) === null || _b === void 0 ? void 0 : _b.defaultMaskLevel) === 'conservative') {
            return '*';
        }
        var maskSelector = (_d = (_c = this.config) === null || _c === void 0 ? void 0 : _c.privacyConfig) === null || _d === void 0 ? void 0 : _d.maskSelector;
        if (!maskSelector) {
            return;
        }
        return maskSelector;
    };
    SessionReplay.prototype.getRecordingPlugins = function (loggingConfig) {
        var _a;
        var plugins = [];
        // Default plugin settings -
        // {
        //   level: ['info', 'log', 'warn', 'error'],
        //   lengthThreshold: 10000,
        //   stringifyOptions: {
        //     stringLengthLimit: undefined,
        //     numOfKeysLimit: 50,
        //     depthOfLimit: 4,
        //   },
        //   logger: window.console,
        //   }
        if ((_a = loggingConfig === null || loggingConfig === void 0 ? void 0 : loggingConfig.console) === null || _a === void 0 ? void 0 : _a.enabled) {
            plugins.push((0, rrweb_plugin_console_record_1.getRecordConsolePlugin)({ level: loggingConfig.console.levels }));
        }
        return plugins.length > 0 ? plugins : undefined;
    };
    SessionReplay.prototype.recordEvents = function () {
        var _this = this;
        var _a, _b;
        var config = this.config;
        var shouldRecord = this.getShouldRecord();
        var sessionId = (_a = this.identifiers) === null || _a === void 0 ? void 0 : _a.sessionId;
        if (!shouldRecord || !sessionId || !config) {
            return;
        }
        this.stopRecordingEvents();
        (_b = this.networkObservers) === null || _b === void 0 ? void 0 : _b.start(function (event) {
            void _this.addCustomRRWebEvent(constants_1.CustomRRwebEvent.FETCH_REQUEST, event);
        });
        var privacyConfig = config.privacyConfig, interactionConfig = config.interactionConfig, loggingConfig = config.loggingConfig;
        var hooks = (interactionConfig === null || interactionConfig === void 0 ? void 0 : interactionConfig.enabled)
            ? {
                mouseInteraction: this.eventsManager &&
                    (0, click_1.clickHook)(this.loggerProvider, {
                        eventsManager: this.eventsManager,
                        sessionId: sessionId,
                        deviceIdFn: this.getDeviceId.bind(this),
                    }),
                scroll: this.scrollHook,
            }
            : {};
        this.loggerProvider.log("Session Replay capture beginning for ".concat(sessionId, "."));
        this.recordCancelCallback = (0, rrweb_1.record)({
            emit: function (event) {
                if (_this.shouldOptOut()) {
                    _this.loggerProvider.log("Opting session ".concat(sessionId, " out of recording due to optOut config."));
                    _this.stopRecordingEvents();
                    _this.sendEvents();
                    return;
                }
                if (_this.eventCompressor) {
                    // Schedule processing during idle time if the browser supports requestIdleCallback
                    _this.eventCompressor.enqueueEvent(event, sessionId);
                }
            },
            inlineStylesheet: config.shouldInlineStylesheet,
            hooks: hooks,
            maskAllInputs: true,
            maskTextClass: constants_1.MASK_TEXT_CLASS,
            blockClass: constants_1.BLOCK_CLASS,
            // rrweb only exposes string type through its types, but arrays are also be supported. #class, ['#class', 'id']
            blockSelector: this.getBlockSelectors(),
            maskInputFn: (0, helpers_1.maskFn)('input', privacyConfig),
            maskTextFn: (0, helpers_1.maskFn)('text', privacyConfig),
            // rrweb only exposes string type through its types, but arrays are also be supported. since rrweb uses .matches() which supports arrays.
            maskTextSelector: this.getMaskTextSelectors(),
            recordCanvas: false,
            errorHandler: function (error) {
                var typedError = error;
                // styled-components relies on this error being thrown and bubbled up, rrweb is otherwise suppressing it
                if (typedError.message.includes('insertRule') && typedError.message.includes('CSSStyleSheet')) {
                    throw typedError;
                }
                // rrweb does monkey patching on certain window functions such as CSSStyleSheet.proptype.insertRule,
                // and errors from external clients calling these functions can get suppressed. Styled components depend
                // on these errors being re-thrown.
                if (typedError._external_) {
                    throw typedError;
                }
                _this.loggerProvider.warn('Error while capturing replay: ', typedError.toString());
                // Return true so that we don't clutter user's consoles with internal rrweb errors
                return true;
            },
            plugins: this.getRecordingPlugins(loggingConfig),
        });
        void this.addCustomRRWebEvent(constants_1.CustomRRwebEvent.DEBUG_INFO);
    };
    SessionReplay.prototype.getDeviceId = function () {
        var _a;
        return (_a = this.identifiers) === null || _a === void 0 ? void 0 : _a.deviceId;
    };
    SessionReplay.prototype.getSessionId = function () {
        var _a;
        return (_a = this.identifiers) === null || _a === void 0 ? void 0 : _a.sessionId;
    };
    SessionReplay.prototype.flush = function (useRetry) {
        var _a;
        if (useRetry === void 0) { useRetry = false; }
        return tslib_1.__awaiter(this, void 0, void 0, function () {
            return tslib_1.__generator(this, function (_b) {
                return [2 /*return*/, (_a = this.eventsManager) === null || _a === void 0 ? void 0 : _a.flush(useRetry)];
            });
        });
    };
    SessionReplay.prototype.shutdown = function () {
        this.teardownEventListeners(true);
        this.stopRecordingEvents();
        this.sendEvents();
    };
    return SessionReplay;
}());
exports.SessionReplay = SessionReplay;
//# sourceMappingURL=session-replay.js.map