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
JavaScript
"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;