diff --git a/src/RolloutEvaluator.ts b/src/RolloutEvaluator.ts index d39d0a4..63b9cb7 100644 --- a/src/RolloutEvaluator.ts +++ b/src/RolloutEvaluator.ts @@ -751,11 +751,12 @@ function getUserAttributeValueAsSemVer(attributeName: string, attributeValue: Us } function getUserAttributeValueAsNumber(attributeName: string, attributeValue: UserAttributeValue, condition: UserConditionUnion, key: string, logger: LoggerWrapper): number | string { - const number = - typeof attributeValue === "number" ? attributeValue : - typeof attributeValue === "string" ? parseFloatStrict(attributeValue.replace(",", ".")) : - NaN; - if (!isNaN(number)) { + if (typeof attributeValue === "number") { + return attributeValue; + } + let number: number; + if (typeof attributeValue === "string" + && (!isNaN(number = parseFloatStrict(attributeValue.replace(",", "."))) || attributeValue === "NaN")) { return number; } return handleInvalidUserAttribute(logger, condition, key, attributeName, `'${attributeValue}' is not a valid decimal number`); @@ -765,11 +766,9 @@ function getUserAttributeValueAsUnixTimeSeconds(attributeName: string, attribute if (attributeValue instanceof Date) { return attributeValue.getTime() / 1000; } - const number = - typeof attributeValue === "number" ? attributeValue : - typeof attributeValue === "string" ? parseFloatStrict(attributeValue.replace(",", ".")) : - NaN; - if (!isNaN(number)) { + let number: number; + if (typeof attributeValue === "string" + && (!isNaN(number = parseFloatStrict(attributeValue.replace(",", "."))) || attributeValue === "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 4dbae35..b337e0b 100644 --- a/src/User.ts +++ b/src/User.ts @@ -2,7 +2,12 @@ export type WellKnownUserObjectAttribute = "Identifier" | "Email" | "Country"; export type UserAttributeValue = string | number | Date | ReadonlyArray; -/** User Object. Contains user attributes which are used for evaluating targeting rules and percentage options. */ +/** + * User Object. Contains user attributes which are used for evaluating targeting rules and percentage options. + * @remarks + * Please note that the `User` class is not designed to be used as a DTO (data transfer object). + * (Since the type of the `custom` property is polymorphic, it's not guaranteed that deserializing a serialized instance produces an instance with an identical or even valid data content.) + **/ export class User { constructor( /** The unique identifier of the user or session (e.g. email address, primary key, session ID, etc.) */ @@ -11,7 +16,40 @@ export class User { public email?: string, /** Country of the user. */ public country?: string, - /** Custom attributes of the user for advanced targeting rule definitions (e.g. user role, subscription type, etc.) */ + /** + * Custom attributes of the user for advanced targeting rule definitions (e.g. user role, subscription type, etc.) + * @remarks + * The set of allowed attribute values depends on the comparison type of the condition which references the User Object attribute. + * `string` values are supported by all comparison types (in some cases they need to be provided in a specific format though). + * Some of the comparison types work with other types of values, as descibed below. + * + * Text-based comparisons (EQUALS, IS ONE OF, etc.)
+ * * accept `string` values,
+ * * all other values are automatically converted to string (a warning will be logged but evaluation will continue as normal). + * + * SemVer-based comparisons (IS ONE OF, <, >=, etc.)
+ * * accept `string` values containing a properly formatted, valid semver value,
+ * * all other values are considered invalid (a warning will be logged and the currently evaluated targeting rule will be skipped). + * + * Number-based comparisons (=, <, >=, etc.)
+ * * accept `number` values,
+ * * accept `string` values containing a properly formatted, valid `number` value,
+ * * all other values are considered invalid (a warning will be logged and the currently evaluated targeting rule will be skipped). + * + * Date time-based comparisons (BEFORE / AFTER)
+ * * accept `Date` values, which are automatically converted to a second-based Unix timestamp,
+ * * accept `number` values representing a second-based Unix timestamp,
+ * * accept `string` values containing a properly formatted, valid `number` value,
+ * * all other values are considered invalid (a warning will be logged and the currently evaluated targeting rule will be skipped). + * + * String array-based comparisons (ARRAY CONTAINS ANY OF / ARRAY NOT CONTAINS ANY OF)
+ * * accept arrays of `string`,
+ * * accept `string` values containing a valid JSON string which can be deserialized to an array of `string`,
+ * * all other values are considered invalid (a warning will be logged and the currently evaluated targeting rule will be skipped). + * + * In case a non-string attribute value needs to be converted to `string` during evaluation, it will always be done using the same format + * which is accepted by the comparisons. + **/ public custom: { [key: string]: UserAttributeValue } = {} ) { } diff --git a/test/ConfigV2EvaluationTests.ts b/test/ConfigV2EvaluationTests.ts index 8f9a93c..331e6bf 100644 --- a/test/ConfigV2EvaluationTests.ts +++ b/test/ConfigV2EvaluationTests.ts @@ -232,7 +232,7 @@ 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", NaN, "80%"], + ["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", "-1", "<2.1"], ["configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "2", "<2.1"], @@ -241,7 +241,8 @@ 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", "NaN", "80%"], + ["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], ["configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", new Date("2023-04-01T01:59:59.9990000+02:00"), false],