Skip to content

Commit

Permalink
Merge pull request #19 from growthbook/feature/changelog_v0.7.0
Browse files Browse the repository at this point in the history
changelog v0.7.0
  • Loading branch information
vazarkevych authored Jul 5, 2024
2 parents 40ccf3e + 51e35d7 commit 8ea1fb9
Show file tree
Hide file tree
Showing 19 changed files with 391 additions and 105 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# 3.9.4
- New Operators $inGroup and $notInGroup to check Saved Groups by reference
- Add argument to evalCondition for definition of Saved Groups
- Add test cases for evalCondition, feature, and run using the new operators

# 3.9.3
- Support Caching Manager on Flutter Web
- Remove a broken test case
Expand Down
58 changes: 38 additions & 20 deletions lib/src/Evaluator/condition_evaluator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,13 @@ class GBConditionEvaluator {
/// This is the main function used to evaluate a condition. It loops through the condition key/value pairs and checks each entry:
/// - attributes : User Attributes
/// - condition : to be evaluated
bool isEvalCondition(Map<String, dynamic> attributes, dynamic conditionObj) {
bool isEvalCondition(
Map<String, dynamic> attributes,
dynamic conditionObj,
// Must be included for `condition` to correctly evaluate group Operators
SavedGroupsValues? savedGroups,
) {
savedGroups ??= {};
if (conditionObj is List) {
return false;
}
Expand All @@ -49,28 +55,28 @@ class GBConditionEvaluator {
var value = conditionObj[key];
switch (key) {
case "\$or":
if (!isEvalOr(attributes, value)) {
if (!isEvalOr(attributes, value, savedGroups)) {
return false;
}
break;
case "\$nor":
if (isEvalOr(attributes, value)) {
if (isEvalOr(attributes, value, savedGroups)) {
return false;
}
break;
case "\$and":
if (!isEvalAnd(attributes, value)) {
if (!isEvalAnd(attributes, value, savedGroups)) {
return false;
}
break;
case "\$not":
if (isEvalCondition(attributes, value)) {
if (isEvalCondition(attributes, value, savedGroups)) {
return false;
}
break;
default:
var element = getPath(attributes, key);
if (!isEvalConditionValue(value, element)) {
if (!isEvalConditionValue(value, element, savedGroups)) {
return false;
}
}
Expand All @@ -81,7 +87,7 @@ class GBConditionEvaluator {
}

/// Evaluate OR conditions against given attributes
bool isEvalOr(Map<String, dynamic> attributes, List conditionObj) {
bool isEvalOr(Map<String, dynamic> attributes, List conditionObj, SavedGroupsValues savedGroups) {
// If conditionObj is empty, return true
if (conditionObj.isEmpty) {
return true;
Expand All @@ -90,7 +96,7 @@ class GBConditionEvaluator {
for (var item in conditionObj) {
// If evalCondition(attributes, conditionObj[i]) is true, break out of
// the loop and return true
if (isEvalCondition(attributes, item)) {
if (isEvalCondition(attributes, item, savedGroups)) {
return true;
}
}
Expand All @@ -100,14 +106,14 @@ class GBConditionEvaluator {
}

/// Evaluate AND conditions against given attributes
bool isEvalAnd(dynamic attributes, List conditionObj) {
bool isEvalAnd(dynamic attributes, List conditionObj, SavedGroupsValues savedGroups) {
// Loop through the conditionObjects

// Loop through the conditionObjects
for (var item in conditionObj) {
// If evalCondition(attributes, conditionObj[i]) is true, break out of
// the loop and return false
if (!isEvalCondition(attributes, item)) {
if (!isEvalCondition(attributes, item, savedGroups)) {
return false;
}
}
Expand Down Expand Up @@ -180,7 +186,7 @@ class GBConditionEvaluator {
}

///Evaluates Condition Value against given condition & attributes
bool isEvalConditionValue(dynamic conditionValue, dynamic attributeValue) {
bool isEvalConditionValue(dynamic conditionValue, dynamic attributeValue, SavedGroupsValues savedGroups) {
// If conditionValue is a string, number, boolean, return true if it's
// "equal" to attributeValue and false if not.
if ((conditionValue as Object?).isPrimitive && (attributeValue as Object?).isPrimitive) {
Expand Down Expand Up @@ -212,7 +218,7 @@ class GBConditionEvaluator {
for (var key in conditionValue.keys) {
// If evalOperatorCondition(key, attributeValue, value)
// is false, return false
if (!evalOperatorCondition(key, attributeValue, conditionValue[key])) {
if (!evalOperatorCondition(key, attributeValue, conditionValue[key], savedGroups)) {
return false;
}
}
Expand All @@ -232,21 +238,21 @@ class GBConditionEvaluator {

/// This checks if attributeValue is an array, and if so at least one of the
/// array items must match the condition
bool elemMatch(dynamic attributeValue, dynamic condition) {
bool elemMatch(dynamic attributeValue, dynamic condition, SavedGroupsValues savedGroups) {
// Loop through items in attributeValue
if (attributeValue is List) {
for (final item in attributeValue) {
// If isOperatorObject(condition)
if (isOperatorObject(condition)) {
// If evalConditionValue(condition, item), break out of loop and
//return true
if (isEvalConditionValue(condition, item)) {
if (isEvalConditionValue(condition, item, savedGroups)) {
return true;
}
}
// Else if evalCondition(item, condition), break out of loop and
//return true
else if (isEvalCondition(item, condition)) {
else if (isEvalCondition(item, condition, savedGroups)) {
return true;
}
}
Expand All @@ -258,15 +264,20 @@ class GBConditionEvaluator {
/// This function is just a case statement that handles all the possible operators
/// There are basic comparison operators in the form attributeValue {op}
/// conditionValue.
bool evalOperatorCondition(String operator, dynamic attributeValue, dynamic conditionValue) {
bool evalOperatorCondition(
String operator,
dynamic attributeValue,
dynamic conditionValue,
SavedGroupsValues savedGroups,
) {
/// Evaluate TYPE operator - whether both are of the same type
if (operator == "\$type") {
return getType(attributeValue).name == conditionValue;
}

/// Evaluate NOT operator - whether condition doesn't contain attribute
if (operator == "\$not") {
return !isEvalConditionValue(conditionValue, attributeValue);
return !isEvalConditionValue(conditionValue, attributeValue, savedGroups);
}

/// Evaluate EXISTS operator - whether condition contains attribute
Expand All @@ -278,6 +289,13 @@ class GBConditionEvaluator {
}
}

switch (operator) {
case "\$inGroup":
return isIn(attributeValue, savedGroups[conditionValue] ?? []);
case "\$notInGroup":
return !isIn(attributeValue, savedGroups[conditionValue] ?? []);
}

/// There are three operators where conditionValue is an array
if (conditionValue is List) {
switch (operator) {
Expand All @@ -299,7 +317,7 @@ class GBConditionEvaluator {
for (var con in conditionValue) {
var result = false;
for (var attr in attributeValue) {
if (isEvalConditionValue(con, attr)) {
if (isEvalConditionValue(con, attr, savedGroups)) {
result = true;
}
}
Expand All @@ -319,12 +337,12 @@ class GBConditionEvaluator {
switch (operator) {
/// Evaluate ELEMENT-MATCH operator - whether condition matches attribute
case "\$elemMatch":
return elemMatch(attributeValue, conditionValue);
return elemMatch(attributeValue, conditionValue, savedGroups);

/// Evaluate SIE operator - whether condition size is same as that
/// of attribute
case "\$size":
return isEvalConditionValue(conditionValue, attributeValue.length);
return isEvalConditionValue(conditionValue, attributeValue.length, savedGroups);

default:
}
Expand Down
56 changes: 19 additions & 37 deletions lib/src/Evaluator/experiment_evaluator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@ class ExperimentEvaluator {
ExperimentEvaluator({required this.attributeOverrides});

// Takes Context and Experiment and returns ExperimentResult
GBExperimentResult evaluateExperiment(
GBContext context, GBExperiment experiment,
{String? featureId}) {
GBExperimentResult evaluateExperiment(GBContext context, GBExperiment experiment, {String? featureId}) {
// Check if experiment.variations has fewer than 2 variations
if (experiment.variations.length < 2 || context.enabled != true) {
// Return an ExperimentResult indicating not in experiment and variationId 0
Expand All @@ -26,13 +24,10 @@ class ExperimentEvaluator {
);
}

if (context.forcedVariation != null &&
context.forcedVariation!.containsKey(experiment.key)) {
if (context.forcedVariation != null && context.forcedVariation!.containsKey(experiment.key)) {
// Retrieve the forced variation for the experiment key
if (context.forcedVariation != null &&
context.forcedVariation?[experiment.key] != null) {
int forcedVariationIndex =
int.parse(context.forcedVariation![experiment.key].toString());
if (context.forcedVariation != null && context.forcedVariation?[experiment.key] != null) {
int forcedVariationIndex = int.parse(context.forcedVariation![experiment.key].toString());

// Return the experiment result using the forced variation index and indicating that no hash was used
return _getExperimentResult(
Expand All @@ -58,8 +53,7 @@ class ExperimentEvaluator {
final hashAttributeAndValue = GBUtils.getHashAttribute(
context: context,
attr: experiment.hashAttribute,
fallback: (context.stickyBucketService != null &&
(experiment.disableStickyBucketing != true))
fallback: (context.stickyBucketService != null && (experiment.disableStickyBucketing != true))
? experiment.fallbackAttribute
: null,
attributeOverrides: attributeOverrides,
Expand All @@ -83,8 +77,7 @@ class ExperimentEvaluator {
bool foundStickyBucket = false;
bool stickyBucketVersionIsBlocked = false;

if (context.stickyBucketService != null &&
(experiment.disableStickyBucketing != true)) {
if (context.stickyBucketService != null && (experiment.disableStickyBucketing != true)) {
final stickyBucketResult = GBUtils.getStickyBucketVariation(
context: context,
experimentKey: experiment.key,
Expand All @@ -97,14 +90,12 @@ class ExperimentEvaluator {
);
foundStickyBucket = stickyBucketResult.variation >= 0;
assigned = stickyBucketResult.variation;
stickyBucketVersionIsBlocked =
stickyBucketResult.versionIsBlocked ?? false;
stickyBucketVersionIsBlocked = stickyBucketResult.versionIsBlocked ?? false;
}

if (!foundStickyBucket) {
if (experiment.filters != null) {
if (GBUtils.isFilteredOut(
experiment.filters!, context, attributeOverrides)) {
if (GBUtils.isFilteredOut(experiment.filters!, context, attributeOverrides)) {
log('Skip because of filters');
return _getExperimentResult(
featureId: featureId,
Expand All @@ -131,8 +122,7 @@ class ExperimentEvaluator {
}

if (experiment.condition != null &&
!GBConditionEvaluator()
.isEvalCondition(context.attributes!, experiment.condition!)) {
!GBConditionEvaluator().isEvalCondition(context.attributes!, experiment.condition!, context.savedGroups)) {
return _getExperimentResult(
featureId: featureId,
context: context,
Expand All @@ -150,8 +140,7 @@ class ExperimentEvaluator {
attributeOverrides: parentCondition.condition,
).evaluateFeature();

if (parentResult.source?.name ==
GBFeatureSource.cyclicPrerequisite.name) {
if (parentResult.source?.name == GBFeatureSource.cyclicPrerequisite.name) {
return _getExperimentResult(
featureId: featureId,
context: context,
Expand All @@ -165,6 +154,7 @@ class ExperimentEvaluator {
final evalCondition = GBConditionEvaluator().isEvalCondition(
evalObj,
parentCondition.condition,
context.savedGroups,
);

if (!evalCondition) {
Expand Down Expand Up @@ -262,22 +252,19 @@ class ExperimentEvaluator {
stickyBucketUsed: foundStickyBucket,
);

if (context.stickyBucketService != null &&
(experiment.disableStickyBucketing != true)) {
if (context.stickyBucketService != null && (experiment.disableStickyBucketing != true)) {
final stickyBucketDoc = GBUtils.generateStickyBucketAssignmentDoc(
context: context,
attributeName: hashAttribute,
attributeValue: hashValue,
newAssignments: {
GBUtils.getStickyBucketExperimentKey(
experiment.key, experiment.bucketVersion ?? 0): result.key
GBUtils.getStickyBucketExperimentKey(experiment.key, experiment.bucketVersion ?? 0): result.key
},
);

if (stickyBucketDoc.hasChanged) {
context.stickyBucketAssignmentDocs ??= {};
context.stickyBucketAssignmentDocs![stickyBucketDoc.key] =
stickyBucketDoc.doc;
context.stickyBucketAssignmentDocs![stickyBucketDoc.key] = stickyBucketDoc.doc;
context.stickyBucketService?.saveAssignments(stickyBucketDoc.doc);
}
}
Expand All @@ -303,17 +290,15 @@ class ExperimentEvaluator {
int targetVariationIndex = variationIndex;

// Check whether variationIndex lies within bounds of variations size
if (targetVariationIndex < 0 ||
targetVariationIndex >= experiment.variations.length) {
if (targetVariationIndex < 0 || targetVariationIndex >= experiment.variations.length) {
// Set to 0
targetVariationIndex = 0;
inExperiment = false;
}
final hashResult = GBUtils.getHashAttribute(
context: context,
attr: experiment.hashAttribute,
fallback: (context.stickyBucketService != null &&
(experiment.disableStickyBucketing != true))
fallback: (context.stickyBucketService != null && (experiment.disableStickyBucketing != true))
? experiment.fallbackAttribute
: null,
attributeOverrides: attributeOverrides,
Expand All @@ -324,16 +309,13 @@ class ExperimentEvaluator {

// Retrieve experiment metadata
List<GBVariationMeta> experimentMeta = experiment.meta ?? [];
GBVariationMeta? meta = (experimentMeta.length > targetVariationIndex)
? experimentMeta[targetVariationIndex]
: null;
GBVariationMeta? meta =
(experimentMeta.length > targetVariationIndex) ? experimentMeta[targetVariationIndex] : null;

return GBExperimentResult(
inExperiment: inExperiment,
variationID: targetVariationIndex,
value: (experiment.variations.length > targetVariationIndex)
? experiment.variations[targetVariationIndex]
: {},
value: (experiment.variations.length > targetVariationIndex) ? experiment.variations[targetVariationIndex] : {},
hashAttribute: hashAttribute,
hashValue: hashValue,
key: meta?.key ?? '$targetVariationIndex',
Expand Down
2 changes: 2 additions & 0 deletions lib/src/Evaluator/feature_evaluator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ class FeatureEvaluator {
bool evalCondition = GBConditionEvaluator().isEvalCondition(
evalObj,
parentCondition.condition,
context.savedGroups,
);

// If the evaluation condition is false
Expand Down Expand Up @@ -125,6 +126,7 @@ class FeatureEvaluator {
!GBConditionEvaluator().isEvalCondition(
getAttributes(),
rule.condition!,
context.savedGroups,
)) {
log('Skip rule because of condition');
continue; // Skip to the next rule
Expand Down
2 changes: 2 additions & 0 deletions lib/src/Features/feature_data_source.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ abstract class FeaturesFlowDelegate {
void featuresFetchedSuccessfully({required GBFeatures gbFeatures, required bool isRemote});
void featuresAPIModelSuccessfully(FeaturedDataModel model);
void featuresFetchFailed({required GBError? error, required bool isRemote});
void savedGroupsFetchedSuccessfully({required SavedGroupsValues savedGroups, required bool isRemote});
void savedGroupsFetchFailed({required GBError? error, required bool isRemote});
}

class FeatureDataSource {
Expand Down
Loading

0 comments on commit 8ea1fb9

Please sign in to comment.