Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f5e60a7
wip: Holdout Model created
muzahidul-opti Mar 20, 2025
38386cd
wip: add holdout model test cases
muzahidul-opti Mar 20, 2025
a732fdb
Merge branch 'master' into muzahid/holdout-FSSDK-11371
muzahidul-opti Mar 20, 2025
99eea3d
wip: assging holdoutids to feature flags
muzahidul-opti Mar 24, 2025
326adce
wip: included and excluded flags updated to required
muzahidul-opti Mar 24, 2025
d1cc64d
wip: move OptimizelyExperiment compliance to OptmizelyConfig file
muzahidul-opti Mar 25, 2025
247d0eb
Merge branch 'muzahid/holdout-FSSDK-11371' into muzahid/FSSDK-11372
muzahidul-opti Mar 25, 2025
01f3cd7
wip: fix uint test
muzahidul-opti Mar 25, 2025
5404d97
Merge branch 'master' into muzahid/FSSDK-11372
muzahidul-opti Apr 7, 2025
3560de9
wip: make holdouts array empty if no key presented
muzahidul-opti Apr 7, 2025
51ba12a
wip: add id and key to experiement core
muzahidul-opti Apr 7, 2025
2c29e82
wip: clean up
muzahidul-opti Apr 7, 2025
16920c7
wip: clean up
muzahidul-opti Apr 8, 2025
7bad66d
clean up
muzahidul-opti Apr 8, 2025
b337b35
wip: address review request
muzahidul-opti Apr 9, 2025
c694a57
wip: Update holdout sample data
muzahidul-opti Apr 9, 2025
fd6535a
wip: update flag to holdout mapping approach
muzahidul-opti Apr 9, 2025
987ff6c
wip: holdout config struct added
muzahidul-opti Apr 15, 2025
528aa0d
wip: unit tests updated
muzahidul-opti Apr 15, 2025
c868c29
wip: HoldoutConfig test cases added
muzahidul-opti Apr 15, 2025
e876960
wip: fix holdout config tests
muzahidul-opti Apr 15, 2025
9e1669c
clean up: fix test cases
muzahidul-opti Apr 15, 2025
f310734
cleanup: add unit test cases
muzahidul-opti Apr 16, 2025
0ce7aa3
Update Sources/Data Model/HoldoutConfig.swift
muzahidul-opti Apr 17, 2025
05ed9f0
cleanup: address review changes
muzahidul-opti Apr 17, 2025
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
34 changes: 34 additions & 0 deletions OptimizelySwiftSDK.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -2034,6 +2034,22 @@
984FE51E2CC8AA88004F6F41 /* UserProfileTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 984FE5102CC8AA88004F6F41 /* UserProfileTracker.swift */; };
984FE51F2CC8AA88004F6F41 /* UserProfileTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 984FE5102CC8AA88004F6F41 /* UserProfileTracker.swift */; };
984FE5202CC8AA88004F6F41 /* UserProfileTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 984FE5102CC8AA88004F6F41 /* UserProfileTracker.swift */; };
98AC97E22DAE4579001405DD /* HoldoutConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98AC97E12DAE4579001405DD /* HoldoutConfig.swift */; };
98AC97E32DAE4579001405DD /* HoldoutConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98AC97E12DAE4579001405DD /* HoldoutConfig.swift */; };
98AC97E42DAE4579001405DD /* HoldoutConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98AC97E12DAE4579001405DD /* HoldoutConfig.swift */; };
98AC97E52DAE4579001405DD /* HoldoutConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98AC97E12DAE4579001405DD /* HoldoutConfig.swift */; };
98AC97E62DAE4579001405DD /* HoldoutConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98AC97E12DAE4579001405DD /* HoldoutConfig.swift */; };
98AC97E72DAE4579001405DD /* HoldoutConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98AC97E12DAE4579001405DD /* HoldoutConfig.swift */; };
98AC97E82DAE4579001405DD /* HoldoutConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98AC97E12DAE4579001405DD /* HoldoutConfig.swift */; };
98AC97E92DAE4579001405DD /* HoldoutConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98AC97E12DAE4579001405DD /* HoldoutConfig.swift */; };
98AC97EA2DAE4579001405DD /* HoldoutConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98AC97E12DAE4579001405DD /* HoldoutConfig.swift */; };
98AC97EB2DAE4579001405DD /* HoldoutConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98AC97E12DAE4579001405DD /* HoldoutConfig.swift */; };
98AC97EC2DAE4579001405DD /* HoldoutConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98AC97E12DAE4579001405DD /* HoldoutConfig.swift */; };
98AC97ED2DAE4579001405DD /* HoldoutConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98AC97E12DAE4579001405DD /* HoldoutConfig.swift */; };
98AC97EE2DAE4579001405DD /* HoldoutConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98AC97E12DAE4579001405DD /* HoldoutConfig.swift */; };
98AC97EF2DAE4579001405DD /* HoldoutConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98AC97E12DAE4579001405DD /* HoldoutConfig.swift */; };
98AC97F02DAE4579001405DD /* HoldoutConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98AC97E12DAE4579001405DD /* HoldoutConfig.swift */; };
98AC97F12DAE4579001405DD /* HoldoutConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98AC97E12DAE4579001405DD /* HoldoutConfig.swift */; };
BD1C3E8524E4399C0084B4DA /* SemanticVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B97DD93249D327F003DE606 /* SemanticVersion.swift */; };
BD64853C2491474500F30986 /* Optimizely.h in Headers */ = {isa = PBXBuildFile; fileRef = 6E75167A22C520D400B2B157 /* Optimizely.h */; settings = {ATTRIBUTES = (Public, ); }; };
BD64853E2491474500F30986 /* Audience.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75169822C520D400B2B157 /* Audience.swift */; };
Expand Down Expand Up @@ -2477,6 +2493,7 @@
982C071E2D8C82AE0068B1FF /* HoldoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HoldoutTests.swift; sourceTree = "<group>"; };
984FE5102CC8AA88004F6F41 /* UserProfileTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileTracker.swift; sourceTree = "<group>"; };
987F11D92AF3F56F0083D3F9 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
98AC97E12DAE4579001405DD /* HoldoutConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HoldoutConfig.swift; sourceTree = "<group>"; };
BD6485812491474500F30986 /* Optimizely.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Optimizely.framework; sourceTree = BUILT_PRODUCTS_DIR; };
C78CAF572445AD8D009FE876 /* OptimizelyJSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyJSON.swift; sourceTree = "<group>"; };
C78CAF652446DB91009FE876 /* OptimizelyClientTests_OptimizelyJSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyClientTests_OptimizelyJSON.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2857,6 +2874,7 @@
6E75169022C520D400B2B157 /* Variation.swift */,
6E75169122C520D400B2B157 /* TrafficAllocation.swift */,
6E75169222C520D400B2B157 /* Project.swift */,
98AC97E12DAE4579001405DD /* HoldoutConfig.swift */,
6E75169322C520D400B2B157 /* Experiment.swift */,
980CC9072D833F2800E07D24 /* ExperimentCore.swift */,
980CC8F62D833F0D00E07D24 /* Holdout.swift */,
Expand Down Expand Up @@ -4181,6 +4199,7 @@
845945C3287758A100D13E11 /* OdpConfig.swift in Sources */,
6E14CD832423F9A100010234 /* DataStoreQueueStackImpl.swift in Sources */,
848617F12863E21200B7F41B /* OdpEventApiManager.swift in Sources */,
98AC97E52DAE4579001405DD /* HoldoutConfig.swift in Sources */,
6E14CD812423F9A100010234 /* DataStoreUserDefaults.swift in Sources */,
6E14CD802423F9A100010234 /* DataStoreMemory.swift in Sources */,
6E14CDA02423F9C300010234 /* OptimizelyClient+Extension.swift in Sources */,
Expand Down Expand Up @@ -4369,6 +4388,7 @@
6E424CB926324B1D0081004A /* Constants.swift in Sources */,
6E424CBA26324B1D0081004A /* Notifications.swift in Sources */,
6E424CBB26324B1D0081004A /* MurmurHash3.swift in Sources */,
98AC97EC2DAE4579001405DD /* HoldoutConfig.swift in Sources */,
8464087628130D3200CCF97D /* Integration.swift in Sources */,
848617CE2863DC2700B7F41B /* OdpSegmentManager.swift in Sources */,
848617F02863E21200B7F41B /* OdpEventApiManager.swift in Sources */,
Expand Down Expand Up @@ -4410,6 +4430,7 @@
845945BD2877589E00D13E11 /* OdpConfig.swift in Sources */,
984FE51C2CC8AA88004F6F41 /* UserProfileTracker.swift in Sources */,
6E75171322C520D400B2B157 /* OptimizelyClient+ObjC.swift in Sources */,
98AC97EF2DAE4579001405DD /* HoldoutConfig.swift in Sources */,
6E75191922C520D500B2B157 /* OPTNotificationCenter.swift in Sources */,
6E7518A122C520D400B2B157 /* FeatureFlag.swift in Sources */,
6E994B3525A3E6EA00999262 /* DecisionResponse.swift in Sources */,
Expand Down Expand Up @@ -4494,6 +4515,7 @@
845945C7287758A300D13E11 /* OdpConfig.swift in Sources */,
6E75175622C520D400B2B157 /* LogMessage.swift in Sources */,
848617F62863E21200B7F41B /* OdpEventApiManager.swift in Sources */,
98AC97EB2DAE4579001405DD /* HoldoutConfig.swift in Sources */,
6E75193822C520D500B2B157 /* OPTDataStore.swift in Sources */,
6E75191422C520D500B2B157 /* BackgroundingCallbacks.swift in Sources */,
6E75172622C520D400B2B157 /* OptimizelyResult.swift in Sources */,
Expand Down Expand Up @@ -4596,6 +4618,7 @@
6EF8DE2024BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */,
6E7517D822C520D400B2B157 /* DefaultNotificationCenter.swift in Sources */,
6E75177622C520D400B2B157 /* SDKVersion.swift in Sources */,
98AC97E32DAE4579001405DD /* HoldoutConfig.swift in Sources */,
84518B1F287665020023F104 /* OptimizelyClientTests_ODP.swift in Sources */,
6E7516FE22C520D400B2B157 /* OptimizelyLogLevel.swift in Sources */,
6E75173A22C520D400B2B157 /* MurmurHash3.swift in Sources */,
Expand Down Expand Up @@ -4720,6 +4743,7 @@
6EC6DD4A24ABF89B0017D296 /* OptimizelyUserContext.swift in Sources */,
6E75170122C520D400B2B157 /* OptimizelyLogLevel.swift in Sources */,
8464087B28130D3200CCF97D /* Integration.swift in Sources */,
98AC97EA2DAE4579001405DD /* HoldoutConfig.swift in Sources */,
84E2E96C28540B5E001114AB /* OptimizelySdkSettings.swift in Sources */,
6EF8DE3A24BF7D69008B9488 /* DecisionReasons.swift in Sources */,
6E7516B922C520D400B2B157 /* DefaultUserProfileService.swift in Sources */,
Expand Down Expand Up @@ -4866,6 +4890,7 @@
6E7517C522C520D400B2B157 /* DefaultDatafileHandler.swift in Sources */,
6E75190922C520D500B2B157 /* Attribute.swift in Sources */,
6E75177B22C520D400B2B157 /* SDKVersion.swift in Sources */,
98AC97F12DAE4579001405DD /* HoldoutConfig.swift in Sources */,
84E7ABC827D2A1F100447CAE /* ThreadSafeLogger.swift in Sources */,
6E7E9B562523F8C6009E4426 /* OptimizelyUserContextTests_Decide_Legacy.swift in Sources */,
6EC6DD3C24ABF6990017D296 /* OptimizelyClient+Decide.swift in Sources */,
Expand Down Expand Up @@ -5035,6 +5060,7 @@
84E7ABC927D2A1F100447CAE /* ThreadSafeLogger.swift in Sources */,
6E9B119122C5488300C22D81 /* EventForDispatchTests.swift in Sources */,
6E7517EA22C520D400B2B157 /* DefaultDecisionService.swift in Sources */,
98AC97F02DAE4579001405DD /* HoldoutConfig.swift in Sources */,
6E75171C22C520D400B2B157 /* OptimizelyClient+ObjC.swift in Sources */,
6E7516B022C520D400B2B157 /* DefaultLogger.swift in Sources */,
0B97DD9C249D3735003DE606 /* SemanticVersion.swift in Sources */,
Expand Down Expand Up @@ -5159,6 +5185,7 @@
6E7516B522C520D400B2B157 /* DefaultUserProfileService.swift in Sources */,
6E7516A922C520D400B2B157 /* DefaultLogger.swift in Sources */,
6E7517D722C520D400B2B157 /* DefaultNotificationCenter.swift in Sources */,
98AC97E72DAE4579001405DD /* HoldoutConfig.swift in Sources */,
6E75181F22C520D400B2B157 /* BatchEventBuilder.swift in Sources */,
84F6BADD27FD011B004BE62A /* OptimizelyUserContextTests_ODP_Decide.swift in Sources */,
84E2E9472852A378001114AB /* VuidManager.swift in Sources */,
Expand Down Expand Up @@ -5309,6 +5336,7 @@
84E7ABC427D2A1F100447CAE /* ThreadSafeLogger.swift in Sources */,
6E9B117B22C5488100C22D81 /* EventForDispatchTests.swift in Sources */,
6E7517E522C520D400B2B157 /* DefaultDecisionService.swift in Sources */,
98AC97E92DAE4579001405DD /* HoldoutConfig.swift in Sources */,
6E75171722C520D400B2B157 /* OptimizelyClient+ObjC.swift in Sources */,
6E7516AB22C520D400B2B157 /* DefaultLogger.swift in Sources */,
0B97DD9B249D3733003DE606 /* SemanticVersion.swift in Sources */,
Expand Down Expand Up @@ -5433,6 +5461,7 @@
84E7ABC527D2A1F100447CAE /* ThreadSafeLogger.swift in Sources */,
6E7518BE22C520D400B2B157 /* Variable.swift in Sources */,
6E7518CA22C520D400B2B157 /* Audience.swift in Sources */,
98AC97E62DAE4579001405DD /* HoldoutConfig.swift in Sources */,
848617E42863E21200B7F41B /* OdpSegmentApiManager.swift in Sources */,
6E75187622C520D400B2B157 /* Variation.swift in Sources */,
6E7517F222C520D400B2B157 /* DataStoreMemory.swift in Sources */,
Expand Down Expand Up @@ -5537,6 +5566,7 @@
84E7ABCA27D2A1F100447CAE /* ThreadSafeLogger.swift in Sources */,
6E7518C322C520D400B2B157 /* Variable.swift in Sources */,
6E7518CF22C520D400B2B157 /* Audience.swift in Sources */,
98AC97E42DAE4579001405DD /* HoldoutConfig.swift in Sources */,
848617E92863E21200B7F41B /* OdpSegmentApiManager.swift in Sources */,
6E75187B22C520D400B2B157 /* Variation.swift in Sources */,
6E7517F722C520D400B2B157 /* DataStoreMemory.swift in Sources */,
Expand Down Expand Up @@ -5592,6 +5622,7 @@
845945BC2877589D00D13E11 /* OdpConfig.swift in Sources */,
984FE5192CC8AA88004F6F41 /* UserProfileTracker.swift in Sources */,
6E75184022C520D400B2B157 /* Event.swift in Sources */,
98AC97EE2DAE4579001405DD /* HoldoutConfig.swift in Sources */,
6E7516E222C520D400B2B157 /* OPTEventDispatcher.swift in Sources */,
6E7517D422C520D400B2B157 /* DefaultNotificationCenter.swift in Sources */,
6E994B3425A3E6EA00999262 /* DecisionResponse.swift in Sources */,
Expand Down Expand Up @@ -5676,6 +5707,7 @@
845945C02877589F00D13E11 /* OdpConfig.swift in Sources */,
6E75175022C520D400B2B157 /* LogMessage.swift in Sources */,
848617EE2863E21200B7F41B /* OdpEventApiManager.swift in Sources */,
98AC97E82DAE4579001405DD /* HoldoutConfig.swift in Sources */,
6E75193222C520D500B2B157 /* OPTDataStore.swift in Sources */,
6E75190E22C520D500B2B157 /* BackgroundingCallbacks.swift in Sources */,
6E75172022C520D400B2B157 /* OptimizelyResult.swift in Sources */,
Expand Down Expand Up @@ -5841,6 +5873,7 @@
75C71A3125E454460084187E /* Group.swift in Sources */,
75C71A3225E454460084187E /* Variable.swift in Sources */,
848617DD2863E21200B7F41B /* OdpSegmentApiManager.swift in Sources */,
98AC97E22DAE4579001405DD /* HoldoutConfig.swift in Sources */,
75C71A3325E454460084187E /* Attribute.swift in Sources */,
75C71A3425E454460084187E /* BackgroundingCallbacks.swift in Sources */,
75C71A3525E454460084187E /* OPTNotificationCenter.swift in Sources */,
Expand Down Expand Up @@ -5890,6 +5923,7 @@
845945BE2877589E00D13E11 /* OdpConfig.swift in Sources */,
984FE51A2CC8AA88004F6F41 /* UserProfileTracker.swift in Sources */,
BD6485462491474500F30986 /* Event.swift in Sources */,
98AC97ED2DAE4579001405DD /* HoldoutConfig.swift in Sources */,
BD6485472491474500F30986 /* OPTEventDispatcher.swift in Sources */,
BD6485482491474500F30986 /* DefaultNotificationCenter.swift in Sources */,
6E994B3625A3E6EA00999262 /* DecisionResponse.swift in Sources */,
Expand Down
2 changes: 2 additions & 0 deletions Sources/Data Model/FeatureFlag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ struct FeatureFlag: Codable, Equatable, OptimizelyFeature {
case variables
}

