diff --git a/.vscode/settings.json b/.vscode/settings.json index e697c65..7aa411e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,8 @@ { "editor.rulers": [160], + "[typescript]": { + "editor.defaultFormatter": "dbaeumer.vscode-eslint" + }, "eslint.format.enable": true, "eslint.validate": [ "typescript" diff --git a/package-lock.json b/package-lock.json index 5af3137..f6990d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "configcat-common", - "version": "9.2.0", + "version": "9.3.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "configcat-common", - "version": "9.2.0", + "version": "9.3.0", "license": "MIT", "dependencies": { "tslib": "^2.4.1" diff --git a/package.json b/package.json index a16d27c..e42385f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "configcat-common", - "version": "9.2.0", + "version": "9.3.0", "description": "ConfigCat is a configuration as a service that lets you manage your features and configurations without actually deploying new code.", "main": "lib/index.js", "types": "lib/index.d.ts", diff --git a/src/ConfigCatLogger.ts b/src/ConfigCatLogger.ts index 12f1861..089f348 100644 --- a/src/ConfigCatLogger.ts +++ b/src/ConfigCatLogger.ts @@ -64,6 +64,9 @@ export interface IConfigCatLogger { /** Gets the log level (the minimum level to use for filtering log events). */ readonly level?: LogLevel; + /** Gets the character sequence to use for line breaks in log messages. Defaults to "\n". */ + readonly eol?: string; + /** * Writes an event into the log. * @param level Event severity level. @@ -79,6 +82,10 @@ export class LoggerWrapper implements IConfigCatLogger { return this.logger.level ?? LogLevel.Warn; } + get eol(): string { + return this.logger.eol ?? "\n"; + } + constructor( private readonly logger: IConfigCatLogger, private readonly hooks?: SafeHooksWrapper) { @@ -369,7 +376,7 @@ export class ConfigCatConsoleLogger implements IConfigCatLogger { /** * Create an instance of ConfigCatConsoleLogger */ - constructor(public level = LogLevel.Warn) { + constructor(public level = LogLevel.Warn, readonly eol = "\n") { } /** @inheritdoc */ @@ -381,7 +388,7 @@ export class ConfigCatConsoleLogger implements IConfigCatLogger { level === LogLevel.Error ? [console.error, "ERROR"] : [console.log, LogLevel[level].toUpperCase()]; - const exceptionString = exception !== void 0 ? "\n" + errorToString(exception, true) : ""; + const exceptionString = exception !== void 0 ? this.eol + errorToString(exception, true) : ""; logMethod(`${this.SOURCE} - ${levelString} - [${eventId}] ${message}${exceptionString}`); } diff --git a/src/ConfigJson.ts b/src/ConfigJson.ts index 03e7a39..d056561 100644 --- a/src/ConfigJson.ts +++ b/src/ConfigJson.ts @@ -83,10 +83,10 @@ export type UserCondition = } & UserConditionComparisonValue export type UserConditionComparisonValue = { - [UserComparator.IsOneOf]: UserConditionStringListComparisonValue; - [UserComparator.IsNotOneOf]: UserConditionStringListComparisonValue; - [UserComparator.ContainsAnyOf]: UserConditionStringListComparisonValue; - [UserComparator.NotContainsAnyOf]: UserConditionStringListComparisonValue; + [UserComparator.TextIsOneOf]: UserConditionStringListComparisonValue; + [UserComparator.TextIsNotOneOf]: UserConditionStringListComparisonValue; + [UserComparator.TextContainsAnyOf]: UserConditionStringListComparisonValue; + [UserComparator.TextNotContainsAnyOf]: UserConditionStringListComparisonValue; [UserComparator.SemVerIsOneOf]: UserConditionStringListComparisonValue; [UserComparator.SemVerIsNotOneOf]: UserConditionStringListComparisonValue; [UserComparator.SemVerLess]: UserConditionStringComparisonValue; @@ -99,8 +99,8 @@ export type UserConditionComparisonValueUnix Epoch is less than the comparison value. */ + /** IS ONE OF (hashed) - Checks whether the comparison attribute is equal to any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). */ + SensitiveTextIsOneOf = 16, + /** IS NOT ONE OF (hashed) - Checks whether the comparison attribute is not equal to any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). */ + SensitiveTextIsNotOneOf = 17, + /** BEFORE (UTC datetime) - Checks whether the comparison attribute interpreted as the seconds elapsed since Unix Epoch is less than the comparison value. */ DateTimeBefore = 18, - /** AFTER (UTC datetime) - It matches when the comparison attribute interpreted as the seconds elapsed since Unix Epoch is greater than the comparison value. */ + /** AFTER (UTC datetime) - Checks whether the comparison attribute interpreted as the seconds elapsed since Unix Epoch is greater than the comparison value. */ DateTimeAfter = 19, - /** EQUALS (hashed) - It matches when the comparison attribute is equal to the comparison value (where the comparison is performed using the salted SHA256 hashes of the values). */ + /** EQUALS (hashed) - Checks whether the comparison attribute is equal to the comparison value (where the comparison is performed using the salted SHA256 hashes of the values). */ SensitiveTextEquals = 20, - /** NOT EQUALS (hashed) - It matches when the comparison attribute is not equal to the comparison value (where the comparison is performed using the salted SHA256 hashes of the values). */ + /** NOT EQUALS (hashed) - Checks whether the comparison attribute is not equal to the comparison value (where the comparison is performed using the salted SHA256 hashes of the values). */ SensitiveTextNotEquals = 21, - /** STARTS WITH ANY OF (hashed) - It matches when the comparison attribute starts with any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). */ + /** STARTS WITH ANY OF (hashed) - Checks whether the comparison attribute starts with any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). */ SensitiveTextStartsWithAnyOf = 22, - /** NOT STARTS WITH ANY OF (hashed) - It matches when the comparison attribute does not start with any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). */ + /** NOT STARTS WITH ANY OF (hashed) - Checks whether the comparison attribute does not start with any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). */ SensitiveTextNotStartsWithAnyOf = 23, - /** ENDS WITH ANY OF (hashed) - It matches when the comparison attribute ends with any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). */ + /** ENDS WITH ANY OF (hashed) - Checks whether the comparison attribute ends with any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). */ SensitiveTextEndsWithAnyOf = 24, - /** NOT ENDS WITH ANY OF (hashed) - It matches when the comparison attribute does not end with any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). */ + /** NOT ENDS WITH ANY OF (hashed) - Checks whether the comparison attribute does not end with any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). */ SensitiveTextNotEndsWithAnyOf = 25, - /** ARRAY CONTAINS ANY OF (hashed) - It matches when the comparison attribute interpreted as a comma-separated list contains any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). */ + /** ARRAY CONTAINS ANY OF (hashed) - Checks whether the comparison attribute interpreted as a comma-separated list contains any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). */ SensitiveArrayContainsAnyOf = 26, - /** ARRAY NOT CONTAINS ANY OF (hashed) - It matches when the comparison attribute interpreted as a comma-separated list does not contain any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). */ + /** ARRAY NOT CONTAINS ANY OF (hashed) - Checks whether the comparison attribute interpreted as a comma-separated list does not contain any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). */ SensitiveArrayNotContainsAnyOf = 27, - /** EQUALS (cleartext) - It matches when the comparison attribute is equal to the comparison value. */ + /** EQUALS (cleartext) - Checks whether the comparison attribute is equal to the comparison value. */ TextEquals = 28, - /** NOT EQUALS (cleartext) - It matches when the comparison attribute is not equal to the comparison value. */ + /** NOT EQUALS (cleartext) - Checks whether the comparison attribute is not equal to the comparison value. */ TextNotEquals = 29, - /** STARTS WITH ANY OF (cleartext) - It matches when the comparison attribute starts with any of the comparison values. */ + /** STARTS WITH ANY OF (cleartext) - Checks whether the comparison attribute starts with any of the comparison values. */ TextStartsWithAnyOf = 30, - /** NOT STARTS WITH ANY OF (cleartext) - It matches when the comparison attribute does not start with any of the comparison values. */ + /** NOT STARTS WITH ANY OF (cleartext) - Checks whether the comparison attribute does not start with any of the comparison values. */ TextNotStartsWithAnyOf = 31, - /** ENDS WITH ANY OF (cleartext) - It matches when the comparison attribute ends with any of the comparison values. */ + /** ENDS WITH ANY OF (cleartext) - Checks whether the comparison attribute ends with any of the comparison values. */ TextEndsWithAnyOf = 32, - /** NOT ENDS WITH ANY OF (cleartext) - It matches when the comparison attribute does not end with any of the comparison values. */ + /** NOT ENDS WITH ANY OF (cleartext) - Checks whether the comparison attribute does not end with any of the comparison values. */ TextNotEndsWithAnyOf = 33, - /** ARRAY CONTAINS ANY OF (cleartext) - It matches when the comparison attribute interpreted as a comma-separated list contains any of the comparison values. */ + /** ARRAY CONTAINS ANY OF (cleartext) - Checks whether the comparison attribute interpreted as a comma-separated list contains any of the comparison values. */ ArrayContainsAnyOf = 34, - /** ARRAY NOT CONTAINS ANY OF (cleartext) - It matches when the comparison attribute interpreted as a comma-separated list does not contain any of the comparison values. */ + /** ARRAY NOT CONTAINS ANY OF (cleartext) - Checks whether the comparison attribute interpreted as a comma-separated list does not contain any of the comparison values. */ ArrayNotContainsAnyOf = 35, } /** Prerequisite flag comparison operator used during the evaluation process. */ export enum PrerequisiteFlagComparator { - /** EQUALS - It matches when the evaluated value of the specified prerequisite flag is equal to the comparison value. */ + /** EQUALS - Checks whether the evaluated value of the specified prerequisite flag is equal to the comparison value. */ Equals = 0, - /** NOT EQUALS - It matches when the evaluated value of the specified prerequisite flag is not equal to the comparison value. */ + /** NOT EQUALS - Checks whether the evaluated value of the specified prerequisite flag is not equal to the comparison value. */ NotEquals = 1 } /** Segment comparison operator used during the evaluation process. */ export enum SegmentComparator { - /** IS IN SEGMENT - It matches when the conditions of the specified segment are evaluated to true. */ + /** IS IN SEGMENT - Checks whether the conditions of the specified segment are evaluated to true. */ IsIn = 0, - /** IS NOT IN SEGMENT - It matches when the conditions of the specified segment are evaluated to false. */ + /** IS NOT IN SEGMENT - Checks whether the conditions of the specified segment are evaluated to false. */ IsNotIn = 1, } diff --git a/src/EvaluateLogBuilder.ts b/src/EvaluateLogBuilder.ts index e9e5e0c..9ce8fbe 100644 --- a/src/EvaluateLogBuilder.ts +++ b/src/EvaluateLogBuilder.ts @@ -1,7 +1,7 @@ import { PrerequisiteFlagComparator, SegmentComparator, UserComparator } from "./ConfigJson"; -import type { PrerequisiteFlagCondition, SegmentCondition, SettingValue, TargetingRule, UserCondition, UserConditionUnion } from "./ProjectConfig"; +import type { PrerequisiteFlagCondition, SegmentCondition, Setting, SettingValue, TargetingRule, UserCondition, UserConditionUnion } from "./ProjectConfig"; import { isAllowedValue } from "./RolloutEvaluator"; -import { formatStringList, isArray } from "./Utils"; +import { formatStringList, isArray, isStringArray } from "./Utils"; const invalidValuePlaceholder = ""; const invalidNamePlaceholder = ""; @@ -14,6 +14,9 @@ export class EvaluateLogBuilder { private log = ""; private indent = ""; + constructor(private readonly eol: string) { + } + resetIndent(): this { this.indent = ""; return this; @@ -30,7 +33,7 @@ export class EvaluateLogBuilder { } newLine(text?: string): this { - this.log += "\n" + this.indent + (text ?? ""); + this.log += this.eol + this.indent + (text ?? ""); return this; } @@ -43,20 +46,20 @@ export class EvaluateLogBuilder { return this.log; } - appendEvaluationResult(isMatch: boolean): this { - return this.append(`${isMatch}`); - } - private appendUserConditionCore(comparisonAttribute: string, comparator: UserComparator, comparisonValue?: unknown) { return this.append(`User.${comparisonAttribute} ${formatUserComparator(comparator)} '${comparisonValue ?? invalidValuePlaceholder}'`); } private appendUserConditionString(comparisonAttribute: string, comparator: UserComparator, comparisonValue: string, isSensitive: boolean) { + if (typeof comparisonValue !== "string") { + return this.appendUserConditionCore(comparisonAttribute, comparator); + } + return this.appendUserConditionCore(comparisonAttribute, comparator, !isSensitive ? comparisonValue : ""); } private appendUserConditionStringList(comparisonAttribute: string, comparator: UserComparator, comparisonValue: ReadonlyArray, isSensitive: boolean): this { - if (comparisonValue == null) { + if (!isStringArray(comparisonValue)) { return this.appendUserConditionCore(comparisonAttribute, comparator); } @@ -74,7 +77,7 @@ export class EvaluateLogBuilder { } private appendUserConditionNumber(comparisonAttribute: string, comparator: UserComparator, comparisonValue: number, isDateTime?: boolean) { - if (comparisonValue == null) { + if (typeof comparisonValue !== "number") { return this.appendUserConditionCore(comparisonAttribute, comparator); } @@ -86,12 +89,14 @@ export class EvaluateLogBuilder { } appendUserCondition(condition: UserConditionUnion): this { - const { comparisonAttribute, comparator } = condition; + const comparisonAttribute = typeof condition.comparisonAttribute === "string" ? condition.comparisonAttribute : invalidNamePlaceholder; + const comparator = condition.comparator; + switch (condition.comparator) { - case UserComparator.IsOneOf: - case UserComparator.IsNotOneOf: - case UserComparator.ContainsAnyOf: - case UserComparator.NotContainsAnyOf: + case UserComparator.TextIsOneOf: + case UserComparator.TextIsNotOneOf: + case UserComparator.TextContainsAnyOf: + case UserComparator.TextNotContainsAnyOf: case UserComparator.SemVerIsOneOf: case UserComparator.SemVerIsNotOneOf: case UserComparator.TextStartsWithAnyOf: @@ -118,8 +123,8 @@ export class EvaluateLogBuilder { case UserComparator.NumberGreaterOrEquals: return this.appendUserConditionNumber(comparisonAttribute, comparator, condition.comparisonValue); - case UserComparator.SensitiveIsOneOf: - case UserComparator.SensitiveIsNotOneOf: + case UserComparator.SensitiveTextIsOneOf: + case UserComparator.SensitiveTextIsNotOneOf: case UserComparator.SensitiveTextStartsWithAnyOf: case UserComparator.SensitiveTextNotStartsWithAnyOf: case UserComparator.SensitiveTextEndsWithAnyOf: @@ -141,8 +146,12 @@ export class EvaluateLogBuilder { } } - appendPrerequisiteFlagCondition(condition: PrerequisiteFlagCondition): this { - const prerequisiteFlagKey = condition.prerequisiteFlagKey; + appendPrerequisiteFlagCondition(condition: PrerequisiteFlagCondition, settings: Readonly<{ [name: string]: Setting }>): this { + const prerequisiteFlagKey = + typeof condition.prerequisiteFlagKey !== "string" ? invalidNamePlaceholder : + !(condition.prerequisiteFlagKey in settings) ? invalidReferencePlaceholder : + condition.prerequisiteFlagKey; + const comparator = condition.comparator; const comparisonValue = condition.comparisonValue; @@ -153,18 +162,24 @@ export class EvaluateLogBuilder { const segment = condition.segment; const comparator = condition.comparator; - const segmentName = segment?.name ?? - (segment == null ? invalidReferencePlaceholder : invalidNamePlaceholder); + const segmentName = + segment == null ? invalidReferencePlaceholder : + typeof segment.name !== "string" || !segment.name ? invalidNamePlaceholder : + segment.name; return this.append(`User ${formatSegmentComparator(comparator)} '${segmentName}'`); } - appendConditionConsequence(isMatch: boolean): this { - this.append(" => ").appendEvaluationResult(isMatch); - return isMatch ? this : this.append(", skipping the remaining AND conditions"); + appendConditionResult(result: boolean): this { + return this.append(`${result}`); + } + + appendConditionConsequence(result: boolean): this { + this.append(" => ").appendConditionResult(result); + return result ? this : this.append(", skipping the remaining AND conditions"); } - appendTargetingRuleThenPart(targetingRule: TargetingRule, newLine: boolean): this { + private appendTargetingRuleThenPart(targetingRule: TargetingRule, newLine: boolean): this { (newLine ? this.newLine() : this.append(" ")) .append("THEN"); @@ -184,14 +199,14 @@ export class EvaluateLogBuilder { export function formatUserComparator(comparator: UserComparator): string { switch (comparator) { - case UserComparator.IsOneOf: - case UserComparator.SensitiveIsOneOf: + case UserComparator.TextIsOneOf: + case UserComparator.SensitiveTextIsOneOf: case UserComparator.SemVerIsOneOf: return "IS ONE OF"; - case UserComparator.IsNotOneOf: - case UserComparator.SensitiveIsNotOneOf: + case UserComparator.TextIsNotOneOf: + case UserComparator.SensitiveTextIsNotOneOf: case UserComparator.SemVerIsNotOneOf: return "IS NOT ONE OF"; - case UserComparator.ContainsAnyOf: return "CONTAINS ANY OF"; - case UserComparator.NotContainsAnyOf: return "NOT CONTAINS ANY OF"; + case UserComparator.TextContainsAnyOf: return "CONTAINS ANY OF"; + case UserComparator.TextNotContainsAnyOf: return "NOT CONTAINS ANY OF"; case UserComparator.SemVerLess: case UserComparator.NumberLess: return "<"; case UserComparator.SemVerLessOrEquals: @@ -225,7 +240,7 @@ export function formatUserComparator(comparator: UserComparator): string { } export function formatUserCondition(condition: UserConditionUnion): string { - return new EvaluateLogBuilder().appendUserCondition(condition).toString(); + return new EvaluateLogBuilder("").appendUserCondition(condition).toString(); } export function formatPrerequisiteFlagComparator(comparator: PrerequisiteFlagComparator): string { diff --git a/src/ProjectConfig.ts b/src/ProjectConfig.ts index 00860c5..a1a395c 100644 --- a/src/ProjectConfig.ts +++ b/src/ProjectConfig.ts @@ -272,10 +272,10 @@ export interface ICondition; - [UserComparator.IsNotOneOf]: Readonly; - [UserComparator.ContainsAnyOf]: Readonly; - [UserComparator.NotContainsAnyOf]: Readonly; + [UserComparator.TextIsOneOf]: Readonly; + [UserComparator.TextIsNotOneOf]: Readonly; + [UserComparator.TextContainsAnyOf]: Readonly; + [UserComparator.TextNotContainsAnyOf]: Readonly; [UserComparator.SemVerIsOneOf]: Readonly; [UserComparator.SemVerIsNotOneOf]: Readonly; [UserComparator.SemVerLess]: string; @@ -288,8 +288,8 @@ export type UserConditionComparisonValueTypeMap = { [UserComparator.NumberLessOrEquals]: number; [UserComparator.NumberGreater]: number; [UserComparator.NumberGreaterOrEquals]: number; - [UserComparator.SensitiveIsOneOf]: Readonly; - [UserComparator.SensitiveIsNotOneOf]: Readonly; + [UserComparator.SensitiveTextIsOneOf]: Readonly; + [UserComparator.SensitiveTextIsNotOneOf]: Readonly; [UserComparator.DateTimeBefore]: number; [UserComparator.DateTimeAfter]: number; [UserComparator.SensitiveTextEquals]: string; diff --git a/src/RolloutEvaluator.ts b/src/RolloutEvaluator.ts index 4d6c26f..bc3bc3a 100644 --- a/src/RolloutEvaluator.ts +++ b/src/RolloutEvaluator.ts @@ -6,17 +6,11 @@ import { sha1, sha256 } from "./Hash"; import type { ConditionUnion, IPercentageOption, ITargetingRule, PercentageOption, PrerequisiteFlagCondition, ProjectConfig, SegmentCondition, Setting, SettingValue, SettingValueContainer, TargetingRule, UserConditionUnion, VariationIdValue } from "./ProjectConfig"; import type { ISemVer } from "./Semver"; import { parse as parseSemVer } from "./Semver"; -import type { User, UserAttributeValue } from "./User"; -import { getUserAttributes } from "./User"; -import { errorToString, formatStringList, isArray, parseFloatStrict, utf8Encode } from "./Utils"; +import type { User, UserAttributeValue, WellKnownUserObjectAttribute } from "./User"; +import { getUserAttribute, getUserAttributes } from "./User"; +import { errorToString, formatStringList, isArray, isStringArray, parseFloatStrict, utf8Encode } from "./Utils"; export class EvaluateContext { - private $userAttributes?: { [key: string]: UserAttributeValue } | null; - get userAttributes(): { [key: string]: UserAttributeValue } | null { - const attributes = this.$userAttributes; - return attributes !== void 0 ? attributes : (this.$userAttributes = this.user ? getUserAttributes(this.user) : null); - } - private $visitedFlags?: string[]; get visitedFlags(): string[] { return this.$visitedFlags ??= []; } @@ -35,7 +29,6 @@ export class EvaluateContext { static forPrerequisiteFlag(key: string, setting: Setting, dependentFlagContext: EvaluateContext): EvaluateContext { const context = new EvaluateContext(key, setting, dependentFlagContext.user, dependentFlagContext.settings); - context.$userAttributes = dependentFlagContext.userAttributes; context.$visitedFlags = dependentFlagContext.visitedFlags; // crucial to use the computed property here to make sure the list is created! context.logBuilder = dependentFlagContext.logBuilder; return context; @@ -69,12 +62,12 @@ export class RolloutEvaluator implements IRolloutEvaluator { // Building the evaluation log is expensive, so let's not do it if it wouldn't be logged anyway. if (this.logger.isEnabled(LogLevel.Info)) { - context.logBuilder = logBuilder = new EvaluateLogBuilder(); + context.logBuilder = logBuilder = new EvaluateLogBuilder(this.logger.eol); logBuilder.append(`Evaluating '${context.key}'`); - if (context.userAttributes) { - logBuilder.append(` for User '${JSON.stringify(context.userAttributes)}'`); + if (context.user) { + logBuilder.append(` for User '${JSON.stringify(getUserAttributes(context.user))}'`); } logBuilder.increaseIndent(); @@ -188,10 +181,10 @@ export class RolloutEvaluator implements IRolloutEvaluator { } } - private evaluatePercentageOptions(percentageOptions: ReadonlyArray, targetingRule: TargetingRule | undefined, context: EvaluateContext): IEvaluateResult | undefined { + private evaluatePercentageOptions(percentageOptions: ReadonlyArray, matchedTargetingRule: TargetingRule | undefined, context: EvaluateContext): IEvaluateResult | undefined { const logBuilder = context.logBuilder; - if (!context.userAttributes) { + if (!context.user) { logBuilder?.newLine("Skipping % options because the User Object is missing."); if (!context.isMissingUserObjectLogged) { @@ -202,8 +195,17 @@ export class RolloutEvaluator implements IRolloutEvaluator { return; } - const percentageOptionsAttributeName = context.setting.percentageOptionsAttribute; - const percentageOptionsAttributeValue = context.userAttributes[percentageOptionsAttributeName]; + let percentageOptionsAttributeName = context.setting.percentageOptionsAttribute; + let percentageOptionsAttributeValue: UserAttributeValue | null | undefined; + + if (percentageOptionsAttributeName == null) { + percentageOptionsAttributeName = "Identifier"; + percentageOptionsAttributeValue = context.user.identifier ?? ""; + } + else { + percentageOptionsAttributeValue = getUserAttribute(context.user, percentageOptionsAttributeName); + } + if (percentageOptionsAttributeValue == null) { logBuilder?.newLine(`Skipping % options because the User.${percentageOptionsAttributeName} attribute is missing.`); @@ -234,10 +236,10 @@ export class RolloutEvaluator implements IRolloutEvaluator { logBuilder?.newLine(`- Hash value ${hashValue} selects % option ${i + 1} (${percentageOption.percentage}%), '${valueToString(percentageOption.value)}'.`); - return { selectedValue: percentageOption, matchedTargetingRule: targetingRule, matchedPercentageOption: percentageOption }; + return { selectedValue: percentageOption, matchedTargetingRule, matchedPercentageOption: percentageOption }; } - throw new Error("Sum of percentage option percentages are less than 100."); + throw new Error("Sum of percentage option percentages is less than 100."); } private evaluateConditions(conditions: ReadonlyArray, targetingRule: TargetingRule | undefined, contextSalt: string, context: EvaluateContext): boolean | string { @@ -283,17 +285,17 @@ export class RolloutEvaluator implements IRolloutEvaluator { throw new Error(); // execution should never get here } - const isMatch = result === true; + const success = result === true; if (logBuilder) { if (!targetingRule || conditions.length > 1) { - logBuilder.appendConditionConsequence(isMatch); + logBuilder.appendConditionConsequence(success); } logBuilder.decreaseIndent(); } - if (!isMatch) { + if (!success) { break; } } @@ -309,7 +311,7 @@ export class RolloutEvaluator implements IRolloutEvaluator { const logBuilder = context.logBuilder; logBuilder?.appendUserCondition(condition); - if (!context.userAttributes) { + if (!context.user) { if (!context.isMissingUserObjectLogged) { this.logger.userObjectIsMissing(context.key); context.isMissingUserObjectLogged = true; @@ -319,7 +321,7 @@ export class RolloutEvaluator implements IRolloutEvaluator { } const userAttributeName = condition.comparisonAttribute; - const userAttributeValue = context.userAttributes[userAttributeName]; + const userAttributeValue = getUserAttribute(context.user, userAttributeName); if (userAttributeValue == null || userAttributeValue === "") { // besides null and undefined, empty string is considered missing value as well this.logger.userObjectAttributeIsMissingCondition(formatUserCondition(condition), context.key, userAttributeName); return missingUserAttributeError(userAttributeName); @@ -338,16 +340,16 @@ export class RolloutEvaluator implements IRolloutEvaluator { return this.evaluateSensitiveTextEquals(text, condition.comparisonValue, context.setting.configJsonSalt, contextSalt, condition.comparator === UserComparator.SensitiveTextNotEquals); - case UserComparator.IsOneOf: - case UserComparator.IsNotOneOf: + case UserComparator.TextIsOneOf: + case UserComparator.TextIsNotOneOf: text = getUserAttributeValueAsText(userAttributeName, userAttributeValue, condition, context.key, this.logger); - return this.evaluateIsOneOf(text, condition.comparisonValue, condition.comparator === UserComparator.IsNotOneOf); + return this.evaluateTextIsOneOf(text, condition.comparisonValue, condition.comparator === UserComparator.TextIsNotOneOf); - case UserComparator.SensitiveIsOneOf: - case UserComparator.SensitiveIsNotOneOf: + case UserComparator.SensitiveTextIsOneOf: + case UserComparator.SensitiveTextIsNotOneOf: text = getUserAttributeValueAsText(userAttributeName, userAttributeValue, condition, context.key, this.logger); - return this.evaluateSensitiveIsOneOf(text, condition.comparisonValue, - context.setting.configJsonSalt, contextSalt, condition.comparator === UserComparator.SensitiveIsNotOneOf); + return this.evaluateSensitiveTextIsOneOf(text, condition.comparisonValue, + context.setting.configJsonSalt, contextSalt, condition.comparator === UserComparator.SensitiveTextIsNotOneOf); case UserComparator.TextStartsWithAnyOf: case UserComparator.TextNotStartsWithAnyOf: @@ -371,10 +373,10 @@ export class RolloutEvaluator implements IRolloutEvaluator { return this.evaluateSensitiveTextSliceEqualsAnyOf(text, condition.comparisonValue, context.setting.configJsonSalt, contextSalt, false, condition.comparator === UserComparator.SensitiveTextNotEndsWithAnyOf); - case UserComparator.ContainsAnyOf: - case UserComparator.NotContainsAnyOf: + case UserComparator.TextContainsAnyOf: + case UserComparator.TextNotContainsAnyOf: text = getUserAttributeValueAsText(userAttributeName, userAttributeValue, condition, context.key, this.logger); - return this.evaluateContainsAnyOf(text, condition.comparisonValue, condition.comparator === UserComparator.NotContainsAnyOf); + return this.evaluateTextContainsAnyOf(text, condition.comparisonValue, condition.comparator === UserComparator.TextNotContainsAnyOf); case UserComparator.SemVerIsOneOf: case UserComparator.SemVerIsNotOneOf: @@ -439,17 +441,17 @@ export class RolloutEvaluator implements IRolloutEvaluator { return (hash === comparisonValue) !== negate; } - private evaluateIsOneOf(text: string, comparisonValues: ReadonlyArray, negate: boolean): boolean { + private evaluateTextIsOneOf(text: string, comparisonValues: ReadonlyArray, negate: boolean): boolean { // NOTE: Array.prototype.indexOf uses strict equality. - const isMatch = comparisonValues.indexOf(text) >= 0; - return isMatch !== negate; + const result = comparisonValues.indexOf(text) >= 0; + return result !== negate; } - private evaluateSensitiveIsOneOf(text: string, comparisonValues: ReadonlyArray, configJsonSalt: string, contextSalt: string, negate: boolean): boolean { + private evaluateSensitiveTextIsOneOf(text: string, comparisonValues: ReadonlyArray, configJsonSalt: string, contextSalt: string, negate: boolean): boolean { const hash = hashComparisonValue(text, configJsonSalt, contextSalt); // NOTE: Array.prototype.indexOf uses strict equality. - const isMatch = comparisonValues.indexOf(hash) >= 0; - return isMatch !== negate; + const result = comparisonValues.indexOf(hash) >= 0; + return result !== negate; } private evaluateTextSliceEqualsAnyOf(text: string, comparisonValues: ReadonlyArray, startsWith: boolean, negate: boolean): boolean { @@ -461,8 +463,8 @@ export class RolloutEvaluator implements IRolloutEvaluator { } // NOTE: String.prototype.startsWith/endsWith were introduced after ES5. We'd rather work around them instead of polyfilling them. - const isMatch = (startsWith ? text.lastIndexOf(item, 0) : text.indexOf(item, text.length - item.length)) >= 0; - if (isMatch) { + const result = (startsWith ? text.lastIndexOf(item, 0) : text.indexOf(item, text.length - item.length)) >= 0; + if (result) { return !negate; } } @@ -486,8 +488,8 @@ export class RolloutEvaluator implements IRolloutEvaluator { const sliceUtf8 = startsWith ? textUtf8.slice(0, sliceLength) : textUtf8.slice(textUtf8.length - sliceLength); const hash = hashComparisonValueSlice(sliceUtf8, configJsonSalt, contextSalt); - const isMatch = hash === item.slice(index + 1); - if (isMatch) { + const result = hash === item.slice(index + 1); + if (result) { return !negate; } } @@ -495,7 +497,7 @@ export class RolloutEvaluator implements IRolloutEvaluator { return negate; } - private evaluateContainsAnyOf(text: string, comparisonValues: ReadonlyArray, negate: boolean): boolean { + private evaluateTextContainsAnyOf(text: string, comparisonValues: ReadonlyArray, negate: boolean): boolean { for (let i = 0; i < comparisonValues.length; i++) { if (text.indexOf(comparisonValues[i]) >= 0) { return !negate; @@ -573,8 +575,8 @@ export class RolloutEvaluator implements IRolloutEvaluator { private evaluateArrayContainsAnyOf(array: ReadonlyArray, comparisonValues: ReadonlyArray, negate: boolean): boolean { for (let i = 0; i < array.length; i++) { // NOTE: Array.prototype.indexOf uses strict equality. - const isMatch = comparisonValues.indexOf(array[i]) >= 0; - if (isMatch) { + const result = comparisonValues.indexOf(array[i]) >= 0; + if (result) { return !negate; } } @@ -586,8 +588,8 @@ export class RolloutEvaluator implements IRolloutEvaluator { for (let i = 0; i < array.length; i++) { const hash = hashComparisonValue(array[i], configJsonSalt, contextSalt); // NOTE: Array.prototype.indexOf uses strict equality. - const isMatch = comparisonValues.indexOf(hash) >= 0; - if (isMatch) { + const result = comparisonValues.indexOf(hash) >= 0; + if (result) { return !negate; } } @@ -595,9 +597,9 @@ export class RolloutEvaluator implements IRolloutEvaluator { return negate; } - private evaluatePrerequisiteFlagCondition(condition: PrerequisiteFlagCondition, context: EvaluateContext): boolean | string { + private evaluatePrerequisiteFlagCondition(condition: PrerequisiteFlagCondition, context: EvaluateContext): boolean { const logBuilder = context.logBuilder; - logBuilder?.appendPrerequisiteFlagCondition(condition); + logBuilder?.appendPrerequisiteFlagCondition(condition, context.settings); const prerequisiteFlagKey = condition.prerequisiteFlagKey; const prerequisiteFlag = context.settings[prerequisiteFlagKey]; @@ -645,8 +647,8 @@ export class RolloutEvaluator implements IRolloutEvaluator { logBuilder?.newLine(`Prerequisite flag evaluation result: '${valueToString(prerequisiteFlagValue)}'.`) .newLine("Condition (") - .appendPrerequisiteFlagCondition(condition) - .append(") evaluates to ").appendEvaluationResult(result).append(".") + .appendPrerequisiteFlagCondition(condition, context.settings) + .append(") evaluates to ").appendConditionResult(result).append(".") .decreaseIndent() .newLine(")"); @@ -657,7 +659,7 @@ export class RolloutEvaluator implements IRolloutEvaluator { const logBuilder = context.logBuilder; logBuilder?.appendSegmentCondition(condition); - if (!context.userAttributes) { + if (!context.user) { if (!context.isMissingUserObjectLogged) { this.logger.userObjectIsMissing(context.key); context.isMissingUserObjectLogged = true; @@ -696,7 +698,7 @@ export class RolloutEvaluator implements IRolloutEvaluator { logBuilder.newLine("Condition (").appendSegmentCondition(condition).append(")"); (!isEvaluationError(result) - ? logBuilder.append(" evaluates to ").appendEvaluationResult(result) + ? logBuilder.append(" evaluates to ").appendConditionResult(result) : logBuilder.append(" failed to evaluate")) .append("."); @@ -713,10 +715,6 @@ function isEvaluationError(isMatchOrError: boolean | string): isMatchOrError is return typeof isMatchOrError === "string"; } -export function isStringArray(value: unknown): value is string[] { - return isArray(value) && !value.some(item => typeof item !== "string"); -} - function hashComparisonValue(value: string, configJsonSalt: string, contextSalt: string) { return hashComparisonValueSlice(utf8Encode(value), configJsonSalt, contextSalt); } @@ -756,7 +754,7 @@ function getUserAttributeValueAsNumber(attributeName: string, attributeValue: Us } let number: number; if (typeof attributeValue === "string" - && (!isNaN(number = parseFloatStrict(attributeValue.replace(",", "."))) || attributeValue === "NaN")) { + && (!isNaN(number = parseFloatStrict(attributeValue.replace(",", "."))) || attributeValue.trim() === "NaN")) { return number; } return handleInvalidUserAttribute(logger, condition, key, attributeName, `'${attributeValue}' is not a valid decimal number`); @@ -771,7 +769,7 @@ function getUserAttributeValueAsUnixTimeSeconds(attributeName: string, attribute } let number: number; if (typeof attributeValue === "string" - && (!isNaN(number = parseFloatStrict(attributeValue.replace(",", "."))) || attributeValue === "NaN")) { + && (!isNaN(number = 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)`); diff --git a/src/User.ts b/src/User.ts index 0e70205..bf7a1d0 100644 --- a/src/User.ts +++ b/src/User.ts @@ -51,8 +51,18 @@ export class User { } } -// NOTE: This could be an instance method of the User class, however formerly we suggested `const user = { ... }`-style initialization in the SDK docs, +// NOTE: These functions could be instance methods of the User class, however formerly we suggested `const user = { ... }`-style initialization in the SDK docs, // which would lead to "...is not a function" errors if we called functions on instances created that way as those don't have the correct prototype. + +export function getUserAttribute(user: User, name: string): UserAttributeValue | null | undefined { + switch (name) { + case "Identifier": return user.identifier ?? ""; + case "Email": return user.email; + case "Country": return user.country; + default: return user.custom?.[name]; + } +} + export function getUserAttributes(user: User): { [key: string]: UserAttributeValue } { const result: { [key: string]: UserAttributeValue } = {}; diff --git a/src/Utils.ts b/src/Utils.ts index 7264ec0..fc1760d 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -22,9 +22,8 @@ export function isArray(value: unknown): value is readonly unknown[] { return Array.isArray(value); } -export function isPromiseLike(obj: unknown): obj is PromiseLike { - // See also: https://stackoverflow.com/a/27746324/8656352 - return typeof (obj as PromiseLike)?.then === "function"; +export function isStringArray(value: unknown): value is string[] { + return isArray(value) && !value.some(item => typeof item !== "string"); } export function formatStringList(items: ReadonlyArray, maxLength = 0, getOmittedItemsText?: (count: number) => string, separator = ", "): string { @@ -38,13 +37,18 @@ export function formatStringList(items: ReadonlyArray, maxLength = 0, ge if (maxLength > 0 && length > maxLength) { items = items.slice(0, maxLength); if (getOmittedItemsText) { - appendix = getOmittedItemsText?.(length - maxLength); + appendix = getOmittedItemsText(length - maxLength); } } return "'" + items.join("'" + separator + "'") + "'" + appendix; } +export function isPromiseLike(obj: unknown): obj is PromiseLike { + // See also: https://stackoverflow.com/a/27746324/8656352 + return typeof (obj as PromiseLike)?.then === "function"; +} + export function utf8Encode(text: string): string { function codePointAt(text: string, index: number): number { const ch = text.charCodeAt(index); @@ -93,7 +97,7 @@ export function utf8Encode(text: string): string { } export function parseFloatStrict(value: unknown): number { - // NOTE: parseFloat is too forgiving, it allows leading/trailing whitespace and ignores invalid characters after the number. + // NOTE: JS's float to string conversion is too forgiving, it accepts hex numbers and ignores invalid characters after the number. if (typeof value === "number") { return value; diff --git a/src/index.ts b/src/index.ts index 793e37c..0db0a2c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -34,9 +34,10 @@ export function disposeAllClients(): void { /** * Creates an instance of `ConfigCatConsoleLogger`. * @param logLevel Log level (the minimum level to use for filtering log events). + * @param eol The character sequence to use for line breaks in log messages. Defaults to "\n". */ -export function createConsoleLogger(logLevel: LogLevel): IConfigCatLogger { - return new ConfigCatConsoleLogger(logLevel); +export function createConsoleLogger(logLevel: LogLevel, eol?: string): IConfigCatLogger { + return new ConfigCatConsoleLogger(logLevel, eol); } /** diff --git a/test/ConfigCatCacheTests.ts b/test/ConfigCatCacheTests.ts index edafa1f..645b703 100644 --- a/test/ConfigCatCacheTests.ts +++ b/test/ConfigCatCacheTests.ts @@ -94,8 +94,8 @@ describe("ConfigCatCache", () => { }); for (const [sdkKey, expectedCacheKey] of [ - ["test1", "7f845c43ecc95e202b91e271435935e6d1391e5d"], - ["test2", "a78b7e323ef543a272c74540387566a22415148a"], + ["configcat-sdk-1/TEST_KEY-0123456789012/1234567890123456789012", "f83ba5d45bceb4bb704410f51b704fb6dfa19942"], + ["configcat-sdk-1/TEST_KEY2-123456789012/1234567890123456789012", "da7bfd8662209c8ed3f9db96daed4f8d91ba5876"], ]) { it(`Cache key generation should be platform independent - ${sdkKey}`, () => { const options = new ManualPollOptions(sdkKey, "common", "1.0.0"); diff --git a/test/ConfigV2EvaluationTests.ts b/test/ConfigV2EvaluationTests.ts index 063e635..6f482a9 100644 --- a/test/ConfigV2EvaluationTests.ts +++ b/test/ConfigV2EvaluationTests.ts @@ -235,6 +235,7 @@ describe("Setting evaluation (config v2)", () => { ["configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", Infinity, ">5"], ["configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", NaN, "<>4.2"], ["configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "-Infinity", "<2.1"], + ["configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", " -Infinity ", "<2.1"], ["configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "-1", "<2.1"], ["configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "2", "<2.1"], ["configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "2.1", "<=2,1"], @@ -242,7 +243,9 @@ describe("Setting evaluation (config v2)", () => { ["configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "3", "<>4.2"], ["configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "5", ">=5"], ["configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "Infinity", ">5"], + ["configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", " Infinity ", ">5"], ["configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "NaN", "<>4.2"], + ["configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", " NaN ", "<>4.2"], ["configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "NaNa", "80%"], // Date time-based comparisons ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", new Date("2023-03-31T23:59:59.9990000Z"), false], @@ -265,12 +268,15 @@ describe("Setting evaluation (config v2)", () => { ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", 1682899199, true], ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", 1682899201, false], ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "-Infinity", false], + ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", " -Infinity ", false], ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "1680307199.999", false], ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "1680307200.001", true], ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "1682899199.999", true], ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "1682899200.001", false], ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "+Infinity", false], + ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", " +Infinity ", false], ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "NaN", false], + ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", " NaN ", false], // String array-based comparisons ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "stringArrayContainsAnyOfDogDefaultCat", "12345", "Custom1", ["x", "read"], "Dog"], ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "stringArrayContainsAnyOfDogDefaultCat", "12345", "Custom1", ["x", "Read"], "Cat"], @@ -293,4 +299,156 @@ describe("Setting evaluation (config v2)", () => { assert.strictEqual(evaluationDetails.value, expectedReturnValue); }); } + + for (const [key, customAttributeValue, expectedReturnValue] of + <[string, number | Date | ReadonlyArray, string][]>[ + ["numberToStringConversion", .12345, "1"], + ["numberToStringConversionInt", 125.0, "4"], + ["numberToStringConversionPositiveExp", -1.23456789e96, "2"], + ["numberToStringConversionNegativeExp", -12345.6789E-100, "4"], + ["numberToStringConversionNaN", NaN, "3"], + ["numberToStringConversionPositiveInf", Infinity, "4"], + ["numberToStringConversionNegativeInf", -Infinity, "3"], + ["dateToStringConversion", new Date("2023-03-31T23:59:59.9990000Z"), "3"], + ["dateToStringConversion", 1680307199.999, "3"], + ["dateToStringConversionNaN", NaN, "3"], + ["dateToStringConversionPositiveInf", Infinity, "1"], + ["dateToStringConversionNegativeInf", -Infinity, "5"], + ["stringArrayToStringConversion", ["read", "Write", " eXecute "], "4"], + ["stringArrayToStringConversionEmpty", [], "5"], + ["stringArrayToStringConversionSpecialChars", ["+<>%\"'\\/\t\r\n"], "3"], + ["stringArrayToStringConversionUnicode", ["äöüÄÖÜçéèñışğ⢙✓😀"], "2"], + ]) { + it(`Comparison attribute conversion to canonical string representation - key: ${key} | customAttributeValue: ${customAttributeValue}`, async () => { + const configLocation = new LocalFileConfigLocation("test", "data", "comparison_attribute_conversion.json"); + const config = await configLocation.fetchConfigAsync(); + + const fakeLogger = new FakeLogger(); + const logger = new LoggerWrapper(fakeLogger); + const evaluator = new RolloutEvaluator(logger); + + const user = new User("12345", void 0, void 0, { + ["Custom1"]: customAttributeValue, + }); + + const evaluationDetails = evaluate(evaluator, config.settings, key, "default", user!, null, logger); + + assert.strictEqual(evaluationDetails.value, expectedReturnValue); + }); + } + + for (const [key, expectedReturnValue] of + <[string, string][]>[ + ["isoneof", "no trim"], + ["isnotoneof", "no trim"], + ["isoneofhashed", "no trim"], + ["isnotoneofhashed", "no trim"], + ["equalshashed", "no trim"], + ["notequalshashed", "no trim"], + ["arraycontainsanyofhashed", "no trim"], + ["arraynotcontainsanyofhashed", "no trim"], + ["equals", "no trim"], + ["notequals", "no trim"], + ["startwithanyof", "no trim"], + ["notstartwithanyof", "no trim"], + ["endswithanyof", "no trim"], + ["notendswithanyof", "no trim"], + ["arraycontainsanyof", "no trim"], + ["arraynotcontainsanyof", "no trim"], + ["startwithanyofhashed", "no trim"], + ["notstartwithanyofhashed", "no trim"], + ["endswithanyofhashed", "no trim"], + ["notendswithanyofhashed", "no trim"], + //semver comparators user values trimmed because of backward compatibility + ["semverisoneof", "4 trim"], + ["semverisnotoneof", "5 trim"], + ["semverless", "6 trim"], + ["semverlessequals", "7 trim"], + ["semvergreater", "8 trim"], + ["semvergreaterequals", "9 trim"], + //number and date comparators user values trimmed because of backward compatibility + ["numberequals", "10 trim"], + ["numbernotequals", "11 trim"], + ["numberless", "12 trim"], + ["numberlessequals", "13 trim"], + ["numbergreater", "14 trim"], + ["numbergreaterequals", "15 trim"], + ["datebefore", "18 trim"], + ["dateafter", "19 trim"], + //"contains any of" and "not contains any of" is a special case, the not trimmed user attribute checked against not trimmed comparator values. + ["containsanyof", "no trim"], + ["notcontainsanyof", "no trim"], + ]) { + it(`Comparison attribute trimming - key: ${key}`, async () => { + const configLocation = new LocalFileConfigLocation("test", "data", "comparison_attribute_trimming.json"); + const config = await configLocation.fetchConfigAsync(); + + const fakeLogger = new FakeLogger(); + const logger = new LoggerWrapper(fakeLogger); + const evaluator = new RolloutEvaluator(logger); + + const user = new User(" 12345 ", void 0, "[\" USA \"]", { + ["Version"]: " 1.0.0 ", + ["Number"]: " 3 ", + ["Date"]: " 1705253400 " + }); + + const evaluationDetails = evaluate(evaluator, config.settings, key, "default", user!, null, logger); + + assert.strictEqual(evaluationDetails.value, expectedReturnValue); + }); + } + + for (const [key, expectedReturnValue] of + <[string, string][]>[ + ["isoneof", "no trim"], + ["isnotoneof", "no trim"], + ["containsanyof", "no trim"], + ["notcontainsanyof", "no trim"], + ["isoneofhashed", "no trim"], + ["isnotoneofhashed", "no trim"], + ["equalshashed", "no trim"], + ["notequalshashed", "no trim"], + ["arraycontainsanyofhashed", "no trim"], + ["arraynotcontainsanyofhashed", "no trim"], + ["equals", "no trim"], + ["notequals", "no trim"], + ["startwithanyof", "no trim"], + ["notstartwithanyof", "no trim"], + ["endswithanyof", "no trim"], + ["notendswithanyof", "no trim"], + ["arraycontainsanyof", "no trim"], + ["arraynotcontainsanyof", "no trim"], + ["startwithanyofhashed", "no trim"], + ["notstartwithanyofhashed", "no trim"], + ["endswithanyofhashed", "no trim"], + ["notendswithanyofhashed", "no trim"], + //semver comparator values trimmed because of backward compatibility + ["semverisoneof", "4 trim"], + ["semverisnotoneof", "5 trim"], + ["semverless", "6 trim"], + ["semverlessequals", "7 trim"], + ["semvergreater", "8 trim"], + ["semvergreaterequals", "9 trim"], + ]) { + it(`Comparison value trimming - key: ${key}`, async () => { + const configLocation = new LocalFileConfigLocation("test", "data", "comparison_value_trimming.json"); + const config = await configLocation.fetchConfigAsync(); + + const fakeLogger = new FakeLogger(); + const logger = new LoggerWrapper(fakeLogger); + const evaluator = new RolloutEvaluator(logger); + + const user = new User("12345", void 0, "[\"USA\"]", { + ["Version"]: "1.0.0", + ["Number"]: "3", + ["Date"]: "1705253400" + }); + + const evaluationDetails = evaluate(evaluator, config.settings, key, "default", user!, null, logger); + + assert.strictEqual(evaluationDetails.value, expectedReturnValue); + }); + } + }); diff --git a/test/UserTests.ts b/test/UserTests.ts index d38acab..ac80ab1 100644 --- a/test/UserTests.ts +++ b/test/UserTests.ts @@ -1,7 +1,7 @@ import { assert } from "chai"; import "mocha"; import { User } from "../src/index"; -import { WellKnownUserObjectAttribute, getUserAttributes } from "../src/User"; +import { WellKnownUserObjectAttribute, getUserAttribute, getUserAttributes } from "../src/User"; const identifierAttribute: WellKnownUserObjectAttribute = "Identifier"; const emailAttribute: WellKnownUserObjectAttribute = "Email"; @@ -51,6 +51,7 @@ describe("User Object", () => { const user = createUser(attributeName, attributeValue); assert.strictEqual(getUserAttributes(user)[attributeName], expectedValue); + assert.strictEqual(getUserAttribute(user, attributeName) ?? void 0, expectedValue); }); } @@ -66,8 +67,13 @@ describe("User Object", () => { // Assert assert.strictEqual(actualAttributes[identifierAttribute], "id"); + assert.strictEqual(getUserAttribute(user, identifierAttribute), "id"); + assert.strictEqual(actualAttributes[emailAttribute], "id@example.com"); + assert.strictEqual(getUserAttribute(user, emailAttribute), "id@example.com"); + assert.strictEqual(actualAttributes[countryAttribute], "US"); + assert.strictEqual(getUserAttribute(user, countryAttribute), "US"); assert.equal(3, Object.keys(actualAttributes).length); }); @@ -89,9 +95,16 @@ describe("User Object", () => { // Assert assert.strictEqual(actualAttributes[identifierAttribute], "id"); + assert.strictEqual(getUserAttribute(user, identifierAttribute), "id"); + assert.strictEqual(actualAttributes[emailAttribute], "id@example.com"); + assert.strictEqual(getUserAttribute(user, emailAttribute), "id@example.com"); + assert.strictEqual(actualAttributes[countryAttribute], "US"); + assert.strictEqual(getUserAttribute(user, countryAttribute), "US"); + assert.strictEqual(actualAttributes["myCustomAttribute"], "myCustomAttributeValue"); + assert.strictEqual(getUserAttribute(user, "myCustomAttribute"), "myCustomAttributeValue"); assert.equal(4, Object.keys(actualAttributes).length); }); @@ -119,6 +132,7 @@ describe("User Object", () => { // Assert assert.strictEqual(actualAttributes[attributeName], attributeValue); + assert.strictEqual(getUserAttribute(user, attributeName), attributeValue); assert.equal(4, Object.keys(actualAttributes).length); }); @@ -143,9 +157,16 @@ describe("User Object", () => { // Assert assert.strictEqual(actualAttributes[identifierAttribute], "id"); + assert.strictEqual(getUserAttribute(user, identifierAttribute), "id"); + assert.strictEqual(actualAttributes[emailAttribute], "id@example.com"); + assert.strictEqual(getUserAttribute(user, emailAttribute), "id@example.com"); + assert.strictEqual(actualAttributes[countryAttribute], "US"); + assert.strictEqual(getUserAttribute(user, countryAttribute), "US"); + assert.strictEqual(actualAttributes["myCustomAttribute"], "myCustomAttributeValue"); + assert.strictEqual(getUserAttribute(user, "myCustomAttribute"), "myCustomAttributeValue"); assert.equal(4, Object.keys(actualAttributes).length); }); diff --git a/test/UtilsTests.ts b/test/UtilsTests.ts index b9eb44c..d4566ec 100644 --- a/test/UtilsTests.ts +++ b/test/UtilsTests.ts @@ -20,6 +20,7 @@ describe("Utils", () => { [" ", NaN], ["NaN", NaN], ["Infinity", Infinity], + ["+Infinity", Infinity], ["-Infinity", -Infinity], ["1", 1], ["1 ", 1], @@ -41,6 +42,8 @@ describe("Utils", () => { ["1234567890.0", 1234567890], ["1234567890e0", 1234567890], [".1234567890", 0.1234567890], + ["+.1234567890", 0.1234567890], + ["-.1234567890", -0.1234567890], ["+0.123e-3", 0.000123], ["-0.123e+3", -123], ]) { diff --git a/test/data/comparison_attribute_conversion.json b/test/data/comparison_attribute_conversion.json new file mode 100644 index 0000000..5a900ae --- /dev/null +++ b/test/data/comparison_attribute_conversion.json @@ -0,0 +1,789 @@ +{ + "p": { + "u": "https://test-cdn-global.configcat.com", + "r": 0, + "s": "uM29sy1rjx71ze3ehr\u002BqCnoIpx8NZgL8V//MN7OL1aM=" + }, + "f": { + "numberToStringConversion": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "0.12345" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "numberToStringConversionInt": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "125" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "numberToStringConversionPositiveExp": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "-1.23456789e+96" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "numberToStringConversionNegativeExp": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "-1.23456789e-96" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "numberToStringConversionNaN": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "NaN" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "numberToStringConversionPositiveInf": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "Infinity" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "numberToStringConversionNegativeInf": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "-Infinity" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "dateToStringConversion": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "1680307199.999" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "dateToStringConversionNaN": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "NaN" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "dateToStringConversionPositiveInf": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "Infinity" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "dateToStringConversionNegativeInf": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "-Infinity" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "stringArrayToStringConversion": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "[\"read\",\"Write\",\" eXecute \"]" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "stringArrayToStringConversionEmpty": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "[]" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "stringArrayToStringConversionSpecialChars": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "[\"+<>%\\\"'\\\\/\\t\\r\\n\"]" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "stringArrayToStringConversionUnicode": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "[\"äöüÄÖÜçéèñışğ⢙✓😀\"]" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + } + } +} diff --git a/test/data/comparison_attribute_trimming.json b/test/data/comparison_attribute_trimming.json new file mode 100644 index 0000000..a42df5f --- /dev/null +++ b/test/data/comparison_attribute_trimming.json @@ -0,0 +1,985 @@ +{ + "p": { + "u": "https://test-cdn-eu.configcat.com", + "r": 0, + "s": "VjBfGYcmyHzLBv5EINgSBbX6/rYevYGWQhF3Zk5t8i4=" + }, + "f": { + "arraycontainsanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Country", + "c": 34, + "l": [ + "USA" + ] + } + } + ], + "s": { + "v": { + "s": "34 trim" + }, + "i": "99c90883" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "9c66d87c" + }, + "arraycontainsanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Country", + "c": 26, + "l": [ + "09d5761537a8136eb7fc45a53917b51cb9dcd2bb9b62ffa24ace0e8a7600a3c7" + ] + } + } + ], + "s": { + "v": { + "s": "26 trim" + }, + "i": "706c94b6" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "3b342be3" + }, + "arraynotcontainsanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Country", + "c": 35, + "l": [ + "USA" + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "4eeb2176" + } + } + ], + "v": { + "s": "35 trim" + }, + "i": "98bc8ebb" + }, + "arraynotcontainsanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Country", + "c": 27, + "l": [ + "99d06b6b3669b906803c285267f76fe4e2ccc194b00801ab07f2fd49939b6960" + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "8f248790" + } + } + ], + "v": { + "s": "27 trim" + }, + "i": "278ddbe9" + }, + "endswithanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 32, + "l": [ + "12345" + ] + } + } + ], + "s": { + "v": { + "s": "32 trim" + }, + "i": "0ac9e321" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "777456df" + }, + "endswithanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 24, + "l": [ + "5_7eb158c29b48b62cec860dffc459171edbfeef458bcc8e8bb62956d823eef3df" + ] + } + } + ], + "s": { + "v": { + "s": "24 trim" + }, + "i": "0364bf98" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "2f6fc77b" + }, + "equals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 28, + "s": "12345" + } + } + ], + "s": { + "v": { + "s": "28 trim" + }, + "i": "f2a682ca" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "0f806923" + }, + "equalshashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 20, + "s": "ea0d05859bb737105eea40bc605f6afd542c8f50f8497cd21ace38e731d7eef0" + } + } + ], + "s": { + "v": { + "s": "20 trim" + }, + "i": "6f1798e9" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "771ecd4d" + }, + "isnotoneof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 1, + "l": [ + "12345" + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "79d49e05" + } + } + ], + "v": { + "s": "1 trim" + }, + "i": "61d13448" + }, + "isnotoneofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 1, + "l": [ + "12345" + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "1c2df623" + } + } + ], + "v": { + "s": "17 trim" + }, + "i": "0bc3daa1" + }, + "isoneof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 0, + "l": [ + "12345" + ] + } + } + ], + "s": { + "v": { + "s": "0 trim" + }, + "i": "308f0749" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "90984858" + }, + "isoneofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 16, + "l": [ + "1765b470044971bbc19e7bed10112199c5da9c626455f86be109fef96e747911" + ] + } + } + ], + "s": { + "v": { + "s": "16 trim" + }, + "i": "cd78a85d" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "30b9483f" + }, + "notendswithanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 33, + "l": [ + "12345" + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "b0d7203e" + } + } + ], + "v": { + "s": "33 trim" + }, + "i": "89740c7e" + }, + "notendswithanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 25, + "l": [ + "5_2a338d3beb8ebe2e711d198420d04e2627e39501c2fcc7d5b3b8d93540691097" + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "059f59e3" + } + } + ], + "v": { + "s": "25 trim" + }, + "i": "c1e95c48" + }, + "notequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 29, + "s": "12345" + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "af1f1e95" + } + } + ], + "v": { + "s": "29 trim" + }, + "i": "219e6bac" + }, + "notequalshashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 21, + "s": "650fe0e8e86030b5f73ccd77e6532f307adf82506048a22f02d95386206ecea1" + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "9fe2b26b" + } + } + ], + "v": { + "s": "21 trim" + }, + "i": "9211e9f1" + }, + "notstartwithanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 31, + "l": [ + "12345" + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "ebe3ed2d" + } + } + ], + "v": { + "s": "31 trim" + }, + "i": "7deb7219" + }, + "notstartwithanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 23, + "l": [ + "5_586ab2ec61946cb1457d4af170d88e7f14e655d9debf352b4ab6bf5bf77df3f7" + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "7b606e54" + } + } + ], + "v": { + "s": "23 trim" + }, + "i": "edec740e" + }, + "semvergreater": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 8, + "s": "0.1.1" + } + } + ], + "s": { + "v": { + "s": "8 trim" + }, + "i": "25edfdc1" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "cb0224fd" + }, + "semvergreaterequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 9, + "s": "0.1.1" + } + } + ], + "s": { + "v": { + "s": "9 trim" + }, + "i": "d8960b43" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "530ea45c" + }, + "semverisnotoneof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 5, + "l": [ + "1.0.1" + ] + } + } + ], + "s": { + "v": { + "s": "5 trim" + }, + "i": "cb1bad57" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "4a7025a4" + }, + "semverisoneof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 4, + "l": [ + "1.0.0" + ] + } + } + ], + "s": { + "v": { + "s": "4 trim" + }, + "i": "6cc37494" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "842a56b5" + }, + "semverless": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 6, + "s": "1.0.1" + } + } + ], + "s": { + "v": { + "s": "6 trim" + }, + "i": "64c04b67" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "ae58de40" + }, + "semverlessequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 7, + "s": "1.0.1" + } + } + ], + "s": { + "v": { + "s": "7 trim" + }, + "i": "7c62748d" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "631a1888" + }, + "startwithanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 30, + "l": [ + "12345" + ] + } + } + ], + "s": { + "v": { + "s": "30 trim" + }, + "i": "475a9c4f" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "5a73105a" + }, + "startwithanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 22, + "l": [ + "5_67a323069ee45fef4ccd8365007d4713f7a3bc87764943b1139e8e50d1aee8fd" + ] + } + } + ], + "s": { + "v": { + "s": "22 trim" + }, + "i": "7650175d" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "a38edbee" + }, + "dateafter": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Date", + "c": 19, + "d": 1705251600 + } + } + ], + "s": { + "v": { + "s": "19 trim" + }, + "i": "83e580ce" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "1c12e0cc" + }, + "datebefore": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Date", + "c": 18, + "d": 1705255200 + } + } + ], + "s": { + "v": { + "s": "18 trim" + }, + "i": "34614b07" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "26d4f328" + }, + "numberequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Number", + "c": 10, + "d": 3 + } + } + ], + "s": { + "v": { + "s": "10 trim" + }, + "i": "6a8c0a08" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "7b8e49b9" + }, + "numbergreater": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Number", + "c": 14, + "d": 2 + } + } + ], + "s": { + "v": { + "s": "14 trim" + }, + "i": "2037a7a4" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "902f9bd9" + }, + "numbergreaterequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Number", + "c": 15, + "d": 2 + } + } + ], + "s": { + "v": { + "s": "15 trim" + }, + "i": "527c49d2" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "2280c961" + }, + "numberless": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Number", + "c": 12, + "d": 4 + } + } + ], + "s": { + "v": { + "s": "12 trim" + }, + "i": "c454f775" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "ec935943" + }, + "numberlessequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Number", + "c": 13, + "d": 4 + } + } + ], + "s": { + "v": { + "s": "13 trim" + }, + "i": "1e31aed8" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "1d53c679" + }, + "numbernotequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Number", + "c": 11, + "d": 6 + } + } + ], + "s": { + "v": { + "s": "11 trim" + }, + "i": "e8d7cf05" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "21c749a7" + }, + "containsanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 2, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "f750380a" + } + } + ], + "v": { + "s": "2 trim" + }, + "i": "c3ab37cf" + }, + "notcontainsanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 3, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "3 trim" + }, + "i": "4b8760c4" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "f91ecf16" + } + } +} \ No newline at end of file diff --git a/test/data/comparison_value_trimming.json b/test/data/comparison_value_trimming.json new file mode 100644 index 0000000..db91703 --- /dev/null +++ b/test/data/comparison_value_trimming.json @@ -0,0 +1,777 @@ +{ + "p": { + "u": "https://test-cdn-eu.configcat.com", + "r": 0, + "s": "zsVN1DQ9Oa2FjFc96MvPfMM5Vs+KKV00NyybJZipyf4=" + }, + "f": { + "arraycontainsanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Country", + "c": 34, + "l": [ + " USA " + ] + } + } + ], + "s": { + "v": { + "s": "34 trim" + }, + "i": "99c90883" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "9c66d87c" + }, + "arraycontainsanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Country", + "c": 26, + "l": [ + " 028fdb841bf3b2cc27fce407da08f87acd3a58a08c67d819cdb9351857b14237 " + ] + } + } + ], + "s": { + "v": { + "s": "26 trim" + }, + "i": "706c94b6" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "3b342be3" + }, + "arraynotcontainsanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Country", + "c": 35, + "l": [ + " USA " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "4eeb2176" + } + } + ], + "v": { + "s": "35 trim" + }, + "i": "98bc8ebb" + }, + "arraynotcontainsanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Country", + "c": 27, + "l": [ + " 60b747c290642863f9a6c68773ed309a9fb02c6c1ae65c77037046918f4c1d3c " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "8f248790" + } + } + ], + "v": { + "s": "27 trim" + }, + "i": "278ddbe9" + }, + "containsanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 2, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "2 trim" + }, + "i": "f750380a" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "c3ab37cf" + }, + "endswithanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 32, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "32 trim" + }, + "i": "0ac9e321" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "777456df" + }, + "endswithanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 24, + "l": [ + " 5_a6ce5e2838d4e0c27cd705c90f39e60d79056062983c39951668cf947ec406c2 " + ] + } + } + ], + "s": { + "v": { + "s": "24 trim" + }, + "i": "0364bf98" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "2f6fc77b" + }, + "equals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 28, + "s": " 12345 " + } + } + ], + "s": { + "v": { + "s": "28 trim" + }, + "i": "f2a682ca" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "0f806923" + }, + "equalshashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 20, + "s": " a2868640b1fe24c98e50b168756d83fd03779dd4349d6ddab5d7d6ef8dad13bd " + } + } + ], + "s": { + "v": { + "s": "20 trim" + }, + "i": "6f1798e9" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "771ecd4d" + }, + "isnotoneof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 1, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "79d49e05" + } + } + ], + "v": { + "s": "1 trim" + }, + "i": "61d13448" + }, + "isnotoneofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 1, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "1c2df623" + } + } + ], + "v": { + "s": "17 trim" + }, + "i": "0bc3daa1" + }, + "isoneof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 0, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "0 trim" + }, + "i": "308f0749" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "90984858" + }, + "isoneofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 16, + "l": [ + " 55ce90920d20fc0bf8078471062a85f82cc5ea2226012a901a5045775bace0f4 " + ] + } + } + ], + "s": { + "v": { + "s": "16 trim" + }, + "i": "cd78a85d" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "30b9483f" + }, + "notcontainsanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 3, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "4b8760c4" + } + } + ], + "v": { + "s": "3 trim" + }, + "i": "f91ecf16" + }, + "notendswithanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 33, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "b0d7203e" + } + } + ], + "v": { + "s": "33 trim" + }, + "i": "89740c7e" + }, + "notendswithanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 25, + "l": [ + " 5_c517fc957907e30b6a790540a20172a3a5d3a7458a85e340a7b1a1ac982be278 " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "059f59e3" + } + } + ], + "v": { + "s": "25 trim" + }, + "i": "c1e95c48" + }, + "notequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 29, + "s": " 12345 " + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "af1f1e95" + } + } + ], + "v": { + "s": "29 trim" + }, + "i": "219e6bac" + }, + "notequalshashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 21, + "s": " 31ceae14b865b0842e93fdc3a42a7e45780ccc41772ca9355db50e09d81e13ef " + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "9fe2b26b" + } + } + ], + "v": { + "s": "21 trim" + }, + "i": "9211e9f1" + }, + "notstartwithanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 31, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "ebe3ed2d" + } + } + ], + "v": { + "s": "31 trim" + }, + "i": "7deb7219" + }, + "notstartwithanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 23, + "l": [ + " 5_3643bbdd1bce4021fe4dbd55e6cc2f4902e4f50e592597d1a2d0e944fb7dfb42 " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "7b606e54" + } + } + ], + "v": { + "s": "23 trim" + }, + "i": "edec740e" + }, + "semvergreater": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 8, + "s": " 0.1.1 " + } + } + ], + "s": { + "v": { + "s": "8 trim" + }, + "i": "25edfdc1" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "cb0224fd" + }, + "semvergreaterequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 9, + "s": " 0.1.1 " + } + } + ], + "s": { + "v": { + "s": "9 trim" + }, + "i": "d8960b43" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "530ea45c" + }, + "semverisnotoneof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 5, + "l": [ + " 1.0.1 " + ] + } + } + ], + "s": { + "v": { + "s": "5 trim" + }, + "i": "cb1bad57" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "4a7025a4" + }, + "semverisoneof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 4, + "l": [ + " 1.0.0 " + ] + } + } + ], + "s": { + "v": { + "s": "4 trim" + }, + "i": "6cc37494" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "842a56b5" + }, + "semverless": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 6, + "s": " 1.0.1 " + } + } + ], + "s": { + "v": { + "s": "6 trim" + }, + "i": "64c04b67" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "ae58de40" + }, + "semverlessequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 7, + "s": " 1.0.1 " + } + } + ], + "s": { + "v": { + "s": "7 trim" + }, + "i": "7c62748d" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "631a1888" + }, + "startwithanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 30, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "30 trim" + }, + "i": "475a9c4f" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "5a73105a" + }, + "startwithanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 22, + "l": [ + " 5_3e052709552ca9d5bd6c459cb7ab0389f3210f6aafc3d006a2481635e9614a7c " + ] + } + } + ], + "s": { + "v": { + "s": "22 trim" + }, + "i": "7650175d" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "a38edbee" + } + } +} \ No newline at end of file diff --git a/test/data/evaluationlog/prerequisite_flag.json b/test/data/evaluationlog/prerequisite_flag.json index 674e2d3..9c35c00 100644 --- a/test/data/evaluationlog/prerequisite_flag.json +++ b/test/data/evaluationlog/prerequisite_flag.json @@ -30,6 +30,12 @@ }, "returnValue": "Horse", "expectedLog": "prerequisite_flag.txt" + }, + { + "key": "dependentFeatureMultipleLevels", + "defaultValue": "default", + "returnValue": "Dog", + "expectedLog": "prerequisite_flag_multilevel.txt" } ] } diff --git a/test/data/evaluationlog/prerequisite_flag/prerequisite_flag_multilevel.txt b/test/data/evaluationlog/prerequisite_flag/prerequisite_flag_multilevel.txt new file mode 100644 index 0000000..e9b9da6 --- /dev/null +++ b/test/data/evaluationlog/prerequisite_flag/prerequisite_flag_multilevel.txt @@ -0,0 +1,24 @@ +INFO [5000] Evaluating 'dependentFeatureMultipleLevels' + Evaluating targeting rules and applying the first match if any: + - IF Flag 'intermediateFeature' EQUALS 'true' + ( + Evaluating prerequisite flag 'intermediateFeature': + Evaluating targeting rules and applying the first match if any: + - IF Flag 'mainFeatureWithoutUserCondition' EQUALS 'true' + ( + Evaluating prerequisite flag 'mainFeatureWithoutUserCondition': + Prerequisite flag evaluation result: 'true'. + Condition (Flag 'mainFeatureWithoutUserCondition' EQUALS 'true') evaluates to true. + ) => true + AND Flag 'mainFeatureWithoutUserCondition' EQUALS 'true' + ( + Evaluating prerequisite flag 'mainFeatureWithoutUserCondition': + Prerequisite flag evaluation result: 'true'. + Condition (Flag 'mainFeatureWithoutUserCondition' EQUALS 'true') evaluates to true. + ) => true + THEN 'true' => MATCH, applying rule + Prerequisite flag evaluation result: 'true'. + Condition (Flag 'intermediateFeature' EQUALS 'true') evaluates to true. + ) + THEN 'Dog' => MATCH, applying rule + Returning 'Dog'. diff --git a/test/data/evaluationlog/segment.json b/test/data/evaluationlog/segment.json index 41744c2..1bb4df5 100644 --- a/test/data/evaluationlog/segment.json +++ b/test/data/evaluationlog/segment.json @@ -1,6 +1,6 @@ { - "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d9f207-6883-43e5-868c-cbf677af3fe6/244cf8b0-f604-11e8-b543-f23c917f9d8d", - "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/LcYz135LE0qbcacz2mgXnA", + "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08dbd6ca-a85f-4ed0-888a-2da18def92b5/244cf8b0-f604-11e8-b543-f23c917f9d8d", + "sdkKey": "configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/y_ZB7o-Xb0Swxth-ZlMSeA", "tests": [ { "key": "featureWithSegmentTargeting", @@ -8,6 +8,12 @@ "returnValue": false, "expectedLog": "segment_no_user.txt" }, + { + "key": "featureWithSegmentTargetingMultipleConditions", + "defaultValue": false, + "returnValue": false, + "expectedLog": "segment_no_user_multi_conditions.txt" + }, { "key": "featureWithNegatedSegmentTargetingCleartext", "defaultValue": false, diff --git a/test/data/evaluationlog/segment/segment_no_user_multi_conditions.txt b/test/data/evaluationlog/segment/segment_no_user_multi_conditions.txt new file mode 100644 index 0000000..3d547b5 --- /dev/null +++ b/test/data/evaluationlog/segment/segment_no_user_multi_conditions.txt @@ -0,0 +1,7 @@ +WARNING [3001] Cannot evaluate targeting rules and % options for setting 'featureWithSegmentTargetingMultipleConditions' (User Object is missing). You should pass a User Object to the evaluation methods like `getValueAsync()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'featureWithSegmentTargetingMultipleConditions' + Evaluating targeting rules and applying the first match if any: + - IF User IS IN SEGMENT 'Beta users (cleartext)' => false, skipping the remaining AND conditions + THEN 'true' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'false'.