From bf531bf85f7ebcd503d3f5716812f97e6d06a3d3 Mon Sep 17 00:00:00 2001 From: vazarkevych Date: Mon, 26 Aug 2024 12:42:31 +0300 Subject: [PATCH 1/3] Added saved groups, added $inGroup $notInGroup operators, removed versionCompare test cases --- growthbook/growthbook.py | 88 +- tests/cases.json | 4409 ++++++++++++++++++++++++++++++++------ tests/test_growthbook.py | 23 +- 3 files changed, 3756 insertions(+), 764 deletions(-) diff --git a/growthbook/growthbook.py b/growthbook/growthbook.py index d7adcef..5763a84 100644 --- a/growthbook/growthbook.py +++ b/growthbook/growthbook.py @@ -162,36 +162,36 @@ def isIn(conditionValue, attributeValue) -> bool: return attributeValue in conditionValue -def evalCondition(attributes: dict, condition: dict) -> bool: +def evalCondition(attributes: dict, condition: dict, savedGroups: dict = None) -> bool: if "$or" in condition: - return evalOr(attributes, condition["$or"]) + return evalOr(attributes, condition["$or"], savedGroups) if "$nor" in condition: - return not evalOr(attributes, condition["$nor"]) + return not evalOr(attributes, condition["$nor"], savedGroups) if "$and" in condition: - return evalAnd(attributes, condition["$and"]) + return evalAnd(attributes, condition["$and"], savedGroups) if "$not" in condition: - return not evalCondition(attributes, condition["$not"]) + return not evalCondition(attributes, condition["$not"], savedGroups) for key, value in condition.items(): - if not evalConditionValue(value, getPath(attributes, key)): + if not evalConditionValue(value, getPath(attributes, key), savedGroups): return False return True -def evalOr(attributes, conditions) -> bool: +def evalOr(attributes, conditions, savedGroups) -> bool: if len(conditions) == 0: return True for condition in conditions: - if evalCondition(attributes, condition): + if evalCondition(attributes, condition, savedGroups): return True return False -def evalAnd(attributes, conditions) -> bool: +def evalAnd(attributes, conditions, savedGroups) -> bool: for condition in conditions: - if not evalCondition(attributes, condition): + if not evalCondition(attributes, condition, savedGroups): return False return True @@ -231,25 +231,25 @@ def getPath(attributes, path): return current -def evalConditionValue(conditionValue, attributeValue) -> bool: +def evalConditionValue(conditionValue, attributeValue, savedGroups) -> bool: if type(conditionValue) is dict and isOperatorObject(conditionValue): for key, value in conditionValue.items(): - if not evalOperatorCondition(key, attributeValue, value): + if not evalOperatorCondition(key, attributeValue, value, savedGroups): return False return True return conditionValue == attributeValue -def elemMatch(condition, attributeValue) -> bool: +def elemMatch(condition, attributeValue, savedGroups) -> bool: if not type(attributeValue) is list: return False for item in attributeValue: if isOperatorObject(condition): - if evalConditionValue(condition, item): + if evalConditionValue(condition, item, savedGroups): return True else: - if evalCondition(item, condition): + if evalCondition(item, condition, savedGroups): return True return False @@ -275,7 +275,7 @@ def compare(val1, val2) -> int: return 0 -def evalOperatorCondition(operator, attributeValue, conditionValue) -> bool: +def evalOperatorCondition(operator, attributeValue, conditionValue, savedGroups) -> bool: if operator == "$eq": try: return compare(attributeValue, conditionValue) == 0 @@ -318,6 +318,18 @@ def evalOperatorCondition(operator, attributeValue, conditionValue) -> bool: return paddedVersionString(attributeValue) > paddedVersionString(conditionValue) elif operator == "$vgte": return paddedVersionString(attributeValue) >= paddedVersionString(conditionValue) + elif operator == "$inGroup": + if not type(conditionValue) is str: + return False + if not conditionValue in savedGroups: + return False + return isIn(savedGroups[conditionValue] or [], attributeValue) + elif operator == "$notInGroup": + if not type(conditionValue) is str: + return False + if not conditionValue in savedGroups: + return True + return not isIn(savedGroups[conditionValue] or [], attributeValue) elif operator == "$regex": try: r = re.compile(conditionValue) @@ -333,18 +345,18 @@ def evalOperatorCondition(operator, attributeValue, conditionValue) -> bool: return False return not isIn(conditionValue, attributeValue) elif operator == "$elemMatch": - return elemMatch(conditionValue, attributeValue) + return elemMatch(conditionValue, attributeValue, savedGroups) elif operator == "$size": if not (type(attributeValue) is list): return False - return evalConditionValue(conditionValue, len(attributeValue)) + return evalConditionValue(conditionValue, len(attributeValue), savedGroups) elif operator == "$all": if not (type(attributeValue) is list): return False for cond in conditionValue: passing = False for attr in attributeValue: - if evalConditionValue(cond, attr): + if evalConditionValue(cond, attr, savedGroups): passing = True if not passing: return False @@ -356,7 +368,7 @@ def evalOperatorCondition(operator, attributeValue, conditionValue) -> bool: elif operator == "$type": return getType(attributeValue) == conditionValue elif operator == "$not": - return not evalConditionValue(conditionValue, attributeValue) + return not evalConditionValue(conditionValue, attributeValue, savedGroups) return False @@ -893,7 +905,7 @@ def _fetch_features( if not decryption_key: raise ValueError("Must specify decryption_key") try: - decrypted = decrypt(decoded["encryptedFeatures"], decryption_key) + decrypted = decrypt(decoded['encryptedFeatures'], decryption_key) return json.loads(decrypted) except Exception: logger.warning( @@ -901,7 +913,7 @@ def _fetch_features( ) return None elif "features" in decoded: - return decoded["features"] + return decoded else: logger.warning("GrowthBook API response missing features") return None @@ -917,15 +929,17 @@ async def _fetch_features_async( if not decryption_key: raise ValueError("Must specify decryption_key") try: - decrypted = decrypt(decoded["encryptedFeatures"], decryption_key) - return json.loads(decrypted) + decryptedFeatures = decrypt(decoded["encryptedFeatures"], decryption_key) + decoded['features'] = json.loads(decryptedFeatures) + del decoded['encryptedFeatures'] + return decoded except Exception: logger.warning( "Failed to decrypt features from GrowthBook API response" ) return None elif "features" in decoded: - return decoded["features"] + return decoded else: logger.warning("GrowthBook API response missing features") return None @@ -956,6 +970,7 @@ def __init__( forced_variations: dict = {}, sticky_bucket_service: AbstractStickyBucketService = None, sticky_bucket_identifier_attributes: List[str] = None, + savedGroups: dict = {}, # Deprecated args trackingCallback=None, qaMode: bool = False, @@ -968,6 +983,7 @@ def __init__( self._attributes = attributes self._url = url self._features: Dict[str, Feature] = {} + self._saved_groups = savedGroups self._api_host = api_host self._client_key = client_key self._decryption_key = decryption_key @@ -998,11 +1014,14 @@ def load_features(self) -> None: if not self._client_key: raise ValueError("Must specify `client_key` to refresh features") - features = feature_repo.load_features( + response = feature_repo.load_features( self._api_host, self._client_key, self._decryption_key, self._cache_ttl ) - if features is not None: - self.setFeatures(features) + if response is not None and "features" in response.keys(): + self.setFeatures(response["features"]) + + if response is not None and "savedGroups" in response: + self._saved_groups = response["savedGroups"] async def load_features_async(self) -> None: if not self._client_key: @@ -1011,8 +1030,11 @@ async def load_features_async(self) -> None: features = await feature_repo.load_features_async( self._api_host, self._client_key, self._decryption_key, self._cache_ttl ) - if features is not None: - self.setFeatures(features) + if features is not None and "features" in features: + self.setFeatures(features["features"]) + + if features is not None and "savedGroups" in features: + self._saved_groups = features["savedGroups"] # @deprecated, use set_features def setFeatures(self, features: dict) -> None: @@ -1096,7 +1118,7 @@ def eval_prereqs(self, parentConditions: List[dict], stack: Set[str]) -> str: if parentRes.source == "cyclicPrerequisite": return "cyclic" - if not evalCondition({'value': parentRes.value}, parentCondition.get("condition", None)): + if not evalCondition({'value': parentRes.value}, parentCondition.get("condition", None), self._saved_groups): if parentCondition.get("gate", False): return "gate" return "fail" @@ -1132,7 +1154,7 @@ def _eval_feature(self, key: str, stack: Set[str]) -> FeatureResult: continue if rule.condition: - if not evalCondition(self._attributes, rule.condition): + if not evalCondition(self._attributes, rule.condition, self._saved_groups): logger.debug( "Skip rule because of failed condition, feature %s", key ) @@ -1412,7 +1434,7 @@ def _run(self, experiment: Experiment, featureId: Optional[str] = None) -> Resul # 8. Exclude if condition is false if experiment.condition and not evalCondition( - self._attributes, experiment.condition + self._attributes, experiment.condition, self._saved_groups ): logger.debug( "Skip experiment %s because user failed the condition", experiment.key diff --git a/tests/cases.json b/tests/cases.json index 7d3fc38..c398d5c 100644 --- a/tests/cases.json +++ b/tests/cases.json @@ -63,24 +63,32 @@ "$and": [ { "$groups": { - "$elemMatch": { "$eq": "a" } + "$elemMatch": { + "$eq": "a" + } } }, { "$groups": { - "$elemMatch": { "$eq": "b" } + "$elemMatch": { + "$eq": "b" + } } }, { "$or": [ { "$groups": { - "$elemMatch": { "$eq": "c" } + "$elemMatch": { + "$eq": "c" + } } }, { "$groups": { - "$elemMatch": { "$eq": "e" } + "$elemMatch": { + "$eq": "e" + } } } ] @@ -88,21 +96,30 @@ { "$not": { "$groups": { - "$elemMatch": { "$eq": "f" } + "$elemMatch": { + "$eq": "f" + } } } }, { "$not": { "$groups": { - "$elemMatch": { "$eq": "g" } + "$elemMatch": { + "$eq": "g" + } } } } ] }, { - "$groups": ["a", "b", "c", "d"] + "$groups": [ + "a", + "b", + "c", + "d" + ] }, true ], @@ -112,24 +129,32 @@ "$and": [ { "$groups": { - "$elemMatch": { "$eq": "a" } + "$elemMatch": { + "$eq": "a" + } } }, { "$groups": { - "$elemMatch": { "$eq": "b" } + "$elemMatch": { + "$eq": "b" + } } }, { "$or": [ { "$groups": { - "$elemMatch": { "$eq": "c" } + "$elemMatch": { + "$eq": "c" + } } }, { "$groups": { - "$elemMatch": { "$eq": "e" } + "$elemMatch": { + "$eq": "e" + } } } ] @@ -137,21 +162,30 @@ { "$not": { "$groups": { - "$elemMatch": { "$eq": "d" } + "$elemMatch": { + "$eq": "d" + } } } }, { "$not": { "$groups": { - "$elemMatch": { "$eq": "g" } + "$elemMatch": { + "$eq": "g" + } } } } ] }, { - "$groups": ["a", "b", "c", "d"] + "$groups": [ + "a", + "b", + "c", + "d" + ] }, false ], @@ -389,7 +423,11 @@ "$in - pass", { "num": { - "$in": [1, 2, 3] + "$in": [ + 1, + 2, + 3 + ] } }, { @@ -401,7 +439,11 @@ "$in - fail", { "num": { - "$in": [1, 2, 3] + "$in": [ + 1, + 2, + 3 + ] } }, { @@ -425,11 +467,18 @@ "$in - array pass 1", { "tags": { - "$in": ["a", "b"] + "$in": [ + "a", + "b" + ] } }, { - "tags": ["d", "e", "a"] + "tags": [ + "d", + "e", + "a" + ] }, true ], @@ -437,11 +486,18 @@ "$in - array pass 2", { "tags": { - "$in": ["a", "b"] + "$in": [ + "a", + "b" + ] } }, { - "tags": ["d", "b", "f"] + "tags": [ + "d", + "b", + "f" + ] }, true ], @@ -449,11 +505,18 @@ "$in - array pass 3", { "tags": { - "$in": ["a", "b"] + "$in": [ + "a", + "b" + ] } }, { - "tags": ["d", "b", "a"] + "tags": [ + "d", + "b", + "a" + ] }, true ], @@ -461,11 +524,18 @@ "$in - array fail 1", { "tags": { - "$in": ["a", "b"] + "$in": [ + "a", + "b" + ] } }, { - "tags": ["d", "e", "f"] + "tags": [ + "d", + "e", + "f" + ] }, false ], @@ -473,7 +543,10 @@ "$in - array fail 2", { "tags": { - "$in": ["a", "b"] + "$in": [ + "a", + "b" + ] } }, { @@ -485,7 +558,11 @@ "$nin - pass", { "num": { - "$nin": [1, 2, 3] + "$nin": [ + 1, + 2, + 3 + ] } }, { @@ -497,7 +574,11 @@ "$nin - fail", { "num": { - "$nin": [1, 2, 3] + "$nin": [ + 1, + 2, + 3 + ] } }, { @@ -521,11 +602,18 @@ "$nin - array fail 1", { "tags": { - "$nin": ["a", "b"] + "$nin": [ + "a", + "b" + ] } }, { - "tags": ["d", "e", "a"] + "tags": [ + "d", + "e", + "a" + ] }, false ], @@ -533,11 +621,18 @@ "$nin - array fail 2", { "tags": { - "$nin": ["a", "b"] + "$nin": [ + "a", + "b" + ] } }, { - "tags": ["d", "b", "f"] + "tags": [ + "d", + "b", + "f" + ] }, false ], @@ -545,11 +640,18 @@ "$nin - array fail 3", { "tags": { - "$nin": ["a", "b"] + "$nin": [ + "a", + "b" + ] } }, { - "tags": ["d", "b", "a"] + "tags": [ + "d", + "b", + "a" + ] }, false ], @@ -557,11 +659,18 @@ "$nin - array pass 1", { "tags": { - "$nin": ["a", "b"] + "$nin": [ + "a", + "b" + ] } }, { - "tags": ["d", "e", "f"] + "tags": [ + "d", + "e", + "f" + ] }, true ], @@ -569,7 +678,10 @@ "$nin - array pass 2", { "tags": { - "$nin": ["a", "b"] + "$nin": [ + "a", + "b" + ] } }, { @@ -587,7 +699,12 @@ } }, { - "nums": [0, 5, -20, 15] + "nums": [ + 0, + 5, + -20, + 15 + ] }, true ], @@ -601,7 +718,12 @@ } }, { - "nums": [0, 5, -20, 8] + "nums": [ + 0, + 5, + -20, + 8 + ] }, false ], @@ -609,7 +731,9 @@ "missing attribute - fail", { "pets.dog.name": { - "$in": ["fido"] + "$in": [ + "fido" + ] } }, { @@ -1063,7 +1187,10 @@ } }, { - "a": [1, 2] + "a": [ + 1, + 2 + ] }, true ], @@ -1118,7 +1245,9 @@ [ "$size empty - pass", { - "tags": { "$size": 0 } + "tags": { + "$size": 0 + } }, { "tags": [] @@ -1128,10 +1257,14 @@ [ "$size empty - fail", { - "tags": { "$size": 0 } + "tags": { + "$size": 0 + } }, { - "tags": [10] + "tags": [ + 10 + ] }, false ], @@ -1143,7 +1276,11 @@ } }, { - "tags": ["a", "b", "c"] + "tags": [ + "a", + "b", + "c" + ] }, true ], @@ -1155,7 +1292,10 @@ } }, { - "tags": ["a", "b"] + "tags": [ + "a", + "b" + ] }, false ], @@ -1167,7 +1307,12 @@ } }, { - "tags": ["a", "b", "c", "d"] + "tags": [ + "a", + "b", + "c", + "d" + ] }, false ], @@ -1193,7 +1338,11 @@ } }, { - "tags": [0, 1, 2] + "tags": [ + 0, + 1, + 2 + ] }, true ], @@ -1207,7 +1356,10 @@ } }, { - "tags": [0, 1] + "tags": [ + 0, + 1 + ] }, false ], @@ -1221,7 +1373,9 @@ } }, { - "tags": [0] + "tags": [ + 0 + ] }, false ], @@ -1235,7 +1389,11 @@ } }, { - "tags": ["foo", "bar", "baz"] + "tags": [ + "foo", + "bar", + "baz" + ] }, true ], @@ -1249,7 +1407,10 @@ } }, { - "tags": ["foo", "baz"] + "tags": [ + "foo", + "baz" + ] }, false ], @@ -1258,12 +1419,19 @@ { "tags": { "$elemMatch": { - "$in": ["a", "b"] + "$in": [ + "a", + "b" + ] } } }, { - "tags": ["d", "e", "b"] + "tags": [ + "d", + "e", + "b" + ] }, true ], @@ -1272,12 +1440,19 @@ { "tags": { "$elemMatch": { - "$in": ["a", "b"] + "$in": [ + "a", + "b" + ] } } }, { - "tags": ["d", "e", "f"] + "tags": [ + "d", + "e", + "f" + ] }, false ], @@ -1293,7 +1468,10 @@ } }, { - "tags": ["foo", "baz"] + "tags": [ + "foo", + "baz" + ] }, true ], @@ -1309,7 +1487,11 @@ } }, { - "tags": ["foo", "bar", "baz"] + "tags": [ + "foo", + "bar", + "baz" + ] }, false ], @@ -1410,11 +1592,18 @@ "$all - pass", { "tags": { - "$all": ["one", "three"] + "$all": [ + "one", + "three" + ] } }, { - "tags": ["one", "two", "three"] + "tags": [ + "one", + "two", + "three" + ] }, true ], @@ -1422,11 +1611,18 @@ "$all - fail", { "tags": { - "$all": ["one", "three"] + "$all": [ + "one", + "three" + ] } }, { - "tags": ["one", "two", "four"] + "tags": [ + "one", + "two", + "four" + ] }, false ], @@ -1434,7 +1630,10 @@ "$all - fail not array", { "tags": { - "$all": ["one", "three"] + "$all": [ + "one", + "three" + ] } }, { @@ -1525,47 +1724,74 @@ [ "equals array - pass", { - "tags": ["hello", "world"] + "tags": [ + "hello", + "world" + ] }, { - "tags": ["hello", "world"] + "tags": [ + "hello", + "world" + ] }, true ], [ "equals array - fail order", { - "tags": ["hello", "world"] + "tags": [ + "hello", + "world" + ] }, { - "tags": ["world", "hello"] + "tags": [ + "world", + "hello" + ] }, false ], [ "equals array - fail missing item", { - "tags": ["hello", "world"] + "tags": [ + "hello", + "world" + ] }, { - "tags": ["hello"] + "tags": [ + "hello" + ] }, false ], [ "equals array - fail extra item", { - "tags": ["hello", "world"] + "tags": [ + "hello", + "world" + ] }, { - "tags": ["hello", "world", "foo"] + "tags": [ + "hello", + "world", + "foo" + ] }, false ], [ "equals array - fail type mismatch", { - "tags": ["hello", "world"] + "tags": [ + "hello", + "world" + ] }, { "tags": "hello world" @@ -2019,257 +2245,1357 @@ "v": "1.2.3-alpha" }, true - ] - ], - "versionCompare": { - "lt": [ - ["0.9.99", "1.0.0", true], - ["0.9.0", "0.10.0", true], - ["1.0.0-0.0", "1.0.0-0.0.0", true], - ["1.0.0-9999", "1.0.0--", true], - ["1.0.0-99", "1.0.0-100", true], - ["1.0.0-alpha", "1.0.0-alpha.1", true], - ["1.0.0-alpha.1", "1.0.0-alpha.beta", true], - ["1.0.0-alpha.beta", "1.0.0-beta", true], - ["1.0.0-beta", "1.0.0-beta.2", true], - ["1.0.0-beta.2", "1.0.0-beta.11", true], - ["1.0.0-beta.11", "1.0.0-rc.1", true], - ["1.0.0-rc.1", "1.0.0", true], - ["1.0.0-0", "1.0.0--1", true], - ["1.0.0-0", "1.0.0-1", true], - ["1.0.0-1.0", "1.0.0-1.-1", true] - ], - "gt": [ - ["0.0.0", "0.0.0-foo", true], - ["0.0.1", "0.0.0", true], - ["1.0.0", "0.9.9", true], - ["0.10.0", "0.9.0", true], - ["0.99.0", "0.10.0", true], - ["2.0.0", "1.2.3", true], - ["v0.0.0", "0.0.0-foo", true], - ["v0.0.1", "0.0.0", true], - ["v1.0.0", "0.9.9", true], - ["v0.10.0", "0.9.0", true], - ["v0.99.0", "0.10.0", true], - ["v2.0.0", "1.2.3", true], - ["0.0.0", "v0.0.0-foo", true], - ["0.0.1", "v0.0.0", true], - ["1.0.0", "v0.9.9", true], - ["0.10.0", "v0.9.0", true], - ["0.99.0", "v0.10.0", true], - ["2.0.0", "v1.2.3", true], - ["1.2.3", "1.2.3-asdf", true], - ["1.2.3", "1.2.3-4", true], - ["1.2.3", "1.2.3-4-foo", true], - ["1.2.3-5-foo", "1.2.3-5", true], - ["1.2.3-5", "1.2.3-4", true], - ["1.2.3-5-foo", "1.2.3-5-Foo", true], - ["3.0.0", "2.7.2+asdf", true], - ["1.2.3-a.10", "1.2.3-a.5", true], - ["1.2.3-a.b", "1.2.3-a.5", true], - ["1.2.3-a.b", "1.2.3-a", true], - ["1.2.3-a.b.c", "1.2.3-a.b.c.d", false], - ["1.2.3-a.b.c.10.d.5", "1.2.3-a.b.c.5.d.100", true], - ["1.2.3-r2", "1.2.3-r100", true], - ["1.2.3-r100", "1.2.3-R2", true], - ["a.b.c.d.e.f", "1.2.3", true], - ["10.0.0", "9.0.0", true], - ["10000.0.0", "9999.0.0", true] - ], - "eq": [ - ["1.2.3", "1.2.3", true], - ["1.2.3", "v1.2.3", true], - ["1.2.3-0", "v1.2.3-0", true], - ["1.2.3-1", "1.2.3-1", true], - ["1.2.3-1", "v1.2.3-1", true], - ["1.2.3-beta", "1.2.3-beta", true], - ["1.2.3-beta", "v1.2.3-beta", true], - ["1.2.3-beta+build", "1.2.3-beta+otherbuild", true], - ["1.2.3-beta+build", "v1.2.3-beta+otherbuild", true], - ["1-2-3", "1.2.3", true], - ["1-2-3", "1-2.3+build99", true], - ["1-2-3", "v1.2.3", true], - ["1.2.3.4", "1.2.3-4", true] - ] - }, - "hash": [ - ["", "a", 1, 0.22], - ["", "b", 1, 0.077], - ["b", "a", 1, 0.946], - ["ef", "d", 1, 0.652], - ["asdf", "8952klfjas09ujk", 1, 0.549], - ["", "123", 1, 0.011], - ["", "___)((*\":&", 1, 0.563], - ["seed", "a", 2, 0.0505], - ["seed", "b", 2, 0.2696], - ["foo", "ab", 2, 0.2575], - ["foo", "def", 2, 0.2019], - ["89123klj", "8952klfjas09ujkasdf", 2, 0.124], - ["90850943850283058242805", "123", 2, 0.7516], - ["()**(%$##$%#$#", "___)((*\":&", 2, 0.0128], - ["abc", "def", 99, null] - ], - "getBucketRange": [ - [ - "normal 50/50", - [2, 1, null], - [ - [0, 0.5], - [0.5, 1] - ] ], [ - "reduced coverage", - [2, 0.5, null], - [ - [0, 0.25], - [0.5, 0.75] - ] + "version 0.9.99 < 1.0.0", + { + "version": { + "$vlt": "1.0.0" + } + }, + { + "version": "0.9.99" + }, + true ], [ - "zero coverage", - [2, 0, null], - [ - [0, 0], - [0.5, 0.5] - ] + "version 0.9.0 < 0.10.0", + { + "version": { + "$vlt": "0.10.0" + } + }, + { + "version": "0.9.0" + }, + true ], [ - "4 variations", - [4, 1, null], - [ - [0, 0.25], - [0.25, 0.5], - [0.5, 0.75], - [0.75, 1] - ] + "version 1.0.0-0.0 < 1.0.0-0.0.0", + { + "version": { + "$vlt": "1.0.0-0.0.0" + } + }, + { + "version": "1.0.0-0.0" + }, + true ], [ - "uneven weights", - [2, 1, [0.4, 0.6]], - [ - [0, 0.4], - [0.4, 1] - ] + "version 1.0.0-9999 < 1.0.0--", + { + "version": { + "$vlt": "1.0.0--" + } + }, + { + "version": "1.0.0-9999" + }, + true ], [ - "uneven weights, 3 variations", - [3, 1, [0.2, 0.3, 0.5]], - [ - [0, 0.2], - [0.2, 0.5], - [0.5, 1] - ] + "version 1.0.0-99 < 1.0.0-100", + { + "version": { + "$vlt": "1.0.0-100" + } + }, + { + "version": "1.0.0-99" + }, + true ], [ - "uneven weights, reduced coverage, 3 variations", - [3, 0.2, [0.2, 0.3, 0.5]], - [ - [0, 0.04], - [0.2, 0.26], - [0.5, 0.6] - ] + "version 1.0.0-alpha < 1.0.0-alpha.1", + { + "version": { + "$vlt": "1.0.0-alpha.1" + } + }, + { + "version": "1.0.0-alpha" + }, + true ], [ - "negative coverage", - [2, -0.2, null], - [ - [0, 0], - [0.5, 0.5] - ] + "version 1.0.0-alpha.1 < 1.0.0-alpha.beta", + { + "version": { + "$vlt": "1.0.0-alpha.beta" + } + }, + { + "version": "1.0.0-alpha.1" + }, + true ], [ - "coverage above 1", - [2, 1.5, null], - [ - [0, 0.5], - [0.5, 1] - ] + "version 1.0.0-alpha.beta < 1.0.0-beta", + { + "version": { + "$vlt": "1.0.0-beta" + } + }, + { + "version": "1.0.0-alpha.beta" + }, + true ], [ - "weights sum below 1", - [2, 1, [0.4, 0.1]], - [ - [0, 0.5], - [0.5, 1] - ] + "version 1.0.0-beta < 1.0.0-beta.2", + { + "version": { + "$vlt": "1.0.0-beta.2" + } + }, + { + "version": "1.0.0-beta" + }, + true ], [ - "weights sum above 1", - [2, 1, [0.7, 0.6]], - [ - [0, 0.5], - [0.5, 1] - ] + "version 1.0.0-beta.2 < 1.0.0-beta.11", + { + "version": { + "$vlt": "1.0.0-beta.11" + } + }, + { + "version": "1.0.0-beta.2" + }, + true ], [ - "weights.length not equal to num variations", - [4, 1, [0.4, 0.4, 0.2]], - [ - [0, 0.25], - [0.25, 0.5], - [0.5, 0.75], - [0.75, 1] - ] + "version 1.0.0-beta.11 < 1.0.0-rc.1", + { + "version": { + "$vlt": "1.0.0-rc.1" + } + }, + { + "version": "1.0.0-beta.11" + }, + true ], [ - "weights sum almost equals 1", - [2, 1, [0.4, 0.5999]], - [ - [0, 0.4], - [0.4, 0.9999] - ] - ] - ], - "feature": [ - [ - "unknown feature key", - {}, - "my-feature", + "version 1.0.0-rc.1 < 1.0.0", { - "value": null, - "on": false, - "off": true, - "source": "unknownFeature" - } + "version": { + "$vlt": "1.0.0" + } + }, + { + "version": "1.0.0-rc.1" + }, + true ], [ - "defaults when empty", - { "features": { "feature": {} } }, - "feature", + "version 1.0.0-0 < 1.0.0--1", { - "value": null, - "on": false, - "off": true, - "source": "defaultValue" - } + "version": { + "$vlt": "1.0.0--1" + } + }, + { + "version": "1.0.0-0" + }, + true ], [ - "uses defaultValue - number", - { "features": { "feature": { "defaultValue": 1 } } }, - "feature", + "version 1.0.0-0 < 1.0.0-1", { - "value": 1, - "on": true, - "off": false, - "source": "defaultValue" - } + "version": { + "$vlt": "1.0.0-1" + } + }, + { + "version": "1.0.0-0" + }, + true ], [ - "uses custom values - string", - { "features": { "feature": { "defaultValue": "yes" } } }, - "feature", + "version 1.0.0-1.0 < 1.0.0-1.-1", { - "value": "yes", - "on": true, - "off": false, - "source": "defaultValue" - } + "version": { + "$vlt": "1.0.0-1.-1" + } + }, + { + "version": "1.0.0-1.0" + }, + true ], [ - "force rules", + "version 1.2.3-a.b.c < 1.2.3-a.b.c.d", { - "features": { + "version": { + "$vlt": "1.2.3-a.b.c.d" + } + }, + { + "version": "1.2.3-a.b.c" + }, + true + ], + [ + "version 0.0.0 > 0.0.0-foo", + { + "version": { + "$vgt": "0.0.0-foo" + } + }, + { + "version": "0.0.0" + }, + true + ], + [ + "version 0.0.1 > 0.0.0", + { + "version": { + "$vgt": "0.0.0" + } + }, + { + "version": "0.0.1" + }, + true + ], + [ + "version 1.0.0 > 0.9.9", + { + "version": { + "$vgt": "0.9.9" + } + }, + { + "version": "1.0.0" + }, + true + ], + [ + "version 0.10.0 > 0.9.0", + { + "version": { + "$vgt": "0.9.0" + } + }, + { + "version": "0.10.0" + }, + true + ], + [ + "version 0.99.0 > 0.10.0", + { + "version": { + "$vgt": "0.10.0" + } + }, + { + "version": "0.99.0" + }, + true + ], + [ + "version 2.0.0 > 1.2.3", + { + "version": { + "$vgt": "1.2.3" + } + }, + { + "version": "2.0.0" + }, + true + ], + [ + "version v0.0.0 > 0.0.0-foo", + { + "version": { + "$vgt": "0.0.0-foo" + } + }, + { + "version": "v0.0.0" + }, + true + ], + [ + "version v0.0.1 > 0.0.0", + { + "version": { + "$vgt": "0.0.0" + } + }, + { + "version": "v0.0.1" + }, + true + ], + [ + "version v1.0.0 > 0.9.9", + { + "version": { + "$vgt": "0.9.9" + } + }, + { + "version": "v1.0.0" + }, + true + ], + [ + "version v0.10.0 > 0.9.0", + { + "version": { + "$vgt": "0.9.0" + } + }, + { + "version": "v0.10.0" + }, + true + ], + [ + "version v0.99.0 > 0.10.0", + { + "version": { + "$vgt": "0.10.0" + } + }, + { + "version": "v0.99.0" + }, + true + ], + [ + "version v2.0.0 > 1.2.3", + { + "version": { + "$vgt": "1.2.3" + } + }, + { + "version": "v2.0.0" + }, + true + ], + [ + "version 0.0.0 > v0.0.0-foo", + { + "version": { + "$vgt": "v0.0.0-foo" + } + }, + { + "version": "0.0.0" + }, + true + ], + [ + "version 0.0.1 > v0.0.0", + { + "version": { + "$vgt": "v0.0.0" + } + }, + { + "version": "0.0.1" + }, + true + ], + [ + "version 1.0.0 > v0.9.9", + { + "version": { + "$vgt": "v0.9.9" + } + }, + { + "version": "1.0.0" + }, + true + ], + [ + "version 0.10.0 > v0.9.0", + { + "version": { + "$vgt": "v0.9.0" + } + }, + { + "version": "0.10.0" + }, + true + ], + [ + "version 0.99.0 > v0.10.0", + { + "version": { + "$vgt": "v0.10.0" + } + }, + { + "version": "0.99.0" + }, + true + ], + [ + "version 2.0.0 > v1.2.3", + { + "version": { + "$vgt": "v1.2.3" + } + }, + { + "version": "2.0.0" + }, + true + ], + [ + "version 1.2.3 > 1.2.3-asdf", + { + "version": { + "$vgt": "1.2.3-asdf" + } + }, + { + "version": "1.2.3" + }, + true + ], + [ + "version 1.2.3 > 1.2.3-4", + { + "version": { + "$vgt": "1.2.3-4" + } + }, + { + "version": "1.2.3" + }, + true + ], + [ + "version 1.2.3 > 1.2.3-4-foo", + { + "version": { + "$vgt": "1.2.3-4-foo" + } + }, + { + "version": "1.2.3" + }, + true + ], + [ + "version 1.2.3-5-foo > 1.2.3-5", + { + "version": { + "$vgt": "1.2.3-5" + } + }, + { + "version": "1.2.3-5-foo" + }, + true + ], + [ + "version 1.2.3-5 > 1.2.3-4", + { + "version": { + "$vgt": "1.2.3-4" + } + }, + { + "version": "1.2.3-5" + }, + true + ], + [ + "version 1.2.3-5-foo > 1.2.3-5-Foo", + { + "version": { + "$vgt": "1.2.3-5-Foo" + } + }, + { + "version": "1.2.3-5-foo" + }, + true + ], + [ + "version 3.0.0 > 2.7.2+asdf", + { + "version": { + "$vgt": "2.7.2+asdf" + } + }, + { + "version": "3.0.0" + }, + true + ], + [ + "version 1.2.3-a.10 > 1.2.3-a.5", + { + "version": { + "$vgt": "1.2.3-a.5" + } + }, + { + "version": "1.2.3-a.10" + }, + true + ], + [ + "version 1.2.3-a.b > 1.2.3-a.5", + { + "version": { + "$vgt": "1.2.3-a.5" + } + }, + { + "version": "1.2.3-a.b" + }, + true + ], + [ + "version 1.2.3-a.b > 1.2.3-a", + { + "version": { + "$vgt": "1.2.3-a" + } + }, + { + "version": "1.2.3-a.b" + }, + true + ], + [ + "version 1.2.3-a.b.c.10.d.5 > 1.2.3-a.b.c.5.d.100", + { + "version": { + "$vgt": "1.2.3-a.b.c.5.d.100" + } + }, + { + "version": "1.2.3-a.b.c.10.d.5" + }, + true + ], + [ + "version 1.2.3-r2 > 1.2.3-r100", + { + "version": { + "$vgt": "1.2.3-r100" + } + }, + { + "version": "1.2.3-r2" + }, + true + ], + [ + "version 1.2.3-r100 > 1.2.3-R2", + { + "version": { + "$vgt": "1.2.3-R2" + } + }, + { + "version": "1.2.3-r100" + }, + true + ], + [ + "version a.b.c.d.e.f > 1.2.3", + { + "version": { + "$vgt": "1.2.3" + } + }, + { + "version": "a.b.c.d.e.f" + }, + true + ], + [ + "version 10.0.0 > 9.0.0", + { + "version": { + "$vgt": "9.0.0" + } + }, + { + "version": "10.0.0" + }, + true + ], + [ + "version 10000.0.0 > 9999.0.0", + { + "version": { + "$vgt": "9999.0.0" + } + }, + { + "version": "10000.0.0" + }, + true + ], + [ + "version 1.2.3 == 1.2.3", + { + "version": { + "$veq": "1.2.3" + } + }, + { + "version": "1.2.3" + }, + true + ], + [ + "version 1.2.3 == v1.2.3", + { + "version": { + "$veq": "v1.2.3" + } + }, + { + "version": "1.2.3" + }, + true + ], + [ + "version 1.2.3-0 == v1.2.3-0", + { + "version": { + "$veq": "v1.2.3-0" + } + }, + { + "version": "1.2.3-0" + }, + true + ], + [ + "version 1.2.3-1 == 1.2.3-1", + { + "version": { + "$veq": "1.2.3-1" + } + }, + { + "version": "1.2.3-1" + }, + true + ], + [ + "version 1.2.3-1 == v1.2.3-1", + { + "version": { + "$veq": "v1.2.3-1" + } + }, + { + "version": "1.2.3-1" + }, + true + ], + [ + "version 1.2.3-beta == 1.2.3-beta", + { + "version": { + "$veq": "1.2.3-beta" + } + }, + { + "version": "1.2.3-beta" + }, + true + ], + [ + "version 1.2.3-beta == v1.2.3-beta", + { + "version": { + "$veq": "v1.2.3-beta" + } + }, + { + "version": "1.2.3-beta" + }, + true + ], + [ + "version 1.2.3-beta+build == 1.2.3-beta+otherbuild", + { + "version": { + "$veq": "1.2.3-beta+otherbuild" + } + }, + { + "version": "1.2.3-beta+build" + }, + true + ], + [ + "version 1.2.3-beta+build == v1.2.3-beta+otherbuild", + { + "version": { + "$veq": "v1.2.3-beta+otherbuild" + } + }, + { + "version": "1.2.3-beta+build" + }, + true + ], + [ + "version 1-2-3 == 1.2.3", + { + "version": { + "$veq": "1.2.3" + } + }, + { + "version": "1-2-3" + }, + true + ], + [ + "version 1-2-3 == 1-2.3+build99", + { + "version": { + "$veq": "1-2.3+build99" + } + }, + { + "version": "1-2-3" + }, + true + ], + [ + "version 1-2-3 == v1.2.3", + { + "version": { + "$veq": "v1.2.3" + } + }, + { + "version": "1-2-3" + }, + true + ], + [ + "version 1.2.3.4 == 1.2.3-4", + { + "version": { + "$veq": "1.2.3-4" + } + }, + { + "version": "1.2.3.4" + }, + true + ], + [ + "$inGroup passes for member of known group id", + { + "id": { + "$inGroup": "group_id" + } + }, + { + "id": 1 + }, + true, + { + "group_id": [ + 1, + 2, + 3 + ] + } + ], + [ + "$inGroup fails for non-member of known group id", + { + "id": { + "$inGroup": "group_id" + } + }, + { + "id": 5 + }, + false, + { + "group_id": [ + 1, + 2, + 3 + ] + } + ], + [ + "$inGroup fails for unknown group id", + { + "id": { + "$inGroup": "unknowngroup_id" + } + }, + { + "id": 1 + }, + false, + { + "group_id": [ + 1, + 2, + 3 + ] + } + ], + [ + "$notInGroup fails for member of known group id", + { + "id": { + "$notInGroup": "group_id" + } + }, + { + "id": 1 + }, + false, + { + "group_id": [ + 1, + 2, + 3 + ] + } + ], + [ + "$notInGroup passes for non-member of known group id", + { + "id": { + "$notInGroup": "group_id" + } + }, + { + "id": 5 + }, + true, + { + "group_id": [ + 1, + 2, + 3 + ] + } + ], + [ + "$notInGroup passes for unknown group id", + { + "id": { + "$notInGroup": "unknowngroup_id" + } + }, + { + "id": 1 + }, + true, + { + "group_id": [ + 1, + 2, + 3 + ] + } + ], + [ + "$inGroup passes for properly typed data", + { + "id": { + "$inGroup": "group_id" + } + }, + { + "id": "2" + }, + true, + { + "group_id": [ + 1, + "2", + 3 + ] + } + ], + [ + "$inGroup fails for improperly typed data", + { + "id": { + "$inGroup": "group_id" + } + }, + { + "id": "3" + }, + false, + { + "group_id": [ + 1, + "2", + 3 + ] + } + ] + ], + "hash": [ + [ + "", + "a", + 1, + 0.22 + ], + [ + "", + "b", + 1, + 0.077 + ], + [ + "b", + "a", + 1, + 0.946 + ], + [ + "ef", + "d", + 1, + 0.652 + ], + [ + "asdf", + "8952klfjas09ujk", + 1, + 0.549 + ], + [ + "", + "123", + 1, + 0.011 + ], + [ + "", + "___)((*\":&", + 1, + 0.563 + ], + [ + "seed", + "a", + 2, + 0.0505 + ], + [ + "seed", + "b", + 2, + 0.2696 + ], + [ + "foo", + "ab", + 2, + 0.2575 + ], + [ + "foo", + "def", + 2, + 0.2019 + ], + [ + "89123klj", + "8952klfjas09ujkasdf", + 2, + 0.124 + ], + [ + "90850943850283058242805", + "123", + 2, + 0.7516 + ], + [ + "()**(%$##$%#$#", + "___)((*\":&", + 2, + 0.0128 + ], + [ + "abc", + "def", + 99, + null + ] + ], + "getBucketRange": [ + [ + "normal 50/50", + [ + 2, + 1, + null + ], + [ + [ + 0, + 0.5 + ], + [ + 0.5, + 1 + ] + ] + ], + [ + "reduced coverage", + [ + 2, + 0.5, + null + ], + [ + [ + 0, + 0.25 + ], + [ + 0.5, + 0.75 + ] + ] + ], + [ + "zero coverage", + [ + 2, + 0, + null + ], + [ + [ + 0, + 0 + ], + [ + 0.5, + 0.5 + ] + ] + ], + [ + "4 variations", + [ + 4, + 1, + null + ], + [ + [ + 0, + 0.25 + ], + [ + 0.25, + 0.5 + ], + [ + 0.5, + 0.75 + ], + [ + 0.75, + 1 + ] + ] + ], + [ + "uneven weights", + [ + 2, + 1, + [ + 0.4, + 0.6 + ] + ], + [ + [ + 0, + 0.4 + ], + [ + 0.4, + 1 + ] + ] + ], + [ + "uneven weights, 3 variations", + [ + 3, + 1, + [ + 0.2, + 0.3, + 0.5 + ] + ], + [ + [ + 0, + 0.2 + ], + [ + 0.2, + 0.5 + ], + [ + 0.5, + 1 + ] + ] + ], + [ + "uneven weights, reduced coverage, 3 variations", + [ + 3, + 0.2, + [ + 0.2, + 0.3, + 0.5 + ] + ], + [ + [ + 0, + 0.04 + ], + [ + 0.2, + 0.26 + ], + [ + 0.5, + 0.6 + ] + ] + ], + [ + "negative coverage", + [ + 2, + -0.2, + null + ], + [ + [ + 0, + 0 + ], + [ + 0.5, + 0.5 + ] + ] + ], + [ + "coverage above 1", + [ + 2, + 1.5, + null + ], + [ + [ + 0, + 0.5 + ], + [ + 0.5, + 1 + ] + ] + ], + [ + "weights sum below 1", + [ + 2, + 1, + [ + 0.4, + 0.1 + ] + ], + [ + [ + 0, + 0.5 + ], + [ + 0.5, + 1 + ] + ] + ], + [ + "weights sum above 1", + [ + 2, + 1, + [ + 0.7, + 0.6 + ] + ], + [ + [ + 0, + 0.5 + ], + [ + 0.5, + 1 + ] + ] + ], + [ + "weights.length not equal to num variations", + [ + 4, + 1, + [ + 0.4, + 0.4, + 0.2 + ] + ], + [ + [ + 0, + 0.25 + ], + [ + 0.25, + 0.5 + ], + [ + 0.5, + 0.75 + ], + [ + 0.75, + 1 + ] + ] + ], + [ + "weights sum almost equals 1", + [ + 2, + 1, + [ + 0.4, + 0.5999 + ] + ], + [ + [ + 0, + 0.4 + ], + [ + 0.4, + 0.9999 + ] + ] + ] + ], + "feature": [ + [ + "unknown feature key", + {}, + "my-feature", + { + "value": null, + "on": false, + "off": true, + "source": "unknownFeature" + } + ], + [ + "defaults when empty", + { + "features": { + "feature": {} + } + }, + "feature", + { + "value": null, + "on": false, + "off": true, + "source": "defaultValue" + } + ], + [ + "uses defaultValue - number", + { + "features": { + "feature": { + "defaultValue": 1 + } + } + }, + "feature", + { + "value": 1, + "on": true, + "off": false, + "source": "defaultValue" + } + ], + [ + "uses custom values - string", + { + "features": { + "feature": { + "defaultValue": "yes" + } + } + }, + "feature", + { + "value": "yes", + "on": true, + "off": false, + "source": "defaultValue" + } + ], + [ + "force rules", + { + "features": { "feature": { "defaultValue": 2, "rules": [ @@ -2426,7 +3752,12 @@ { "force": 1, "condition": { - "country": { "$in": ["US", "CA"] }, + "country": { + "$in": [ + "US", + "CA" + ] + }, "browser": "firefox" } } @@ -2456,7 +3787,12 @@ { "force": 1, "condition": { - "country": { "$in": ["US", "CA"] }, + "country": { + "$in": [ + "US", + "CA" + ] + }, "browser": "firefox" } } @@ -2504,7 +3840,9 @@ { "features": { "feature": { - "rules": [{}] + "rules": [ + {} + ] } } }, @@ -2526,7 +3864,11 @@ "feature": { "rules": [ { - "variations": ["a", "b", "c"] + "variations": [ + "a", + "b", + "c" + ] } ] } @@ -2539,7 +3881,11 @@ "off": false, "experiment": { "key": "feature", - "variations": ["a", "b", "c"] + "variations": [ + "a", + "b", + "c" + ] }, "experimentResult": { "featureId": "feature", @@ -2566,7 +3912,11 @@ "feature": { "rules": [ { - "variations": ["a", "b", "c"] + "variations": [ + "a", + "b", + "c" + ] } ] } @@ -2579,7 +3929,11 @@ "off": false, "experiment": { "key": "feature", - "variations": ["a", "b", "c"] + "variations": [ + "a", + "b", + "c" + ] }, "experimentResult": { "featureId": "feature", @@ -2606,7 +3960,11 @@ "feature": { "rules": [ { - "variations": ["a", "b", "c"] + "variations": [ + "a", + "b", + "c" + ] } ] } @@ -2619,7 +3977,11 @@ "off": false, "experiment": { "key": "feature", - "variations": ["a", "b", "c"] + "variations": [ + "a", + "b", + "c" + ] }, "experimentResult": { "featureId": "feature", @@ -2654,8 +4016,14 @@ "name": "Test", "phase": "1", "ranges": [ - [0, 0.1], - [0.1, 1.0] + [ + 0, + 0.1 + ], + [ + 0.1, + 1.0 + ] ], "meta": [ { @@ -2671,14 +4039,31 @@ { "attribute": "anonId", "seed": "pricing", - "ranges": [[0, 1]] + "ranges": [ + [ + 0, + 1 + ] + ] } ], - "namespace": ["pricing", 0, 1], + "namespace": [ + "pricing", + 0, + 1 + ], "key": "hello", - "variations": [true, false], - "weights": [0.1, 0.9], - "condition": { "premium": true } + "variations": [ + true, + false + ], + "weights": [ + 0.1, + 0.9 + ], + "condition": { + "premium": true + } } ] } @@ -2693,8 +4078,14 @@ "experiment": { "coverage": 0.99, "ranges": [ - [0, 0.1], - [0.1, 1.0] + [ + 0, + 0.1 + ], + [ + 0.1, + 1.0 + ] ], "meta": [ { @@ -2710,7 +4101,12 @@ { "attribute": "anonId", "seed": "pricing", - "ranges": [[0, 1]] + "ranges": [ + [ + 0, + 1 + ] + ] } ], "name": "Test", @@ -2718,11 +4114,23 @@ "seed": "feature", "hashVersion": 2, "hashAttribute": "anonId", - "namespace": ["pricing", 0, 1], + "namespace": [ + "pricing", + 0, + 1 + ], "key": "hello", - "variations": [true, false], - "weights": [0.1, 0.9], - "condition": { "premium": true } + "variations": [ + true, + false + ], + "weights": [ + 0.1, + 0.9 + ], + "condition": { + "premium": true + } }, "experimentResult": { "featureId": "feature", @@ -2751,15 +4159,21 @@ "rules": [ { "force": 1, - "condition": { "browser": "chrome" } + "condition": { + "browser": "chrome" + } }, { "force": 2, - "condition": { "browser": "firefox" } + "condition": { + "browser": "firefox" + } }, { "force": 3, - "condition": { "browser": "safari" } + "condition": { + "browser": "safari" + } } ] } @@ -2785,15 +4199,21 @@ "rules": [ { "force": 1, - "condition": { "browser": "chrome" } + "condition": { + "browser": "chrome" + } }, { "force": 2, - "condition": { "browser": "firefox" } + "condition": { + "browser": "firefox" + } }, { "force": 3, - "condition": { "browser": "safari" } + "condition": { + "browser": "safari" + } } ] } @@ -2819,15 +4239,21 @@ "rules": [ { "force": 1, - "condition": { "browser": "chrome" } + "condition": { + "browser": "chrome" + } }, { "force": 2, - "condition": { "browser": "firefox" } + "condition": { + "browser": "firefox" + } }, { "force": 3, - "condition": { "browser": "safari" } + "condition": { + "browser": "safari" + } } ] } @@ -2844,13 +4270,20 @@ [ "skips experiment on coverage", { - "attributes": { "id": "123" }, + "attributes": { + "id": "123" + }, "features": { "feature": { "defaultValue": 0, "rules": [ { - "variations": [0, 1, 2, 3], + "variations": [ + 0, + 1, + 2, + 3 + ], "coverage": 0.01 }, { @@ -2871,14 +4304,25 @@ [ "skips experiment on namespace", { - "attributes": { "id": "123" }, + "attributes": { + "id": "123" + }, "features": { "feature": { "defaultValue": 0, "rules": [ { - "variations": [0, 1, 2, 3], - "namespace": ["pricing", 0, 0.01] + "variations": [ + 0, + 1, + 2, + 3 + ], + "namespace": [ + "pricing", + 0, + 0.01 + ] }, { "force": 3 @@ -2898,13 +4342,18 @@ [ "handles integer hashAttribute", { - "attributes": { "id": 123 }, + "attributes": { + "id": 123 + }, "features": { "feature": { "defaultValue": 0, "rules": [ { - "variations": [0, 1] + "variations": [ + 0, + 1 + ] } ] } @@ -2918,7 +4367,10 @@ "source": "experiment", "experiment": { "key": "feature", - "variations": [0, 1] + "variations": [ + 0, + 1 + ] }, "experimentResult": { "featureId": "feature", @@ -2937,13 +4389,20 @@ [ "skip experiment on missing hashAttribute", { - "attributes": { "id": "123" }, + "attributes": { + "id": "123" + }, "features": { "feature": { "defaultValue": 0, "rules": [ { - "variations": [0, 1, 2, 3], + "variations": [ + 0, + 1, + 2, + 3 + ], "hashAttribute": "company" }, { @@ -2964,7 +4423,9 @@ [ "include experiments when forced", { - "attributes": { "id": "123" }, + "attributes": { + "id": "123" + }, "forcedVariations": { "feature": 1 }, @@ -2973,7 +4434,12 @@ "defaultValue": 0, "rules": [ { - "variations": [0, 1, 2, 3] + "variations": [ + 0, + 1, + 2, + 3 + ] }, { "force": 3 @@ -2990,7 +4456,12 @@ "source": "experiment", "experiment": { "key": "feature", - "variations": [0, 1, 2, 3] + "variations": [ + 0, + 1, + 2, + 3 + ] }, "experimentResult": { "featureId": "feature", @@ -3018,7 +4489,10 @@ { "force": 2, "coverage": 0.01, - "range": [0, 0.99] + "range": [ + 0, + 0.99 + ] } ] } @@ -3045,7 +4519,10 @@ { "force": 2, "hashVersion": 2, - "range": [0.96, 0.97] + "range": [ + 0.96, + 0.97 + ] } ] } @@ -3071,7 +4548,10 @@ "rules": [ { "force": 2, - "range": [0, 0.01] + "range": [ + 0, + 0.01 + ] } ] } @@ -3100,7 +4580,12 @@ "filters": [ { "seed": "seed", - "ranges": [[0, 0.01]] + "ranges": [ + [ + 0, + 0.01 + ] + ] } ] } @@ -3128,7 +4613,10 @@ "rules": [ { "force": 2, - "range": [0, 0.5], + "range": [ + 0, + 0.5 + ], "seed": "fjdslafdsa", "hashVersion": 2 } @@ -3156,11 +4644,20 @@ "rules": [ { "key": "holdout", - "variations": [1, 2], + "variations": [ + 1, + 2 + ], "hashVersion": 2, "ranges": [ - [0, 0.01], - [0.01, 1.0] + [ + 0, + 0.01 + ], + [ + 0.01, + 1.0 + ] ], "meta": [ {}, @@ -3171,11 +4668,20 @@ }, { "key": "experiment", - "variations": [3, 4], + "variations": [ + 3, + 4 + ], "hashVersion": 2, "ranges": [ - [0, 0.5], - [0.5, 1.0] + [ + 0, + 0.5 + ], + [ + 0.5, + 1.0 + ] ] } ] @@ -3191,10 +4697,19 @@ "experiment": { "key": "experiment", "hashVersion": 2, - "variations": [3, 4], + "variations": [ + 3, + 4 + ], "ranges": [ - [0, 0.5], - [0.5, 1.0] + [ + 0, + 0.5 + ], + [ + 0.5, + 1.0 + ] ] }, "experimentResult": { @@ -3224,10 +4739,19 @@ { "key": "holdout", "hashVersion": 2, - "variations": [1, 2], + "variations": [ + 1, + 2 + ], "ranges": [ - [0, 0.99], - [0.99, 1.0] + [ + 0, + 0.99 + ], + [ + 0.99, + 1.0 + ] ], "meta": [ {}, @@ -3239,10 +4763,19 @@ { "key": "experiment", "hashVersion": 2, - "variations": [3, 4], + "variations": [ + 3, + 4 + ], "ranges": [ - [0, 0.5], - [0.5, 1.0] + [ + 0, + 0.5 + ], + [ + 0.5, + 1.0 + ] ] } ] @@ -3258,8 +4791,14 @@ "experiment": { "hashVersion": 2, "ranges": [ - [0, 0.99], - [0.99, 1.0] + [ + 0, + 0.99 + ], + [ + 0.99, + 1.0 + ] ], "meta": [ {}, @@ -3268,7 +4807,10 @@ } ], "key": "holdout", - "variations": [1, 2] + "variations": [ + 1, + 2 + ] }, "experimentResult": { "featureId": "feature", @@ -3297,11 +4839,20 @@ "defaultValue": "silver", "rules": [ { - "condition": { "country": "Canada" }, + "condition": { + "country": "Canada" + }, "force": "red" }, { - "condition": { "country": { "$in": ["USA", "Mexico"] } }, + "condition": { + "country": { + "$in": [ + "USA", + "Mexico" + ] + } + }, "force": "green" } ] @@ -3313,13 +4864,17 @@ "parentConditions": [ { "id": "parentFlag", - "condition": { "value": "green" }, + "condition": { + "value": "green" + }, "gate": true } ] }, { - "condition": { "memberType": "basic" }, + "condition": { + "memberType": "basic" + }, "force": "success" } ] @@ -3350,13 +4905,17 @@ "parentConditions": [ { "id": "parentFlag", - "condition": { "value": "green" }, + "condition": { + "value": "green" + }, "gate": true } ] }, { - "condition": { "memberType": "basic" }, + "condition": { + "memberType": "basic" + }, "force": "success" } ] @@ -3384,11 +4943,20 @@ "defaultValue": "silver", "rules": [ { - "condition": { "country": "Canada" }, + "condition": { + "country": "Canada" + }, "force": "red" }, { - "condition": { "country": { "$in": ["USA", "Mexico"] } }, + "condition": { + "country": { + "$in": [ + "USA", + "Mexico" + ] + } + }, "force": "green" } ] @@ -3400,13 +4968,17 @@ "parentConditions": [ { "id": "parentFlag", - "condition": { "value": "green" }, + "condition": { + "value": "green" + }, "gate": true } ] }, { - "condition": { "memberType": "basic" }, + "condition": { + "memberType": "basic" + }, "force": "success" } ] @@ -3434,11 +5006,20 @@ "defaultValue": "silver", "rules": [ { - "condition": { "country": "Canada" }, + "condition": { + "country": "Canada" + }, "force": "red" }, { - "condition": { "country": { "$in": ["USA", "Mexico"] } }, + "condition": { + "country": { + "$in": [ + "USA", + "Mexico" + ] + } + }, "force": "green" } ] @@ -3447,7 +5028,9 @@ "defaultValue": 0, "rules": [ { - "condition": { "id": "123" }, + "condition": { + "id": "123" + }, "force": 2 } ] @@ -3459,7 +5042,9 @@ "parentConditions": [ { "id": "parentFlag1", - "condition": { "value": "green" }, + "condition": { + "value": "green" + }, "gate": true } ] @@ -3468,13 +5053,19 @@ "parentConditions": [ { "id": "parentFlag2", - "condition": { "value": { "$gt": 1 } }, + "condition": { + "value": { + "$gt": 1 + } + }, "gate": true } ] }, { - "condition": { "memberType": "basic" }, + "condition": { + "memberType": "basic" + }, "force": "success" } ] @@ -3505,17 +5096,30 @@ "parentConditions": [ { "id": "parentFlag2", - "condition": { "value": { "$gt": 1 } }, + "condition": { + "value": { + "$gt": 1 + } + }, "gate": true } ] }, { - "condition": { "country": "Canada" }, + "condition": { + "country": "Canada" + }, "force": "red" }, { - "condition": { "country": { "$in": ["USA", "Mexico"] } }, + "condition": { + "country": { + "$in": [ + "USA", + "Mexico" + ] + } + }, "force": "green" } ] @@ -3524,7 +5128,9 @@ "defaultValue": 0, "rules": [ { - "condition": { "id": "123" }, + "condition": { + "id": "123" + }, "force": 2 } ] @@ -3536,13 +5142,17 @@ "parentConditions": [ { "id": "parentFlag1", - "condition": { "value": "green" }, + "condition": { + "value": "green" + }, "gate": true } ] }, { - "condition": { "memberType": "basic" }, + "condition": { + "memberType": "basic" + }, "force": "success" } ] @@ -3571,12 +5181,21 @@ "rules": [ { "key": "experiment", - "variations": [0, 1], + "variations": [ + 0, + 1 + ], "hashAttribute": "id", "hashVersion": 2, "ranges": [ - [0, 0.5], - [0.5, 1.0] + [ + 0, + 0.5 + ], + [ + 0.5, + 1.0 + ] ] } ] @@ -3588,13 +5207,17 @@ "parentConditions": [ { "id": "parentExperimentFlag", - "condition": { "value": 1 }, + "condition": { + "value": 1 + }, "gate": true } ] }, { - "condition": { "memberType": "basic" }, + "condition": { + "memberType": "basic" + }, "force": "success" } ] @@ -3623,7 +5246,9 @@ "parentConditions": [ { "id": "flag2", - "condition": { "value": true }, + "condition": { + "value": true + }, "gate": true } ] @@ -3637,7 +5262,9 @@ "parentConditions": [ { "id": "flag1", - "condition": { "value": true }, + "condition": { + "value": true + }, "gate": true } ] @@ -3646,340 +5273,924 @@ } } }, - "flag1", + "flag1", + { + "value": null, + "on": false, + "off": true, + "source": "cyclicPrerequisite" + } + ], + [ + "SavedGroups correctly pulled from context for force rule", + { + "attributes": { + "id": 123 + }, + "features": { + "inGroup_force_rule": { + "defaultValue": false, + "rules": [ + { + "force": true, + "condition": { + "id": { + "$inGroup": "group_id" + } + } + } + ] + } + }, + "savedGroups": { + "group_id": [ + 123, + 456 + ] + } + }, + "inGroup_force_rule", + { + "value": true, + "on": true, + "off": false, + "source": "force" + } + ], + [ + "SavedGroups correctly pulled from context for experiment rule", + { + "attributes": { + "id": 123 + }, + "features": { + "inGroup_experiment_rule": { + "defaultValue": 0, + "rules": [ + { + "key": "experiment", + "condition": { + "id": { + "$inGroup": "group_id" + } + }, + "hashVersion": 2, + "variations": [ + 1, + 2 + ], + "ranges": [ + [ + 0, + 0.5 + ], + [ + 0.5, + 1.0 + ] + ] + } + ] + } + }, + "savedGroups": { + "group_id": [ + 123, + 456 + ] + } + }, + "inGroup_experiment_rule", { - "value": null, - "on": false, - "off": true, - "source": "cyclicPrerequisite" + "value": 1, + "on": true, + "off": false, + "source": "experiment", + "experiment": { + "hashVersion": 2, + "condition": { + "id": { + "$inGroup": "group_id" + } + }, + "variations": [ + 1, + 2 + ], + "ranges": [ + [ + 0, + 0.5 + ], + [ + 0.5, + 1.0 + ] + ], + "key": "experiment" + }, + "experimentResult": { + "featureId": "inGroup_experiment_rule", + "hashAttribute": "id", + "hashUsed": true, + "hashValue": 123, + "inExperiment": true, + "key": "0", + "value": 1, + "variationId": 0, + "bucket": 0.1736, + "stickyBucketUsed": false + } } ] ], "run": [ [ "default weights - 1", - { "attributes": { "id": "1" } }, - { "key": "my-test", "variations": [0, 1] }, + { + "attributes": { + "id": "1" + } + }, + { + "key": "my-test", + "variations": [ + 0, + 1 + ] + }, 1, true, true ], [ "default weights - 2", - { "attributes": { "id": "2" } }, - { "key": "my-test", "variations": [0, 1] }, + { + "attributes": { + "id": "2" + } + }, + { + "key": "my-test", + "variations": [ + 0, + 1 + ] + }, 0, true, true ], [ "default weights - 3", - { "attributes": { "id": "3" } }, - { "key": "my-test", "variations": [0, 1] }, + { + "attributes": { + "id": "3" + } + }, + { + "key": "my-test", + "variations": [ + 0, + 1 + ] + }, 0, true, true ], [ "default weights - 4", - { "attributes": { "id": "4" } }, - { "key": "my-test", "variations": [0, 1] }, + { + "attributes": { + "id": "4" + } + }, + { + "key": "my-test", + "variations": [ + 0, + 1 + ] + }, 1, true, true ], [ "default weights - 5", - { "attributes": { "id": "5" } }, - { "key": "my-test", "variations": [0, 1] }, + { + "attributes": { + "id": "5" + } + }, + { + "key": "my-test", + "variations": [ + 0, + 1 + ] + }, 1, true, true ], [ "default weights - 6", - { "attributes": { "id": "6" } }, - { "key": "my-test", "variations": [0, 1] }, + { + "attributes": { + "id": "6" + } + }, + { + "key": "my-test", + "variations": [ + 0, + 1 + ] + }, 1, true, true ], [ "default weights - 7", - { "attributes": { "id": "7" } }, - { "key": "my-test", "variations": [0, 1] }, + { + "attributes": { + "id": "7" + } + }, + { + "key": "my-test", + "variations": [ + 0, + 1 + ] + }, 0, true, true ], [ "default weights - 8", - { "attributes": { "id": "8" } }, - { "key": "my-test", "variations": [0, 1] }, + { + "attributes": { + "id": "8" + } + }, + { + "key": "my-test", + "variations": [ + 0, + 1 + ] + }, 1, true, true ], [ "default weights - 9", - { "attributes": { "id": "9" } }, - { "key": "my-test", "variations": [0, 1] }, + { + "attributes": { + "id": "9" + } + }, + { + "key": "my-test", + "variations": [ + 0, + 1 + ] + }, 0, true, true ], [ "uneven weights - 1", - { "attributes": { "id": "1" } }, - { "key": "my-test", "variations": [0, 1], "weights": [0.1, 0.9] }, + { + "attributes": { + "id": "1" + } + }, + { + "key": "my-test", + "variations": [ + 0, + 1 + ], + "weights": [ + 0.1, + 0.9 + ] + }, 1, true, true ], [ "uneven weights - 2", - { "attributes": { "id": "2" } }, - { "key": "my-test", "variations": [0, 1], "weights": [0.1, 0.9] }, + { + "attributes": { + "id": "2" + } + }, + { + "key": "my-test", + "variations": [ + 0, + 1 + ], + "weights": [ + 0.1, + 0.9 + ] + }, 1, true, true ], [ "uneven weights - 3", - { "attributes": { "id": "3" } }, - { "key": "my-test", "variations": [0, 1], "weights": [0.1, 0.9] }, + { + "attributes": { + "id": "3" + } + }, + { + "key": "my-test", + "variations": [ + 0, + 1 + ], + "weights": [ + 0.1, + 0.9 + ] + }, 0, true, true ], [ "uneven weights - 4", - { "attributes": { "id": "4" } }, - { "key": "my-test", "variations": [0, 1], "weights": [0.1, 0.9] }, + { + "attributes": { + "id": "4" + } + }, + { + "key": "my-test", + "variations": [ + 0, + 1 + ], + "weights": [ + 0.1, + 0.9 + ] + }, 1, true, true ], [ "uneven weights - 5", - { "attributes": { "id": "5" } }, - { "key": "my-test", "variations": [0, 1], "weights": [0.1, 0.9] }, + { + "attributes": { + "id": "5" + } + }, + { + "key": "my-test", + "variations": [ + 0, + 1 + ], + "weights": [ + 0.1, + 0.9 + ] + }, 1, true, true ], [ "uneven weights - 6", - { "attributes": { "id": "6" } }, - { "key": "my-test", "variations": [0, 1], "weights": [0.1, 0.9] }, + { + "attributes": { + "id": "6" + } + }, + { + "key": "my-test", + "variations": [ + 0, + 1 + ], + "weights": [ + 0.1, + 0.9 + ] + }, 1, true, true ], [ "uneven weights - 7", - { "attributes": { "id": "7" } }, - { "key": "my-test", "variations": [0, 1], "weights": [0.1, 0.9] }, + { + "attributes": { + "id": "7" + } + }, + { + "key": "my-test", + "variations": [ + 0, + 1 + ], + "weights": [ + 0.1, + 0.9 + ] + }, 0, true, true ], [ "uneven weights - 8", - { "attributes": { "id": "8" } }, - { "key": "my-test", "variations": [0, 1], "weights": [0.1, 0.9] }, + { + "attributes": { + "id": "8" + } + }, + { + "key": "my-test", + "variations": [ + 0, + 1 + ], + "weights": [ + 0.1, + 0.9 + ] + }, 1, true, true ], [ "uneven weights - 9", - { "attributes": { "id": "9" } }, - { "key": "my-test", "variations": [0, 1], "weights": [0.1, 0.9] }, + { + "attributes": { + "id": "9" + } + }, + { + "key": "my-test", + "variations": [ + 0, + 1 + ], + "weights": [ + 0.1, + 0.9 + ] + }, 1, true, true ], [ "coverage - 1", - { "attributes": { "id": "1" } }, - { "key": "my-test", "variations": [0, 1], "coverage": 0.4 }, + { + "attributes": { + "id": "1" + } + }, + { + "key": "my-test", + "variations": [ + 0, + 1 + ], + "coverage": 0.4 + }, 0, false, false ], [ "coverage - 2", - { "attributes": { "id": "2" } }, - { "key": "my-test", "variations": [0, 1], "coverage": 0.4 }, + { + "attributes": { + "id": "2" + } + }, + { + "key": "my-test", + "variations": [ + 0, + 1 + ], + "coverage": 0.4 + }, 0, true, true ], [ "coverage - 3", - { "attributes": { "id": "3" } }, - { "key": "my-test", "variations": [0, 1], "coverage": 0.4 }, + { + "attributes": { + "id": "3" + } + }, + { + "key": "my-test", + "variations": [ + 0, + 1 + ], + "coverage": 0.4 + }, 0, true, true ], [ "coverage - 4", - { "attributes": { "id": "4" } }, - { "key": "my-test", "variations": [0, 1], "coverage": 0.4 }, + { + "attributes": { + "id": "4" + } + }, + { + "key": "my-test", + "variations": [ + 0, + 1 + ], + "coverage": 0.4 + }, 0, false, false ], [ "coverage - 5", - { "attributes": { "id": "5" } }, - { "key": "my-test", "variations": [0, 1], "coverage": 0.4 }, + { + "attributes": { + "id": "5" + } + }, + { + "key": "my-test", + "variations": [ + 0, + 1 + ], + "coverage": 0.4 + }, 1, true, true ], [ "coverage - 6", - { "attributes": { "id": "6" } }, - { "key": "my-test", "variations": [0, 1], "coverage": 0.4 }, + { + "attributes": { + "id": "6" + } + }, + { + "key": "my-test", + "variations": [ + 0, + 1 + ], + "coverage": 0.4 + }, 0, false, false ], [ "coverage - 7", - { "attributes": { "id": "7" } }, - { "key": "my-test", "variations": [0, 1], "coverage": 0.4 }, + { + "attributes": { + "id": "7" + } + }, + { + "key": "my-test", + "variations": [ + 0, + 1 + ], + "coverage": 0.4 + }, 0, true, true ], [ "coverage - 8", - { "attributes": { "id": "8" } }, - { "key": "my-test", "variations": [0, 1], "coverage": 0.4 }, + { + "attributes": { + "id": "8" + } + }, + { + "key": "my-test", + "variations": [ + 0, + 1 + ], + "coverage": 0.4 + }, 1, true, true ], [ "coverage - 9", - { "attributes": { "id": "9" } }, - { "key": "my-test", "variations": [0, 1], "coverage": 0.4 }, + { + "attributes": { + "id": "9" + } + }, + { + "key": "my-test", + "variations": [ + 0, + 1 + ], + "coverage": 0.4 + }, 0, false, false ], [ "three way test - 1", - { "attributes": { "id": "1" } }, - { "key": "my-test", "variations": [0, 1, 2] }, + { + "attributes": { + "id": "1" + } + }, + { + "key": "my-test", + "variations": [ + 0, + 1, + 2 + ] + }, 2, true, true ], [ "three way test - 2", - { "attributes": { "id": "2" } }, - { "key": "my-test", "variations": [0, 1, 2] }, + { + "attributes": { + "id": "2" + } + }, + { + "key": "my-test", + "variations": [ + 0, + 1, + 2 + ] + }, 0, true, true ], [ "three way test - 3", - { "attributes": { "id": "3" } }, - { "key": "my-test", "variations": [0, 1, 2] }, + { + "attributes": { + "id": "3" + } + }, + { + "key": "my-test", + "variations": [ + 0, + 1, + 2 + ] + }, 0, true, true ], [ "three way test - 4", - { "attributes": { "id": "4" } }, - { "key": "my-test", "variations": [0, 1, 2] }, + { + "attributes": { + "id": "4" + } + }, + { + "key": "my-test", + "variations": [ + 0, + 1, + 2 + ] + }, 2, true, true ], [ "three way test - 5", - { "attributes": { "id": "5" } }, - { "key": "my-test", "variations": [0, 1, 2] }, + { + "attributes": { + "id": "5" + } + }, + { + "key": "my-test", + "variations": [ + 0, + 1, + 2 + ] + }, 1, true, true ], [ "three way test - 6", - { "attributes": { "id": "6" } }, - { "key": "my-test", "variations": [0, 1, 2] }, + { + "attributes": { + "id": "6" + } + }, + { + "key": "my-test", + "variations": [ + 0, + 1, + 2 + ] + }, 2, true, true ], [ "three way test - 7", - { "attributes": { "id": "7" } }, - { "key": "my-test", "variations": [0, 1, 2] }, + { + "attributes": { + "id": "7" + } + }, + { + "key": "my-test", + "variations": [ + 0, + 1, + 2 + ] + }, 0, true, true ], [ "three way test - 8", - { "attributes": { "id": "8" } }, - { "key": "my-test", "variations": [0, 1, 2] }, + { + "attributes": { + "id": "8" + } + }, + { + "key": "my-test", + "variations": [ + 0, + 1, + 2 + ] + }, 1, true, true ], [ "three way test - 9", - { "attributes": { "id": "9" } }, - { "key": "my-test", "variations": [0, 1, 2] }, + { + "attributes": { + "id": "9" + } + }, + { + "key": "my-test", + "variations": [ + 0, + 1, + 2 + ] + }, 0, true, true ], [ "test name - my-test", - { "attributes": { "id": "1" } }, - { "key": "my-test", "variations": [0, 1] }, + { + "attributes": { + "id": "1" + } + }, + { + "key": "my-test", + "variations": [ + 0, + 1 + ] + }, 1, true, true ], [ "test name - my-test-3", - { "attributes": { "id": "1" } }, - { "key": "my-test-3", "variations": [0, 1] }, + { + "attributes": { + "id": "1" + } + }, + { + "key": "my-test-3", + "variations": [ + 0, + 1 + ] + }, 0, true, true ], [ "empty id", - { "attributes": { "id": "" } }, - { "key": "my-test", "variations": [0, 1] }, + { + "attributes": { + "id": "" + } + }, + { + "key": "my-test", + "variations": [ + 0, + 1 + ] + }, 0, false, false ], [ "null id", - { "attributes": { "id": null } }, - { "key": "my-test", "variations": [0, 1] }, + { + "attributes": { + "id": null + } + }, + { + "key": "my-test", + "variations": [ + 0, + 1 + ] + }, 0, false, false ], [ "missing id", - { "attributes": {} }, - { "key": "my-test", "variations": [0, 1] }, + { + "attributes": {} + }, + { + "key": "my-test", + "variations": [ + 0, + 1 + ] + }, 0, false, false @@ -3987,31 +6198,68 @@ [ "missing attributes", {}, - { "key": "my-test", "variations": [0, 1] }, + { + "key": "my-test", + "variations": [ + 0, + 1 + ] + }, 0, false, false ], [ "single variation", - { "attributes": { "id": "1" } }, - { "key": "my-test", "variations": [0] }, + { + "attributes": { + "id": "1" + } + }, + { + "key": "my-test", + "variations": [ + 0 + ] + }, 0, false, false ], [ "negative forced variation", - { "attributes": { "id": "1" } }, - { "key": "my-test", "variations": [0, 1], "force": -8 }, + { + "attributes": { + "id": "1" + } + }, + { + "key": "my-test", + "variations": [ + 0, + 1 + ], + "force": -8 + }, 0, false, false ], [ "high forced variation", - { "attributes": { "id": "1" } }, - { "key": "my-test", "variations": [0, 1], "force": 25 }, + { + "attributes": { + "id": "1" + } + }, + { + "key": "my-test", + "variations": [ + 0, + 1 + ], + "force": 25 + }, 0, false, false @@ -4026,7 +6274,10 @@ }, { "key": "my-test", - "variations": [0, 1], + "variations": [ + 0, + 1 + ], "condition": { "browser": "firefox" } @@ -4045,7 +6296,10 @@ }, { "key": "my-test", - "variations": [0, 1], + "variations": [ + 0, + 1 + ], "condition": { "browser": "firefox" } @@ -4064,7 +6318,10 @@ }, { "key": "my-test", - "variations": [0, 1], + "variations": [ + 0, + 1 + ], "hashAttribute": "companyId" }, 1, @@ -4081,7 +6338,10 @@ }, { "key": "my-test", - "variations": [0, 1] + "variations": [ + 0, + 1 + ] }, 0, false, @@ -4097,7 +6357,10 @@ }, { "key": "forced-test-qs", - "variations": [0, 1] + "variations": [ + 0, + 1 + ] }, 1, true, @@ -4113,7 +6376,10 @@ { "key": "my-test", "active": true, - "variations": [0, 1] + "variations": [ + 0, + 1 + ] }, 1, true, @@ -4129,7 +6395,10 @@ { "key": "my-test", "active": false, - "variations": [0, 1] + "variations": [ + 0, + 1 + ] }, 0, false, @@ -4146,7 +6415,10 @@ { "key": "my-test", "active": false, - "variations": [0, 1] + "variations": [ + 0, + 1 + ] }, 1, true, @@ -4163,7 +6435,10 @@ "key": "my-test", "force": 1, "coverage": 0.01, - "variations": [0, 1] + "variations": [ + 0, + 1 + ] }, 0, false, @@ -4199,12 +6474,19 @@ [ "Force variation from context", { - "attributes": { "id": "1" }, - "forcedVariations": { "my-test": 0 } + "attributes": { + "id": "1" + }, + "forcedVariations": { + "my-test": 0 + } }, { "key": "my-test", - "variations": [0, 1] + "variations": [ + 0, + 1 + ] }, 0, true, @@ -4213,12 +6495,17 @@ [ "Skips experiments in QA mode", { - "attributes": { "id": "1" }, + "attributes": { + "id": "1" + }, "qaMode": true }, { "key": "my-test", - "variations": [0, 1] + "variations": [ + 0, + 1 + ] }, 0, false, @@ -4227,13 +6514,20 @@ [ "Works in QA mode if forced in context", { - "attributes": { "id": "1" }, + "attributes": { + "id": "1" + }, "qaMode": true, - "forcedVariations": { "my-test": 1 } + "forcedVariations": { + "my-test": 1 + } }, { "key": "my-test", - "variations": [0, 1] + "variations": [ + 0, + 1 + ] }, 1, true, @@ -4242,12 +6536,17 @@ [ "Works in QA mode if forced in experiment", { - "attributes": { "id": "1" }, + "attributes": { + "id": "1" + }, "qaMode": true }, { "key": "my-test", - "variations": [0, 1], + "variations": [ + 0, + 1 + ], "force": 1 }, 1, @@ -4263,8 +6562,15 @@ }, { "key": "my-test", - "variations": [0, 1], - "namespace": ["namespace", 0.1, 1] + "variations": [ + 0, + 1 + ], + "namespace": [ + "namespace", + 0.1, + 1 + ] }, 1, true, @@ -4279,8 +6585,15 @@ }, { "key": "my-test", - "variations": [0, 1], - "namespace": ["namespace", 0, 0.1] + "variations": [ + 0, + 1 + ], + "namespace": [ + "namespace", + 0, + 0.1 + ] }, 0, false, @@ -4295,7 +6608,10 @@ }, { "key": "no-coverage", - "variations": [0, 1], + "variations": [ + 0, + 1 + ], "coverage": 0 }, 0, @@ -4312,19 +6628,33 @@ }, { "key": "filtered", - "variations": [0, 1], + "variations": [ + 0, + 1 + ], "filters": [ { "seed": "seed", "ranges": [ - [0, 0.1], - [0.2, 0.4] + [ + 0, + 0.1 + ], + [ + 0.2, + 0.4 + ] ] }, { "seed": "seed", "attribute": "anonId", - "ranges": [[0.8, 1.0]] + "ranges": [ + [ + 0.8, + 1.0 + ] + ] } ] }, @@ -4342,19 +6672,33 @@ }, { "key": "filtered", - "variations": [0, 1], + "variations": [ + 0, + 1 + ], "filters": [ { "seed": "seed", "ranges": [ - [0, 0.1], - [0.2, 0.4] + [ + 0, + 0.1 + ], + [ + 0.2, + 0.4 + ] ] }, { "seed": "seed", "attribute": "anonId", - "ranges": [[0.6, 0.8]] + "ranges": [ + [ + 0.6, + 0.8 + ] + ] } ] }, @@ -4371,17 +6715,30 @@ }, { "key": "filtered", - "variations": [0, 1], + "variations": [ + 0, + 1 + ], "filters": [ { "seed": "seed", "ranges": [ - [0, 0.1], - [0.2, 0.4] + [ + 0, + 0.1 + ], + [ + 0.2, + 0.4 + ] ] } ], - "namespace": ["test", 0, 0.001] + "namespace": [ + "test", + 0, + 0.001 + ] }, 1, true, @@ -4396,13 +6753,25 @@ }, { "key": "ranges", - "variations": [0, 1], + "variations": [ + 0, + 1 + ], "ranges": [ - [0.99, 1.0], - [0.0, 0.99] + [ + 0.99, + 1.0 + ], + [ + 0.0, + 0.99 + ] ], "coverage": 0.01, - "weights": [0.99, 0.01] + "weights": [ + 0.99, + 0.01 + ] }, 1, true, @@ -4417,10 +6786,19 @@ }, { "key": "configs", - "variations": [0, 1], + "variations": [ + 0, + 1 + ], "ranges": [ - [0, 0.1], - [0.9, 1.0] + [ + 0, + 0.1 + ], + [ + 0.9, + 1.0 + ] ] }, 0, @@ -4438,10 +6816,19 @@ "key": "key", "seed": "foo", "hashVersion": 2, - "variations": [0, 1], + "variations": [ + 0, + 1 + ], "ranges": [ - [0, 0.5], - [0.5, 1.0] + [ + 0, + 0.5 + ], + [ + 0.5, + 1.0 + ] ] }, 1, @@ -4459,7 +6846,10 @@ "key": "key", "seed": "foo", "hashVersion": 2, - "variations": [0, 1] + "variations": [ + 0, + 1 + ] }, 1, true, @@ -4476,8 +6866,14 @@ "key": "key", "seed": "foo", "hashVersion": 2, - "variations": [0, 1], - "weights": [0.5, 0.5], + "variations": [ + 0, + 1 + ], + "weights": [ + 0.5, + 0.5 + ], "coverage": 0.99 }, 1, @@ -4487,7 +6883,9 @@ [ "Prerequisite condition passes", { - "attributes": { "id": "1" }, + "attributes": { + "id": "1" + }, "features": { "parentFlag": { "defaultValue": true @@ -4496,7 +6894,10 @@ }, { "key": "my-test", - "variations": [0, 1], + "variations": [ + 0, + 1 + ], "parentConditions": [ { "id": "parentFlag", @@ -4513,7 +6914,9 @@ [ "Prerequisite condition fails", { - "attributes": { "id": "1" }, + "attributes": { + "id": "1" + }, "features": { "parentFlag": { "defaultValue": false @@ -4522,7 +6925,10 @@ }, { "key": "my-test", - "variations": [0, 1], + "variations": [ + 0, + 1 + ], "parentConditions": [ { "id": "parentFlag", @@ -4535,195 +6941,555 @@ 0, false, false + ], + [ + "SavedGroups correctly pulled from context for experiment", + { + "attributes": { + "id": "4" + }, + "savedGroups": { + "group_id": [ + "4", + "5", + "6" + ] + } + }, + { + "key": "group-filtered-test", + "condition": { + "id": { + "$inGroup": "group_id" + } + }, + "variations": [ + 0, + 1, + 2 + ] + }, + 0, + true, + true + ] + ], + "chooseVariation": [ + [ + "even range, 0.2", + 0.2, + [ + [ + 0, + 0.5 + ], + [ + 0.5, + 1 + ] + ], + 0 + ], + [ + "even range, 0.4", + 0.4, + [ + [ + 0, + 0.5 + ], + [ + 0.5, + 1 + ] + ], + 0 + ], + [ + "even range, 0.6", + 0.6, + [ + [ + 0, + 0.5 + ], + [ + 0.5, + 1 + ] + ], + 1 + ], + [ + "even range, 0.8", + 0.8, + [ + [ + 0, + 0.5 + ], + [ + 0.5, + 1 + ] + ], + 1 + ], + [ + "even range, 0", + 0, + [ + [ + 0, + 0.5 + ], + [ + 0.5, + 1 + ] + ], + 0 + ], + [ + "even range, 0.5", + 0.5, + [ + [ + 0, + 0.5 + ], + [ + 0.5, + 1 + ] + ], + 1 + ], + [ + "reduced range, 0.2", + 0.2, + [ + [ + 0, + 0.25 + ], + [ + 0.5, + 0.75 + ] + ], + 0 + ], + [ + "reduced range, 0.4", + 0.4, + [ + [ + 0, + 0.25 + ], + [ + 0.5, + 0.75 + ] + ], + -1 + ], + [ + "reduced range, 0.6", + 0.6, + [ + [ + 0, + 0.25 + ], + [ + 0.5, + 0.75 + ] + ], + 1 + ], + [ + "reduced range, 0.8", + 0.8, + [ + [ + 0, + 0.25 + ], + [ + 0.5, + 0.75 + ] + ], + -1 + ], + [ + "reduced range, 0.25", + 0.25, + [ + [ + 0, + 0.25 + ], + [ + 0.5, + 0.75 + ] + ], + -1 + ], + [ + "reduced range, 0.5", + 0.5, + [ + [ + 0, + 0.25 + ], + [ + 0.5, + 0.75 + ] + ], + 1 + ], + [ + "zero range", + 0.5, + [ + [ + 0, + 0.5 + ], + [ + 0.5, + 0.5 + ], + [ + 0.5, + 1 + ] + ], + 2 + ] + ], + "getQueryStringOverride": [ + [ + "empty url", + "my-test", + "", + 2, + null + ], + [ + "no query string", + "my-test", + "http://example.com", + 2, + null + ], + [ + "empty query string", + "my-test", + "http://example.com?", + 2, + null + ], + [ + "no query string match", + "my-test", + "http://example.com?somequery", + 2, + null + ], + [ + "invalid query string", + "my-test", + "http://example.com??&&&?#", + 2, + null + ], + [ + "simple match 0", + "my-test", + "http://example.com?my-test=0", + 2, + 0 + ], + [ + "simple match 1", + "my-test", + "http://example.com?my-test=1", + 2, + 1 + ], + [ + "negative variation", + "my-test", + "http://example.com?my-test=-1", + 2, + null + ], + [ + "float", + "my-test", + "http://example.com?my-test=2.054", + 2, + null + ], + [ + "string", + "my-test", + "http://example.com?my-test=foo", + 2, + null + ], + [ + "variation too high", + "my-test", + "http://example.com?my-test=5", + 2, + null + ], + [ + "high numVariations", + "my-test", + "http://example.com?my-test=5", + 6, + 5 + ], + [ + "equal to numVariations", + "my-test", + "http://example.com?my-test=5", + 5, + null + ], + [ + "other query string before", + "my-test", + "http://example.com?foo=bar&my-test=1", + 2, + 1 + ], + [ + "other query string after", + "my-test", + "http://example.com?foo=bar&my-test=1&bar=baz", + 2, + 1 + ], + [ + "anchor", + "my-test", + "http://example.com?my-test=1#foo", + 2, + 1 ] ], - "chooseVariation": [ + "inNamespace": [ [ - "even range, 0.2", - 0.2, + "user 1, namespace1, 1", + "1", [ - [0, 0.5], - [0.5, 1] + "namespace1", + 0, + 0.4 ], - 0 + false ], [ - "even range, 0.4", - 0.4, + "user 1, namespace1, 2", + "1", [ - [0, 0.5], - [0.5, 1] + "namespace1", + 0.4, + 1 ], - 0 + true ], [ - "even range, 0.6", - 0.6, + "user 1, namespace2, 1", + "1", [ - [0, 0.5], - [0.5, 1] + "namespace2", + 0, + 0.4 ], - 1 + false ], [ - "even range, 0.8", - 0.8, + "user 1, namespace2, 2", + "1", [ - [0, 0.5], - [0.5, 1] + "namespace2", + 0.4, + 1 ], - 1 + true ], [ - "even range, 0", - 0, + "user 2, namespace1, 1", + "2", [ - [0, 0.5], - [0.5, 1] + "namespace1", + 0, + 0.4 ], - 0 + false ], [ - "even range, 0.5", - 0.5, + "user 2, namespace1, 2", + "2", [ - [0, 0.5], - [0.5, 1] + "namespace1", + 0.4, + 1 ], - 1 + true ], [ - "reduced range, 0.2", - 0.2, + "user 2, namespace2, 1", + "2", [ - [0, 0.25], - [0.5, 0.75] + "namespace2", + 0, + 0.4 ], - 0 + false ], [ - "reduced range, 0.4", - 0.4, + "user 2, namespace2, 2", + "2", [ - [0, 0.25], - [0.5, 0.75] + "namespace2", + 0.4, + 1 ], - -1 + true ], [ - "reduced range, 0.6", - 0.6, + "user 3, namespace1, 1", + "3", [ - [0, 0.25], - [0.5, 0.75] + "namespace1", + 0, + 0.4 ], - 1 + false ], [ - "reduced range, 0.8", - 0.8, + "user 3, namespace1, 2", + "3", [ - [0, 0.25], - [0.5, 0.75] + "namespace1", + 0.4, + 1 ], - -1 + true ], [ - "reduced range, 0.25", - 0.25, + "user 3, namespace2, 1", + "3", [ - [0, 0.25], - [0.5, 0.75] + "namespace2", + 0, + 0.4 ], - -1 + true ], [ - "reduced range, 0.5", - 0.5, + "user 3, namespace2, 2", + "3", [ - [0, 0.25], - [0.5, 0.75] + "namespace2", + 0.4, + 1 ], - 1 + false ], [ - "zero range", - 0.5, + "user 4, namespace1, 1", + "4", [ - [0, 0.5], - [0.5, 0.5], - [0.5, 1] + "namespace1", + 0, + 0.4 ], - 2 + false + ], + [ + "user 4, namespace1, 2", + "4", + [ + "namespace1", + 0.4, + 1 + ], + true + ], + [ + "user 4, namespace2, 1", + "4", + [ + "namespace2", + 0, + 0.4 + ], + true + ], + [ + "user 4, namespace2, 2", + "4", + [ + "namespace2", + 0.4, + 1 + ], + false ] ], - "getQueryStringOverride": [ - ["empty url", "my-test", "", 2, null], - ["no query string", "my-test", "http://example.com", 2, null], - ["empty query string", "my-test", "http://example.com?", 2, null], + "getEqualWeights": [ [ - "no query string match", - "my-test", - "http://example.com?somequery", - 2, - null + -1, + [] ], - ["invalid query string", "my-test", "http://example.com??&&&?#", 2, null], - ["simple match 0", "my-test", "http://example.com?my-test=0", 2, 0], - ["simple match 1", "my-test", "http://example.com?my-test=1", 2, 1], - ["negative variation", "my-test", "http://example.com?my-test=-1", 2, null], - ["float", "my-test", "http://example.com?my-test=2.054", 2, null], - ["string", "my-test", "http://example.com?my-test=foo", 2, null], - ["variation too high", "my-test", "http://example.com?my-test=5", 2, null], - ["high numVariations", "my-test", "http://example.com?my-test=5", 6, 5], [ - "equal to numVariations", - "my-test", - "http://example.com?my-test=5", - 5, - null + 0, + [] ], [ - "other query string before", - "my-test", - "http://example.com?foo=bar&my-test=1", - 2, - 1 + 1, + [ + 1 + ] ], [ - "other query string after", - "my-test", - "http://example.com?foo=bar&my-test=1&bar=baz", 2, - 1 + [ + 0.5, + 0.5 + ] ], - ["anchor", "my-test", "http://example.com?my-test=1#foo", 2, 1] - ], - "inNamespace": [ - ["user 1, namespace1, 1", "1", ["namespace1", 0, 0.4], false], - ["user 1, namespace1, 2", "1", ["namespace1", 0.4, 1], true], - ["user 1, namespace2, 1", "1", ["namespace2", 0, 0.4], false], - ["user 1, namespace2, 2", "1", ["namespace2", 0.4, 1], true], - ["user 2, namespace1, 1", "2", ["namespace1", 0, 0.4], false], - ["user 2, namespace1, 2", "2", ["namespace1", 0.4, 1], true], - ["user 2, namespace2, 1", "2", ["namespace2", 0, 0.4], false], - ["user 2, namespace2, 2", "2", ["namespace2", 0.4, 1], true], - ["user 3, namespace1, 1", "3", ["namespace1", 0, 0.4], false], - ["user 3, namespace1, 2", "3", ["namespace1", 0.4, 1], true], - ["user 3, namespace2, 1", "3", ["namespace2", 0, 0.4], true], - ["user 3, namespace2, 2", "3", ["namespace2", 0.4, 1], false], - ["user 4, namespace1, 1", "4", ["namespace1", 0, 0.4], false], - ["user 4, namespace1, 2", "4", ["namespace1", 0.4, 1], true], - ["user 4, namespace2, 1", "4", ["namespace2", 0, 0.4], true], - ["user 4, namespace2, 2", "4", ["namespace2", 0.4, 1], false] - ], - "getEqualWeights": [ - [-1, []], - [0, []], - [1, [1]], - [2, [0.5, 0.5]], - [3, [0.33333333, 0.33333333, 0.33333333]], - [4, [0.25, 0.25, 0.25, 0.25]] + [ + 3, + [ + 0.33333333, + 0.33333333, + 0.33333333 + ] + ], + [ + 4, + [ + 0.25, + 0.25, + 0.25, + 0.25 + ] + ] ], "decrypt": [ [ @@ -4791,13 +7557,20 @@ [ "use fallbackAttribute when missing hashAttribute", { - "attributes": { "anonymousId": "123" }, + "attributes": { + "anonymousId": "123" + }, "features": { "feature": { "defaultValue": 0, "rules": [ { - "variations": [0, 1, 2, 3], + "variations": [ + 0, + 1, + 2, + 3 + ], "hashAttribute": "id", "fallbackAttribute": "anonymousId" } @@ -4820,7 +7593,9 @@ }, { "anonymousId||123": { - "assignments": { "feature__0": "3" }, + "assignments": { + "feature__0": "3" + }, "attributeName": "anonymousId", "attributeValue": "123" } @@ -4846,11 +7621,31 @@ "fallbackAttribute": "deviceId", "hashVersion": 2, "bucketVersion": 0, - "condition": { "country": "USA" }, - "variations": ["control", "red", "blue"], - "meta": [{ "key": "0" }, { "key": "1" }, { "key": "2" }], + "condition": { + "country": "USA" + }, + "variations": [ + "control", + "red", + "blue" + ], + "meta": [ + { + "key": "0" + }, + { + "key": "1" + }, + { + "key": "2" + } + ], "coverage": 1, - "weights": [0.3334, 0.3333, 0.3333], + "weights": [ + 0.3334, + 0.3333, + 0.3333 + ], "phase": "0" } ] @@ -4873,7 +7668,9 @@ }, { "deviceId||d123": { - "assignments": { "feature-exp__0": "1" }, + "assignments": { + "feature-exp__0": "1" + }, "attributeName": "deviceId", "attributeValue": "d123" } @@ -4899,11 +7696,31 @@ "fallbackAttribute": "deviceId", "hashVersion": 2, "bucketVersion": 0, - "condition": { "country": "USA" }, - "variations": ["control", "red", "blue"], - "meta": [{ "key": "0" }, { "key": "1" }, { "key": "2" }], + "condition": { + "country": "USA" + }, + "variations": [ + "control", + "red", + "blue" + ], + "meta": [ + { + "key": "0" + }, + { + "key": "1" + }, + { + "key": "2" + } + ], "coverage": 1, - "weights": [0.3334, 0.3333, 0.3333], + "weights": [ + 0.3334, + 0.3333, + 0.3333 + ], "phase": "0" } ] @@ -4934,7 +7751,9 @@ }, { "deviceId||d123": { - "assignments": { "feature-exp__0": "2" }, + "assignments": { + "feature-exp__0": "2" + }, "attributeName": "deviceId", "attributeValue": "d123" } @@ -4960,11 +7779,31 @@ "fallbackAttribute": "deviceId", "hashVersion": 2, "bucketVersion": 0, - "condition": { "country": "USA" }, - "variations": ["control", "red", "blue"], - "meta": [{ "key": "0" }, { "key": "1" }, { "key": "2" }], + "condition": { + "country": "USA" + }, + "variations": [ + "control", + "red", + "blue" + ], + "meta": [ + { + "key": "0" + }, + { + "key": "1" + }, + { + "key": "2" + } + ], "coverage": 1, - "weights": [0.3334, 0.3333, 0.3333], + "weights": [ + 0.3334, + 0.3333, + 0.3333 + ], "phase": "0" } ] @@ -4995,12 +7834,16 @@ }, { "deviceId||d456": { - "assignments": { "feature-exp__0": "2" }, + "assignments": { + "feature-exp__0": "2" + }, "attributeName": "deviceId", "attributeValue": "d456" }, "deviceId||d123": { - "assignments": { "feature-exp__0": "1" }, + "assignments": { + "feature-exp__0": "1" + }, "attributeName": "deviceId", "attributeValue": "d123" } @@ -5026,11 +7869,31 @@ "fallbackAttribute": "anonymousId", "hashVersion": 2, "bucketVersion": 0, - "condition": { "country": "USA" }, - "variations": ["control", "red", "blue"], - "meta": [{ "key": "0" }, { "key": "1" }, { "key": "2" }], + "condition": { + "country": "USA" + }, + "variations": [ + "control", + "red", + "blue" + ], + "meta": [ + { + "key": "0" + }, + { + "key": "1" + }, + { + "key": "2" + } + ], "coverage": 1, - "weights": [0.3334, 0.3333, 0.3333], + "weights": [ + 0.3334, + 0.3333, + 0.3333 + ], "phase": "0" } ] @@ -5061,12 +7924,16 @@ }, { "anonymousId||ses123": { - "assignments": { "feature-exp__0": "1" }, + "assignments": { + "feature-exp__0": "1" + }, "attributeName": "anonymousId", "attributeValue": "ses123" }, "id||i123": { - "assignments": { "feature-exp__0": "1" }, + "assignments": { + "feature-exp__0": "1" + }, "attributeName": "id", "attributeValue": "i123" } @@ -5092,11 +7959,31 @@ "fallbackAttribute": "anonymousId", "hashVersion": 2, "bucketVersion": 0, - "condition": { "country": "USA" }, - "variations": ["control", "red", "blue"], - "meta": [{ "key": "0" }, { "key": "1" }, { "key": "2" }], + "condition": { + "country": "USA" + }, + "variations": [ + "control", + "red", + "blue" + ], + "meta": [ + { + "key": "0" + }, + { + "key": "1" + }, + { + "key": "2" + } + ], "coverage": 1, - "weights": [0.3334, 0.3333, 0.3333], + "weights": [ + 0.3334, + 0.3333, + 0.3333 + ], "phase": "0" } ] @@ -5134,12 +8021,16 @@ }, { "anonymousId||ses123": { - "assignments": { "feature-exp__0": "2" }, + "assignments": { + "feature-exp__0": "2" + }, "attributeName": "anonymousId", "attributeValue": "ses123" }, "id||i123": { - "assignments": { "feature-exp__0": "1" }, + "assignments": { + "feature-exp__0": "1" + }, "attributeName": "id", "attributeValue": "i123" } @@ -5164,11 +8055,31 @@ "fallbackAttribute": "deviceId", "hashVersion": 2, "bucketVersion": 3, - "condition": { "country": "USA" }, - "variations": ["control", "red", "blue"], - "meta": [{ "key": "0" }, { "key": "1" }, { "key": "2" }], + "condition": { + "country": "USA" + }, + "variations": [ + "control", + "red", + "blue" + ], + "meta": [ + { + "key": "0" + }, + { + "key": "1" + }, + { + "key": "2" + } + ], "coverage": 1, - "weights": [0.3334, 0.3333, 0.3333], + "weights": [ + 0.3334, + 0.3333, + 0.3333 + ], "phase": "0" } ] @@ -5176,7 +8087,9 @@ }, "stickyBucketAssignmentDocs": { "id||i123": { - "assignments": { "feature-exp__0": "1" }, + "assignments": { + "feature-exp__0": "1" + }, "attributeName": "id", "attributeValue": "i123" } @@ -5226,11 +8139,31 @@ "hashVersion": 2, "bucketVersion": 3, "minBucketVersion": 3, - "condition": { "country": "USA" }, - "variations": ["control", "red", "blue"], - "meta": [{ "key": "0" }, { "key": "1" }, { "key": "2" }], + "condition": { + "country": "USA" + }, + "variations": [ + "control", + "red", + "blue" + ], + "meta": [ + { + "key": "0" + }, + { + "key": "1" + }, + { + "key": "2" + } + ], "coverage": 1, - "weights": [0.3334, 0.3333, 0.3333], + "weights": [ + 0.3334, + 0.3333, + 0.3333 + ], "phase": "0" } ] @@ -5238,7 +8171,9 @@ }, "stickyBucketAssignmentDocs": { "id||i123": { - "assignments": { "feature-exp__0": "1" }, + "assignments": { + "feature-exp__0": "1" + }, "attributeName": "id", "attributeValue": "i123" } @@ -5276,11 +8211,31 @@ "hashVersion": 2, "bucketVersion": 1, "disableStickyBucketing": true, - "condition": { "country": "USA" }, - "variations": ["control", "red", "blue"], - "meta": [{ "key": "0" }, { "key": "1" }, { "key": "2" }], + "condition": { + "country": "USA" + }, + "variations": [ + "control", + "red", + "blue" + ], + "meta": [ + { + "key": "0" + }, + { + "key": "1" + }, + { + "key": "2" + } + ], "coverage": 1, - "weights": [0.3334, 0.3333, 0.3333], + "weights": [ + 0.3334, + 0.3333, + 0.3333 + ], "phase": "0" } ] @@ -5290,7 +8245,9 @@ "id||i123": { "attributeName": "id", "attributeValue": "i123", - "assignments": { "feature-exp__0": "1" } + "assignments": { + "feature-exp__0": "1" + } } } }, @@ -5311,7 +8268,9 @@ "id||i123": { "attributeName": "id", "attributeValue": "i123", - "assignments": { "feature-exp__0": "1" } + "assignments": { + "feature-exp__0": "1" + } } } ] @@ -5320,7 +8279,9 @@ [ "redirects correctly without query strings", { - "attributes": { "id": "1" }, + "attributes": { + "id": "1" + }, "url": "http://www.example.com/home", "experiments": [ { @@ -5332,7 +8293,10 @@ "pattern": "http://www.example.com/home" } ], - "weights": [0.1, 0.9], + "weights": [ + 0.1, + 0.9 + ], "variations": [ {}, { @@ -5353,7 +8317,9 @@ [ "redirects with query string on original url and persistQueryString enabled", { - "attributes": { "id": "1" }, + "attributes": { + "id": "1" + }, "url": "http://www.example.com/home?color=blue&food=sushi", "experiments": [ { @@ -5365,7 +8331,10 @@ "pattern": "http://www.example.com/home" } ], - "weights": [0.1, 0.9], + "weights": [ + 0.1, + 0.9 + ], "variations": [ {}, { @@ -5387,7 +8356,9 @@ [ "merges query strings on original url & redirect url with param conflicts correctly when persistQueryString enabled", { - "attributes": { "id": "1" }, + "attributes": { + "id": "1" + }, "url": "http://www.example.com/home?color=blue&food=sushi&title=original", "experiments": [ { @@ -5399,7 +8370,10 @@ "pattern": "http://www.example.com/home" } ], - "weights": [0.1, 0.9], + "weights": [ + 0.1, + 0.9 + ], "variations": [ {}, { @@ -5421,7 +8395,9 @@ [ "only performs a redirect for first eligible experiment when there are multiple eligible experiments", { - "attributes": { "id": "1" }, + "attributes": { + "id": "1" + }, "url": "http://www.example.com/home", "experiments": [ { @@ -5433,7 +8409,10 @@ "pattern": "http://www.example.com/" } ], - "weights": [0.1, 0.9], + "weights": [ + 0.1, + 0.9 + ], "variations": [ {}, { @@ -5450,7 +8429,10 @@ "pattern": "http://www.example.com/home" } ], - "weights": [0.1, 0.9], + "weights": [ + 0.1, + 0.9 + ], "variations": [ {}, { @@ -5467,7 +8449,10 @@ "pattern": "http://www.example.com/home" } ], - "weights": [0.1, 0.9], + "weights": [ + 0.1, + 0.9 + ], "variations": [ {}, { @@ -5486,4 +8471,4 @@ ] ] ] -} +} \ No newline at end of file diff --git a/tests/test_growthbook.py b/tests/test_growthbook.py index f3cbeda..ccf1a84 100644 --- a/tests/test_growthbook.py +++ b/tests/test_growthbook.py @@ -98,23 +98,8 @@ def test_equal_weights(getEqualWeights_data): def test_conditions(evalCondition_data): - _, condition, attributes, expected = evalCondition_data - assert evalCondition(attributes, condition) == expected - - -def test_version_lt(versionCompare_lt_data): - v1, v2, should_match = versionCompare_lt_data - assert (paddedVersionString(v1) < paddedVersionString(v2)) == should_match - - -def test_version_gt(versionCompare_gt_data): - v1, v2, should_match = versionCompare_gt_data - assert (paddedVersionString(v1) > paddedVersionString(v2)) == should_match - - -def test_version_eq(versionCompare_eq_data): - v1, v2, should_match = versionCompare_eq_data - assert (paddedVersionString(v1) == paddedVersionString(v2)) == should_match + _, condition, attributes, expected, savedGroups = (evalCondition_data + [None]*5)[:5] + assert evalCondition(attributes, condition, savedGroups) == expected def test_decrypt(decrypt_data): @@ -661,8 +646,8 @@ def __init__(self, status: int, data: str) -> None: def test_feature_repository(mocker): m = mocker.patch.object(feature_repo, "_get") - expected = {"feature": {"defaultValue": 5}} - m.return_value = MockHttpResp(200, json.dumps({"features": expected})) + expected = {"features": {"feature": {"defaultValue": 5}}} + m.return_value = MockHttpResp(200, json.dumps(expected)) features = feature_repo.load_features("https://cdn.growthbook.io", "sdk-abc123") m.assert_called_once_with("https://cdn.growthbook.io/api/features/sdk-abc123") From 07a84c901d527f85e8fd376dfb0c20c44f430788 Mon Sep 17 00:00:00 2001 From: vazarkevych Date: Wed, 28 Aug 2024 19:21:26 +0300 Subject: [PATCH 2/3] Merge branch main, added savedGroups decryption --- growthbook/growthbook.py | 265 +++++++++++++++++++++++++++++++++------ tests/test_growthbook.py | 2 +- 2 files changed, 228 insertions(+), 39 deletions(-) diff --git a/growthbook/growthbook.py b/growthbook/growthbook.py index 5763a84..a632af4 100644 --- a/growthbook/growthbook.py +++ b/growthbook/growthbook.py @@ -9,6 +9,7 @@ import sys import json from abc import ABC, abstractmethod +import threading import logging from typing import Optional, Any, Set, Tuple, List, Dict @@ -23,7 +24,9 @@ from base64 import b64decode from time import time import aiohttp +import asyncio +from aiohttp.client_exceptions import ClientConnectorError, ClientResponseError, ClientPayloadError from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.primitives import padding from urllib3 import PoolManager @@ -817,10 +820,138 @@ def destroy(self) -> None: self.docs.clear() +class SSEClient: + def __init__(self, api_host, client_key, on_event, reconnect_delay=5, headers=None): + self.api_host = api_host + self.client_key = client_key + + self.on_event = on_event + self.reconnect_delay = reconnect_delay + + self._sse_session = None + self._sse_thread = None + self._loop = None + + self.is_running = False + + self.headers = { + "Accept": "application/json; q=0.5, text/event-stream", + "Cache-Control": "no-cache", + } + + if headers: + self.headers.update(headers) + + def connect(self): + if self.is_running: + logger.debug("Streaming session is already running.") + return + + self.is_running = True + self._sse_thread = threading.Thread(target=self._run_sse_channel) + self._sse_thread.start() + + def disconnect(self): + self.is_running = False + if self._loop and self._loop.is_running(): + future = asyncio.run_coroutine_threadsafe(self._stop_session(), self._loop) + try: + future.result() + except Exception as e: + logger.error(f"Streaming disconnect error: {e}") + + if self._sse_thread: + self._sse_thread.join(timeout=5) + + logger.debug("Streaming session disconnected") + + def _get_sse_url(self, api_host: str, client_key: str) -> str: + api_host = (api_host or "https://cdn.growthbook.io").rstrip("/") + return f"{api_host}/sub/{client_key}" + + async def _init_session(self): + url = self._get_sse_url(self.api_host, self.client_key) + + while self.is_running: + try: + async with aiohttp.ClientSession(headers=self.headers) as session: + self._sse_session = session + + async with session.get(url) as response: + response.raise_for_status() + await self._process_response(response) + except ClientResponseError as e: + logger.error(f"Streaming error, closing connection: {e.status} {e.message}") + self.is_running = False + break + except (ClientConnectorError, ClientPayloadError) as e: + logger.error(f"Streaming error: {e}") + if not self.is_running: + break + await self._wait_for_reconnect() + except TimeoutError: + logger.warning(f"Streaming connection timed out after {self.timeout} seconds.") + await self._wait_for_reconnect() + except asyncio.CancelledError: + logger.debug("Streaming was cancelled.") + break + finally: + await self._close_session() + + async def _process_response(self, response): + event_data = {} + async for line in response.content: + decoded_line = line.decode('utf-8').strip() + if decoded_line.startswith("event:"): + event_data['type'] = decoded_line[len("event:"):].strip() + elif decoded_line.startswith("data:"): + event_data['data'] = event_data.get('data', '') + f"\n{decoded_line[len('data:'):].strip()}" + elif not decoded_line: + if 'type' in event_data and 'data' in event_data: + self.on_event(event_data) + event_data = {} + + if 'type' in event_data and 'data' in event_data: + self.on_event(event_data) + + async def _wait_for_reconnect(self): + logger.debug(f"Attempting to reconnect streaming in {self.reconnect_delay}") + await asyncio.sleep(self.reconnect_delay) + + async def _close_session(self): + if self._sse_session: + await self._sse_session.close() + logger.debug("Streaming session closed.") + + def _run_sse_channel(self): + self._loop = asyncio.new_event_loop() + + try: + self._loop.run_until_complete(self._init_session()) + except asyncio.CancelledError: + pass + finally: + self._loop.run_until_complete(self._loop.shutdown_asyncgens()) + self._loop.close() + + async def _stop_session(self): + if self._sse_session: + await self._sse_session.close() + + if self._loop and self._loop.is_running(): + tasks = [task for task in asyncio.all_tasks(self._loop) if not task.done()] + for task in tasks: + task.cancel() + try: + await task + except asyncio.CancelledError: + pass + class FeatureRepository(object): def __init__(self) -> None: self.cache: AbstractFeatureCache = InMemoryFeatureCache() self.http: Optional[PoolManager] = None + self.sse_client: Optional[SSEClient] = None def set_cache(self, cache: AbstractFeatureCache) -> None: self.cache = cache @@ -828,6 +959,9 @@ def set_cache(self, cache: AbstractFeatureCache) -> None: def clear_cache(self): self.cache.clear() + def save_in_cache(self, key: str, res, ttl: int = 60): + self.cache.set(key, res, ttl) + # Loads features with an in-memory cache in front def load_features( self, api_host: str, client_key: str, decryption_key: str = "", ttl: int = 60 @@ -892,31 +1026,49 @@ async def _fetch_and_decode_async(self, api_host: str, client_key: str) -> Optio except Exception as e: logger.warning("Failed to decode feature JSON from GrowthBook API: %s", e) return None - - # Fetch features from the GrowthBook API - def _fetch_features( - self, api_host: str, client_key: str, decryption_key: str = "" - ) -> Optional[Dict]: - decoded = self._fetch_and_decode(api_host, client_key) - if not decoded: - return None - - if "encryptedFeatures" in decoded: + + def decrypt_response(self, data, decryption_key: str): + if "encryptedFeatures" in data: if not decryption_key: raise ValueError("Must specify decryption_key") try: - decrypted = decrypt(decoded['encryptedFeatures'], decryption_key) - return json.loads(decrypted) + decryptedFeatures = decrypt(data["encryptedFeatures"], decryption_key) + data['features'] = json.loads(decryptedFeatures) + del data['encryptedFeatures'] except Exception: logger.warning( "Failed to decrypt features from GrowthBook API response" ) return None - elif "features" in decoded: - return decoded - else: + elif "features" not in data: logger.warning("GrowthBook API response missing features") + + if "encryptedSavedGroups" in data: + if not decryption_key: + raise ValueError("Must specify decryption_key") + try: + decryptedFeatures = decrypt(data["encryptedSavedGroups"], decryption_key) + data['savedGroups'] = json.loads(decryptedFeatures) + del data['encryptedSavedGroups'] + return data + except Exception: + logger.warning( + "Failed to decrypt saved groups from GrowthBook API response" + ) + + return data + + # Fetch features from the GrowthBook API + def _fetch_features( + self, api_host: str, client_key: str, decryption_key: str = "" + ) -> Optional[Dict]: + decoded = self._fetch_and_decode(api_host, client_key) + if not decoded: return None + + data = self.decrypt_response(decoded, decryption_key) + + return data async def _fetch_features_async( self, api_host: str, client_key: str, decryption_key: str = "" @@ -925,24 +1077,17 @@ async def _fetch_features_async( if not decoded: return None - if "encryptedFeatures" in decoded: - if not decryption_key: - raise ValueError("Must specify decryption_key") - try: - decryptedFeatures = decrypt(decoded["encryptedFeatures"], decryption_key) - decoded['features'] = json.loads(decryptedFeatures) - del decoded['encryptedFeatures'] - return decoded - except Exception: - logger.warning( - "Failed to decrypt features from GrowthBook API response" - ) - return None - elif "features" in decoded: - return decoded - else: - logger.warning("GrowthBook API response missing features") - return None + data = self.decrypt_response(decoded, decryption_key) + + return data + + + def startAutoRefresh(self, api_host, client_key, cb): + self.sse_client = self.sse_client or SSEClient(api_host=api_host, client_key=client_key, on_event=cb) + self.sse_client.connect() + + def stopAutoRefresh(self): + self.sse_client.disconnect() @staticmethod def _get_features_url(api_host: str, client_key: str) -> str: @@ -953,7 +1098,6 @@ def _get_features_url(api_host: str, client_key: str) -> str: # Singleton instance feature_repo = FeatureRepository() - class GrowthBook(object): def __init__( self, @@ -971,6 +1115,7 @@ def __init__( sticky_bucket_service: AbstractStickyBucketService = None, sticky_bucket_identifier_attributes: List[str] = None, savedGroups: dict = {}, + streaming: bool = False, # Deprecated args trackingCallback=None, qaMode: bool = False, @@ -997,6 +1142,8 @@ def __init__( self._qaMode = qa_mode or qaMode self._trackingCallback = on_experiment_viewed or trackingCallback + self._streaming = streaming + # Deprecated args self._user = user self._groups = groups @@ -1010,6 +1157,10 @@ def __init__( if features: self.setFeatures(features) + if self._streaming: + self.load_features() + self.startAutoRefresh() + def load_features(self) -> None: if not self._client_key: raise ValueError("Must specify `client_key` to refresh features") @@ -1030,11 +1181,49 @@ async def load_features_async(self) -> None: features = await feature_repo.load_features_async( self._api_host, self._client_key, self._decryption_key, self._cache_ttl ) - if features is not None and "features" in features: - self.setFeatures(features["features"]) - if features is not None and "savedGroups" in features: - self._saved_groups = features["savedGroups"] + if features is not None: + if "features" in features: + self.setFeatures(features["features"]) + if "savedGroups" in features: + self._saved_groups = features["savedGroups"] + feature_repo.save_in_cache(self._client_key, features, self._cache_ttl) + + def features_event_handler(self, features): + decoded = json.loads(features) + if not decoded: + return None + + data = feature_repo.decrypt_response(decoded, self._decryption_key) + + if data is not None: + if "features" in data: + self.setFeatures(data["features"]) + if "savedGroups" in data: + self._saved_groups = data["savedGroups"] + feature_repo.save_in_cache(self._client_key, features, self._cache_ttl) + + def dispatch_sse_event(self, event_data): + event_type = event_data['type'] + data = event_data['data'] + if event_type == 'features-updated': + self.load_features() + elif event_type == 'features': + self.features_event_handler(data) + + + def startAutoRefresh(self): + if not self._client_key: + raise ValueError("Must specify `client_key` to start features streaming") + + feature_repo.startAutoRefresh( + api_host=self._api_host, + client_key=self._client_key, + cb=self.dispatch_sse_event + ) + + def stopAutoRefresh(self): + feature_repo.stopAutoRefresh() # @deprecated, use set_features def setFeatures(self, features: dict) -> None: diff --git a/tests/test_growthbook.py b/tests/test_growthbook.py index ccf1a84..52ad529 100644 --- a/tests/test_growthbook.py +++ b/tests/test_growthbook.py @@ -707,7 +707,7 @@ def test_feature_repository_encrypted(mocker): ) m.assert_called_once_with("https://cdn.growthbook.io/api/features/sdk-abc123") - assert features == {"feature": {"defaultValue": True}} + assert features == {"features": {"feature": {"defaultValue": True}}} feature_repo.clear_cache() From 8a2f32dc95d3d06ee966d5379a8fa0fcd9102728 Mon Sep 17 00:00:00 2001 From: vazarkevych Date: Wed, 28 Aug 2024 19:35:40 +0300 Subject: [PATCH 3/3] Fix merging issues --- growthbook/growthbook.py | 59 +++------------------------------------- 1 file changed, 4 insertions(+), 55 deletions(-) diff --git a/growthbook/growthbook.py b/growthbook/growthbook.py index 6963ab6..20acb26 100644 --- a/growthbook/growthbook.py +++ b/growthbook/growthbook.py @@ -1082,14 +1082,6 @@ async def _fetch_features_async( return data - def startAutoRefresh(self, api_host, client_key, cb): - self.sse_client = self.sse_client or SSEClient(api_host=api_host, client_key=client_key, on_event=cb) - self.sse_client.connect() - - def stopAutoRefresh(self): - self.sse_client.disconnect() - - def startAutoRefresh(self, api_host, client_key, cb): self.sse_client = self.sse_client or SSEClient(api_host=api_host, client_key=client_key, on_event=cb) self.sse_client.connect() @@ -1197,7 +1189,7 @@ async def load_features_async(self) -> None: self._saved_groups = features["savedGroups"] feature_repo.save_in_cache(self._client_key, features, self._cache_ttl) - def features_event_handler(self, features): + def _features_event_handler(self, features): decoded = json.loads(features) if not decoded: return None @@ -1211,56 +1203,13 @@ def features_event_handler(self, features): self._saved_groups = data["savedGroups"] feature_repo.save_in_cache(self._client_key, features, self._cache_ttl) - def dispatch_sse_event(self, event_data): - event_type = event_data['type'] - data = event_data['data'] - if event_type == 'features-updated': - self.load_features() - elif event_type == 'features': - self.features_event_handler(data) - - - def startAutoRefresh(self): - if not self._client_key: - raise ValueError("Must specify `client_key` to start features streaming") - - feature_repo.startAutoRefresh( - api_host=self._api_host, - client_key=self._client_key, - cb=self.dispatch_sse_event - ) - - def stopAutoRefresh(self): - feature_repo.stopAutoRefresh() - - def features_event_handler(self, features): - decoded = json.loads(features) - if not decoded: - return None - - if "encryptedFeatures" in decoded: - if not self._decryption_key: - raise ValueError("Must specify decryption_key") - try: - decrypted = decrypt(decoded["encryptedFeatures"], self._decryption_key) - return json.loads(decrypted) - except Exception: - logger.warning( - "Failed to decrypt features from GrowthBook API response" - ) - return None - elif "features" in decoded: - self.set_features(decoded["features"]) - else: - logger.warning("GrowthBook API response missing features") - - def dispatch_sse_event(self, event_data): + def _dispatch_sse_event(self, event_data): event_type = event_data['type'] data = event_data['data'] if event_type == 'features-updated': self.load_features() elif event_type == 'features': - self.features_event_handler(data) + self._features_event_handler(data) def startAutoRefresh(self): @@ -1270,7 +1219,7 @@ def startAutoRefresh(self): feature_repo.startAutoRefresh( api_host=self._api_host, client_key=self._client_key, - cb=self.dispatch_sse_event + cb=self._dispatch_sse_event ) def stopAutoRefresh(self):