Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:

- name: SonarQube Scan (Push)
if: ${{ github.event_name == 'push' }}
uses: SonarSource/sonarcloud-github[email protected]
uses: SonarSource/sonarqube-scan[email protected]
env:
SONAR_TOKEN: ${{ secrets.SONARQUBE_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand All @@ -46,7 +46,7 @@ jobs:

- name: SonarQube Scan (Pull Request)
if: ${{ github.event_name == 'pull_request' }}
uses: SonarSource/sonarcloud-github[email protected]
uses: SonarSource/sonarqube-scan[email protected]
env:
SONAR_TOKEN: ${{ secrets.SONARQUBE_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand Down
5 changes: 5 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
9.0.0 (Nov 21, 2025)
- BREAKING CHANGE:
- Changed new evaluator.
- Added fallback treatment.

8.0.0 (Nov 10, 2025)
- BREAKING CHANGE:
- Changed Rule Based Segment and Feature Flag storages.
Expand Down
3 changes: 3 additions & 0 deletions conf/conf.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package conf

import (
"crypto/tls"

"github.com/splitio/go-split-commons/v9/dtos"
)

// RedisConfig struct is used to cofigure the redis parameters
Expand Down Expand Up @@ -89,6 +91,7 @@ type AdvancedConfig struct {
FlagsSpecVersion string
LargeSegment *LargeSegmentConfig
RulesConfig *RulesConfig
FallbackTreatment dtos.FallbackTreatmentConfig
}

type LargeSegmentConfig struct {
Expand Down
68 changes: 68 additions & 0 deletions dtos/fallbacktreatment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package dtos

const (
labelPrefix = "fallback - "
)

type FallbackTreatment struct {
Treatment *string
Config *string
label *string
}

func (f *FallbackTreatment) Label() *string {
return f.label
}

type FallbackTreatmentConfig struct {
GlobalFallbackTreatment *FallbackTreatment
ByFlagFallbackTreatment map[string]FallbackTreatment
}

type FallbackTreatmentCalculator interface {
Resolve(flagName string, label *string) FallbackTreatment
}

type FallbackTreatmentCalculatorImp struct {
fallbackTreatmentConfig *FallbackTreatmentConfig
}

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 val, ok := byFlag[flagName]; ok {
return FallbackTreatment{
Treatment: val.Treatment,
Config: val.Config,
label: f.resolveLabel(label),
}
}
}
if f.fallbackTreatmentConfig.GlobalFallbackTreatment != nil {
return FallbackTreatment{
Treatment: f.fallbackTreatmentConfig.GlobalFallbackTreatment.Treatment,
Config: f.fallbackTreatmentConfig.GlobalFallbackTreatment.Config,
label: f.resolveLabel(label),
}
}
}
controlTreatment := "control"
return FallbackTreatment{
Treatment: &controlTreatment,
label: label,
}
}

func (f *FallbackTreatmentCalculatorImp) resolveLabel(label *string) *string {
if label == nil {
return nil
}
result := labelPrefix + *label
return &result
}
65 changes: 65 additions & 0 deletions dtos/fallbacktreatment_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package dtos

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestFallbackTreatmentCalculatorResolve(t *testing.T) {
// Initial setup with both global and flag-specific treatments
stringConfig := "flag1_config"
globalTreatment := "global_treatment"
flag1Treatment := "flag1_treatment"
config := &FallbackTreatmentConfig{
GlobalFallbackTreatment: &FallbackTreatment{
Treatment: &globalTreatment,
},
ByFlagFallbackTreatment: map[string]FallbackTreatment{
"flag1": {
Treatment: &flag1Treatment,
Config: &stringConfig,
},
},
}
calc := NewFallbackTreatmentCalculatorImp(config)

// Test flag-specific treatment with label
label := "some_label"
result := calc.Resolve("flag1", &label)
assert.Equal(t, "flag1_treatment", *result.Treatment)
assert.Equal(t, &stringConfig, result.Config)
assert.Equal(t, "fallback - some_label", *result.label)

// Test fallback to global treatment when flag not found
result = calc.Resolve("flag2", &label)
assert.Equal(t, "global_treatment", *result.Treatment)
assert.Nil(t, result.Config)
assert.Equal(t, "fallback - some_label", *result.label)

// Test nil label handling
result = calc.Resolve("flag1", nil)
assert.Equal(t, "flag1_treatment", *result.Treatment)
assert.Equal(t, &stringConfig, result.Config)
assert.Nil(t, result.label)

// Test default control when no config
calcNoConfig := NewFallbackTreatmentCalculatorImp(nil)
result = calcNoConfig.Resolve("flag1", &label)
assert.Equal(t, "control", *result.Treatment)
assert.Nil(t, result.Config)
assert.Equal(t, "some_label", *result.label)

// Test global treatment when no flag-specific treatments exist
configGlobalOnly := &FallbackTreatmentConfig{
GlobalFallbackTreatment: &FallbackTreatment{
Treatment: &globalTreatment,
Config: nil,
},
}
calcGlobalOnly := NewFallbackTreatmentCalculatorImp(configGlobalOnly)
result = calcGlobalOnly.Resolve("any_flag", &label)
assert.Equal(t, "global_treatment", *result.Treatment)
assert.Nil(t, result.Config)
assert.Equal(t, "fallback - some_label", *result.label)
}
8 changes: 4 additions & 4 deletions engine/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import (
"fmt"
"math"

"github.com/splitio/go-split-commons/v8/engine/evaluator/impressionlabels"
"github.com/splitio/go-split-commons/v8/engine/grammar"
"github.com/splitio/go-split-commons/v8/engine/grammar/constants"
"github.com/splitio/go-split-commons/v8/engine/hash"
"github.com/splitio/go-split-commons/v9/engine/evaluator/impressionlabels"
"github.com/splitio/go-split-commons/v9/engine/grammar"
"github.com/splitio/go-split-commons/v9/engine/grammar/constants"
"github.com/splitio/go-split-commons/v9/engine/hash"

"github.com/splitio/go-toolkit/v5/hasher"
"github.com/splitio/go-toolkit/v5/logging"
Expand Down
8 changes: 4 additions & 4 deletions engine/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import (
"os"
"testing"

"github.com/splitio/go-split-commons/v8/dtos"
"github.com/splitio/go-split-commons/v8/engine/grammar"
"github.com/splitio/go-split-commons/v8/engine/grammar/constants"
"github.com/splitio/go-split-commons/v8/engine/hash"
"github.com/splitio/go-split-commons/v9/dtos"
"github.com/splitio/go-split-commons/v9/engine/grammar"
"github.com/splitio/go-split-commons/v9/engine/grammar/constants"
"github.com/splitio/go-split-commons/v9/engine/hash"

"github.com/splitio/go-toolkit/v5/hasher"
"github.com/splitio/go-toolkit/v5/logging"
Expand Down
42 changes: 28 additions & 14 deletions engine/evaluator/evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import (
"fmt"
"time"

"github.com/splitio/go-split-commons/v8/dtos"
"github.com/splitio/go-split-commons/v8/engine"
"github.com/splitio/go-split-commons/v8/engine/evaluator/impressionlabels"
"github.com/splitio/go-split-commons/v8/engine/grammar"
"github.com/splitio/go-split-commons/v8/storage"
"github.com/splitio/go-split-commons/v9/dtos"
"github.com/splitio/go-split-commons/v9/engine"
"github.com/splitio/go-split-commons/v9/engine/evaluator/impressionlabels"
"github.com/splitio/go-split-commons/v9/engine/grammar"
"github.com/splitio/go-split-commons/v9/storage"

"github.com/splitio/go-toolkit/v5/logging"
)
Expand Down Expand Up @@ -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
Expand All @@ -53,21 +54,25 @@ 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
}

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)
Expand Down Expand Up @@ -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
Expand Down
Loading