diff --git a/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj b/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj index 08dd7398..8dc64e4e 100644 --- a/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj +++ b/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj @@ -284,9 +284,18 @@ OptlyConfig\OptimizelyConfig.cs + + OptlyConfig\OptimizelyAttribute.cs + + + OptlyConfig\OptimizelyEvent.cs + OptlyConfig\OptimizelyExperiment.cs + + OptlyConfig\OptimizelyAudience.cs + OptlyConfig\OptimizelyFeature.cs diff --git a/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj b/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj index 9ceeddcf..e5c64c6c 100644 --- a/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj +++ b/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj @@ -300,6 +300,12 @@ OptlyConfig\OptimizelyConfig.cs + + + OptlyConfig\OptimizelyAttribute.cs + + + OptlyConfig\OptimizelyEvent.cs OptlyConfig\OptimizelyExperiment.cs @@ -312,6 +318,9 @@ OptlyConfig\OptimizelyVariation.cs + + + OptlyConfig\OptimizelyAudience.cs OptlyConfig\OptimizelyConfigService.cs diff --git a/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj b/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj index 67487cdc..7d5e7c81 100644 --- a/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj +++ b/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj @@ -82,10 +82,13 @@ + + + diff --git a/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj b/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj index b64a9c2b..b43a21cb 100644 --- a/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj +++ b/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj @@ -258,9 +258,15 @@ Event\EventProcessor.cs + + + OptlyConfig\OptimizelyAttribute.cs OptlyConfig\OptimizelyConfig.cs + + + OptlyConfig\OptimizelyEvent.cs OptlyConfig\OptimizelyExperiment.cs @@ -270,6 +276,9 @@ OptlyConfig\OptimizelyVariable.cs + + + OptlyConfig\OptimizelyAudience.cs OptlyConfig\OptimizelyVariation.cs diff --git a/OptimizelySDK.Tests/OptimizelyConfigTests/OptimizelyConfigTest.cs b/OptimizelySDK.Tests/OptimizelyConfigTests/OptimizelyConfigTest.cs index c393cc7f..51025bf2 100644 --- a/OptimizelySDK.Tests/OptimizelyConfigTests/OptimizelyConfigTest.cs +++ b/OptimizelySDK.Tests/OptimizelyConfigTests/OptimizelyConfigTest.cs @@ -22,6 +22,8 @@ using OptimizelySDK.OptlyConfig; using System.Collections.Generic; using System.Threading; +using OptimizelySDK.Tests.UtilsTests; +using Newtonsoft.Json.Linq; namespace OptimizelySDK.Tests.OptimizelyConfigTests { @@ -40,6 +42,67 @@ public void Setup() #region Test OptimizelyConfigService + static Type[] ParameterTypes = { + typeof(ProjectConfig), + }; + + private PrivateObject CreatePrivateOptimizelyConfigService(ProjectConfig projectConfig) + { + return new PrivateObject(typeof(OptimizelyConfigService), ParameterTypes, + new object[] + { + projectConfig + }); + } + + [Test] + public void TestGetOptimizelyConfigServiceSerializedAudiences() + { + var datafileProjectConfig = DatafileProjectConfig.Create(TestData.TypedAudienceDatafile, new NoOpLogger(), new ErrorHandler.NoOpErrorHandler()); + var optlyConfigService = CreatePrivateOptimizelyConfigService(datafileProjectConfig); + + var audienceConditions = new List> + { + new List() { "or", "3468206642", "3988293898" }, + new List() { "or", "3468206642", "3988293898", "3468206646" }, + new List() { "not", "3468206642" }, + new List() { "or", "3468206642" }, + new List() { "and", "3468206642" }, + new List() { "3468206642" }, + new List() { "3468206642", "3988293898" }, + new List() { "and", new JArray() { "or", "3468206642", "3988293898" }, "3468206646" }, + new List() { "and", new JArray() { "or", "3468206642", new JArray() { "and", "3988293898", "3468206646" } }, new JArray() { "and", "3988293899", new JArray() { "or", "3468206647", "3468206643" } } }, + new List() { "and", "and" }, + new List() { "not", new JArray() { "and", "3468206642", "3988293898" } }, + new List() { }, + new List() { "or", "3468206642", "999999999" }, + + }; + + var expectedAudienceOutputs = new List + { + "\"exactString\" OR \"substringString\"", + "\"exactString\" OR \"substringString\" OR \"exactNumber\"", + "NOT \"exactString\"", + "\"exactString\"", + "\"exactString\"", + "\"exactString\"", + "\"exactString\" OR \"substringString\"", + "(\"exactString\" OR \"substringString\") AND \"exactNumber\"", + "(\"exactString\" OR (\"substringString\" AND \"exactNumber\")) AND (\"exists\" AND (\"gtNumber\" OR \"exactBoolean\"))", + "", + "NOT (\"exactString\" AND \"substringString\")", + "", + "\"exactString\" OR \"999999999\"", + }; + + for (int testNo = 0; testNo < audienceConditions.Count; testNo++) + { + var result = (string)optlyConfigService.Invoke("GetSerializedAudiences", audienceConditions[testNo], datafileProjectConfig.AudienceIdMap); + Assert.AreEqual(result, expectedAudienceOutputs[testNo]); + } + } + [Test] public void TestAfterDisposeGetOptimizelyConfigIsNoLongerValid() { @@ -66,76 +129,56 @@ public void TestAfterDisposeGetOptimizelyConfigIsNoLongerValid() } [Test] - public void TestPollingGivenOnlySdkKeyGetOptimizelyConfig() + public void TestGetOptimizelyConfigServiceNullConfig() { - HttpProjectConfigManager httpManager = new HttpProjectConfigManager.Builder() - .WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z") - .WithLogger(LoggerMock.Object) - .WithPollingInterval(TimeSpan.FromMilliseconds(1000)) - .WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(500)) - .WithStartByDefault() - .Build(true); - - Assert.NotNull(httpManager.GetConfig()); - - var optimizely = new Optimizely(httpManager); - - var optimizelyConfig = optimizely.GetOptimizelyConfig(); - - Assert.NotNull(optimizelyConfig); - Assert.NotNull(optimizelyConfig.ExperimentsMap); - Assert.NotNull(optimizelyConfig.FeaturesMap); - Assert.NotNull(optimizelyConfig.Revision); - - optimizely.Dispose(); - - var optimizelyConfigAfterDispose = optimizely.GetOptimizelyConfig(); - Assert.Null(optimizelyConfigAfterDispose); + OptimizelyConfig optimizelyConfig = new OptimizelyConfigService(null).GetOptimizelyConfig(); + Assert.IsNull(optimizelyConfig); } [Test] - public void TestPollingMultipleTimesGetOptimizelyConfig() + public void TestGetOptimizelyConfigWithDuplicateExperimentKeys() { - HttpProjectConfigManager httpManager = new HttpProjectConfigManager.Builder() - .WithSdkKey("QBw9gFM8oTn7ogY9ANCC1z") - .WithLogger(LoggerMock.Object) - .WithPollingInterval(TimeSpan.FromMilliseconds(100)) - .WithBlockingTimeoutPeriod(TimeSpan.FromMilliseconds(500)) - .WithStartByDefault() - .Build(true); + var datafileProjectConfig = DatafileProjectConfig.Create(TestData.DuplicateExpKeysDatafile, new NoOpLogger(), new ErrorHandler.NoOpErrorHandler()); + var optimizelyConfigService = new OptimizelyConfigService(datafileProjectConfig); + var optimizelyConfig = optimizelyConfigService.GetOptimizelyConfig(); + Assert.AreEqual(optimizelyConfig.ExperimentsMap.Count, 1); - Assert.NotNull(httpManager.GetConfig()); - - var optimizely = new Optimizely(httpManager); - - var optimizelyConfig = optimizely.GetOptimizelyConfig(); - - Assert.NotNull(optimizelyConfig); - Assert.NotNull(optimizelyConfig.ExperimentsMap); - Assert.NotNull(optimizelyConfig.FeaturesMap); - Assert.NotNull(optimizelyConfig.Revision); - - Thread.Sleep(210); - - optimizelyConfig = optimizely.GetOptimizelyConfig(); - - Assert.NotNull(optimizelyConfig); - Assert.NotNull(optimizelyConfig.ExperimentsMap); - Assert.NotNull(optimizelyConfig.FeaturesMap); - Assert.NotNull(optimizelyConfig.Revision); + var experimentMapFlag1 = optimizelyConfig.FeaturesMap["flag1"].ExperimentsMap; //9300000007569 + var experimentMapFlag2 = optimizelyConfig.FeaturesMap["flag2"].ExperimentsMap; // 9300000007573 + Assert.AreEqual(experimentMapFlag1["targeted_delivery"].Id, "9300000007569"); + Assert.AreEqual(experimentMapFlag2["targeted_delivery"].Id, "9300000007573"); + } - optimizely.Dispose(); + [Test] + public void TestGetOptimizelyConfigWithDuplicateRuleKeys() + { + var datafileProjectConfig = DatafileProjectConfig.Create(TestData.DuplicateRuleKeysDatafile, new NoOpLogger(), new ErrorHandler.NoOpErrorHandler()); + var optimizelyConfigService = new OptimizelyConfigService(datafileProjectConfig); + var optimizelyConfig = optimizelyConfigService.GetOptimizelyConfig(); + Assert.AreEqual(optimizelyConfig.ExperimentsMap.Count, 0); + + var rolloutFlag1 = optimizelyConfig.FeaturesMap["flag_1"].DeliveryRules[0]; // 9300000004977, + var rolloutFlag2 = optimizelyConfig.FeaturesMap["flag_2"].DeliveryRules[0]; // 9300000004979 + var rolloutFlag3 = optimizelyConfig.FeaturesMap["flag_3"].DeliveryRules[0]; // 9300000004981 + Assert.AreEqual(rolloutFlag1.Id, "9300000004977"); + Assert.AreEqual(rolloutFlag1.Key, "targeted_delivery"); + Assert.AreEqual(rolloutFlag2.Id, "9300000004979"); + Assert.AreEqual(rolloutFlag2.Key, "targeted_delivery"); + Assert.AreEqual(rolloutFlag3.Id, "9300000004981"); + Assert.AreEqual(rolloutFlag3.Key, "targeted_delivery"); - var optimizelyConfigAfterDispose = optimizely.GetOptimizelyConfig(); - Assert.Null(optimizelyConfigAfterDispose); } [Test] - public void TestGetOptimizelyConfigServiceNullConfig() + public void TestGetOptimizelyConfigSDKAndEnvironmentKeyDefault() { - OptimizelyConfig optimizelyConfig = new OptimizelyConfigService(null).GetOptimizelyConfig(); - Assert.IsNull(optimizelyConfig); + var datafileProjectConfig = DatafileProjectConfig.Create(TestData.DuplicateRuleKeysDatafile, new NoOpLogger(), new ErrorHandler.NoOpErrorHandler()); + var optimizelyConfigService = new OptimizelyConfigService(datafileProjectConfig); + var optimizelyConfig = optimizelyConfigService.GetOptimizelyConfig(); + + Assert.AreEqual(optimizelyConfig.SDKKey, ""); + Assert.AreEqual(optimizelyConfig.EnvironmentKey, ""); } [Test] @@ -148,6 +191,7 @@ public void TestGetOptimizelyConfigService() "feat_with_var_test", new OptimizelyExperiment ( id: "11564051718", key:"feat_with_var_test", + audiences: "", variationsMap: new Dictionary { { @@ -173,6 +217,7 @@ public void TestGetOptimizelyConfigService() "typed_audience_experiment", new OptimizelyExperiment ( id: "1323241597", key:"typed_audience_experiment", + audiences: "", variationsMap: new Dictionary { { @@ -189,6 +234,7 @@ public void TestGetOptimizelyConfigService() "audience_combinations_experiment", new OptimizelyExperiment ( id: "1323241598", key:"audience_combinations_experiment", + audiences: "(\"exactString\" OR \"substringString\") AND (\"exists\" OR \"exactNumber\" OR \"gtNumber\" OR \"ltNumber\" OR \"exactBoolean\")", variationsMap: new Dictionary { { @@ -205,6 +251,7 @@ public void TestGetOptimizelyConfigService() "feat2_with_var_test", new OptimizelyExperiment( id: "1323241599", key:"feat2_with_var_test", + audiences: "(\"exactString\" OR \"substringString\") AND (\"exists\" OR \"exactNumber\" OR \"gtNumber\" OR \"ltNumber\" OR \"exactBoolean\")", variationsMap: new Dictionary { { @@ -235,6 +282,22 @@ public void TestGetOptimizelyConfigService() "feat_no_vars", new OptimizelyFeature ( id: "11477755619", key: "feat_no_vars", + experimentRules: new List(), + deliveryRules: new List() { new OptimizelyExperiment( + id: "11488548027", + key:"feat_no_vars_rule", + audiences: "", + variationsMap: new Dictionary + { + { + "11557362669", new OptimizelyVariation ( + id: "11557362669", + key: "11557362669", + featureEnabled: true, + variablesMap: new Dictionary()) + } + } + ) }, experimentsMap: new Dictionary(), variablesMap: new Dictionary()) }, @@ -242,12 +305,57 @@ public void TestGetOptimizelyConfigService() "feat_with_var", new OptimizelyFeature ( id: "11567102051", key: "feat_with_var", + experimentRules: new List() { + new OptimizelyExperiment( + id: "11564051718", + key:"feat_with_var_test", + audiences: "", + variationsMap: new Dictionary + { + { + "variation_2", new OptimizelyVariation ( + id: "11617170975", + key: "variation_2", + featureEnabled: true, + variablesMap: new Dictionary + { + { + "x" , new OptimizelyVariable ( + id: "11535264366", + key: "x", + type: "string", + value: "xyz") + } + }) + } + } + ) + }, + deliveryRules: new List() { new OptimizelyExperiment( + id: "11630490911", + key:"feat_with_var_rule", + audiences: "", + variationsMap: new Dictionary + { + { + "11475708558", new OptimizelyVariation ( + id: "11475708558", + key: "11475708558", + featureEnabled: false, + variablesMap: new Dictionary() + { + { "x" , new OptimizelyVariable("11535264366", "x", "string", "x") } + }) + } + } + ) }, experimentsMap: new Dictionary { { "feat_with_var_test", new OptimizelyExperiment( id: "11564051718", key:"feat_with_var_test", + audiences: "", variationsMap: new Dictionary { { @@ -281,6 +389,23 @@ public void TestGetOptimizelyConfigService() "feat2", new OptimizelyFeature ( id: "11567102052", key: "feat2", + deliveryRules: new List() { new OptimizelyExperiment( + id: "11488548028", + key:"11488548028", + audiences: "(\"exactString\" OR \"substringString\") AND (\"exists\" OR \"exactNumber\" OR \"gtNumber\" OR \"ltNumber\" OR \"exactBoolean\")", + variationsMap: new Dictionary + { + { + "11557362670", new OptimizelyVariation ( + id: "11557362670", + key: "11557362670", + featureEnabled: true, + variablesMap: new Dictionary() + ) + } + } + ) }, + experimentRules: new List(), experimentsMap: new Dictionary(), variablesMap: new Dictionary()) }, @@ -288,12 +413,67 @@ public void TestGetOptimizelyConfigService() "feat2_with_var", new OptimizelyFeature ( id: "11567102053", key: "feat2_with_var", + deliveryRules: new List() + { + new OptimizelyExperiment( + id: "11630490912", + key:"11630490912", + audiences: "", + variationsMap: new Dictionary + { + { + "11475708559", new OptimizelyVariation ( + id: "11475708559", + key: "11475708559", + featureEnabled: false, + variablesMap: new Dictionary() + { + { + "z" , new OptimizelyVariable ( + id: "11535264367", + key: "z", + type: "integer", + value: "10") + } + }) + } + } + ) + }, + experimentRules: new List() + { + new OptimizelyExperiment ( + id: "1323241599", + key:"feat2_with_var_test", + audiences: "(\"exactString\" OR \"substringString\") AND (\"exists\" OR \"exactNumber\" OR \"gtNumber\" OR \"ltNumber\" OR \"exactBoolean\")", + variationsMap: new Dictionary + { + { + "variation_2", new OptimizelyVariation ( + id: "1423767505", + key: "variation_2", + featureEnabled: true, + variablesMap: new Dictionary + { + { + "z" , new OptimizelyVariable ( + id: "11535264367", + key: "z", + type: "integer", + value: "150") + } + }) + } + } + ) + }, experimentsMap: new Dictionary { { "feat2_with_var_test", new OptimizelyExperiment ( id: "1323241599", key:"feat2_with_var_test", + audiences: "(\"exactString\" OR \"substringString\") AND (\"exists\" OR \"exactNumber\" OR \"gtNumber\" OR \"ltNumber\" OR \"exactBoolean\")", variationsMap: new Dictionary { { @@ -326,13 +506,67 @@ public void TestGetOptimizelyConfigService() }; OptimizelyConfig optimizelyConfig = new OptimizelyConfigService(datafileProjectConfig).GetOptimizelyConfig(); - OptimizelyConfig expectedOptimizelyConfig = new OptimizelyConfig(datafileProjectConfig.Revision, datafileProjectConfig.SDKKey, datafileProjectConfig.EnvironmentKey, experimentsMap, featuresMap); + OptimizelyConfig expectedOptimizelyConfig = new OptimizelyConfig(datafileProjectConfig.Revision, + datafileProjectConfig.SDKKey, + datafileProjectConfig.EnvironmentKey, + attributes: new OptimizelyAttribute[] + { + new OptimizelyAttribute + { + Id = "594015", Key = "house" + }, + new OptimizelyAttribute + { + Id = "594016", Key = "lasers" + }, + new OptimizelyAttribute + { + Id = "594017", Key = "should_do_it" + }, + new OptimizelyAttribute + { + Id = "594018", Key = "favorite_ice_cream" + } + }, + audiences: new OptimizelyAudience[] + { + new OptimizelyAudience("0", "$$dummy", "{\"type\": \"custom_attribute\", \"name\": \"$opt_dummy_attribute\", \"value\": \"impossible_value\"}"), + new OptimizelyAudience("3468206643", "$$dummyExactBoolean", "{\"type\": \"custom_attribute\", \"name\": \"$opt_dummy_attribute\", \"value\": \"impossible_value\"}"), + new OptimizelyAudience("3468206646", "$$dummyExactNumber", "{\"type\": \"custom_attribute\", \"name\": \"$opt_dummy_attribute\", \"value\": \"impossible_value\"}"), + new OptimizelyAudience("3988293899", "$$dummyExists", "{\"type\": \"custom_attribute\", \"name\": \"$opt_dummy_attribute\", \"value\": \"impossible_value\"}"), + new OptimizelyAudience("3468206647", "$$dummyGtNumber", "{\"type\": \"custom_attribute\", \"name\": \"$opt_dummy_attribute\", \"value\": \"impossible_value\"}"), + new OptimizelyAudience("3468206644", "$$dummyLtNumber", "{\"type\": \"custom_attribute\", \"name\": \"$opt_dummy_attribute\", \"value\": \"impossible_value\"}"), + new OptimizelyAudience("3988293898", "$$dummySubstringString", "{\"type\": \"custom_attribute\", \"name\": \"$opt_dummy_attribute\", \"value\": \"impossible_value\"}"), + new OptimizelyAudience("3468206643", "exactBoolean", "[\"and\",[\"or\",[\"or\",{\"name\":\"should_do_it\",\"type\":\"custom_attribute\",\"match\":\"exact\",\"value\":true}]]]"), + new OptimizelyAudience("3468206646", "exactNumber", "[\"and\",[\"or\",[\"or\",{\"name\":\"lasers\",\"type\":\"custom_attribute\",\"match\":\"exact\",\"value\":45.5}]]]"), + new OptimizelyAudience("3468206642", "exactString", "[\"and\", [\"or\", [\"or\", {\"name\": \"house\", \"type\": \"custom_attribute\", \"value\": \"Gryffindor\"}]]]"), + new OptimizelyAudience("3988293899", "exists", "[\"and\",[\"or\",[\"or\",{\"name\":\"favorite_ice_cream\",\"type\":\"custom_attribute\",\"match\":\"exists\"}]]]"), + new OptimizelyAudience("3468206647", "gtNumber", "[\"and\",[\"or\",[\"or\",{\"name\":\"lasers\",\"type\":\"custom_attribute\",\"match\":\"gt\",\"value\":70}]]]"), + new OptimizelyAudience("3468206644", "ltNumber", "[\"and\",[\"or\",[\"or\",{\"name\":\"lasers\",\"type\":\"custom_attribute\",\"match\":\"lt\",\"value\":1.0}]]]"), + new OptimizelyAudience("3468206645", "notChrome", "[\"and\", [\"or\", [\"not\", [\"or\", {\"name\": \"browser_type\", \"type\": \"custom_attribute\", \"value\":\"Chrome\"}]]]]"), + new OptimizelyAudience("3468206648", "notExist", "[\"not\",{\"name\":\"input_value\",\"type\":\"custom_attribute\",\"match\":\"exists\"}]"), + new OptimizelyAudience("3988293898", "substringString", "[\"and\",[\"or\",[\"or\",{\"name\":\"house\",\"type\":\"custom_attribute\",\"match\":\"substring\",\"value\":\"Slytherin\"}]]]"), + }, + events: new OptimizelyEvent[] + { + new OptimizelyEvent() + { + Id = "594089", Key = "item_bought", ExperimentIds = new string[] { "11564051718", "1323241597" } + }, + new OptimizelyEvent() + { + Id = "594090", Key = "user_signed_up", ExperimentIds = new string[] { "1323241598", "1323241599" } + } + }, + experimentsMap: experimentsMap, + featuresMap: featuresMap, + datafile: TestData.TypedAudienceDatafile); Assert.IsTrue(TestData.CompareObjects(optimizelyConfig, expectedOptimizelyConfig)); } #endregion - #region OptimizelyConfig entity tests + #region OptimizelyConfig entity tests [Test] public void TestOptimizelyConfigEntity() @@ -340,12 +574,18 @@ public void TestOptimizelyConfigEntity() OptimizelyConfig expectedOptlyFeature = new OptimizelyConfig("123", "testSdkKey", "Development", - new Dictionary(), - new Dictionary() + attributes: new OptimizelyAttribute[0], + audiences: new OptimizelyAudience[0], + events: new OptimizelyEvent[0], + experimentsMap: new Dictionary(), + featuresMap: new Dictionary() ); Assert.AreEqual(expectedOptlyFeature.Revision, "123"); Assert.AreEqual(expectedOptlyFeature.SDKKey, "testSdkKey"); Assert.AreEqual(expectedOptlyFeature.EnvironmentKey, "Development"); + Assert.AreEqual(expectedOptlyFeature.Attributes, new Entity.Attribute[0]); + Assert.AreEqual(expectedOptlyFeature.Audiences, new OptimizelyAudience[0]); + Assert.AreEqual(expectedOptlyFeature.Events, new Entity.Event[0]); Assert.AreEqual(expectedOptlyFeature.ExperimentsMap, new Dictionary()); Assert.AreEqual(expectedOptlyFeature.FeaturesMap, new Dictionary()); } @@ -354,11 +594,16 @@ public void TestOptimizelyConfigEntity() public void TestOptimizelyFeatureEntity() { OptimizelyFeature expectedOptlyFeature = new OptimizelyFeature("1", "featKey", + new List(), + new List(), new Dictionary(), new Dictionary() ); Assert.AreEqual(expectedOptlyFeature.Id, "1"); Assert.AreEqual(expectedOptlyFeature.Key, "featKey"); + Assert.AreEqual(expectedOptlyFeature.ExperimentRules, new List()); + Assert.AreEqual(expectedOptlyFeature.DeliveryRules, new List()); + Assert.AreEqual(expectedOptlyFeature.Key, "featKey"); Assert.AreEqual(expectedOptlyFeature.ExperimentsMap, new Dictionary()); Assert.AreEqual(expectedOptlyFeature.VariablesMap, new Dictionary()); } @@ -367,6 +612,7 @@ public void TestOptimizelyFeatureEntity() public void TestOptimizelyExperimentEntity() { OptimizelyExperiment expectedOptlyExp = new OptimizelyExperiment("1", "exKey", + "", new Dictionary { { "varKey", new OptimizelyVariation("1", "varKey", true, new Dictionary()) @@ -374,6 +620,7 @@ public void TestOptimizelyExperimentEntity() }); Assert.AreEqual(expectedOptlyExp.Id, "1"); Assert.AreEqual(expectedOptlyExp.Key, "exKey"); + Assert.AreEqual(expectedOptlyExp.Audiences, ""); Assert.AreEqual(expectedOptlyExp.VariationsMap["varKey"], new OptimizelyVariation("1", "varKey", true, new Dictionary())); } diff --git a/OptimizelySDK.Tests/OptimizelySDK.Tests.csproj b/OptimizelySDK.Tests/OptimizelySDK.Tests.csproj index 99a0995c..1a4e7df1 100644 --- a/OptimizelySDK.Tests/OptimizelySDK.Tests.csproj +++ b/OptimizelySDK.Tests/OptimizelySDK.Tests.csproj @@ -78,6 +78,7 @@ + @@ -123,6 +124,8 @@ + + diff --git a/OptimizelySDK.Tests/Utils/TestData.cs b/OptimizelySDK.Tests/Utils/TestData.cs index 1d173659..172c7056 100644 --- a/OptimizelySDK.Tests/Utils/TestData.cs +++ b/OptimizelySDK.Tests/Utils/TestData.cs @@ -29,6 +29,8 @@ public class TestData private static string typedAudienceDatafile = null; private static string emptyRolloutDatafile = null; private static string emptyDatafile = null; + private static string duplicateExpKeysDatafile = null; + private static string duplicateRuleKeysDatafile = null; public static string Datafile { @@ -38,6 +40,18 @@ public static string Datafile } } + public static string DuplicateExpKeysDatafile { + get { + return duplicateExpKeysDatafile ?? (duplicateExpKeysDatafile = LoadJsonData("similar_exp_keys.json")); + } + } + + public static string DuplicateRuleKeysDatafile { + get { + return duplicateRuleKeysDatafile ?? (duplicateRuleKeysDatafile = LoadJsonData("similar_rule_keys_bucketing.json")); + } + } + public static string SimpleABExperimentsDatafile { get diff --git a/OptimizelySDK.Tests/similar_exp_keys.json b/OptimizelySDK.Tests/similar_exp_keys.json new file mode 100644 index 00000000..5428060e --- /dev/null +++ b/OptimizelySDK.Tests/similar_exp_keys.json @@ -0,0 +1,125 @@ +{ + "version": "4", + "rollouts": [], + "typedAudiences": [ + { + "id": "20415611520", + "conditions": [ + "and", + [ + "or", + [ + "or", + { + "value": true, + "type": "custom_attribute", + "name": "hiddenLiveEnabled", + "match": "exact" + } + ] + ] + ], + "name": "test1" + }, + { + "id": "20406066925", + "conditions": [ + "and", + [ + "or", + [ + "or", + { + "value": false, + "type": "custom_attribute", + "name": "hiddenLiveEnabled", + "match": "exact" + } + ] + ] + ], + "name": "test2" + } + ], + "anonymizeIP": true, + "projectId": "20430981610", + "variables": [], + "featureFlags": [ + { + "experimentIds": ["9300000007569"], + "rolloutId": "", + "variables": [], + "id": "3045", + "key": "flag1" + }, + { + "experimentIds": ["9300000007573"], + "rolloutId": "", + "variables": [], + "id": "3046", + "key": "flag2" + } + ], + "experiments": [ + { + "status": "Running", + "audienceConditions": ["or", "20415611520"], + "audienceIds": ["20415611520"], + "variations": [ + { + "variables": [], + "id": "8045", + "key": "variation1", + "featureEnabled": true + } + ], + "forcedVariations": {}, + "key": "targeted_delivery", + "layerId": "9300000007569", + "trafficAllocation": [{ "entityId": "8045", "endOfRange": 10000 }], + "id": "9300000007569" + }, + { + "status": "Running", + "audienceConditions": ["or", "20406066925"], + "audienceIds": ["20406066925"], + "variations": [ + { + "variables": [], + "id": "8048", + "key": "variation2", + "featureEnabled": true + } + ], + "forcedVariations": {}, + "key": "targeted_delivery", + "layerId": "9300000007573", + "trafficAllocation": [{ "entityId": "8048", "endOfRange": 10000 }], + "id": "9300000007573" + } + ], + "audiences": [ + { + "id": "20415611520", + "conditions": "[\"or\", {\"match\": \"exact\", \"name\": \"$opt_dummy_attribute\", \"type\": \"custom_attribute\", \"value\": \"$opt_dummy_value\"}]", + "name": "test1" + }, + { + "id": "20406066925", + "conditions": "[\"or\", {\"match\": \"exact\", \"name\": \"$opt_dummy_attribute\", \"type\": \"custom_attribute\", \"value\": \"$opt_dummy_value\"}]", + "name": "test2" + }, + { + "conditions": "[\"or\", {\"match\": \"exact\", \"name\": \"$opt_dummy_attribute\", \"type\": \"custom_attribute\", \"value\": \"$opt_dummy_value\"}]", + "id": "$opt_dummy_audience", + "name": "Optimizely-Generated Audience for Backwards Compatibility" + } + ], + "groups": [], + "attributes": [{ "id": "20408641883", "key": "hiddenLiveEnabled" }], + "botFiltering": false, + "accountId": "17882702980", + "events": [], + "revision": "25", + "sendFlagDecisions": true +} diff --git a/OptimizelySDK.Tests/similar_rule_keys_bucketing.json b/OptimizelySDK.Tests/similar_rule_keys_bucketing.json new file mode 100644 index 00000000..b4a3898e --- /dev/null +++ b/OptimizelySDK.Tests/similar_rule_keys_bucketing.json @@ -0,0 +1,170 @@ +{ + "version": "4", + "rollouts": [ + { + "experiments": [ + { + "status": "Running", + "audienceConditions": [], + "audienceIds": [], + "variations": [ + { + "variables": [], + "id": "5452", + "key": "on", + "featureEnabled": true + } + ], + "forcedVariations": {}, + "key": "targeted_delivery", + "layerId": "9300000004981", + "trafficAllocation": [{ "entityId": "5452", "endOfRange": 10000 }], + "id": "9300000004981" + }, + { + "status": "Running", + "audienceConditions": [], + "audienceIds": [], + "variations": [ + { + "variables": [], + "id": "5451", + "key": "off", + "featureEnabled": false + } + ], + "forcedVariations": {}, + "key": "default-rollout-2029-20301771717", + "layerId": "default-layer-rollout-2029-20301771717", + "trafficAllocation": [{ "entityId": "5451", "endOfRange": 10000 }], + "id": "default-rollout-2029-20301771717" + } + ], + "id": "rollout-2029-20301771717" + }, + { + "experiments": [ + { + "status": "Running", + "audienceConditions": [], + "audienceIds": [], + "variations": [ + { + "variables": [], + "id": "5450", + "key": "on", + "featureEnabled": true + } + ], + "forcedVariations": {}, + "key": "targeted_delivery", + "layerId": "9300000004979", + "trafficAllocation": [{ "entityId": "5450", "endOfRange": 10000 }], + "id": "9300000004979" + }, + { + "status": "Running", + "audienceConditions": [], + "audienceIds": [], + "variations": [ + { + "variables": [], + "id": "5449", + "key": "off", + "featureEnabled": false + } + ], + "forcedVariations": {}, + "key": "default-rollout-2028-20301771717", + "layerId": "default-layer-rollout-2028-20301771717", + "trafficAllocation": [{ "entityId": "5449", "endOfRange": 10000 }], + "id": "default-rollout-2028-20301771717" + } + ], + "id": "rollout-2028-20301771717" + }, + { + "experiments": [ + { + "status": "Running", + "audienceConditions": [], + "audienceIds": [], + "variations": [ + { + "variables": [], + "id": "5448", + "key": "on", + "featureEnabled": true + } + ], + "forcedVariations": {}, + "key": "targeted_delivery", + "layerId": "9300000004977", + "trafficAllocation": [{ "entityId": "5448", "endOfRange": 10000 }], + "id": "9300000004977" + }, + { + "status": "Running", + "audienceConditions": [], + "audienceIds": [], + "variations": [ + { + "variables": [], + "id": "5447", + "key": "off", + "featureEnabled": false + } + ], + "forcedVariations": {}, + "key": "default-rollout-2027-20301771717", + "layerId": "default-layer-rollout-2027-20301771717", + "trafficAllocation": [{ "entityId": "5447", "endOfRange": 10000 }], + "id": "default-rollout-2027-20301771717" + } + ], + "id": "rollout-2027-20301771717" + } + ], + "typedAudiences": [], + "anonymizeIP": true, + "projectId": "20286295225", + "variables": [], + "featureFlags": [ + { + "experimentIds": [], + "rolloutId": "rollout-2029-20301771717", + "variables": [], + "id": "2029", + "key": "flag_3" + }, + { + "experimentIds": [], + "rolloutId": "rollout-2028-20301771717", + "variables": [], + "id": "2028", + "key": "flag_2" + }, + { + "experimentIds": [], + "rolloutId": "rollout-2027-20301771717", + "variables": [], + "id": "2027", + "key": "flag_1" + } + ], + "experiments": [], + "audiences": [ + { + "conditions": "[\"or\", {\"match\": \"exact\", \"name\": \"$opt_dummy_attribute\", \"type\": \"custom_attribute\", \"value\": \"$opt_dummy_value\"}]", + "id": "$opt_dummy_audience", + "name": "Optimizely-Generated Audience for Backwards Compatibility" + } + ], + "groups": [], + "attributes": [], + "botFiltering": false, + "accountId": "19947277778", + "events": [], + "revision": "11", + "sendFlagDecisions": true +} diff --git a/OptimizelySDK.Tests/typed_audience_datafile.json b/OptimizelySDK.Tests/typed_audience_datafile.json index 77052074..7c6fee4b 100644 --- a/OptimizelySDK.Tests/typed_audience_datafile.json +++ b/OptimizelySDK.Tests/typed_audience_datafile.json @@ -215,7 +215,8 @@ "forcedVariations": {} } ], - "audiences": [{ + "audiences": [ + { "id": "3468206642", "name": "exactString", "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"house\", \"type\": \"custom_attribute\", \"value\": \"Gryffindor\"}]]]" @@ -259,6 +260,11 @@ "id": "0", "name": "$$dummy", "conditions": "{\"type\": \"custom_attribute\", \"name\": \"$opt_dummy_attribute\", \"value\": \"impossible_value\"}" + }, + { + "id": "$opt_dummy_audience", + "name": "dummy_audience", + "conditions": "{\"type\": \"custom_attribute\", \"name\": \"$opt_dummy_attribute\", \"value\": \"impossible_value\"}" } ], "typedAudiences": [{ diff --git a/OptimizelySDK/Config/DatafileProjectConfig.cs b/OptimizelySDK/Config/DatafileProjectConfig.cs index 7d88f46c..b9bbb1de 100644 --- a/OptimizelySDK/Config/DatafileProjectConfig.cs +++ b/OptimizelySDK/Config/DatafileProjectConfig.cs @@ -72,12 +72,12 @@ public enum OPTLYSDKVersion /// /// SDK key of the datafile. /// - public string SDKKey { get; set; } + public string SDKKey { get; set; } = ""; /// /// Environment key of the datafile. /// - public string EnvironmentKey { get; set; } + public string EnvironmentKey { get; set; } = ""; /// /// SendFlagDecisions determines whether impressions events are sent for ALL decision types. diff --git a/OptimizelySDK/OptimizelySDK.csproj b/OptimizelySDK/OptimizelySDK.csproj index b6fc4c24..50f8b137 100644 --- a/OptimizelySDK/OptimizelySDK.csproj +++ b/OptimizelySDK/OptimizelySDK.csproj @@ -121,6 +121,9 @@ + + + diff --git a/OptimizelySDK/OptlyConfig/OptimizelyAttribute.cs b/OptimizelySDK/OptlyConfig/OptimizelyAttribute.cs new file mode 100644 index 00000000..5e0fb921 --- /dev/null +++ b/OptimizelySDK/OptlyConfig/OptimizelyAttribute.cs @@ -0,0 +1,24 @@ +/* + * Copyright 2021, 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 OptimizelySDK.Entity; + +namespace OptimizelySDK.OptlyConfig +{ + public class OptimizelyAttribute : IdKeyEntity + { + } +} diff --git a/OptimizelySDK/OptlyConfig/OptimizelyAudience.cs b/OptimizelySDK/OptlyConfig/OptimizelyAudience.cs new file mode 100644 index 00000000..88d2119f --- /dev/null +++ b/OptimizelySDK/OptlyConfig/OptimizelyAudience.cs @@ -0,0 +1,43 @@ +/* + * Copyright 2021, 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. + */ + +namespace OptimizelySDK.OptlyConfig +{ + public class OptimizelyAudience + { + /// + /// Audience ID + /// + public string Id { get; set; } + + /// + /// Audience Name + /// + public string Name { get; set; } + + /// + /// Audience Conditions + /// + public object Conditions { get; set; } + + public OptimizelyAudience(string id, string name, object conditions) + { + Id = id; + Name = name; + Conditions = conditions; + } + } +} diff --git a/OptimizelySDK/OptlyConfig/OptimizelyConfig.cs b/OptimizelySDK/OptlyConfig/OptimizelyConfig.cs index 146c839c..55406a76 100644 --- a/OptimizelySDK/OptlyConfig/OptimizelyConfig.cs +++ b/OptimizelySDK/OptlyConfig/OptimizelyConfig.cs @@ -22,15 +22,16 @@ namespace OptimizelySDK.OptlyConfig public class OptimizelyConfig { public string Revision { get; private set; } - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public string SDKKey { get; private set; } - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public string EnvironmentKey { get; private set; } + public OptimizelyEvent[] Events { get; private set; } + public OptimizelyAudience[] Audiences { get; private set; } + public OptimizelyAttribute[] Attributes { get; private set; } public IDictionary ExperimentsMap { get; private set; } public IDictionary FeaturesMap { get; private set; } private string _datafile; - + public OptimizelyConfig(string revision, IDictionary experimentsMap, IDictionary featuresMap, string datafile = null) { Revision = revision; @@ -39,10 +40,13 @@ public OptimizelyConfig(string revision, IDictionary experimentsMap, IDictionary featuresMap, string datafile = null) + public OptimizelyConfig(string revision, string sdkKey, string environmentKey, OptimizelyAttribute[] attributes, OptimizelyAudience[] audiences, OptimizelyEvent[] events, IDictionary experimentsMap, IDictionary featuresMap, string datafile = null) { Revision = revision; SDKKey = sdkKey; + Attributes = attributes; + Audiences = audiences; + Events = events; EnvironmentKey = environmentKey; ExperimentsMap = experimentsMap; FeaturesMap = featuresMap; diff --git a/OptimizelySDK/OptlyConfig/OptimizelyConfigService.cs b/OptimizelySDK/OptlyConfig/OptimizelyConfigService.cs index 43eba8bd..14653918 100644 --- a/OptimizelySDK/OptlyConfig/OptimizelyConfigService.cs +++ b/OptimizelySDK/OptlyConfig/OptimizelyConfigService.cs @@ -14,39 +14,111 @@ * limitations under the License. */ +using System; using System.Collections.Generic; +using System.Collections; using System.Linq; +using System.Text; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using OptimizelySDK.Entity; - namespace OptimizelySDK.OptlyConfig { public class OptimizelyConfigService { private OptimizelyConfig OptimizelyConfig; + private IDictionary> featureIdVariablesMap; + + public OptimizelyConfigService(ProjectConfig projectConfig) { if (projectConfig == null) { return; } - var experimentMap = GetExperimentsMap(projectConfig); - var featureMap = GetFeaturesMap(projectConfig, experimentMap); + featureIdVariablesMap = GetFeatureVariablesByIdMap(projectConfig); + var attributes = GetAttributes(projectConfig); + var audiences = GetAudiences(projectConfig); + var experimentsMapById = GetExperimentsMapById(projectConfig); + var experimentsKeyMap = GetExperimentsKeyMap(experimentsMapById); + + var featureMap = GetFeaturesMap(projectConfig, experimentsMapById); + var events = GetEvents(projectConfig); + OptimizelyConfig = new OptimizelyConfig(projectConfig.Revision, projectConfig.SDKKey, projectConfig.EnvironmentKey, - experimentMap, + attributes, + audiences, + events, + experimentsKeyMap, featureMap, projectConfig.ToDatafile()); } + private OptimizelyEvent[] GetEvents(ProjectConfig projectConfig) + { + var optimizelyEvents = new List(); + foreach (var ev in projectConfig.Events) + { + var optimizelyEvent = new OptimizelyEvent(); + optimizelyEvent.Id = ev.Id; + optimizelyEvent.Key = ev.Key; + optimizelyEvent.ExperimentIds = ev.ExperimentIds; + optimizelyEvents.Add(optimizelyEvent); + } + return optimizelyEvents.ToArray(); + } + + private OptimizelyAudience[] GetAudiences(ProjectConfig projectConfig) + { + var filteredAudiencesArr = Array.FindAll(projectConfig.Audiences, aud => !aud.Id.Equals("$opt_dummy_audience")); + var optimizelyAudience = filteredAudiencesArr.Select(aud => new OptimizelyAudience(aud.Id, aud.Name, aud.Conditions)); + var typedAudiences = projectConfig.TypedAudiences?.Select(aud => new OptimizelyAudience(aud.Id, + aud.Name, + JsonConvert.SerializeObject(aud.Conditions))); + optimizelyAudience = optimizelyAudience.Concat(typedAudiences).OrderBy( aud => aud.Name); + + return optimizelyAudience.ToArray(); + } + + private OptimizelyAttribute[] GetAttributes(ProjectConfig projectConfig) + { + var attributes = new List(); + foreach (var attr in projectConfig.Attributes) + { + var attribute = new OptimizelyAttribute(); + attribute.Id = attr.Id; + attribute.Key = attr.Key; + attributes.Add(attribute); + } + return attributes.ToArray(); + } + + /// + /// Converts Experiment Id map to Experiment Key map. + /// + /// + /// Map of experiment key. + + private IDictionary GetExperimentsKeyMap(IDictionary experimentsMapById) + { + var experimentKeyMaps = new Dictionary(); + + foreach(var experiment in experimentsMapById.Values) { + experimentKeyMaps[experiment.Key] = experiment; + } + return experimentKeyMaps; + } + /// /// Gets Map of all experiments except rollouts /// /// The project config /// Dictionary | Dictionary of experiment key and value as experiment object - private IDictionary GetExperimentsMap(ProjectConfig projectConfig) + private IDictionary GetExperimentsMapById(ProjectConfig projectConfig) { var experimentsMap = new Dictionary(); var featureVariableIdMap = GetVariableIdMap(projectConfig); @@ -55,50 +127,68 @@ private IDictionary GetExperimentsMap(ProjectConfi foreach (Experiment experiment in experiments) { - var variationsMap = new Dictionary(); - foreach (Variation variation in experiment.Variations) - { - var variablesMap = MergeFeatureVariables(projectConfig, - featureVariableIdMap, - experiment.Id, - variation); - - var optimizelyVariation = new OptimizelyVariation(variation.Id, - variation.Key, - variation.FeatureEnabled, - variablesMap); - - variationsMap.Add(variation.Key, optimizelyVariation); - } + var featureId = projectConfig.GetExperimentFeatureList(experiment.Id)?.FirstOrDefault(); + var variationsMap = GetVariationsMap(experiment.Variations, featureVariableIdMap, featureId); + var experimentAudience = GetExperimentAudiences(experiment, projectConfig); var optimizelyExperiment = new OptimizelyExperiment(experiment.Id, experiment.Key, + experimentAudience, variationsMap); - experimentsMap.Add(experiment.Key, optimizelyExperiment); + experimentsMap.Add(experiment.Id, optimizelyExperiment); } - + return experimentsMap; } + /// + /// Gets Map of all experiment variations and variables including rollouts + /// + /// variations + /// The map of feature variables and id + /// feature Id of the feature + /// Dictionary | Dictionary of experiment key and value as experiment object + private IDictionary GetVariationsMap(IEnumerable variations, + IDictionary featureVariableIdMap, + string featureId) + { + var variationsMap = new Dictionary(); + foreach (Variation variation in variations) + { + var variablesMap = MergeFeatureVariables( + featureVariableIdMap, + featureId, + variation.FeatureVariableUsageInstances, + variation.IsFeatureEnabled); + + var optimizelyVariation = new OptimizelyVariation(variation.Id, + variation.Key, + variation.FeatureEnabled, + variablesMap); + + variationsMap.Add(variation.Key, optimizelyVariation); + } + + return variationsMap; + } + /// /// Make map of featureVariable which are associated with given feature experiment /// - /// The project config /// Map containing variable ID as key and Object of featureVariable - /// experimentId of featureExperiment - /// variation + /// feature Id of featureExperiment + /// IEnumerable of features variable usage + /// isFeatureEnabled of variation /// Dictionary | Dictionary of FeatureVariable key and value as FeatureVariable object private IDictionary MergeFeatureVariables( - ProjectConfig projectConfig, IDictionary variableIdMap, - string experimentId, - Variation variation) - { - var featureId = projectConfig.GetExperimentFeatureList(experimentId)?.FirstOrDefault(); - var featureIdVariablesMap = GetFeatureIdVariablesMap(projectConfig); + string featureId, + IEnumerable featureVariableUsages, + bool isFeatureEnabled) + { var variablesMap = new Dictionary(); - - if (featureId?.Any() ?? false) + + if (!string.IsNullOrEmpty(featureId)) { variablesMap = featureIdVariablesMap[featureId]?.Select(f => new OptimizelyVariable(f.Id, f.Key, @@ -106,13 +196,13 @@ private IDictionary MergeFeatureVariables( f.DefaultValue) ).ToDictionary(k => k.Key, v => v); - foreach (var featureVariableUsage in variation.FeatureVariableUsageInstances) + foreach (var featureVariableUsage in featureVariableUsages) { var defaultVariable = variableIdMap[featureVariableUsage.Id]; var optimizelyVariable = new OptimizelyVariable(featureVariableUsage.Id, defaultVariable.Key, defaultVariable.Type.ToString().ToLower(), - variation.IsFeatureEnabled ? featureVariableUsage.Value : defaultVariable.DefaultValue); + isFeatureEnabled ? featureVariableUsage.Value : defaultVariable.DefaultValue); variablesMap[defaultVariable.Key] = optimizelyVariable; } @@ -125,19 +215,30 @@ private IDictionary MergeFeatureVariables( /// Gets Map of all FeatureFlags and associated experiment map inside it /// /// The project config - /// Dictionary of experiment key and value as experiment object + /// Dictionary of experiment Id as key and value as experiment object /// Dictionary | Dictionary of FeatureFlag key and value as OptimizelyFeature object - private IDictionary GetFeaturesMap(ProjectConfig projectConfig, IDictionary experimentsMap) + private IDictionary GetFeaturesMap(ProjectConfig projectConfig, IDictionary experimentsMapById) { - var FeaturesMap = new Dictionary(); + var FeaturesMap = new Dictionary(); - foreach (var featureFlag in projectConfig.FeatureFlags) - { - var featureExperimentMap = experimentsMap.Where(expMap => featureFlag.ExperimentIds.Contains(expMap.Value.Id)).ToDictionary(k => k.Key, v => v.Value); + foreach (var featureFlag in projectConfig.FeatureFlags) + { + + var featureExperimentMap = experimentsMapById.Where(expMap => featureFlag.ExperimentIds.Contains(expMap.Key)) + .ToDictionary(k => k.Value.Key, v => v.Value); var featureVariableMap = featureFlag.Variables.Select(v => (OptimizelyVariable)v).ToDictionary(k => k.Key, v => v) ?? new Dictionary(); - - var optimizelyFeature = new OptimizelyFeature(featureFlag.Id, featureFlag.Key, featureExperimentMap, featureVariableMap); + + var experimentRules = featureExperimentMap.Select(exMap => exMap.Value).ToList(); + var rollout = projectConfig.GetRolloutFromId(featureFlag.RolloutId); + var deliveryRules = GetDeliveryRules(featureFlag.Id, rollout.Experiments, projectConfig); + + var optimizelyFeature = new OptimizelyFeature(featureFlag.Id, + featureFlag.Key, + experimentRules, + deliveryRules, + featureExperimentMap, + featureVariableMap); FeaturesMap.Add(featureFlag.Key, optimizelyFeature); } @@ -153,13 +254,134 @@ private IDictionary GetFeaturesMap(ProjectConfig proj /// /// The project config /// Dictionary | Dictionary of FeatureFlag key and value as list of all FeatureVariable inside it - private IDictionary> GetFeatureIdVariablesMap(ProjectConfig projectConfig) - { + private IDictionary> GetFeatureVariablesByIdMap(ProjectConfig projectConfig) + { var featureIdVariablesMap = projectConfig?.FeatureFlags?.ToDictionary(k => k.Id, v => v.Variables); - + return featureIdVariablesMap ?? new Dictionary>(); } + /// + /// Gets stringify audiences used in given experiment + /// + /// The experiment + /// The project config + /// string | Audiences used in experiment. + private string GetExperimentAudiences(Experiment experiment, ProjectConfig projectConfig) + { + if (experiment.AudienceConditionsString == null) + { + return ""; + } + var s = JsonConvert.DeserializeObject>(experiment.AudienceConditionsString); + return GetSerializedAudiences(s, projectConfig.AudienceIdMap); + } + + readonly string[] AUDIENCE_CONDITIONS = { "and", "or", "not" }; + + /// + /// Converts list of audience conditions to serialized audiences used in experiment + /// for examples: + /// 1. Input: ["or", "1", "2"] + /// Output: "\"us\" OR \"female\"" + /// 2. Input: ["not", "1"] + /// Output: "NOT \"us\"" + /// 3. Input: ["or", "1"] + /// Output: "\"us\"" + /// 4. Input: ["and", ["or", "1", ["and", "2", "3"]], ["and", "11", ["or", "12", "13"]]] + /// Output: "(\"us\" OR (\"female\" AND \"adult\")) AND (\"fr\" AND (\"male\" OR \"kid\"))" + /// + /// List of audience conditions in experiment + /// The audience Id map + /// string | Serialized audience in which IDs are replaced with audience name. + private string GetSerializedAudiences(List audienceConditions, Dictionary audienceIdMap) + { + StringBuilder sAudience = new StringBuilder(""); + + if (audienceConditions != null) + { + string cond = ""; + foreach (var item in audienceConditions) + { + var subAudience = ""; + // Checks if item is list of conditions means if it is sub audience + if (item is JArray) + { + subAudience = GetSerializedAudiences(((JArray)item).ToObject>(), audienceIdMap); + subAudience = "(" + subAudience + ")"; + } + else if (AUDIENCE_CONDITIONS.Contains(item.ToString())) // Checks if item is an audience condition + { + cond = item.ToString().ToUpper(); + } + else + { // Checks if item is audience id + var itemStr = item.ToString(); + var audienceName = audienceIdMap.ContainsKey(itemStr) ? audienceIdMap[itemStr].Name : itemStr; + // if audience condition is "NOT" then add "NOT" at start. Otherwise check if there is already audience id in sAudience then append condition between saudience and item + if (!string.IsNullOrEmpty(sAudience.ToString()) || cond.Equals("NOT")) + { + cond = string.IsNullOrEmpty(cond) ? "OR" : cond; + sAudience = string.IsNullOrEmpty(sAudience.ToString()) ? new StringBuilder(cond + " \"" + audienceIdMap[itemStr]?.Name + "\"") : + sAudience.Append(" " + cond + " \"" + audienceName + "\""); + } + else + { + sAudience = new StringBuilder("\"" + audienceName + "\""); + } + } + // Checks if sub audience is empty or not + if (!string.IsNullOrEmpty(subAudience)) + { + if (!string.IsNullOrEmpty(sAudience.ToString()) || cond == "NOT") + { + cond = string.IsNullOrEmpty(cond) ? "OR" : cond; + sAudience = string.IsNullOrEmpty(sAudience.ToString()) ? new StringBuilder(cond + " " + subAudience) : + sAudience.Append(" " + cond + " " + subAudience); + } + else + { + sAudience = sAudience.Append(subAudience); + } + } + } + } + return sAudience.ToString(); + } + + /// + /// Gets list of rollout experiments + /// + /// Feature ID + /// Experiments + /// Project Config + /// List | List of Optimizely rollout experiments. + private List GetDeliveryRules(string featureId, IEnumerable experiments, + ProjectConfig projectConfig) + { + if (experiments == null) + { + return new List(); + } + + var featureVariableIdMap = GetVariableIdMap(projectConfig); + + var deliveryRules = new List(); + + foreach (var experiment in experiments) + { + var optimizelyExperiment = new OptimizelyExperiment( + id: experiment.Id, + key: experiment.Key, + audiences: GetExperimentAudiences(experiment, projectConfig), + variationsMap: GetVariationsMap(experiment.Variations, featureVariableIdMap, featureId) + ); + deliveryRules.Add(optimizelyExperiment); + } + + return deliveryRules; + } + /// /// Gets Map of FeatureVariable with respect to featureVariableId /// diff --git a/OptimizelySDK/OptlyConfig/OptimizelyEvent.cs b/OptimizelySDK/OptlyConfig/OptimizelyEvent.cs new file mode 100644 index 00000000..53b5a9c1 --- /dev/null +++ b/OptimizelySDK/OptlyConfig/OptimizelyEvent.cs @@ -0,0 +1,28 @@ +/* + * Copyright 2021, 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 OptimizelySDK.Entity; + +namespace OptimizelySDK.OptlyConfig +{ + public class OptimizelyEvent : IdKeyEntity + { + /// + /// Associated Experiment with this Event + /// + public string[] ExperimentIds { get; set; } + } +} diff --git a/OptimizelySDK/OptlyConfig/OptimizelyExperiment.cs b/OptimizelySDK/OptlyConfig/OptimizelyExperiment.cs index a1acd32c..7b0cb84e 100644 --- a/OptimizelySDK/OptlyConfig/OptimizelyExperiment.cs +++ b/OptimizelySDK/OptlyConfig/OptimizelyExperiment.cs @@ -1,5 +1,5 @@ -/* - * Copyright 2019, Optimizely +/* + * Copyright 2019, 2021, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,12 +20,15 @@ namespace OptimizelySDK.OptlyConfig { public class OptimizelyExperiment : Entity.IdKeyEntity { + public IDictionary VariationsMap { get; private set; } + public string Audiences { get; private set; } - public OptimizelyExperiment(string id, string key, IDictionary variationsMap) + public OptimizelyExperiment(string id, string key, string audiences, IDictionary variationsMap) { Id = id; Key = key; + Audiences = audiences; VariationsMap = variationsMap; } } diff --git a/OptimizelySDK/OptlyConfig/OptimizelyFeature.cs b/OptimizelySDK/OptlyConfig/OptimizelyFeature.cs index 7e989e09..28091ee7 100644 --- a/OptimizelySDK/OptlyConfig/OptimizelyFeature.cs +++ b/OptimizelySDK/OptlyConfig/OptimizelyFeature.cs @@ -1,5 +1,5 @@ /* - * Copyright 2019, Optimizely + * Copyright 2019, 2021, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,13 +19,18 @@ namespace OptimizelySDK.OptlyConfig { public class OptimizelyFeature : Entity.IdKeyEntity { + + public List ExperimentRules { get; private set; } + public List DeliveryRules { get; private set; } public IDictionary ExperimentsMap { get; private set; } public IDictionary VariablesMap { get; private set; } - public OptimizelyFeature(string id, string key, IDictionary experimentsMap, IDictionary variablesMap) + public OptimizelyFeature(string id, string key, List experimentRules, List deliveryRules, IDictionary experimentsMap, IDictionary variablesMap) { Id = id; Key = key; + ExperimentRules = experimentRules; + DeliveryRules = deliveryRules; ExperimentsMap = experimentsMap; VariablesMap = variablesMap; }