Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions OptimizelySDK.Net35/OptimizelySDK.Net35.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@
<Compile Include="..\OptimizelySDK\Entity\Experiment.cs">
<Link>Entity\Experiment.cs</Link>
</Compile>
<Compile Include="..\OptimizelySDK\Entity\CmabConfig.cs">
<Link>Entity\CmabConfig.cs</Link>
</Compile>
<Compile Include="..\OptimizelySDK\Entity\Holdout.cs">
<Link>Entity\Holdout.cs</Link>
</Compile>
Expand Down
3 changes: 3 additions & 0 deletions OptimizelySDK.Net40/OptimizelySDK.Net40.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@
<Compile Include="..\OptimizelySDK\Entity\Experiment.cs">
<Link>Entity\Experiment.cs</Link>
</Compile>
<Compile Include="..\OptimizelySDK\Entity\CmabConfig.cs">
<Link>Entity\CmabConfig.cs</Link>
</Compile>
<Compile Include="..\OptimizelySDK\Entity\Holdout.cs">
<Link>Entity\Holdout.cs</Link>
</Compile>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
<Compile Include="..\OptimizelySDK\Entity\Event.cs" />
<Compile Include="..\OptimizelySDK\Entity\EventTags.cs" />
<Compile Include="..\OptimizelySDK\Entity\Experiment.cs" />
<Compile Include="..\OptimizelySDK\Entity\CmabConfig.cs" />
<Compile Include="..\OptimizelySDK\Entity\Holdout.cs" />
<Compile Include="..\OptimizelySDK\Entity\ExperimentCore.cs" />
<Compile Include="..\OptimizelySDK\Entity\FeatureDecision.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,9 @@
<Compile Include="..\OptimizelySDK\Entity\Experiment.cs">
<Link>Entity\Experiment.cs</Link>
</Compile>
<Compile Include="..\OptimizelySDK\Entity\CmabConfig.cs">
<Link>Entity\CmabConfig.cs</Link>
</Compile>
<Compile Include="..\OptimizelySDK\Entity\Holdout.cs">
<Link>Entity\Holdout.cs</Link>
</Compile>
Expand Down
35 changes: 35 additions & 0 deletions OptimizelySDK.Tests/ProjectConfigTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
28 changes: 28 additions & 0 deletions OptimizelySDK/Config/DatafileProjectConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,13 @@ private Dictionary<string, Dictionary<string, Variation>> _VariationIdMap

public Dictionary<string, Attribute> AttributeKeyMap => _AttributeKeyMap;

/// <summary>
/// Associative array of attribute ID to Attribute(s) in the datafile
/// </summary>
private Dictionary<string, Attribute> _AttributeIdMap;

public Dictionary<string, Attribute> AttributeIdMap => _AttributeIdMap;

/// <summary>
/// Associative array of audience ID to Audience(s) in the datafile
/// </summary>
Expand Down Expand Up @@ -332,6 +339,8 @@ private void Initialize()
true);
_AttributeKeyMap = ConfigParser<Attribute>.GenerateMap(Attributes,
a => a.Key, true);
_AttributeIdMap = ConfigParser<Attribute>.GenerateMap(Attributes,
a => a.Id, true);
_AudienceIdMap = ConfigParser<Audience>.GenerateMap(Audiences,
a => a.Id.ToString(), true);
_FeatureKeyMap = ConfigParser<FeatureFlag>.GenerateMap(FeatureFlags,
Expand Down Expand Up @@ -653,6 +662,25 @@ public Attribute GetAttribute(string attributeKey)
return new Attribute();
}

/// <summary>
/// Get the Attribute from the ID
/// </summary>
/// <param name="attributeId">ID of the Attribute</param>
/// <returns>Attribute Entity corresponding to the ID or a dummy entity if ID is invalid</returns>
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();
}

