Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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\Cmab.cs">
<Link>Entity\Cmab.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\Cmab.cs">
<Link>Entity\Cmab.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\Cmab.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\Cmab.cs">
<Link>Entity\Cmab.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/Cmab.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 Cmab
{
/// <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 Cmab class with specified values.
/// </summary>
/// <param name="attributeIds">List of attribute IDs for CMAB</param>
/// <param name="trafficAllocation">Traffic allocation value</param>
public Cmab(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("Cmab{{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 Cmab 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\Cmab.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