Skip to content
Closed
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
10 changes: 10 additions & 0 deletions pkg/cmab/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,16 @@ func (m *MockProjectConfig) GetHoldoutsForFlag(featureKey string) []entities.Hol
return args.Get(0).([]entities.Holdout)
}

func (m *MockProjectConfig) GetGlobalHoldouts() []entities.Holdout {
args := m.Called()
return args.Get(0).([]entities.Holdout)
}

func (m *MockProjectConfig) GetHoldoutsForRule(ruleID string) []entities.Holdout {
args := m.Called(ruleID)
return args.Get(0).([]entities.Holdout)
}

type CmabServiceTestSuite struct {
suite.Suite
mockClient *MockCmabClient
Expand Down
26 changes: 24 additions & 2 deletions pkg/config/datafileprojectconfig/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ type DatafileProjectConfig struct {
holdouts []entities.Holdout
holdoutIDMap map[string]entities.Holdout
flagHoldoutsMap map[string][]entities.Holdout
ruleHoldoutsMap map[string][]entities.Holdout
}

// GetDatafile returns a string representation of the environment's datafile
Expand Down Expand Up @@ -284,14 +285,34 @@ func (c DatafileProjectConfig) GetRegion() string {
return c.region
}

// GetHoldoutsForFlag returns all holdouts applicable to the given feature flag
// GetHoldoutsForFlag returns all holdouts applicable to the given feature flag.
// This returns global holdouts (includedRules == nil) for flag-level evaluation.
func (c DatafileProjectConfig) GetHoldoutsForFlag(featureKey string) []entities.Holdout {
if holdouts, exists := c.flagHoldoutsMap[featureKey]; exists {
return holdouts
}
return []entities.Holdout{}
}

// GetGlobalHoldouts returns all global holdouts (those with includedRules == nil).
func (c DatafileProjectConfig) GetGlobalHoldouts() []entities.Holdout {
global := []entities.Holdout{}
for _, h := range c.holdouts {
if h.IsGlobal() {
global = append(global, h)
}
}
return global
}

// GetHoldoutsForRule returns all local holdouts targeting the given rule ID.
func (c DatafileProjectConfig) GetHoldoutsForRule(ruleID string) []entities.Holdout {
if holdouts, exists := c.ruleHoldoutsMap[ruleID]; exists {
return holdouts
}
return []entities.Holdout{}
}

// NewDatafileProjectConfig initializes a new datafile from a json byte array using the default JSON datafile parser
func NewDatafileProjectConfig(jsonDatafile []byte, logger logging.OptimizelyLogProducer) (*DatafileProjectConfig, error) {
datafile, err := Parse(jsonDatafile)
Expand Down Expand Up @@ -338,7 +359,7 @@ func NewDatafileProjectConfig(jsonDatafile []byte, logger logging.OptimizelyLogP

audienceMap, audienceSegmentList := mappers.MapAudiences(append(datafile.TypedAudiences, datafile.Audiences...))
flagVariationsMap := mappers.MapFlagVariations(featureMap)
holdouts, holdoutIDMap, flagHoldoutsMap := mappers.MapHoldouts(datafile.Holdouts, featureMap)
holdouts, holdoutIDMap, flagHoldoutsMap, ruleHoldoutsMap := mappers.MapHoldouts(datafile.Holdouts, featureMap)

attributeKeyMap := make(map[string]entities.Attribute)
attributeIDToKeyMap := make(map[string]string)
Expand Down Expand Up @@ -384,6 +405,7 @@ func NewDatafileProjectConfig(jsonDatafile []byte, logger logging.OptimizelyLogP
holdouts: holdouts,
holdoutIDMap: holdoutIDMap,
flagHoldoutsMap: flagHoldoutsMap,
ruleHoldoutsMap: ruleHoldoutsMap,
}

logger.Info("Datafile is valid.")
Expand Down
4 changes: 4 additions & 0 deletions pkg/config/datafileprojectconfig/entities/entities.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ type Holdout struct {
AudienceConditions interface{} `json:"audienceConditions"`
Variations []Variation `json:"variations"`
TrafficAllocation []TrafficAllocation `json:"trafficAllocation"`
// IncludedRules is an optional list of rule IDs this holdout targets.
// nil means global holdout (applies to all rules); a non-nil slice (even
// empty) means local holdout (applies only to listed rules).
IncludedRules []string `json:"includedRules,omitempty"`
}

// Integration represents a integration from the Optimizely datafile
Expand Down
38 changes: 30 additions & 8 deletions pkg/config/datafileprojectconfig/mappers/holdout.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,24 @@ import (
"github.com/optimizely/go-sdk/v2/pkg/entities"
)

// MapHoldouts maps the raw datafile holdout entities to SDK Holdout entities
// All running holdouts apply to all flags
// MapHoldouts maps the raw datafile holdout entities to SDK Holdout entities.
// It returns:
// - holdoutList: all mapped running holdouts
// - holdoutIDMap: map from holdout ID to holdout
// - flagHoldoutsMap: map from feature key to global holdouts (for flag-level evaluation)
// - ruleHoldoutsMap: map from rule ID to local holdouts targeting that rule
func MapHoldouts(holdouts []datafileEntities.Holdout, featureMap map[string]entities.Feature) (
holdoutList []entities.Holdout,
holdoutIDMap map[string]entities.Holdout,
flagHoldoutsMap map[string][]entities.Holdout,
ruleHoldoutsMap map[string][]entities.Holdout,
) {
holdoutList = []entities.Holdout{}
holdoutIDMap = make(map[string]entities.Holdout)
flagHoldoutsMap = make(map[string][]entities.Holdout)
ruleHoldoutsMap = make(map[string][]entities.Holdout)

runningHoldouts := []entities.Holdout{}
globalHoldouts := []entities.Holdout{}

for _, holdout := range holdouts {
// Only process running holdouts
Expand All @@ -44,17 +50,26 @@ func MapHoldouts(holdouts []datafileEntities.Holdout, featureMap map[string]enti
mappedHoldout := mapHoldout(holdout)
holdoutList = append(holdoutList, mappedHoldout)
holdoutIDMap[holdout.ID] = mappedHoldout
runningHoldouts = append(runningHoldouts, mappedHoldout)

if mappedHoldout.IsGlobal() {
// Global holdout: applies to all rules across all flags
globalHoldouts = append(globalHoldouts, mappedHoldout)
} else {
// Local holdout: register per targeted rule ID
for _, ruleID := range mappedHoldout.IncludedRules {
ruleHoldoutsMap[ruleID] = append(ruleHoldoutsMap[ruleID], mappedHoldout)
}
}
}

// All running holdouts apply to all flags
// Populate flagHoldoutsMap with global holdouts only (flag-level evaluation)
for _, feature := range featureMap {
if len(runningHoldouts) > 0 {
flagHoldoutsMap[feature.Key] = runningHoldouts
if len(globalHoldouts) > 0 {
flagHoldoutsMap[feature.Key] = globalHoldouts
}
}

return holdoutList, holdoutIDMap, flagHoldoutsMap
return holdoutList, holdoutIDMap, flagHoldoutsMap, ruleHoldoutsMap
}

func mapHoldout(datafileHoldout datafileEntities.Holdout) entities.Holdout {
Expand Down Expand Up @@ -98,6 +113,12 @@ func mapHoldout(datafileHoldout datafileEntities.Holdout) entities.Holdout {
}
}

// Preserve IncludedRules as-is (nil = global, non-nil slice = local)
var includedRules []string
if datafileHoldout.IncludedRules != nil {
includedRules = datafileHoldout.IncludedRules
}

return entities.Holdout{
ID: datafileHoldout.ID,
Key: datafileHoldout.Key,
Expand All @@ -107,5 +128,6 @@ func mapHoldout(datafileHoldout datafileEntities.Holdout) entities.Holdout {
Variations: variations,
TrafficAllocation: trafficAllocation,
AudienceConditionTree: audienceConditionTree,
IncludedRules: includedRules,
}
}
Loading
Loading