• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

optimizely / go-sdk / 16977156753

14 Aug 2025 09:30PM UTC coverage: 91.699%. First build
16977156753

Pull #415

github

Mat001
add holdout model
Pull Request #415: [FSSDK-11551] Adding project config support for holdouts to go-sdk

105 of 111 new or added lines in 3 files covered. (94.59%)

5501 of 5999 relevant lines covered (91.7%)

9225.39 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.17
/pkg/config/datafileprojectconfig/mappers/holdout.go
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 {
1✔
37
        holdoutMaps := HoldoutMaps{
1✔
38
                HoldoutIDMap:     make(map[string]entities.Holdout),
1✔
39
                GlobalHoldouts:   []entities.Holdout{},
1✔
40
                IncludedHoldouts: make(map[string][]entities.Holdout),
1✔
41
                ExcludedHoldouts: make(map[string][]entities.Holdout),
1✔
42
                FlagHoldoutsMap:  make(map[string][]string),
1✔
43
        }
1✔
44

1✔
45
        for _, datafileHoldout := range holdouts {
3✔
46
                // Map variations similar to experiments
2✔
47
                variationMap := make(map[string]entities.Variation)
2✔
48
                variationKeyToIDMap := make(map[string]string)
2✔
49

2✔
50
                for _, variation := range datafileHoldout.Variations {
5✔
51
                        variableMap := make(map[string]entities.VariationVariable)
3✔
52
                        for _, variable := range variation.Variables {
3✔
NEW
53
                                variableMap[variable.ID] = entities.VariationVariable{
×
NEW
54
                                        ID:    variable.ID,
×
NEW
55
                                        Value: variable.Value,
×
NEW
56
                                }
×
NEW
57
                        }
×
58

59
                        v := entities.Variation{
3✔
60
                                ID:             variation.ID,
3✔
61
                                Key:            variation.Key,
3✔
62
                                FeatureEnabled: variation.FeatureEnabled,
3✔
63
                                Variables:      variableMap,
3✔
64
                        }
3✔
65
                        variationMap[variation.ID] = v
3✔
66
                        variationKeyToIDMap[variation.Key] = variation.ID
3✔
67
                }
68

69
                // Map traffic allocation
70
                trafficAllocation := []entities.Range{}
2✔
71
                for _, allocation := range datafileHoldout.TrafficAllocation {
5✔
72
                        trafficAllocation = append(trafficAllocation, entities.Range{
3✔
73
                                EntityID:   allocation.EntityID,
3✔
74
                                EndOfRange: allocation.EndOfRange,
3✔
75
                        })
3✔
76
                }
3✔
77

78
                // Build audience condition tree
79
                var audienceConditionTree *entities.TreeNode
2✔
80
                if datafileHoldout.AudienceConditions != nil {
2✔
NEW
81
                        audienceConditionTree, _, _ = buildConditionTree(datafileHoldout.AudienceConditions)
×
82
                } else if len(datafileHoldout.AudienceIds) > 0 {
3✔
83
                        // Build from audience IDs similar to experiments
1✔
84
                        audienceConditionTree, _ = buildAudienceConditionTree(datafileHoldout.AudienceIds)
1✔
85
                }
1✔
86

87
                // Convert status string to HoldoutStatus type
88
                status := entities.HoldoutStatus(datafileHoldout.Status)
2✔
89

2✔
90
                // Create the runtime holdout entity
2✔
91
                holdout := entities.Holdout{
2✔
92
                        ID:                    datafileHoldout.ID,
2✔
93
                        Key:                   datafileHoldout.Key,
2✔
94
                        Status:                status,
2✔
95
                        Variations:            variationMap,
2✔
96
                        VariationKeyToIDMap:   variationKeyToIDMap,
2✔
97
                        TrafficAllocation:     trafficAllocation,
2✔
98
                        AudienceIds:           datafileHoldout.AudienceIds,
2✔
99
                        AudienceConditions:    datafileHoldout.AudienceConditions,
2✔
100
                        AudienceConditionTree: audienceConditionTree,
2✔
101
                        IncludedFlags:         datafileHoldout.IncludedFlags,
2✔
102
                        ExcludedFlags:         datafileHoldout.ExcludedFlags,
2✔
103
                }
2✔
104

2✔
105
                // Add to ID map
2✔
106
                holdoutMaps.HoldoutIDMap[holdout.ID] = holdout
2✔
107

2✔
108
                // Categorize holdouts based on flag targeting
2✔
109
                if len(datafileHoldout.IncludedFlags) == 0 {
3✔
110
                        // This is a global holdout (applies to all flags unless excluded)
1✔
111
                        holdoutMaps.GlobalHoldouts = append(holdoutMaps.GlobalHoldouts, holdout)
1✔
112

1✔
113
                        // Add to excluded flags map
1✔
114
                        for _, flagID := range datafileHoldout.ExcludedFlags {
2✔
115
                                holdoutMaps.ExcludedHoldouts[flagID] = append(holdoutMaps.ExcludedHoldouts[flagID], holdout)
1✔
116
                        }
1✔
117
                } else {
1✔
118
                        // This holdout specifically includes certain flags
1✔
119
                        for _, flagID := range datafileHoldout.IncludedFlags {
3✔
120
                                holdoutMaps.IncludedHoldouts[flagID] = append(holdoutMaps.IncludedHoldouts[flagID], holdout)
2✔
121
                        }
2✔
122
                }
123
        }
124

125
        return holdoutMaps
1✔
126
}
127

128
// GetHoldoutsForFlag returns the holdout IDs that apply to a specific flag
129
// This follows the logic from JavaScript SDK: global holdouts (minus excluded) + specifically included
130
func GetHoldoutsForFlag(flagID string, holdoutMaps HoldoutMaps) []string {
4✔
131
        // Check cache first
4✔
132
        if cachedHoldoutIDs, exists := holdoutMaps.FlagHoldoutsMap[flagID]; exists {
5✔
133
                return cachedHoldoutIDs
1✔
134
        }
1✔
135

136
        holdoutIDs := []string{}
3✔
137

3✔
138
        // Add global holdouts that don't exclude this flag
3✔
139
        for _, holdout := range holdoutMaps.GlobalHoldouts {
9✔
140
                isExcluded := false
6✔
141
                for _, excludedFlagID := range holdout.ExcludedFlags {
9✔
142
                        if excludedFlagID == flagID {
4✔
143
                                isExcluded = true
1✔
144
                                break
1✔
145
                        }
146
                }
147
                if !isExcluded {
11✔
148
                        holdoutIDs = append(holdoutIDs, holdout.ID)
5✔
149
                }
5✔
150
        }
151

152
        // Add holdouts that specifically include this flag
153
        if includedHoldouts, exists := holdoutMaps.IncludedHoldouts[flagID]; exists {
4✔
154
                for _, holdout := range includedHoldouts {
2✔
155
                        holdoutIDs = append(holdoutIDs, holdout.ID)
1✔
156
                }
1✔
157
        }
158

159
        // Cache the result
160
        holdoutMaps.FlagHoldoutsMap[flagID] = holdoutIDs
3✔
161

3✔
162
        return holdoutIDs
3✔
163
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc