Skip to content

Commit 88e9b1a

Browse files
committed
simplify
1 parent 8cf5b32 commit 88e9b1a

File tree

8 files changed

+355
-29
lines changed

8 files changed

+355
-29
lines changed

pkg/config/datafileprojectconfig/config.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,13 @@ func NewDatafileProjectConfig(jsonDatafile []byte, logger logging.OptimizelyLogP
318318
audienceMap, audienceSegmentList := mappers.MapAudiences(append(datafile.TypedAudiences, datafile.Audiences...))
319319
flagVariationsMap := mappers.MapFlagVariations(featureMap)
320320

321+
// Process holdouts and populate HoldoutIDs for features
322+
holdoutMaps := mappers.MapHoldouts(datafile.Holdouts, audienceMap)
323+
for featureKey, feature := range featureMap {
324+
feature.HoldoutIDs = mappers.GetHoldoutsForFlag(feature.ID, holdoutMaps)
325+
featureMap[featureKey] = feature
326+
}
327+
321328
attributeKeyMap := make(map[string]entities.Attribute)
322329
attributeIDToKeyMap := make(map[string]string)
323330

pkg/config/datafileprojectconfig/entities/entities.go

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,18 +40,24 @@ type Cmab struct {
4040
TrafficAllocation int `json:"trafficAllocation"`
4141
}
4242

43-
// Experiment represents an Experiment object from the Optimizely datafile
44-
type Experiment struct {
43+
// ExperimentCore contains the shared properties between Experiment and Holdout
44+
// This represents the common bucketing and targeting logic
45+
type ExperimentCore struct {
4546
ID string `json:"id"`
4647
Key string `json:"key"`
47-
LayerID string `json:"layerId"`
48-
Status string `json:"status"`
4948
Variations []Variation `json:"variations"`
5049
TrafficAllocation []TrafficAllocation `json:"trafficAllocation"`
5150
AudienceIds []string `json:"audienceIds"`
52-
ForcedVariations map[string]string `json:"forcedVariations"`
5351
AudienceConditions interface{} `json:"audienceConditions"`
54-
Cmab *Cmab `json:"cmab,omitempty"` // is optional
52+
}
53+
54+
// Experiment represents an Experiment object from the Optimizely datafile
55+
type Experiment struct {
56+
ExperimentCore
57+
LayerID string `json:"layerId"`
58+
Status string `json:"status"`
59+
ForcedVariations map[string]string `json:"forcedVariations"`
60+
Cmab *Cmab `json:"cmab,omitempty"` // is optional
5561
}
5662

5763
// Group represents an Group object from the Optimizely datafile
@@ -62,14 +68,36 @@ type Group struct {
6268
Experiments []Experiment `json:"experiments"`
6369
}
6470

71+
// HoldoutStatus represents the status of a holdout
72+
type HoldoutStatus string
73+
74+
const (
75+
// HoldoutStatusDraft - the holdout status is draft
76+
HoldoutStatusDraft HoldoutStatus = "Draft"
77+
// HoldoutStatusRunning - the holdout status is running
78+
HoldoutStatusRunning HoldoutStatus = "Running"
79+
// HoldoutStatusConcluded - the holdout status is concluded
80+
HoldoutStatusConcluded HoldoutStatus = "Concluded"
81+
// HoldoutStatusArchived - the holdout status is archived
82+
HoldoutStatusArchived HoldoutStatus = "Archived"
83+
)
84+
85+
// Holdout represents a Holdout object from the Optimizely datafile
86+
// Holdouts share core properties with Experiments through ExperimentCore embedding
87+
type Holdout struct {
88+
ExperimentCore
89+
Status HoldoutStatus `json:"status"`
90+
IncludedFlags []string `json:"includedFlags"`
91+
ExcludedFlags []string `json:"excludedFlags"`
92+
}
93+
6594
// FeatureFlag represents a FeatureFlag object from the Optimizely datafile
6695
type FeatureFlag struct {
6796
ID string `json:"id"`
6897
RolloutID string `json:"rolloutId"`
6998
Key string `json:"key"`
7099
ExperimentIDs []string `json:"experimentIds"`
71100
Variables []Variable `json:"variables"`
72-
HoldoutIDs []string `json:"holdoutIds"`
73101
}
74102

75103
// Variable represents a Variable object from the Optimizely datafile
@@ -133,6 +161,7 @@ type Datafile struct {
133161
Integrations []Integration `json:"integrations"`
134162
TypedAudiences []Audience `json:"typedAudiences"`
135163
Variables []string `json:"variables"`
164+
Holdouts []Holdout `json:"holdouts"`
136165
AccountID string `json:"accountId"`
137166
ProjectID string `json:"projectId"`
138167
Revision string `json:"revision"`
@@ -142,4 +171,5 @@ type Datafile struct {
142171
SendFlagDecisions bool `json:"sendFlagDecisions"`
143172
SDKKey string `json:"sdkKey,omitempty"`
144173
EnvironmentKey string `json:"environmentKey,omitempty"`
174+
Region string `json:"region,omitempty"`
145175
}

pkg/config/datafileprojectconfig/mappers/experiment_test.go

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,12 @@ func TestMapExperiments(t *testing.T) {
125125
func TestMapExperimentsWithStringAudienceCondition(t *testing.T) {
126126

127127
rawExperiment := datafileEntities.Experiment{
128-
ID: "11111",
129-
AudienceIds: []string{"31111"},
130-
Key: "test_experiment_11111",
131-
AudienceConditions: "31111",
128+
ExperimentCore: datafileEntities.ExperimentCore{
129+
ID: "11111",
130+
AudienceIds: []string{"31111"},
131+
Key: "test_experiment_11111",
132+
AudienceConditions: "31111",
133+
},
132134
}
133135

134136
rawExperiments := []datafileEntities.Experiment{rawExperiment}
@@ -167,7 +169,9 @@ func TestMapExperimentsWithStringAudienceCondition(t *testing.T) {
167169
func TestMergeExperiments(t *testing.T) {
168170

169171
rawExperiment := datafileEntities.Experiment{
170-
ID: "11111",
172+
ExperimentCore: datafileEntities.ExperimentCore{
173+
ID: "11111",
174+
},
171175
}
172176
rawGroup := datafileEntities.Group{
173177
Policy: "random",
@@ -184,7 +188,9 @@ func TestMergeExperiments(t *testing.T) {
184188
},
185189
Experiments: []datafileEntities.Experiment{
186190
{
187-
ID: "11112",
191+
ExperimentCore: datafileEntities.ExperimentCore{
192+
ID: "11112",
193+
},
188194
},
189195
},
190196
}
@@ -195,10 +201,14 @@ func TestMergeExperiments(t *testing.T) {
195201

196202
expectedExperiments := []datafileEntities.Experiment{
197203
{
198-
ID: "11111",
204+
ExperimentCore: datafileEntities.ExperimentCore{
205+
ID: "11111",
206+
},
199207
},
200208
{
201-
ID: "11112",
209+
ExperimentCore: datafileEntities.ExperimentCore{
210+
ID: "11112",
211+
},
202212
},
203213
}
204214

@@ -302,15 +312,17 @@ func TestMapCmab(t *testing.T) {
302312
func TestMapExperimentWithCmab(t *testing.T) {
303313
// Create a raw experiment with CMAB configuration
304314
rawExperiment := datafileEntities.Experiment{
305-
ID: "exp1",
306-
Key: "experiment_1",
307-
LayerID: "layer1",
308-
Variations: []datafileEntities.Variation{
309-
{ID: "var1", Key: "variation_1"},
310-
},
311-
TrafficAllocation: []datafileEntities.TrafficAllocation{
312-
{EntityID: "var1", EndOfRange: 10000},
315+
ExperimentCore: datafileEntities.ExperimentCore{
316+
ID: "exp1",
317+
Key: "experiment_1",
318+
Variations: []datafileEntities.Variation{
319+
{ID: "var1", Key: "variation_1"},
320+
},
321+
TrafficAllocation: []datafileEntities.TrafficAllocation{
322+
{EntityID: "var1", EndOfRange: 10000},
323+
},
313324
},
325+
LayerID: "layer1",
314326
Cmab: &datafileEntities.Cmab{
315327
AttributeIds: []string{"attr1", "attr2"},
316328
TrafficAllocation: 5000, // Changed from array to int

pkg/config/datafileprojectconfig/mappers/feature.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ func MapFeatures(featureFlags []datafileEntities.FeatureFlag, rolloutMap map[str
6262
feature.ExperimentIDs = featureFlag.ExperimentIDs
6363
feature.FeatureExperiments = featureExperiments
6464
feature.VariableMap = variableMap
65-
feature.HoldoutIDs = featureFlag.HoldoutIDs
65+
// HoldoutIDs will be populated later when holdouts are processed from the datafile
6666
featureMap[featureFlag.Key] = feature
6767
}
6868
return featureMap

pkg/config/datafileprojectconfig/mappers/feature_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,7 @@ func TestMapFeaturesWithHoldoutIds(t *testing.T) {
8585
"key": "test_feature_22222",
8686
"rolloutId": "42222",
8787
"experimentIds": ["32222"],
88-
"variables": [{"defaultValue":"test","id":"2","key":"var","type":"string"}],
89-
"holdoutIds": ["holdout_1", "holdout_2"]
88+
"variables": [{"defaultValue":"test","id":"2","key":"var","type":"string"}]
9089
}`
9190

9291
var rawFeatureFlag datafileEntities.FeatureFlag
@@ -104,11 +103,12 @@ func TestMapFeaturesWithHoldoutIds(t *testing.T) {
104103
}
105104
featureMap := MapFeatures(rawFeatureFlags, rolloutMap, experimentMap)
106105

107-
// Verify that holdoutIds are properly mapped
106+
// Verify that the feature is created properly
107+
// HoldoutIDs will be populated later when holdouts are processed from the datafile
108108
feature := featureMap["test_feature_22222"]
109-
expectedHoldoutIds := []string{"holdout_1", "holdout_2"}
110109

111-
assert.Equal(t, expectedHoldoutIds, feature.HoldoutIDs)
110+
// For now, HoldoutIDs should be nil/empty since they're not in the datafile directly
111+
assert.Nil(t, feature.HoldoutIDs)
112112
assert.Equal(t, "22222", feature.ID)
113113
assert.Equal(t, "test_feature_22222", feature.Key)
114114
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/****************************************************************************
2+
* Copyright 2025, Optimizely, Inc. and contributors *
3+
* *
4+
* Licensed under the Apache License, Version 2.0 (the "License"); *
5+
* you may not use this file except in compliance with the License. *
6+
* You may obtain a copy of the License at *
7+
* *
8+
* http://www.apache.org/licenses/LICENSE-2.0 *
9+
* *
10+
* Unless required by applicable law or agreed to in writing, software *
11+
* distributed under the License is distributed on an "AS IS" BASIS, *
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13+
* See the License for the specific language governing permissions and *
14+
* limitations under the License. *
15+
***************************************************************************/
16+
17+
// Package mappers ...
18+
package mappers
19+
20+
import (
21+
datafileEntities "github.com/optimizely/go-sdk/v2/pkg/config/datafileprojectconfig/entities"
22+
"github.com/optimizely/go-sdk/v2/pkg/entities"
23+
)
24+
25+
// HoldoutMaps contains the different holdout mappings for efficient lookup
26+
type HoldoutMaps struct {
27+
HoldoutIDMap map[string]entities.Holdout // Map holdout ID to holdout
28+
GlobalHoldouts []entities.Holdout // Holdouts with no specific flag inclusion
29+
IncludedHoldouts map[string][]entities.Holdout // Map flag ID to holdouts that include it
30+
ExcludedHoldouts map[string][]entities.Holdout // Map flag ID to holdouts that exclude it
31+
FlagHoldoutsMap map[string][]string // Cached map of flag ID to holdout IDs
32+
}
33+
34+
// MapHoldouts maps the raw datafile holdout entities to SDK Holdout entities
35+
// and creates the necessary mappings for efficient holdout lookup
36+
func MapHoldouts(holdouts []datafileEntities.Holdout, audienceMap map[string]entities.Audience) HoldoutMaps {
37+
holdoutMaps := HoldoutMaps{
38+
HoldoutIDMap: make(map[string]entities.Holdout),
39+
GlobalHoldouts: []entities.Holdout{},
40+
IncludedHoldouts: make(map[string][]entities.Holdout),
41+
ExcludedHoldouts: make(map[string][]entities.Holdout),
42+
FlagHoldoutsMap: make(map[string][]string),
43+
}
44+
45+
for _, datafileHoldout := range holdouts {
46+
// Create minimal runtime holdout entity - only what's needed for flag dependency
47+
holdout := entities.Holdout{
48+
ID: datafileHoldout.ID,
49+
Key: datafileHoldout.Key,
50+
Status: entities.HoldoutStatus(datafileHoldout.Status),
51+
IncludedFlags: datafileHoldout.IncludedFlags,
52+
ExcludedFlags: datafileHoldout.ExcludedFlags,
53+
}
54+
55+
// Add to ID map
56+
holdoutMaps.HoldoutIDMap[holdout.ID] = holdout
57+
58+
// Categorize holdouts based on flag targeting
59+
if len(datafileHoldout.IncludedFlags) == 0 {
60+
// This is a global holdout (applies to all flags unless excluded)
61+
holdoutMaps.GlobalHoldouts = append(holdoutMaps.GlobalHoldouts, holdout)
62+
63+
// Add to excluded flags map
64+
for _, flagID := range datafileHoldout.ExcludedFlags {
65+
holdoutMaps.ExcludedHoldouts[flagID] = append(holdoutMaps.ExcludedHoldouts[flagID], holdout)
66+
}
67+
} else {
68+
// This holdout specifically includes certain flags
69+
for _, flagID := range datafileHoldout.IncludedFlags {
70+
holdoutMaps.IncludedHoldouts[flagID] = append(holdoutMaps.IncludedHoldouts[flagID], holdout)
71+
}
72+
}
73+
}
74+
75+
return holdoutMaps
76+
}
77+
78+
// GetHoldoutsForFlag returns the holdout IDs that apply to a specific flag
79+
// This follows the logic from JavaScript SDK: global holdouts (minus excluded) + specifically included
80+
func GetHoldoutsForFlag(flagID string, holdoutMaps HoldoutMaps) []string {
81+
// Check cache first
82+
if cachedHoldoutIDs, exists := holdoutMaps.FlagHoldoutsMap[flagID]; exists {
83+
return cachedHoldoutIDs
84+
}
85+
86+
holdoutIDs := []string{}
87+
88+
// Add global holdouts that don't exclude this flag
89+
for _, holdout := range holdoutMaps.GlobalHoldouts {
90+
isExcluded := false
91+
for _, excludedFlagID := range holdout.ExcludedFlags {
92+
if excludedFlagID == flagID {
93+
isExcluded = true
94+
break
95+
}
96+
}
97+
if !isExcluded {
98+
holdoutIDs = append(holdoutIDs, holdout.ID)
99+
}
100+
}
101+
102+
// Add holdouts that specifically include this flag
103+
if includedHoldouts, exists := holdoutMaps.IncludedHoldouts[flagID]; exists {
104+
for _, holdout := range includedHoldouts {
105+
holdoutIDs = append(holdoutIDs, holdout.ID)
106+
}
107+
}
108+
109+
// Cache the result
110+
holdoutMaps.FlagHoldoutsMap[flagID] = holdoutIDs
111+
112+
return holdoutIDs
113+
}

0 commit comments

Comments
 (0)