"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createSessionReplayJoinedConfigGenerator = exports.MAX_MIN_SESSION_DURATION_MS = exports.SessionReplayJoinedConfigGenerator = exports.removeInvalidSelectorsFromPrivacyConfig = void 0;
var tslib_1 = require("tslib");
var analytics_core_1 = require("@amplitude/analytics-core");
var helpers_1 = require("../helpers");
var local_config_1 = require("./local-config");
// Budget for waiting on the remote config response before falling back to the local cache.
// The inner fetch in analytics-core uses a 1000ms per-attempt AbortController timeout with
// up to 3 retries — but this outer timeout is the only thing the SR plugin waits on, so
// only the first attempt can possibly complete in time (attempt 2 starts at ~1000-1333ms
// after backoff jitter and runs to ~2000-2333ms, well past any reasonable outer budget).
// 1500ms is set above the 1000ms inner abort to avoid a tie with the inner cutoff and
// give the first attempt's resolution path room to finish; everything else falls through
// to the cache fallback. See SR-4234: prefer remote over a stale cache that would
// otherwise win the synchronous race in 'all' mode.
var REMOTE_CONFIG_TIMEOUT_MS = 1500;
var removeInvalidSelectorsFromPrivacyConfig = function (privacyConfig, loggerProvider) {
    // This allows us to not search the DOM.
    var fragment = document.createDocumentFragment();
    var dropInvalidSelectors = function (selectors) {
        if (selectors === void 0) { selectors = []; }
        if (typeof selectors === 'string') {
            selectors = [selectors];
        }
        selectors = selectors.filter(function (selector) {
            try {
                fragment.querySelector(selector);
            }
            catch (_a) {
                loggerProvider.warn("[session-replay-browser] omitting selector \"".concat(selector, "\" because it is invalid"));
                return false;
            }
            return true;
        });
        if (selectors.length === 0) {
            return undefined;
        }
        return selectors;
    };
    privacyConfig.blockSelector = dropInvalidSelectors(privacyConfig.blockSelector);
    privacyConfig.maskSelector = dropInvalidSelectors(privacyConfig.maskSelector);
    privacyConfig.unmaskSelector = dropInvalidSelectors(privacyConfig.unmaskSelector);
    return privacyConfig;
};
exports.removeInvalidSelectorsFromPrivacyConfig = removeInvalidSelectorsFromPrivacyConfig;
var SessionReplayJoinedConfigGenerator = /** @class */ (function () {
    function SessionReplayJoinedConfigGenerator(remoteConfigClient, localConfig) {
        this.localConfig = localConfig;
        this.remoteConfigClient = remoteConfigClient;
    }
    SessionReplayJoinedConfigGenerator.prototype.generateJoinedConfig = function () {
        var _a, _b, _c, _d, _e, _f, _g;
        return tslib_1.__awaiter(this, void 0, void 0, function () {
            var config, sessionReplayRemoteConfig, error_1, samplingConfig, remotePrivacyConfig, targetingConfig, localPrivacyConfig, joinedPrivacyConfig, privacyConfigSelectorMap, selectorMap, _h, _j, _k, selector, selectorType;
            var e_1, _l;
            var _this = this;
            return tslib_1.__generator(this, function (_m) {
                switch (_m.label) {
                    case 0:
                        config = tslib_1.__assign({}, this.localConfig);
                        // Special case here as optOut is implemented via getter/setter
                        config.optOut = this.localConfig.optOut;
                        // We always want captureEnabled to be true, unless there's an override
                        // in the remote config.
                        config.captureEnabled = true;
                        _m.label = 1;
                    case 1:
                        _m.trys.push([1, 3, , 4]);
                        // Subscribe with a timeout so the SDK prefers the remote response and only falls back
                        // to cache after the budget elapses. 'all' mode would race a synchronous cache read
                        // against the network and resolve on whichever fires first — cache always wins, so a
                        // stale cache silently overrides the live config (SR-4234).
                        return [4 /*yield*/, new Promise(function (resolve, reject) {
                                _this.remoteConfigClient.subscribe('configs.sessionReplay', { timeout: REMOTE_CONFIG_TIMEOUT_MS }, function (remoteConfig, source) {
                                    var _a;
                                    _this.localConfig.loggerProvider.debug("Session Replay remote configuration received from ".concat(source, ":"), JSON.stringify(remoteConfig, null, 2));
                                    if (!remoteConfig) {
                                        reject(new Error('No remote config received'));
                                        return;
                                    }
                                    // remoteConfig is already filtered to 'configs.sessionReplay' namespace
                                    var namespaceConfig = remoteConfig;
                                    var samplingConfig = namespaceConfig.sr_sampling_config;
                                    var privacyConfig = namespaceConfig.sr_privacy_config;
                                    var targetingConfig = namespaceConfig.sr_targeting_config;
                                    var ugcFilterRules = (_a = config.interactionConfig) === null || _a === void 0 ? void 0 : _a.ugcFilterRules;
                                    // This is intentionally forced to only be set through the remote config.
                                    config.interactionConfig = namespaceConfig.sr_interaction_config;
                                    if (config.interactionConfig && ugcFilterRules) {
                                        config.interactionConfig.ugcFilterRules = ugcFilterRules;
                                    }
                                    // This is intentionally forced to only be set through the remote config.
                                    config.loggingConfig = namespaceConfig.sr_logging_config;
                                    if (samplingConfig || privacyConfig || targetingConfig) {
                                        sessionReplayRemoteConfig = {};
                                        if (samplingConfig) {
                                            sessionReplayRemoteConfig.sr_sampling_config = samplingConfig;
                                        }
                                        if (privacyConfig) {
                                            sessionReplayRemoteConfig.sr_privacy_config = privacyConfig;
                                        }
                                        if (targetingConfig) {
                                            sessionReplayRemoteConfig.sr_targeting_config = targetingConfig;
                                        }
                                    }
                                    resolve();
                                });
                            })];
                    case 2:
                        // Subscribe with a timeout so the SDK prefers the remote response and only falls back
                        // to cache after the budget elapses. 'all' mode would race a synchronous cache read
                        // against the network and resolve on whichever fires first — cache always wins, so a
                        // stale cache silently overrides the live config (SR-4234).
                        _m.sent();
                        return [3 /*break*/, 4];
                    case 3:
                        error_1 = _m.sent();
                        this.localConfig.loggerProvider.error('Failed to generate joined config: ', error_1);
                        config.captureEnabled = false;
                        return [2 /*return*/, {
                                localConfig: this.localConfig,
                                joinedConfig: config,
                                remoteConfig: undefined,
                            }];
                    case 4:
                        if (!sessionReplayRemoteConfig) {
                            return [2 /*return*/, {
                                    localConfig: this.localConfig,
                                    joinedConfig: config,
                                    remoteConfig: sessionReplayRemoteConfig,
                                }];
                        }
                        samplingConfig = sessionReplayRemoteConfig.sr_sampling_config, remotePrivacyConfig = sessionReplayRemoteConfig.sr_privacy_config, targetingConfig = sessionReplayRemoteConfig.sr_targeting_config;
                        if (samplingConfig && Object.keys(samplingConfig).length > 0) {
                            if (Object.prototype.hasOwnProperty.call(samplingConfig, 'capture_enabled')) {
                                config.captureEnabled = samplingConfig.capture_enabled;
                            }
                            else {
                                config.captureEnabled = false;
                            }
                            if (Object.prototype.hasOwnProperty.call(samplingConfig, 'sample_rate')) {
                                config.sampleRate = samplingConfig.sample_rate;
                            }
                            if (Object.prototype.hasOwnProperty.call(samplingConfig, 'min_session_duration_ms')) {
                                config.minSessionDurationMs = this.sanitizeMinSessionDurationMs(samplingConfig.min_session_duration_ms);
                            }
                        }
                        else {
                            // If config API response was valid (ie 200), but no config returned, assume that
                            // customer has not yet set up config, and use sample rate from SDK options,
                            // allowing for immediate replay capture
                            config.captureEnabled = true;
                            this.localConfig.loggerProvider.debug('Remote config successfully fetched, but no values set for project, Session Replay capture enabled.');
                        }
                        // Remote config join acts somewhat like a left join between the remote and the local
                        // config. That is, remote config has precedence over local values as with sampling.
                        // However, non conflicting values will be added to the lists.
                        // Here's an example to illustrate:
                        //
                        // Remote config:   {'.selector1': 'MASK',   '.selector2': 'UNMASK'}
                        // Local config:    {'.selector1': 'UNMASK',                         '.selector3': 'MASK'}
                        //
                        // Resolved config: {'.selector1': 'MASK',   '.selector2': 'UNMASK', '.selector3': 'MASK'}
                        // config.privacyConfig = {
                        //   ...(config.privacyConfig ?? {}),
                        //   ...remotePrivacyConfig,
                        // };
                        if (remotePrivacyConfig) {
                            localPrivacyConfig = (_a = config.privacyConfig) !== null && _a !== void 0 ? _a : {};
                            joinedPrivacyConfig = {
                                defaultMaskLevel: (_c = (_b = remotePrivacyConfig.defaultMaskLevel) !== null && _b !== void 0 ? _b : localPrivacyConfig.defaultMaskLevel) !== null && _c !== void 0 ? _c : 'medium',
                                blockSelector: [],
                                maskSelector: [],
                                unmaskSelector: [],
                                maskAttributes: tslib_1.__spreadArray([], tslib_1.__read(new Set(tslib_1.__spreadArray(tslib_1.__spreadArray([], tslib_1.__read(((_d = localPrivacyConfig.maskAttributes) !== null && _d !== void 0 ? _d : [])), false), tslib_1.__read(((_e = remotePrivacyConfig.maskAttributes) !== null && _e !== void 0 ? _e : [])), false))), false),
                                urlMaskLevels: tslib_1.__spreadArray(tslib_1.__spreadArray([], tslib_1.__read(((_f = remotePrivacyConfig.urlMaskLevels) !== null && _f !== void 0 ? _f : [])), false), tslib_1.__read(((_g = localPrivacyConfig.urlMaskLevels) !== null && _g !== void 0 ? _g : [])), false),
                            };
                            privacyConfigSelectorMap = function (privacyConfig) {
                                var e_2, _a, e_3, _b, e_4, _c;
                                var _d, _e, _f;
                                var selectorMap = {};
                                if (typeof privacyConfig.blockSelector === 'string') {
                                    privacyConfig.blockSelector = [privacyConfig.blockSelector];
                                }
                                try {
                                    for (var _g = tslib_1.__values((_d = privacyConfig.blockSelector) !== null && _d !== void 0 ? _d : []), _h = _g.next(); !_h.done; _h = _g.next()) {
                                        var selector = _h.value;
                                        selectorMap[selector] = 'block';
                                    }
                                }
                                catch (e_2_1) { e_2 = { error: e_2_1 }; }
                                finally {
                                    try {
                                        if (_h && !_h.done && (_a = _g.return)) _a.call(_g);
                                    }
                                    finally { if (e_2) throw e_2.error; }
                                }
                                try {
                                    for (var _j = tslib_1.__values((_e = privacyConfig.maskSelector) !== null && _e !== void 0 ? _e : []), _k = _j.next(); !_k.done; _k = _j.next()) {
                                        var selector = _k.value;
                                        selectorMap[selector] = 'mask';
                                    }
                                }
                                catch (e_3_1) { e_3 = { error: e_3_1 }; }
                                finally {
                                    try {
                                        if (_k && !_k.done && (_b = _j.return)) _b.call(_j);
                                    }
                                    finally { if (e_3) throw e_3.error; }
                                }
                                try {
                                    for (var _l = tslib_1.__values((_f = privacyConfig.unmaskSelector) !== null && _f !== void 0 ? _f : []), _m = _l.next(); !_m.done; _m = _l.next()) {
                                        var selector = _m.value;
                                        selectorMap[selector] = 'unmask';
                                    }
                                }
                                catch (e_4_1) { e_4 = { error: e_4_1 }; }
                                finally {
                                    try {
                                        if (_m && !_m.done && (_c = _l.return)) _c.call(_l);
                                    }
                                    finally { if (e_4) throw e_4.error; }
                                }
                                return selectorMap;
                            };
                            selectorMap = tslib_1.__assign(tslib_1.__assign({}, privacyConfigSelectorMap(localPrivacyConfig)), privacyConfigSelectorMap(remotePrivacyConfig));
                            try {
                                for (_h = tslib_1.__values(Object.entries(selectorMap)), _j = _h.next(); !_j.done; _j = _h.next()) {
                                    _k = tslib_1.__read(_j.value, 2), selector = _k[0], selectorType = _k[1];
                                    if (selectorType === 'mask') {
                                        joinedPrivacyConfig.maskSelector.push(selector);
                                    }
                                    else if (selectorType === 'block') {
                                        joinedPrivacyConfig.blockSelector.push(selector);
                                    }
                                    else if (selectorType === 'unmask') {
                                        joinedPrivacyConfig.unmaskSelector.push(selector);
                                    }
                                }
                            }
                            catch (e_1_1) { e_1 = { error: e_1_1 }; }
                            finally {
                                try {
                                    if (_j && !_j.done && (_l = _h.return)) _l.call(_h);
                                }
                                finally { if (e_1) throw e_1.error; }
                            }
                            config.privacyConfig = (0, exports.removeInvalidSelectorsFromPrivacyConfig)(joinedPrivacyConfig, this.localConfig.loggerProvider);
                        }
                        if (targetingConfig && Object.keys(targetingConfig).length > 0) {
                            config.targetingConfig = targetingConfig;
                        }
                        this.localConfig.loggerProvider.debug(JSON.stringify({ name: 'session replay joined config', config: (0, helpers_1.getDebugConfig)(config) }, null, 2));
                        return [2 /*return*/, {
                                localConfig: this.localConfig,
                                joinedConfig: config,
                                remoteConfig: sessionReplayRemoteConfig,
                            }];
                }
            });
        });
    };
    /**
     * Defensive bounds for the remote-supplied min_session_duration_ms. A misconfigured
     * value (e.g. 30_000_000) would silently suppress every replay until the config is
     * pushed again, so we clamp to a sane ceiling and warn on out-of-range inputs.
     * Returns undefined for clearly invalid values so the gate falls back to disabled
     * rather than carrying a NaN through downstream comparisons.
     */
    SessionReplayJoinedConfigGenerator.prototype.sanitizeMinSessionDurationMs = function (raw) {
        if (typeof raw !== 'number' || !Number.isFinite(raw)) {
            this.localConfig.loggerProvider.warn("min_session_duration_ms remote value is not a finite number (got ".concat(String(raw), "); ignoring."));
            return undefined;
        }
        if (raw < 0) {
            this.localConfig.loggerProvider.warn("min_session_duration_ms remote value is negative (".concat(raw, "); ignoring."));
            return undefined;
        }
        if (raw > exports.MAX_MIN_SESSION_DURATION_MS) {
            this.localConfig.loggerProvider.warn("min_session_duration_ms remote value ".concat(raw, " exceeds ").concat(exports.MAX_MIN_SESSION_DURATION_MS, "ms ceiling; clamping."));
            return exports.MAX_MIN_SESSION_DURATION_MS;
        }
        return raw;
    };
    return SessionReplayJoinedConfigGenerator;
}());
exports.SessionReplayJoinedConfigGenerator = SessionReplayJoinedConfigGenerator;
/**
 * Upper bound for the remote-configured replay min duration. 60 seconds is well above
 * any reasonable bounce threshold; values higher than this are almost certainly typos
 * (e.g. seconds confused for milliseconds, or an extra zero) and would otherwise
 * suppress every replay until the config is corrected.
 */
exports.MAX_MIN_SESSION_DURATION_MS = 60000;
var createSessionReplayJoinedConfigGenerator = function (apiKey, options) { return tslib_1.__awaiter(void 0, void 0, void 0, function () {
    var localConfig, remoteConfigClient;
    return tslib_1.__generator(this, function (_a) {
        localConfig = new local_config_1.SessionReplayLocalConfig(apiKey, options);
        remoteConfigClient = new analytics_core_1.RemoteConfigClient(apiKey, localConfig.loggerProvider, localConfig.serverZone, options.configServerUrl, 
        // Custom transport for the config GET. SR's FetchConfigRequest is structurally compatible
        // with RemoteConfigFetchRequest, so the customer's handleFetchConfig drops straight in.
        options.handleFetchConfig);
        return [2 /*return*/, new SessionReplayJoinedConfigGenerator(remoteConfigClient, localConfig)];
    });
}); };
exports.createSessionReplayJoinedConfigGenerator = createSessionReplayJoinedConfigGenerator;
//# sourceMappingURL=joined-config.js.map