Skip to content

Commit 99eea3d

Browse files
wip: assging holdoutids to feature flags
1 parent a732fdb commit 99eea3d

File tree

5 files changed

+162
-3
lines changed

5 files changed

+162
-3
lines changed

Sources/Data Model/FeatureFlag.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ struct FeatureFlag: Codable, Equatable, OptimizelyFeature {
4141
var variablesMap: [String: OptimizelyVariable] = [:]
4242
var experimentRules: [OptimizelyExperiment] = []
4343
var deliveryRules: [OptimizelyExperiment] = []
44+
var holdoutIds: [String] = []
4445
}
4546

4647
// MARK: - Utils

Sources/Data Model/Project.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ struct Project: Codable, Equatable {
4646
var sendFlagDecisions: Bool?
4747
var sdkKey: String?
4848
var environmentKey: String?
49-
49+
// Holdouts
50+
// This would be non optional once API is completed
51+
var holdouts: [Holdout]?
5052
let logger = OPTLoggerFactory.getLogger()
5153

5254
// Required since logger is not decodable
@@ -57,11 +59,13 @@ struct Project: Codable, Equatable {
5759
case anonymizeIP
5860
// V4
5961
case rollouts, integrations, typedAudiences, featureFlags, botFiltering, sendFlagDecisions, sdkKey, environmentKey
62+
// holdouts
63+
case holdouts
6064
}
6165

6266
// Required since logger is not equatable
6367
static func == (lhs: Project, rhs: Project) -> Bool {
64-
return lhs.version == rhs.version && lhs.projectId == rhs.projectId && lhs.experiments == rhs.experiments &&
68+
return lhs.version == rhs.version && lhs.projectId == rhs.projectId && lhs.experiments == rhs.experiments && lhs.holdouts == rhs.holdouts &&
6569
lhs.audiences == rhs.audiences && lhs.groups == rhs.groups && lhs.attributes == rhs.attributes &&
6670
lhs.accountId == rhs.accountId && lhs.events == rhs.events && lhs.revision == rhs.revision &&
6771
lhs.anonymizeIP == rhs.anonymizeIP && lhs.rollouts == rhs.rollouts &&

Sources/Data Model/ProjectConfig.swift

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,16 @@
1717
import Foundation
1818

1919
class ProjectConfig {
20+
private var isUpdating = false // Flag to prevent recursion
2021

2122
var project: Project! {
2223
didSet {
23-
updateProjectDependentProps()
24+
if !isUpdating {
25+
updateProjectDependentProps()
26+
}
2427
}
2528
}
29+
2630
let logger = OPTLoggerFactory.getLogger()
2731

2832
// local runtime forcedVariations [UserId: [ExperimentId: VariationId]]
@@ -40,6 +44,7 @@ class ProjectConfig {
4044
var allExperiments = [Experiment]()
4145
var flagVariationsMap = [String: [Variation]]()
4246
var allSegments = [String]()
47+
var holdoutIdMap = [String: Holdout]()
4348

4449
// MARK: - Init
4550

@@ -66,8 +71,19 @@ class ProjectConfig {
6671
init() {}
6772

6873
func updateProjectDependentProps() {
74+
isUpdating = true
75+
defer { isUpdating = false }
76+
6977
self.allExperiments = project.experiments + project.groups.map { $0.experiments }.flatMap { $0 }
7078

79+
holdoutIdMap = {
80+
var map = [String : Holdout]()
81+
project.holdouts?.forEach { map[$0.id] = $0 }
82+
return map
83+
}()
84+
85+
assignHoldoutIdsToFeatureFlags()
86+
7187
self.experimentKeyMap = {
7288
var map = [String: Experiment]()
7389
allExperiments.forEach { exp in
@@ -155,6 +171,34 @@ class ProjectConfig {
155171

156172
}
157173

174+
private func assignHoldoutIdsToFeatureFlags() {
175+
let flagsWithHoldoutIds = project.featureFlags.map { flag -> FeatureFlag in
176+
var updatedFlag = flag
177+
var holdoutIds = [String]()
178+
for holdout in project.holdouts ?? [] {
179+
if let includedFlags = holdout.includedFlags, !includedFlags.isEmpty {
180+
if includedFlags.contains(flag.id) {
181+
holdoutIds.append(holdout.id)
182+
}
183+
} else if let excludedFlags = holdout.excludedFlags, !excludedFlags.isEmpty {
184+
if !excludedFlags.contains(flag.id) {
185+
holdoutIds.append(holdout.id)
186+
}
187+
} else {
188+
// Global holdout
189+
holdoutIds.append(holdout.id)
190+
}
191+
}
192+
193+
/// Update holdoutIds for the flag
194+
updatedFlag.holdoutIds = holdoutIds
195+
return updatedFlag
196+
}
197+
198+
// Update project featureFlags after mapping with holdoutIds
199+
project.featureFlags = flagsWithHoldoutIds
200+
}
201+
158202
func getAllRulesForFlag(_ flag: FeatureFlag) -> [Experiment] {
159203
var rules = flag.experimentIds.compactMap { experimentIdMap[$0] }
160204
let rollout = self.rolloutIdMap[flag.rolloutId]
@@ -270,6 +314,13 @@ extension ProjectConfig {
270314
return rolloutIdMap[id]
271315
}
272316

317+
/**
318+
* Get a Holdout object for an Id.
319+
*/
320+
func getHoldout(id: String) -> Holdout? {
321+
return holdoutIdMap[id]
322+
}
323+
273324
/**
274325
* Gets an event for a corresponding event key
275326
*/

Tests/OptimizelyTests-DataModel/ProjectConfigTests.swift

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,97 @@ class ProjectConfigTests: XCTestCase {
8080
XCTAssertEqual(featureMap["1004"], ["2002"])
8181
}
8282

83+
func testHoldoutIdMapIsBuiltFromProject() {
84+
var exp0 = ExperimentTests.sampleData
85+
var exp1 = ExperimentTests.sampleData
86+
var exp2 = ExperimentTests.sampleData
87+
var exp3 = ExperimentTests.sampleData
88+
var exp4 = ExperimentTests.sampleData
89+
exp0["id"] = "1000"
90+
exp1["id"] = "1001"
91+
exp2["id"] = "1002"
92+
exp3["id"] = "1003"
93+
exp4["id"] = "1004"
94+
95+
96+
var holdout0 = HoldoutTests.sampleData
97+
var holdout1 = HoldoutTests.sampleData
98+
var holdout2 = HoldoutTests.sampleData
99+
var holdout3 = HoldoutTests.sampleData
100+
var holdout4 = HoldoutTests.sampleData
101+
holdout0["id"] = "3000" // Global holdout (no included or excluded flags)
102+
holdout1["id"] = "3001" // Global holdout (no included or excluded flags)
103+
holdout2["id"] = "3002" // Global holdout (no included or excluded flags)
104+
holdout3["id"] = "3003" // Included flagids ["2000", "2002"]
105+
holdout4["id"] = "3004" // Excluded flagids ["2001"]
106+
107+
holdout3["includedFlags"] = ["2000", "2002"]
108+
holdout4["excludedFlags"] = ["2001"]
109+
110+
var feature0 = FeatureFlagTests.sampleData
111+
var feature1 = FeatureFlagTests.sampleData
112+
var feature2 = FeatureFlagTests.sampleData
113+
var feature3 = FeatureFlagTests.sampleData
114+
115+
feature0["id"] = "2000"
116+
feature0["key"] = "key_2000"
117+
118+
feature1["id"] = "2001"
119+
feature1["key"] = "key_2001"
120+
121+
feature2["id"] = "2002"
122+
feature2["key"] = "key_2002"
123+
124+
feature2["id"] = "2003"
125+
feature2["key"] = "key_2003"
126+
127+
feature0["experimentIds"] = ["1000"]
128+
feature1["experimentIds"] = ["1000", "1001", "1002"]
129+
feature2["experimentIds"] = ["1000", "1003", "1004"]
130+
feature3["experimentIds"] = ["1000", "1003", "1004"]
131+
132+
var projectData = ProjectTests.sampleData
133+
projectData["experiments"] = [exp0, exp1, exp2, exp3, exp4]
134+
projectData["featureFlags"] = [feature0, feature1, feature2]
135+
projectData["holdouts"] = [holdout0, holdout1, holdout2, holdout3, holdout4]
136+
137+
// check experimentFeatureMap extracted properly
138+
139+
let model: Project = try! OTUtils.model(from: projectData)
140+
let projectConfig = ProjectConfig()
141+
projectConfig.project = model
142+
143+
let holdoutIdMap = projectConfig.holdoutIdMap
144+
145+
XCTAssertEqual(holdoutIdMap["3000"]?.includedFlags, nil)
146+
XCTAssertEqual(holdoutIdMap["3000"]?.excludedFlags, nil)
147+
148+
XCTAssertEqual(holdoutIdMap["3001"]?.includedFlags, nil)
149+
XCTAssertEqual(holdoutIdMap["3001"]?.excludedFlags, nil)
150+
151+
XCTAssertEqual(holdoutIdMap["3002"]?.includedFlags, nil)
152+
XCTAssertEqual(holdoutIdMap["3002"]?.excludedFlags, nil)
153+
154+
XCTAssertEqual(holdoutIdMap["3003"]?.includedFlags, ["2000", "2002"])
155+
XCTAssertEqual(holdoutIdMap["3003"]?.excludedFlags, nil)
156+
157+
158+
XCTAssertEqual(holdoutIdMap["3004"]?.includedFlags, nil)
159+
XCTAssertEqual(holdoutIdMap["3004"]?.excludedFlags, ["2001"])
160+
161+
let featureFlagKeyMap = projectConfig.featureFlagKeyMap
162+
163+
/// Test Global holdout + included
164+
XCTAssertEqual(featureFlagKeyMap["key_2000"]?.holdoutIds, ["3000", "3001", "3002", "3003", "3004"])
165+
XCTAssertEqual(featureFlagKeyMap["key_2002"]?.holdoutIds, ["3000", "3001", "3002", "3003", "3004"])
166+
167+
/// Test Global holdout - excluded
168+
XCTAssertEqual(featureFlagKeyMap["key_2001"]?.holdoutIds, ["3000", "3001", "3002"])
169+
170+
/// Test Global holdout
171+
XCTAssertEqual(featureFlagKeyMap["key_2003"]?.holdoutIds, ["3000", "3001", "3002", "3003", "3004"])
172+
}
173+
83174
func testFlagVariations() {
84175
let datafile = OTUtils.loadJSONDatafile("decide_datafile")!
85176
let optimizely = OptimizelyClient(sdkKey: "12345",

Tests/OptimizelyTests-DataModel/ProjectTests.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class ProjectTests: XCTestCase {
2222
static var sampleData: [String: Any] = ["version": "4",
2323
"projectId": "11111",
2424
"experiments": [ExperimentTests.sampleData],
25+
"holdouts": [HoldoutTests.sampleData],
2526
"audiences": [AudienceTests.sampleData],
2627
"groups": [GroupTests.sampleData],
2728
"attributes": [AttributeTests.sampleData],
@@ -49,6 +50,7 @@ extension ProjectTests {
4950
XCTAssert(model.version == "4")
5051
XCTAssert(model.projectId == "11111")
5152
XCTAssert(model.experiments == [try! OTUtils.model(from: ExperimentTests.sampleData)])
53+
XCTAssert(model.holdouts == [try! OTUtils.model(from: HoldoutTests.sampleData)])
5254
XCTAssert(model.audiences == [try! OTUtils.model(from: AudienceTests.sampleData)])
5355
XCTAssert(model.groups == [try! OTUtils.model(from: GroupTests.sampleData)])
5456
XCTAssert(model.attributes == [try! OTUtils.model(from: AttributeTests.sampleData)])
@@ -210,6 +212,16 @@ extension ProjectTests {
210212
XCTAssertNil(model.sendFlagDecisions)
211213
}
212214

215+
func testDecodeSuccessWithMissingHoldouts() {
216+
var data: [String: Any] = ProjectTests.sampleData
217+
data["holdouts"] = nil
218+
219+
let model: Project = try! OTUtils.model(from: data)
220+
XCTAssertNotNil(model)
221+
XCTAssertNil(model.holdouts)
222+
223+
}
224+
213225
}
214226

215227
// MARK: - Encode

0 commit comments

Comments
 (0)