/// <summary>
/// Get the Variation from the keys
/// </summary>
Expand Down
63 changes: 63 additions & 0 deletions OptimizelySDK/Entity/CmabConfig.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Class representing CMAB (Contextual Multi-Armed Bandit) configuration for experiments.
/// </summary>
public class CmabConfig
{
/// <summary>
/// List of attribute IDs that are relevant for CMAB decision making.
/// These attributes will be used to filter user attributes when making CMAB requests.
/// </summary>
[JsonProperty("attributeIds")]
public List<string> AttributeIds { get; set; }

/// <summary>
/// Traffic allocation value for CMAB experiments.
/// Determines what portion of traffic should be allocated to CMAB decision making.
/// </summary>
[JsonProperty("trafficAllocation")]
public int? TrafficAllocation { get; set; }

/// <summary>
/// Initializes a new instance of the CmabConfig class with specified values.
/// </summary>
/// <param name="attributeIds">List of attribute IDs for CMAB</param>
/// <param name="trafficAllocation">Traffic allocation value</param>
public CmabConfig(List<string> attributeIds, int? trafficAllocation = null)
{
AttributeIds = attributeIds ?? new List<string>();
TrafficAllocation = trafficAllocation;
}

/// <summary>
/// Returns a string representation of the CMAB configuration.
/// </summary>
/// <returns>String representation</returns>
public override string ToString()
{
var attributeList = AttributeIds ?? new List<string>();
return string.Format("CmabDict{{AttributeIds=[{0}], TrafficAllocation={1}}}",
string.Join(", ", attributeList.ToArray()), TrafficAllocation);
}
}
}
7 changes: 7 additions & 0 deletions OptimizelySDK/Entity/Experiment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

using System.Collections.Generic;
using Newtonsoft.Json;

namespace OptimizelySDK.Entity
{
Expand Down Expand Up @@ -48,6 +49,12 @@ public class Experiment : ExperimentCore
public bool IsInMutexGroup =>
!string.IsNullOrEmpty(GroupPolicy) && GroupPolicy == MUTEX_GROUP_POLICY;

/// <summary>
/// CMAB (Contextual Multi-Armed Bandit) configuration for the experiment.
/// </summary>
[JsonProperty("cmab")]
public CmabConfig Cmab { get; set; }

/// <summary>
/// Determin if user is forced variation of experiment
/// </summary>
Expand Down
1 change: 1 addition & 0 deletions OptimizelySDK/OptimizelySDK.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
<Compile Include="Entity\Event.cs"/>
<Compile Include="Entity\EventTags.cs"/>
<Compile Include="Entity\Experiment.cs"/>
<Compile Include="Entity\CmabConfig.cs"/>
<Compile Include="Entity\ExperimentCore.cs"/>
<Compile Include="Entity\Holdout.cs"/>
<Compile Include="Entity\FeatureDecision.cs"/>
Expand Down
12 changes: 12 additions & 0 deletions OptimizelySDK/ProjectConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,11 @@ public interface ProjectConfig
/// </summary>
Dictionary<string, Attribute> AttributeKeyMap { get; }

/// <summary>
/// Associative array of attribute ID to Attribute(s) in the datafile
/// </summary>
Dictionary<string, Attribute> AttributeIdMap { get; }

/// <summary>
/// Associative array of audience ID to Audience(s) in the datafile
/// </summary>
Expand Down Expand Up @@ -234,6 +239,13 @@ public interface ProjectConfig
/// <returns>Attribute Entity corresponding to the key or a dummy entity if key is invalid</returns>
Attribute GetAttribute(string attributeKey);

/// <summary>
/// Get the Attribute from the ID
/// </summary>
/// <param name="attributeId">ID of the Attribute</param>
/// <returns>Attribute Entity corresponding to the ID or a dummy entity if ID is invalid</returns>
Attribute GetAttributeById(string attributeId);

/// <summary>
/// Get the Variation from the keys
/// </summary>
Expand Down
8 changes: 7 additions & 1 deletion OptimizelySDK/Utils/ConfigParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,13 @@ public static Dictionary<string, T> GenerateMap(IEnumerable<T> entities,
Func<T, string> getKey, bool clone
)
{
return entities.ToDictionary(e => getKey(e), e => clone ? (T)e.Clone() : e);
var dictionary = new Dictionary<string, T>();
foreach (var entity in entities)
{
var key = getKey(entity);
dictionary[key] = clone ? (T)entity.Clone() : entity;
}
return dictionary;
}
}
}
16 changes: 15 additions & 1 deletion OptimizelySDK/Utils/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,20 @@
},
"forcedVariations": {
"type": "object"
},
"cmab": {
"type": "object",
"properties": {
"attributeIds": {
"type": "array",
"items": {
"type": "string"
}
},
"trafficAllocation": {
"type": "integer"
}
}
}
},
"required": [
Expand Down Expand Up @@ -279,4 +293,4 @@
"version",
"revision"
]
}
}
Loading