From 0ec0c20dbc07b860f8551319a2305458976be388 Mon Sep 17 00:00:00 2001 From: Nadia Mayor Date: Tue, 18 Nov 2025 11:52:12 -0300 Subject: [PATCH] Added fallback treatment in the evaluator --- dtos/fallbacktreatment.go | 10 ++-- dtos/fallbacktreatment_test.go | 2 +- engine/evaluator/evaluator.go | 32 +++++++++---- engine/evaluator/evaluator_test.go | 46 +++++++++++++++---- .../dependency_test/dependency_test.go | 1 + 5 files changed, 66 insertions(+), 25 deletions(-) diff --git a/dtos/fallbacktreatment.go b/dtos/fallbacktreatment.go index e1c30a44..482f2c9d 100644 --- a/dtos/fallbacktreatment.go +++ b/dtos/fallbacktreatment.go @@ -12,26 +12,26 @@ type FallbackTreatment struct { type FallbackTreatmentConfig struct { GlobalFallbackTreatment *FallbackTreatment - byFlagFallbackTreatment map[string]FallbackTreatment + ByFlagFallbackTreatment map[string]FallbackTreatment } type FallbackTreatmentCalculator interface { - Resolve(flagName string, label string) FallbackTreatment + Resolve(flagName string, label *string) FallbackTreatment } type FallbackTreatmentCalculatorImp struct { fallbackTreatmentConfig *FallbackTreatmentConfig } -func NewFallbackTreatmentCalculatorImp(fallbackTreatmentConfig *FallbackTreatmentConfig) FallbackTreatmentCalculatorImp { - return FallbackTreatmentCalculatorImp{ +func NewFallbackTreatmentCalculatorImp(fallbackTreatmentConfig *FallbackTreatmentConfig) FallbackTreatmentCalculator { + return &FallbackTreatmentCalculatorImp{ fallbackTreatmentConfig: fallbackTreatmentConfig, } } func (f *FallbackTreatmentCalculatorImp) Resolve(flagName string, label *string) FallbackTreatment { if f.fallbackTreatmentConfig != nil { - if byFlag := f.fallbackTreatmentConfig.byFlagFallbackTreatment; byFlag != nil { + if byFlag := f.fallbackTreatmentConfig.ByFlagFallbackTreatment; byFlag != nil { if val, ok := byFlag[flagName]; ok { return FallbackTreatment{ Treatment: val.Treatment, diff --git a/dtos/fallbacktreatment_test.go b/dtos/fallbacktreatment_test.go index c741a103..aa3f148e 100644 --- a/dtos/fallbacktreatment_test.go +++ b/dtos/fallbacktreatment_test.go @@ -13,7 +13,7 @@ func TestFallbackTreatmentCalculatorResolve(t *testing.T) { GlobalFallbackTreatment: &FallbackTreatment{ Treatment: "global_treatment", }, - byFlagFallbackTreatment: map[string]FallbackTreatment{ + ByFlagFallbackTreatment: map[string]FallbackTreatment{ "flag1": { Treatment: "flag1_treatment", Config: &stringConfig, diff --git a/engine/evaluator/evaluator.go b/engine/evaluator/evaluator.go index 045d782e..2e19bb4b 100644 --- a/engine/evaluator/evaluator.go +++ b/engine/evaluator/evaluator.go @@ -37,10 +37,11 @@ type Results struct { // Evaluator struct is the main evaluator type Evaluator struct { - splitStorage storage.SplitStorageConsumer - eng *engine.Engine - logger logging.LoggerInterface - ruleBuilder grammar.RuleBuilder + splitStorage storage.SplitStorageConsumer + eng *engine.Engine + logger logging.LoggerInterface + ruleBuilder grammar.RuleBuilder + fallbackTratmentCalculator dtos.FallbackTreatmentCalculator } // NewEvaluator instantiates an Evaluator struct and returns a reference to it @@ -53,11 +54,13 @@ func NewEvaluator( logger logging.LoggerInterface, featureFlagRules []string, ruleBasedSegmentRules []string, + fallbackTreatmentCalculator dtos.FallbackTreatmentCalculator, ) *Evaluator { e := &Evaluator{ - splitStorage: splitStorage, - eng: eng, - logger: logger, + splitStorage: splitStorage, + eng: eng, + logger: logger, + fallbackTratmentCalculator: fallbackTreatmentCalculator, } e.ruleBuilder = grammar.NewRuleBuilder(segmentStorage, ruleBasedSegmentStorage, largeSegmentStorage, featureFlagRules, ruleBasedSegmentRules, logger, e) return e @@ -65,9 +68,11 @@ func NewEvaluator( func (e *Evaluator) evaluateTreatment(key string, bucketingKey string, featureFlag string, splitDto *dtos.SplitDTO, attributes map[string]interface{}) *Result { var config *string + label := impressionlabels.SplitNotFound if splitDto == nil { - e.logger.Warning(fmt.Sprintf("Feature flag %s not found, returning control.", featureFlag)) - return &Result{Treatment: Control, Label: impressionlabels.SplitNotFound, Config: config} + fallbackTratment := e.fallbackTratmentCalculator.Resolve(featureFlag, &label) + e.logger.Warning(fmt.Sprintf("Feature flag %s not found, returning fallback treatment.", featureFlag)) + return &Result{Treatment: fallbackTratment.Treatment, Label: *fallbackTratment.Label, Config: fallbackTratment.Config} } split := grammar.NewSplit(splitDto, e.logger, e.ruleBuilder) @@ -113,6 +118,15 @@ func (e *Evaluator) evaluateTreatment(key string, bucketingKey string, featureFl label = impressionlabels.NoConditionMatched } + if *treatment == Control { + fallbackTreatment := e.fallbackTratmentCalculator.Resolve(featureFlag, &label) + return &Result{ + Treatment: fallbackTreatment.Treatment, + Label: *fallbackTreatment.Label, + Config: fallbackTreatment.Config, + } + } + if _, ok := split.Configurations()[*treatment]; ok { treatmentConfig := split.Configurations()[*treatment] config = &treatmentConfig diff --git a/engine/evaluator/evaluator_test.go b/engine/evaluator/evaluator_test.go index b3b4a70b..24435702 100644 --- a/engine/evaluator/evaluator_test.go +++ b/engine/evaluator/evaluator_test.go @@ -308,7 +308,8 @@ func TestSplitWithoutConfigurations(t *testing.T) { nil, logger, syncProxyFeatureFlagsRules, - syncProxyRuleBasedSegmentRules) + syncProxyRuleBasedSegmentRules, + nil) key := "test" result := evaluator.EvaluateFeature(key, &key, "mysplittest", nil) @@ -333,7 +334,8 @@ func TestSplitWithtConfigurations(t *testing.T) { nil, logger, syncProxyFeatureFlagsRules, - syncProxyRuleBasedSegmentRules) + syncProxyRuleBasedSegmentRules, + nil) key := "test" result := evaluator.EvaluateFeature(key, &key, "mysplittest2", nil) @@ -358,7 +360,8 @@ func TestSplitWithtConfigurationsButKilled(t *testing.T) { nil, logger, syncProxyFeatureFlagsRules, - syncProxyRuleBasedSegmentRules) + syncProxyRuleBasedSegmentRules, + nil) key := "test" result := evaluator.EvaluateFeature(key, &key, "mysplittest3", nil) @@ -383,7 +386,8 @@ func TestSplitWithConfigurationsButKilledWithConfigsOnDefault(t *testing.T) { nil, logger, syncProxyFeatureFlagsRules, - syncProxyRuleBasedSegmentRules) + syncProxyRuleBasedSegmentRules, + nil) key := "test" result := evaluator.EvaluateFeature(key, &key, "mysplittest4", nil) @@ -399,6 +403,14 @@ func TestSplitWithConfigurationsButKilledWithConfigsOnDefault(t *testing.T) { func TestMultipleEvaluations(t *testing.T) { logger := logging.NewLogger(nil) + fallbackTreatmentConfig := dtos.FallbackTreatmentConfig{GlobalFallbackTreatment: &dtos.FallbackTreatment{ + Treatment: "fallback", + }, + ByFlagFallbackTreatment: map[string]dtos.FallbackTreatment{ + "flag1": { + Treatment: "on", + }, + }} evaluator := NewEvaluator( &mockStorage{}, @@ -408,7 +420,8 @@ func TestMultipleEvaluations(t *testing.T) { nil, logger, syncProxyFeatureFlagsRules, - syncProxyRuleBasedSegmentRules) + syncProxyRuleBasedSegmentRules, + dtos.NewFallbackTreatmentCalculatorImp(&fallbackTreatmentConfig)) key := "test" splits := []string{"mysplittest", "mysplittest2", "mysplittest3", "mysplittest4", "mysplittest5"} @@ -442,7 +455,7 @@ func TestMultipleEvaluations(t *testing.T) { t.Error("Unexpected configs") } - if result.Evaluations["mysplittest5"].Treatment != "control" { + if result.Evaluations["mysplittest5"].Treatment != "fallback" { t.Error("Wrong treatment result") } if result.Evaluations["mysplittest5"].Config != nil { @@ -467,7 +480,8 @@ func TestNoConditionMatched(t *testing.T) { nil, logger, syncProxyFeatureFlagsRules, - syncProxyRuleBasedSegmentRules) + syncProxyRuleBasedSegmentRules, + nil) key := "test" result := evaluator.EvaluateFeature(key, &key, "some", nil) @@ -508,6 +522,15 @@ func TestEvaluationByFlagSets(t *testing.T) { }, } + fallbackTreatmentConfig := dtos.FallbackTreatmentConfig{GlobalFallbackTreatment: &dtos.FallbackTreatment{ + Treatment: "fallback", + }, + ByFlagFallbackTreatment: map[string]dtos.FallbackTreatment{ + "mysplittest5": { + Treatment: "on", + }, + }} + evaluator := NewEvaluator( mockedStorage, nil, @@ -516,7 +539,8 @@ func TestEvaluationByFlagSets(t *testing.T) { nil, logger, syncProxyFeatureFlagsRules, - syncProxyRuleBasedSegmentRules) + syncProxyRuleBasedSegmentRules, + dtos.NewFallbackTreatmentCalculatorImp(&fallbackTreatmentConfig)) result := evaluator.EvaluateFeatureByFlagSets(key, &key, []string{"set1", "set2", "set3"}, nil) if result.Evaluations["mysplittest"].Treatment != "off" { @@ -547,7 +571,7 @@ func TestEvaluationByFlagSets(t *testing.T) { t.Error("Unexpected configs") } - if result.Evaluations["mysplittest5"].Treatment != "control" { + if result.Evaluations["mysplittest5"].Treatment != "on" { t.Error("Wrong treatment result") } if result.Evaluations["mysplittest5"].Config != nil { @@ -652,6 +676,7 @@ func TestPrerequisitesMatching(t *testing.T) { logging.NewLogger(nil), syncProxyFeatureFlagsRules, syncProxyRuleBasedSegmentRules, + nil, ) // Create split DTO with prerequisites @@ -747,7 +772,8 @@ func TestEvaluationByFlagSetsASetEmpty(t *testing.T) { nil, logger, syncProxyFeatureFlagsRules, - syncProxyRuleBasedSegmentRules) + syncProxyRuleBasedSegmentRules, + nil) result := evaluator.EvaluateFeatureByFlagSets(key, &key, []string{"set2"}, nil) if len(result.Evaluations) != 0 { diff --git a/engine/grammar/dependency_test/dependency_test.go b/engine/grammar/dependency_test/dependency_test.go index c60eba94..cd17b0c7 100644 --- a/engine/grammar/dependency_test/dependency_test.go +++ b/engine/grammar/dependency_test/dependency_test.go @@ -137,6 +137,7 @@ func TestDependencyMatcher(t *testing.T) { logger, syncProxyFeatureFlagsRules, syncProxyRuleBasedSegmentRules, + nil, ) ruleBuilder := grammar.NewRuleBuilder(nil, nil, nil, syncProxyFeatureFlagsRules, syncProxyRuleBasedSegmentRules, logger, evaluator)