// var holdoutIds: [String] = []

// MARK: - OptimizelyConfig

var experimentsMap: [String: OptimizelyExperiment] = [:]
Expand Down
4 changes: 1 addition & 3 deletions Sources/Data Model/Holdout.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,10 @@ struct Holdout: Codable, ExperimentCore {
case id, key, status, layerId, variations, trafficAllocation, audienceIds, audienceConditions, includedFlags, excludedFlags
}

var variationsMap: [String : OptimizelyVariation] = [:]
var variationsMap: [String: OptimizelyVariation] = [:]
// replace with serialized string representation with audience names when ProjectConfig is ready
var audiences: String = ""


init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

Expand Down Expand Up @@ -76,7 +75,6 @@ extension Holdout: Equatable {
}
}


extension Holdout {
var isActivated: Bool {
return status == .running
Expand Down
99 changes: 99 additions & 0 deletions Sources/Data Model/HoldoutConfig.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
//
// Copyright 2022, Optimizely, Inc. and contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Foundation

struct HoldoutConfig {
var allHoldouts: [Holdout] {
didSet {
updateHoldoutProperties()
}
}
private(set) var holdoutIdMap: [String: Holdout] = [:]
private(set) var global: [Holdout] = []
private(set) var others: [Holdout] = []
private(set) var includedHoldouts: [String: [Holdout]] = [:]
private(set) var excludedHoldouts: [String: [Holdout]] = [:]
private(set) var flagHoldoutsMap: [String: [Holdout]] = [:]

init(allholdouts: [Holdout] = []) {
self.allHoldouts = allholdouts
updateHoldoutProperties()
}

mutating func updateHoldoutProperties() {
holdoutIdMap = {
var map = [String: Holdout]()
allHoldouts.forEach { map[$0.id] = $0 }
return map
}()
flagHoldoutsMap = [:]
global = []
others = []
includedHoldouts = [:]
excludedHoldouts = [:]

for holdout in allHoldouts {
switch (holdout.includedFlags.isEmpty, holdout.excludedFlags.isEmpty) {
case (true, true):
global.append(holdout)
case (false, _):
holdout.includedFlags.forEach { flagId in
if var existing = includedHoldouts[flagId] {
existing.append(holdout)
includedHoldouts[flagId] = existing
} else {
includedHoldouts[flagId] = [holdout]
}
}
case (_, false):
others.append(holdout)
holdout.excludedFlags.forEach { flagId in
if var existing = excludedHoldouts[flagId] {
existing.append(holdout)
excludedHoldouts[flagId] = existing
} else {
excludedHoldouts[flagId] = [holdout]
}
}
}
}
}

mutating func getHoldoutForFlag(id: String) -> [Holdout] {
guard !allHoldouts.isEmpty else { return [] }

if let holdouts = flagHoldoutsMap[id] {
return holdouts
}

if let included = includedHoldouts[id], !included.isEmpty {
flagHoldoutsMap[id] = global + included
} else {
let excluded = excludedHoldouts[id] ?? []
let filteredHoldouts = others.filter { holdout in
return !excluded.contains(holdout)
}
flagHoldoutsMap[id] = global + filteredHoldouts
}
return flagHoldoutsMap[id] ?? []
}

func getHoldout(id: String) -> Holdout? {
return holdoutIdMap[id]
}
}

Loading
Loading