From 3e303f2e23fa3d79cf88dfa452638eeb4cea983e Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Wed, 24 Dec 2025 19:06:14 +0600 Subject: [PATCH 1/7] Update cmab dependency --- android/build.gradle | 2 +- example/ios/Runner.xcodeproj/project.pbxproj | 14 ++++++++------ .../xcshareddata/xcschemes/Runner.xcscheme | 5 ++++- ios/optimizely_flutter_sdk.podspec | 2 +- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 7d1edec..b4d9ca6 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -69,7 +69,7 @@ dependencies { implementation 'org.slf4j:slf4j-api:2.0.7' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.1.0" - implementation "com.optimizely.ab:android-sdk:5.0.1" + implementation "com.optimizely.ab:android-sdk:5.1.0" implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.2' implementation ('com.google.guava:guava:19.0') { exclude group:'com.google.guava', module:'listenablefuture' diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 815ca79..56af059 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -68,7 +68,6 @@ 8E60C66DA76D705E5A9DCACA /* Pods-Runner.release.xcconfig */, 3D86A8B550CB0FBA7A8F2A03 /* Pods-Runner.profile.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; @@ -156,7 +155,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -200,10 +199,12 @@ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -231,6 +232,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -356,7 +358,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 6CHVYYTX7N; + DEVELOPMENT_TEAM = BDMC9C2X5M; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10.0; @@ -486,7 +488,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 6CHVYYTX7N; + DEVELOPMENT_TEAM = BDMC9C2X5M; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10.0; @@ -510,7 +512,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 6CHVYYTX7N; + DEVELOPMENT_TEAM = BDMC9C2X5M; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10.0; diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index c87d15a..9c12df5 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ diff --git a/ios/optimizely_flutter_sdk.podspec b/ios/optimizely_flutter_sdk.podspec index 2aa6953..cc0069d 100644 --- a/ios/optimizely_flutter_sdk.podspec +++ b/ios/optimizely_flutter_sdk.podspec @@ -13,7 +13,7 @@ Pod::Spec.new do |s| s.source = { :path => '.' } s.source_files = 'Classes/**/*' s.dependency 'Flutter' - s.dependency 'OptimizelySwiftSDK', '5.1.1' + s.dependency 'OptimizelySwiftSDK', '5.2.0' s.platform = :ios, '10.0' # Flutter.framework does not contain a i386 slice. s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } From bbc8df16e89e1f1e92f37a1eb53717a65a668080 Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Wed, 24 Dec 2025 22:02:37 +0600 Subject: [PATCH 2/7] feat: add CMAB support with decideAsync API and cache control options Add support for Contextual Multi-Armed Bandit (CMAB) experimentation including CmabConfig for initialization, decideAsync methods for async decisions, and CMAB-specific cache options (ignoreCmabCache, resetCmabCache, invalidateUserCmabCache). --- ios/Classes/HelperClasses/Constants.swift | 12 ++- ios/Classes/HelperClasses/Utils.swift | 6 ++ .../SwiftOptimizelyFlutterSdkPlugin.swift | 94 +++++++++++++++++-- lib/optimizely_flutter_sdk.dart | 11 ++- lib/src/data_objects/cmab_config.dart | 33 +++++++ lib/src/optimizely_client_wrapper.dart | 14 +++ .../user_context/optimizely_user_context.dart | 61 +++++++++++- lib/src/utils/constants.dart | 7 ++ lib/src/utils/utils.dart | 3 + 9 files changed, 229 insertions(+), 12 deletions(-) create mode 100644 lib/src/data_objects/cmab_config.dart diff --git a/ios/Classes/HelperClasses/Constants.swift b/ios/Classes/HelperClasses/Constants.swift index f80735c..0b86175 100644 --- a/ios/Classes/HelperClasses/Constants.swift +++ b/ios/Classes/HelperClasses/Constants.swift @@ -29,6 +29,7 @@ struct API { static let setAttributes = "setAttributes" static let trackEvent = "trackEvent" static let decide = "decide" + static let decideAsync = "decideAsync" static let setForcedDecision = "setForcedDecision" static let getForcedDecision = "getForcedDecision" static let removeForcedDecision = "removeForcedDecision" @@ -62,6 +63,9 @@ struct DecideOption { static let ignoreUserProfileService = "ignoreUserProfileService" static let includeReasons = "includeReasons" static let excludeVariables = "excludeVariables" + static let ignoreCmabCache = "ignoreCmabCache" + static let resetCmabCache = "resetCmabCache" + static let invalidateUserCmabCache = "invalidateUserCmabCache" } struct SegmentOption { @@ -115,7 +119,13 @@ struct RequestParameterKey { static let timeoutForOdpEventInSecs = "timeoutForOdpEventInSecs" static let disableOdp = "disableOdp" static let enableVuid = "enableVuid" - static let sdkVersion = "sdkVersion"; + static let sdkVersion = "sdkVersion" + + // CMAB Config + static let cmabConfig = "cmabConfig" + static let cmabCacheSize = "cmabCacheSize" + static let cmabCacheTimeoutInSecs = "cmabCacheTimeoutInSecs" + static let cmabPredictionEndpoint = "cmabPredictionEndpoint" } struct ResponseKey { diff --git a/ios/Classes/HelperClasses/Utils.swift b/ios/Classes/HelperClasses/Utils.swift index 7636b68..675990d 100644 --- a/ios/Classes/HelperClasses/Utils.swift +++ b/ios/Classes/HelperClasses/Utils.swift @@ -169,6 +169,12 @@ public class Utils: NSObject { convertedOptions.append(OptimizelyDecideOption.excludeVariables) case DecideOption.includeReasons: convertedOptions.append(OptimizelyDecideOption.includeReasons) + case DecideOption.ignoreCmabCache: + convertedOptions.append(OptimizelyDecideOption.ignoreCmabCache) + case DecideOption.resetCmabCache: + convertedOptions.append(OptimizelyDecideOption.resetCmabCache) + case DecideOption.invalidateUserCmabCache: + convertedOptions.append(OptimizelyDecideOption.invalidateUserCmabCache) default: break } } diff --git a/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift b/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift index be81576..cf19a18 100644 --- a/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift +++ b/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift @@ -70,6 +70,7 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin { case API.setAttributes: setAttributes(call, result: result) case API.trackEvent: trackEvent(call, result: result) case API.decide: decide(call, result: result) + case API.decideAsync: decideAsync(call, result: result) case API.setForcedDecision: setForcedDecision(call, result: result) case API.getForcedDecision: getForcedDecision(call, result: result) case API.removeForcedDecision: removeForcedDecision(call, result: result) @@ -152,7 +153,31 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin { } } let optimizelySdkSettings = OptimizelySdkSettings(segmentsCacheSize: segmentsCacheSize, segmentsCacheTimeoutInSecs: segmentsCacheTimeoutInSecs, timeoutForSegmentFetchInSecs: timeoutForSegmentFetchInSecs, timeoutForOdpEventInSecs: timeoutForOdpEventInSecs, disableOdp: disableOdp, enableVuid: enableVuid, sdkName: sdkName, sdkVersion: sdkVersion) - + + // CMAB Config + var cmabConfig: CmabConfig? + if let cmabConfigDict = parameters[RequestParameterKey.cmabConfig] as? Dictionary { + var cacheSize = 100 + var cacheTimeoutInSecs = 1800 + var predictionEndpoint: String? = nil + + if let size = cmabConfigDict[RequestParameterKey.cmabCacheSize] as? Int { + cacheSize = size + } + if let timeout = cmabConfigDict[RequestParameterKey.cmabCacheTimeoutInSecs] as? Int { + cacheTimeoutInSecs = timeout + } + if let endpoint = cmabConfigDict[RequestParameterKey.cmabPredictionEndpoint] as? String { + predictionEndpoint = endpoint + } + + cmabConfig = CmabConfig( + cacheSize: cacheSize, + cacheTimeoutInSecs: cacheTimeoutInSecs, + predictionEndpoint: predictionEndpoint + ) + } + // Datafile Download Interval var datafilePeriodicDownloadInterval = 10 * 60 // seconds @@ -178,14 +203,15 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin { // Creating new instance let optimizelyInstance = OptimizelyClient( - sdkKey:sdkKey, + sdkKey:sdkKey, logger:logger, - eventDispatcher: eventDispatcher, - datafileHandler: datafileHandler, - periodicDownloadInterval: datafilePeriodicDownloadInterval, + eventDispatcher: eventDispatcher, + datafileHandler: datafileHandler, + periodicDownloadInterval: datafilePeriodicDownloadInterval, defaultLogLevel: defaultLogLevel, - defaultDecideOptions: defaultDecideOptions, - settings: optimizelySdkSettings) + defaultDecideOptions: defaultDecideOptions, + settings: optimizelySdkSettings, + cmabConfig: cmabConfig) optimizelyInstance.start{ [weak self] res in switch res { @@ -580,7 +606,59 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin { result(self.createResponse(success: true, result: resultMap)) } - + + /// Asynchronously returns a key-map of decision results for flag keys and a user context. + /// This method supports CMAB (Contextual Multi-Armed Bandit) experiments. + func decideAsync(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let (parameters, userContext) = getParametersAndUserContext( + arguments: call.arguments, result: result) else { + return + } + + var decideKeys: [String]? + if let keys = parameters[RequestParameterKey.decideKeys] as? [String] { + decideKeys = keys + } + + var decideOptions: [String]? + if let options = parameters[RequestParameterKey.decideOptions] as? [String] { + decideOptions = options + } + + let options = Utils.getDecideOptions(options: decideOptions) + + // Call appropriate async method based on keys + if let keys = decideKeys, keys.count == 1 { + // Single key async + userContext.decideAsync(key: keys[0], options: options) { [weak self] decision in + guard let self = self else { return } + var resultMap = [String: Any]() + resultMap[keys[0]] = Utils.convertDecisionToDictionary(decision: decision) + result(self.createResponse(success: true, result: resultMap)) + } + } else if let keys = decideKeys, keys.count > 1 { + // Multiple keys async + userContext.decideAsync(keys: keys, options: options) { [weak self] decisions in + guard let self = self else { return } + var resultMap = [String: Any]() + for (key, decision) in decisions { + resultMap[key] = Utils.convertDecisionToDictionary(decision: decision) + } + result(self.createResponse(success: true, result: resultMap)) + } + } else { + // All flags async + userContext.decideAllAsync(options: options) { [weak self] decisions in + guard let self = self else { return } + var resultMap = [String: Any]() + for (key, decision) in decisions { + resultMap[key] = Utils.convertDecisionToDictionary(decision: decision) + } + result(self.createResponse(success: true, result: resultMap)) + } + } + } + /// Sets the forced decision for a given decision context. func setForcedDecision(_ call: FlutterMethodCall, result: @escaping FlutterResult) { guard let (parameters, userContext) = getParametersAndUserContext(arguments: call.arguments, result: result) else { diff --git a/lib/optimizely_flutter_sdk.dart b/lib/optimizely_flutter_sdk.dart index cc7d11d..f05e45a 100644 --- a/lib/optimizely_flutter_sdk.dart +++ b/lib/optimizely_flutter_sdk.dart @@ -23,6 +23,7 @@ import 'package:optimizely_flutter_sdk/src/data_objects/datafile_options.dart'; import 'package:optimizely_flutter_sdk/src/data_objects/event_options.dart'; import 'package:optimizely_flutter_sdk/src/data_objects/get_vuid_response.dart'; import 'package:optimizely_flutter_sdk/src/data_objects/sdk_settings.dart'; +import 'package:optimizely_flutter_sdk/src/data_objects/cmab_config.dart'; import 'package:optimizely_flutter_sdk/src/data_objects/get_variation_response.dart'; import 'package:optimizely_flutter_sdk/src/data_objects/optimizely_config_response.dart'; import 'package:optimizely_flutter_sdk/src/optimizely_client_wrapper.dart'; @@ -51,6 +52,8 @@ export 'package:optimizely_flutter_sdk/src/data_objects/event_options.dart' show EventOptions; export 'package:optimizely_flutter_sdk/src/data_objects/sdk_settings.dart' show SDKSettings; +export 'package:optimizely_flutter_sdk/src/data_objects/cmab_config.dart' + show CmabConfig; export 'package:optimizely_flutter_sdk/src/data_objects/datafile_options.dart' show DatafileHostOptions; export 'package:optimizely_flutter_sdk/src/data_objects/log_level.dart' @@ -72,6 +75,7 @@ class OptimizelyFlutterSdk { final Set _defaultDecideOptions; final OptimizelyLogLevel _defaultLogLevel; final SDKSettings _sdkSettings; + final CmabConfig? _cmabConfig; static OptimizelyLogger? _customLogger; /// Get the current logger static OptimizelyLogger? get logger { @@ -84,13 +88,15 @@ class OptimizelyFlutterSdk { Set defaultDecideOptions = const {}, OptimizelyLogLevel defaultLogLevel = OptimizelyLogLevel.info, SDKSettings sdkSettings = const SDKSettings(), - OptimizelyLogger? logger}) + CmabConfig? cmabConfig, + OptimizelyLogger? logger}) : _eventOptions = eventOptions, _datafilePeriodicDownloadInterval = datafilePeriodicDownloadInterval, _datafileHostOptions = datafileHostOptions, _defaultDecideOptions = defaultDecideOptions, _defaultLogLevel = defaultLogLevel, - _sdkSettings = sdkSettings { + _sdkSettings = sdkSettings, + _cmabConfig = cmabConfig { // Set the logger if provided _customLogger = logger ?? DefaultOptimizelyLogger(); LoggerBridge.initialize(_customLogger); @@ -106,6 +112,7 @@ class OptimizelyFlutterSdk { _defaultDecideOptions, _defaultLogLevel, _sdkSettings, + _cmabConfig, _customLogger ); } diff --git a/lib/src/data_objects/cmab_config.dart b/lib/src/data_objects/cmab_config.dart new file mode 100644 index 0000000..3fb8011 --- /dev/null +++ b/lib/src/data_objects/cmab_config.dart @@ -0,0 +1,33 @@ +/**************************************************************************** + * Copyright 2025, 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. * + ***************************************************************************/ + +/// Configuration for CMAB (Contextual Multi-Armed Bandit) service +class CmabConfig { + /// The maximum size of CMAB decision cache (default = 100) + final int cacheSize; + + /// The timeout in seconds of CMAB cache (default = 1800 / 30 minutes) + final int cacheTimeoutInSecs; + + /// The CMAB prediction endpoint (optional, default endpoint used if null) + final String? predictionEndpoint; + + const CmabConfig({ + this.cacheSize = 100, + this.cacheTimeoutInSecs = 1800, + this.predictionEndpoint, + }); +} diff --git a/lib/src/optimizely_client_wrapper.dart b/lib/src/optimizely_client_wrapper.dart index a7c092d..46e1627 100644 --- a/lib/src/optimizely_client_wrapper.dart +++ b/lib/src/optimizely_client_wrapper.dart @@ -64,6 +64,7 @@ class OptimizelyClientWrapper { Set defaultDecideOptions, OptimizelyLogLevel defaultLogLevel, SDKSettings sdkSettings, + CmabConfig? cmabConfig, OptimizelyLogger? logger) async { _channel.setMethodCallHandler(methodCallHandler); final convertedOptions = Utils.convertDecideOptions(defaultDecideOptions); @@ -95,6 +96,19 @@ class OptimizelyClientWrapper { }; requestDict[Constants.optimizelySdkSettings] = optimizelySdkSettings; + // CMAB Config params + if (cmabConfig != null) { + Map cmabConfigMap = { + Constants.cmabCacheSize: cmabConfig.cacheSize, + Constants.cmabCacheTimeoutInSecs: cmabConfig.cacheTimeoutInSecs, + }; + if (cmabConfig.predictionEndpoint != null) { + cmabConfigMap[Constants.cmabPredictionEndpoint] = + cmabConfig.predictionEndpoint; + } + requestDict[Constants.cmabConfig] = cmabConfigMap; + } + // clearing notification listeners, if they are mapped to the same sdkKey. activateCallbacksById.remove(sdkKey); decisionCallbacksById.remove(sdkKey); diff --git a/lib/src/user_context/optimizely_user_context.dart b/lib/src/user_context/optimizely_user_context.dart index 906951f..d0f4cb1 100644 --- a/lib/src/user_context/optimizely_user_context.dart +++ b/lib/src/user_context/optimizely_user_context.dart @@ -41,7 +41,16 @@ enum OptimizelyDecideOption { includeReasons, /// exclude variable values from the decision result. - excludeVariables + excludeVariables, + + /// ignore CMAB cache (bypass cache, make fresh request). + ignoreCmabCache, + + /// reset entire CMAB cache. + resetCmabCache, + + /// invalidate CMAB cache for current user only. + invalidateUserCmabCache } /// Options controlling audience segments. @@ -218,6 +227,56 @@ class OptimizelyUserContext { return result; } + /// Asynchronously returns a decision result for a given flag key and a user context. + /// This method supports CMAB (Contextual Multi-Armed Bandit) experiments. + /// + /// Takes [key] A flag key for which a decision will be made. + /// Optional [options] A set of [OptimizelyDecideOption] for decision-making. + /// Returns [DecideResponse] A decision result. + Future decideAsync(String key, + [Set options = const {}]) async { + final result = await _decideAsync([key], options); + return DecideResponse(result); + } + + /// Asynchronously returns a key-map of decision results for multiple flag keys. + /// This method supports CMAB (Contextual Multi-Armed Bandit) experiments. + /// + /// Takes [keys] A [List] of flag keys for which decisions will be made. + /// Optional [options] A set of [OptimizelyDecideOption] for decision-making. + /// Returns [DecideForKeysResponse] All decision results mapped by flag keys. + Future decideForKeysAsync(List keys, + [Set options = const {}]) async { + final result = await _decideAsync(keys, options); + return DecideForKeysResponse(result); + } + + /// Asynchronously returns a key-map of decision results for all active flag keys. + /// This method supports CMAB (Contextual Multi-Armed Bandit) experiments. + /// + /// Optional [options] A set of [OptimizelyDecideOption] for decision-making. + /// Returns [DecideForKeysResponse] All decision results mapped by flag keys. + Future decideAllAsync( + [Set options = const {}]) async { + final result = await _decideAsync([], options); + return DecideForKeysResponse(result); + } + + /// Private helper for async decide operations + Future> _decideAsync( + [List keys = const [], + Set options = const {}]) async { + final convertedOptions = Utils.convertDecideOptions(options); + var result = Map.from( + await _channel.invokeMethod(Constants.decideAsyncMethod, { + Constants.sdkKey: _sdkKey, + Constants.userContextId: _userContextId, + Constants.keys: keys, + Constants.optimizelyDecideOption: convertedOptions, + })); + return result; + } + /// Sets the forced decision for a given decision context. /// /// Takes [context] The [OptimizelyDecisionContext] containing flagKey and ruleKey. diff --git a/lib/src/utils/constants.dart b/lib/src/utils/constants.dart index f5874c1..7e299db 100644 --- a/lib/src/utils/constants.dart +++ b/lib/src/utils/constants.dart @@ -37,6 +37,7 @@ class Constants { static const String getAttributesMethod = "getAttributes"; static const String trackEventMethod = "trackEvent"; static const String decideMethod = "decide"; + static const String decideAsyncMethod = "decideAsync"; static const String setForcedDecision = "setForcedDecision"; static const String getForcedDecision = "getForcedDecision"; static const String removeForcedDecision = "removeForcedDecision"; @@ -137,6 +138,12 @@ class Constants { static const String disableOdp = "disableOdp"; static const String enableVuid = "enableVuid"; + // CMAB Config params + static const String cmabConfig = "cmabConfig"; + static const String cmabCacheSize = "cmabCacheSize"; + static const String cmabCacheTimeoutInSecs = "cmabCacheTimeoutInSecs"; + static const String cmabPredictionEndpoint = "cmabPredictionEndpoint"; + // Response keys static const String responseSuccess = "success"; static const String responseResult = "result"; diff --git a/lib/src/utils/utils.dart b/lib/src/utils/utils.dart index e697b80..1342ad1 100644 --- a/lib/src/utils/utils.dart +++ b/lib/src/utils/utils.dart @@ -26,6 +26,9 @@ class Utils { OptimizelyDecideOption.ignoreUserProfileService: "ignoreUserProfileService", OptimizelyDecideOption.includeReasons: "includeReasons", OptimizelyDecideOption.excludeVariables: "excludeVariables", + OptimizelyDecideOption.ignoreCmabCache: "ignoreCmabCache", + OptimizelyDecideOption.resetCmabCache: "resetCmabCache", + OptimizelyDecideOption.invalidateUserCmabCache: "invalidateUserCmabCache", }; static Map segmentOptions = { From 9965f55e314bbb6b80af1a619009e63ff6d0ae38 Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Wed, 24 Dec 2025 22:21:07 +0600 Subject: [PATCH 3/7] Predictionendpoint templating fixed --- ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift | 3 ++- lib/src/data_objects/cmab_config.dart | 11 ++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift b/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift index cf19a18..1978119 100644 --- a/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift +++ b/ios/Classes/SwiftOptimizelyFlutterSdkPlugin.swift @@ -168,7 +168,8 @@ public class SwiftOptimizelyFlutterSdkPlugin: NSObject, FlutterPlugin { cacheTimeoutInSecs = timeout } if let endpoint = cmabConfigDict[RequestParameterKey.cmabPredictionEndpoint] as? String { - predictionEndpoint = endpoint + // Convert platform-agnostic placeholder {ruleId} to Swift format %@ + predictionEndpoint = endpoint.replacingOccurrences(of: "{ruleId}", with: "%@") } cmabConfig = CmabConfig( diff --git a/lib/src/data_objects/cmab_config.dart b/lib/src/data_objects/cmab_config.dart index 3fb8011..5fdfaa6 100644 --- a/lib/src/data_objects/cmab_config.dart +++ b/lib/src/data_objects/cmab_config.dart @@ -22,7 +22,16 @@ class CmabConfig { /// The timeout in seconds of CMAB cache (default = 1800 / 30 minutes) final int cacheTimeoutInSecs; - /// The CMAB prediction endpoint (optional, default endpoint used if null) + /// The CMAB prediction endpoint template (optional, default endpoint used if null) + /// + /// Provide a URL template with '{ruleId}' placeholder which will be replaced + /// with the actual rule ID at runtime. + /// + /// Example: 'https://custom-endpoint.example.com/predict/{ruleId}' + /// Default: 'https://prediction.cmab.optimizely.com/predict/{ruleId}' + /// + /// Note: The placeholder is automatically converted to platform-specific format + /// (%@ for iOS, %s for Android) when passed to native SDKs. final String? predictionEndpoint; const CmabConfig({ From f9d9c0035b4242854648c9714cef23fc97671dd2 Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Wed, 24 Dec 2025 22:21:25 +0600 Subject: [PATCH 4/7] Cmab Sample api added --- example/lib/main.dart | 38 ++++- example/lib/sample_api.dart | 324 ++++++++++++++++++++++++++++++++++++ 2 files changed, 361 insertions(+), 1 deletion(-) create mode 100644 example/lib/sample_api.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index 39c905e..f85b051 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'dart:math'; import 'package:optimizely_flutter_sdk/optimizely_flutter_sdk.dart'; import 'package:optimizely_flutter_sdk_example/custom_logger.dart'; +import 'package:optimizely_flutter_sdk_example/sample_api.dart'; void main() { runApp(const MyApp()); @@ -143,6 +144,18 @@ class _MyAppState extends State { if (!mounted) return; } + Future _runCmabExamples() async { + setState(() { + uiResponse = 'Running CMAB examples... Check console for output.'; + }); + + await CmabSampleApi.runAllCmabExamples(); + + setState(() { + uiResponse = 'CMAB examples completed! Check console for detailed output.'; + }); + } + @override Widget build(BuildContext context) { return MaterialApp( @@ -151,7 +164,30 @@ class _MyAppState extends State { title: const Text('Plugin example app'), ), body: Center( - child: Text(uiResponse), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: SingleChildScrollView( + child: Text(uiResponse), + ), + ), + const SizedBox(height: 20), + ElevatedButton( + onPressed: _runCmabExamples, + child: const Text('Run CMAB Examples'), + ), + const SizedBox(height: 10), + const Text( + 'Note: Update SDK_KEY and CMAB_FLAG_KEY\nin sample_api.dart before running', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 12, color: Colors.grey), + ), + ], + ), + ), ), ), ); diff --git a/example/lib/sample_api.dart b/example/lib/sample_api.dart new file mode 100644 index 0000000..b8a883d --- /dev/null +++ b/example/lib/sample_api.dart @@ -0,0 +1,324 @@ +/// ************************************************************************** +/// Copyright 2025, 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 'package:optimizely_flutter_sdk/optimizely_flutter_sdk.dart'; + +/// CMAB (Contextual Multi-Armed Bandit) API usage examples +/// +/// This class demonstrates how to use CMAB features in the Optimizely Flutter SDK: +/// - Initializing SDK with CmabConfig +/// - Using decideAsync for CMAB-enabled experiments +/// - CMAB cache control options +/// - Combining CMAB with other decide options +class CmabSampleApi { + // Replace with your actual SDK key + static const String SDK_KEY = 'Q9LjjQmUn5pDCjKMc1jFC'; + + // Replace with your CMAB-enabled flag key + static const String CMAB_FLAG_KEY = 'cmab-flag'; + + /// Example 1: Basic CMAB initialization and single flag decision + /// + /// This example shows: + /// - How to initialize the SDK with default CmabConfig + /// - How to create a user context with attributes + /// - How to use decideAsync() for a single flag + /// - How to access decision results + static Future basicCmabExample() async { + print('\n========== Example 1: Basic CMAB Usage =========='); + + try { + // Initialize SDK with default CMAB configuration + // Default cache size: 100, cache timeout: 1800 seconds (30 minutes) + var flutterSDK = OptimizelyFlutterSdk( + SDK_KEY, + cmabConfig: CmabConfig(), // Uses defaults + ); + + var response = await flutterSDK.initializeClient(); + if (!response.success) { + print('Failed to initialize SDK: ${response.reason}'); + return; + } + print('✓ SDK initialized successfully'); + + // Create user context with attributes + // CMAB uses these attributes to make personalized decisions + var userContext = await flutterSDK.createUserContext( + userId: 'user_123', + attributes: { + 'country': 'us' + }, + ); + + if (userContext == null) { + print('Failed to create user context'); + return; + } + print('✓ User context created for user_123'); + + // Use decideAsync for CMAB-enabled flag + // This makes an async call to the CMAB service for personalized variation + // Always use ignoreUserProfileService with CMAB to get correct decisions + print('\nMaking async decision for flag: $CMAB_FLAG_KEY'); + var decision = await userContext.decideAsync( + CMAB_FLAG_KEY, + {OptimizelyDecideOption.ignoreUserProfileService}, + ); + + // Access decision results + if (decision.decision != null) { + print('✓ Decision received:'); + print(' - Flag Key: ${decision.decision!.flagKey}'); + print(' - Variation Key: ${decision.decision!.variationKey}'); + print(' - Enabled: ${decision.decision!.enabled}'); + print(' - Variables: ${decision.decision!.variables}'); + } else { + print('✗ No decision returned'); + } + } catch (e) { + print('Error in basicCmabExample: $e'); + } + } + + /// Example 2: CMAB cache control options + /// + /// This example demonstrates the three CMAB-specific cache options: + /// - ignoreCmabCache: Bypass cache and make fresh CMAB request + /// - resetCmabCache: Clear entire CMAB cache before decision + /// - invalidateUserCmabCache: Clear cache for current user only + static Future cmabCacheOptionsExample() async { + print('\n========== Example 2: CMAB Cache Options =========='); + + try { + var flutterSDK = OptimizelyFlutterSdk( + SDK_KEY, + cmabConfig: CmabConfig(), + ); + + await flutterSDK.initializeClient(); + var userContext = await flutterSDK.createUserContext( + userId: 'user_456', + attributes: {'country': 'us'}, + ); + + if (userContext == null) return; + + // Option 1: Ignore CMAB Cache + // Use this when you want to bypass the cache and get a fresh decision + // from the CMAB service (e.g., for real-time personalization) + print('\n1. Using ignoreCmabCache option:'); + var decision1 = await userContext.decideAsync( + CMAB_FLAG_KEY, + { + OptimizelyDecideOption.ignoreCmabCache, + OptimizelyDecideOption.ignoreUserProfileService, + }, + ); + print(' ✓ Fresh decision from CMAB service (cache bypassed)'); + print(' - Variation: ${decision1.decision?.variationKey}'); + + // Option 2: Reset CMAB Cache + // Use this to clear the entire CMAB cache before making a decision + // Useful when you want to refresh all cached CMAB decisions + print('\n2. Using resetCmabCache option:'); + var decision2 = await userContext.decideAsync( + CMAB_FLAG_KEY, + { + OptimizelyDecideOption.resetCmabCache, + OptimizelyDecideOption.ignoreUserProfileService, + }, + ); + print(' ✓ Entire CMAB cache cleared, new decision fetched'); + print(' - Variation: ${decision2.decision?.variationKey}'); + + // Option 3: Invalidate User CMAB Cache + // Use this to clear cache for the current user only + // Other users' cached decisions remain intact + print('\n3. Using invalidateUserCmabCache option:'); + var decision3 = await userContext.decideAsync( + CMAB_FLAG_KEY, + { + OptimizelyDecideOption.invalidateUserCmabCache, + OptimizelyDecideOption.ignoreUserProfileService, + }, + ); + print(' ✓ User-specific cache cleared, new decision fetched'); + print(' - Variation: ${decision3.decision?.variationKey}'); + + // Regular cached decision (for comparison) + print('\n4. Regular decision (uses cache if available):'); + var decision4 = await userContext.decideAsync( + CMAB_FLAG_KEY, + {OptimizelyDecideOption.ignoreUserProfileService}, + ); + print(' ✓ Decision returned (may be from cache)'); + print(' - Variation: ${decision4.decision?.variationKey}'); + + } catch (e) { + print('Error in cmabCacheOptionsExample: $e'); + } + } + + /// Example 3: Custom CMAB configuration + /// + /// This example shows how to customize CMAB settings: + /// - Custom cache size + /// - Custom cache timeout + /// - Custom prediction endpoint (optional) + static Future customCmabConfigExample() async { + print('\n========== Example 3: Custom CMAB Configuration =========='); + + try { + // Initialize with custom CMAB configuration + var flutterSDK = OptimizelyFlutterSdk( + SDK_KEY, + cmabConfig: CmabConfig( + cacheSize: 200, // Store up to 200 decisions in cache + cacheTimeoutInSecs: 3600, // Cache expires after 1 hour (3600 seconds) + predictionEndpoint: 'https://custom-endpoint.example.com/predict/{ruleId}', // Optional custom endpoint template + ), + ); + + print('✓ SDK initialized with custom CMAB config:'); + print(' - Cache Size: 200 decisions'); + print(' - Cache Timeout: 3600 seconds (1 hour)'); + print(' - Prediction Endpoint: https://custom-endpoint.example.com/predict/{ruleId}'); + + await flutterSDK.initializeClient(); + + var userContext = await flutterSDK.createUserContext( + userId: 'user_789', + attributes: {'country': 'us'}, + ); + + if (userContext == null) return; + + // Make decision with custom config + var decision = await userContext.decideAsync( + CMAB_FLAG_KEY, + {OptimizelyDecideOption.ignoreUserProfileService}, + ); + + print('\n✓ Decision made with custom cache settings:'); + print(' - Variation: ${decision.decision?.variationKey}'); + print(' - This decision will be cached for 1 hour'); + print(' - Cache can store up to 200 user decisions'); + + } catch (e) { + print('Error in customCmabConfigExample: $e'); + } + } + + /// Example 4: Combining CMAB options with other decide options + /// + /// This example shows how to use CMAB cache options together with + /// other OptimizelyDecideOption values like includeReasons + static Future combinedOptionsExample() async { + print('\n========== Example 4: Combined Options =========='); + + try { + var flutterSDK = OptimizelyFlutterSdk( + SDK_KEY, + cmabConfig: CmabConfig(), + ); + + await flutterSDK.initializeClient(); + + var userContext = await flutterSDK.createUserContext( + userId: 'user_999', + attributes: {'country': 'us'}, + ); + + if (userContext == null) return; + + // Combine ignoreCmabCache with includeReasons and ignoreUserProfileService + // This gives you a fresh decision with detailed reasoning + print('\nCombining ignoreCmabCache + includeReasons + ignoreUserProfileService:'); + var decision = await userContext.decideAsync( + CMAB_FLAG_KEY, + { + OptimizelyDecideOption.ignoreCmabCache, + OptimizelyDecideOption.includeReasons, + OptimizelyDecideOption.ignoreUserProfileService, + }, + ); + + print('✓ Decision with combined options:'); + print(' - Variation: ${decision.decision?.variationKey}'); + print(' - Enabled: ${decision.decision?.enabled}'); + + if (decision.decision?.reasons != null && + decision.decision!.reasons.isNotEmpty) { + print(' - Reasons:'); + for (var reason in decision.decision!.reasons) { + print(' • $reason'); + } + } + + // Another combination: resetCmabCache + excludeVariables + ignoreUserProfileService + print('\nCombining resetCmabCache + excludeVariables + ignoreUserProfileService:'); + var decision2 = await userContext.decideAsync( + CMAB_FLAG_KEY, + { + OptimizelyDecideOption.resetCmabCache, + OptimizelyDecideOption.excludeVariables, + OptimizelyDecideOption.ignoreUserProfileService, + }, + ); + + print('✓ Decision without variables:'); + print(' - Variation: ${decision2.decision?.variationKey}'); + print(' - Variables excluded: ${decision2.decision?.variables.isEmpty}'); + + } catch (e) { + print('Error in combinedOptionsExample: $e'); + } + } + + /// Run all CMAB examples sequentially + /// + /// This runs all the example methods in order, demonstrating + /// the complete CMAB API functionality + static Future runAllCmabExamples() async { + print('\n╔════════════════════════════════════════════════════════╗'); + print('║ CMAB API Examples - Optimizely Flutter SDK ║'); + print('╚════════════════════════════════════════════════════════╝'); + + print('\nIMPORTANT: Update SDK_KEY and CMAB_FLAG_KEY constants'); + print('in sample_api.dart before running these examples.\n'); + + try { + await basicCmabExample(); + await Future.delayed(Duration(seconds: 1)); // Pause between examples + + await cmabCacheOptionsExample(); + await Future.delayed(Duration(seconds: 1)); + + await customCmabConfigExample(); + await Future.delayed(Duration(seconds: 1)); + + await combinedOptionsExample(); + + print('\n╔════════════════════════════════════════════════════════╗'); + print('║ All CMAB Examples Completed Successfully! ✓ ║'); + print('╚════════════════════════════════════════════════════════╝\n'); + + } catch (e) { + print('\n✗ Error running CMAB examples: $e'); + } + } +} From 5d5e11934002460d1820479aacdf36e41d686548 Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Wed, 24 Dec 2025 22:54:15 +0600 Subject: [PATCH 5/7] CMAB android basic implementation done --- .../OptimizelyFlutterClient.java | 72 +++++++++++++++++++ .../OptimizelyFlutterSdkPlugin.java | 4 ++ .../helper_classes/ArgumentsParser.java | 4 ++ .../helper_classes/Constants.java | 7 ++ 4 files changed, 87 insertions(+) diff --git a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterClient.java b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterClient.java index a4f6ce4..7f48db0 100644 --- a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterClient.java +++ b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterClient.java @@ -59,6 +59,10 @@ import static com.optimizely.optimizely_flutter_sdk.helper_classes.Constants.RequestParameterKey.SEGMENTS_CACHE_TIMEOUT_IN_SECONDS; import static com.optimizely.optimizely_flutter_sdk.helper_classes.Constants.RequestParameterKey.TIMEOUT_FOR_ODP_EVENT_IN_SECONDS; import static com.optimizely.optimizely_flutter_sdk.helper_classes.Constants.RequestParameterKey.TIMEOUT_FOR_SEGMENT_FETCH_IN_SECONDS; +import static com.optimizely.optimizely_flutter_sdk.helper_classes.Constants.RequestParameterKey.CMAB_CONFIG; +import static com.optimizely.optimizely_flutter_sdk.helper_classes.Constants.RequestParameterKey.CMAB_CACHE_SIZE; +import static com.optimizely.optimizely_flutter_sdk.helper_classes.Constants.RequestParameterKey.CMAB_CACHE_TIMEOUT_IN_SECS; +import static com.optimizely.optimizely_flutter_sdk.helper_classes.Constants.RequestParameterKey.CMAB_PREDICTION_ENDPOINT; import static com.optimizely.optimizely_flutter_sdk.helper_classes.Utils.getNotificationListenerType; import java.util.Collections; @@ -187,6 +191,25 @@ protected void initializeOptimizely(@NonNull ArgumentsParser argumentsParser, @N optimizelyManagerBuilder.withVuidEnabled(); } + // CMAB Config + Map cmabConfig = argumentsParser.getCmabConfig(); + if (cmabConfig != null) { + if (cmabConfig.containsKey(CMAB_CACHE_SIZE)) { + Integer cmabCacheSize = (Integer) cmabConfig.get(CMAB_CACHE_SIZE); + optimizelyManagerBuilder.withCmabCacheSize(cmabCacheSize); + } + if (cmabConfig.containsKey(CMAB_CACHE_TIMEOUT_IN_SECS)) { + Integer cmabCacheTimeout = (Integer) cmabConfig.get(CMAB_CACHE_TIMEOUT_IN_SECS); + optimizelyManagerBuilder.withCmabCacheTimeout(cmabCacheTimeout, TimeUnit.SECONDS); + } + if (cmabConfig.containsKey(CMAB_PREDICTION_ENDPOINT)) { + String endpoint = (String) cmabConfig.get(CMAB_PREDICTION_ENDPOINT); + // Convert platform-agnostic placeholder {ruleId} to Android format %s + String androidEndpoint = endpoint.replace("{ruleId}", "%s"); + optimizelyManagerBuilder.withCmabPredictionEndpoint(androidEndpoint); + } + } + OptimizelyManager optimizelyManager = optimizelyManagerBuilder.build(context); optimizelyManager.initialize(context, null, (OptimizelyClient client) -> { @@ -364,6 +387,55 @@ protected void decide(ArgumentsParser argumentsParser, @NonNull Result result) { result.success(createResponse(s)); } + protected void decideAsync(ArgumentsParser argumentsParser, @NonNull Result result) { + String sdkKey = argumentsParser.getSdkKey(); + OptimizelyUserContext userContext = getUserContext(argumentsParser); + if (!isUserContextValid(sdkKey, userContext, result)) { + return; + } + + List decideKeys = argumentsParser.getDecideKeys(); + List decideOptions = argumentsParser.getDecideOptions(); + + // Determine which async method to call based on keys + if (decideKeys == null || decideKeys.isEmpty()) { + // decideAllAsync + userContext.decideAllAsync(decideOptions, decisions -> { + Map optimizelyDecisionResponseMap = new HashMap<>(); + if (decisions != null) { + for (Map.Entry entry : decisions.entrySet()) { + optimizelyDecisionResponseMap.put(entry.getKey(), new OptimizelyDecisionResponse(entry.getValue())); + } + } + ObjectMapper mapper = new ObjectMapper(); + Map s = mapper.convertValue(optimizelyDecisionResponseMap, LinkedHashMap.class); + result.success(createResponse(s)); + }); + } else if (decideKeys.size() == 1) { + // decideAsync for single key + userContext.decideAsync(decideKeys.get(0), decideOptions, decision -> { + Map optimizelyDecisionResponseMap = new HashMap<>(); + optimizelyDecisionResponseMap.put(decideKeys.get(0), new OptimizelyDecisionResponse(decision)); + ObjectMapper mapper = new ObjectMapper(); + Map s = mapper.convertValue(optimizelyDecisionResponseMap, LinkedHashMap.class); + result.success(createResponse(s)); + }); + } else { + // decideForKeysAsync for multiple keys + userContext.decideForKeysAsync(decideKeys, decideOptions, decisions -> { + Map optimizelyDecisionResponseMap = new HashMap<>(); + if (decisions != null) { + for (Map.Entry entry : decisions.entrySet()) { + optimizelyDecisionResponseMap.put(entry.getKey(), new OptimizelyDecisionResponse(entry.getValue())); + } + } + ObjectMapper mapper = new ObjectMapper(); + Map s = mapper.convertValue(optimizelyDecisionResponseMap, LinkedHashMap.class); + result.success(createResponse(s)); + }); + } + } + protected void setForcedDecision(ArgumentsParser argumentsParser, @NonNull Result result) { String sdkKey = argumentsParser.getSdkKey(); OptimizelyUserContext userContext = getUserContext(argumentsParser); diff --git a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterSdkPlugin.java b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterSdkPlugin.java index 5ca2d8e..9e8dc3f 100644 --- a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterSdkPlugin.java +++ b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/OptimizelyFlutterSdkPlugin.java @@ -112,6 +112,10 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { decide(argumentsParser, result); break; } + case APIs.DECIDE_ASYNC: { + decideAsync(argumentsParser, result); + break; + } case APIs.SET_FORCED_DECISION: { setForcedDecision(argumentsParser, result); break; diff --git a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/ArgumentsParser.java b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/ArgumentsParser.java index 6d2741f..7758bcf 100644 --- a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/ArgumentsParser.java +++ b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/ArgumentsParser.java @@ -151,4 +151,8 @@ public List getSegmentOptions() { public Map getOptimizelySdkSettings() { return (Map) arguments.get(Constants.RequestParameterKey.OPTIMIZELY_SDK_SETTINGS); } + + public Map getCmabConfig() { + return (Map) arguments.get(Constants.RequestParameterKey.CMAB_CONFIG); + } } diff --git a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/Constants.java b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/Constants.java index 62f0ce9..8d17d29 100644 --- a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/Constants.java +++ b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/Constants.java @@ -34,6 +34,7 @@ public static class APIs { public static final String SET_FORCED_DECISION = "setForcedDecision"; public static final String TRACK_EVENT = "trackEvent"; public static final String DECIDE = "decide"; + public static final String DECIDE_ASYNC = "decideAsync"; public static final String ADD_NOTIFICATION_LISTENER = "addNotificationListener"; public static final String REMOVE_NOTIFICATION_LISTENER = "removeNotificationListener"; public static final String CLEAR_ALL_NOTIFICATION_LISTENERS = "clearAllNotificationListeners"; @@ -97,6 +98,12 @@ public static class RequestParameterKey { public static final String TIMEOUT_FOR_ODP_EVENT_IN_SECONDS = "timeoutForOdpEventInSecs"; public static final String DISABLE_ODP = "disableOdp"; public static final String ENABLE_VUID = "enableVuid"; + + // CMAB Config + public static final String CMAB_CONFIG = "cmabConfig"; + public static final String CMAB_CACHE_SIZE = "cmabCacheSize"; + public static final String CMAB_CACHE_TIMEOUT_IN_SECS = "cmabCacheTimeoutInSecs"; + public static final String CMAB_PREDICTION_ENDPOINT = "cmabPredictionEndpoint"; } public static class ErrorMessage { From 7fb925c254fba22d64c1e8f3f39f5e0fb05e204d Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Wed, 24 Dec 2025 23:35:14 +0600 Subject: [PATCH 6/7] Add unit test cases --- example/lib/main.dart | 5 - test/cmab_test.dart | 493 ++++++++++++++++++++++++++ test/optimizely_flutter_sdk_test.dart | 27 ++ 3 files changed, 520 insertions(+), 5 deletions(-) create mode 100644 test/cmab_test.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index f85b051..78a324f 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -180,11 +180,6 @@ class _MyAppState extends State { child: const Text('Run CMAB Examples'), ), const SizedBox(height: 10), - const Text( - 'Note: Update SDK_KEY and CMAB_FLAG_KEY\nin sample_api.dart before running', - textAlign: TextAlign.center, - style: TextStyle(fontSize: 12, color: Colors.grey), - ), ], ), ), diff --git a/test/cmab_test.dart b/test/cmab_test.dart new file mode 100644 index 0000000..81865bc --- /dev/null +++ b/test/cmab_test.dart @@ -0,0 +1,493 @@ +/// ************************************************************************** +/// Copyright 2022-2023, 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 "package:flutter/services.dart"; +import "package:flutter_test/flutter_test.dart"; +import "package:optimizely_flutter_sdk/optimizely_flutter_sdk.dart"; +import 'package:optimizely_flutter_sdk/src/optimizely_client_wrapper.dart'; +import 'package:optimizely_flutter_sdk/src/utils/constants.dart'; +import 'package:optimizely_flutter_sdk/src/utils/utils.dart'; +import 'test_utils.dart'; + +void main() { + const String testSDKKey = "KZbunNn9bVfBWLpZPq2XC4"; + const String userId = "uid-351ea8"; + const String flagKey = "flag_1"; + const String userContextId = "123"; + const Map attributes = {"abc": 123}; + + const MethodChannel channel = MethodChannel("optimizely_flutter_sdk"); + TestDefaultBinaryMessenger? tester; + + setUp(() async { + TestWidgetsFlutterBinding.ensureInitialized(); + OptimizelyClientWrapper.decisionCallbacksById = {}; + OptimizelyClientWrapper.trackCallbacksById = {}; + OptimizelyClientWrapper.configUpdateCallbacksById = {}; + OptimizelyClientWrapper.logEventCallbacksById = {}; + OptimizelyClientWrapper.nextCallbackId = 0; + tester = TestDefaultBinaryMessengerBinding.instance?.defaultBinaryMessenger; + }); + + tearDown(() { + tester?.setMockMethodCallHandler(channel, null); + }); + + group('CmabConfig', () { + test('creates CmabConfig with default values', () { + const config = CmabConfig(); + + expect(config.cacheSize, equals(100)); + expect(config.cacheTimeoutInSecs, equals(1800)); + expect(config.predictionEndpoint, isNull); + }); + + test('creates CmabConfig with custom values', () { + const config = CmabConfig( + cacheSize: 200, + cacheTimeoutInSecs: 3600, + predictionEndpoint: "https://custom-endpoint.com/predict/{ruleId}", + ); + + expect(config.cacheSize, equals(200)); + expect(config.cacheTimeoutInSecs, equals(3600)); + expect(config.predictionEndpoint, equals("https://custom-endpoint.com/predict/{ruleId}")); + }); + + test('creates CmabConfig with null predictionEndpoint', () { + const config = CmabConfig( + cacheSize: 150, + cacheTimeoutInSecs: 2400, + ); + + expect(config.cacheSize, equals(150)); + expect(config.cacheTimeoutInSecs, equals(2400)); + expect(config.predictionEndpoint, isNull); + }); + + test('CmabConfig with same values are equal', () { + const config1 = CmabConfig(cacheSize: 100, cacheTimeoutInSecs: 1800); + const config2 = CmabConfig(cacheSize: 100, cacheTimeoutInSecs: 1800); + + expect(config1.cacheSize, equals(config2.cacheSize)); + expect(config1.cacheTimeoutInSecs, equals(config2.cacheTimeoutInSecs)); + expect(config1.predictionEndpoint, equals(config2.predictionEndpoint)); + }); + }); + + group('OptimizelyFlutterSdk initialization with CmabConfig', () { + test('initializes SDK with default CmabConfig', () async { + Map? receivedCmabConfig; + const defaultConfig = CmabConfig(); + + tester?.setMockMethodCallHandler(channel, (MethodCall methodCall) async { + if (methodCall.method == Constants.initializeMethod) { + var cmabArg = methodCall.arguments[Constants.cmabConfig]; + if (cmabArg != null) { + receivedCmabConfig = Map.from(cmabArg as Map); + } + return {Constants.responseSuccess: true}; + } + return null; + }); + + var sdk = OptimizelyFlutterSdk(testSDKKey, cmabConfig: defaultConfig); + await sdk.initializeClient(); + + expect(receivedCmabConfig, isNotNull); + expect(receivedCmabConfig![Constants.cmabCacheSize], equals(100)); + expect(receivedCmabConfig![Constants.cmabCacheTimeoutInSecs], equals(1800)); + expect(receivedCmabConfig!.containsKey(Constants.cmabPredictionEndpoint), isFalse); + }); + + test('initializes SDK with custom CmabConfig', () async { + Map? receivedCmabConfig; + const customConfig = CmabConfig( + cacheSize: 250, + cacheTimeoutInSecs: 3000, + predictionEndpoint: "https://test.com/predict/{ruleId}", + ); + + tester?.setMockMethodCallHandler(channel, (MethodCall methodCall) async { + if (methodCall.method == Constants.initializeMethod) { + var cmabArg = methodCall.arguments[Constants.cmabConfig]; + if (cmabArg != null) { + receivedCmabConfig = Map.from(cmabArg as Map); + } + return {Constants.responseSuccess: true}; + } + return null; + }); + + var sdk = OptimizelyFlutterSdk(testSDKKey, cmabConfig: customConfig); + await sdk.initializeClient(); + + expect(receivedCmabConfig, isNotNull); + expect(receivedCmabConfig![Constants.cmabCacheSize], equals(250)); + expect(receivedCmabConfig![Constants.cmabCacheTimeoutInSecs], equals(3000)); + expect(receivedCmabConfig![Constants.cmabPredictionEndpoint], equals("https://test.com/predict/{ruleId}")); + }); + + test('initializes SDK with CmabConfig without predictionEndpoint', () async { + Map? receivedCmabConfig; + const customConfig = CmabConfig( + cacheSize: 300, + cacheTimeoutInSecs: 2500, + ); + + tester?.setMockMethodCallHandler(channel, (MethodCall methodCall) async { + if (methodCall.method == Constants.initializeMethod) { + var cmabArg = methodCall.arguments[Constants.cmabConfig]; + if (cmabArg != null) { + receivedCmabConfig = Map.from(cmabArg as Map); + } + return {Constants.responseSuccess: true}; + } + return null; + }); + + var sdk = OptimizelyFlutterSdk(testSDKKey, cmabConfig: customConfig); + await sdk.initializeClient(); + + expect(receivedCmabConfig, isNotNull); + expect(receivedCmabConfig![Constants.cmabCacheSize], equals(300)); + expect(receivedCmabConfig![Constants.cmabCacheTimeoutInSecs], equals(2500)); + expect(receivedCmabConfig!.containsKey(Constants.cmabPredictionEndpoint), isFalse); + }); + + test('CmabConfig is serialized correctly in initialization', () async { + Map? receivedCmabConfig; + const customConfig = CmabConfig( + cacheSize: 500, + cacheTimeoutInSecs: 4000, + predictionEndpoint: "https://production.com/ml/{ruleId}", + ); + + tester?.setMockMethodCallHandler(channel, (MethodCall methodCall) async { + if (methodCall.method == Constants.initializeMethod) { + expect(methodCall.arguments[Constants.sdkKey], equals(testSDKKey)); + var cmabArg = methodCall.arguments[Constants.cmabConfig]; + if (cmabArg != null) { + receivedCmabConfig = Map.from(cmabArg as Map); + } + return {Constants.responseSuccess: true}; + } + return null; + }); + + var sdk = OptimizelyFlutterSdk(testSDKKey, cmabConfig: customConfig); + await sdk.initializeClient(); + + expect(receivedCmabConfig, isNotNull); + expect(receivedCmabConfig, isA>()); + expect(receivedCmabConfig!.keys.length, equals(3)); + }); + + test('multiple SDKs can have different CmabConfigs', () async { + const config1 = CmabConfig(cacheSize: 100); + const config2 = CmabConfig(cacheSize: 200); + + var sdk1 = OptimizelyFlutterSdk(testSDKKey, cmabConfig: config1); + var sdk2 = OptimizelyFlutterSdk("different_key", cmabConfig: config2); + + expect(sdk1, isNotNull); + expect(sdk2, isNotNull); + }); + + group('decideAsync methods', () { + test('decideAsync single flag sends correct method call', () async { + List? receivedKeys; + List? receivedOptions; + + tester?.setMockMethodCallHandler(channel, (MethodCall methodCall) async { + if (methodCall.method == Constants.initializeMethod) { + return {Constants.responseSuccess: true}; + } + if (methodCall.method == Constants.createUserContextMethod) { + return { + Constants.responseSuccess: true, + Constants.responseResult: {Constants.userContextId: userContextId}, + }; + } + if (methodCall.method == Constants.decideAsyncMethod) { + receivedKeys = List.from(methodCall.arguments[Constants.keys]); + receivedOptions = List.from(methodCall.arguments[Constants.optimizelyDecideOption]); + return { + Constants.responseSuccess: true, + Constants.responseResult: {flagKey: TestUtils.decideResponseMap}, + }; + } + return null; + }); + + var sdk = OptimizelyFlutterSdk(testSDKKey); + await sdk.initializeClient(); + var userContext = await sdk.createUserContext(userId: userId, attributes: attributes); + + await userContext!.decideAsync(flagKey); + + expect(receivedKeys, equals([flagKey])); + expect(receivedOptions, isNotNull); + }); + + test('decideAsync returns correct DecideResponse', () async { + tester?.setMockMethodCallHandler(channel, (MethodCall methodCall) async { + if (methodCall.method == Constants.initializeMethod) { + return {Constants.responseSuccess: true}; + } + if (methodCall.method == Constants.createUserContextMethod) { + return { + Constants.responseSuccess: true, + Constants.responseResult: {Constants.userContextId: userContextId}, + }; + } + if (methodCall.method == Constants.decideAsyncMethod) { + return { + Constants.responseSuccess: true, + Constants.responseResult: {flagKey: TestUtils.decideResponseMap}, + }; + } + return null; + }); + + var sdk = OptimizelyFlutterSdk(testSDKKey); + await sdk.initializeClient(); + var userContext = await sdk.createUserContext(userId: userId, attributes: attributes); + + var response = await userContext!.decideAsync(flagKey); + + expect(response.success, isTrue); + expect(response.decision, isNotNull); + expect(response.decision!.flagKey, equals("feature_1")); // From TestUtils.decideResponseMap + expect(response.decision!.variationKey, isNotNull); + expect(response.decision!.enabled, isTrue); + }); + + test('decideForKeysAsync sends multiple keys', () async { + List? receivedKeys; + const keys = ["flag_1", "flag_2", "flag_3"]; + + tester?.setMockMethodCallHandler(channel, (MethodCall methodCall) async { + if (methodCall.method == Constants.initializeMethod) { + return {Constants.responseSuccess: true}; + } + if (methodCall.method == Constants.createUserContextMethod) { + return { + Constants.responseSuccess: true, + Constants.responseResult: {Constants.userContextId: userContextId}, + }; + } + if (methodCall.method == Constants.decideAsyncMethod) { + receivedKeys = List.from(methodCall.arguments[Constants.keys]); + Map results = {}; + for (var key in keys) { + results[key] = TestUtils.decideResponseMap; + } + return { + Constants.responseSuccess: true, + Constants.responseResult: results, + }; + } + return null; + }); + + var sdk = OptimizelyFlutterSdk(testSDKKey); + await sdk.initializeClient(); + var userContext = await sdk.createUserContext(userId: userId, attributes: attributes); + + await userContext!.decideForKeysAsync(keys); + + expect(receivedKeys, equals(keys)); + }); + + test('decideAllAsync sends empty keys array', () async { + List? receivedKeys; + + tester?.setMockMethodCallHandler(channel, (MethodCall methodCall) async { + if (methodCall.method == Constants.initializeMethod) { + return {Constants.responseSuccess: true}; + } + if (methodCall.method == Constants.createUserContextMethod) { + return { + Constants.responseSuccess: true, + Constants.responseResult: {Constants.userContextId: userContextId}, + }; + } + if (methodCall.method == Constants.decideAsyncMethod) { + receivedKeys = List.from(methodCall.arguments[Constants.keys]); + return { + Constants.responseSuccess: true, + Constants.responseResult: { + "flag_1": TestUtils.decideResponseMap, + "flag_2": TestUtils.decideResponseMap, + }, + }; + } + return null; + }); + + var sdk = OptimizelyFlutterSdk(testSDKKey); + await sdk.initializeClient(); + var userContext = await sdk.createUserContext(userId: userId, attributes: attributes); + + await userContext!.decideAllAsync(); + + expect(receivedKeys, equals([])); + }); + + test('decideAsync methods accept OptimizelyDecideOption', () async { + List? receivedOptions; + const options = {OptimizelyDecideOption.includeReasons}; + + tester?.setMockMethodCallHandler(channel, (MethodCall methodCall) async { + if (methodCall.method == Constants.initializeMethod) { + return {Constants.responseSuccess: true}; + } + if (methodCall.method == Constants.createUserContextMethod) { + return { + Constants.responseSuccess: true, + Constants.responseResult: {Constants.userContextId: userContextId}, + }; + } + if (methodCall.method == Constants.decideAsyncMethod) { + receivedOptions = List.from(methodCall.arguments[Constants.optimizelyDecideOption]); + return { + Constants.responseSuccess: true, + Constants.responseResult: {flagKey: TestUtils.decideResponseMap}, + }; + } + return null; + }); + + var sdk = OptimizelyFlutterSdk(testSDKKey); + await sdk.initializeClient(); + var userContext = await sdk.createUserContext(userId: userId, attributes: attributes); + + await userContext!.decideAsync(flagKey, options); + + expect(receivedOptions, contains(OptimizelyDecideOption.includeReasons.name)); + }); + + test('decideAllAsync returns DecideForKeysResponse', () async { + tester?.setMockMethodCallHandler(channel, (MethodCall methodCall) async { + if (methodCall.method == Constants.initializeMethod) { + return {Constants.responseSuccess: true}; + } + if (methodCall.method == Constants.createUserContextMethod) { + return { + Constants.responseSuccess: true, + Constants.responseResult: {Constants.userContextId: userContextId}, + }; + } + if (methodCall.method == Constants.decideAsyncMethod) { + return { + Constants.responseSuccess: true, + Constants.responseResult: { + "flag_1": TestUtils.decideResponseMap, + "flag_2": TestUtils.decideResponseMap, + "flag_3": TestUtils.decideResponseMap, + }, + }; + } + return null; + }); + + var sdk = OptimizelyFlutterSdk(testSDKKey); + await sdk.initializeClient(); + var userContext = await sdk.createUserContext(userId: userId, attributes: attributes); + + var response = await userContext!.decideAllAsync(); + + expect(response.success, isTrue); + expect(response.decisions.length, equals(3)); + }); + }); + + group('CMAB DecideOptions', () { + test('ignoreCmabCache option is converted correctly', () { + final options = {OptimizelyDecideOption.ignoreCmabCache}; + final converted = Utils.convertDecideOptions(options); + + expect(converted, contains(OptimizelyDecideOption.ignoreCmabCache.name)); + }); + + test('resetCmabCache option is converted correctly', () { + final options = {OptimizelyDecideOption.resetCmabCache}; + final converted = Utils.convertDecideOptions(options); + + expect(converted, contains(OptimizelyDecideOption.resetCmabCache.name)); + }); + + test('invalidateUserCmabCache option is converted correctly', () { + final options = {OptimizelyDecideOption.invalidateUserCmabCache}; + final converted = Utils.convertDecideOptions(options); + + expect(converted, contains(OptimizelyDecideOption.invalidateUserCmabCache.name)); + }); + + test('multiple CMAB options are converted correctly', () { + final options = { + OptimizelyDecideOption.ignoreCmabCache, + OptimizelyDecideOption.resetCmabCache, + OptimizelyDecideOption.includeReasons, + }; + final converted = Utils.convertDecideOptions(options); + + expect(converted.length, equals(3)); + expect(converted, contains(OptimizelyDecideOption.ignoreCmabCache.name)); + expect(converted, contains(OptimizelyDecideOption.resetCmabCache.name)); + expect(converted, contains(OptimizelyDecideOption.includeReasons.name)); + }); + + test('CMAB options work with standard options', () { + final options = { + OptimizelyDecideOption.ignoreCmabCache, + OptimizelyDecideOption.disableDecisionEvent, + OptimizelyDecideOption.excludeVariables, + }; + final converted = Utils.convertDecideOptions(options); + + expect(converted.length, equals(3)); + expect(converted, contains(OptimizelyDecideOption.ignoreCmabCache.name)); + expect(converted, contains(OptimizelyDecideOption.disableDecisionEvent.name)); + expect(converted, contains(OptimizelyDecideOption.excludeVariables.name)); + }); + + test('all three CMAB cache options can be used together', () { + final options = { + OptimizelyDecideOption.ignoreCmabCache, + OptimizelyDecideOption.resetCmabCache, + OptimizelyDecideOption.invalidateUserCmabCache, + }; + final converted = Utils.convertDecideOptions(options); + + expect(converted.length, equals(3)); + expect(converted, contains(OptimizelyDecideOption.ignoreCmabCache.name)); + expect(converted, contains(OptimizelyDecideOption.resetCmabCache.name)); + expect(converted, contains(OptimizelyDecideOption.invalidateUserCmabCache.name)); + }); + }); + + group('CMAB Constants', () { + test('CMAB constants are defined correctly', () { + expect(Constants.cmabConfig, equals("cmabConfig")); + expect(Constants.cmabCacheSize, equals("cmabCacheSize")); + expect(Constants.cmabCacheTimeoutInSecs, equals("cmabCacheTimeoutInSecs")); + expect(Constants.cmabPredictionEndpoint, equals("cmabPredictionEndpoint")); + expect(Constants.decideAsyncMethod, equals("decideAsync")); + }); + }); +} \ No newline at end of file diff --git a/test/optimizely_flutter_sdk_test.dart b/test/optimizely_flutter_sdk_test.dart index 862c4b0..555af58 100644 --- a/test/optimizely_flutter_sdk_test.dart +++ b/test/optimizely_flutter_sdk_test.dart @@ -109,6 +109,14 @@ void main() { ); } + // To Check if CmabConfig was received + var cmabConfigMap = methodCall.arguments[Constants.cmabConfig]; + if (cmabConfigMap is Map) { + // CmabConfig received - validate it has expected keys + expect(cmabConfigMap.containsKey(Constants.cmabCacheSize), isTrue); + expect(cmabConfigMap.containsKey(Constants.cmabCacheTimeoutInSecs), isTrue); + } + // Resetting to default for every test datafileHostOptions = const DatafileHostOptions("", ""); if (methodCall.arguments[Constants.datafileHostPrefix] != null && @@ -311,6 +319,25 @@ void main() { Constants.responseSuccess: true, Constants.responseResult: result, }; + case Constants.decideAsyncMethod: + expect(methodCall.arguments[Constants.sdkKey], isNotEmpty); + expect(methodCall.arguments[Constants.userContextId], + equals(userContextId)); + var asyncKeys = List.from(methodCall.arguments[Constants.keys]); + decideOptions.addAll(List.from( + methodCall.arguments[Constants.optimizelyDecideOption])); + // for decideAllAsync + if (asyncKeys.isEmpty) { + asyncKeys = ["123", "456", "789"]; + } + Map asyncResult = {}; + for (final key in asyncKeys) { + asyncResult[key] = TestUtils.decideResponseMap; + } + return { + Constants.responseSuccess: true, + Constants.responseResult: asyncResult, + }; case Constants.setForcedDecision: expect(methodCall.arguments[Constants.sdkKey], isNotEmpty); expect(methodCall.arguments[Constants.userContextId], From 2ec0a3f62b21d797bc254191f6588258b766a1f5 Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Wed, 31 Dec 2025 11:05:45 +0600 Subject: [PATCH 7/7] fix cmab decide options for android --- .../optimizely_flutter_sdk/helper_classes/Constants.java | 3 +++ .../optimizely_flutter_sdk/helper_classes/Utils.java | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/Constants.java b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/Constants.java index 8d17d29..2dc4a42 100644 --- a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/Constants.java +++ b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/Constants.java @@ -157,6 +157,9 @@ public static class DecideOption { public static final String IGNORE_USER_PROFILE_SERVICE = "ignoreUserProfileService"; public static final String INCLUDE_REASONS = "includeReasons"; public static final String EXCLUDE_VARIABLES = "excludeVariables"; + public static final String IGNORE_CMAB_CACHE = "ignoreCmabCache"; + public static final String RESET_CMAB_CACHE = "resetCmabCache"; + public static final String INVALIDATE_USER_CMAB_CACHE = "invalidateUserCmabCache"; } public static class SegmentOption { diff --git a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/Utils.java b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/Utils.java index ba4e5a4..2269e21 100644 --- a/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/Utils.java +++ b/android/src/main/java/com/optimizely/optimizely_flutter_sdk/helper_classes/Utils.java @@ -65,6 +65,15 @@ public static List getDecideOptions(List options case Constants.DecideOption.INCLUDE_REASONS: convertedOptions.add(OptimizelyDecideOption.INCLUDE_REASONS); break; + case Constants.DecideOption.IGNORE_CMAB_CACHE: + convertedOptions.add(OptimizelyDecideOption.IGNORE_CMAB_CACHE); + break; + case Constants.DecideOption.RESET_CMAB_CACHE: + convertedOptions.add(OptimizelyDecideOption.RESET_CMAB_CACHE); + break; + case Constants.DecideOption.INVALIDATE_USER_CMAB_CACHE: + convertedOptions.add(OptimizelyDecideOption.INVALIDATE_USER_CMAB_CACHE); + break; default: break; }