UNPKG

configcat-common

Version:

ConfigCat is a configuration as a service that lets you manage your features and configurations without actually deploying new code.

705 lines (704 loc) 41.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getTimestampAsDate = exports.handleInvalidReturnValue = exports.isAllowedValue = exports.checkSettingsAvailable = exports.evaluateAll = exports.evaluate = exports.evaluationDetailsFromDefaultValue = exports.RolloutEvaluator = exports.EvaluateContext = void 0; var ConfigCatLogger_1 = require("./ConfigCatLogger"); var ConfigJson_1 = require("./ConfigJson"); var EvaluateLogBuilder_1 = require("./EvaluateLogBuilder"); var Hash_1 = require("./Hash"); var Semver_1 = require("./Semver"); var User_1 = require("./User"); var Utils_1 = require("./Utils"); var EvaluateContext = /** @class */ (function () { function EvaluateContext(key, setting, user, settings) { this.key = key; this.setting = setting; this.user = user; this.settings = settings; } Object.defineProperty(EvaluateContext.prototype, "visitedFlags", { get: function () { var _a; return (_a = this.$visitedFlags) !== null && _a !== void 0 ? _a : (this.$visitedFlags = []); }, enumerable: false, configurable: true }); EvaluateContext.forPrerequisiteFlag = function (key, setting, dependentFlagContext) { var context = new EvaluateContext(key, setting, dependentFlagContext.user, dependentFlagContext.settings); context.$visitedFlags = dependentFlagContext.visitedFlags; // crucial to use the computed property here to make sure the list is created! context.logBuilder = dependentFlagContext.logBuilder; return context; }; return EvaluateContext; }()); exports.EvaluateContext = EvaluateContext; var targetingRuleIgnoredMessage = "The current targeting rule is ignored and the evaluation continues with the next rule."; var missingUserObjectError = "cannot evaluate, User Object is missing"; var missingUserAttributeError = function (attributeName) { return "cannot evaluate, the User." + attributeName + " attribute is missing"; }; var invalidUserAttributeError = function (attributeName, reason) { return "cannot evaluate, the User." + attributeName + " attribute is invalid (" + reason + ")"; }; var RolloutEvaluator = /** @class */ (function () { function RolloutEvaluator(logger) { this.logger = logger; } RolloutEvaluator.prototype.evaluate = function (defaultValue, context) { this.logger.debug("RolloutEvaluator.evaluate() called."); var logBuilder = context.logBuilder; // Building the evaluation log is expensive, so let's not do it if it wouldn't be logged anyway. if (this.logger.isEnabled(ConfigCatLogger_1.LogLevel.Info)) { context.logBuilder = logBuilder = new EvaluateLogBuilder_1.EvaluateLogBuilder(this.logger.eol); logBuilder.append("Evaluating '" + context.key + "'"); if (context.user) { logBuilder.append(" for User '" + JSON.stringify(User_1.getUserAttributes(context.user)) + "'"); } logBuilder.increaseIndent(); } var returnValue; try { var result = void 0, isValidReturnValue = void 0; if (defaultValue != null) { // NOTE: We've already checked earlier in the call chain that the defaultValue is of an allowed type (see also ensureAllowedDefaultValue). var settingType = context.setting.type; // A negative setting type indicates a setting which comes from a flag override (see also Setting.fromValue). if (settingType >= 0 && !isCompatibleValue(defaultValue, settingType)) { throw new TypeError("The type of a setting must match the type of the specified default value. " + ("Setting's type was " + ConfigJson_1.SettingType[settingType] + " but the default value's type was " + typeof defaultValue + ". ") + ("Please use a default value which corresponds to the setting type " + ConfigJson_1.SettingType[settingType] + ". ") + "Learn more: https://configcat.com/docs/sdk-reference/js/#setting-type-mapping"); } result = this.evaluateSetting(context); returnValue = result.selectedValue.value; // When a default value other than null or undefined is specified, the return value must have the same type as the default value // so that the consistency between TS (compile-time) and JS (run-time) return value types is maintained. isValidReturnValue = typeof returnValue === typeof defaultValue; } else { result = this.evaluateSetting(context); returnValue = result.selectedValue.value; // When the specified default value is null or undefined, the return value can be of whatever allowed type (boolean, string, number). isValidReturnValue = isAllowedValue(returnValue); } if (!isValidReturnValue) { handleInvalidReturnValue(returnValue); } return result; } catch (err) { logBuilder === null || logBuilder === void 0 ? void 0 : logBuilder.resetIndent().increaseIndent(); returnValue = defaultValue; throw err; } finally { if (logBuilder) { logBuilder.newLine("Returning '" + returnValue + "'.") .decreaseIndent(); this.logger.settingEvaluated(logBuilder.toString()); } } }; RolloutEvaluator.prototype.evaluateSetting = function (context) { var evaluateResult; var targetingRules = context.setting.targetingRules; if (targetingRules.length > 0 && (evaluateResult = this.evaluateTargetingRules(targetingRules, context))) { return evaluateResult; } var percentageOptions = context.setting.percentageOptions; if (percentageOptions.length > 0 && (evaluateResult = this.evaluatePercentageOptions(percentageOptions, void 0, context))) { return evaluateResult; } return { selectedValue: context.setting }; }; RolloutEvaluator.prototype.evaluateTargetingRules = function (targetingRules, context) { var logBuilder = context.logBuilder; logBuilder === null || logBuilder === void 0 ? void 0 : logBuilder.newLine("Evaluating targeting rules and applying the first match if any:"); for (var i = 0; i < targetingRules.length; i++) { var targetingRule = targetingRules[i]; var conditions = targetingRule.conditions; var isMatchOrError = this.evaluateConditions(conditions, targetingRule, context.key, context); if (isMatchOrError !== true) { if (isEvaluationError(isMatchOrError)) { logBuilder === null || logBuilder === void 0 ? void 0 : logBuilder.increaseIndent().newLine(targetingRuleIgnoredMessage).decreaseIndent(); } continue; } if (!Utils_1.isArray(targetingRule.then)) { return { selectedValue: targetingRule.then, matchedTargetingRule: targetingRule }; } var percentageOptions = targetingRule.then; logBuilder === null || logBuilder === void 0 ? void 0 : logBuilder.increaseIndent(); var evaluateResult = this.evaluatePercentageOptions(percentageOptions, targetingRule, context); if (evaluateResult) { logBuilder === null || logBuilder === void 0 ? void 0 : logBuilder.decreaseIndent(); return evaluateResult; } logBuilder === null || logBuilder === void 0 ? void 0 : logBuilder.newLine(targetingRuleIgnoredMessage).decreaseIndent(); } }; RolloutEvaluator.prototype.evaluatePercentageOptions = function (percentageOptions, matchedTargetingRule, context) { var _a; var logBuilder = context.logBuilder; if (!context.user) { logBuilder === null || logBuilder === void 0 ? void 0 : logBuilder.newLine("Skipping % options because the User Object is missing."); if (!context.isMissingUserObjectLogged) { this.logger.userObjectIsMissing(context.key); context.isMissingUserObjectLogged = true; } return; } var percentageOptionsAttributeName = context.setting.percentageOptionsAttribute; var percentageOptionsAttributeValue; if (percentageOptionsAttributeName == null) { percentageOptionsAttributeName = "Identifier"; percentageOptionsAttributeValue = (_a = context.user.identifier) !== null && _a !== void 0 ? _a : ""; } else { percentageOptionsAttributeValue = User_1.getUserAttribute(context.user, percentageOptionsAttributeName); } if (percentageOptionsAttributeValue == null) { logBuilder === null || logBuilder === void 0 ? void 0 : logBuilder.newLine("Skipping % options because the User." + percentageOptionsAttributeName + " attribute is missing."); if (!context.isMissingUserObjectAttributeLogged) { this.logger.userObjectAttributeIsMissingPercentage(context.key, percentageOptionsAttributeName); context.isMissingUserObjectAttributeLogged = true; } return; } logBuilder === null || logBuilder === void 0 ? void 0 : logBuilder.newLine("Evaluating % options based on the User." + percentageOptionsAttributeName + " attribute:"); var sha1Hash = Hash_1.sha1(context.key + userAttributeValueToString(percentageOptionsAttributeValue)); var hashValue = parseInt(sha1Hash.substring(0, 7), 16) % 100; logBuilder === null || logBuilder === void 0 ? void 0 : logBuilder.newLine("- Computing hash in the [0..99] range from User." + percentageOptionsAttributeName + " => " + hashValue + " (this value is sticky and consistent across all SDKs)"); var bucket = 0; for (var i = 0; i < percentageOptions.length; i++) { var percentageOption = percentageOptions[i]; bucket += percentageOption.percentage; if (hashValue >= bucket) { continue; } logBuilder === null || logBuilder === void 0 ? void 0 : logBuilder.newLine("- Hash value " + hashValue + " selects % option " + (i + 1) + " (" + percentageOption.percentage + "%), '" + EvaluateLogBuilder_1.valueToString(percentageOption.value) + "'."); return { selectedValue: percentageOption, matchedTargetingRule: matchedTargetingRule, matchedPercentageOption: percentageOption }; } throw new Error("Sum of percentage option percentages is less than 100."); }; RolloutEvaluator.prototype.evaluateConditions = function (conditions, targetingRule, contextSalt, context) { // The result of a condition evaluation is either match (true) / no match (false) or an error (string). var result = true; var logBuilder = context.logBuilder; var newLineBeforeThen = false; logBuilder === null || logBuilder === void 0 ? void 0 : logBuilder.newLine("- "); for (var i = 0; i < conditions.length; i++) { var condition = conditions[i]; if (logBuilder) { if (!i) { logBuilder.append("IF ") .increaseIndent(); } else { logBuilder.increaseIndent() .newLine("AND "); } } switch (condition.type) { case "UserCondition": result = this.evaluateUserCondition(condition, contextSalt, context); newLineBeforeThen = conditions.length > 1; break; case "PrerequisiteFlagCondition": result = this.evaluatePrerequisiteFlagCondition(condition, context); newLineBeforeThen = true; break; case "SegmentCondition": result = this.evaluateSegmentCondition(condition, context); newLineBeforeThen = !isEvaluationError(result) || result !== missingUserObjectError || conditions.length > 1; break; default: throw new Error(); // execution should never get here } var success = result === true; if (logBuilder) { if (!targetingRule || conditions.length > 1) { logBuilder.appendConditionConsequence(success); } logBuilder.decreaseIndent(); } if (!success) { break; } } if (targetingRule) { logBuilder === null || logBuilder === void 0 ? void 0 : logBuilder.appendTargetingRuleConsequence(targetingRule, result, newLineBeforeThen); } return result; }; RolloutEvaluator.prototype.evaluateUserCondition = function (condition, contextSalt, context) { var logBuilder = context.logBuilder; logBuilder === null || logBuilder === void 0 ? void 0 : logBuilder.appendUserCondition(condition); if (!context.user) { if (!context.isMissingUserObjectLogged) { this.logger.userObjectIsMissing(context.key); context.isMissingUserObjectLogged = true; } return missingUserObjectError; } var userAttributeName = condition.comparisonAttribute; var userAttributeValue = User_1.getUserAttribute(context.user, userAttributeName); if (userAttributeValue == null || userAttributeValue === "") { // besides null and undefined, empty string is considered missing value as well this.logger.userObjectAttributeIsMissingCondition(EvaluateLogBuilder_1.formatUserCondition(condition), context.key, userAttributeName); return missingUserAttributeError(userAttributeName); } var text, versionOrError, numberOrError, arrayOrError; switch (condition.comparator) { case ConfigJson_1.UserComparator.TextEquals: case ConfigJson_1.UserComparator.TextNotEquals: text = getUserAttributeValueAsText(userAttributeName, userAttributeValue, condition, context.key, this.logger); return this.evaluateTextEquals(text, condition.comparisonValue, condition.comparator === ConfigJson_1.UserComparator.TextNotEquals); case ConfigJson_1.UserComparator.SensitiveTextEquals: case ConfigJson_1.UserComparator.SensitiveTextNotEquals: text = getUserAttributeValueAsText(userAttributeName, userAttributeValue, condition, context.key, this.logger); return this.evaluateSensitiveTextEquals(text, condition.comparisonValue, context.setting.configJsonSalt, contextSalt, condition.comparator === ConfigJson_1.UserComparator.SensitiveTextNotEquals); case ConfigJson_1.UserComparator.TextIsOneOf: case ConfigJson_1.UserComparator.TextIsNotOneOf: text = getUserAttributeValueAsText(userAttributeName, userAttributeValue, condition, context.key, this.logger); return this.evaluateTextIsOneOf(text, condition.comparisonValue, condition.comparator === ConfigJson_1.UserComparator.TextIsNotOneOf); case ConfigJson_1.UserComparator.SensitiveTextIsOneOf: case ConfigJson_1.UserComparator.SensitiveTextIsNotOneOf: text = getUserAttributeValueAsText(userAttributeName, userAttributeValue, condition, context.key, this.logger); return this.evaluateSensitiveTextIsOneOf(text, condition.comparisonValue, context.setting.configJsonSalt, contextSalt, condition.comparator === ConfigJson_1.UserComparator.SensitiveTextIsNotOneOf); case ConfigJson_1.UserComparator.TextStartsWithAnyOf: case ConfigJson_1.UserComparator.TextNotStartsWithAnyOf: text = getUserAttributeValueAsText(userAttributeName, userAttributeValue, condition, context.key, this.logger); return this.evaluateTextSliceEqualsAnyOf(text, condition.comparisonValue, true, condition.comparator === ConfigJson_1.UserComparator.TextNotStartsWithAnyOf); case ConfigJson_1.UserComparator.SensitiveTextStartsWithAnyOf: case ConfigJson_1.UserComparator.SensitiveTextNotStartsWithAnyOf: text = getUserAttributeValueAsText(userAttributeName, userAttributeValue, condition, context.key, this.logger); return this.evaluateSensitiveTextSliceEqualsAnyOf(text, condition.comparisonValue, context.setting.configJsonSalt, contextSalt, true, condition.comparator === ConfigJson_1.UserComparator.SensitiveTextNotStartsWithAnyOf); case ConfigJson_1.UserComparator.TextEndsWithAnyOf: case ConfigJson_1.UserComparator.TextNotEndsWithAnyOf: text = getUserAttributeValueAsText(userAttributeName, userAttributeValue, condition, context.key, this.logger); return this.evaluateTextSliceEqualsAnyOf(text, condition.comparisonValue, false, condition.comparator === ConfigJson_1.UserComparator.TextNotEndsWithAnyOf); case ConfigJson_1.UserComparator.SensitiveTextEndsWithAnyOf: case ConfigJson_1.UserComparator.SensitiveTextNotEndsWithAnyOf: text = getUserAttributeValueAsText(userAttributeName, userAttributeValue, condition, context.key, this.logger); return this.evaluateSensitiveTextSliceEqualsAnyOf(text, condition.comparisonValue, context.setting.configJsonSalt, contextSalt, false, condition.comparator === ConfigJson_1.UserComparator.SensitiveTextNotEndsWithAnyOf); case ConfigJson_1.UserComparator.TextContainsAnyOf: case ConfigJson_1.UserComparator.TextNotContainsAnyOf: text = getUserAttributeValueAsText(userAttributeName, userAttributeValue, condition, context.key, this.logger); return this.evaluateTextContainsAnyOf(text, condition.comparisonValue, condition.comparator === ConfigJson_1.UserComparator.TextNotContainsAnyOf); case ConfigJson_1.UserComparator.SemVerIsOneOf: case ConfigJson_1.UserComparator.SemVerIsNotOneOf: versionOrError = getUserAttributeValueAsSemVer(userAttributeName, userAttributeValue, condition, context.key, this.logger); return typeof versionOrError !== "string" ? this.evaluateSemVerIsOneOf(versionOrError, condition.comparisonValue, condition.comparator === ConfigJson_1.UserComparator.SemVerIsNotOneOf) : versionOrError; case ConfigJson_1.UserComparator.SemVerLess: case ConfigJson_1.UserComparator.SemVerLessOrEquals: case ConfigJson_1.UserComparator.SemVerGreater: case ConfigJson_1.UserComparator.SemVerGreaterOrEquals: versionOrError = getUserAttributeValueAsSemVer(userAttributeName, userAttributeValue, condition, context.key, this.logger); return typeof versionOrError !== "string" ? this.evaluateSemVerRelation(versionOrError, condition.comparator, condition.comparisonValue) : versionOrError; case ConfigJson_1.UserComparator.NumberEquals: case ConfigJson_1.UserComparator.NumberNotEquals: case ConfigJson_1.UserComparator.NumberLess: case ConfigJson_1.UserComparator.NumberLessOrEquals: case ConfigJson_1.UserComparator.NumberGreater: case ConfigJson_1.UserComparator.NumberGreaterOrEquals: numberOrError = getUserAttributeValueAsNumber(userAttributeName, userAttributeValue, condition, context.key, this.logger); return typeof numberOrError !== "string" ? this.evaluateNumberRelation(numberOrError, condition.comparator, condition.comparisonValue) : numberOrError; case ConfigJson_1.UserComparator.DateTimeBefore: case ConfigJson_1.UserComparator.DateTimeAfter: numberOrError = getUserAttributeValueAsUnixTimeSeconds(userAttributeName, userAttributeValue, condition, context.key, this.logger); return typeof numberOrError !== "string" ? this.evaluateDateTimeRelation(numberOrError, condition.comparisonValue, condition.comparator === ConfigJson_1.UserComparator.DateTimeBefore) : numberOrError; case ConfigJson_1.UserComparator.ArrayContainsAnyOf: case ConfigJson_1.UserComparator.ArrayNotContainsAnyOf: arrayOrError = getUserAttributeValueAsStringArray(userAttributeName, userAttributeValue, condition, context.key, this.logger); return typeof arrayOrError !== "string" ? this.evaluateArrayContainsAnyOf(arrayOrError, condition.comparisonValue, condition.comparator === ConfigJson_1.UserComparator.ArrayNotContainsAnyOf) : arrayOrError; case ConfigJson_1.UserComparator.SensitiveArrayContainsAnyOf: case ConfigJson_1.UserComparator.SensitiveArrayNotContainsAnyOf: arrayOrError = getUserAttributeValueAsStringArray(userAttributeName, userAttributeValue, condition, context.key, this.logger); return typeof arrayOrError !== "string" ? this.evaluateSensitiveArrayContainsAnyOf(arrayOrError, condition.comparisonValue, context.setting.configJsonSalt, contextSalt, condition.comparator === ConfigJson_1.UserComparator.SensitiveArrayNotContainsAnyOf) : arrayOrError; default: throw new Error(); // execution should never get here (unless there is an error in the config JSON) } }; RolloutEvaluator.prototype.evaluateTextEquals = function (text, comparisonValue, negate) { return (text === comparisonValue) !== negate; }; RolloutEvaluator.prototype.evaluateSensitiveTextEquals = function (text, comparisonValue, configJsonSalt, contextSalt, negate) { var hash = hashComparisonValue(text, configJsonSalt, contextSalt); return (hash === comparisonValue) !== negate; }; RolloutEvaluator.prototype.evaluateTextIsOneOf = function (text, comparisonValues, negate) { // NOTE: Array.prototype.indexOf uses strict equality. var result = comparisonValues.indexOf(text) >= 0; return result !== negate; }; RolloutEvaluator.prototype.evaluateSensitiveTextIsOneOf = function (text, comparisonValues, configJsonSalt, contextSalt, negate) { var hash = hashComparisonValue(text, configJsonSalt, contextSalt); // NOTE: Array.prototype.indexOf uses strict equality. var result = comparisonValues.indexOf(hash) >= 0; return result !== negate; }; RolloutEvaluator.prototype.evaluateTextSliceEqualsAnyOf = function (text, comparisonValues, startsWith, negate) { for (var i = 0; i < comparisonValues.length; i++) { var item = comparisonValues[i]; if (text.length < item.length) { continue; } // NOTE: String.prototype.startsWith/endsWith were introduced after ES5. We'd rather work around them instead of polyfilling them. var result = (startsWith ? text.lastIndexOf(item, 0) : text.indexOf(item, text.length - item.length)) >= 0; if (result) { return !negate; } } return negate; }; RolloutEvaluator.prototype.evaluateSensitiveTextSliceEqualsAnyOf = function (text, comparisonValues, configJsonSalt, contextSalt, startsWith, negate) { var textUtf8 = Utils_1.utf8Encode(text); for (var i = 0; i < comparisonValues.length; i++) { var item = comparisonValues[i]; var index = item.indexOf("_"); var sliceLength = parseInt(item.slice(0, index)); if (textUtf8.length < sliceLength) { continue; } var sliceUtf8 = startsWith ? textUtf8.slice(0, sliceLength) : textUtf8.slice(textUtf8.length - sliceLength); var hash = hashComparisonValueSlice(sliceUtf8, configJsonSalt, contextSalt); var result = hash === item.slice(index + 1); if (result) { return !negate; } } return negate; }; RolloutEvaluator.prototype.evaluateTextContainsAnyOf = function (text, comparisonValues, negate) { for (var i = 0; i < comparisonValues.length; i++) { if (text.indexOf(comparisonValues[i]) >= 0) { return !negate; } } return negate; }; RolloutEvaluator.prototype.evaluateSemVerIsOneOf = function (version, comparisonValues, negate) { var result = false; for (var i = 0; i < comparisonValues.length; i++) { var item = comparisonValues[i]; // NOTE: Previous versions of the evaluation algorithm ignore empty comparison values. // We keep this behavior for backward compatibility. if (!item.length) { continue; } var version2 = Semver_1.parse(item.trim()); if (!version2) { // NOTE: Previous versions of the evaluation algorithm ignored invalid comparison values. // We keep this behavior for backward compatibility. return false; } if (!result && version.compare(version2) === 0) { // NOTE: Previous versions of the evaluation algorithm require that // none of the comparison values are empty or invalid, that is, we can't stop when finding a match. // We keep this behavior for backward compatibility. result = true; } } return result !== negate; }; RolloutEvaluator.prototype.evaluateSemVerRelation = function (version, comparator, comparisonValue) { var version2 = Semver_1.parse(comparisonValue.trim()); if (!version2) { return false; } var comparisonResult = version.compare(version2); switch (comparator) { case ConfigJson_1.UserComparator.SemVerLess: return comparisonResult < 0; case ConfigJson_1.UserComparator.SemVerLessOrEquals: return comparisonResult <= 0; case ConfigJson_1.UserComparator.SemVerGreater: return comparisonResult > 0; case ConfigJson_1.UserComparator.SemVerGreaterOrEquals: return comparisonResult >= 0; } }; RolloutEvaluator.prototype.evaluateNumberRelation = function (number, comparator, comparisonValue) { switch (comparator) { case ConfigJson_1.UserComparator.NumberEquals: return number === comparisonValue; case ConfigJson_1.UserComparator.NumberNotEquals: return number !== comparisonValue; case ConfigJson_1.UserComparator.NumberLess: return number < comparisonValue; case ConfigJson_1.UserComparator.NumberLessOrEquals: return number <= comparisonValue; case ConfigJson_1.UserComparator.NumberGreater: return number > comparisonValue; case ConfigJson_1.UserComparator.NumberGreaterOrEquals: return number >= comparisonValue; } }; RolloutEvaluator.prototype.evaluateDateTimeRelation = function (number, comparisonValue, before) { return before ? number < comparisonValue : number > comparisonValue; }; RolloutEvaluator.prototype.evaluateArrayContainsAnyOf = function (array, comparisonValues, negate) { for (var i = 0; i < array.length; i++) { // NOTE: Array.prototype.indexOf uses strict equality. var result = comparisonValues.indexOf(array[i]) >= 0; if (result) { return !negate; } } return negate; }; RolloutEvaluator.prototype.evaluateSensitiveArrayContainsAnyOf = function (array, comparisonValues, configJsonSalt, contextSalt, negate) { for (var i = 0; i < array.length; i++) { var hash = hashComparisonValue(array[i], configJsonSalt, contextSalt); // NOTE: Array.prototype.indexOf uses strict equality. var result = comparisonValues.indexOf(hash) >= 0; if (result) { return !negate; } } return negate; }; RolloutEvaluator.prototype.evaluatePrerequisiteFlagCondition = function (condition, context) { var logBuilder = context.logBuilder; logBuilder === null || logBuilder === void 0 ? void 0 : logBuilder.appendPrerequisiteFlagCondition(condition, context.settings); var prerequisiteFlagKey = condition.prerequisiteFlagKey; var prerequisiteFlag = context.settings[prerequisiteFlagKey]; context.visitedFlags.push(context.key); if (context.visitedFlags.indexOf(prerequisiteFlagKey) >= 0) { context.visitedFlags.push(prerequisiteFlagKey); var dependencyCycle = Utils_1.formatStringList(context.visitedFlags, void 0, void 0, " -> "); throw new Error("Circular dependency detected between the following depending flags: " + dependencyCycle + "."); } var prerequisiteFlagContext = EvaluateContext.forPrerequisiteFlag(prerequisiteFlagKey, prerequisiteFlag, context); logBuilder === null || logBuilder === void 0 ? void 0 : logBuilder.newLine("(").increaseIndent().newLine("Evaluating prerequisite flag '" + prerequisiteFlagKey + "':"); var prerequisiteFlagEvaluateResult = this.evaluateSetting(prerequisiteFlagContext); context.visitedFlags.pop(); var prerequisiteFlagValue = prerequisiteFlagEvaluateResult.selectedValue.value; if (typeof prerequisiteFlagValue !== typeof condition.comparisonValue) { if (isAllowedValue(prerequisiteFlagValue)) { throw new Error("Type mismatch between comparison value '" + condition.comparisonValue + "' and prerequisite flag '" + prerequisiteFlagKey + "'."); } else { handleInvalidReturnValue(prerequisiteFlagValue); } } var result; switch (condition.comparator) { case ConfigJson_1.PrerequisiteFlagComparator.Equals: result = prerequisiteFlagValue === condition.comparisonValue; break; case ConfigJson_1.PrerequisiteFlagComparator.NotEquals: result = prerequisiteFlagValue !== condition.comparisonValue; break; default: throw new Error(); // execution should never get here (unless there is an error in the config JSON) } logBuilder === null || logBuilder === void 0 ? void 0 : logBuilder.newLine("Prerequisite flag evaluation result: '" + EvaluateLogBuilder_1.valueToString(prerequisiteFlagValue) + "'.").newLine("Condition (").appendPrerequisiteFlagCondition(condition, context.settings).append(") evaluates to ").appendConditionResult(result).append(".").decreaseIndent().newLine(")"); return result; }; RolloutEvaluator.prototype.evaluateSegmentCondition = function (condition, context) { var logBuilder = context.logBuilder; logBuilder === null || logBuilder === void 0 ? void 0 : logBuilder.appendSegmentCondition(condition); if (!context.user) { if (!context.isMissingUserObjectLogged) { this.logger.userObjectIsMissing(context.key); context.isMissingUserObjectLogged = true; } return missingUserObjectError; } var segment = condition.segment; logBuilder === null || logBuilder === void 0 ? void 0 : logBuilder.newLine("(").increaseIndent().newLine("Evaluating segment '" + segment.name + "':"); var segmentResult = this.evaluateConditions(segment.conditions, void 0, segment.name, context); var result = segmentResult; if (!isEvaluationError(result)) { switch (condition.comparator) { case ConfigJson_1.SegmentComparator.IsIn: break; case ConfigJson_1.SegmentComparator.IsNotIn: result = !result; break; default: throw new Error(); // execution should never get here (unless there is an error in the config JSON) } } if (logBuilder) { logBuilder.newLine("Segment evaluation result: "); (!isEvaluationError(result) ? logBuilder.append("User " + EvaluateLogBuilder_1.formatSegmentComparator(segmentResult ? ConfigJson_1.SegmentComparator.IsIn : ConfigJson_1.SegmentComparator.IsNotIn)) : logBuilder.append(result)) .append("."); logBuilder.newLine("Condition (").appendSegmentCondition(condition).append(")"); (!isEvaluationError(result) ? logBuilder.append(" evaluates to ").appendConditionResult(result) : logBuilder.append(" failed to evaluate")) .append("."); logBuilder .decreaseIndent() .newLine(")"); } return result; }; return RolloutEvaluator; }()); exports.RolloutEvaluator = RolloutEvaluator; function isEvaluationError(isMatchOrError) { return typeof isMatchOrError === "string"; } function hashComparisonValue(value, configJsonSalt, contextSalt) { return hashComparisonValueSlice(Utils_1.utf8Encode(value), configJsonSalt, contextSalt); } function hashComparisonValueSlice(sliceUtf8, configJsonSalt, contextSalt) { return Hash_1.sha256(sliceUtf8 + Utils_1.utf8Encode(configJsonSalt) + Utils_1.utf8Encode(contextSalt)); } function userAttributeValueToString(userAttributeValue) { return typeof userAttributeValue === "string" ? userAttributeValue : userAttributeValue instanceof Date ? (userAttributeValue.getTime() / 1000) + "" : Utils_1.isStringArray(userAttributeValue) ? JSON.stringify(userAttributeValue) : userAttributeValue + ""; } function getUserAttributeValueAsText(attributeName, attributeValue, condition, key, logger) { if (typeof attributeValue === "string") { return attributeValue; } attributeValue = userAttributeValueToString(attributeValue); logger.userObjectAttributeIsAutoConverted(EvaluateLogBuilder_1.formatUserCondition(condition), key, attributeName, attributeValue); return attributeValue; } function getUserAttributeValueAsSemVer(attributeName, attributeValue, condition, key, logger) { var version; if (typeof attributeValue === "string" && (version = Semver_1.parse(attributeValue.trim()))) { return version; } return handleInvalidUserAttribute(logger, condition, key, attributeName, "'" + attributeValue + "' is not a valid semantic version"); } function getUserAttributeValueAsNumber(attributeName, attributeValue, condition, key, logger) { if (typeof attributeValue === "number") { return attributeValue; } var number; if (typeof attributeValue === "string" && (!isNaN(number = Utils_1.parseFloatStrict(attributeValue.replace(",", "."))) || attributeValue.trim() === "NaN")) { return number; } return handleInvalidUserAttribute(logger, condition, key, attributeName, "'" + attributeValue + "' is not a valid decimal number"); } function getUserAttributeValueAsUnixTimeSeconds(attributeName, attributeValue, condition, key, logger) { if (attributeValue instanceof Date) { return attributeValue.getTime() / 1000; } if (typeof attributeValue === "number") { return attributeValue; } var number; if (typeof attributeValue === "string" && (!isNaN(number = Utils_1.parseFloatStrict(attributeValue.replace(",", "."))) || attributeValue.trim() === "NaN")) { return number; } return handleInvalidUserAttribute(logger, condition, key, attributeName, "'" + attributeValue + "' is not a valid Unix timestamp (number of seconds elapsed since Unix epoch)"); } function getUserAttributeValueAsStringArray(attributeName, attributeValue, condition, key, logger) { var stringArray = attributeValue; if (typeof stringArray === "string") { try { stringArray = JSON.parse(stringArray); } catch (err) { /* intentional no-op */ } } if (Utils_1.isStringArray(stringArray)) { return stringArray; } return handleInvalidUserAttribute(logger, condition, key, attributeName, "'" + attributeValue + "' is not a valid string array"); } function handleInvalidUserAttribute(logger, condition, key, attributeName, reason) { logger.userObjectAttributeIsInvalid(EvaluateLogBuilder_1.formatUserCondition(condition), key, reason, attributeName); return invalidUserAttributeError(attributeName, reason); } /* Helper functions */ function evaluationDetailsFromEvaluateResult(key, evaluateResult, fetchTime, user) { return { key: key, value: evaluateResult.selectedValue.value, variationId: evaluateResult.selectedValue.variationId, fetchTime: fetchTime, user: user, isDefaultValue: false, matchedTargetingRule: evaluateResult.matchedTargetingRule, matchedPercentageOption: evaluateResult.matchedPercentageOption, }; } function evaluationDetailsFromDefaultValue(key, defaultValue, fetchTime, user, errorMessage, errorException) { return { key: key, value: defaultValue, fetchTime: fetchTime, user: user, isDefaultValue: true, errorMessage: errorMessage, errorException: errorException }; } exports.evaluationDetailsFromDefaultValue = evaluationDetailsFromDefaultValue; function evaluate(evaluator, settings, key, defaultValue, user, remoteConfig, logger) { var errorMessage; if (!settings) { errorMessage = logger.configJsonIsNotPresentSingle(key, "defaultValue", defaultValue).toString(); return evaluationDetailsFromDefaultValue(key, defaultValue, getTimestampAsDate(remoteConfig), user, errorMessage); } var setting = settings[key]; if (!setting) { errorMessage = logger.settingEvaluationFailedDueToMissingKey(key, "defaultValue", defaultValue, Utils_1.formatStringList(Object.keys(settings))).toString(); return evaluationDetailsFromDefaultValue(key, defaultValue, getTimestampAsDate(remoteConfig), user, errorMessage); } var evaluateResult = evaluator.evaluate(defaultValue, new EvaluateContext(key, setting, user, settings)); return evaluationDetailsFromEvaluateResult(key, evaluateResult, getTimestampAsDate(remoteConfig), user); } exports.evaluate = evaluate; function evaluateAll(evaluator, settings, user, remoteConfig, logger, defaultReturnValue) { var errors; if (!checkSettingsAvailable(settings, logger, defaultReturnValue)) { return [[], errors]; } var evaluationDetailsArray = []; for (var _i = 0, _a = Object.entries(settings); _i < _a.length; _i++) { var _b = _a[_i], key = _b[0], setting = _b[1]; var evaluationDetails = void 0; try { var evaluateResult = evaluator.evaluate(null, new EvaluateContext(key, setting, user, settings)); evaluationDetails = evaluationDetailsFromEvaluateResult(key, evaluateResult, getTimestampAsDate(remoteConfig), user); } catch (err) { errors !== null && errors !== void 0 ? errors : (errors = []); errors.push(err); evaluationDetails = evaluationDetailsFromDefaultValue(key, null, getTimestampAsDate(remoteConfig), user, Utils_1.errorToString(err), err); } evaluationDetailsArray.push(evaluationDetails); } return [evaluationDetailsArray, errors]; } exports.evaluateAll = evaluateAll; function checkSettingsAvailable(settings, logger, defaultReturnValue) { if (!settings) { logger.configJsonIsNotPresent(defaultReturnValue); return false; } return true; } exports.checkSettingsAvailable = checkSettingsAvailable; function isAllowedValue(value) { return typeof value === "boolean" || typeof value === "string" || typeof value === "number"; } exports.isAllowedValue = isAllowedValue; function isCompatibleValue(value, settingType) { switch (settingType) { case ConfigJson_1.SettingType.Boolean: return typeof value === "boolean"; case ConfigJson_1.SettingType.String: return typeof value === "string"; case ConfigJson_1.SettingType.Int: case ConfigJson_1.SettingType.Double: return typeof value === "number"; default: return false; } } function handleInvalidReturnValue(value) { throw new TypeError(value === null ? "Setting value is null." : value === void 0 ? "Setting value is undefined." : "Setting value '" + value + "' is of an unsupported type (" + typeof value + ")."); } exports.handleInvalidReturnValue = handleInvalidReturnValue; function getTimestampAsDate(projectConfig) { return projectConfig ? new Date(projectConfig.timestamp) : void 0; } exports.getTimestampAsDate = getTimestampAsDate;