diff --git a/OptimizelySwiftSDK.xcodeproj/project.pbxproj b/OptimizelySwiftSDK.xcodeproj/project.pbxproj index a442597c..4b6e1c17 100644 --- a/OptimizelySwiftSDK.xcodeproj/project.pbxproj +++ b/OptimizelySwiftSDK.xcodeproj/project.pbxproj @@ -2012,6 +2012,23 @@ 980CC9172D833F2800E07D24 /* ExperimentCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 980CC9072D833F2800E07D24 /* ExperimentCore.swift */; }; 98137C552A41E86F004896EB /* OptimizelyClientTests_Init_Async_Await.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98137C542A41E86F004896EB /* OptimizelyClientTests_Init_Async_Await.swift */; }; 98137C572A42BA0F004896EB /* OptimizelyUserContextTests_ODP_Aync_Await.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98137C562A42BA0F004896EB /* OptimizelyUserContextTests_ODP_Aync_Await.swift */; }; + 98261A182ED89A8500F7230A /* CmabConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98261A172ED89A8500F7230A /* CmabConfig.swift */; }; + 98261A192ED89A8500F7230A /* CmabConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98261A172ED89A8500F7230A /* CmabConfig.swift */; }; + 98261A1A2ED89A8500F7230A /* CmabConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98261A172ED89A8500F7230A /* CmabConfig.swift */; }; + 98261A1B2ED89A8500F7230A /* CmabConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98261A172ED89A8500F7230A /* CmabConfig.swift */; }; + 98261A1C2ED89A8500F7230A /* CmabConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98261A172ED89A8500F7230A /* CmabConfig.swift */; }; + 98261A1D2ED89A8500F7230A /* CmabConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98261A172ED89A8500F7230A /* CmabConfig.swift */; }; + 98261A1E2ED89A8500F7230A /* CmabConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98261A172ED89A8500F7230A /* CmabConfig.swift */; }; + 98261A1F2ED89A8500F7230A /* CmabConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98261A172ED89A8500F7230A /* CmabConfig.swift */; }; + 98261A202ED89A8500F7230A /* CmabConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98261A172ED89A8500F7230A /* CmabConfig.swift */; }; + 98261A212ED89A8500F7230A /* CmabConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98261A172ED89A8500F7230A /* CmabConfig.swift */; }; + 98261A222ED89A8500F7230A /* CmabConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98261A172ED89A8500F7230A /* CmabConfig.swift */; }; + 98261A232ED89A8500F7230A /* CmabConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98261A172ED89A8500F7230A /* CmabConfig.swift */; }; + 98261A242ED89A8500F7230A /* CmabConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98261A172ED89A8500F7230A /* CmabConfig.swift */; }; + 98261A252ED89A8500F7230A /* CmabConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98261A172ED89A8500F7230A /* CmabConfig.swift */; }; + 98261A262ED89A8500F7230A /* CmabConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98261A172ED89A8500F7230A /* CmabConfig.swift */; }; + 98261A272ED89A8500F7230A /* CmabConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98261A172ED89A8500F7230A /* CmabConfig.swift */; }; + 98261A472EDDC35900F7230A /* OptimizelyClientTests_Cmab_Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98261A462EDDC35900F7230A /* OptimizelyClientTests_Cmab_Config.swift */; }; 982C071F2D8C82AE0068B1FF /* HoldoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 982C071E2D8C82AE0068B1FF /* HoldoutTests.swift */; }; 982C07202D8C82AE0068B1FF /* HoldoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 982C071E2D8C82AE0068B1FF /* HoldoutTests.swift */; }; 9841590F2E13013E0042C01E /* OptimizelyUserContextTests_Decide_Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9841590E2E13013E0042C01E /* OptimizelyUserContextTests_Decide_Async.swift */; }; @@ -2574,6 +2591,8 @@ 980CC9072D833F2800E07D24 /* ExperimentCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperimentCore.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 = ""; }; + 98261A172ED89A8500F7230A /* CmabConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmabConfig.swift; sourceTree = ""; }; + 98261A462EDDC35900F7230A /* OptimizelyClientTests_Cmab_Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyClientTests_Cmab_Config.swift; sourceTree = ""; }; 982C071E2D8C82AE0068B1FF /* HoldoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HoldoutTests.swift; sourceTree = ""; }; 9841590E2E13013E0042C01E /* OptimizelyUserContextTests_Decide_Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyUserContextTests_Decide_Async.swift; sourceTree = ""; }; 984159112E141B640042C01E /* OptimizelyUserContextTests_Decide_CMAB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyUserContextTests_Decide_CMAB.swift; sourceTree = ""; }; @@ -3227,6 +3246,7 @@ 6E7519BE22C5211100B2B157 /* OptimizelyClientTests_Invalid.swift */, 6E593FB425BB9C5500EC72BC /* OptimizelyClientTests_Decide.swift */, 84518B1E287665020023F104 /* OptimizelyClientTests_ODP.swift */, + 98261A462EDDC35900F7230A /* OptimizelyClientTests_Cmab_Config.swift */, 6E7519BA22C5211100B2B157 /* OptimizelyClientTests_Evaluation.swift */, 6E7519BB22C5211100B2B157 /* OptimizelyClientTests_DatafileHandler.swift */, 6E7519C022C5211100B2B157 /* OptimizelyClientTests_Variables.swift */, @@ -3304,6 +3324,7 @@ children = ( 98F28A402E02DD6D00A86546 /* CmabClient.swift */, 98F28A552E0451CC00A86546 /* CmabService.swift */, + 98261A172ED89A8500F7230A /* CmabConfig.swift */, ); path = CMAB; sourceTree = ""; @@ -4332,6 +4353,7 @@ 848617CF2863DC2700B7F41B /* OdpSegmentManager.swift in Sources */, 6E14CD952423F9A700010234 /* Group.swift in Sources */, 984FE5142CC8AA88004F6F41 /* UserProfileTracker.swift in Sources */, + 98261A272ED89A8500F7230A /* CmabConfig.swift in Sources */, 84E2E96828540B5E001114AB /* OptimizelySdkSettings.swift in Sources */, 6E14CD9A2423F9C300010234 /* DataStoreQueueStack.swift in Sources */, 6E14CD732423F96F00010234 /* OptimizelyResult.swift in Sources */, @@ -4462,6 +4484,7 @@ 6E424D0226324B620081004A /* Audience.swift in Sources */, 989428B62DBFA431008BA1C8 /* MockBucketer.swift in Sources */, 6E424D0326324B620081004A /* AttributeValue.swift in Sources */, + 98261A1F2ED89A8500F7230A /* CmabConfig.swift in Sources */, 84E2E9482852A378001114AB /* VuidManager.swift in Sources */, 6E424D0426324B620081004A /* ConditionLeaf.swift in Sources */, 980CC90E2D833F2800E07D24 /* ExperimentCore.swift in Sources */, @@ -4578,6 +4601,7 @@ 6E75190122C520D500B2B157 /* Attribute.swift in Sources */, 848617DB2863E21200B7F41B /* OdpSegmentApiManager.swift in Sources */, 6E7516B322C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, + 98261A182ED89A8500F7230A /* CmabConfig.swift in Sources */, 84E2E9432852A378001114AB /* VuidManager.swift in Sources */, 6E75183522C520D400B2B157 /* EventForDispatch.swift in Sources */, 6E7517D522C520D400B2B157 /* DefaultNotificationCenter.swift in Sources */, @@ -4659,6 +4683,7 @@ 848617D42863DC2700B7F41B /* OdpSegmentManager.swift in Sources */, 6E75184822C520D400B2B157 /* Event.swift in Sources */, 984FE5172CC8AA88004F6F41 /* UserProfileTracker.swift in Sources */, + 98261A262ED89A8500F7230A /* CmabConfig.swift in Sources */, 84E2E96D28540B5E001114AB /* OptimizelySdkSettings.swift in Sources */, 6E75170E22C520D400B2B157 /* OptimizelyClient.swift in Sources */, 6E75177A22C520D400B2B157 /* SDKVersion.swift in Sources */, @@ -4799,6 +4824,7 @@ 6E75180822C520D400B2B157 /* DataStoreFile.swift in Sources */, 6E7518EC22C520D400B2B157 /* ConditionHolder.swift in Sources */, 6E7516AA22C520D400B2B157 /* DefaultLogger.swift in Sources */, + 98261A472EDDC35900F7230A /* OptimizelyClientTests_Cmab_Config.swift in Sources */, 84E2E97A2855875E001114AB /* OdpEventManager.swift in Sources */, 6E75186822C520D400B2B157 /* Rollout.swift in Sources */, 6E9B11E122C548A200C22D81 /* OptimizelyClientTests_ObjcOthers.m in Sources */, @@ -4809,6 +4835,7 @@ 6E75172222C520D400B2B157 /* OptimizelyResult.swift in Sources */, 6E7517E422C520D400B2B157 /* DefaultDecisionService.swift in Sources */, 6E9B11D722C548A200C22D81 /* OptimizelyErrorTests.swift in Sources */, + 98261A1A2ED89A8500F7230A /* CmabConfig.swift in Sources */, C78CAF8624485029009FE876 /* OptimizelyClientTests_OptimizelyJSON_Objc.m in Sources */, 6E75194C22C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, 6E652308278E688B00954EA1 /* LruCache.swift in Sources */, @@ -4975,6 +5002,7 @@ 6E75178522C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, 6E75171922C520D400B2B157 /* OptimizelyClient+ObjC.swift in Sources */, 6E75192B22C520D500B2B157 /* DataStoreQueueStack.swift in Sources */, + 98261A232ED89A8500F7230A /* CmabConfig.swift in Sources */, 6E7517A922C520D400B2B157 /* Array+Extension.swift in Sources */, 6E75186B22C520D400B2B157 /* Rollout.swift in Sources */, 6E75183B22C520D400B2B157 /* EventForDispatch.swift in Sources */, @@ -4998,6 +5026,7 @@ 84861808286CF33700B7F41B /* OdpEvent.swift in Sources */, 6E9B117422C5487100C22D81 /* DecisionServiceTests_Others.swift in Sources */, 6E9B116E22C5487100C22D81 /* LoggerTests.swift in Sources */, + 98261A1D2ED89A8500F7230A /* CmabConfig.swift in Sources */, 84F6BADE27FD011B004BE62A /* OptimizelyUserContextTests_ODP_Decide.swift in Sources */, 98F28A5C2E0451CC00A86546 /* CmabService.swift in Sources */, 6E75180D22C520D400B2B157 /* DataStoreFile.swift in Sources */, @@ -5158,6 +5187,7 @@ 6EF8DE1824BD1BB2008B9488 /* OptimizelyDecision.swift in Sources */, 6E7518C222C520D400B2B157 /* Variable.swift in Sources */, 6E75192E22C520D500B2B157 /* DataStoreQueueStack.swift in Sources */, + 98261A1C2ED89A8500F7230A /* CmabConfig.swift in Sources */, 6E75172822C520D400B2B157 /* OptimizelyResult.swift in Sources */, 6E75170422C520D400B2B157 /* OptimizelyLogLevel.swift in Sources */, 6E75187A22C520D400B2B157 /* Variation.swift in Sources */, @@ -5293,6 +5323,7 @@ 6E9B115A22C5486E00C22D81 /* DecisionServiceTests_Others.swift in Sources */, 6E9B115422C5486E00C22D81 /* LoggerTests.swift in Sources */, 6E7518DF22C520D400B2B157 /* ConditionLeaf.swift in Sources */, + 98261A1B2ED89A8500F7230A /* CmabConfig.swift in Sources */, 6E75172D22C520D400B2B157 /* Constants.swift in Sources */, 98F28A5A2E0451CC00A86546 /* CmabService.swift in Sources */, 6E75172122C520D400B2B157 /* OptimizelyResult.swift in Sources */, @@ -5549,6 +5580,7 @@ 6E9B11AE22C5489300C22D81 /* OTUtils.swift in Sources */, 98AC97F32DAE9685001405DD /* HoldoutConfigTests.swift in Sources */, 6E9B117A22C5488100C22D81 /* AttributeValueTests.swift in Sources */, + 98261A192ED89A8500F7230A /* CmabConfig.swift in Sources */, 6E994B3B25A3E6EA00999262 /* DecisionResponse.swift in Sources */, 84E2E97B2855875E001114AB /* OdpEventManager.swift in Sources */, 6E75175322C520D400B2B157 /* LogMessage.swift in Sources */, @@ -5614,6 +5646,7 @@ 84B4D75A27E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */, 6E7517DA22C520D400B2B157 /* DefaultNotificationCenter.swift in Sources */, 6E7517E622C520D400B2B157 /* DefaultDecisionService.swift in Sources */, + 98261A242ED89A8500F7230A /* CmabConfig.swift in Sources */, 984FE51F2CC8AA88004F6F41 /* UserProfileTracker.swift in Sources */, 98F28A5B2E0451CC00A86546 /* CmabService.swift in Sources */, 6E75171822C520D400B2B157 /* OptimizelyClient+ObjC.swift in Sources */, @@ -5723,6 +5756,7 @@ 84B4D75F27E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */, 6E7517DF22C520D400B2B157 /* DefaultNotificationCenter.swift in Sources */, 6E7517EB22C520D400B2B157 /* DefaultDecisionService.swift in Sources */, + 98261A212ED89A8500F7230A /* CmabConfig.swift in Sources */, 984FE51D2CC8AA88004F6F41 /* UserProfileTracker.swift in Sources */, 98F28A562E0451CC00A86546 /* CmabService.swift in Sources */, 6E75171D22C520D400B2B157 /* OptimizelyClient+ObjC.swift in Sources */, @@ -5833,6 +5867,7 @@ 6E75195422C520D500B2B157 /* OPTBucketer.swift in Sources */, 848617DA2863E21200B7F41B /* OdpSegmentApiManager.swift in Sources */, 6E75171E22C520D400B2B157 /* OptimizelyResult.swift in Sources */, + 98261A252ED89A8500F7230A /* CmabConfig.swift in Sources */, 84E2E9422852A378001114AB /* VuidManager.swift in Sources */, 6E75172A22C520D400B2B157 /* Constants.swift in Sources */, 6E7516A622C520D400B2B157 /* DefaultLogger.swift in Sources */, @@ -5914,6 +5949,7 @@ 848617CC2863DC2700B7F41B /* OdpSegmentManager.swift in Sources */, 6E75184222C520D400B2B157 /* Event.swift in Sources */, 984FE5122CC8AA88004F6F41 /* UserProfileTracker.swift in Sources */, + 98261A1E2ED89A8500F7230A /* CmabConfig.swift in Sources */, 84E2E96528540B5E001114AB /* OptimizelySdkSettings.swift in Sources */, 6E75170822C520D400B2B157 /* OptimizelyClient.swift in Sources */, 6E75177422C520D400B2B157 /* SDKVersion.swift in Sources */, @@ -6028,6 +6064,7 @@ 6E4544AD270E67C800F2CEBC /* NetworkReachability.swift in Sources */, 75C71A0D25E454460084187E /* OptimizelyUserContext+ObjC.swift in Sources */, 75C71A0E25E454460084187E /* DefaultLogger.swift in Sources */, + 98261A202ED89A8500F7230A /* CmabConfig.swift in Sources */, 75C71A0F25E454460084187E /* DefaultUserProfileService.swift in Sources */, 75C71A1025E454460084187E /* DefaultEventDispatcher.swift in Sources */, 75C71A1125E454460084187E /* OPTLogger.swift in Sources */, @@ -6144,6 +6181,7 @@ BD6485502491474500F30986 /* OPTBucketer.swift in Sources */, 848617DC2863E21200B7F41B /* OdpSegmentApiManager.swift in Sources */, BD6485512491474500F30986 /* OptimizelyResult.swift in Sources */, + 98261A222ED89A8500F7230A /* CmabConfig.swift in Sources */, 84E2E9442852A378001114AB /* VuidManager.swift in Sources */, BD6485522491474500F30986 /* Constants.swift in Sources */, BD6485532491474500F30986 /* DefaultLogger.swift in Sources */, diff --git a/Sources/CMAB/CmabClient.swift b/Sources/CMAB/CmabClient.swift index e94f941c..bb524082 100644 --- a/Sources/CMAB/CmabClient.swift +++ b/Sources/CMAB/CmabClient.swift @@ -54,14 +54,17 @@ class DefaultCmabClient: CmabClient { let maxWaitTime: TimeInterval let cmabQueue = DispatchQueue(label: "com.optimizley.cmab") let logger = OPTLoggerFactory.getLogger() + let predictionEndpoint: String init(session: URLSession = .shared, retryConfig: CmabRetryConfig = CmabRetryConfig(), - maxWaitTime: TimeInterval = 10.0 + maxWaitTime: TimeInterval = 10.0, + predictionEndpoint: String? = nil ) { self.session = session self.retryConfig = retryConfig self.maxWaitTime = maxWaitTime + self.predictionEndpoint = predictionEndpoint ?? CMAB_PREDICTION_END_POINT } func fetchDecision( @@ -71,9 +74,8 @@ class DefaultCmabClient: CmabClient { cmabUUID: String, completion: @escaping (Result) -> Void ) { - let urlString = "https://prediction.cmab.optimizely.com/predict/\(ruleId)" - guard let url = URL(string: urlString) else { - completion(.failure(CmabClientError.fetchFailed("Invalid URL"))) + guard let url = getUrl(ruleId: ruleId) else { + completion(.failure(CmabClientError.fetchFailed("Invalid CMAB prediction endpoint"))) return } let attrType = "custom_attribute" @@ -98,6 +100,15 @@ class DefaultCmabClient: CmabClient { ) } + func getUrl(ruleId: String) -> URL? { + let urlString = String(format: predictionEndpoint, ruleId) + guard let url = URL(string: urlString) else { + self.logger.e("Invalid CMAB endpoint") + return nil + } + return url + } + private func doFetchWithRetry( url: URL, requestBody: [String: Any], diff --git a/Sources/CMAB/CmabConfig.swift b/Sources/CMAB/CmabConfig.swift new file mode 100644 index 00000000..52d5eea2 --- /dev/null +++ b/Sources/CMAB/CmabConfig.swift @@ -0,0 +1,49 @@ +// +// 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 + +let DEFAULT_CMAB_CACHE_TIMEOUT = 30 * 60 // 30 minutes +let DEFAULT_CMAB_CACHE_SIZE = 100 +let CMAB_PREDICTION_END_POINT = "https://prediction.cmab.optimizely.com/predict/%@" + +/// Configuration for CMAB (Contextual Multi-Armed Bandit) service +public struct CmabConfig { + /// The maximum size of CMAB decision cache + let cacheSize: Int + /// The timeout in seconds of CMAB cache + let cacheTimeoutInSecs: Int + /// The CMAB prediction endpoint + var predictionEndpoint: String? + + /// - cmabCacheSize: The maximum size of cmab cache (optional. default = 100). + /// - cmabCacheTimeoutInSecs: The timeout in seconds of amb cache (optional. default = 30 * 60). + /// - predictionEndpoint: Set the CMAB prediction endpoint. default will be used if nil is set. + public init( + cacheSize: Int = 100, + cacheTimeoutInSecs: Int = 30 * 60, + predictionEndpoint: String? = nil + ) { + self.cacheSize = cacheSize + self.cacheTimeoutInSecs = cacheTimeoutInSecs + // Sanitize and validate endpoint + if let sanitized = predictionEndpoint?.trimmingCharacters(in: .whitespaces), !sanitized.isEmpty { + self.predictionEndpoint = sanitized + } else { + self.predictionEndpoint = CMAB_PREDICTION_END_POINT + } + } +} diff --git a/Sources/CMAB/CmabService.swift b/Sources/CMAB/CmabService.swift index 733bc0bf..536a4622 100644 --- a/Sources/CMAB/CmabService.swift +++ b/Sources/CMAB/CmabService.swift @@ -41,15 +41,12 @@ protocol CmabService { completion: @escaping CmabDecisionCompletionHandler) } -let DEFAULT_CMAB_CACHE_TIMEOUT = 30 * 60 // 30 minutes -let DEFAULT_CMAB_CACHE_SIZE = 100 - typealias CmabCache = LruCache class DefaultCmabService: CmabService { typealias UserAttributes = [String : Any?] - private let cmabClient: CmabClient + let cmabClient: CmabClient let cmabCache: CmabCache private let logger = OPTLoggerFactory.getLogger() @@ -201,11 +198,11 @@ extension DefaultCmabService { let cache = CmabCache(size: DEFAULT_CMAB_CACHE_SIZE, timeoutInSecs: DEFAULT_CMAB_CACHE_TIMEOUT) return DefaultCmabService(cmabClient: DefaultCmabClient(), cmabCache: cache) } - - static func createDefault(cacheSize: Int, cacheTimeout: Int) -> DefaultCmabService { + + static func createDefault(config: CmabConfig) -> DefaultCmabService { // if cache timeout is set to 0 or negative, use default timeout - let timeout = cacheTimeout <= 0 ? DEFAULT_CMAB_CACHE_TIMEOUT : cacheTimeout - let cache = CmabCache(size: cacheSize, timeoutInSecs: timeout) - return DefaultCmabService(cmabClient: DefaultCmabClient(), cmabCache: cache) + let timeout = config.cacheTimeoutInSecs <= 0 ? DEFAULT_CMAB_CACHE_TIMEOUT : config.cacheTimeoutInSecs + let cache = CmabCache(size: config.cacheSize, timeoutInSecs: timeout) + return DefaultCmabService(cmabClient: DefaultCmabClient(predictionEndpoint: config.predictionEndpoint), cmabCache: cache) } } diff --git a/Sources/Extensions/OptimizelyClient+Extension.swift b/Sources/Extensions/OptimizelyClient+Extension.swift index 80ae0552..bd3e5563 100644 --- a/Sources/Extensions/OptimizelyClient+Extension.swift +++ b/Sources/Extensions/OptimizelyClient+Extension.swift @@ -67,7 +67,8 @@ extension OptimizelyClient { periodicDownloadInterval: Int?, defaultLogLevel: OptimizelyLogLevel? = nil, defaultDecideOptions: [OptimizelyDecideOption]? = nil, - settings: OptimizelySdkSettings? = nil) { + settings: OptimizelySdkSettings? = nil, + cmabConfig: CmabConfig? = nil) { self.init(sdkKey: sdkKey, logger: logger, @@ -77,7 +78,8 @@ extension OptimizelyClient { odpManager: odpManager, defaultLogLevel: defaultLogLevel, defaultDecideOptions: defaultDecideOptions, - settings: settings) + settings: settings, + cmabConfig: cmabConfig) let interval = periodicDownloadInterval ?? 10 * 60 if interval > 0 { diff --git a/Sources/ODP/OptimizelySdkSettings.swift b/Sources/ODP/OptimizelySdkSettings.swift index 5887e372..9b6b45d8 100644 --- a/Sources/ODP/OptimizelySdkSettings.swift +++ b/Sources/ODP/OptimizelySdkSettings.swift @@ -25,10 +25,6 @@ public struct OptimizelySdkSettings { let timeoutForSegmentFetchInSecs: Int /// The timeout in seconds of odp event dispatch - OS default timeout will be used if this is set to zero. let timeoutForOdpEventInSecs: Int - /// The maximum size of cmab cache - let cmabCacheSize: Int - /// The timeout in seconds of cmab cache - let cmabCacheTimeoutInSecs: Int /// ODP features are disabled if this is set to true. let disableOdp: Bool /// VUID is enabled if this is set to true. @@ -41,8 +37,6 @@ public struct OptimizelySdkSettings { /// - segmentsCacheTimeoutInSecs: The timeout in seconds of audience segments cache (optional. default = 600). Set to zero to disable timeout. /// - timeoutForSegmentFetchInSecs: The timeout in seconds of odp segment fetch (optional. default = 10) - OS default timeout will be used if this is set to zero. /// - timeoutForOdpEventInSecs: The timeout in seconds of odp event dispatch (optional. default = 10) - OS default timeout will be used if this is set to zero. - /// - cmabCacheSize: The maximum size of cmab cache (optional. default = 100). - /// - cmabCacheTimeoutInSecs: The timeout in seconds of amb cache (optional. default = 30 * 60). /// - disableOdp: Set this flag to true (default = false) to disable ODP features /// - enableVuid: Set this flag to true (default = false) to enable vuid. /// - sdkName: Set this flag to override sdkName included in events @@ -51,24 +45,20 @@ public struct OptimizelySdkSettings { segmentsCacheTimeoutInSecs: Int = 600, timeoutForSegmentFetchInSecs: Int = 10, timeoutForOdpEventInSecs: Int = 10, - cmabCacheSize: Int = 100, - cmabCacheTimeoutInSecs: Int = 30 * 60, disableOdp: Bool = false, enableVuid: Bool = false, sdkName: String? = nil, sdkVersion: String? = nil) { self.segmentsCacheSize = segmentsCacheSize self.segmentsCacheTimeoutInSecs = segmentsCacheTimeoutInSecs - self.cmabCacheSize = cmabCacheSize - self.cmabCacheTimeoutInSecs = cmabCacheTimeoutInSecs self.timeoutForSegmentFetchInSecs = timeoutForSegmentFetchInSecs self.timeoutForOdpEventInSecs = timeoutForOdpEventInSecs self.disableOdp = disableOdp self.enableVuid = enableVuid - if let _sdkName = sdkName, _sdkName != "" { + if let _sdkName = sdkName, !_sdkName.isEmpty { Utils.swiftSdkClientName = _sdkName } - if let _sdkVersion = sdkVersion, _sdkVersion != "" { + if let _sdkVersion = sdkVersion, !_sdkVersion.isEmpty { Utils.sdkVersion = _sdkVersion } } diff --git a/Sources/Optimizely/OptimizelyClient.swift b/Sources/Optimizely/OptimizelyClient.swift index f3d4993f..7314b884 100644 --- a/Sources/Optimizely/OptimizelyClient.swift +++ b/Sources/Optimizely/OptimizelyClient.swift @@ -76,8 +76,9 @@ open class OptimizelyClient: NSObject { /// - userProfileService: custom UserProfileService (optional) /// - odpManager: custom OdpManager (optional) /// - defaultLogLevel: default log level (optional. default = .info) - /// - defaultDecisionOptions: default decision options (optional) + /// - defaultDecideOptions: default decision options (optional) /// - settings: SDK configuration (optional) + /// - cmabConfig: CMAB configuration (optional) public init(sdkKey: String, logger: OPTLogger? = nil, eventDispatcher: OPTEventDispatcher? = nil, @@ -86,7 +87,8 @@ open class OptimizelyClient: NSObject { odpManager: OdpManager? = nil, defaultLogLevel: OptimizelyLogLevel? = nil, defaultDecideOptions: [OptimizelyDecideOption]? = nil, - settings: OptimizelySdkSettings? = nil) { + settings: OptimizelySdkSettings? = nil, + cmabConfig: CmabConfig? = nil) { self.sdkKey = sdkKey self.sdkSettings = settings ?? OptimizelySdkSettings() @@ -107,9 +109,9 @@ open class OptimizelyClient: NSObject { let userProfileService = userProfileService ?? DefaultUserProfileService() let logger = logger ?? DefaultLogger() type(of: logger).logLevel = defaultLogLevel ?? .info - - let cmabService = DefaultCmabService.createDefault(cacheSize: self.sdkSettings.cmabCacheSize, cacheTimeout: self.sdkSettings.cmabCacheTimeoutInSecs) - + + let cmabService = DefaultCmabService.createDefault(config: cmabConfig ?? CmabConfig()) + self.registerServices(sdkKey: sdkKey, logger: logger, eventDispatcher: eventDispatcher ?? DefaultEventDispatcher.sharedInstance, diff --git a/Tests/OptimizelyTests-APIs/OptimizelyClientTests_Cmab_Config.swift b/Tests/OptimizelyTests-APIs/OptimizelyClientTests_Cmab_Config.swift new file mode 100644 index 00000000..767537e1 --- /dev/null +++ b/Tests/OptimizelyTests-APIs/OptimizelyClientTests_Cmab_Config.swift @@ -0,0 +1,77 @@ +// +// 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 +import XCTest + +class OptimizelyClientTests_Cmab_Config: XCTestCase { + + var optimizely: OptimizelyClient! + + override func setUp() { + super.setUp() + + let datafile = OTUtils.loadJSONDatafile("decide_audience_segments")! + optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey) + try! optimizely.start(datafile: datafile) + } + + override func tearDown() { + Utils.sdkVersion = OPTIMIZELYSDKVERSION + Utils.swiftSdkClientName = "swift-sdk" + } + + // MARK: - ODP configuration + + func test_config_default() { + let optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey) + let cmabService = ((optimizely.decisionService as! DefaultDecisionService).cmabService as! DefaultCmabService) + let cmabCache = cmabService.cmabCache + let cmabClient = cmabService.cmabClient as! DefaultCmabClient + XCTAssertEqual(100, cmabCache.maxSize) + XCTAssertEqual(30 * 60, cmabCache.timeoutInSecs) + XCTAssertEqual("https://prediction.cmab.optimizely.com/predict/%@", cmabClient.predictionEndpoint) + } + + func test_cmab_custom_config() { + var cmabConfig = CmabConfig(cacheSize: 50, cacheTimeoutInSecs: 120) + var optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey, cmabConfig: cmabConfig) + var cmabService = ((optimizely.decisionService as! DefaultDecisionService).cmabService as! DefaultCmabService) + var cmabCache = cmabService.cmabCache + var cmabClient = cmabService.cmabClient as! DefaultCmabClient + XCTAssertEqual(50, cmabCache.maxSize) + XCTAssertEqual(120, cmabCache.timeoutInSecs) + XCTAssertEqual("https://prediction.cmab.optimizely.com/predict/rule_123", cmabClient.getUrl(ruleId: "rule_123")?.absoluteString) + + cmabConfig = CmabConfig(cacheSize: 50, cacheTimeoutInSecs: -10, predictionEndpoint: "http://demo.cmab.com/%@/predict") + optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey, cmabConfig: cmabConfig) + cmabService = ((optimizely.decisionService as! DefaultDecisionService).cmabService as! DefaultCmabService) + cmabCache = cmabService.cmabCache + cmabClient = cmabService.cmabClient as! DefaultCmabClient + XCTAssertEqual(50, cmabCache.maxSize) + XCTAssertEqual(1800, cmabCache.timeoutInSecs) + XCTAssertEqual("http://demo.cmab.com/rule_1234/predict", cmabClient.getUrl(ruleId: "rule_1234")?.absoluteString) + + cmabConfig = CmabConfig(predictionEndpoint: "http://fowardslash.com/predict/%@/v1/") + optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey, cmabConfig: cmabConfig) + cmabService = ((optimizely.decisionService as! DefaultDecisionService).cmabService as! DefaultCmabService) + cmabCache = cmabService.cmabCache + cmabClient = cmabService.cmabClient as! DefaultCmabClient + XCTAssertEqual(100, cmabCache.maxSize) + XCTAssertEqual(1800, cmabCache.timeoutInSecs) + XCTAssertEqual("http://fowardslash.com/predict/rule-12345/v1/", cmabClient.getUrl(ruleId: "rule-12345")?.absoluteString) + } +} diff --git a/Tests/OptimizelyTests-APIs/OptimizelyClientTests_ODP.swift b/Tests/OptimizelyTests-APIs/OptimizelyClientTests_ODP.swift index 4ee7b054..c517abd5 100644 --- a/Tests/OptimizelyTests-APIs/OptimizelyClientTests_ODP.swift +++ b/Tests/OptimizelyTests-APIs/OptimizelyClientTests_ODP.swift @@ -37,9 +37,12 @@ class OptimizelyClientTests_ODP: XCTestCase { func testSdkSettings_default() { let optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey) - let cmabCache = ((optimizely.decisionService as! DefaultDecisionService).cmabService as! DefaultCmabService).cmabCache + let cmabService = ((optimizely.decisionService as! DefaultDecisionService).cmabService as! DefaultCmabService) + let cmabCache = cmabService.cmabCache + let cmabClient = cmabService.cmabClient as! DefaultCmabClient XCTAssertEqual(100, cmabCache.maxSize) XCTAssertEqual(30 * 60, cmabCache.timeoutInSecs) + XCTAssertEqual("https://prediction.cmab.optimizely.com/predict/%@", cmabClient.predictionEndpoint) XCTAssertEqual(100, optimizely.odpManager.segmentManager?.segmentsCache.maxSize) XCTAssertEqual(600, optimizely.odpManager.segmentManager?.segmentsCache.timeoutInSecs) XCTAssertEqual(10, optimizely.odpManager.segmentManager?.apiMgr.resourceTimeoutInSecs) @@ -48,30 +51,16 @@ class OptimizelyClientTests_ODP: XCTestCase { } func testSdkSettings_custom() { - var sdkSettings = OptimizelySdkSettings(segmentsCacheSize: 12, - segmentsCacheTimeoutInSecs: 345, - cmabCacheSize: 50, - cmabCacheTimeoutInSecs: 120) + var sdkSettings = OptimizelySdkSettings(segmentsCacheSize: 12, segmentsCacheTimeoutInSecs: 345) var optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey, settings: sdkSettings) - var cmabCache = ((optimizely.decisionService as! DefaultDecisionService).cmabService as! DefaultCmabService).cmabCache - XCTAssertEqual(50, cmabCache.maxSize) - XCTAssertEqual(120, cmabCache.timeoutInSecs) XCTAssertEqual(12, optimizely.odpManager.segmentManager?.segmentsCache.maxSize) XCTAssertEqual(345, optimizely.odpManager.segmentManager?.segmentsCache.timeoutInSecs) - sdkSettings = OptimizelySdkSettings(timeoutForSegmentFetchInSecs: 34, - timeoutForOdpEventInSecs: 45) + sdkSettings = OptimizelySdkSettings(timeoutForSegmentFetchInSecs: 34, timeoutForOdpEventInSecs: 45) optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey, settings: sdkSettings) XCTAssertEqual(34, optimizely.odpManager.segmentManager?.apiMgr.resourceTimeoutInSecs) XCTAssertEqual(45, optimizely.odpManager.eventManager?.apiMgr.resourceTimeoutInSecs) - sdkSettings = OptimizelySdkSettings(cmabCacheSize: 50, cmabCacheTimeoutInSecs: -10) - - optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey, settings: sdkSettings) - cmabCache = ((optimizely.decisionService as! DefaultDecisionService).cmabService as! DefaultCmabService).cmabCache - XCTAssertEqual(50, cmabCache.maxSize) - XCTAssertEqual(1800, cmabCache.timeoutInSecs) - sdkSettings = OptimizelySdkSettings(disableOdp: true) optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey, settings: sdkSettings) XCTAssertEqual(false, optimizely.odpManager.enabled) diff --git a/Tests/OptimizelyTests-Common/DecisionListenerTests.swift b/Tests/OptimizelyTests-Common/DecisionListenerTests.swift index 30c85d42..32451ccd 100644 --- a/Tests/OptimizelyTests-Common/DecisionListenerTests.swift +++ b/Tests/OptimizelyTests-Common/DecisionListenerTests.swift @@ -1219,7 +1219,8 @@ class FakeManager: OptimizelyClient { odpManager: OdpManager? = nil, defaultLogLevel: OptimizelyLogLevel? = nil, defaultDecideOptions: [OptimizelyDecideOption]? = nil, - settings: OptimizelySdkSettings? = nil) { + settings: OptimizelySdkSettings? = nil, + cmabConfig: CmabConfig? = nil) { // clear shared handlers HandlerRegistryService.shared.removeAll()