Skip to content

Commit 907917e

Browse files
authored
[FSSDK-11551] Adding project config support for holdouts to go-sdk (#415)
* adding project config support * simplify * clean formatting * Implement holdout support per reviewer requirements - Remove HoldoutIDs exposure, expose holdout lists instead - Holdout logic consolidated in config.go (not separate files) - Only Running status supported - Add comprehensive tests based on JS/Swift SDK patterns - GetHoldoutsForFlag returns []entities.Holdout objects - Proper holdout categorization (global/included/excluded) - Result caching for performance
1 parent e9f9fa7 commit 907917e

File tree

6 files changed

+533
-123
lines changed

6 files changed

+533
-123
lines changed

pkg/config/datafileprojectconfig/config.go

Lines changed: 104 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"errors"
2222
"fmt"
2323

24+
datafileEntities "github.com/optimizely/go-sdk/v2/pkg/config/datafileprojectconfig/entities"
2425
"github.com/optimizely/go-sdk/v2/pkg/config/datafileprojectconfig/mappers"
2526
"github.com/optimizely/go-sdk/v2/pkg/entities"
2627
"github.com/optimizely/go-sdk/v2/pkg/logging"
@@ -58,8 +59,12 @@ type DatafileProjectConfig struct {
5859
sdkKey string
5960
environmentKey string
6061
region string
61-
62-
flagVariationsMap map[string][]entities.Variation
62+
flagVariationsMap map[string][]entities.Variation
63+
holdoutIDMap map[string]entities.Holdout
64+
globalHoldouts []entities.Holdout
65+
includedHoldouts map[string][]entities.Holdout
66+
excludedHoldouts map[string][]entities.Holdout
67+
flagHoldoutsMap map[string][]entities.Holdout
6368
}
6469

6570
// GetDatafile returns a string representation of the environment's datafile
@@ -266,6 +271,56 @@ func (c DatafileProjectConfig) GetGroupByID(groupID string) (entities.Group, err
266271
return entities.Group{}, fmt.Errorf(`group with ID "%s" not found`, groupID)
267272
}
268273

274+
// GetHoldoutsForFlag returns the holdouts that apply to a specific flag
275+
func (c *DatafileProjectConfig) GetHoldoutsForFlag(flagKey string) []entities.Holdout {
276+
// Get flag ID from key
277+
feature, exists := c.featureMap[flagKey]
278+
if !exists {
279+
return []entities.Holdout{}
280+
}
281+
282+
flagID := feature.ID
283+
284+
// Check cache first
285+
if cachedHoldouts, exists := c.flagHoldoutsMap[flagID]; exists {
286+
return cachedHoldouts
287+
}
288+
289+
holdouts := []entities.Holdout{}
290+
291+
// Add global holdouts that don't exclude this flag
292+
for _, holdout := range c.globalHoldouts {
293+
isExcluded := false
294+
for _, excludedFlagID := range holdout.ExcludedFlags {
295+
if excludedFlagID == flagID {
296+
isExcluded = true
297+
break
298+
}
299+
}
300+
if !isExcluded {
301+
holdouts = append(holdouts, holdout)
302+
}
303+
}
304+
305+
// Add holdouts that specifically include this flag
306+
if includedHoldouts, exists := c.includedHoldouts[flagID]; exists {
307+
holdouts = append(holdouts, includedHoldouts...)
308+
}
309+
310+
// Cache the result
311+
c.flagHoldoutsMap[flagID] = holdouts
312+
313+
return holdouts
314+
}
315+
316+
// GetHoldout returns a holdout by its ID
317+
func (c DatafileProjectConfig) GetHoldout(holdoutID string) (entities.Holdout, error) {
318+
if holdout, ok := c.holdoutIDMap[holdoutID]; ok {
319+
return holdout, nil
320+
}
321+
return entities.Holdout{}, fmt.Errorf(`holdout with ID "%s" not found`, holdoutID)
322+
}
323+
269324
// SendFlagDecisions determines whether impressions events are sent for ALL decision types
270325
func (c DatafileProjectConfig) SendFlagDecisions() bool {
271326
return c.sendFlagDecisions
@@ -324,6 +379,48 @@ func NewDatafileProjectConfig(jsonDatafile []byte, logger logging.OptimizelyLogP
324379
audienceMap, audienceSegmentList := mappers.MapAudiences(append(datafile.TypedAudiences, datafile.Audiences...))
325380
flagVariationsMap := mappers.MapFlagVariations(featureMap)
326381

382+
// Process holdouts
383+
holdoutIDMap := make(map[string]entities.Holdout)
384+
globalHoldouts := []entities.Holdout{}
385+
includedHoldouts := make(map[string][]entities.Holdout)
386+
excludedHoldouts := make(map[string][]entities.Holdout)
387+
flagHoldoutsMap := make(map[string][]entities.Holdout)
388+
389+
for _, datafileHoldout := range datafile.Holdouts {
390+
// Only process running holdouts
391+
if datafileHoldout.Status != datafileEntities.HoldoutStatusRunning {
392+
continue
393+
}
394+
395+
// Create runtime holdout entity
396+
holdout := entities.Holdout{
397+
ID: datafileHoldout.ID,
398+
Key: datafileHoldout.Key,
399+
Status: entities.HoldoutStatus(datafileHoldout.Status),
400+
IncludedFlags: datafileHoldout.IncludedFlags,
401+
ExcludedFlags: datafileHoldout.ExcludedFlags,
402+
}
403+
404+
// Add to ID map
405+
holdoutIDMap[holdout.ID] = holdout
406+
407+
// Categorize holdouts based on flag targeting
408+
if len(datafileHoldout.IncludedFlags) == 0 {
409+
// This is a global holdout (applies to all flags unless excluded)
410+
globalHoldouts = append(globalHoldouts, holdout)
411+
412+
// Add to excluded flags map
413+
for _, flagID := range datafileHoldout.ExcludedFlags {
414+
excludedHoldouts[flagID] = append(excludedHoldouts[flagID], holdout)
415+
}
416+
} else {
417+
// This holdout specifically includes certain flags
418+
for _, flagID := range datafileHoldout.IncludedFlags {
419+
includedHoldouts[flagID] = append(includedHoldouts[flagID], holdout)
420+
}
421+
}
422+
}
423+
327424
attributeKeyMap := make(map[string]entities.Attribute)
328425
attributeIDToKeyMap := make(map[string]string)
329426

@@ -365,6 +462,11 @@ func NewDatafileProjectConfig(jsonDatafile []byte, logger logging.OptimizelyLogP
365462
attributeKeyMap: attributeKeyMap,
366463
attributeIDToKeyMap: attributeIDToKeyMap,
367464
region: region,
465+
holdoutIDMap: holdoutIDMap,
466+
globalHoldouts: globalHoldouts,
467+
includedHoldouts: includedHoldouts,
468+
excludedHoldouts: excludedHoldouts,
469+
flagHoldoutsMap: flagHoldoutsMap,
368470
}
369471

370472
logger.Info("Datafile is valid.")

0 commit comments

Comments
 (0)