From b27f1c6a284450fbff8f4947455ecd53b3ca3815 Mon Sep 17 00:00:00 2001 From: Muzahidul Islam Date: Tue, 22 Oct 2024 23:31:23 +0600 Subject: [PATCH 01/14] wip: decision service updated --- .../DefaultDecisionService.swift | 150 ++++++++++++++++-- 1 file changed, 138 insertions(+), 12 deletions(-) diff --git a/Sources/Implementation/DefaultDecisionService.swift b/Sources/Implementation/DefaultDecisionService.swift index 276d3a158..7f817a147 100644 --- a/Sources/Implementation/DefaultDecisionService.swift +++ b/Sources/Implementation/DefaultDecisionService.swift @@ -22,11 +22,13 @@ struct FeatureDecision { let source: String } +typealias UserProfile = OPTUserProfileService.UPProfile + class DefaultDecisionService: OPTDecisionService { let bucketer: OPTBucketer let userProfileService: OPTUserProfileService - + var profileTracker: UserProfileTracker? // thread-safe lazy logger load (after HandlerRegisterService ready) private let threadSafeLogger = ThreadSafeLogger() var logger: OPTLogger { @@ -35,7 +37,7 @@ class DefaultDecisionService: OPTDecisionService { // user-profile-service read-modify-write lock for supporting multiple clients static let upsRMWLock = DispatchQueue(label: "ups-rmw") - + init(userProfileService: OPTUserProfileService) { self.bucketer = DefaultBucketer() self.userProfileService = userProfileService @@ -47,6 +49,73 @@ class DefaultDecisionService: OPTDecisionService { options: [OptimizelyDecideOption]? = nil) -> DecisionResponse { let reasons = DecisionReasons(options: options) + let userId = user.userId + //let attributes = user.attributes + let experimentId = experiment.id + let ignoreUPS = (options ?? []).contains(.ignoreUserProfileService) + if !ignoreUPS { + let profile = getUserProfile(userId: userId, reasons: reasons) + profileTracker = UserProfileTracker(userProfile: profile, profileUpdated: false, userProfileService: self.userProfileService) + } + + let response = getVariation(config: config, experiment: experiment, user: user, _reasons: reasons) + + let profileUpdated = profileTracker?.profileUpdated ?? false + if (!ignoreUPS && profileUpdated) { + saveProfile(profile: profileTracker?.userProfile) + profileTracker = nil + } + + return response + } + + func getVariation(config: ProjectConfig, + featureFlags: [FeatureFlag], + user: OptimizelyUserContext, + options: [OptimizelyDecideOption]? = nil) -> [DecisionResponse] { + let upsReasons = DecisionReasons(options: options) + let userId = user.userId + let ignoreUPS = (options ?? []).contains(.ignoreUserProfileService) + if !ignoreUPS { + let profile = getUserProfile(userId: userId, reasons: upsReasons) + profileTracker = UserProfileTracker(userProfile: profile, profileUpdated: false, userProfileService: self.userProfileService) + } + + var decisions = [DecisionResponse]() + + for flag in featureFlags { + var reasons = upsReasons + let decisionVariationResponse = getVariationForFeatureExperiment(config: config, featureFlag: flag, user: user) + reasons.merge(decisionVariationResponse.reasons) + var decision = decisionVariationResponse.result + if decision != nil { + decisions.append(DecisionResponse(result: decision, reasons: reasons)) + continue + } + let decisionFeaturesResponse = getVariationForFeatureRollout(config: config, featureFlag: flag, user: user) + reasons.merge(decisionFeaturesResponse.reasons) + decision = decisionFeaturesResponse.result + + decisions.append(DecisionResponse(result: decision, reasons: reasons)) + + } + + // save profile + let profileUpdated = profileTracker?.profileUpdated ?? false + if !ignoreUPS && profileUpdated { + saveProfile(profile: profileTracker?.userProfile) + profileTracker = nil + } + + return decisions + } + + func getVariation(config: ProjectConfig, + experiment: Experiment, + user: OptimizelyUserContext, + options: [OptimizelyDecideOption]? = nil, + _reasons: DecisionReasons) -> DecisionResponse { + var reasons = _reasons let userId = user.userId let attributes = user.attributes let experimentId = experiment.id @@ -85,19 +154,31 @@ class DefaultDecisionService: OPTDecisionService { reasons.addInfo(info) } - // ---- check if a valid variation is stored in the user profile ---- - let ignoreUPS = (options ?? []).contains(.ignoreUserProfileService) - - if !ignoreUPS, - let variationId = getVariationIdFromProfile(userId: userId, experimentId: experimentId), + /// FIXME: Need to check the logic + if let profile = self.profileTracker?.userProfile, + let _userId = profile[UserProfileKeys.kUserId] as? String, + let variationId = getVariationIdFromProfile(userId: _userId, experimentId: experimentId), let variation = experiment.getVariation(id: variationId) { - + let info = LogMessage.gotVariationFromUserProfile(variation.key, experiment.key, userId) logger.i(info) reasons.addInfo(info) return DecisionResponse(result: variation, reasons: reasons) } +// // ---- check if a valid variation is stored in the user profile ---- +// let ignoreUPS = (options ?? []).contains(.ignoreUserProfileService) +// +// if !ignoreUPS, +// let variationId = getVariationIdFromProfile(userId: userId, experimentId: experimentId), +// let variation = experiment.getVariation(id: variationId) { +// +// let info = LogMessage.gotVariationFromUserProfile(variation.key, experiment.key, userId) +// logger.i(info) +// reasons.addInfo(info) +// return DecisionResponse(result: variation, reasons: reasons) +// } +// var bucketedVariation: Variation? // ---- check if the user passes audience targeting before bucketing ---- let audienceResponse = doesMeetAudienceConditions(config: config, @@ -116,10 +197,7 @@ class DefaultDecisionService: OPTDecisionService { let info = LogMessage.userBucketedIntoVariationInExperiment(userId, experiment.key, variation.key) logger.i(info) reasons.addInfo(info) - // save to user profile - if !ignoreUPS { - self.saveProfile(userId: userId, experimentId: experimentId, variationId: variation.id) - } + self.profileTracker?.updateProfile(experiment: experiment, variation: variation) } else { let info = LogMessage.userNotBucketedIntoVariation(userId) logger.i(info) @@ -452,6 +530,22 @@ class DefaultDecisionService: OPTDecisionService { // MARK: - UserProfileService Helpers extension DefaultDecisionService { + // fixme: Need to cross check the logic + private func getUserProfile(userId: String, reasons: DecisionReasons) -> UserProfile { + var userProfile: UserProfile? + let lookUpUserProfile = userProfileService.lookup(userId: userId) + if lookUpUserProfile != nil { + userProfile = lookUpUserProfile + } else { + logger.w("The user profile service return nil for userId: \(userId)") + } + + if userProfile == nil { + userProfile = [String: Any]() + } + + return userProfile! + } func getVariationIdFromProfile(userId: String, experimentId: String) -> String? { @@ -483,4 +577,36 @@ extension DefaultDecisionService { } } + func saveProfile(profile: UserProfile?) { + DefaultDecisionService.upsRMWLock.sync { + guard let profile = profile else { return } + self.userProfileService.save(userProfile: profile) + } + } +} + +class UserProfileTracker { + var userProfile: UserProfile + var profileUpdated: Bool + var userProfileService: OPTUserProfileService + + init(userProfile: UserProfile, profileUpdated: Bool, userProfileService: OPTUserProfileService) { + self.userProfile = userProfile + self.profileUpdated = profileUpdated + self.userProfileService = userProfileService + } + + func updateProfile(experiment: Experiment, variation: Variation) { + let experimentId = experiment.id + let variationId = variation.id + var bucketMap = userProfile[UserProfileKeys.kBucketMap] as? OPTUserProfileService.UPBucketMap ?? OPTUserProfileService.UPBucketMap() + bucketMap[experimentId] = [UserProfileKeys.kVariationId: variationId] + userProfile[UserProfileKeys.kBucketMap] = bucketMap +// profile[UserProfileKeys.kUserId] = userId + + } + + func save() { + userProfileService.save(userProfile: userProfile) + } } From 1f9688341c1884c12a884fea81281e5568649486 Mon Sep 17 00:00:00 2001 From: Muzahidul Islam Date: Wed, 23 Oct 2024 14:41:18 +0600 Subject: [PATCH 02/14] WIP: decision service batch updated changes added --- OptimizelySwiftSDK.xcodeproj/project.pbxproj | 34 +++ .../DefaultDecisionService.swift | 283 ++++++++---------- .../Implementation/UserProfileTracker.swift | 67 +++++ 3 files changed, 219 insertions(+), 165 deletions(-) create mode 100644 Sources/Implementation/UserProfileTracker.swift diff --git a/OptimizelySwiftSDK.xcodeproj/project.pbxproj b/OptimizelySwiftSDK.xcodeproj/project.pbxproj index d5c85cdd1..af0854c49 100644 --- a/OptimizelySwiftSDK.xcodeproj/project.pbxproj +++ b/OptimizelySwiftSDK.xcodeproj/project.pbxproj @@ -1984,6 +1984,22 @@ 984E2FDD2B27199C001F477A /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 987F11D92AF3F56F0083D3F9 /* PrivacyInfo.xcprivacy */; }; 984E2FDE2B27199D001F477A /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 987F11D92AF3F56F0083D3F9 /* PrivacyInfo.xcprivacy */; }; 984E2FDF2B27199D001F477A /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 987F11D92AF3F56F0083D3F9 /* PrivacyInfo.xcprivacy */; }; + 984FE5112CC8AA88004F6F41 /* UserProfileTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 984FE5102CC8AA88004F6F41 /* UserProfileTracker.swift */; }; + 984FE5122CC8AA88004F6F41 /* UserProfileTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 984FE5102CC8AA88004F6F41 /* UserProfileTracker.swift */; }; + 984FE5132CC8AA88004F6F41 /* UserProfileTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 984FE5102CC8AA88004F6F41 /* UserProfileTracker.swift */; }; + 984FE5142CC8AA88004F6F41 /* UserProfileTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 984FE5102CC8AA88004F6F41 /* UserProfileTracker.swift */; }; + 984FE5152CC8AA88004F6F41 /* UserProfileTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 984FE5102CC8AA88004F6F41 /* UserProfileTracker.swift */; }; + 984FE5162CC8AA88004F6F41 /* UserProfileTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 984FE5102CC8AA88004F6F41 /* UserProfileTracker.swift */; }; + 984FE5172CC8AA88004F6F41 /* UserProfileTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 984FE5102CC8AA88004F6F41 /* UserProfileTracker.swift */; }; + 984FE5182CC8AA88004F6F41 /* UserProfileTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 984FE5102CC8AA88004F6F41 /* UserProfileTracker.swift */; }; + 984FE5192CC8AA88004F6F41 /* UserProfileTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 984FE5102CC8AA88004F6F41 /* UserProfileTracker.swift */; }; + 984FE51A2CC8AA88004F6F41 /* UserProfileTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 984FE5102CC8AA88004F6F41 /* UserProfileTracker.swift */; }; + 984FE51B2CC8AA88004F6F41 /* UserProfileTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 984FE5102CC8AA88004F6F41 /* UserProfileTracker.swift */; }; + 984FE51C2CC8AA88004F6F41 /* UserProfileTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 984FE5102CC8AA88004F6F41 /* UserProfileTracker.swift */; }; + 984FE51D2CC8AA88004F6F41 /* UserProfileTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 984FE5102CC8AA88004F6F41 /* UserProfileTracker.swift */; }; + 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 */; }; 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 */; }; @@ -2422,6 +2438,7 @@ 84F6BADC27FD011B004BE62A /* OptimizelyUserContextTests_ODP_Decide.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptimizelyUserContextTests_ODP_Decide.swift; sourceTree = ""; }; 98137C542A41E86F004896EB /* OptimizelyClientTests_Init_Async_Await.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyClientTests_Init_Async_Await.swift; sourceTree = ""; }; 98137C562A42BA0F004896EB /* OptimizelyUserContextTests_ODP_Aync_Await.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyUserContextTests_ODP_Aync_Await.swift; sourceTree = ""; }; + 984FE5102CC8AA88004F6F41 /* UserProfileTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileTracker.swift; sourceTree = ""; }; 987F11D92AF3F56F0083D3F9 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 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 = ""; }; @@ -2762,6 +2779,7 @@ 6E75167E22C520D400B2B157 /* DefaultBucketer.swift */, 6E75167F22C520D400B2B157 /* DefaultNotificationCenter.swift */, 6E75168022C520D400B2B157 /* DefaultDecisionService.swift */, + 984FE5102CC8AA88004F6F41 /* UserProfileTracker.swift */, 6EF8DE3024BF7D69008B9488 /* DecisionReasons.swift */, 6E994B3325A3E6EA00999262 /* DecisionResponse.swift */, 6E75168122C520D400B2B157 /* Datastore */, @@ -4129,6 +4147,7 @@ 6E14CDA22423F9C300010234 /* Array+Extension.swift in Sources */, 848617CF2863DC2700B7F41B /* OdpSegmentManager.swift in Sources */, 6E14CD952423F9A700010234 /* Group.swift in Sources */, + 984FE5142CC8AA88004F6F41 /* UserProfileTracker.swift in Sources */, 84E2E96828540B5E001114AB /* OptimizelySdkSettings.swift in Sources */, 6E14CD9A2423F9C300010234 /* DataStoreQueueStack.swift in Sources */, 6E14CD732423F96F00010234 /* OptimizelyResult.swift in Sources */, @@ -4273,6 +4292,7 @@ 6E424D1126324B620081004A /* Variable.swift in Sources */, 6E424D1226324B620081004A /* Attribute.swift in Sources */, 6E424D1326324B620081004A /* BackgroundingCallbacks.swift in Sources */, + 984FE5112CC8AA88004F6F41 /* UserProfileTracker.swift in Sources */, 845945C2287758A000D13E11 /* OdpConfig.swift in Sources */, 6E424D1426324B620081004A /* OPTNotificationCenter.swift in Sources */, 6E424D5026324C4D0081004A /* OptimizelyDecideOption.swift in Sources */, @@ -4343,6 +4363,7 @@ 8464087128130D3200CCF97D /* Integration.swift in Sources */, 6E623F03253F9045000617D0 /* DecisionInfo.swift in Sources */, 845945BD2877589E00D13E11 /* OdpConfig.swift in Sources */, + 984FE51C2CC8AA88004F6F41 /* UserProfileTracker.swift in Sources */, 6E75171322C520D400B2B157 /* OptimizelyClient+ObjC.swift in Sources */, 6E75191922C520D500B2B157 /* OPTNotificationCenter.swift in Sources */, 6E7518A122C520D400B2B157 /* FeatureFlag.swift in Sources */, @@ -4433,6 +4454,7 @@ 6E75173222C520D400B2B157 /* Constants.swift in Sources */, 848617D42863DC2700B7F41B /* OdpSegmentManager.swift in Sources */, 6E75184822C520D400B2B157 /* Event.swift in Sources */, + 984FE5172CC8AA88004F6F41 /* UserProfileTracker.swift in Sources */, 84E2E96D28540B5E001114AB /* OptimizelySdkSettings.swift in Sources */, 6E75170E22C520D400B2B157 /* OptimizelyClient.swift in Sources */, 6E75177A22C520D400B2B157 /* SDKVersion.swift in Sources */, @@ -4601,6 +4623,7 @@ 6E20050C26B4D28500278087 /* MockLogger.swift in Sources */, 6E75176A22C520D400B2B157 /* Utils.swift in Sources */, 6E75171622C520D400B2B157 /* OptimizelyClient+ObjC.swift in Sources */, + 984FE5152CC8AA88004F6F41 /* UserProfileTracker.swift in Sources */, 6E7517F022C520D400B2B157 /* DataStoreMemory.swift in Sources */, 6E9B11D922C548A200C22D81 /* OptimizelyClientTests_Invalid.swift in Sources */, 848617D02863DC2700B7F41B /* OdpSegmentManager.swift in Sources */, @@ -4702,6 +4725,7 @@ 6E7518EF22C520D400B2B157 /* ConditionHolder.swift in Sources */, 6E75182F22C520D400B2B157 /* BatchEvent.swift in Sources */, 6E75191F22C520D500B2B157 /* OPTNotificationCenter.swift in Sources */, + 984FE5202CC8AA88004F6F41 /* UserProfileTracker.swift in Sources */, 6E7518B322C520D400B2B157 /* Group.swift in Sources */, 6E20050F26B4D28500278087 /* MockLogger.swift in Sources */, 6EC6DD3A24ABF6990017D296 /* OptimizelyClient+Decide.swift in Sources */, @@ -4870,6 +4894,7 @@ 6E20051126B4D28600278087 /* MockLogger.swift in Sources */, 6E7516DF22C520D400B2B157 /* OPTUserProfileService.swift in Sources */, 6EF8DE3C24BF7D69008B9488 /* DecisionReasons.swift in Sources */, + 984FE5182CC8AA88004F6F41 /* UserProfileTracker.swift in Sources */, 6E7518B522C520D400B2B157 /* Group.swift in Sources */, 6E9B116B22C5487100C22D81 /* NotificationCenterTests.swift in Sources */, 6E7516F722C520D400B2B157 /* OptimizelyError.swift in Sources */, @@ -4972,6 +4997,7 @@ 84E2E96F28540B5E001114AB /* OptimizelySdkSettings.swift in Sources */, 6E7517A022C520D400B2B157 /* DataStoreQueueStackImpl+Extension.swift in Sources */, 6E7517AC22C520D400B2B157 /* Array+Extension.swift in Sources */, + 984FE5132CC8AA88004F6F41 /* UserProfileTracker.swift in Sources */, 6EA425A52218E6AE00B074B5 /* (null) in Sources */, 6E8A3D522637408500DAEA13 /* MockDatafileHandler.swift in Sources */, 6E75180E22C520D400B2B157 /* DataStoreFile.swift in Sources */, @@ -5070,6 +5096,7 @@ 6E6522E3278E4F3800954EA1 /* OdpManager.swift in Sources */, 6EA2CC272345618E001E7531 /* OptimizelyConfig.swift in Sources */, 84861815286D0B8900B7F41B /* OdpVuidManagerTests.swift in Sources */, + 984FE51E2CC8AA88004F6F41 /* UserProfileTracker.swift in Sources */, C78CAFA724486E0A009FE876 /* OptimizelyJSON+ObjC.swift in Sources */, 6E75185B22C520D400B2B157 /* FeatureVariable.swift in Sources */, 6E7516B522C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, @@ -5239,6 +5266,7 @@ 84E2E96A28540B5E001114AB /* OptimizelySdkSettings.swift in Sources */, 6E75179B22C520D400B2B157 /* DataStoreQueueStackImpl+Extension.swift in Sources */, 6E7517A722C520D400B2B157 /* Array+Extension.swift in Sources */, + 984FE5162CC8AA88004F6F41 /* UserProfileTracker.swift in Sources */, 6EA425962218E6AD00B074B5 /* (null) in Sources */, 6E8A3D4D2637408500DAEA13 /* MockDatafileHandler.swift in Sources */, 6E75180922C520D400B2B157 /* DataStoreFile.swift in Sources */, @@ -5307,6 +5335,7 @@ 84B4D75A27E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */, 6E7517DA22C520D400B2B157 /* DefaultNotificationCenter.swift in Sources */, 6E7517E622C520D400B2B157 /* DefaultDecisionService.swift in Sources */, + 984FE51F2CC8AA88004F6F41 /* UserProfileTracker.swift in Sources */, 6E75171822C520D400B2B157 /* OptimizelyClient+ObjC.swift in Sources */, 6E75174822C520D400B2B157 /* HandlerRegistryService.swift in Sources */, 84E2E94C2852A378001114AB /* OdpVuidManager.swift in Sources */, @@ -5408,6 +5437,7 @@ 84B4D75F27E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */, 6E7517DF22C520D400B2B157 /* DefaultNotificationCenter.swift in Sources */, 6E7517EB22C520D400B2B157 /* DefaultDecisionService.swift in Sources */, + 984FE51D2CC8AA88004F6F41 /* UserProfileTracker.swift in Sources */, 6E75171D22C520D400B2B157 /* OptimizelyClient+ObjC.swift in Sources */, 6E75174D22C520D400B2B157 /* HandlerRegistryService.swift in Sources */, 84E2E9512852A378001114AB /* OdpVuidManager.swift in Sources */, @@ -5493,6 +5523,7 @@ 8464087028130D3200CCF97D /* Integration.swift in Sources */, 6E623F02253F9045000617D0 /* DecisionInfo.swift in Sources */, 845945BC2877589D00D13E11 /* OdpConfig.swift in Sources */, + 984FE5192CC8AA88004F6F41 /* UserProfileTracker.swift in Sources */, 6E75184022C520D400B2B157 /* Event.swift in Sources */, 6E7516E222C520D400B2B157 /* OPTEventDispatcher.swift in Sources */, 6E7517D422C520D400B2B157 /* DefaultNotificationCenter.swift in Sources */, @@ -5583,6 +5614,7 @@ 6E75172C22C520D400B2B157 /* Constants.swift in Sources */, 848617CC2863DC2700B7F41B /* OdpSegmentManager.swift in Sources */, 6E75184222C520D400B2B157 /* Event.swift in Sources */, + 984FE5122CC8AA88004F6F41 /* UserProfileTracker.swift in Sources */, 84E2E96528540B5E001114AB /* OptimizelySdkSettings.swift in Sources */, 6E75170822C520D400B2B157 /* OptimizelyClient.swift in Sources */, 6E75177422C520D400B2B157 /* SDKVersion.swift in Sources */, @@ -5727,6 +5759,7 @@ 75C71A2925E454460084187E /* ProjectConfig.swift in Sources */, 75C71A2A25E454460084187E /* FeatureVariable.swift in Sources */, 75C71A2B25E454460084187E /* Rollout.swift in Sources */, + 984FE51B2CC8AA88004F6F41 /* UserProfileTracker.swift in Sources */, 6E424BFF263228FD0081004A /* AtomicDictionary.swift in Sources */, 75C71A2C25E454460084187E /* Variation.swift in Sources */, 75C71A2D25E454460084187E /* TrafficAllocation.swift in Sources */, @@ -5782,6 +5815,7 @@ 8464087228130D3200CCF97D /* Integration.swift in Sources */, 6E623F04253F9045000617D0 /* DecisionInfo.swift in Sources */, 845945BE2877589E00D13E11 /* OdpConfig.swift in Sources */, + 984FE51A2CC8AA88004F6F41 /* UserProfileTracker.swift in Sources */, BD6485462491474500F30986 /* Event.swift in Sources */, BD6485472491474500F30986 /* OPTEventDispatcher.swift in Sources */, BD6485482491474500F30986 /* DefaultNotificationCenter.swift in Sources */, diff --git a/Sources/Implementation/DefaultDecisionService.swift b/Sources/Implementation/DefaultDecisionService.swift index 7f817a147..b3379cd40 100644 --- a/Sources/Implementation/DefaultDecisionService.swift +++ b/Sources/Implementation/DefaultDecisionService.swift @@ -25,24 +25,24 @@ struct FeatureDecision { typealias UserProfile = OPTUserProfileService.UPProfile class DefaultDecisionService: OPTDecisionService { - let bucketer: OPTBucketer let userProfileService: OPTUserProfileService - var profileTracker: UserProfileTracker? // thread-safe lazy logger load (after HandlerRegisterService ready) private let threadSafeLogger = ThreadSafeLogger() - var logger: OPTLogger { - return threadSafeLogger.logger - } // user-profile-service read-modify-write lock for supporting multiple clients static let upsRMWLock = DispatchQueue(label: "ups-rmw") + + var logger: OPTLogger { + return threadSafeLogger.logger + } init(userProfileService: OPTUserProfileService) { self.bucketer = DefaultBucketer() self.userProfileService = userProfileService } + /// Public Method func getVariation(config: ProjectConfig, experiment: Experiment, user: OptimizelyUserContext, @@ -50,72 +50,29 @@ class DefaultDecisionService: OPTDecisionService { let reasons = DecisionReasons(options: options) let userId = user.userId - //let attributes = user.attributes - let experimentId = experiment.id let ignoreUPS = (options ?? []).contains(.ignoreUserProfileService) + var profileTracker: UserProfileTracker? if !ignoreUPS { - let profile = getUserProfile(userId: userId, reasons: reasons) - profileTracker = UserProfileTracker(userProfile: profile, profileUpdated: false, userProfileService: self.userProfileService) + profileTracker = UserProfileTracker(userId: userId, userProfileService: self.userProfileService, logger: self.logger) + profileTracker?.loadUserProfile() } - let response = getVariation(config: config, experiment: experiment, user: user, _reasons: reasons) + let response = getVariation(config: config, experiment: experiment, user: user, userProfileTracker: profileTracker, reasons: reasons) - let profileUpdated = profileTracker?.profileUpdated ?? false - if (!ignoreUPS && profileUpdated) { - saveProfile(profile: profileTracker?.userProfile) - profileTracker = nil + if (!ignoreUPS) { + profileTracker?.save() } return response } - func getVariation(config: ProjectConfig, - featureFlags: [FeatureFlag], - user: OptimizelyUserContext, - options: [OptimizelyDecideOption]? = nil) -> [DecisionResponse] { - let upsReasons = DecisionReasons(options: options) - let userId = user.userId - let ignoreUPS = (options ?? []).contains(.ignoreUserProfileService) - if !ignoreUPS { - let profile = getUserProfile(userId: userId, reasons: upsReasons) - profileTracker = UserProfileTracker(userProfile: profile, profileUpdated: false, userProfileService: self.userProfileService) - } - - var decisions = [DecisionResponse]() - - for flag in featureFlags { - var reasons = upsReasons - let decisionVariationResponse = getVariationForFeatureExperiment(config: config, featureFlag: flag, user: user) - reasons.merge(decisionVariationResponse.reasons) - var decision = decisionVariationResponse.result - if decision != nil { - decisions.append(DecisionResponse(result: decision, reasons: reasons)) - continue - } - let decisionFeaturesResponse = getVariationForFeatureRollout(config: config, featureFlag: flag, user: user) - reasons.merge(decisionFeaturesResponse.reasons) - decision = decisionFeaturesResponse.result - - decisions.append(DecisionResponse(result: decision, reasons: reasons)) - - } - - // save profile - let profileUpdated = profileTracker?.profileUpdated ?? false - if !ignoreUPS && profileUpdated { - saveProfile(profile: profileTracker?.userProfile) - profileTracker = nil - } - - return decisions - } - func getVariation(config: ProjectConfig, experiment: Experiment, user: OptimizelyUserContext, options: [OptimizelyDecideOption]? = nil, - _reasons: DecisionReasons) -> DecisionResponse { - var reasons = _reasons + userProfileTracker: UserProfileTracker?, + reasons: DecisionReasons) -> DecisionResponse { + var decisionReasons = reasons let userId = user.userId let attributes = user.attributes let experimentId = experiment.id @@ -127,16 +84,18 @@ class DefaultDecisionService: OPTDecisionService { if !experiment.isActivated { let info = LogMessage.experimentNotRunning(experiment.key) logger.i(info) - reasons.addInfo(info) - return DecisionResponse(result: nil, reasons: reasons) + decisionReasons.addInfo(info) + return DecisionResponse(result: nil, reasons: decisionReasons) } // ---- check if the user is forced into a variation ---- let decisionResponse = config.getForcedVariation(experimentKey: experiment.key, userId: userId) - reasons.merge(decisionResponse.reasons) + + decisionReasons.merge(decisionResponse.reasons) + if let variationId = decisionResponse.result?.id, let variation = experiment.getVariation(id: variationId) { - return DecisionResponse(result: variation, reasons: reasons) + return DecisionResponse(result: variation, reasons: decisionReasons) } // ---- check to see if user is white-listed for a certain variation ---- @@ -144,73 +103,61 @@ class DefaultDecisionService: OPTDecisionService { if let variation = experiment.getVariation(key: variationKey) { let info = LogMessage.forcedVariationFound(variationKey, userId) logger.i(info) - reasons.addInfo(info) - return DecisionResponse(result: variation, reasons: reasons) + decisionReasons.addInfo(info) + return DecisionResponse(result: variation, reasons: decisionReasons) } // mapped to invalid variation - ignore and continue for other deciesions let info = LogMessage.forcedVariationFoundButInvalid(variationKey, userId) logger.e(info) - reasons.addInfo(info) + decisionReasons.addInfo(info) } - /// FIXME: Need to check the logic - if let profile = self.profileTracker?.userProfile, - let _userId = profile[UserProfileKeys.kUserId] as? String, + /// Load variation from tracker + if let _userId = userProfileTracker?.userId, let variationId = getVariationIdFromProfile(userId: _userId, experimentId: experimentId), let variation = experiment.getVariation(id: variationId) { - + let info = LogMessage.gotVariationFromUserProfile(variation.key, experiment.key, userId) logger.i(info) - reasons.addInfo(info) - return DecisionResponse(result: variation, reasons: reasons) + decisionReasons.addInfo(info) + return DecisionResponse(result: variation, reasons: decisionReasons) } -// // ---- check if a valid variation is stored in the user profile ---- -// let ignoreUPS = (options ?? []).contains(.ignoreUserProfileService) -// -// if !ignoreUPS, -// let variationId = getVariationIdFromProfile(userId: userId, experimentId: experimentId), -// let variation = experiment.getVariation(id: variationId) { -// -// let info = LogMessage.gotVariationFromUserProfile(variation.key, experiment.key, userId) -// logger.i(info) -// reasons.addInfo(info) -// return DecisionResponse(result: variation, reasons: reasons) -// } -// var bucketedVariation: Variation? // ---- check if the user passes audience targeting before bucketing ---- let audienceResponse = doesMeetAudienceConditions(config: config, experiment: experiment, user: user) - reasons.merge(audienceResponse.reasons) + decisionReasons.merge(audienceResponse.reasons) + if audienceResponse.result ?? false { // bucket user into a variation let decisionResponse = bucketer.bucketExperiment(config: config, experiment: experiment, bucketingId: bucketingId) - reasons.merge(decisionResponse.reasons) + decisionReasons.merge(decisionResponse.reasons) + bucketedVariation = decisionResponse.result if let variation = bucketedVariation { let info = LogMessage.userBucketedIntoVariationInExperiment(userId, experiment.key, variation.key) logger.i(info) - reasons.addInfo(info) - self.profileTracker?.updateProfile(experiment: experiment, variation: variation) + decisionReasons.addInfo(info) + userProfileTracker?.updateProfile(experiment: experiment, variation: variation) } else { let info = LogMessage.userNotBucketedIntoVariation(userId) logger.i(info) - reasons.addInfo(info) + decisionReasons.addInfo(info) } } else { let info = LogMessage.userNotInExperiment(userId, experiment.key) logger.i(info) - reasons.addInfo(info) + decisionReasons.addInfo(info) } - return DecisionResponse(result: bucketedVariation, reasons: reasons) + return DecisionResponse(result: bucketedVariation, reasons: decisionReasons) } func doesMeetAudienceConditions(config: ProjectConfig, @@ -268,42 +215,95 @@ class DefaultDecisionService: OPTDecisionService { return DecisionResponse(result: result, reasons: reasons) } + /// Public Method func getVariationForFeature(config: ProjectConfig, featureFlag: FeatureFlag, user: OptimizelyUserContext, options: [OptimizelyDecideOption]? = nil) -> DecisionResponse { - let reasons = DecisionReasons(options: options) - // Evaluate in this order: + guard let response = getVariationForFeatureList(config: config, featureFlags: [featureFlag], user: user, options: options).first else { + let reasons = DecisionReasons(options: options) + return DecisionResponse(result: nil, reasons: reasons) + } - // 1. Attempt to bucket user into experiment using feature flag. - // Check if the feature flag is under an experiment and the the user is bucketed into one of these experiments - var decisionResponse = getVariationForFeatureExperiment(config: config, - featureFlag: featureFlag, - user: user, - options: options) - reasons.merge(decisionResponse.reasons) - if let decision = decisionResponse.result { - return DecisionResponse(result: decision, reasons: reasons) + return response + +// // Evaluate in this order: +// +// // 1. Attempt to bucket user into experiment using feature flag. +// // Check if the feature flag is under an experiment and the the user is bucketed into one of these experiments +// var decisionResponse = getVariationForFeatureExperiment(config: config, +// featureFlag: featureFlag, +// user: user, +// userProfileTracker: nil, +// options: options) +// reasons.merge(decisionResponse.reasons) +// if let decision = decisionResponse.result { +// return DecisionResponse(result: decision, reasons: reasons) +// } +// +// // 2. Attempt to bucket user into rollout using the feature flag. +// // Check if the feature flag has rollout and the user is bucketed into one of it's rules +// decisionResponse = getVariationForFeatureRollout(config: config, +// featureFlag: featureFlag, +// user: user, +// options: options) +// reasons.merge(decisionResponse.reasons) +// if let decision = decisionResponse.result { +// return DecisionResponse(result: decision, reasons: reasons) +// } +// +// return DecisionResponse(result: nil, reasons: reasons) + } + + func getVariationForFeatureList(config: ProjectConfig, + featureFlags: [FeatureFlag], + user: OptimizelyUserContext, + options: [OptimizelyDecideOption]? = nil) -> [DecisionResponse] { + + var reasons = DecisionReasons(options: options) + let userId = user.userId + let ignoreUPS = (options ?? []).contains(.ignoreUserProfileService) + var profileTracker: UserProfileTracker? + if !ignoreUPS { + profileTracker = UserProfileTracker(userId: userId, userProfileService: self.userProfileService, logger: self.logger) + profileTracker?.loadUserProfile() } - // 2. Attempt to bucket user into rollout using the feature flag. - // Check if the feature flag has rollout and the user is bucketed into one of it's rules - decisionResponse = getVariationForFeatureRollout(config: config, - featureFlag: featureFlag, - user: user, - options: options) - reasons.merge(decisionResponse.reasons) - if let decision = decisionResponse.result { - return DecisionResponse(result: decision, reasons: reasons) + var decisions = [DecisionResponse]() + + for featureFlag in featureFlags { + var decisionResponse = getVariationForFeatureExperiment(config: config, featureFlag: featureFlag, user: user, userProfileTracker: profileTracker) + + reasons.merge(decisionResponse.reasons) + + if let decision = decisionResponse.result { + decisions.append(DecisionResponse(result: decision, reasons: reasons)) + continue + } + + decisionResponse = getVariationForFeatureRollout(config: config, featureFlag: featureFlag, user: user) + + reasons.merge(decisionResponse.reasons) + + if let decision = decisionResponse.result { + decisions.append(DecisionResponse(result: decision, reasons: reasons)) + } } - return DecisionResponse(result: nil, reasons: reasons) + // save profile + if !ignoreUPS { + profileTracker?.save() + } + + return decisions } + func getVariationForFeatureExperiment(config: ProjectConfig, featureFlag: FeatureFlag, user: OptimizelyUserContext, + userProfileTracker: UserProfileTracker? = nil, options: [OptimizelyDecideOption]? = nil) -> DecisionResponse { let reasons = DecisionReasons(options: options) @@ -322,6 +322,7 @@ class DefaultDecisionService: OPTDecisionService { flagKey: featureFlag.key, rule: experiment, user: user, + userProfileTracker: userProfileTracker, options: options) reasons.merge(decisionResponse.reasons) if let variation = decisionResponse.result { @@ -392,8 +393,9 @@ class DefaultDecisionService: OPTDecisionService { flagKey: String, rule: Experiment, user: OptimizelyUserContext, + userProfileTracker: UserProfileTracker?, options: [OptimizelyDecideOption]? = nil) -> DecisionResponse { - let reasons = DecisionReasons(options: options) + var reasons = DecisionReasons(options: options) // check forced-decision first @@ -406,18 +408,18 @@ class DefaultDecisionService: OPTDecisionService { return DecisionResponse(result: variation, reasons: reasons) } - // regular decision - let decisionResponse = getVariation(config: config, experiment: rule, user: user, - options: options) + userProfileTracker: userProfileTracker, + reasons: reasons) reasons.merge(decisionResponse.reasons) let variation = decisionResponse.result return DecisionResponse(result: variation, reasons: reasons) } + func getVariationFromDeliveryRule(config: ProjectConfig, flagKey: String, rules: [Experiment], @@ -502,6 +504,7 @@ class DefaultDecisionService: OPTDecisionService { return bucketingId } + /// Public Method func findValidatedForcedDecision(config: ProjectConfig, user: OptimizelyUserContext, context: OptimizelyDecisionContext) -> DecisionResponse { @@ -530,23 +533,6 @@ class DefaultDecisionService: OPTDecisionService { // MARK: - UserProfileService Helpers extension DefaultDecisionService { - // fixme: Need to cross check the logic - private func getUserProfile(userId: String, reasons: DecisionReasons) -> UserProfile { - var userProfile: UserProfile? - let lookUpUserProfile = userProfileService.lookup(userId: userId) - if lookUpUserProfile != nil { - userProfile = lookUpUserProfile - } else { - logger.w("The user profile service return nil for userId: \(userId)") - } - - if userProfile == nil { - userProfile = [String: Any]() - } - - return userProfile! - } - func getVariationIdFromProfile(userId: String, experimentId: String) -> String? { if let profile = userProfileService.lookup(userId: userId), @@ -576,37 +562,4 @@ extension DefaultDecisionService { self.logger.i(.savedVariationInUserProfile(variationId, experimentId, userId)) } } - - func saveProfile(profile: UserProfile?) { - DefaultDecisionService.upsRMWLock.sync { - guard let profile = profile else { return } - self.userProfileService.save(userProfile: profile) - } - } -} - -class UserProfileTracker { - var userProfile: UserProfile - var profileUpdated: Bool - var userProfileService: OPTUserProfileService - - init(userProfile: UserProfile, profileUpdated: Bool, userProfileService: OPTUserProfileService) { - self.userProfile = userProfile - self.profileUpdated = profileUpdated - self.userProfileService = userProfileService - } - - func updateProfile(experiment: Experiment, variation: Variation) { - let experimentId = experiment.id - let variationId = variation.id - var bucketMap = userProfile[UserProfileKeys.kBucketMap] as? OPTUserProfileService.UPBucketMap ?? OPTUserProfileService.UPBucketMap() - bucketMap[experimentId] = [UserProfileKeys.kVariationId: variationId] - userProfile[UserProfileKeys.kBucketMap] = bucketMap -// profile[UserProfileKeys.kUserId] = userId - - } - - func save() { - userProfileService.save(userProfile: userProfile) - } } diff --git a/Sources/Implementation/UserProfileTracker.swift b/Sources/Implementation/UserProfileTracker.swift new file mode 100644 index 000000000..29806f9ab --- /dev/null +++ b/Sources/Implementation/UserProfileTracker.swift @@ -0,0 +1,67 @@ +// +// 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 + +class UserProfileTracker { + var userId: String + var profileUpdated: Bool = false + var userProfileService: OPTUserProfileService + var userProfile: UserProfile? + var logger: OPTLogger + + // user-profile-service read-modify-write lock for supporting multiple clients + static let upsRMWLock = DispatchQueue(label: "ups-rmw") + + init(userId: String, userProfileService: OPTUserProfileService, logger: OPTLogger) { + self.userId = userId + self.userProfileService = userProfileService + self.logger = logger + } + + func loadUserProfile() { + userProfile = userProfileService.lookup(userId: userId) ?? [String: Any]() + } + + func updateProfile(experiment: Experiment, variation: Variation) { + let experimentId = experiment.id + let variationId = variation.id + var bucketMap = userProfile?[UserProfileKeys.kBucketMap] as? OPTUserProfileService.UPBucketMap ?? OPTUserProfileService.UPBucketMap() + bucketMap[experimentId] = [UserProfileKeys.kVariationId: variationId] + userProfile?[UserProfileKeys.kBucketMap] = bucketMap + userProfile?[UserProfileKeys.kUserId] = userId + profileUpdated = true + logger.i("Update variation of experiment \(experimentId) for user \(userId)") + } + + func save() { + UserProfileTracker.upsRMWLock.sync { + guard profileUpdated else { + logger.w("Profile not updated for \(userId)") + return + } + + guard let userProfile else { + logger.e("Failed to save user profile for \(userId)") + return + } + + userProfileService.save(userProfile: userProfile) + logger.i("Saved user profile for \(userId)") + } + + } +} From e59c08e8ade8184a560fdf9a21c4078766849f1d Mon Sep 17 00:00:00 2001 From: Muzahidul Islam Date: Wed, 23 Oct 2024 14:49:27 +0600 Subject: [PATCH 03/14] WIP: Reason merge logic updated --- .../DefaultDecisionService.swift | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/Sources/Implementation/DefaultDecisionService.swift b/Sources/Implementation/DefaultDecisionService.swift index b3379cd40..6d9a5588c 100644 --- a/Sources/Implementation/DefaultDecisionService.swift +++ b/Sources/Implementation/DefaultDecisionService.swift @@ -47,7 +47,7 @@ class DefaultDecisionService: OPTDecisionService { experiment: Experiment, user: OptimizelyUserContext, options: [OptimizelyDecideOption]? = nil) -> DecisionResponse { - let reasons = DecisionReasons(options: options) +// let reasons = DecisionReasons(options: options) let userId = user.userId let ignoreUPS = (options ?? []).contains(.ignoreUserProfileService) @@ -57,7 +57,7 @@ class DefaultDecisionService: OPTDecisionService { profileTracker?.loadUserProfile() } - let response = getVariation(config: config, experiment: experiment, user: user, userProfileTracker: profileTracker, reasons: reasons) + let response = getVariation(config: config, experiment: experiment, user: user, userProfileTracker: profileTracker) if (!ignoreUPS) { profileTracker?.save() @@ -70,9 +70,9 @@ class DefaultDecisionService: OPTDecisionService { experiment: Experiment, user: OptimizelyUserContext, options: [OptimizelyDecideOption]? = nil, - userProfileTracker: UserProfileTracker?, - reasons: DecisionReasons) -> DecisionResponse { - var decisionReasons = reasons + userProfileTracker: UserProfileTracker?) -> DecisionResponse { +// var decisionReasons = reasons + var decisionReasons = DecisionReasons(options: options) let userId = user.userId let attributes = user.attributes let experimentId = experiment.id @@ -396,9 +396,7 @@ class DefaultDecisionService: OPTDecisionService { userProfileTracker: UserProfileTracker?, options: [OptimizelyDecideOption]? = nil) -> DecisionResponse { var reasons = DecisionReasons(options: options) - // check forced-decision first - let forcedDecisionResponse = findValidatedForcedDecision(config: config, user: user, context: OptimizelyDecisionContext(flagKey: flagKey, ruleKey: rule.key)) @@ -411,11 +409,9 @@ class DefaultDecisionService: OPTDecisionService { let decisionResponse = getVariation(config: config, experiment: rule, user: user, - userProfileTracker: userProfileTracker, - reasons: reasons) - reasons.merge(decisionResponse.reasons) + userProfileTracker: userProfileTracker) let variation = decisionResponse.result - + reasons.merge(decisionResponse.reasons) return DecisionResponse(result: variation, reasons: reasons) } From 541b9f505327849c3a69a906482734ea26dfa95e Mon Sep 17 00:00:00 2001 From: Muzahidul Islam Date: Wed, 23 Oct 2024 14:58:53 +0600 Subject: [PATCH 04/14] WIP: clean up --- .../DefaultDecisionService.swift | 39 +++++++++---------- .../Implementation/UserProfileTracker.swift | 4 +- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/Sources/Implementation/DefaultDecisionService.swift b/Sources/Implementation/DefaultDecisionService.swift index 6d9a5588c..45c702de6 100644 --- a/Sources/Implementation/DefaultDecisionService.swift +++ b/Sources/Implementation/DefaultDecisionService.swift @@ -47,8 +47,6 @@ class DefaultDecisionService: OPTDecisionService { experiment: Experiment, user: OptimizelyUserContext, options: [OptimizelyDecideOption]? = nil) -> DecisionResponse { -// let reasons = DecisionReasons(options: options) - let userId = user.userId let ignoreUPS = (options ?? []).contains(.ignoreUserProfileService) var profileTracker: UserProfileTracker? @@ -71,8 +69,7 @@ class DefaultDecisionService: OPTDecisionService { user: OptimizelyUserContext, options: [OptimizelyDecideOption]? = nil, userProfileTracker: UserProfileTracker?) -> DecisionResponse { -// var decisionReasons = reasons - var decisionReasons = DecisionReasons(options: options) + let reasons = DecisionReasons(options: options) let userId = user.userId let attributes = user.attributes let experimentId = experiment.id @@ -84,18 +81,18 @@ class DefaultDecisionService: OPTDecisionService { if !experiment.isActivated { let info = LogMessage.experimentNotRunning(experiment.key) logger.i(info) - decisionReasons.addInfo(info) - return DecisionResponse(result: nil, reasons: decisionReasons) + reasons.addInfo(info) + return DecisionResponse(result: nil, reasons: reasons) } // ---- check if the user is forced into a variation ---- let decisionResponse = config.getForcedVariation(experimentKey: experiment.key, userId: userId) - decisionReasons.merge(decisionResponse.reasons) + reasons.merge(decisionResponse.reasons) if let variationId = decisionResponse.result?.id, let variation = experiment.getVariation(id: variationId) { - return DecisionResponse(result: variation, reasons: decisionReasons) + return DecisionResponse(result: variation, reasons: reasons) } // ---- check to see if user is white-listed for a certain variation ---- @@ -103,14 +100,14 @@ class DefaultDecisionService: OPTDecisionService { if let variation = experiment.getVariation(key: variationKey) { let info = LogMessage.forcedVariationFound(variationKey, userId) logger.i(info) - decisionReasons.addInfo(info) - return DecisionResponse(result: variation, reasons: decisionReasons) + reasons.addInfo(info) + return DecisionResponse(result: variation, reasons: reasons) } // mapped to invalid variation - ignore and continue for other deciesions let info = LogMessage.forcedVariationFoundButInvalid(variationKey, userId) logger.e(info) - decisionReasons.addInfo(info) + reasons.addInfo(info) } /// Load variation from tracker @@ -120,8 +117,8 @@ class DefaultDecisionService: OPTDecisionService { let info = LogMessage.gotVariationFromUserProfile(variation.key, experiment.key, userId) logger.i(info) - decisionReasons.addInfo(info) - return DecisionResponse(result: variation, reasons: decisionReasons) + reasons.addInfo(info) + return DecisionResponse(result: variation, reasons: reasons) } var bucketedVariation: Variation? @@ -129,35 +126,35 @@ class DefaultDecisionService: OPTDecisionService { let audienceResponse = doesMeetAudienceConditions(config: config, experiment: experiment, user: user) - decisionReasons.merge(audienceResponse.reasons) + reasons.merge(audienceResponse.reasons) if audienceResponse.result ?? false { // bucket user into a variation let decisionResponse = bucketer.bucketExperiment(config: config, experiment: experiment, bucketingId: bucketingId) - decisionReasons.merge(decisionResponse.reasons) + reasons.merge(decisionResponse.reasons) bucketedVariation = decisionResponse.result if let variation = bucketedVariation { let info = LogMessage.userBucketedIntoVariationInExperiment(userId, experiment.key, variation.key) logger.i(info) - decisionReasons.addInfo(info) + reasons.addInfo(info) userProfileTracker?.updateProfile(experiment: experiment, variation: variation) } else { let info = LogMessage.userNotBucketedIntoVariation(userId) logger.i(info) - decisionReasons.addInfo(info) + reasons.addInfo(info) } } else { let info = LogMessage.userNotInExperiment(userId, experiment.key) logger.i(info) - decisionReasons.addInfo(info) + reasons.addInfo(info) } - return DecisionResponse(result: bucketedVariation, reasons: decisionReasons) + return DecisionResponse(result: bucketedVariation, reasons: reasons) } func doesMeetAudienceConditions(config: ProjectConfig, @@ -261,7 +258,7 @@ class DefaultDecisionService: OPTDecisionService { user: OptimizelyUserContext, options: [OptimizelyDecideOption]? = nil) -> [DecisionResponse] { - var reasons = DecisionReasons(options: options) + let reasons = DecisionReasons(options: options) let userId = user.userId let ignoreUPS = (options ?? []).contains(.ignoreUserProfileService) var profileTracker: UserProfileTracker? @@ -395,7 +392,7 @@ class DefaultDecisionService: OPTDecisionService { user: OptimizelyUserContext, userProfileTracker: UserProfileTracker?, options: [OptimizelyDecideOption]? = nil) -> DecisionResponse { - var reasons = DecisionReasons(options: options) + let reasons = DecisionReasons(options: options) // check forced-decision first let forcedDecisionResponse = findValidatedForcedDecision(config: config, user: user, diff --git a/Sources/Implementation/UserProfileTracker.swift b/Sources/Implementation/UserProfileTracker.swift index 29806f9ab..c8dda38b4 100644 --- a/Sources/Implementation/UserProfileTracker.swift +++ b/Sources/Implementation/UserProfileTracker.swift @@ -54,12 +54,12 @@ class UserProfileTracker { return } - guard let userProfile else { + guard let profile = userProfile else { logger.e("Failed to save user profile for \(userId)") return } - userProfileService.save(userProfile: userProfile) + userProfileService.save(userProfile: profile) logger.i("Saved user profile for \(userId)") } From 9e493fd2003cd6347c83842805168c61eb34e71c Mon Sep 17 00:00:00 2001 From: Muzahidul Islam Date: Wed, 23 Oct 2024 20:40:11 +0600 Subject: [PATCH 05/14] WIP: decide method updated --- .../OptimizelyClient+Decide.swift | 206 ++++++++++++------ 1 file changed, 136 insertions(+), 70 deletions(-) diff --git a/Sources/Optimizely+Decide/OptimizelyClient+Decide.swift b/Sources/Optimizely+Decide/OptimizelyClient+Decide.swift index da7e3c04b..0b32b5d8d 100644 --- a/Sources/Optimizely+Decide/OptimizelyClient+Decide.swift +++ b/Sources/Optimizely+Decide/OptimizelyClient+Decide.swift @@ -62,57 +62,122 @@ extension OptimizelyClient { key: String, options: [OptimizelyDecideOption]? = nil) -> OptimizelyDecision { - guard let config = self.config else { + guard config != nil else { return OptimizelyDecision.errorDecision(key: key, user: user, error: .sdkNotReady) } + + let allOptions = defaultDecideOptions + (options ?? []) + let decisionMap = decide(user: user, keys: [key], options: allOptions) + return decisionMap[key] ?? OptimizelyDecision.errorDecision(key: key, user: user, error: .generic) + } + + func decide(user: OptimizelyUserContext, + keys: [String], + options: [OptimizelyDecideOption]? = nil) -> [String: OptimizelyDecision] { + guard let config = self.config else { + logger.e(OptimizelyError.sdkNotReady) + return [:] + } + + var decisionMap = [String : OptimizelyDecision]() + + guard keys.count > 0 else { return decisionMap } + + var validKeys = [String]() + var flagsWithoutForceDecision = [FeatureFlag]() + var flagDecisions = [String: FeatureDecision]() + var decisionReasonMap = [String : DecisionReasons]() - guard let feature = config.getFeatureFlag(key: key) else { - return OptimizelyDecision.errorDecision(key: key, user: user, error: .featureKeyInvalid(key)) + let allOptions = options ?? [] + + for key in keys { + guard let flags = config.getFeatureFlag(key: key) else { + decisionMap[key] = OptimizelyDecision.errorDecision(key: key, user: user, error: .featureKeyInvalid(key)) + continue + } + validKeys.append(key) + + // check forced-decisions first + let decisionReasons = DecisionReasons(options: allOptions) + let forcedDecisionResponse = decisionService.findValidatedForcedDecision(config: config, + user: user, + context: OptimizelyDecisionContext(flagKey: key)) + + decisionReasons.merge(forcedDecisionResponse.reasons) + decisionReasonMap[key] = decisionReasons + + if let variation = forcedDecisionResponse.result { + let featureDecision = FeatureDecision(experiment: nil, variation: variation, source: Constants.DecisionSource.featureTest.rawValue) + flagDecisions[key] = featureDecision + } else { + flagsWithoutForceDecision.append(flags) + } } - let userId = user.userId - let attributes = user.attributes - let allOptions = defaultDecideOptions + (options ?? []) - let reasons = DecisionReasons(options: allOptions) - var decisionEventDispatched = false - var enabled = false + let decisionList = (decisionService as? DefaultDecisionService)?.getVariationForFeatureList(config: config, featureFlags: flagsWithoutForceDecision, user: user, options: allOptions) - var decision: FeatureDecision? + for index in 0.. OptimizelyDecision { - if let variation = forcedDecisionResponse.result { - decision = FeatureDecision(experiment: nil, variation: variation, source: Constants.DecisionSource.featureTest.rawValue) - } else { - // regular decision - - let decisionResponse = decisionService.getVariationForFeature(config: config, - featureFlag: feature, - user: user, - options: allOptions) - reasons.merge(decisionResponse.reasons) - decision = decisionResponse.result - } - - if let featureEnabled = decision?.variation.featureEnabled { - enabled = featureEnabled + guard let feature = config.getFeatureFlag(key: flagKey) else { + return OptimizelyDecision.errorDecision(key: flagKey, user: user, error: .featureKeyInvalid(flagKey)) } + let userId = user.userId + let attributes = user.attributes + let flagEnabled = flagDecision?.variation.featureEnabled ?? false + + logger.i("Feature \(flagKey) is enabled for user \(userId) \(flagEnabled)") + + var decisionEventDispatched = false + if !allOptions.contains(.disableDecisionEvent) { - let ruleType = decision?.source ?? Constants.DecisionSource.rollout.rawValue - if shouldSendDecisionEvent(source: ruleType, decision: decision) { - sendImpressionEvent(experiment: decision?.experiment, - variation: decision?.variation, + let ruleType = flagDecision?.source ?? Constants.DecisionSource.rollout.rawValue + if shouldSendDecisionEvent(source: ruleType, decision: flagDecision) { + sendImpressionEvent(experiment: flagDecision?.experiment, + variation: flagDecision?.variation, userId: userId, attributes: attributes, flagKey: feature.key, ruleType: ruleType, - enabled: enabled) + enabled: flagEnabled) decisionEventDispatched = true } } @@ -120,9 +185,9 @@ extension OptimizelyClient { var variableMap = [String: Any]() if !allOptions.contains(.excludeVariables) { let decisionResponse = getDecisionVariableMap(feature: feature, - variation: decision?.variation, - enabled: enabled) - reasons.merge(decisionResponse.reasons) + variation: flagDecision?.variation, + enabled: flagEnabled) + decisionReasons.merge(decisionResponse.reasons) variableMap = decisionResponse.result ?? [:] } @@ -130,27 +195,27 @@ extension OptimizelyClient { if let opt = OptimizelyJSON(map: variableMap) { optimizelyJSON = opt } else { - reasons.addError(OptimizelyError.invalidJSONVariable) + decisionReasons.addError(OptimizelyError.invalidJSONVariable) optimizelyJSON = OptimizelyJSON.createEmpty() } - let ruleKey = decision?.experiment?.key - let reasonsToReport = reasons.toReport() + let ruleKey = flagDecision?.experiment?.key + let reasonsToReport = decisionReasons.toReport() sendDecisionNotification(userId: userId, attributes: attributes, decisionInfo: DecisionInfo(decisionType: .flag, - experiment: decision?.experiment, - variation: decision?.variation, + experiment: flagDecision?.experiment, + variation: flagDecision?.variation, feature: feature, - featureEnabled: enabled, + featureEnabled: flagEnabled, variableValues: variableMap, ruleKey: ruleKey, reasons: reasonsToReport, decisionEventDispatched: decisionEventDispatched)) - return OptimizelyDecision(variationKey: decision?.variation.key, - enabled: enabled, + return OptimizelyDecision(variationKey: flagDecision?.variation.key, + enabled: flagEnabled, variables: optimizelyJSON, ruleKey: ruleKey, flagKey: feature.key, @@ -158,30 +223,31 @@ extension OptimizelyClient { reasons: reasonsToReport) } - func decide(user: OptimizelyUserContext, - keys: [String], - options: [OptimizelyDecideOption]? = nil) -> [String: OptimizelyDecision] { - guard config != nil else { - logger.e(OptimizelyError.sdkNotReady) - return [:] - } - - guard keys.count > 0 else { return [:] } - - let allOptions = defaultDecideOptions + (options ?? []) - - var decisions = [String: OptimizelyDecision]() - - let enabledFlagsOnly = allOptions.contains(.enabledFlagsOnly) - keys.forEach { key in - let decision = decide(user: user, key: key, options: options) - if !enabledFlagsOnly || decision.enabled { - decisions[key] = decision - } - } - - return decisions - } + +// func decide(user: OptimizelyUserContext, +// keys: [String], +// options: [OptimizelyDecideOption]? = nil) -> [String: OptimizelyDecision] { +// guard config != nil else { +// logger.e(OptimizelyError.sdkNotReady) +// return [:] +// } +// +// guard keys.count > 0 else { return [:] } +// +// let allOptions = defaultDecideOptions + (options ?? []) +// +// var decisions = [String: OptimizelyDecision]() +// +// let enabledFlagsOnly = allOptions.contains(.enabledFlagsOnly) +// keys.forEach { key in +// let decision = decide(user: user, key: key, options: options) +// if !enabledFlagsOnly || decision.enabled { +// decisions[key] = decision +// } +// } +// +// return decisions +// } func decideAll(user: OptimizelyUserContext, options: [OptimizelyDecideOption]? = nil) -> [String: OptimizelyDecision] { From 6305e1a4858b5284dc04ae271fc79d1866477c99 Mon Sep 17 00:00:00 2001 From: Muzahidul Islam Date: Wed, 23 Oct 2024 22:25:24 +0600 Subject: [PATCH 06/14] WIP: cean up --- .../OptimizelyClient+Decide.swift | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/Sources/Optimizely+Decide/OptimizelyClient+Decide.swift b/Sources/Optimizely+Decide/OptimizelyClient+Decide.swift index 0b32b5d8d..36aed45b0 100644 --- a/Sources/Optimizely+Decide/OptimizelyClient+Decide.swift +++ b/Sources/Optimizely+Decide/OptimizelyClient+Decide.swift @@ -62,18 +62,23 @@ extension OptimizelyClient { key: String, options: [OptimizelyDecideOption]? = nil) -> OptimizelyDecision { - guard config != nil else { + guard let config = self.config else { return OptimizelyDecision.errorDecision(key: key, user: user, error: .sdkNotReady) } + + guard let _ = config.getFeatureFlag(key: key) else { + return OptimizelyDecision.errorDecision(key: key, user: user, error: .featureKeyInvalid(key)) + } let allOptions = defaultDecideOptions + (options ?? []) + /// Need to remove enable flags let decisionMap = decide(user: user, keys: [key], options: allOptions) return decisionMap[key] ?? OptimizelyDecision.errorDecision(key: key, user: user, error: .generic) } func decide(user: OptimizelyUserContext, - keys: [String], - options: [OptimizelyDecideOption]? = nil) -> [String: OptimizelyDecision] { + keys: [String], + options: [OptimizelyDecideOption]? = nil) -> [String: OptimizelyDecision] { guard let config = self.config else { logger.e(OptimizelyError.sdkNotReady) return [:] @@ -85,7 +90,7 @@ extension OptimizelyClient { var validKeys = [String]() var flagsWithoutForceDecision = [FeatureFlag]() - var flagDecisions = [String: FeatureDecision]() + var flagDecisions = [String : FeatureDecision]() var decisionReasonMap = [String : DecisionReasons]() let allOptions = options ?? [] @@ -95,14 +100,15 @@ extension OptimizelyClient { decisionMap[key] = OptimizelyDecision.errorDecision(key: key, user: user, error: .featureKeyInvalid(key)) continue } + validKeys.append(key) // check forced-decisions first - let decisionReasons = DecisionReasons(options: allOptions) let forcedDecisionResponse = decisionService.findValidatedForcedDecision(config: config, user: user, context: OptimizelyDecisionContext(flagKey: key)) + let decisionReasons = DecisionReasons(options: allOptions) decisionReasons.merge(forcedDecisionResponse.reasons) decisionReasonMap[key] = decisionReasons From 826d7b43754778ebf7e4d1bc4552de6bcfdf89d1 Mon Sep 17 00:00:00 2001 From: Muzahidul Islam Date: Wed, 23 Oct 2024 23:01:28 +0600 Subject: [PATCH 07/14] WIP: Decision listener test case fixed --- .../OptimizelyTests-Common/DecisionListenerTests.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Tests/OptimizelyTests-Common/DecisionListenerTests.swift b/Tests/OptimizelyTests-Common/DecisionListenerTests.swift index d5871a5a9..3837ff15a 100644 --- a/Tests/OptimizelyTests-Common/DecisionListenerTests.swift +++ b/Tests/OptimizelyTests-Common/DecisionListenerTests.swift @@ -1262,6 +1262,16 @@ class FakeDecisionService: DefaultDecisionService { let featureDecision = FeatureDecision(experiment: experiment, variation: tmpVariation, source: source) return DecisionResponse.responseNoReasons(result: featureDecision) } + + override func getVariationForFeatureExperiment(config: ProjectConfig, featureFlag: FeatureFlag, user: OptimizelyUserContext, userProfileTracker: UserProfileTracker? = nil, options: [OptimizelyDecideOption]? = nil) -> DecisionResponse { + guard let experiment = self.experiment, let tmpVariation = self.variation else { + return DecisionResponse.nilNoReasons() + } + + let featureDecision = FeatureDecision(experiment: experiment, variation: tmpVariation, source: source) + return DecisionResponse.responseNoReasons(result: featureDecision) + } + } fileprivate extension HandlerRegistryService { From 1ea00fb46e40baeeec65195c205a02ca78138e5c Mon Sep 17 00:00:00 2001 From: Muzahidul Islam Date: Thu, 24 Oct 2024 00:01:48 +0600 Subject: [PATCH 08/14] WIP: cleanup --- .../Implementation/DefaultDecisionService.swift | 12 +++++++++--- .../OptimizelyClient+Decide.swift | 15 +++++++-------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/Sources/Implementation/DefaultDecisionService.swift b/Sources/Implementation/DefaultDecisionService.swift index 45c702de6..f7ee1b827 100644 --- a/Sources/Implementation/DefaultDecisionService.swift +++ b/Sources/Implementation/DefaultDecisionService.swift @@ -218,12 +218,16 @@ class DefaultDecisionService: OPTDecisionService { user: OptimizelyUserContext, options: [OptimizelyDecideOption]? = nil) -> DecisionResponse { - guard let response = getVariationForFeatureList(config: config, featureFlags: [featureFlag], user: user, options: options).first else { - let reasons = DecisionReasons(options: options) + let response = getVariationForFeatureList(config: config, featureFlags: [featureFlag], user: user, options: options).first + + guard response?.result != nil else { + let reasons = response?.reasons ?? DecisionReasons(options: options) return DecisionResponse(result: nil, reasons: reasons) } - return response + return response! + + // // Evaluate in this order: // @@ -285,6 +289,8 @@ class DefaultDecisionService: OPTDecisionService { if let decision = decisionResponse.result { decisions.append(DecisionResponse(result: decision, reasons: reasons)) + } else { + decisions.append(DecisionResponse(result: nil, reasons: reasons)) } } diff --git a/Sources/Optimizely+Decide/OptimizelyClient+Decide.swift b/Sources/Optimizely+Decide/OptimizelyClient+Decide.swift index 36aed45b0..a95c831ac 100644 --- a/Sources/Optimizely+Decide/OptimizelyClient+Decide.swift +++ b/Sources/Optimizely+Decide/OptimizelyClient+Decide.swift @@ -123,17 +123,16 @@ extension OptimizelyClient { let decisionList = (decisionService as? DefaultDecisionService)?.getVariationForFeatureList(config: config, featureFlags: flagsWithoutForceDecision, user: user, options: allOptions) for index in 0.. Date: Tue, 29 Oct 2024 21:29:32 +0600 Subject: [PATCH 09/14] Fix: Default options issue fixed --- .../OptimizelyClient+Decide.swift | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Sources/Optimizely+Decide/OptimizelyClient+Decide.swift b/Sources/Optimizely+Decide/OptimizelyClient+Decide.swift index a95c831ac..fb9c8112c 100644 --- a/Sources/Optimizely+Decide/OptimizelyClient+Decide.swift +++ b/Sources/Optimizely+Decide/OptimizelyClient+Decide.swift @@ -70,15 +70,23 @@ extension OptimizelyClient { return OptimizelyDecision.errorDecision(key: key, user: user, error: .featureKeyInvalid(key)) } - let allOptions = defaultDecideOptions + (options ?? []) - /// Need to remove enable flags - let decisionMap = decide(user: user, keys: [key], options: allOptions) + var allOptions = defaultDecideOptions + (options ?? []) + allOptions.removeAll(where: { $0 == .enabledFlagsOnly }) + + let decisionMap = decide(user: user, keys: [key], options: allOptions, ignoreDefaultOptions: true) return decisionMap[key] ?? OptimizelyDecision.errorDecision(key: key, user: user, error: .generic) } func decide(user: OptimizelyUserContext, keys: [String], options: [OptimizelyDecideOption]? = nil) -> [String: OptimizelyDecision] { + return decide(user: user, keys: keys, options: options, ignoreDefaultOptions: false) + } + + func decide(user: OptimizelyUserContext, + keys: [String], + options: [OptimizelyDecideOption]? = nil, + ignoreDefaultOptions: Bool) -> [String: OptimizelyDecision] { guard let config = self.config else { logger.e(OptimizelyError.sdkNotReady) return [:] @@ -93,7 +101,7 @@ extension OptimizelyClient { var flagDecisions = [String : FeatureDecision]() var decisionReasonMap = [String : DecisionReasons]() - let allOptions = options ?? [] + let allOptions = ignoreDefaultOptions ? (options ?? []) : defaultDecideOptions + (options ?? []) for key in keys { guard let flags = config.getFeatureFlag(key: key) else { From 3e0f56051aeb0df38cd4d7ad1b1e82d1668b81e2 Mon Sep 17 00:00:00 2001 From: Muzahidul Islam Date: Tue, 29 Oct 2024 21:50:48 +0600 Subject: [PATCH 10/14] WIP: Remove boiler code --- .../OptimizelyClient+Decide.swift | 26 ------------------- 1 file changed, 26 deletions(-) diff --git a/Sources/Optimizely+Decide/OptimizelyClient+Decide.swift b/Sources/Optimizely+Decide/OptimizelyClient+Decide.swift index fb9c8112c..be4732a6e 100644 --- a/Sources/Optimizely+Decide/OptimizelyClient+Decide.swift +++ b/Sources/Optimizely+Decide/OptimizelyClient+Decide.swift @@ -236,32 +236,6 @@ extension OptimizelyClient { reasons: reasonsToReport) } - -// func decide(user: OptimizelyUserContext, -// keys: [String], -// options: [OptimizelyDecideOption]? = nil) -> [String: OptimizelyDecision] { -// guard config != nil else { -// logger.e(OptimizelyError.sdkNotReady) -// return [:] -// } -// -// guard keys.count > 0 else { return [:] } -// -// let allOptions = defaultDecideOptions + (options ?? []) -// -// var decisions = [String: OptimizelyDecision]() -// -// let enabledFlagsOnly = allOptions.contains(.enabledFlagsOnly) -// keys.forEach { key in -// let decision = decide(user: user, key: key, options: options) -// if !enabledFlagsOnly || decision.enabled { -// decisions[key] = decision -// } -// } -// -// return decisions -// } - func decideAll(user: OptimizelyUserContext, options: [OptimizelyDecideOption]? = nil) -> [String: OptimizelyDecision] { guard let config = self.config else { From e132c566b1607e4c9285a35f31be6f52830822c4 Mon Sep 17 00:00:00 2001 From: Muzahidul Islam Date: Tue, 29 Oct 2024 23:38:29 +0600 Subject: [PATCH 11/14] WIP: Batch load and save test case added --- .../DefaultDecisionService.swift | 45 ++++-------- .../DecisionServiceTests_Features.swift | 71 +++++++++++++++++++ 2 files changed, 85 insertions(+), 31 deletions(-) diff --git a/Sources/Implementation/DefaultDecisionService.swift b/Sources/Implementation/DefaultDecisionService.swift index f7ee1b827..9267a4f6e 100644 --- a/Sources/Implementation/DefaultDecisionService.swift +++ b/Sources/Implementation/DefaultDecisionService.swift @@ -111,8 +111,8 @@ class DefaultDecisionService: OPTDecisionService { } /// Load variation from tracker - if let _userId = userProfileTracker?.userId, - let variationId = getVariationIdFromProfile(userId: _userId, experimentId: experimentId), + if let profile = userProfileTracker?.userProfile, + let variationId = getVariationIdFromProfile(profile: profile, experimentId: experimentId), let variation = experiment.getVariation(id: variationId) { let info = LogMessage.gotVariationFromUserProfile(variation.key, experiment.key, userId) @@ -226,35 +226,6 @@ class DefaultDecisionService: OPTDecisionService { } return response! - - - -// // Evaluate in this order: -// -// // 1. Attempt to bucket user into experiment using feature flag. -// // Check if the feature flag is under an experiment and the the user is bucketed into one of these experiments -// var decisionResponse = getVariationForFeatureExperiment(config: config, -// featureFlag: featureFlag, -// user: user, -// userProfileTracker: nil, -// options: options) -// reasons.merge(decisionResponse.reasons) -// if let decision = decisionResponse.result { -// return DecisionResponse(result: decision, reasons: reasons) -// } -// -// // 2. Attempt to bucket user into rollout using the feature flag. -// // Check if the feature flag has rollout and the user is bucketed into one of it's rules -// decisionResponse = getVariationForFeatureRollout(config: config, -// featureFlag: featureFlag, -// user: user, -// options: options) -// reasons.merge(decisionResponse.reasons) -// if let decision = decisionResponse.result { -// return DecisionResponse(result: decision, reasons: reasons) -// } -// -// return DecisionResponse(result: nil, reasons: reasons) } func getVariationForFeatureList(config: ProjectConfig, @@ -544,6 +515,18 @@ extension DefaultDecisionService { } } + func getVariationIdFromProfile(profile: UserProfile?, + experimentId: String) -> String? { + if let _profile = profile, + let bucketMap = _profile[UserProfileKeys.kBucketMap] as? OPTUserProfileService.UPBucketMap, + let experimentMap = bucketMap[experimentId], + let variationId = experimentMap[UserProfileKeys.kVariationId] { + return variationId + } else { + return nil + } + } + func saveProfile(userId: String, experimentId: String, variationId: String) { diff --git a/Tests/OptimizelyTests-Common/DecisionServiceTests_Features.swift b/Tests/OptimizelyTests-Common/DecisionServiceTests_Features.swift index 2f243b029..4101578d9 100644 --- a/Tests/OptimizelyTests-Common/DecisionServiceTests_Features.swift +++ b/Tests/OptimizelyTests-Common/DecisionServiceTests_Features.swift @@ -289,6 +289,61 @@ extension DecisionServiceTests_Features { } +// MARK: - Test getVariationForFeatureList() + +extension DecisionServiceTests_Features { + func testGetVariationForFeatureListBatchUPSLoadAndSave() { + let mockProfileService = MocProfileService() + + let ups_service = DefaultDecisionService(userProfileService: mockProfileService) + + let flag1: FeatureFlag = try! OTUtils.model( + from:[ + "id": "553339214", + "key": "house", + "experimentIds": [kExperimentId], + "rolloutId": "", + "variables": [] + ] + ) + + let flag2: FeatureFlag = try! OTUtils.model( + from:[ + "id": "553339215", + "key": "house", + "experimentIds": [kExperimentId], + "rolloutId": "", + "variables": [] + ] + ) + + let flag3: FeatureFlag = try! OTUtils.model( + from:[ + "id": "553339216", + "key": "house", + "experimentIds": [kExperimentId], + "rolloutId": "", + "variables": [] + ] + ) + + let pair = ups_service.getVariationForFeatureList( + config: config, + featureFlags: [flag1, flag2, flag3], + user: optimizely.createUserContext(userId: kUserId, + attributes: kAttributesCountryMatch) + ) + + + XCTAssertEqual(mockProfileService.lookupCount, 1) + XCTAssertEqual(mockProfileService.saveCount, 1) + XCTAssertEqual(pair.count, 3) + XCTAssert(pair[0].result?.experiment?.key == kExperimentKey) + XCTAssert(pair[0].result?.variation.key == kVariationKeyD) + XCTAssert(pair[0].result?.source == Constants.DecisionSource.featureTest.rawValue) + } +} + // MARK: - Test getVariationForFeatureRollout() extension DecisionServiceTests_Features { @@ -466,3 +521,19 @@ extension DecisionServiceTests_Features { } } + +class MocProfileService: DefaultUserProfileService { + var lookupCount = 0 + var saveCount = 0 + + override func lookup(userId: String) -> DefaultUserProfileService.UPProfile? { + lookupCount += 1 + return super.lookup(userId: userId) + } + + override func save(userProfile: DefaultUserProfileService.UPProfile) { + super.save(userProfile: userProfile) + saveCount += 1 + } + +} From 33c176e02bb0b8977043b1097662710c10ca1e21 Mon Sep 17 00:00:00 2001 From: Muzahidul Islam Date: Wed, 30 Oct 2024 16:18:50 +0600 Subject: [PATCH 12/14] WIP: Rollout decision seperate from experiemnt decision --- .../DefaultDecisionService.swift | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/Sources/Implementation/DefaultDecisionService.swift b/Sources/Implementation/DefaultDecisionService.swift index 9267a4f6e..b2ee69199 100644 --- a/Sources/Implementation/DefaultDecisionService.swift +++ b/Sources/Implementation/DefaultDecisionService.swift @@ -254,15 +254,18 @@ class DefaultDecisionService: OPTDecisionService { continue } - decisionResponse = getVariationForFeatureRollout(config: config, featureFlag: featureFlag, user: user) - - reasons.merge(decisionResponse.reasons) - - if let decision = decisionResponse.result { - decisions.append(DecisionResponse(result: decision, reasons: reasons)) - } else { - decisions.append(DecisionResponse(result: nil, reasons: reasons)) + if ignoreUPS { + decisionResponse = getVariationForFeatureRollout(config: config, featureFlag: featureFlag, user: user) + + reasons.merge(decisionResponse.reasons) + + if let decision = decisionResponse.result { + decisions.append(DecisionResponse(result: decision, reasons: reasons)) + } else { + decisions.append(DecisionResponse(result: nil, reasons: reasons)) + } } + } // save profile From 58c5c611a06895565a5369e3ca9494aecb13e030 Mon Sep 17 00:00:00 2001 From: Muzahidul Islam Date: Wed, 30 Oct 2024 16:33:37 +0600 Subject: [PATCH 13/14] WIP: Rollout decision added --- .../DefaultDecisionService.swift | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/Sources/Implementation/DefaultDecisionService.swift b/Sources/Implementation/DefaultDecisionService.swift index b2ee69199..9267a4f6e 100644 --- a/Sources/Implementation/DefaultDecisionService.swift +++ b/Sources/Implementation/DefaultDecisionService.swift @@ -254,18 +254,15 @@ class DefaultDecisionService: OPTDecisionService { continue } - if ignoreUPS { - decisionResponse = getVariationForFeatureRollout(config: config, featureFlag: featureFlag, user: user) - - reasons.merge(decisionResponse.reasons) - - if let decision = decisionResponse.result { - decisions.append(DecisionResponse(result: decision, reasons: reasons)) - } else { - decisions.append(DecisionResponse(result: nil, reasons: reasons)) - } - } + decisionResponse = getVariationForFeatureRollout(config: config, featureFlag: featureFlag, user: user) + reasons.merge(decisionResponse.reasons) + + if let decision = decisionResponse.result { + decisions.append(DecisionResponse(result: decision, reasons: reasons)) + } else { + decisions.append(DecisionResponse(result: nil, reasons: reasons)) + } } // save profile From 10fbae7ed03480fe8161c36eac2123fde6e9d92d Mon Sep 17 00:00:00 2001 From: Muzahidul Islam <129880873+muzahidul-opti@users.noreply.github.com> Date: Thu, 7 Nov 2024 19:03:26 +0600 Subject: [PATCH 14/14] Update UserProfileTracker.swift --- Sources/Implementation/UserProfileTracker.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Implementation/UserProfileTracker.swift b/Sources/Implementation/UserProfileTracker.swift index c8dda38b4..e632418ff 100644 --- a/Sources/Implementation/UserProfileTracker.swift +++ b/Sources/Implementation/UserProfileTracker.swift @@ -1,5 +1,5 @@ // -// Copyright 2022, Optimizely, Inc. and contributors +// Copyright 2024, 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.