From 8de6a278160c2d53d0a9d137570b7aac7ff2050c Mon Sep 17 00:00:00 2001 From: Md Junaed Hossain <169046794+junaed-optimizely@users.noreply.github.com> Date: Fri, 19 Sep 2025 21:56:35 +0600 Subject: [PATCH 1/3] [FSSDK-11138] update project config --- .../OptimizelySDK.Net35.csproj | 3 + .../OptimizelySDK.Net40.csproj | 3 + .../OptimizelySDK.NetStandard16.csproj | 1 + .../OptimizelySDK.NetStandard20.csproj | 3 + OptimizelySDK.Tests/ProjectConfigTest.cs | 35 +++++++++++ OptimizelySDK/Config/DatafileProjectConfig.cs | 28 +++++++++ OptimizelySDK/Entity/CmabConfig.cs | 63 +++++++++++++++++++ OptimizelySDK/Entity/Experiment.cs | 7 +++ OptimizelySDK/OptimizelySDK.csproj | 1 + OptimizelySDK/ProjectConfig.cs | 12 ++++ OptimizelySDK/Utils/schema.json | 16 ++++- 11 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 OptimizelySDK/Entity/CmabConfig.cs diff --git a/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj b/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj index 303a742e..1cf0a929 100644 --- a/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj +++ b/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj @@ -88,6 +88,9 @@ Entity\Experiment.cs + + Entity\CmabConfig.cs + Entity\Holdout.cs diff --git a/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj b/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj index 3e0a9ea5..835895a2 100644 --- a/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj +++ b/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj @@ -90,6 +90,9 @@ Entity\Experiment.cs + + Entity\CmabConfig.cs + Entity\Holdout.cs diff --git a/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj b/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj index ab55bbac..6949973c 100644 --- a/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj +++ b/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj @@ -26,6 +26,7 @@ + diff --git a/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj b/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj index 12f9cb55..0ec76a6c 100644 --- a/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj +++ b/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj @@ -178,6 +178,9 @@ Entity\Experiment.cs + + Entity\CmabConfig.cs + Entity\Holdout.cs diff --git a/OptimizelySDK.Tests/ProjectConfigTest.cs b/OptimizelySDK.Tests/ProjectConfigTest.cs index 15e95926..52373b2d 100644 --- a/OptimizelySDK.Tests/ProjectConfigTest.cs +++ b/OptimizelySDK.Tests/ProjectConfigTest.cs @@ -1455,5 +1455,40 @@ public void TestMissingHoldoutsField_BackwardCompatibility() } #endregion + + [Test] + public void TestCmabFieldPopulation() + { + + var datafileJson = JObject.Parse(TestData.Datafile); + var experiments = (JArray)datafileJson["experiments"]; + + if (experiments.Count > 0) + { + var firstExperiment = (JObject)experiments[0]; + + firstExperiment["cmab"] = new JObject + { + ["attributeIds"] = new JArray { "7723280020", "7723348204" }, + ["trafficAllocation"] = 4000 + }; + + firstExperiment["trafficAllocation"] = new JArray(); + } + + var modifiedDatafile = datafileJson.ToString(); + var projectConfig = DatafileProjectConfig.Create(modifiedDatafile, LoggerMock.Object, ErrorHandlerMock.Object); + var experimentWithCmab = projectConfig.GetExperimentFromKey("test_experiment"); + + Assert.IsNotNull(experimentWithCmab.Cmab); + Assert.AreEqual(2, experimentWithCmab.Cmab.AttributeIds.Count); + Assert.Contains("7723280020", experimentWithCmab.Cmab.AttributeIds); + Assert.Contains("7723348204", experimentWithCmab.Cmab.AttributeIds); + Assert.AreEqual(4000, experimentWithCmab.Cmab.TrafficAllocation); + + var experimentWithoutCmab = projectConfig.GetExperimentFromKey("paused_experiment"); + + Assert.IsNull(experimentWithoutCmab.Cmab); + } } } diff --git a/OptimizelySDK/Config/DatafileProjectConfig.cs b/OptimizelySDK/Config/DatafileProjectConfig.cs index e701bc4e..52593e78 100644 --- a/OptimizelySDK/Config/DatafileProjectConfig.cs +++ b/OptimizelySDK/Config/DatafileProjectConfig.cs @@ -195,6 +195,13 @@ private Dictionary> _VariationIdMap public Dictionary AttributeKeyMap => _AttributeKeyMap; + /// + /// Associative array of attribute ID to Attribute(s) in the datafile + /// + private Dictionary _AttributeIdMap; + + public Dictionary AttributeIdMap => _AttributeIdMap; + /// /// Associative array of audience ID to Audience(s) in the datafile /// @@ -332,6 +339,8 @@ private void Initialize() true); _AttributeKeyMap = ConfigParser.GenerateMap(Attributes, a => a.Key, true); + _AttributeIdMap = ConfigParser.GenerateMap(Attributes, + a => a.Id, true); _AudienceIdMap = ConfigParser.GenerateMap(Audiences, a => a.Id.ToString(), true); _FeatureKeyMap = ConfigParser.GenerateMap(FeatureFlags, @@ -653,6 +662,25 @@ public Attribute GetAttribute(string attributeKey) return new Attribute(); } + /// + /// Get the Attribute from the ID + /// + /// ID of the Attribute + /// Attribute Entity corresponding to the ID or a dummy entity if ID is invalid + public Attribute GetAttributeById(string attributeId) + { + if (_AttributeIdMap.ContainsKey(attributeId)) + { + return _AttributeIdMap[attributeId]; + } + + var message = $@"Attribute ID ""{attributeId}"" is not in datafile."; + Logger.Log(LogLevel.ERROR, message); + ErrorHandler.HandleError( + new InvalidAttributeException("Provided attribute is not in datafile.")); + return new Attribute(); + } + /// /// Get the Variation from the keys /// diff --git a/OptimizelySDK/Entity/CmabConfig.cs b/OptimizelySDK/Entity/CmabConfig.cs new file mode 100644 index 00000000..5615ed6a --- /dev/null +++ b/OptimizelySDK/Entity/CmabConfig.cs @@ -0,0 +1,63 @@ +/* + * Copyright 2025, Optimizely + * + * 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. + */ + +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace OptimizelySDK.Entity +{ + /// + /// Class representing CMAB (Contextual Multi-Armed Bandit) configuration for experiments. + /// + public class CmabConfig + { + /// + /// List of attribute IDs that are relevant for CMAB decision making. + /// These attributes will be used to filter user attributes when making CMAB requests. + /// + [JsonProperty("attributeIds")] + public List AttributeIds { get; set; } + + /// + /// Traffic allocation value for CMAB experiments. + /// Determines what portion of traffic should be allocated to CMAB decision making. + /// + [JsonProperty("trafficAllocation")] + public int? TrafficAllocation { get; set; } + + /// + /// Initializes a new instance of the CmabConfig class with specified values. + /// + /// List of attribute IDs for CMAB + /// Traffic allocation value + public CmabConfig(List attributeIds, int? trafficAllocation = null) + { + AttributeIds = attributeIds ?? new List(); + TrafficAllocation = trafficAllocation; + } + + /// + /// Returns a string representation of the CMAB configuration. + /// + /// String representation + public override string ToString() + { + var attributeList = AttributeIds ?? new List(); + return string.Format("CmabDict{{AttributeIds=[{0}], TrafficAllocation={1}}}", + string.Join(", ", attributeList.ToArray()), TrafficAllocation); + } + } +} diff --git a/OptimizelySDK/Entity/Experiment.cs b/OptimizelySDK/Entity/Experiment.cs index 8a7b4036..0ca5993a 100644 --- a/OptimizelySDK/Entity/Experiment.cs +++ b/OptimizelySDK/Entity/Experiment.cs @@ -15,6 +15,7 @@ */ using System.Collections.Generic; +using Newtonsoft.Json; namespace OptimizelySDK.Entity { @@ -48,6 +49,12 @@ public class Experiment : ExperimentCore public bool IsInMutexGroup => !string.IsNullOrEmpty(GroupPolicy) && GroupPolicy == MUTEX_GROUP_POLICY; + /// + /// CMAB (Contextual Multi-Armed Bandit) configuration for the experiment. + /// + [JsonProperty("cmab")] + public CmabConfig Cmab { get; set; } + /// /// Determin if user is forced variation of experiment /// diff --git a/OptimizelySDK/OptimizelySDK.csproj b/OptimizelySDK/OptimizelySDK.csproj index 14201b4e..946b4a0d 100644 --- a/OptimizelySDK/OptimizelySDK.csproj +++ b/OptimizelySDK/OptimizelySDK.csproj @@ -84,6 +84,7 @@ + diff --git a/OptimizelySDK/ProjectConfig.cs b/OptimizelySDK/ProjectConfig.cs index 992c900b..28f63d24 100644 --- a/OptimizelySDK/ProjectConfig.cs +++ b/OptimizelySDK/ProjectConfig.cs @@ -113,6 +113,11 @@ public interface ProjectConfig /// Dictionary AttributeKeyMap { get; } + /// + /// Associative array of attribute ID to Attribute(s) in the datafile + /// + Dictionary AttributeIdMap { get; } + /// /// Associative array of audience ID to Audience(s) in the datafile /// @@ -234,6 +239,13 @@ public interface ProjectConfig /// Attribute Entity corresponding to the key or a dummy entity if key is invalid Attribute GetAttribute(string attributeKey); + /// + /// Get the Attribute from the ID + /// + /// ID of the Attribute + /// Attribute Entity corresponding to the ID or a dummy entity if ID is invalid + Attribute GetAttributeById(string attributeId); + /// /// Get the Variation from the keys /// diff --git a/OptimizelySDK/Utils/schema.json b/OptimizelySDK/Utils/schema.json index c434bbac..dd3fedef 100644 --- a/OptimizelySDK/Utils/schema.json +++ b/OptimizelySDK/Utils/schema.json @@ -182,6 +182,20 @@ }, "forcedVariations": { "type": "object" + }, + "cmab": { + "type": "object", + "properties": { + "attributeIds": { + "type": "array", + "items": { + "type": "string" + } + }, + "trafficAllocation": { + "type": "integer" + } + } } }, "required": [ @@ -279,4 +293,4 @@ "version", "revision" ] -} \ No newline at end of file +} From 54e13fabe331b92e143e54ab4e930d443745ad06 Mon Sep 17 00:00:00 2001 From: Md Junaed Hossain <169046794+junaed-optimizely@users.noreply.github.com> Date: Sat, 20 Sep 2025 00:21:38 +0600 Subject: [PATCH 2/3] [FSSDK-11138] config parser update --- OptimizelySDK/Utils/ConfigParser.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/OptimizelySDK/Utils/ConfigParser.cs b/OptimizelySDK/Utils/ConfigParser.cs index 318ff1b6..c6b86916 100644 --- a/OptimizelySDK/Utils/ConfigParser.cs +++ b/OptimizelySDK/Utils/ConfigParser.cs @@ -33,7 +33,13 @@ public static Dictionary GenerateMap(IEnumerable entities, Func getKey, bool clone ) { - return entities.ToDictionary(e => getKey(e), e => clone ? (T)e.Clone() : e); + var dictionary = new Dictionary(); + foreach (var entity in entities) + { + var key = getKey(entity); + dictionary[key] = clone ? (T)entity.Clone() : entity; + } + return dictionary; } } } From 63cd446ac54782bf20c110ce09fb90032eac17f4 Mon Sep 17 00:00:00 2001 From: Md Junaed Hossain <169046794+junaed-optimizely@users.noreply.github.com> Date: Mon, 22 Sep 2025 20:42:37 +0600 Subject: [PATCH 3/3] [FSSDK-11138] review update --- OptimizelySDK.Net35/OptimizelySDK.Net35.csproj | 4 ++-- OptimizelySDK.Net40/OptimizelySDK.Net40.csproj | 4 ++-- .../OptimizelySDK.NetStandard16.csproj | 2 +- .../OptimizelySDK.NetStandard20.csproj | 4 ++-- OptimizelySDK/Entity/{CmabConfig.cs => Cmab.cs} | 8 ++++---- OptimizelySDK/Entity/Experiment.cs | 2 +- OptimizelySDK/OptimizelySDK.csproj | 2 +- 7 files changed, 13 insertions(+), 13 deletions(-) rename OptimizelySDK/Entity/{CmabConfig.cs => Cmab.cs} (88%) diff --git a/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj b/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj index 1cf0a929..e441df53 100644 --- a/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj +++ b/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj @@ -88,8 +88,8 @@ Entity\Experiment.cs - - Entity\CmabConfig.cs + + Entity\Cmab.cs Entity\Holdout.cs diff --git a/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj b/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj index 835895a2..c1150280 100644 --- a/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj +++ b/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj @@ -90,8 +90,8 @@ Entity\Experiment.cs - - Entity\CmabConfig.cs + + Entity\Cmab.cs Entity\Holdout.cs diff --git a/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj b/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj index 6949973c..1490ba14 100644 --- a/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj +++ b/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj @@ -26,7 +26,7 @@ - + diff --git a/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj b/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj index 0ec76a6c..418f606d 100644 --- a/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj +++ b/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj @@ -178,8 +178,8 @@ Entity\Experiment.cs - - Entity\CmabConfig.cs + + Entity\Cmab.cs Entity\Holdout.cs diff --git a/OptimizelySDK/Entity/CmabConfig.cs b/OptimizelySDK/Entity/Cmab.cs similarity index 88% rename from OptimizelySDK/Entity/CmabConfig.cs rename to OptimizelySDK/Entity/Cmab.cs index 5615ed6a..f8caec87 100644 --- a/OptimizelySDK/Entity/CmabConfig.cs +++ b/OptimizelySDK/Entity/Cmab.cs @@ -22,7 +22,7 @@ namespace OptimizelySDK.Entity /// /// Class representing CMAB (Contextual Multi-Armed Bandit) configuration for experiments. /// - public class CmabConfig + public class Cmab { /// /// List of attribute IDs that are relevant for CMAB decision making. @@ -39,11 +39,11 @@ public class CmabConfig public int? TrafficAllocation { get; set; } /// - /// Initializes a new instance of the CmabConfig class with specified values. + /// Initializes a new instance of the Cmab class with specified values. /// /// List of attribute IDs for CMAB /// Traffic allocation value - public CmabConfig(List attributeIds, int? trafficAllocation = null) + public Cmab(List attributeIds, int? trafficAllocation = null) { AttributeIds = attributeIds ?? new List(); TrafficAllocation = trafficAllocation; @@ -56,7 +56,7 @@ public CmabConfig(List attributeIds, int? trafficAllocation = null) public override string ToString() { var attributeList = AttributeIds ?? new List(); - return string.Format("CmabDict{{AttributeIds=[{0}], TrafficAllocation={1}}}", + return string.Format("Cmab{{AttributeIds=[{0}], TrafficAllocation={1}}}", string.Join(", ", attributeList.ToArray()), TrafficAllocation); } } diff --git a/OptimizelySDK/Entity/Experiment.cs b/OptimizelySDK/Entity/Experiment.cs index 0ca5993a..52e99015 100644 --- a/OptimizelySDK/Entity/Experiment.cs +++ b/OptimizelySDK/Entity/Experiment.cs @@ -53,7 +53,7 @@ public class Experiment : ExperimentCore /// CMAB (Contextual Multi-Armed Bandit) configuration for the experiment. /// [JsonProperty("cmab")] - public CmabConfig Cmab { get; set; } + public Cmab Cmab { get; set; } /// /// Determin if user is forced variation of experiment diff --git a/OptimizelySDK/OptimizelySDK.csproj b/OptimizelySDK/OptimizelySDK.csproj index 946b4a0d..b4537b92 100644 --- a/OptimizelySDK/OptimizelySDK.csproj +++ b/OptimizelySDK/OptimizelySDK.csproj @@ -84,7 +84,7 @@ - +