From 01bad8fbdd339440a445fb0e867d7ae281aa15cc Mon Sep 17 00:00:00 2001 From: muhammadnoman Date: Thu, 5 Nov 2020 20:48:30 +0500 Subject: [PATCH 01/34] Added OptimizelyUserContext and basic functionalities and classes related to it --- .../OptimizelyDecisions/DecisionMessage.cs | 32 +++++ .../OptimizelyDecisions/DecisionReasons.cs | 31 +++++ .../DefaultDecisionReasons.cs | 59 ++++++++ .../ErrorsDecisionReasons.cs | 42 ++++++ .../OptimizelyDecideOption.cs | 27 ++++ .../OptimizelyDecisions/OptimizelyDecision.cs | 66 +++++++++ OptimizelySDK/OptimizelyUserContext.cs | 130 ++++++++++++++++++ 7 files changed, 387 insertions(+) create mode 100644 OptimizelySDK/OptimizelyDecisions/DecisionMessage.cs create mode 100644 OptimizelySDK/OptimizelyDecisions/DecisionReasons.cs create mode 100644 OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs create mode 100644 OptimizelySDK/OptimizelyDecisions/ErrorsDecisionReasons.cs create mode 100644 OptimizelySDK/OptimizelyDecisions/OptimizelyDecideOption.cs create mode 100644 OptimizelySDK/OptimizelyDecisions/OptimizelyDecision.cs create mode 100644 OptimizelySDK/OptimizelyUserContext.cs diff --git a/OptimizelySDK/OptimizelyDecisions/DecisionMessage.cs b/OptimizelySDK/OptimizelyDecisions/DecisionMessage.cs new file mode 100644 index 00000000..ab2d1214 --- /dev/null +++ b/OptimizelySDK/OptimizelyDecisions/DecisionMessage.cs @@ -0,0 +1,32 @@ +/* + * Copyright 2020, 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.OptimizelyDecisions +{ + public class DecisionMessage + { + public const string SDK_NOT_READY = "Optimizely SDK not configured properly yet."; + public const string FLAG_KEY_INVALID = "No flag was found for key \"%s\"."; + public const string VARIABLE_VALUE_INVALID = "Variable value for key \"%s\" is invalid or wrong type."; + + private string Format { get; set; } + + public string Reason(params object[] args) + { + return string.Format(Format, args); + } + } +} diff --git a/OptimizelySDK/OptimizelyDecisions/DecisionReasons.cs b/OptimizelySDK/OptimizelyDecisions/DecisionReasons.cs new file mode 100644 index 00000000..c1c0820b --- /dev/null +++ b/OptimizelySDK/OptimizelyDecisions/DecisionReasons.cs @@ -0,0 +1,31 @@ +/* + * Copyright 2020, 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; + +namespace OptimizelySDK.OptimizelyDecisions +{ + /// + /// Interface implemented by all condition classes for audience evaluation. + /// + public interface IDecisionReasons + { + void AddError(string format, params object[] args); + string AddInfo(string format, params object[] args); + List ToReport(); + } + +} diff --git a/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs b/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs new file mode 100644 index 00000000..d7c48ba3 --- /dev/null +++ b/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs @@ -0,0 +1,59 @@ +/* + * Copyright 2020, 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; +using System.Collections.Generic; +using System.Linq; + +namespace OptimizelySDK.OptimizelyDecisions +{ + public class DefaultDecisionReasons : IDecisionReasons + { + private List Errors = new List(); + private List Logs = new List(); + + public static IDecisionReasons NewInstance(List options) + { + if (options != null && options.Contains(OptimizelyDecideOption.INCLUDE_REASONS)) return new DefaultDecisionReasons(); + else return new ErrorsDecisionReasons(); + } + + public static IDecisionReasons newInstance() + { + return NewInstance(null); + } + + public void AddError(string format, params object[] args) + { + string message = string.Format(format, args); + Errors.Add(message); + } + + public string AddInfo(string format, params object[] args) + { + string message = string.Format(format, args); + Logs.Add(message); + return message; + } + + public List ToReport() + { + List reasons = new List(Errors); + reasons.Concat(Logs); + return reasons; + } + } +} diff --git a/OptimizelySDK/OptimizelyDecisions/ErrorsDecisionReasons.cs b/OptimizelySDK/OptimizelyDecisions/ErrorsDecisionReasons.cs new file mode 100644 index 00000000..890416ea --- /dev/null +++ b/OptimizelySDK/OptimizelyDecisions/ErrorsDecisionReasons.cs @@ -0,0 +1,42 @@ +/* + * Copyright 2020, 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; + +namespace OptimizelySDK.OptimizelyDecisions +{ + public class ErrorsDecisionReasons : IDecisionReasons + { + private readonly List errors = new List(); + + public void AddError(string format, params object[] args) + { + string message = string.Format(format, args); + errors.Add(message); + } + + public string AddInfo(string format, params object[] args) + { + // skip tracking and pass-through reasons other than critical errors. + return string.Format(format, args); + } + + public List ToReport() + { + return errors; + } + } +} diff --git a/OptimizelySDK/OptimizelyDecisions/OptimizelyDecideOption.cs b/OptimizelySDK/OptimizelyDecisions/OptimizelyDecideOption.cs new file mode 100644 index 00000000..bdf13915 --- /dev/null +++ b/OptimizelySDK/OptimizelyDecisions/OptimizelyDecideOption.cs @@ -0,0 +1,27 @@ +/* + * Copyright 2020, 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.OptimizelyDecisions +{ + public enum OptimizelyDecideOption + { + DISABLE_DECISION_EVENT, + ENABLED_FLAGS_ONLY, + IGNORE_USER_PROFILE_SERVICE, + INCLUDE_REASONS, + EXCLUDE_VARIABLES + } +} diff --git a/OptimizelySDK/OptimizelyDecisions/OptimizelyDecision.cs b/OptimizelySDK/OptimizelyDecisions/OptimizelyDecision.cs new file mode 100644 index 00000000..a0610d02 --- /dev/null +++ b/OptimizelySDK/OptimizelyDecisions/OptimizelyDecision.cs @@ -0,0 +1,66 @@ +/* + * Copyright 2020, 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.ErrorHandler; +using OptimizelySDK.Logger; +using System.Collections.Generic; + +namespace OptimizelySDK.OptimizelyDecisions +{ + public class OptimizelyDecision + { + private string VariationKey { get; set; } + private bool Enabled { get; set; } + private OptimizelyJSON Variables { get; set; } + private string RuleKey { get; set; } + private string FlagKey { get; set; } + private OptimizelyUserContext UserContext { get; set; } + private List Reasons { get; set; } + + public OptimizelyDecision(string variationKey, + bool enabled, + OptimizelyJSON variables, + string ruleKey, + string flagKey, + OptimizelyUserContext userContext, + List reasons) + { + VariationKey = variationKey; + Enabled = enabled; + Variables = variables; + RuleKey = ruleKey; + FlagKey = flagKey; + UserContext = userContext; + Reasons = reasons; + } + + public static OptimizelyDecision NewErrorDecision(string key, + OptimizelyUserContext optimizelyUserContext, + string error, + IErrorHandler errorHandler, + ILogger logger) + { + return new OptimizelyDecision( + null, + false, + new OptimizelyJSON(new Dictionary(), errorHandler, logger), + null, + key, + optimizelyUserContext, + new List() { error }); + } + } +} diff --git a/OptimizelySDK/OptimizelyUserContext.cs b/OptimizelySDK/OptimizelyUserContext.cs new file mode 100644 index 00000000..8bc2761a --- /dev/null +++ b/OptimizelySDK/OptimizelyUserContext.cs @@ -0,0 +1,130 @@ +/* + * Copyright 2020, 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.Logger; +using System.Collections.Generic; +using OptimizelySDK.ErrorHandler; +using OptimizelySDK.Entity; +using OptimizelySDK.OptimizelyDecisions; + +namespace OptimizelySDK +{ + public class OptimizelyUserContext + { + private ILogger Logger; + private IErrorHandler ErrorHandler; + private string UserId { get; set; } + private UserAttributes UserAttributes { get; set; } + private Optimizely Optimizely { get; set; } + + public OptimizelyUserContext(Optimizely optimizely, string userId, UserAttributes userAttributes, IErrorHandler errorHandler, ILogger logger) + { + ErrorHandler = errorHandler; + Logger = logger; + Optimizely = optimizely; + UserAttributes = userAttributes; + UserId = userId; + } + + /// + /// Set an attribute for a given key. + /// + /// An attribute key + /// value An attribute value + public void SetAttribute(string key, object value) + { + UserAttributes.Add(key, value); + } + + /// + /// Returns a decision result ({@link OptimizelyDecision}) for a given flag key and a user context, which contains all data required to deliver the flag. + ///
    + ///
  • If the SDK finds an error, it’ll return a decision with null for variationKey. The decision will include an error message in reasons. + ///
+ ///
+ /// A flag key for which a decision will be made. + /// A decision result. + public OptimizelyDecision Decide(string key) + { + return Decide(key, new List()); + } + + /// + /// Returns a decision result ({@link OptimizelyDecision}) for a given flag key and a user context, which contains all data required to deliver the flag. + ///
    + ///
  • If the SDK finds an error, it’ll return a decision with null for variationKey. The decision will include an error message in reasons. + ///
+ ///
+ /// A flag key for which a decision will be made. + /// A list of options for decision-making. + /// A decision result. + public OptimizelyDecision Decide(string key, + List options) + { + return null; + } + + /// + /// Returns a key-map of decision results for multiple flag keys and a user context. + /// + /// list of flag keys for which a decision will be made. + /// A dictionary of all decision results, mapped by flag keys. + public Dictionary DecideForKeys(List keys) + { + return null; + } + + /// + /// Returns a key-map of decision results ({@link OptimizelyDecision}) for all active flag keys. + /// + /// A dictionary of all decision results, mapped by flag keys. + public Dictionary DecideAll() + { + return DecideAll(new List()); + } + + + /// + /// Returns a key-map of decision results ({@link OptimizelyDecision}) for all active flag keys. + /// + /// A list of options for decision-making. + /// All decision results mapped by flag keys. + public Dictionary DecideAll(List options) + { + return null; + } + + /// + /// Track an event. + /// + /// The event name. + public void TrackEvent(string eventName) + { + TrackEvent(eventName, new EventTags()); + } + + /// + /// Track an event. + /// + /// The event name. + /// A map of event tag names to event tag values. + public void TrackEvent(string eventName, + EventTags eventTags) + { + Optimizely.Track(eventName, UserId, UserAttributes, eventTags); + } + } +} From 35c5e76d087495df92ae2af4b16c1d2b85d457d6 Mon Sep 17 00:00:00 2001 From: muhammadnoman Date: Fri, 6 Nov 2020 21:07:11 +0500 Subject: [PATCH 02/34] Added DecisionTests --- .../OptimizelySDK.Net40.csproj | 21 ++++++ .../OptimizelySDK.NetStandard16.csproj | 7 ++ .../OptimizelySDK.NetStandard20.csproj | 21 ++++++ .../OptimizelyDecisionTest.cs | 69 +++++++++++++++++++ .../OptimizelySDK.Tests.csproj | 1 + ...DecisionReasons.cs => IDecisionReasons.cs} | 0 .../OptimizelyDecisions/OptimizelyDecision.cs | 14 ++-- OptimizelySDK/OptimizelySDK.csproj | 7 ++ 8 files changed, 133 insertions(+), 7 deletions(-) create mode 100644 OptimizelySDK.Tests/OptimizelyDecisions/OptimizelyDecisionTest.cs rename OptimizelySDK/OptimizelyDecisions/{DecisionReasons.cs => IDecisionReasons.cs} (100%) diff --git a/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj b/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj index 5ce0521a..6afe938c 100644 --- a/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj +++ b/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj @@ -318,6 +318,27 @@ OptlyConfig\IOptimizelyConfigManager.cs + + + OptimizelyDecisions\DecisionMessage.cs + + + OptimizelyDecisions\IDecisionReasons.cs + + + OptimizelyDecisions\DefaultDecisionReasons.cs + + + OptimizelyDecisions\ErrorsDecisionReasons.cs + + + OptimizelyDecisions\OptimizelyDecideOption.cs + + + OptimizelyDecisions\OptimizelyDecision.cs + + + OptimizelyUserContext.cs diff --git a/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj b/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj index 868c1d8c..0e164a97 100644 --- a/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj +++ b/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj @@ -88,6 +88,13 @@ + + + + + + + AtomicProjectConfigManager.cs diff --git a/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj b/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj index 1a126f2c..3ef5fe06 100644 --- a/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj +++ b/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj @@ -279,6 +279,27 @@ OptlyConfig\IOptimizelyConfigManager.cs + + + OptimizelyDecisions\DecisionMessage.cs + + + OptimizelyDecisions\IDecisionReasons.cs + + + OptimizelyDecisions\DefaultDecisionReasons.cs + + + OptimizelyDecisions\ErrorsDecisionReasons.cs + + + OptimizelyDecisions\OptimizelyDecideOption.cs + + + OptimizelyDecisions\OptimizelyDecision.cs + + + OptimizelyUserContext.cs diff --git a/OptimizelySDK.Tests/OptimizelyDecisions/OptimizelyDecisionTest.cs b/OptimizelySDK.Tests/OptimizelyDecisions/OptimizelyDecisionTest.cs new file mode 100644 index 00000000..3e8d8c8f --- /dev/null +++ b/OptimizelySDK.Tests/OptimizelyDecisions/OptimizelyDecisionTest.cs @@ -0,0 +1,69 @@ +using Moq; +using NUnit.Framework; +using OptimizelySDK.ErrorHandler; +using OptimizelySDK.Logger; +using OptimizelySDK.OptimizelyDecisions; +using System; +using System.Collections.Generic; + +namespace OptimizelySDK.Tests.OptimizelyDecisions +{ + [TestFixture] + public class OptimizelyDecisionTest + { + private Mock LoggerMock; + private Mock ErrorHandlerMock; + + [SetUp] + public void Initialize() + { + ErrorHandlerMock = new Mock(); + ErrorHandlerMock.Setup(e => e.HandleError(It.IsAny())); + + LoggerMock = new Mock(); + LoggerMock.Setup(i => i.Log(It.IsAny(), It.IsAny())); + } + + [Test] + public void TestNewErrorDecision() + { + var optimizelyDecision = OptimizelyDecision.NewErrorDecision("var_key", null, "some error message", ErrorHandlerMock.Object, LoggerMock.Object); + Assert.IsNull(optimizelyDecision.VariationKey); + Assert.AreEqual(optimizelyDecision.FlagKey, "var_key"); + Assert.AreEqual(optimizelyDecision.Variables.ToDictionary(), new Dictionary()); + Assert.AreEqual(optimizelyDecision.Reasons, new List() { "some error message" }); + Assert.IsNull(optimizelyDecision.RuleKey); + Assert.False(optimizelyDecision.Enabled); + } + + [Test] + public void TestNewDecision() + { + var map = new Dictionary() { + { "strField", "john doe" }, + { "intField", 12 }, + { "objectField", new Dictionary () { + { "inner_field_int", 3 } + } + } + }; + var optimizelyJSONUsingMap = new OptimizelyJSON(map, ErrorHandlerMock.Object, LoggerMock.Object); + string expectedStringObj = "{\"strField\":\"john doe\",\"intField\":12,\"objectField\":{\"inner_field_int\":3}}"; + + var optimizelyDecision = new OptimizelyDecision("var_key", + true, + optimizelyJSONUsingMap, + "experiment", + "feature_key", + null, + new List()); + Assert.AreEqual(optimizelyDecision.VariationKey, "var_key"); + Assert.AreEqual(optimizelyDecision.FlagKey, "feature_key"); + Assert.AreEqual(optimizelyDecision.Variables.ToString(), expectedStringObj); + Assert.AreEqual(optimizelyDecision.Reasons, new List()); + Assert.AreEqual(optimizelyDecision.RuleKey, "experiment"); + Assert.True(optimizelyDecision.Enabled); + } + + } +} diff --git a/OptimizelySDK.Tests/OptimizelySDK.Tests.csproj b/OptimizelySDK.Tests/OptimizelySDK.Tests.csproj index 90bff004..0f44902c 100644 --- a/OptimizelySDK.Tests/OptimizelySDK.Tests.csproj +++ b/OptimizelySDK.Tests/OptimizelySDK.Tests.csproj @@ -78,6 +78,7 @@ + diff --git a/OptimizelySDK/OptimizelyDecisions/DecisionReasons.cs b/OptimizelySDK/OptimizelyDecisions/IDecisionReasons.cs similarity index 100% rename from OptimizelySDK/OptimizelyDecisions/DecisionReasons.cs rename to OptimizelySDK/OptimizelyDecisions/IDecisionReasons.cs diff --git a/OptimizelySDK/OptimizelyDecisions/OptimizelyDecision.cs b/OptimizelySDK/OptimizelyDecisions/OptimizelyDecision.cs index a0610d02..50334cf2 100644 --- a/OptimizelySDK/OptimizelyDecisions/OptimizelyDecision.cs +++ b/OptimizelySDK/OptimizelyDecisions/OptimizelyDecision.cs @@ -22,13 +22,13 @@ namespace OptimizelySDK.OptimizelyDecisions { public class OptimizelyDecision { - private string VariationKey { get; set; } - private bool Enabled { get; set; } - private OptimizelyJSON Variables { get; set; } - private string RuleKey { get; set; } - private string FlagKey { get; set; } - private OptimizelyUserContext UserContext { get; set; } - private List Reasons { get; set; } + public string VariationKey { get; private set; } + public bool Enabled { get; private set; } + public OptimizelyJSON Variables { get; private set; } + public string RuleKey { get; private set; } + public string FlagKey { get; private set; } + public OptimizelyUserContext UserContext { get; private set; } + public List Reasons { get; private set; } public OptimizelyDecision(string variationKey, bool enabled, diff --git a/OptimizelySDK/OptimizelySDK.csproj b/OptimizelySDK/OptimizelySDK.csproj index 9964d942..4ea518da 100644 --- a/OptimizelySDK/OptimizelySDK.csproj +++ b/OptimizelySDK/OptimizelySDK.csproj @@ -87,6 +87,13 @@ + + + + + + + From f79974c976827faea40346dceb61fb66671f1b7a Mon Sep 17 00:00:00 2001 From: muhammadnoman Date: Fri, 6 Nov 2020 21:11:13 +0500 Subject: [PATCH 03/34] Added classes of optimizelyDecision in net35 file --- .../OptimizelySDK.Net35.csproj | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj b/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj index e5730209..81cc70fc 100644 --- a/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj +++ b/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj @@ -302,6 +302,27 @@ OptlyConfig\IOptimizelyConfigManager.cs + + OptimizelyDecisions\DecisionMessage.cs + + + OptimizelyDecisions\IDecisionReasons.cs + + + OptimizelyDecisions\DefaultDecisionReasons.cs + + + OptimizelyDecisions\ErrorsDecisionReasons.cs + + + OptimizelyDecisions\OptimizelyDecideOption.cs + + + OptimizelyDecisions\OptimizelyDecision.cs + + + OptimizelyUserContext.cs + From 7531f3c327a5f226d6a5ca93b31b6394f72b67ee Mon Sep 17 00:00:00 2001 From: muhammadnoman Date: Fri, 6 Nov 2020 22:28:14 +0500 Subject: [PATCH 04/34] Updated decisionmessage class and added decisionMessage test --- .../DecisionReasonsTest.cs | 40 +++++++++++++++++++ .../OptimizelyDecisions/DecisionMessage.cs | 11 ++--- .../DefaultDecisionReasons.cs | 15 ++++--- 3 files changed, 54 insertions(+), 12 deletions(-) create mode 100644 OptimizelySDK.Tests/OptimizelyDecisions/DecisionReasonsTest.cs diff --git a/OptimizelySDK.Tests/OptimizelyDecisions/DecisionReasonsTest.cs b/OptimizelySDK.Tests/OptimizelyDecisions/DecisionReasonsTest.cs new file mode 100644 index 00000000..7e5ad975 --- /dev/null +++ b/OptimizelySDK.Tests/OptimizelyDecisions/DecisionReasonsTest.cs @@ -0,0 +1,40 @@ +/* + * Copyright 2020, 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 NUnit.Framework; +using OptimizelySDK.OptimizelyDecisions; +using System.Collections.Generic; + +namespace OptimizelySDK.Tests.OptimizelyDecisions +{ + [TestFixture] + public class DecisionReasonsTest + { + + [Test] + public void TestNewDecisionReasonWith() + { + var decisionReasons = DefaultDecisionReasons.NewInstance(new List() { OptimizelyDecideOption.INCLUDE_REASONS }); + decisionReasons.AddError(DecisionMessage.Reason(DecisionMessage.FLAG_KEY_INVALID, "invalid_key")); + Assert.AreEqual(decisionReasons.ToReport()[0], "No flag was found for key \"invalid_key\"."); + decisionReasons.AddError(DecisionMessage.Reason(DecisionMessage.VARIABLE_VALUE_INVALID, "invalid_key")); + Assert.AreEqual(decisionReasons.ToReport()[1], "Variable value for key \"invalid_key\" is invalid or wrong type."); + // decisionReasons.AddError(DecisionMessage.Reason(DecisionMessage.SDK_NOT_READY, "")); + // Assert.AreEqual(decisionReasons.ToReport()[2], "Optimizely SDK not configured properly yet."); + } + + } +} diff --git a/OptimizelySDK/OptimizelyDecisions/DecisionMessage.cs b/OptimizelySDK/OptimizelyDecisions/DecisionMessage.cs index ab2d1214..1f2c3301 100644 --- a/OptimizelySDK/OptimizelyDecisions/DecisionMessage.cs +++ b/OptimizelySDK/OptimizelyDecisions/DecisionMessage.cs @@ -19,14 +19,11 @@ namespace OptimizelySDK.OptimizelyDecisions public class DecisionMessage { public const string SDK_NOT_READY = "Optimizely SDK not configured properly yet."; - public const string FLAG_KEY_INVALID = "No flag was found for key \"%s\"."; - public const string VARIABLE_VALUE_INVALID = "Variable value for key \"%s\" is invalid or wrong type."; - - private string Format { get; set; } - - public string Reason(params object[] args) + public const string FLAG_KEY_INVALID = "No flag was found for key \"{0}\"."; + public const string VARIABLE_VALUE_INVALID = "Variable value for key \"{0}\" is invalid or wrong type."; + public static string Reason(string format, params object[] args) { - return string.Format(Format, args); + return string.Format(format, args); } } } diff --git a/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs b/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs index d7c48ba3..2e250c3f 100644 --- a/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs +++ b/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs @@ -14,7 +14,6 @@ * limitations under the License. */ -using System; using System.Collections.Generic; using System.Linq; @@ -23,15 +22,21 @@ namespace OptimizelySDK.OptimizelyDecisions public class DefaultDecisionReasons : IDecisionReasons { private List Errors = new List(); - private List Logs = new List(); + private List Logs = new List(); public static IDecisionReasons NewInstance(List options) { - if (options != null && options.Contains(OptimizelyDecideOption.INCLUDE_REASONS)) return new DefaultDecisionReasons(); - else return new ErrorsDecisionReasons(); + if (options != null && options.Contains(OptimizelyDecideOption.INCLUDE_REASONS)) + { + return new DefaultDecisionReasons(); + } + else + { + return new ErrorsDecisionReasons(); + } } - public static IDecisionReasons newInstance() + public static IDecisionReasons NewInstance() { return NewInstance(null); } From f006b080a0adbf6c9471f1cbb1b4b3167e88f629 Mon Sep 17 00:00:00 2001 From: muhammadnoman Date: Mon, 9 Nov 2020 19:14:51 +0500 Subject: [PATCH 05/34] Added OptimizelyDecisionTest --- .../DecisionReasonsTest.cs | 40 ------------------- .../OptimizelyDecisionTest.cs | 30 +++++++++++++- .../DefaultDecisionReasons.cs | 2 +- 3 files changed, 30 insertions(+), 42 deletions(-) delete mode 100644 OptimizelySDK.Tests/OptimizelyDecisions/DecisionReasonsTest.cs diff --git a/OptimizelySDK.Tests/OptimizelyDecisions/DecisionReasonsTest.cs b/OptimizelySDK.Tests/OptimizelyDecisions/DecisionReasonsTest.cs deleted file mode 100644 index 7e5ad975..00000000 --- a/OptimizelySDK.Tests/OptimizelyDecisions/DecisionReasonsTest.cs +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2020, 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 NUnit.Framework; -using OptimizelySDK.OptimizelyDecisions; -using System.Collections.Generic; - -namespace OptimizelySDK.Tests.OptimizelyDecisions -{ - [TestFixture] - public class DecisionReasonsTest - { - - [Test] - public void TestNewDecisionReasonWith() - { - var decisionReasons = DefaultDecisionReasons.NewInstance(new List() { OptimizelyDecideOption.INCLUDE_REASONS }); - decisionReasons.AddError(DecisionMessage.Reason(DecisionMessage.FLAG_KEY_INVALID, "invalid_key")); - Assert.AreEqual(decisionReasons.ToReport()[0], "No flag was found for key \"invalid_key\"."); - decisionReasons.AddError(DecisionMessage.Reason(DecisionMessage.VARIABLE_VALUE_INVALID, "invalid_key")); - Assert.AreEqual(decisionReasons.ToReport()[1], "Variable value for key \"invalid_key\" is invalid or wrong type."); - // decisionReasons.AddError(DecisionMessage.Reason(DecisionMessage.SDK_NOT_READY, "")); - // Assert.AreEqual(decisionReasons.ToReport()[2], "Optimizely SDK not configured properly yet."); - } - - } -} diff --git a/OptimizelySDK.Tests/OptimizelyDecisions/OptimizelyDecisionTest.cs b/OptimizelySDK.Tests/OptimizelyDecisions/OptimizelyDecisionTest.cs index 3e8d8c8f..b430e4ea 100644 --- a/OptimizelySDK.Tests/OptimizelyDecisions/OptimizelyDecisionTest.cs +++ b/OptimizelySDK.Tests/OptimizelyDecisions/OptimizelyDecisionTest.cs @@ -1,4 +1,20 @@ -using Moq; +/* + * Copyright 2020, 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 Moq; using NUnit.Framework; using OptimizelySDK.ErrorHandler; using OptimizelySDK.Logger; @@ -65,5 +81,17 @@ public void TestNewDecision() Assert.True(optimizelyDecision.Enabled); } + [Test] + public void TestNewDecisionReasonWithDecideAllOptions() + { + var decisionReasons = DefaultDecisionReasons.NewInstance(new List() { OptimizelyDecideOption.INCLUDE_REASONS }); + decisionReasons.AddError(DecisionMessage.Reason(DecisionMessage.FLAG_KEY_INVALID, "invalid_key")); + Assert.AreEqual(decisionReasons.ToReport()[0], "No flag was found for key \"invalid_key\"."); + decisionReasons.AddError(DecisionMessage.Reason(DecisionMessage.VARIABLE_VALUE_INVALID, "invalid_key")); + Assert.AreEqual(decisionReasons.ToReport()[1], "Variable value for key \"invalid_key\" is invalid or wrong type."); + decisionReasons.AddInfo("Some info message."); + Assert.AreEqual(decisionReasons.ToReport()[2], "Some info message."); + } + } } diff --git a/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs b/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs index 2e250c3f..fe1a38a8 100644 --- a/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs +++ b/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs @@ -57,7 +57,7 @@ public string AddInfo(string format, params object[] args) public List ToReport() { List reasons = new List(Errors); - reasons.Concat(Logs); + reasons.AddRange(Logs); return reasons; } } From ac0c6a513d771affdf894cb54c63c02724c7d764 Mon Sep 17 00:00:00 2001 From: muhammadnoman Date: Mon, 9 Nov 2020 20:27:14 +0500 Subject: [PATCH 06/34] Added CreateUserContext in Optimizely --- OptimizelySDK.Tests/OptimizelyTest.cs | 80 ++++++++++++++++++++++++++ OptimizelySDK/IOptimizely.cs | 9 +++ OptimizelySDK/Optimizely.cs | 36 +++++++++++- OptimizelySDK/OptimizelyUserContext.cs | 8 +-- 4 files changed, 126 insertions(+), 7 deletions(-) diff --git a/OptimizelySDK.Tests/OptimizelyTest.cs b/OptimizelySDK.Tests/OptimizelyTest.cs index 4b40e0de..e310571a 100644 --- a/OptimizelySDK.Tests/OptimizelyTest.cs +++ b/OptimizelySDK.Tests/OptimizelyTest.cs @@ -183,6 +183,86 @@ public PrivateObject CreatePrivateOptimizely() } #endregion + #region Test UserContext + + [Test] + public void TestCreateUserContext() + { + var attribute = new UserAttributes + { + { "device_type", "iPhone" }, + { "location", "San Francisco" } + }; + var optlyUserContext = Optimizely.CreateUserContext(TestUserId, attribute); + Assert.AreEqual(TestUserId, optlyUserContext.UserId); + Assert.AreEqual(Optimizely, optlyUserContext.Optimizely); + Assert.AreEqual(attribute, optlyUserContext.UserAttributes); + } + + [Test] + public void TestCreateUserContextWithoutAttributes() + { + var optlyUserContext = Optimizely.CreateUserContext(TestUserId); + Assert.AreEqual(TestUserId, optlyUserContext.UserId); + Assert.AreEqual(Optimizely, optlyUserContext.Optimizely); + Assert.IsTrue(optlyUserContext.UserAttributes.Count == 0); + } + + [Test] + public void TestCreateUserContextMultipleAttribute() + { + + var attribute1 = new UserAttributes + { + { "device_type", "iPhone" }, + { "location", "San Francisco" } + }; + var optlyUserContext1 = Optimizely.CreateUserContext("userId1", attribute1); + + var attribute2 = new UserAttributes + { + { "device_type2", "Samsung" }, + { "location2", "California" } + }; + var optlyUserContext2 = Optimizely.CreateUserContext("userId2", attribute2); + + Assert.AreEqual("userId1", optlyUserContext1.UserId); + Assert.AreEqual(Optimizely, optlyUserContext1.Optimizely); + Assert.AreEqual(attribute1, optlyUserContext1.UserAttributes); + + Assert.AreEqual("userId2", optlyUserContext2.UserId); + Assert.AreEqual(Optimizely, optlyUserContext2.Optimizely); + Assert.AreEqual(attribute2, optlyUserContext2.UserAttributes); + } + + [Test] + public void TestChangeAttributeDoesNotEffectValues() + { + var userId = "testUserId"; + var attribute = new UserAttributes + { + { "device_type", "iPhone" }, + { "location", "San Francisco" } + }; + var optlyUserContext = Optimizely.CreateUserContext(userId, attribute); + Assert.AreEqual(TestUserId, optlyUserContext.UserId); + Assert.AreEqual(Optimizely, optlyUserContext.Optimizely); + Assert.AreEqual(attribute, optlyUserContext.UserAttributes); + + attribute = new UserAttributes + { + { "device_type", "iPhone" }, + { "level", "low" }, + { "location", "San Francisco" } + }; + userId = "InvalidUser"; + Assert.AreEqual("testUserId", optlyUserContext.UserId); + Assert.AreEqual(Optimizely, optlyUserContext.Optimizely); + Assert.AreNotEqual(attribute, optlyUserContext.UserAttributes); + } + + #endregion + #region Test Validate [Test] public void TestInvalidInstanceLogMessages() diff --git a/OptimizelySDK/IOptimizely.cs b/OptimizelySDK/IOptimizely.cs index edd25e8a..836cdb92 100644 --- a/OptimizelySDK/IOptimizely.cs +++ b/OptimizelySDK/IOptimizely.cs @@ -34,6 +34,15 @@ public interface IOptimizely /// null|Variation Representing variation Variation Activate(string experimentKey, string userId, UserAttributes userAttributes = null); + /// + /// Create a context of the user for which decision APIs will be called. + /// A user context will be created successfully even when the SDK is not fully configured yet. + /// + /// The user ID to be used for bucketing. + /// The user's attributes + /// OptimizelyUserContext | An OptimizelyUserContext associated with this OptimizelyClient. + OptimizelyUserContext CreateUserContext(string userId, UserAttributes userAttributes = null); + /// /// Sends conversion event to Optimizely. /// diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs index cfc1a186..b30d00ac 100644 --- a/OptimizelySDK/Optimizely.cs +++ b/OptimizelySDK/Optimizely.cs @@ -29,6 +29,7 @@ using OptimizelySDK.Event; using OptimizelySDK.OptlyConfig; using System.Net; +using OptimizelySDK.OptimizelyDecisions; namespace OptimizelySDK { @@ -59,6 +60,8 @@ public class Optimizely : IOptimizely, IDisposable private EventProcessor EventProcessor; + private List DefaultDecideOptions; + /// /// It returns true if the ProjectConfig is valid otherwise false. /// Also, it may block execution if GetConfig() blocks execution to get ProjectConfig. @@ -158,11 +161,12 @@ public Optimizely(ProjectConfigManager configManager, ILogger logger = null, IErrorHandler errorHandler = null, UserProfileService userProfileService = null, - EventProcessor eventProcessor = null) + EventProcessor eventProcessor = null, + List defaultDecideOptions = null) { ProjectConfigManager = configManager; - InitializeComponents(eventDispatcher, logger, errorHandler, userProfileService, notificationCenter, eventProcessor); + InitializeComponents(eventDispatcher, logger, errorHandler, userProfileService, notificationCenter, eventProcessor, defaultDecideOptions); } private void InitializeComponents(IEventDispatcher eventDispatcher = null, @@ -170,7 +174,8 @@ private void InitializeComponents(IEventDispatcher eventDispatcher = null, IErrorHandler errorHandler = null, UserProfileService userProfileService = null, NotificationCenter notificationCenter = null, - EventProcessor eventProcessor = null) + EventProcessor eventProcessor = null, + List defaultDecideOptions = null) { Logger = logger ?? new NoOpLogger(); EventDispatcher = eventDispatcher ?? new DefaultEventDispatcher(Logger); @@ -181,6 +186,7 @@ private void InitializeComponents(IEventDispatcher eventDispatcher = null, NotificationCenter = notificationCenter ?? new NotificationCenter(Logger); DecisionService = new DecisionService(Bucketer, ErrorHandler, userProfileService, Logger); EventProcessor = eventProcessor ?? new ForwardingEventProcessor(EventDispatcher, NotificationCenter, Logger); + DefaultDecideOptions = defaultDecideOptions ?? new List(); } /// @@ -682,6 +688,30 @@ public OptimizelyJSON GetFeatureVariableJSON(string featureKey, string variableK return GetFeatureVariableValueForType(featureKey, variableKey, userId, userAttributes, FeatureVariable.JSON_TYPE); } + //============ decide ============// + + /// + /// Create a context of the user for which decision APIs will be called. + /// A user context will be created successfully even when the SDK is not fully configured yet. + /// + /// The user ID to be used for bucketing. + /// The user's attributes + /// OptimizelyUserContext | An OptimizelyUserContext associated with this OptimizelyClient. + public OptimizelyUserContext CreateUserContext(string userId, + UserAttributes userAttributes = null) + { + var inputValues = new Dictionary + { + { USER_ID, userId }, + }; + + if (!ValidateStringInputs(inputValues)) + return null; + + + return new OptimizelyUserContext(this, userId, userAttributes, ErrorHandler, Logger); + } + /// /// Sends impression event. /// diff --git a/OptimizelySDK/OptimizelyUserContext.cs b/OptimizelySDK/OptimizelyUserContext.cs index 8bc2761a..e2621f20 100644 --- a/OptimizelySDK/OptimizelyUserContext.cs +++ b/OptimizelySDK/OptimizelyUserContext.cs @@ -26,16 +26,16 @@ public class OptimizelyUserContext { private ILogger Logger; private IErrorHandler ErrorHandler; - private string UserId { get; set; } - private UserAttributes UserAttributes { get; set; } - private Optimizely Optimizely { get; set; } + public string UserId { get; } + public UserAttributes UserAttributes { get; } + public Optimizely Optimizely { get; } public OptimizelyUserContext(Optimizely optimizely, string userId, UserAttributes userAttributes, IErrorHandler errorHandler, ILogger logger) { ErrorHandler = errorHandler; Logger = logger; Optimizely = optimizely; - UserAttributes = userAttributes; + UserAttributes = userAttributes ?? new UserAttributes(); UserId = userId; } From 6c9ac4d72c9a7a0cdf9539b70caed58145bc76a5 Mon Sep 17 00:00:00 2001 From: muhammadnoman Date: Wed, 11 Nov 2020 22:48:46 +0500 Subject: [PATCH 07/34] Added UserContext add reason logs BugFix of userAttributes Unit tests fix --- OptimizelySDK.Tests/BucketerTest.cs | 23 +- OptimizelySDK.Tests/DecisionServiceTest.cs | 93 ++++---- .../OptimizelyUserContextTest.cs | 201 ++++++++++++++++++ OptimizelySDK/Bucketing/Bucketer.cs | 14 +- OptimizelySDK/Bucketing/DecisionService.cs | 177 +++++++++++---- OptimizelySDK/Optimizely.cs | 199 ++++++++++++++--- OptimizelySDK/OptimizelyUserContext.cs | 4 +- OptimizelySDK/Utils/ExperimentUtils.cs | 23 +- 8 files changed, 590 insertions(+), 144 deletions(-) create mode 100644 OptimizelySDK.Tests/OptimizelyUserContextTest.cs diff --git a/OptimizelySDK.Tests/BucketerTest.cs b/OptimizelySDK.Tests/BucketerTest.cs index aeeeb96c..78b0feec 100644 --- a/OptimizelySDK.Tests/BucketerTest.cs +++ b/OptimizelySDK.Tests/BucketerTest.cs @@ -19,6 +19,7 @@ using OptimizelySDK.Config; using OptimizelySDK.Entity; using OptimizelySDK.Logger; +using OptimizelySDK.OptimizelyDecisions; namespace OptimizelySDK.Tests { @@ -98,7 +99,7 @@ public void TestBucketValidExperimentNotInGroup() // control Assert.AreEqual(new Variation { Id = "7722370027", Key = "control" }, - bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId)); + bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance())); LoggerMock.Verify(l => l.Log(It.IsAny(), It.IsAny()), Times.Exactly(2)); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [3000] to user [testUserId] with bucketing ID [testBucketingIdControl!].")); @@ -106,7 +107,7 @@ public void TestBucketValidExperimentNotInGroup() // variation Assert.AreEqual(new Variation { Id = "7721010009", Key = "variation" }, - bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId)); + bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance())); LoggerMock.Verify(l => l.Log(It.IsAny(), It.IsAny()), Times.Exactly(4)); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [7000] to user [testUserId] with bucketing ID [testBucketingIdControl!].")); @@ -114,7 +115,7 @@ public void TestBucketValidExperimentNotInGroup() // no variation Assert.AreEqual(new Variation { }, - bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId)); + bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance())); LoggerMock.Verify(l => l.Log(It.IsAny(), It.IsAny()), Times.Exactly(6)); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [9000] to user [testUserId] with bucketing ID [testBucketingIdControl!].")); @@ -130,7 +131,7 @@ public void TestBucketValidExperimentInGroup() // variation 1 bucketer.SetBucketValues(new[] { 1000, 4000 }); Assert.AreEqual(new Variation { Id = "7722260071", Key = "group_exp_1_var_1" }, - bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId)); + bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance())); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [1000] to user [testUserId] with bucketing ID [testBucketingIdControl!].")); LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [testUserId] is in experiment [group_experiment_1] of group [7722400015].")); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [4000] to user [testUserId] with bucketing ID [testBucketingIdControl!].")); @@ -139,7 +140,7 @@ public void TestBucketValidExperimentInGroup() // variation 2 bucketer.SetBucketValues(new[] { 1500, 7000 }); Assert.AreEqual(new Variation { Id = "7722360022", Key = "group_exp_1_var_2" }, - bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId)); + bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance())); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [1500] to user [testUserId] with bucketing ID [testBucketingIdControl!].")); LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [testUserId] is in experiment [group_experiment_1] of group [7722400015].")); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [7000] to user [testUserId] with bucketing ID [testBucketingIdControl!].")); @@ -148,7 +149,7 @@ public void TestBucketValidExperimentInGroup() // User not in experiment bucketer.SetBucketValues(new[] { 5000, 7000 }); Assert.AreEqual(new Variation { }, - bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId)); + bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance())); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [5000] to user [testUserId] with bucketing ID [testBucketingIdControl!].")); LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [testUserId] is not in experiment [group_experiment_1] of group [7722400015].")); @@ -161,7 +162,7 @@ public void TestBucketInvalidExperiment() var bucketer = new Bucketer(LoggerMock.Object); Assert.AreEqual(new Variation { }, - bucketer.Bucket(Config, new Experiment(), TestBucketingIdControl, TestUserId)); + bucketer.Bucket(Config, new Experiment(), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance())); LoggerMock.Verify(l => l.Log(It.IsAny(), It.IsAny()), Times.Never); } @@ -176,7 +177,7 @@ public void TestBucketWithBucketingId() // make sure that the bucketing ID is used for the variation bucketing and not the user ID Assert.AreEqual(expectedVariation, - bucketer.Bucket(Config, experiment, TestBucketingIdControl, TestUserIdBucketsToVariation)); + bucketer.Bucket(Config, experiment, TestBucketingIdControl, TestUserIdBucketsToVariation, DefaultDecisionReasons.NewInstance())); } // Test for invalid experiment keys, null variation should be returned @@ -187,7 +188,7 @@ public void TestBucketVariationInvalidExperimentsWithBucketingId() var expectedVariation = new Variation(); Assert.AreEqual(expectedVariation, - bucketer.Bucket(Config, Config.GetExperimentFromKey("invalid_experiment"), TestBucketingIdVariation, TestUserId)); + bucketer.Bucket(Config, Config.GetExperimentFromKey("invalid_experiment"), TestBucketingIdVariation, TestUserId, DefaultDecisionReasons.NewInstance())); } // Make sure that the bucketing ID is used to bucket the user into a group and not the user ID @@ -200,7 +201,7 @@ public void TestBucketVariationGroupedExperimentsWithBucketingId() Assert.AreEqual(expectedGroupVariation, bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_2"), - TestBucketingIdGroupExp2Var2, TestUserIdBucketsToNoGroup)); + TestBucketingIdGroupExp2Var2, TestUserIdBucketsToNoGroup, DefaultDecisionReasons.NewInstance())); } // Make sure that user gets bucketed into the rollout rule. @@ -213,7 +214,7 @@ public void TestBucketRolloutRule() var expectedVariation = Config.GetVariationFromId(rolloutRule.Key, "177773"); Assert.True(TestData.CompareObjects(expectedVariation, - bucketer.Bucket(Config, rolloutRule, "testBucketingId", TestUserId))); + bucketer.Bucket(Config, rolloutRule, "testBucketingId", TestUserId, DefaultDecisionReasons.NewInstance()))); } } } \ No newline at end of file diff --git a/OptimizelySDK.Tests/DecisionServiceTest.cs b/OptimizelySDK.Tests/DecisionServiceTest.cs index 0936a857..3c83b3db 100644 --- a/OptimizelySDK.Tests/DecisionServiceTest.cs +++ b/OptimizelySDK.Tests/DecisionServiceTest.cs @@ -24,6 +24,7 @@ using OptimizelySDK.Bucketing; using OptimizelySDK.Utils; using OptimizelySDK.Config; +using OptimizelySDK.OptimizelyDecisions; namespace OptimizelySDK.Tests { @@ -84,7 +85,7 @@ public void TestGetVariationForcedVariationPrecedesAudienceEval() LoggerMock.Verify(l => l.Log(LogLevel.INFO, string.Format("User \"{0}\" is forced in variation \"vtag5\".", WhitelistedUserId)), Times.Once); // no attributes provided for a experiment that has an audience Assert.IsTrue(TestData.CompareObjects(actualVariation, expectedVariation)); - BucketerMock.Verify(_ => _.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + BucketerMock.Verify(_ => _.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Test] @@ -111,7 +112,7 @@ public void TestGetVariationEvaluatesUserProfileBeforeAudienceTargeting() // ensure that a user with a saved user profile, sees the same variation regardless of audience evaluation decisionService.GetVariation(experiment, UserProfileId, ProjectConfig, new UserAttributes()); - BucketerMock.Verify(_ => _.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + BucketerMock.Verify(_ => _.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Test] @@ -123,7 +124,7 @@ public void TestGetForcedVariationReturnsForcedVariation() LoggerMock.Verify(l => l.Log(LogLevel.INFO, string.Format("User \"{0}\" is forced in variation \"{1}\".", WhitelistedUserId, WhitelistedVariation.Key)), Times.Once); - BucketerMock.Verify(_ => _.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + BucketerMock.Verify(_ => _.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Test] @@ -164,7 +165,7 @@ public void TestGetForcedVariationWithInvalidVariation() string.Format("Variation \"{0}\" is not in the datafile. Not activating user \"{1}\".", invalidVariationKey, userId)), Times.Once); - BucketerMock.Verify(_ => _.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + BucketerMock.Verify(_ => _.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Test] @@ -216,7 +217,7 @@ public void TestGetStoredVariationLogsWhenLookupReturnsNull() DecisionService decisionService = new DecisionService(bucketer, ErrorHandlerMock.Object, userProfileService, LoggerMock.Object); - Assert.IsNull(decisionService.GetStoredVariation(experiment, userProfile, ProjectConfig)); + Assert.IsNull(decisionService.GetStoredVariation(experiment, userProfile, ProjectConfig, DefaultDecisionReasons.NewInstance())); LoggerMock.Verify(l => l.Log(LogLevel.INFO, string.Format("No previously activated variation of experiment \"{0}\" for user \"{1}\" found in user profile." , experiment.Key, UserProfileId)), Times.Once); @@ -241,7 +242,7 @@ public void TestGetStoredVariationReturnsNullWhenVariationIsNoLongerInConfig() DecisionService decisionService = new DecisionService(bucketer, ErrorHandlerMock.Object, UserProfileServiceMock.Object, LoggerMock.Object); - Assert.IsNull(decisionService.GetStoredVariation(experiment, storedUserProfile, ProjectConfig)); + Assert.IsNull(decisionService.GetStoredVariation(experiment, storedUserProfile, ProjectConfig, DefaultDecisionReasons.NewInstance())); LoggerMock.Verify(l => l.Log(LogLevel.INFO, string.Format("User \"{0}\" was previously bucketed into variation with ID \"{1}\" for experiment \"{2}\", but no matching variation was found for that user. We will re-bucket the user." , UserProfileId, storedVariationId, experiment.Id)), Times.Once); } @@ -265,7 +266,7 @@ public void TestGetVariationSavesBucketedVariationIntoUserProfile() }); var mockBucketer = new Mock(LoggerMock.Object); - mockBucketer.Setup(m => m.Bucket(ProjectConfig, experiment, UserProfileId, UserProfileId)).Returns(variation); + mockBucketer.Setup(m => m.Bucket(ProjectConfig, experiment, UserProfileId, UserProfileId, It.IsAny())).Returns(variation); DecisionService decisionService = new DecisionService(mockBucketer.Object, ErrorHandlerMock.Object, UserProfileServiceMock.Object, LoggerMock.Object); @@ -317,7 +318,7 @@ public void TestGetVariationSavesANewUserProfile() }); var mockBucketer = new Mock(LoggerMock.Object); - mockBucketer.Setup(m => m.Bucket(ProjectConfig, experiment, UserProfileId, UserProfileId)).Returns(variation); + mockBucketer.Setup(m => m.Bucket(ProjectConfig, experiment, UserProfileId, UserProfileId, It.IsAny())).Returns(variation); Dictionary userProfile = null; @@ -468,7 +469,7 @@ public void TestGetVariationWithBucketingId() public void TestGetVariationForFeatureExperimentGivenNullExperimentIds() { var featureFlag = ProjectConfig.GetFeatureFlagFromKey("empty_feature"); - var decision = DecisionService.GetVariationForFeatureExperiment(featureFlag, GenericUserId, new UserAttributes() { }, ProjectConfig); + var decision = DecisionService.GetVariationForFeatureExperiment(featureFlag, GenericUserId, new UserAttributes() { }, ProjectConfig, new List(), DefaultDecisionReasons.NewInstance()); Assert.IsNull(decision); @@ -487,7 +488,7 @@ public void TestGetVariationForFeatureExperimentGivenExperimentNotInDataFile() ExperimentIds = new List { "29039203" } }; - var decision = DecisionService.GetVariationForFeatureExperiment(featureFlag, GenericUserId, new UserAttributes() { }, ProjectConfig); + var decision = DecisionService.GetVariationForFeatureExperiment(featureFlag, GenericUserId, new UserAttributes() { }, ProjectConfig, new List(), DefaultDecisionReasons.NewInstance()); Assert.IsNull(decision); LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Experiment ID \"29039203\" is not in datafile.")); @@ -502,7 +503,7 @@ public void TestGetVariationForFeatureExperimentGivenNonMutexGroupAndUserNotBuck DecisionServiceMock.Setup(ds => ds.GetVariation(multiVariateExp, "user1", ProjectConfig, null)).Returns(null); var featureFlag = ProjectConfig.GetFeatureFlagFromKey("multi_variate_feature"); - var decision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", new UserAttributes(), ProjectConfig); + var decision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", new UserAttributes(), ProjectConfig, new List(), DefaultDecisionReasons.NewInstance()); Assert.IsNull(decision); LoggerMock.Verify(l => l.Log(LogLevel.INFO, "The user \"user1\" is not bucketed into any of the experiments on the feature \"multi_variate_feature\".")); @@ -518,10 +519,10 @@ public void TestGetVariationForFeatureExperimentGivenNonMutexGroupAndUserIsBucke var userAttributes = new UserAttributes(); DecisionServiceMock.Setup(ds => ds.GetVariation(ProjectConfig.GetExperimentFromKey("test_experiment_multivariate"), - "user1", ProjectConfig, userAttributes)).Returns(variation); + "user1", ProjectConfig, userAttributes, It.IsAny>(), It.IsAny())).Returns(variation); var featureFlag = ProjectConfig.GetFeatureFlagFromKey("multi_variate_feature"); - var decision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig); + var decision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig, new List(), DefaultDecisionReasons.NewInstance()); Assert.IsTrue(TestData.CompareObjects(expectedDecision, decision)); @@ -541,7 +542,7 @@ public void TestGetVariationForFeatureExperimentGivenMutexGroupAndUserIsBucketed userAttributes)).Returns(variation); var featureFlag = ProjectConfig.GetFeatureFlagFromKey("boolean_feature"); - var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig); + var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig, new List(), DefaultDecisionReasons.NewInstance()); Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision)); @@ -553,11 +554,11 @@ public void TestGetVariationForFeatureExperimentGivenMutexGroupAndUserIsBucketed public void TestGetVariationForFeatureExperimentGivenMutexGroupAndUserNotBucketed() { var mutexExperiment = ProjectConfig.GetExperimentFromKey("group_experiment_1"); - DecisionServiceMock.Setup(ds => ds.GetVariation(It.IsAny(), It.IsAny(), ProjectConfig, It.IsAny())). + DecisionServiceMock.Setup(ds => ds.GetVariation(It.IsAny(), It.IsAny(), ProjectConfig, It.IsAny(), It.IsAny>(), It.IsAny())). Returns(null); var featureFlag = ProjectConfig.GetFeatureFlagFromKey("boolean_feature"); - var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", new UserAttributes(), ProjectConfig); + var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", new UserAttributes(), ProjectConfig, new List(), DefaultDecisionReasons.NewInstance()); Assert.IsNull(actualDecision); @@ -582,7 +583,7 @@ public void TestGetVariationForFeatureRolloutWhenNoRuleInRollouts() var decisionService = new DecisionService(new Bucketer(new NoOpLogger()), new NoOpErrorHandler(), null, new NoOpLogger()); - var variation = decisionService.GetVariationForFeatureRollout(featureFlag, "userId1", null, projectConfig); + var variation = decisionService.GetVariationForFeatureRollout(featureFlag, "userId1", null, projectConfig, DefaultDecisionReasons.NewInstance()); Assert.IsNull(variation); } @@ -600,9 +601,9 @@ public void TestGetVariationForFeatureRolloutWhenRolloutIsNotInDataFile() Variables = featureFlag.Variables }; - DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny(), It.IsAny(), It.IsAny(), ProjectConfig)).Returns(null); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny(), It.IsAny(), It.IsAny(), ProjectConfig, new List(), DefaultDecisionReasons.NewInstance())).Returns(null); - var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureRollout(featureFlag, "user1", new UserAttributes(), ProjectConfig); + var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureRollout(featureFlag, "user1", new UserAttributes(), ProjectConfig, DefaultDecisionReasons.NewInstance()); Assert.IsNull(actualDecision); LoggerMock.Verify(l => l.Log(LogLevel.INFO, "The feature flag \"boolean_feature\" is not used in a rollout.")); @@ -624,10 +625,10 @@ public void TestGetVariationForFeatureRolloutWhenUserIsBucketedInTheTargetingRul }; BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny())).Returns(variation); + It.IsAny(), It.IsAny())).Returns(variation); var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, null, LoggerMock.Object); - var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes, ProjectConfig); + var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes, ProjectConfig, DefaultDecisionReasons.NewInstance()); Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision)); } @@ -648,11 +649,11 @@ public void TestGetVariationForFeatureRolloutWhenUserIsNotBucketedInTheTargeting { "browser_type", "chrome" } }; - BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), experiment, It.IsAny(), It.IsAny())).Returns(null); - BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), everyoneElseRule, It.IsAny(), It.IsAny())).Returns(variation); + BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), experiment, It.IsAny(), It.IsAny(), It.IsAny())).Returns(null); + BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), everyoneElseRule, It.IsAny(), It.IsAny(), It.IsAny())).Returns(variation); var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, null, LoggerMock.Object); - var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes, ProjectConfig); + var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes, ProjectConfig, DefaultDecisionReasons.NewInstance()); Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision)); } @@ -668,10 +669,10 @@ public void TestGetVariationForFeatureRolloutWhenUserIsNeitherBucketedInTheTarge { "browser_type", "chrome" } }; - BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(null); + BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(null); var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, null, LoggerMock.Object); - var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes, ProjectConfig); + var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes, ProjectConfig, DefaultDecisionReasons.NewInstance()); Assert.IsNull(actualDecision); } @@ -689,11 +690,11 @@ public void TestGetVariationForFeatureRolloutWhenUserDoesNotQualifyForAnyTargeti var variation = everyoneElseRule.Variations[0]; var expectedDecision = new FeatureDecision(everyoneElseRule, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT); - BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), everyoneElseRule, It.IsAny(), It.IsAny())).Returns(variation); + BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), everyoneElseRule, It.IsAny(), It.IsAny(), It.IsAny())).Returns(variation); var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, null, LoggerMock.Object); // Provide null attributes so that user does not qualify for audience. - var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", null, ProjectConfig); + var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", null, ProjectConfig, DefaultDecisionReasons.NewInstance()); Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision)); @@ -723,7 +724,7 @@ public void TestGetVariationForFeatureRolloutAudienceAndTrafficeAllocationCheck( { { "device_type", "iPhone" }, { "location", "San Francisco" } - }, ProjectConfig); + }, ProjectConfig, DefaultDecisionReasons.NewInstance()); // Returned variation id should be '177773' because of audience 'iPhone users in San Francisco'. var expectedDecision = new FeatureDecision(expWithAudienceiPhoneUsers, varWithAudienceiPhoneUsers, FeatureDecision.DECISION_SOURCE_ROLLOUT); @@ -733,7 +734,7 @@ public void TestGetVariationForFeatureRolloutAudienceAndTrafficeAllocationCheck( actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, new UserAttributes { { "browser_type", "chrome" } - }, ProjectConfig); + }, ProjectConfig, DefaultDecisionReasons.NewInstance()); // Returned variation id should be '177771' because of audience 'Chrome users'. expectedDecision = new FeatureDecision(expWithAudienceChromeUsers, varWithAudienceChromeUsers, FeatureDecision.DECISION_SOURCE_ROLLOUT); @@ -741,7 +742,7 @@ public void TestGetVariationForFeatureRolloutAudienceAndTrafficeAllocationCheck( // Calling with no audience. mockBucketer.Setup(bm => bm.GenerateBucketValue(It.IsAny())).Returns(8000); - actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, new UserAttributes(), ProjectConfig); + actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, new UserAttributes(), ProjectConfig, DefaultDecisionReasons.NewInstance()); // Returned variation id should be of everyone else rule because of no audience. expectedDecision = new FeatureDecision(expWithNoAudience, varWithNoAudience, FeatureDecision.DECISION_SOURCE_ROLLOUT); @@ -752,7 +753,7 @@ public void TestGetVariationForFeatureRolloutAudienceAndTrafficeAllocationCheck( actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, new UserAttributes { { "browser_type", "chrome" } - }, ProjectConfig); + }, ProjectConfig, DefaultDecisionReasons.NewInstance()); // Returned decision entity should be null because bucket value exceeds traffice allocation of everyone else rule. Assert.Null(actualDecision); @@ -768,21 +769,21 @@ public void TestGetVariationForFeatureRolloutCheckAudienceInEveryoneElseRule() var variation = everyoneElseRule.Variations[0]; var expectedDecision = new FeatureDecision(everyoneElseRule, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT); - BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), everyoneElseRule, It.IsAny(), WhitelistedUserId)).Returns(variation); - BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), everyoneElseRule, It.IsAny(), GenericUserId)).Returns(null); + BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), everyoneElseRule, It.IsAny(), WhitelistedUserId, It.IsAny())).Returns(variation); + BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), everyoneElseRule, It.IsAny(), GenericUserId, It.IsAny())).Returns(null); var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, null, LoggerMock.Object); // Returned variation id should be of everyone else rule as it passes audience Id checking. - var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, WhitelistedUserId, null, ProjectConfig); + var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, WhitelistedUserId, null, ProjectConfig, DefaultDecisionReasons.NewInstance()); Assert.True(TestData.CompareObjects(expectedDecision, actualDecision)); // Returned variation id should be null. - actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, null, ProjectConfig); + actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, null, ProjectConfig, DefaultDecisionReasons.NewInstance()); Assert.Null(actualDecision); // Returned variation id should be null as it fails audience Id checking. everyoneElseRule.AudienceIds = new string[] { ProjectConfig.Audiences[0].Id }; - actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, null, ProjectConfig); + actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, null, ProjectConfig, DefaultDecisionReasons.NewInstance()); Assert.Null(actualDecision); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "User \"testUser1\" does not meet the conditions for targeting rule \"1\"."), Times.Once); @@ -807,7 +808,7 @@ public void TestGetVariationForFeatureWhenTheUserIsBucketedIntoFeatureExperiment var expectedDecision = new FeatureDecision(expectedExperiment, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST); DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny(), It.IsAny(), - It.IsAny(), ProjectConfig)).Returns(expectedDecision); + It.IsAny(), ProjectConfig, It.IsAny>(), It.IsAny())).Returns(expectedDecision); var actualDecision = DecisionServiceMock.Object.GetVariationForFeature(featureFlag, "user1", ProjectConfig, new UserAttributes()); Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision)); @@ -826,9 +827,9 @@ public void TestGetVariationForFeatureWhenTheUserIsNotBucketedIntoFeatureExperim var expectedDecision = new FeatureDecision(expectedExperiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT); DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny(), It.IsAny(), - It.IsAny(), ProjectConfig)).Returns(null); + It.IsAny(), ProjectConfig, It.IsAny>(), It.IsAny())).Returns(null); DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureRollout(It.IsAny(), It.IsAny(), - It.IsAny(), ProjectConfig)).Returns(expectedDecision); + It.IsAny(), ProjectConfig, It.IsAny())).Returns(expectedDecision); var actualDecision = DecisionServiceMock.Object.GetVariationForFeature(featureFlag, "user1", ProjectConfig, new UserAttributes()); @@ -844,8 +845,8 @@ public void TestGetVariationForFeatureWhenTheUserIsNeitherBucketedIntoFeatureExp var featureFlag = ProjectConfig.GetFeatureFlagFromKey("string_single_variable_feature"); var expectedDecision = new FeatureDecision(null, null, FeatureDecision.DECISION_SOURCE_ROLLOUT); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny(), It.IsAny(), It.IsAny(), ProjectConfig)).Returns(null); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureRollout(It.IsAny(), It.IsAny(), It.IsAny(), ProjectConfig)).Returns(null); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny(), It.IsAny(), It.IsAny(), ProjectConfig, new List(), DefaultDecisionReasons.NewInstance())).Returns(null); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureRollout(It.IsAny(), It.IsAny(), It.IsAny(), ProjectConfig, DefaultDecisionReasons.NewInstance())).Returns(null); var actualDecision = DecisionServiceMock.Object.GetVariationForFeature(featureFlag, "user1", ProjectConfig, new UserAttributes()); Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision)); @@ -866,8 +867,8 @@ public void TestGetVariationForFeatureWhenTheUserIsBuckedtedInBothExperimentAndR { "browser_type", "chrome" } }; - DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, "user1", ProjectConfig, userAttributes)).Returns(variation); - var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig); + DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, "user1", ProjectConfig, userAttributes, It.IsAny>(), It.IsAny())).Returns(variation); + var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig, new List(), DefaultDecisionReasons.NewInstance()); // The user is bucketed into feature experiment's variation. Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision)); @@ -877,8 +878,8 @@ public void TestGetVariationForFeatureWhenTheUserIsBuckedtedInBothExperimentAndR var rolloutVariation = rolloutExperiment.Variations[0]; var expectedRolloutDecision = new FeatureDecision(rolloutExperiment, rolloutVariation, FeatureDecision.DECISION_SOURCE_ROLLOUT); - BucketerMock.Setup(bm => bm.Bucket(ProjectConfig, rolloutExperiment, It.IsAny(), It.IsAny())).Returns(rolloutVariation); - var actualRolloutDecision = DecisionServiceMock.Object.GetVariationForFeatureRollout(featureFlag, "user1", userAttributes, ProjectConfig); + BucketerMock.Setup(bm => bm.Bucket(ProjectConfig, rolloutExperiment, It.IsAny(), It.IsAny(), It.IsAny())).Returns(rolloutVariation); + var actualRolloutDecision = DecisionServiceMock.Object.GetVariationForFeatureRollout(featureFlag, "user1", userAttributes, ProjectConfig, DefaultDecisionReasons.NewInstance()); // The user is bucketed into feature rollout's variation. diff --git a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs new file mode 100644 index 00000000..029c8499 --- /dev/null +++ b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs @@ -0,0 +1,201 @@ +/** + * + * Copyright 2020, Optimizely 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. + */ + +using Castle.Core.Internal; +using Moq; +using NUnit.Framework; +using OptimizelySDK.Entity; +using OptimizelySDK.ErrorHandler; +using OptimizelySDK.Event.Dispatcher; +using OptimizelySDK.Logger; +using OptimizelySDK.OptimizelyDecisions; +using System; +using System.Collections.Generic; + +namespace OptimizelySDK.Tests +{ + [TestFixture] + public class OptimizelyUserContextTest + { + string UserID = "testUserID"; + private Optimizely Optimizely; + private Mock LoggerMock; + private Mock ErrorHandlerMock; + private Mock EventDispatcherMock; + + [SetUp] + public void SetUp() + { + LoggerMock = new Mock(); + LoggerMock.Setup(i => i.Log(It.IsAny(), It.IsAny())); + + ErrorHandlerMock = new Mock(); + ErrorHandlerMock.Setup(e => e.HandleError(It.IsAny())); + EventDispatcherMock = new Mock(); + + Optimizely = new Optimizely(TestData.Datafile, EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object); + } + + [Test] + public void OptimizelyUserContext_withAttributes() + { + var attributes = new UserAttributes() { { "house", "GRYFFINDOR" } }; + OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object); + + Assert.AreEqual(user.Optimizely, Optimizely); + Assert.AreEqual(user.UserId, UserID); + Assert.AreEqual(user.UserAttributes, attributes); + } + + [Test] + public void OptimizelyUserContext_noAttributes() + { + OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, null, ErrorHandlerMock.Object, LoggerMock.Object); + + Assert.AreEqual(user.Optimizely, Optimizely); + Assert.AreEqual(user.UserId, UserID); + Assert.True(user.UserAttributes.Count == 0); + } + + [Test] + public void SetAttribute() + { + var attributes = new UserAttributes() { { "house", "GRYFFINDOR" } }; + OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object); + + user.SetAttribute("k1", "v1"); + user.SetAttribute("k2", true); + user.SetAttribute("k3", 100); + user.SetAttribute("k4", 3.5); + + Assert.AreEqual(user.Optimizely, Optimizely); + Assert.AreEqual(user.UserId, UserID); + var newAttributes = user.UserAttributes; + Assert.AreEqual(newAttributes["house"], "GRYFFINDOR"); + Assert.AreEqual(newAttributes["k1"], "v1"); + Assert.AreEqual(newAttributes["k2"], true); + Assert.AreEqual(newAttributes["k3"], 100); + Assert.AreEqual(newAttributes["k4"], 3.5); + } + + [Test] + public void SetAttribute_noAttribute() + { + OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, null, ErrorHandlerMock.Object, LoggerMock.Object); + + user.SetAttribute("k1", "v1"); + user.SetAttribute("k2", true); + + Assert.AreEqual(user.Optimizely, Optimizely); + Assert.AreEqual(user.UserId, UserID); + var newAttributes = user.UserAttributes; + Assert.AreEqual(newAttributes["k1"], "v1"); + Assert.AreEqual(newAttributes["k2"], true); + } + + [Test] + public void SetAttribute_override() + { + var attributes = new UserAttributes() { { "house", "GRYFFINDOR" } }; + OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object); + + user.SetAttribute("k1", "v1"); + user.SetAttribute("house", "v2"); + + var newAttributes = user.UserAttributes; + Assert.AreEqual(newAttributes["k1"], "v1"); + Assert.AreEqual(newAttributes["house"], "v2"); + } + + [Test] + public void SetAttribute_nullValue() + { + var attributes = new UserAttributes() { { "k1", null } }; + OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object); + + var newAttributes = user.UserAttributes; + Assert.AreEqual(newAttributes["k1"], null); + + user.SetAttribute("k1", true); + newAttributes = user.UserAttributes; + Assert.AreEqual(newAttributes["k1"], true); + + user.SetAttribute("k1", null); + newAttributes = user.UserAttributes; + Assert.AreEqual(newAttributes["k1"], null); + } + + #region decide + + [Test] + public void Decide() + { + var flagKey = "multi_variate_feature"; + var variablesExpected = Optimizely.GetAllFeatureVariables(flagKey, UserID); + + var user = Optimizely.CreateUserContext(UserID); + user.SetAttribute("browser_type", "chrome"); + var decision = user.Decide(flagKey); + + Assert.AreEqual(decision.VariationKey, "Gred"); + Assert.False(decision.Enabled); + Assert.AreEqual(decision.Variables.ToDictionary(), variablesExpected.ToDictionary()); + Assert.AreEqual(decision.RuleKey, "test_experiment_multivariate"); + Assert.AreEqual(decision.FlagKey, flagKey); + Assert.AreEqual(decision.UserContext, user); + Assert.True(decision.Reasons.IsNullOrEmpty()); + } + + [Test] + public void DecideInvalidFlagKey() + { + var flagKey = "invalid_feature"; + + var user = Optimizely.CreateUserContext(UserID); + user.SetAttribute("browser_type", "chrome"); + var decision = user.Decide(flagKey); + + Assert.Null(decision.VariationKey); + Assert.False(decision.Enabled); + Assert.AreEqual(decision.Variables.ToDictionary(), new Dictionary()); + Assert.IsNull(decision.RuleKey); + Assert.AreEqual(decision.FlagKey, flagKey); + Assert.AreEqual(decision.UserContext, user); + Assert.True(decision.Reasons.IsNullOrEmpty()); + } + + [Test] + public void DecideInvalidConfig() + { + Optimizely optimizely = new Optimizely(TestData.UnsupportedVersionDatafile, EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object); + + var flagKey = "multi_variate_feature"; + var decisionExpected = OptimizelyDecision.NewErrorDecision( + flagKey, + new OptimizelyUserContext(optimizely, UserID, new UserAttributes(), ErrorHandlerMock.Object, LoggerMock.Object), + DecisionMessage.SDK_NOT_READY, + ErrorHandlerMock.Object, + LoggerMock.Object); + var user = optimizely.CreateUserContext(UserID); + var decision = user.Decide(flagKey); + + Assert.IsTrue(TestData.CompareObjects(decision, decisionExpected)); + } + #endregion + + } +} diff --git a/OptimizelySDK/Bucketing/Bucketer.cs b/OptimizelySDK/Bucketing/Bucketer.cs index b8944687..be62727a 100644 --- a/OptimizelySDK/Bucketing/Bucketer.cs +++ b/OptimizelySDK/Bucketing/Bucketer.cs @@ -15,6 +15,7 @@ */ using OptimizelySDK.Entity; using OptimizelySDK.Logger; +using OptimizelySDK.OptimizelyDecisions; using System; using System.Collections.Generic; using System.Text; @@ -102,8 +103,9 @@ private string FindBucket(string bucketingId, string userId, string parentId, IE /// Experiment Experiment in which user is to be bucketed /// A customer-assigned value used to create the key for the murmur hash. /// User identifier + /// Decision log messages. /// Variation which will be shown to the user - public virtual Variation Bucket(ProjectConfig config, Experiment experiment, string bucketingId, string userId) + public virtual Variation Bucket(ProjectConfig config, Experiment experiment, string bucketingId, string userId, IDecisionReasons reasons) { string message; Variation variation; @@ -122,26 +124,26 @@ public virtual Variation Bucket(ProjectConfig config, Experiment experiment, str if (string.IsNullOrEmpty(userExperimentId)) { message = $"User [{userId}] is in no experiment."; - Logger.Log(LogLevel.INFO, message); + Logger.Log(LogLevel.INFO, reasons.AddInfo(message)); return new Variation(); } if (userExperimentId != experiment.Id) { message = $"User [{userId}] is not in experiment [{experiment.Key}] of group [{experiment.GroupId}]."; - Logger.Log(LogLevel.INFO, message); + Logger.Log(LogLevel.INFO, reasons.AddInfo(message)); return new Variation(); } message = $"User [{userId}] is in experiment [{experiment.Key}] of group [{experiment.GroupId}]."; - Logger.Log(LogLevel.INFO, message); + Logger.Log(LogLevel.INFO, reasons.AddInfo(message)); } // Bucket user if not in whitelist and in group (if any). string variationId = FindBucket(bucketingId, userId, experiment.Id, experiment.TrafficAllocation); if (string.IsNullOrEmpty(variationId)) { - Logger.Log(LogLevel.INFO, $"User [{userId}] is in no variation."); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"User [{userId}] is in no variation.")); return new Variation(); } @@ -149,7 +151,7 @@ public virtual Variation Bucket(ProjectConfig config, Experiment experiment, str // success! variation = config.GetVariationFromId(experiment.Key, variationId); message = $"User [{userId}] is in variation [{variation.Key}] of experiment [{experiment.Key}]."; - Logger.Log(LogLevel.INFO, message); + Logger.Log(LogLevel.INFO, reasons.AddInfo(message)); return variation; } } diff --git a/OptimizelySDK/Bucketing/DecisionService.cs b/OptimizelySDK/Bucketing/DecisionService.cs index 3959dbbd..3973b13e 100644 --- a/OptimizelySDK/Bucketing/DecisionService.cs +++ b/OptimizelySDK/Bucketing/DecisionService.cs @@ -19,6 +19,7 @@ using OptimizelySDK.Entity; using OptimizelySDK.ErrorHandler; using OptimizelySDK.Logger; +using OptimizelySDK.OptimizelyDecisions; using OptimizelySDK.Utils; namespace OptimizelySDK.Bucketing @@ -83,20 +84,43 @@ public DecisionService(Bucketer bucketer, IErrorHandler errorHandler, UserProfil /// The userId of the user. /// The user's attributes. This should be filtered to just attributes in the Datafile. /// The Variation the user is allocated into. - public virtual Variation GetVariation(Experiment experiment, string userId, ProjectConfig config, UserAttributes filteredAttributes) + public virtual Variation GetVariation(Experiment experiment, + string userId, + ProjectConfig config, + UserAttributes filteredAttributes) + { + return GetVariation(experiment, userId, config, filteredAttributes, new List(), DefaultDecisionReasons.NewInstance()); + } + + /// + /// Get a Variation of an Experiment for a user to be allocated into. + /// + /// The Experiment the user will be bucketed into. + /// The userId of the user. + /// The user's attributes. This should be filtered to just attributes in the Datafile. + /// The Variation the user is allocated into. + public virtual Variation GetVariation(Experiment experiment, + string userId, + ProjectConfig config, + UserAttributes filteredAttributes, + List options, + IDecisionReasons reasons) { if (!ExperimentUtils.IsExperimentActive(experiment, Logger)) return null; // check if a forced variation is set - var forcedVariation = GetForcedVariation(experiment.Key, userId, config); + var forcedVariation = GetForcedVariation(experiment.Key, userId, config, reasons); if (forcedVariation != null) return forcedVariation; - var variation = GetWhitelistedVariation(experiment, userId); + var variation = GetWhitelistedVariation(experiment, userId, reasons); if (variation != null) return variation; + // fetch the user profile map from the user profile service + var ignoreUPS = options.Contains(OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE); + UserProfile userProfile = null; - if (UserProfileService != null) + if (UserProfileService != null && !ignoreUPS) { try { @@ -104,21 +128,21 @@ public virtual Variation GetVariation(Experiment experiment, string userId, Proj if (userProfileMap != null && UserProfileUtil.IsValidUserProfileMap(userProfileMap)) { userProfile = UserProfileUtil.ConvertMapToUserProfile(userProfileMap); - variation = GetStoredVariation(experiment, userProfile, config); + variation = GetStoredVariation(experiment, userProfile, config, reasons); if (variation != null) return variation; } else if (userProfileMap == null) { - Logger.Log(LogLevel.INFO, "We were unable to get a user profile map from the UserProfileService."); + Logger.Log(LogLevel.INFO, reasons.AddInfo("We were unable to get a user profile map from the UserProfileService.")); } else { - Logger.Log(LogLevel.ERROR, "The UserProfileService returned an invalid map."); + Logger.Log(LogLevel.ERROR, reasons.AddInfo("The UserProfileService returned an invalid map.")); } } catch (Exception exception) { - Logger.Log(LogLevel.ERROR, exception.Message); + Logger.Log(LogLevel.ERROR, reasons.AddInfo(exception.Message)); ErrorHandler.HandleError(new Exceptions.OptimizelyRuntimeException(exception.Message)); } } @@ -126,16 +150,16 @@ public virtual Variation GetVariation(Experiment experiment, string userId, Proj if (ExperimentUtils.DoesUserMeetAudienceConditions(config, experiment, filteredAttributes, LOGGING_KEY_TYPE_EXPERIMENT, experiment.Key, Logger)) { // Get Bucketing ID from user attributes. - string bucketingId = GetBucketingId(userId, filteredAttributes); + string bucketingId = GetBucketingId(userId, filteredAttributes, reasons); - variation = Bucketer.Bucket(config, experiment, bucketingId, userId); + variation = Bucketer.Bucket(config, experiment, bucketingId, userId, reasons); if (variation != null && variation.Key != null) { if (UserProfileService != null) { var bucketerUserProfile = userProfile ?? new UserProfile(userId, new Dictionary()); - SaveVariation(experiment, variation, bucketerUserProfile); + SaveVariation(experiment, variation, bucketerUserProfile, reasons); } else @@ -144,7 +168,7 @@ public virtual Variation GetVariation(Experiment experiment, string userId, Proj return variation; } - Logger.Log(LogLevel.INFO, $"User \"{userId}\" does not meet conditions to be in experiment \"{experiment.Key}\"."); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"User \"{userId}\" does not meet conditions to be in experiment \"{experiment.Key}\".")); return null; } @@ -157,6 +181,18 @@ public virtual Variation GetVariation(Experiment experiment, string userId, Proj /// Project Config /// Variation entity which the given user and experiment should be forced into. public Variation GetForcedVariation(string experimentKey, string userId, ProjectConfig config) + { + return GetForcedVariation(experimentKey, userId, config, DefaultDecisionReasons.NewInstance()); + } + + /// + /// Gets the forced variation for the given user and experiment. + /// + /// The experiment key + /// The user ID + /// Project Config + /// Variation entity which the given user and experiment should be forced into. + public Variation GetForcedVariation(string experimentKey, string userId, ProjectConfig config, IDecisionReasons reasons) { if (ForcedVariationMap.ContainsKey(userId) == false) { @@ -191,8 +227,7 @@ public Variation GetForcedVariation(string experimentKey, string userId, Project // this case is logged in getVariationFromKey if (string.IsNullOrEmpty(variationKey)) return null; - - Logger.Log(LogLevel.DEBUG, $@"Variation ""{variationKey}"" is mapped to experiment ""{experimentKey}"" and user ""{userId}"" in the forced variation map"); + Logger.Log(LogLevel.DEBUG, reasons.AddInfo($@"Variation ""{variationKey}"" is mapped to experiment ""{experimentKey}"" and user ""{userId}"" in the forced variation map")); Variation variation = config.GetVariationFromKey(experimentKey, variationKey); @@ -248,8 +283,6 @@ public bool SetForcedVariation(string experimentKey, string userId, string varia Logger.Log(LogLevel.DEBUG, $@"Set variation ""{variationId}"" for experiment ""{experimentId}"" and user ""{userId}"" in the forced variation map."); return true; } - - /// /// Get the variation the user has been whitelisted into. /// @@ -258,6 +291,19 @@ public bool SetForcedVariation(string experimentKey, string userId, string varia /// if the user is not whitelisted into any variation {@link Variation} /// the user is bucketed into if the user has a specified whitelisted variation. public Variation GetWhitelistedVariation(Experiment experiment, string userId) + { + return GetWhitelistedVariation(experiment, userId, DefaultDecisionReasons.NewInstance()); + } + + /// + /// Get the variation the user has been whitelisted into. + /// + /// in which user is to be bucketed. + /// User Identifier + /// Decision log messages. + /// if the user is not whitelisted into any variation {@link Variation} + /// the user is bucketed into if the user has a specified whitelisted variation. + public Variation GetWhitelistedVariation(Experiment experiment, string userId, IDecisionReasons reasons) { //if a user has a forced variation mapping, return the respective variation Dictionary userIdToVariationKeyMap = experiment.UserIdToKeyVariations; @@ -271,9 +317,9 @@ public Variation GetWhitelistedVariation(Experiment experiment, string userId) : null; if (forcedVariation != null) - Logger.Log(LogLevel.INFO, $"User \"{userId}\" is forced in variation \"{forcedVariationKey}\"."); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"User \"{userId}\" is forced in variation \"{forcedVariationKey}\".")); else - Logger.Log(LogLevel.ERROR, $"Variation \"{forcedVariationKey}\" is not in the datafile. Not activating user \"{userId}\"."); + Logger.Log(LogLevel.ERROR, reasons.AddInfo($"Variation \"{forcedVariationKey}\" is not in the datafile. Not activating user \"{userId}\".")); return forcedVariation; } @@ -284,7 +330,7 @@ public Variation GetWhitelistedVariation(Experiment experiment, string userId) /// which the user was bucketed /// User profile of the user /// The user was previously bucketed into. - public Variation GetStoredVariation(Experiment experiment, UserProfile userProfile, ProjectConfig config) + public Variation GetStoredVariation(Experiment experiment, UserProfile userProfile, ProjectConfig config, IDecisionReasons reasons) { // ---------- Check User Profile for Sticky Bucketing ---------- // If a user profile instance is present then check it for a saved variation @@ -296,7 +342,7 @@ public Variation GetStoredVariation(Experiment experiment, UserProfile userProfi if (decision == null) { - Logger.Log(LogLevel.INFO, $"No previously activated variation of experiment \"{experimentKey}\" for user \"{userProfile.UserId}\" found in user profile."); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"No previously activated variation of experiment \"{experimentKey}\" for user \"{userProfile.UserId}\" found in user profile.")); return null; } @@ -310,11 +356,11 @@ public Variation GetStoredVariation(Experiment experiment, UserProfile userProfi if (savedVariation == null) { - Logger.Log(LogLevel.INFO, $"User \"{userProfile.UserId}\" was previously bucketed into variation with ID \"{variationId}\" for experiment \"{experimentId}\", but no matching variation was found for that user. We will re-bucket the user."); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"User \"{userProfile.UserId}\" was previously bucketed into variation with ID \"{variationId}\" for experiment \"{experimentId}\", but no matching variation was found for that user. We will re-bucket the user.")); return null; } - Logger.Log(LogLevel.INFO, $"Returning previously activated variation \"{savedVariation.Key}\" of experiment \"{experimentKey}\" for user \"{userProfile.UserId}\" from user profile."); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"Returning previously activated variation \"{savedVariation.Key}\" of experiment \"{experimentKey}\" for user \"{userProfile.UserId}\" from user profile.")); return savedVariation; } catch (Exception) @@ -322,7 +368,6 @@ public Variation GetStoredVariation(Experiment experiment, UserProfile userProfi return null; } } - /// /// Save a { @link Variation } of an { @link Experiment } for a user in the {@link UserProfileService}. /// @@ -330,6 +375,17 @@ public Variation GetStoredVariation(Experiment experiment, UserProfile userProfi /// The Variation to save. /// instance of the user information. public void SaveVariation(Experiment experiment, Variation variation, UserProfile userProfile) + { + SaveVariation(experiment, variation, userProfile, DefaultDecisionReasons.NewInstance()); + } + + /// + /// Save a { @link Variation } of an { @link Experiment } for a user in the {@link UserProfileService}. + /// + /// The experiment the user was buck + /// The Variation to save. + /// instance of the user information. + public void SaveVariation(Experiment experiment, Variation variation, UserProfile userProfile, IDecisionReasons reasons) { //only save if the user has implemented a user profile service if (UserProfileService == null) @@ -351,15 +407,15 @@ public void SaveVariation(Experiment experiment, Variation variation, UserProfil try { UserProfileService.Save(userProfile.ToMap()); - Logger.Log(LogLevel.INFO, $"Saved variation \"{variation.Id}\" of experiment \"{experiment.Id}\" for user \"{userProfile.UserId}\"."); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"Saved variation \"{variation.Id}\" of experiment \"{experiment.Id}\" for user \"{userProfile.UserId}\".")); } catch (Exception exception) { - Logger.Log(LogLevel.ERROR, $"Failed to save variation \"{variation.Id}\" of experiment \"{experiment.Id}\" for user \"{userProfile.UserId}\"."); + Logger.Log(LogLevel.ERROR, reasons.AddInfo($"Failed to save variation \"{variation.Id}\" of experiment \"{experiment.Id}\" for user \"{userProfile.UserId}\".")); ErrorHandler.HandleError(new Exceptions.OptimizelyRuntimeException(exception.Message)); } } - + /// /// Try to bucket the user into a rollout rule. /// Evaluate the user for rules in priority order by seeing if the user satisfies the audience. @@ -368,9 +424,14 @@ public void SaveVariation(Experiment experiment, Variation variation, UserProfil /// The feature flag the user wants to access. /// User Identifier /// The user's attributes. This should be filtered to just attributes in the Datafile. + /// Decision log messages. /// null if the user is not bucketed into the rollout or if the feature flag was not attached to a rollout. /// otherwise the FeatureDecision entity - public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag featureFlag, string userId, UserAttributes filteredAttributes, ProjectConfig config) + public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag featureFlag, + string userId, + UserAttributes filteredAttributes, + ProjectConfig config, + IDecisionReasons reasons) { if (featureFlag == null) { @@ -380,7 +441,7 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature if (string.IsNullOrEmpty(featureFlag.RolloutId)) { - Logger.Log(LogLevel.INFO, $"The feature flag \"{featureFlag.Key}\" is not used in a rollout."); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"The feature flag \"{featureFlag.Key}\" is not used in a rollout.")); return null; } @@ -388,7 +449,7 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature if (string.IsNullOrEmpty(rollout.Id)) { - Logger.Log(LogLevel.ERROR, $"The rollout with id \"{featureFlag.RolloutId}\" is not found in the datafile for feature flag \"{featureFlag.Key}\""); + Logger.Log(LogLevel.ERROR, reasons.AddInfo($"The rollout with id \"{featureFlag.RolloutId}\" is not found in the datafile for feature flag \"{featureFlag.Key}\"")); return null; } @@ -400,16 +461,16 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature var rolloutRulesLength = rollout.Experiments.Count; // Get Bucketing ID from user attributes. - string bucketingId = GetBucketingId(userId, filteredAttributes); + string bucketingId = GetBucketingId(userId, filteredAttributes, reasons); // For all rules before the everyone else rule for (int i = 0; i < rolloutRulesLength - 1; i++) { string loggingKey = (i + 1).ToString(); var rolloutRule = rollout.Experiments[i]; - if (ExperimentUtils.DoesUserMeetAudienceConditions(config, rolloutRule, filteredAttributes, LOGGING_KEY_TYPE_RULE, loggingKey, Logger)) + if (ExperimentUtils.DoesUserMeetAudienceConditions(config, rolloutRule, filteredAttributes, LOGGING_KEY_TYPE_RULE, loggingKey, reasons, Logger)) { - variation = Bucketer.Bucket(config, rolloutRule, bucketingId, userId); + variation = Bucketer.Bucket(config, rolloutRule, bucketingId, userId, reasons); if (variation == null || string.IsNullOrEmpty(variation.Id)) break; @@ -424,9 +485,9 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature // Get the last rule which is everyone else rule. var everyoneElseRolloutRule = rollout.Experiments[rolloutRulesLength - 1]; - if (ExperimentUtils.DoesUserMeetAudienceConditions(config, everyoneElseRolloutRule, filteredAttributes, LOGGING_KEY_TYPE_RULE, "Everyone Else", Logger)) + if (ExperimentUtils.DoesUserMeetAudienceConditions(config, everyoneElseRolloutRule, filteredAttributes, LOGGING_KEY_TYPE_RULE, "Everyone Else", reasons, Logger)) { - variation = Bucketer.Bucket(config, everyoneElseRolloutRule, bucketingId, userId); + variation = Bucketer.Bucket(config, everyoneElseRolloutRule, bucketingId, userId, reasons); if (variation != null && !string.IsNullOrEmpty(variation.Id)) { Logger.Log(LogLevel.DEBUG, $"User \"{userId}\" meets conditions for targeting rule \"Everyone Else\"."); @@ -450,7 +511,12 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature /// The user's attributes. This should be filtered to just attributes in the Datafile. /// null if the user is not bucketed into the rollout or if the feature flag was not attached to a rollout. /// Otherwise the FeatureDecision entity - public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag featureFlag, string userId, UserAttributes filteredAttributes, ProjectConfig config) + public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag featureFlag, + string userId, + UserAttributes filteredAttributes, + ProjectConfig config, + List options, + IDecisionReasons reasons) { if (featureFlag == null) { @@ -460,7 +526,7 @@ public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag feat if (featureFlag.ExperimentIds == null || featureFlag.ExperimentIds.Count == 0) { - Logger.Log(LogLevel.INFO, $"The feature flag \"{featureFlag.Key}\" is not used in any experiments."); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"The feature flag \"{featureFlag.Key}\" is not used in any experiments.")); return null; } @@ -471,16 +537,16 @@ public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag feat if (string.IsNullOrEmpty(experiment.Key)) continue; - var variation = GetVariation(experiment, userId, config, filteredAttributes); + var variation = GetVariation(experiment, userId, config, filteredAttributes, options, reasons); if (variation != null && !string.IsNullOrEmpty(variation.Id)) { - Logger.Log(LogLevel.INFO, $"The user \"{userId}\" is bucketed into experiment \"{experiment.Key}\" of feature \"{featureFlag.Key}\"."); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is bucketed into experiment \"{experiment.Key}\" of feature \"{featureFlag.Key}\".")); return new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST); } } - Logger.Log(LogLevel.INFO, $"The user \"{userId}\" is not bucketed into any of the experiments on the feature \"{featureFlag.Key}\"."); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is not bucketed into any of the experiments on the feature \"{featureFlag.Key}\".")); return null; } @@ -493,23 +559,44 @@ public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag feat /// null if the user is not bucketed into any variation or the FeatureDecision entity if the user is /// successfully bucketed. public virtual FeatureDecision GetVariationForFeature(FeatureFlag featureFlag, string userId, ProjectConfig config, UserAttributes filteredAttributes) + { + return GetVariationForFeature(featureFlag, userId, config, filteredAttributes, new List(), DefaultDecisionReasons.NewInstance()); + } + + /// + /// Get the variation the user is bucketed into for the FeatureFlag + /// + /// The feature flag the user wants to access. + /// User Identifier + /// The user's attributes. This should be filtered to just attributes in the Datafile. + /// The user's attributes. This should be filtered to just attributes in the Datafile. + /// An array of decision options. + /// Decision log messages. + /// null if the user is not bucketed into any variation or the FeatureDecision entity if the user is + /// successfully bucketed. + public virtual FeatureDecision GetVariationForFeature(FeatureFlag featureFlag, + string userId, + ProjectConfig config, + UserAttributes filteredAttributes, + List options, + IDecisionReasons reasons) { // Check if the feature flag has an experiment and the user is bucketed into that experiment. - var decision = GetVariationForFeatureExperiment(featureFlag, userId, filteredAttributes, config); + var decision = GetVariationForFeatureExperiment(featureFlag, userId, filteredAttributes, config, options, reasons); if (decision != null) return decision; // Check if the feature flag has rollout and the the user is bucketed into one of its rules. - decision = GetVariationForFeatureRollout(featureFlag, userId, filteredAttributes, config); + decision = GetVariationForFeatureRollout(featureFlag, userId, filteredAttributes, config, reasons); if (decision != null) { - Logger.Log(LogLevel.INFO, $"The user \"{userId}\" is bucketed into a rollout for feature flag \"{featureFlag.Key}\"."); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is bucketed into a rollout for feature flag \"{featureFlag.Key}\".")); return decision; } - Logger.Log(LogLevel.INFO, $"The user \"{userId}\" is not bucketed into a rollout for feature flag \"{featureFlag.Key}\"."); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is not bucketed into a rollout for feature flag \"{featureFlag.Key}\".")); return new FeatureDecision(null, null, FeatureDecision.DECISION_SOURCE_ROLLOUT); } @@ -519,7 +606,7 @@ public virtual FeatureDecision GetVariationForFeature(FeatureFlag featureFlag, s /// User Identifier /// The user's attributes. /// Bucketing Id if it is a string type in attributes, user Id otherwise. - private string GetBucketingId(string userId, UserAttributes filteredAttributes) + private string GetBucketingId(string userId, UserAttributes filteredAttributes, IDecisionReasons reasons) { string bucketingId = userId; @@ -533,7 +620,7 @@ private string GetBucketingId(string userId, UserAttributes filteredAttributes) } else { - Logger.Log(LogLevel.WARN, "BucketingID attribute is not a string. Defaulted to userId"); + Logger.Log(LogLevel.WARN, reasons.AddInfo("BucketingID attribute is not a string. Defaulted to userId")); } } diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs index b30d00ac..49901a1c 100644 --- a/OptimizelySDK/Optimizely.cs +++ b/OptimizelySDK/Optimizely.cs @@ -476,45 +476,45 @@ public virtual bool IsFeatureEnabled(string featureKey, string userId, UserAttri return false; bool featureEnabled = false; - var sourceInfo = new Dictionary(); - var decision = DecisionService.GetVariationForFeature(featureFlag, userId, config, userAttributes); - var variation = decision.Variation; - var decisionSource = decision?.Source ?? FeatureDecision.DECISION_SOURCE_ROLLOUT; - - SendImpressionEvent(decision.Experiment, variation, userId, userAttributes, config, featureKey, decisionSource); + var sourceInfo = new Dictionary(); + var decision = DecisionService.GetVariationForFeature(featureFlag, userId, config, userAttributes); + var variation = decision.Variation; + var decisionSource = decision?.Source ?? FeatureDecision.DECISION_SOURCE_ROLLOUT; - if (variation != null) - { - featureEnabled = variation.FeatureEnabled.GetValueOrDefault(); + SendImpressionEvent(decision.Experiment, variation, userId, userAttributes, config, featureKey, decisionSource); - // This information is only necessary for feature tests. - // For rollouts experiments and variations are an implementation detail only. - if (decision.Source == FeatureDecision.DECISION_SOURCE_FEATURE_TEST) - { - sourceInfo["experimentKey"] = decision.Experiment.Key; - sourceInfo["variationKey"] = variation.Key; - } - else + if (variation != null) { - Logger.Log(LogLevel.INFO, $@"The user ""{userId}"" is not being experimented on feature ""{featureKey}""."); - } - } + featureEnabled = variation.FeatureEnabled.GetValueOrDefault(); - if (featureEnabled == true) - Logger.Log(LogLevel.INFO, $@"Feature flag ""{featureKey}"" is enabled for user ""{userId}""."); - else - Logger.Log(LogLevel.INFO, $@"Feature flag ""{featureKey}"" is not enabled for user ""{userId}""."); + // This information is only necessary for feature tests. + // For rollouts experiments and variations are an implementation detail only. + if (decision.Source == FeatureDecision.DECISION_SOURCE_FEATURE_TEST) + { + sourceInfo["experimentKey"] = decision.Experiment.Key; + sourceInfo["variationKey"] = variation.Key; + } + else + { + Logger.Log(LogLevel.INFO, $@"The user ""{userId}"" is not being experimented on feature ""{featureKey}""."); + } + } - var decisionInfo = new Dictionary - { - { "featureKey", featureKey }, - { "featureEnabled", featureEnabled }, - { "source", decision.Source }, - { "sourceInfo", sourceInfo }, - }; + if (featureEnabled == true) + Logger.Log(LogLevel.INFO, $@"Feature flag ""{featureKey}"" is enabled for user ""{userId}""."); + else + Logger.Log(LogLevel.INFO, $@"Feature flag ""{featureKey}"" is not enabled for user ""{userId}""."); - NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, DecisionNotificationTypes.FEATURE, userId, - userAttributes ?? new UserAttributes(), decisionInfo); + var decisionInfo = new Dictionary + { + { "featureKey", featureKey }, + { "featureEnabled", featureEnabled }, + { "source", decision.Source }, + { "sourceInfo", sourceInfo }, + }; + + NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, DecisionNotificationTypes.FEATURE, userId, + userAttributes ?? new UserAttributes(), decisionInfo); return featureEnabled; } @@ -712,6 +712,139 @@ public OptimizelyUserContext CreateUserContext(string userId, return new OptimizelyUserContext(this, userId, userAttributes, ErrorHandler, Logger); } + public OptimizelyDecision Decide(OptimizelyUserContext user, + string key, + List options) + { + + var config = ProjectConfigManager?.GetConfig(); + if (config == null) + { + return OptimizelyDecision.NewErrorDecision(key, user, DecisionMessage.SDK_NOT_READY, ErrorHandler, Logger); + } + var userId = user.UserId; + var inputValues = new Dictionary + { + { USER_ID, userId }, + }; + + if (!ValidateStringInputs(inputValues)) + return null; + + var flag = config.GetFeatureFlagFromKey(key); + if (flag == null) + { + return OptimizelyDecision.NewErrorDecision(key, + user, + DecisionMessage.Reason(DecisionMessage.FLAG_KEY_INVALID, key), + ErrorHandler, Logger); + } + + var userAttributes = user.UserAttributes; + var decisionEventDispatched = false; + var allOptions = GetAllOptions(options); + var decisionReasons = DefaultDecisionReasons.NewInstance(allOptions); + + var flagDecision = DecisionService.GetVariationForFeature( + flag, + userId, + config, + userAttributes, + allOptions, + decisionReasons); + + var featureEnabled = false; + + var variation = flagDecision.Variation; + + if (variation != null) + { + featureEnabled = variation.FeatureEnabled.GetValueOrDefault(); + } + + if (featureEnabled) + { + Logger.Log(LogLevel.INFO, "Feature \"" + key + "\" is enabled for user \"" + userId + "\""); + } + else + { + Logger.Log(LogLevel.INFO, "Feature \"" + key + "\" is not enabled for user \"" + userId + "\""); + } + var variableMap = new Dictionary(); + if (flag?.Variables != null && !allOptions.Contains(OptimizelyDecideOption.EXCLUDE_VARIABLES)) + { + + foreach (var featureVariable in flag?.Variables) + { + string variableValue = featureVariable.DefaultValue; + if (featureEnabled) + { + var featureVariableUsageInstance = variation.GetFeatureVariableUsageFromId(featureVariable.Id); + if (featureVariableUsageInstance != null) + { + variableValue = featureVariableUsageInstance.Value; + } + } + + var typeCastedValue = GetTypeCastedVariableValue(variableValue, featureVariable.Type); + + if (typeCastedValue is OptimizelyJSON) + typeCastedValue = ((OptimizelyJSON)typeCastedValue).ToDictionary(); + + variableMap.Add(featureVariable.Key, typeCastedValue); + } + } + + var optimizelyJSON = new OptimizelyJSON(variableMap, ErrorHandler, Logger); + + var decisionSource = flagDecision?.Source ?? FeatureDecision.DECISION_SOURCE_ROLLOUT; + if (!allOptions.Contains(OptimizelyDecideOption.DISABLE_DECISION_EVENT)) + { + + SendImpressionEvent(flagDecision.Experiment, variation, userId, userAttributes, config, key, decisionSource); + decisionEventDispatched = true; + } + List reasonsToReport = decisionReasons.ToReport(); + string variationKey = flagDecision.Variation?.Key; + + // TODO: add ruleKey values when available later. use a copy of experimentKey until then. + string ruleKey = flagDecision.Experiment?.Key; + + var decisionInfo = new Dictionary + { + { "flagKey", key }, + { "enabled", featureEnabled }, + { "variables", variableMap }, + { "variationKey", variationKey }, + { "ruleKey", ruleKey }, + { "reasons", decisionReasons }, + { "decisionEventDispatched", decisionEventDispatched }, + { "featureEnabled", featureEnabled }, + }; + + NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, DecisionNotificationTypes.FEATURE, userId, + userAttributes ?? new UserAttributes(), decisionInfo); + + return new OptimizelyDecision( + variationKey, + featureEnabled, + optimizelyJSON, + ruleKey, + key, + user, + reasonsToReport); + } + + private List GetAllOptions(List options) + { + var copiedOptions = new List(DefaultDecideOptions); + if (options != null) + { + copiedOptions.AddRange(options); + } + return copiedOptions; + } + /// /// Sends impression event. /// diff --git a/OptimizelySDK/OptimizelyUserContext.cs b/OptimizelySDK/OptimizelyUserContext.cs index e2621f20..07834546 100644 --- a/OptimizelySDK/OptimizelyUserContext.cs +++ b/OptimizelySDK/OptimizelyUserContext.cs @@ -46,7 +46,7 @@ public OptimizelyUserContext(Optimizely optimizely, string userId, UserAttribute /// value An attribute value public void SetAttribute(string key, object value) { - UserAttributes.Add(key, value); + UserAttributes[key] = value; } /// @@ -74,7 +74,7 @@ public OptimizelyDecision Decide(string key) public OptimizelyDecision Decide(string key, List options) { - return null; + return Optimizely.Decide(this, key, options); } /// diff --git a/OptimizelySDK/Utils/ExperimentUtils.cs b/OptimizelySDK/Utils/ExperimentUtils.cs index 470b014c..b6023480 100644 --- a/OptimizelySDK/Utils/ExperimentUtils.cs +++ b/OptimizelySDK/Utils/ExperimentUtils.cs @@ -17,6 +17,7 @@ using OptimizelySDK.AudienceConditions; using OptimizelySDK.Entity; using OptimizelySDK.Logger; +using OptimizelySDK.OptimizelyDecisions; namespace OptimizelySDK.Utils { @@ -35,6 +36,24 @@ public static bool IsExperimentActive(Experiment experiment, ILogger logger) return true; } + /// + /// Check if the user meets audience conditions to be in experiment or not + /// + /// ProjectConfig Configuration for the project + /// Experiment Entity representing the experiment + /// Attributes of the user. Defaults to empty attributes array if not provided + /// It can be either experiment or rule. + /// In case loggingKeyType is experiment it will be experiment key or else it will be rule number. + /// true if the user meets audience conditions to be in experiment, false otherwise. + public static bool DoesUserMeetAudienceConditions(ProjectConfig config, + Experiment experiment, + UserAttributes userAttributes, + string loggingKeyType, + string loggingKey, + ILogger logger) + { + return DoesUserMeetAudienceConditions(config, experiment, userAttributes, loggingKeyType, loggingKey, DefaultDecisionReasons.NewInstance(), logger); + } /// /// Check if the user meets audience conditions to be in experiment or not @@ -44,12 +63,14 @@ public static bool IsExperimentActive(Experiment experiment, ILogger logger) /// Attributes of the user. Defaults to empty attributes array if not provided /// It can be either experiment or rule. /// In case loggingKeyType is experiment it will be experiment key or else it will be rule number. + /// Decision log messages. /// true if the user meets audience conditions to be in experiment, false otherwise. public static bool DoesUserMeetAudienceConditions(ProjectConfig config, Experiment experiment, UserAttributes userAttributes, string loggingKeyType, string loggingKey, + IDecisionReasons reasons, ILogger logger) { if (userAttributes == null) @@ -73,7 +94,7 @@ public static bool DoesUserMeetAudienceConditions(ProjectConfig config, var result = expConditions.Evaluate(config, userAttributes, logger).GetValueOrDefault(); var resultText = result.ToString().ToUpper(); - logger.Log(LogLevel.INFO, $@"Audiences for {loggingKeyType} ""{loggingKey}"" collectively evaluated to {resultText}"); + logger.Log(LogLevel.INFO, reasons.AddInfo($@"Audiences for {loggingKeyType} ""{loggingKey}"" collectively evaluated to {resultText}")); return result; } } From 9e36946f438bda4d0e59b7f0d4bc0956c45125b3 Mon Sep 17 00:00:00 2001 From: muhammadnoman Date: Wed, 11 Nov 2020 23:02:21 +0500 Subject: [PATCH 08/34] Revert "Added UserContext add reason logs" This reverts commit 6c9ac4d72c9a7a0cdf9539b70caed58145bc76a5. --- OptimizelySDK.Tests/BucketerTest.cs | 23 +- OptimizelySDK.Tests/DecisionServiceTest.cs | 93 ++++---- .../OptimizelyUserContextTest.cs | 201 ------------------ OptimizelySDK/Bucketing/Bucketer.cs | 14 +- OptimizelySDK/Bucketing/DecisionService.cs | 177 ++++----------- OptimizelySDK/Optimizely.cs | 199 +++-------------- OptimizelySDK/OptimizelyUserContext.cs | 4 +- OptimizelySDK/Utils/ExperimentUtils.cs | 23 +- 8 files changed, 144 insertions(+), 590 deletions(-) delete mode 100644 OptimizelySDK.Tests/OptimizelyUserContextTest.cs diff --git a/OptimizelySDK.Tests/BucketerTest.cs b/OptimizelySDK.Tests/BucketerTest.cs index 78b0feec..aeeeb96c 100644 --- a/OptimizelySDK.Tests/BucketerTest.cs +++ b/OptimizelySDK.Tests/BucketerTest.cs @@ -19,7 +19,6 @@ using OptimizelySDK.Config; using OptimizelySDK.Entity; using OptimizelySDK.Logger; -using OptimizelySDK.OptimizelyDecisions; namespace OptimizelySDK.Tests { @@ -99,7 +98,7 @@ public void TestBucketValidExperimentNotInGroup() // control Assert.AreEqual(new Variation { Id = "7722370027", Key = "control" }, - bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance())); + bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId)); LoggerMock.Verify(l => l.Log(It.IsAny(), It.IsAny()), Times.Exactly(2)); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [3000] to user [testUserId] with bucketing ID [testBucketingIdControl!].")); @@ -107,7 +106,7 @@ public void TestBucketValidExperimentNotInGroup() // variation Assert.AreEqual(new Variation { Id = "7721010009", Key = "variation" }, - bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance())); + bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId)); LoggerMock.Verify(l => l.Log(It.IsAny(), It.IsAny()), Times.Exactly(4)); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [7000] to user [testUserId] with bucketing ID [testBucketingIdControl!].")); @@ -115,7 +114,7 @@ public void TestBucketValidExperimentNotInGroup() // no variation Assert.AreEqual(new Variation { }, - bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance())); + bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId)); LoggerMock.Verify(l => l.Log(It.IsAny(), It.IsAny()), Times.Exactly(6)); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [9000] to user [testUserId] with bucketing ID [testBucketingIdControl!].")); @@ -131,7 +130,7 @@ public void TestBucketValidExperimentInGroup() // variation 1 bucketer.SetBucketValues(new[] { 1000, 4000 }); Assert.AreEqual(new Variation { Id = "7722260071", Key = "group_exp_1_var_1" }, - bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance())); + bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId)); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [1000] to user [testUserId] with bucketing ID [testBucketingIdControl!].")); LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [testUserId] is in experiment [group_experiment_1] of group [7722400015].")); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [4000] to user [testUserId] with bucketing ID [testBucketingIdControl!].")); @@ -140,7 +139,7 @@ public void TestBucketValidExperimentInGroup() // variation 2 bucketer.SetBucketValues(new[] { 1500, 7000 }); Assert.AreEqual(new Variation { Id = "7722360022", Key = "group_exp_1_var_2" }, - bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance())); + bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId)); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [1500] to user [testUserId] with bucketing ID [testBucketingIdControl!].")); LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [testUserId] is in experiment [group_experiment_1] of group [7722400015].")); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [7000] to user [testUserId] with bucketing ID [testBucketingIdControl!].")); @@ -149,7 +148,7 @@ public void TestBucketValidExperimentInGroup() // User not in experiment bucketer.SetBucketValues(new[] { 5000, 7000 }); Assert.AreEqual(new Variation { }, - bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance())); + bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId)); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [5000] to user [testUserId] with bucketing ID [testBucketingIdControl!].")); LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [testUserId] is not in experiment [group_experiment_1] of group [7722400015].")); @@ -162,7 +161,7 @@ public void TestBucketInvalidExperiment() var bucketer = new Bucketer(LoggerMock.Object); Assert.AreEqual(new Variation { }, - bucketer.Bucket(Config, new Experiment(), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance())); + bucketer.Bucket(Config, new Experiment(), TestBucketingIdControl, TestUserId)); LoggerMock.Verify(l => l.Log(It.IsAny(), It.IsAny()), Times.Never); } @@ -177,7 +176,7 @@ public void TestBucketWithBucketingId() // make sure that the bucketing ID is used for the variation bucketing and not the user ID Assert.AreEqual(expectedVariation, - bucketer.Bucket(Config, experiment, TestBucketingIdControl, TestUserIdBucketsToVariation, DefaultDecisionReasons.NewInstance())); + bucketer.Bucket(Config, experiment, TestBucketingIdControl, TestUserIdBucketsToVariation)); } // Test for invalid experiment keys, null variation should be returned @@ -188,7 +187,7 @@ public void TestBucketVariationInvalidExperimentsWithBucketingId() var expectedVariation = new Variation(); Assert.AreEqual(expectedVariation, - bucketer.Bucket(Config, Config.GetExperimentFromKey("invalid_experiment"), TestBucketingIdVariation, TestUserId, DefaultDecisionReasons.NewInstance())); + bucketer.Bucket(Config, Config.GetExperimentFromKey("invalid_experiment"), TestBucketingIdVariation, TestUserId)); } // Make sure that the bucketing ID is used to bucket the user into a group and not the user ID @@ -201,7 +200,7 @@ public void TestBucketVariationGroupedExperimentsWithBucketingId() Assert.AreEqual(expectedGroupVariation, bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_2"), - TestBucketingIdGroupExp2Var2, TestUserIdBucketsToNoGroup, DefaultDecisionReasons.NewInstance())); + TestBucketingIdGroupExp2Var2, TestUserIdBucketsToNoGroup)); } // Make sure that user gets bucketed into the rollout rule. @@ -214,7 +213,7 @@ public void TestBucketRolloutRule() var expectedVariation = Config.GetVariationFromId(rolloutRule.Key, "177773"); Assert.True(TestData.CompareObjects(expectedVariation, - bucketer.Bucket(Config, rolloutRule, "testBucketingId", TestUserId, DefaultDecisionReasons.NewInstance()))); + bucketer.Bucket(Config, rolloutRule, "testBucketingId", TestUserId))); } } } \ No newline at end of file diff --git a/OptimizelySDK.Tests/DecisionServiceTest.cs b/OptimizelySDK.Tests/DecisionServiceTest.cs index 3c83b3db..0936a857 100644 --- a/OptimizelySDK.Tests/DecisionServiceTest.cs +++ b/OptimizelySDK.Tests/DecisionServiceTest.cs @@ -24,7 +24,6 @@ using OptimizelySDK.Bucketing; using OptimizelySDK.Utils; using OptimizelySDK.Config; -using OptimizelySDK.OptimizelyDecisions; namespace OptimizelySDK.Tests { @@ -85,7 +84,7 @@ public void TestGetVariationForcedVariationPrecedesAudienceEval() LoggerMock.Verify(l => l.Log(LogLevel.INFO, string.Format("User \"{0}\" is forced in variation \"vtag5\".", WhitelistedUserId)), Times.Once); // no attributes provided for a experiment that has an audience Assert.IsTrue(TestData.CompareObjects(actualVariation, expectedVariation)); - BucketerMock.Verify(_ => _.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + BucketerMock.Verify(_ => _.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Test] @@ -112,7 +111,7 @@ public void TestGetVariationEvaluatesUserProfileBeforeAudienceTargeting() // ensure that a user with a saved user profile, sees the same variation regardless of audience evaluation decisionService.GetVariation(experiment, UserProfileId, ProjectConfig, new UserAttributes()); - BucketerMock.Verify(_ => _.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + BucketerMock.Verify(_ => _.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Test] @@ -124,7 +123,7 @@ public void TestGetForcedVariationReturnsForcedVariation() LoggerMock.Verify(l => l.Log(LogLevel.INFO, string.Format("User \"{0}\" is forced in variation \"{1}\".", WhitelistedUserId, WhitelistedVariation.Key)), Times.Once); - BucketerMock.Verify(_ => _.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + BucketerMock.Verify(_ => _.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Test] @@ -165,7 +164,7 @@ public void TestGetForcedVariationWithInvalidVariation() string.Format("Variation \"{0}\" is not in the datafile. Not activating user \"{1}\".", invalidVariationKey, userId)), Times.Once); - BucketerMock.Verify(_ => _.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + BucketerMock.Verify(_ => _.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Test] @@ -217,7 +216,7 @@ public void TestGetStoredVariationLogsWhenLookupReturnsNull() DecisionService decisionService = new DecisionService(bucketer, ErrorHandlerMock.Object, userProfileService, LoggerMock.Object); - Assert.IsNull(decisionService.GetStoredVariation(experiment, userProfile, ProjectConfig, DefaultDecisionReasons.NewInstance())); + Assert.IsNull(decisionService.GetStoredVariation(experiment, userProfile, ProjectConfig)); LoggerMock.Verify(l => l.Log(LogLevel.INFO, string.Format("No previously activated variation of experiment \"{0}\" for user \"{1}\" found in user profile." , experiment.Key, UserProfileId)), Times.Once); @@ -242,7 +241,7 @@ public void TestGetStoredVariationReturnsNullWhenVariationIsNoLongerInConfig() DecisionService decisionService = new DecisionService(bucketer, ErrorHandlerMock.Object, UserProfileServiceMock.Object, LoggerMock.Object); - Assert.IsNull(decisionService.GetStoredVariation(experiment, storedUserProfile, ProjectConfig, DefaultDecisionReasons.NewInstance())); + Assert.IsNull(decisionService.GetStoredVariation(experiment, storedUserProfile, ProjectConfig)); LoggerMock.Verify(l => l.Log(LogLevel.INFO, string.Format("User \"{0}\" was previously bucketed into variation with ID \"{1}\" for experiment \"{2}\", but no matching variation was found for that user. We will re-bucket the user." , UserProfileId, storedVariationId, experiment.Id)), Times.Once); } @@ -266,7 +265,7 @@ public void TestGetVariationSavesBucketedVariationIntoUserProfile() }); var mockBucketer = new Mock(LoggerMock.Object); - mockBucketer.Setup(m => m.Bucket(ProjectConfig, experiment, UserProfileId, UserProfileId, It.IsAny())).Returns(variation); + mockBucketer.Setup(m => m.Bucket(ProjectConfig, experiment, UserProfileId, UserProfileId)).Returns(variation); DecisionService decisionService = new DecisionService(mockBucketer.Object, ErrorHandlerMock.Object, UserProfileServiceMock.Object, LoggerMock.Object); @@ -318,7 +317,7 @@ public void TestGetVariationSavesANewUserProfile() }); var mockBucketer = new Mock(LoggerMock.Object); - mockBucketer.Setup(m => m.Bucket(ProjectConfig, experiment, UserProfileId, UserProfileId, It.IsAny())).Returns(variation); + mockBucketer.Setup(m => m.Bucket(ProjectConfig, experiment, UserProfileId, UserProfileId)).Returns(variation); Dictionary userProfile = null; @@ -469,7 +468,7 @@ public void TestGetVariationWithBucketingId() public void TestGetVariationForFeatureExperimentGivenNullExperimentIds() { var featureFlag = ProjectConfig.GetFeatureFlagFromKey("empty_feature"); - var decision = DecisionService.GetVariationForFeatureExperiment(featureFlag, GenericUserId, new UserAttributes() { }, ProjectConfig, new List(), DefaultDecisionReasons.NewInstance()); + var decision = DecisionService.GetVariationForFeatureExperiment(featureFlag, GenericUserId, new UserAttributes() { }, ProjectConfig); Assert.IsNull(decision); @@ -488,7 +487,7 @@ public void TestGetVariationForFeatureExperimentGivenExperimentNotInDataFile() ExperimentIds = new List { "29039203" } }; - var decision = DecisionService.GetVariationForFeatureExperiment(featureFlag, GenericUserId, new UserAttributes() { }, ProjectConfig, new List(), DefaultDecisionReasons.NewInstance()); + var decision = DecisionService.GetVariationForFeatureExperiment(featureFlag, GenericUserId, new UserAttributes() { }, ProjectConfig); Assert.IsNull(decision); LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Experiment ID \"29039203\" is not in datafile.")); @@ -503,7 +502,7 @@ public void TestGetVariationForFeatureExperimentGivenNonMutexGroupAndUserNotBuck DecisionServiceMock.Setup(ds => ds.GetVariation(multiVariateExp, "user1", ProjectConfig, null)).Returns(null); var featureFlag = ProjectConfig.GetFeatureFlagFromKey("multi_variate_feature"); - var decision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", new UserAttributes(), ProjectConfig, new List(), DefaultDecisionReasons.NewInstance()); + var decision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", new UserAttributes(), ProjectConfig); Assert.IsNull(decision); LoggerMock.Verify(l => l.Log(LogLevel.INFO, "The user \"user1\" is not bucketed into any of the experiments on the feature \"multi_variate_feature\".")); @@ -519,10 +518,10 @@ public void TestGetVariationForFeatureExperimentGivenNonMutexGroupAndUserIsBucke var userAttributes = new UserAttributes(); DecisionServiceMock.Setup(ds => ds.GetVariation(ProjectConfig.GetExperimentFromKey("test_experiment_multivariate"), - "user1", ProjectConfig, userAttributes, It.IsAny>(), It.IsAny())).Returns(variation); + "user1", ProjectConfig, userAttributes)).Returns(variation); var featureFlag = ProjectConfig.GetFeatureFlagFromKey("multi_variate_feature"); - var decision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig, new List(), DefaultDecisionReasons.NewInstance()); + var decision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig); Assert.IsTrue(TestData.CompareObjects(expectedDecision, decision)); @@ -542,7 +541,7 @@ public void TestGetVariationForFeatureExperimentGivenMutexGroupAndUserIsBucketed userAttributes)).Returns(variation); var featureFlag = ProjectConfig.GetFeatureFlagFromKey("boolean_feature"); - var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig, new List(), DefaultDecisionReasons.NewInstance()); + var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig); Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision)); @@ -554,11 +553,11 @@ public void TestGetVariationForFeatureExperimentGivenMutexGroupAndUserIsBucketed public void TestGetVariationForFeatureExperimentGivenMutexGroupAndUserNotBucketed() { var mutexExperiment = ProjectConfig.GetExperimentFromKey("group_experiment_1"); - DecisionServiceMock.Setup(ds => ds.GetVariation(It.IsAny(), It.IsAny(), ProjectConfig, It.IsAny(), It.IsAny>(), It.IsAny())). + DecisionServiceMock.Setup(ds => ds.GetVariation(It.IsAny(), It.IsAny(), ProjectConfig, It.IsAny())). Returns(null); var featureFlag = ProjectConfig.GetFeatureFlagFromKey("boolean_feature"); - var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", new UserAttributes(), ProjectConfig, new List(), DefaultDecisionReasons.NewInstance()); + var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", new UserAttributes(), ProjectConfig); Assert.IsNull(actualDecision); @@ -583,7 +582,7 @@ public void TestGetVariationForFeatureRolloutWhenNoRuleInRollouts() var decisionService = new DecisionService(new Bucketer(new NoOpLogger()), new NoOpErrorHandler(), null, new NoOpLogger()); - var variation = decisionService.GetVariationForFeatureRollout(featureFlag, "userId1", null, projectConfig, DefaultDecisionReasons.NewInstance()); + var variation = decisionService.GetVariationForFeatureRollout(featureFlag, "userId1", null, projectConfig); Assert.IsNull(variation); } @@ -601,9 +600,9 @@ public void TestGetVariationForFeatureRolloutWhenRolloutIsNotInDataFile() Variables = featureFlag.Variables }; - DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny(), It.IsAny(), It.IsAny(), ProjectConfig, new List(), DefaultDecisionReasons.NewInstance())).Returns(null); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny(), It.IsAny(), It.IsAny(), ProjectConfig)).Returns(null); - var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureRollout(featureFlag, "user1", new UserAttributes(), ProjectConfig, DefaultDecisionReasons.NewInstance()); + var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureRollout(featureFlag, "user1", new UserAttributes(), ProjectConfig); Assert.IsNull(actualDecision); LoggerMock.Verify(l => l.Log(LogLevel.INFO, "The feature flag \"boolean_feature\" is not used in a rollout.")); @@ -625,10 +624,10 @@ public void TestGetVariationForFeatureRolloutWhenUserIsBucketedInTheTargetingRul }; BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny())).Returns(variation); + It.IsAny())).Returns(variation); var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, null, LoggerMock.Object); - var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes, ProjectConfig, DefaultDecisionReasons.NewInstance()); + var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes, ProjectConfig); Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision)); } @@ -649,11 +648,11 @@ public void TestGetVariationForFeatureRolloutWhenUserIsNotBucketedInTheTargeting { "browser_type", "chrome" } }; - BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), experiment, It.IsAny(), It.IsAny(), It.IsAny())).Returns(null); - BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), everyoneElseRule, It.IsAny(), It.IsAny(), It.IsAny())).Returns(variation); + BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), experiment, It.IsAny(), It.IsAny())).Returns(null); + BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), everyoneElseRule, It.IsAny(), It.IsAny())).Returns(variation); var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, null, LoggerMock.Object); - var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes, ProjectConfig, DefaultDecisionReasons.NewInstance()); + var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes, ProjectConfig); Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision)); } @@ -669,10 +668,10 @@ public void TestGetVariationForFeatureRolloutWhenUserIsNeitherBucketedInTheTarge { "browser_type", "chrome" } }; - BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(null); + BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(null); var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, null, LoggerMock.Object); - var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes, ProjectConfig, DefaultDecisionReasons.NewInstance()); + var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes, ProjectConfig); Assert.IsNull(actualDecision); } @@ -690,11 +689,11 @@ public void TestGetVariationForFeatureRolloutWhenUserDoesNotQualifyForAnyTargeti var variation = everyoneElseRule.Variations[0]; var expectedDecision = new FeatureDecision(everyoneElseRule, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT); - BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), everyoneElseRule, It.IsAny(), It.IsAny(), It.IsAny())).Returns(variation); + BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), everyoneElseRule, It.IsAny(), It.IsAny())).Returns(variation); var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, null, LoggerMock.Object); // Provide null attributes so that user does not qualify for audience. - var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", null, ProjectConfig, DefaultDecisionReasons.NewInstance()); + var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", null, ProjectConfig); Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision)); @@ -724,7 +723,7 @@ public void TestGetVariationForFeatureRolloutAudienceAndTrafficeAllocationCheck( { { "device_type", "iPhone" }, { "location", "San Francisco" } - }, ProjectConfig, DefaultDecisionReasons.NewInstance()); + }, ProjectConfig); // Returned variation id should be '177773' because of audience 'iPhone users in San Francisco'. var expectedDecision = new FeatureDecision(expWithAudienceiPhoneUsers, varWithAudienceiPhoneUsers, FeatureDecision.DECISION_SOURCE_ROLLOUT); @@ -734,7 +733,7 @@ public void TestGetVariationForFeatureRolloutAudienceAndTrafficeAllocationCheck( actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, new UserAttributes { { "browser_type", "chrome" } - }, ProjectConfig, DefaultDecisionReasons.NewInstance()); + }, ProjectConfig); // Returned variation id should be '177771' because of audience 'Chrome users'. expectedDecision = new FeatureDecision(expWithAudienceChromeUsers, varWithAudienceChromeUsers, FeatureDecision.DECISION_SOURCE_ROLLOUT); @@ -742,7 +741,7 @@ public void TestGetVariationForFeatureRolloutAudienceAndTrafficeAllocationCheck( // Calling with no audience. mockBucketer.Setup(bm => bm.GenerateBucketValue(It.IsAny())).Returns(8000); - actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, new UserAttributes(), ProjectConfig, DefaultDecisionReasons.NewInstance()); + actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, new UserAttributes(), ProjectConfig); // Returned variation id should be of everyone else rule because of no audience. expectedDecision = new FeatureDecision(expWithNoAudience, varWithNoAudience, FeatureDecision.DECISION_SOURCE_ROLLOUT); @@ -753,7 +752,7 @@ public void TestGetVariationForFeatureRolloutAudienceAndTrafficeAllocationCheck( actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, new UserAttributes { { "browser_type", "chrome" } - }, ProjectConfig, DefaultDecisionReasons.NewInstance()); + }, ProjectConfig); // Returned decision entity should be null because bucket value exceeds traffice allocation of everyone else rule. Assert.Null(actualDecision); @@ -769,21 +768,21 @@ public void TestGetVariationForFeatureRolloutCheckAudienceInEveryoneElseRule() var variation = everyoneElseRule.Variations[0]; var expectedDecision = new FeatureDecision(everyoneElseRule, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT); - BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), everyoneElseRule, It.IsAny(), WhitelistedUserId, It.IsAny())).Returns(variation); - BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), everyoneElseRule, It.IsAny(), GenericUserId, It.IsAny())).Returns(null); + BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), everyoneElseRule, It.IsAny(), WhitelistedUserId)).Returns(variation); + BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), everyoneElseRule, It.IsAny(), GenericUserId)).Returns(null); var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, null, LoggerMock.Object); // Returned variation id should be of everyone else rule as it passes audience Id checking. - var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, WhitelistedUserId, null, ProjectConfig, DefaultDecisionReasons.NewInstance()); + var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, WhitelistedUserId, null, ProjectConfig); Assert.True(TestData.CompareObjects(expectedDecision, actualDecision)); // Returned variation id should be null. - actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, null, ProjectConfig, DefaultDecisionReasons.NewInstance()); + actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, null, ProjectConfig); Assert.Null(actualDecision); // Returned variation id should be null as it fails audience Id checking. everyoneElseRule.AudienceIds = new string[] { ProjectConfig.Audiences[0].Id }; - actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, null, ProjectConfig, DefaultDecisionReasons.NewInstance()); + actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, null, ProjectConfig); Assert.Null(actualDecision); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "User \"testUser1\" does not meet the conditions for targeting rule \"1\"."), Times.Once); @@ -808,7 +807,7 @@ public void TestGetVariationForFeatureWhenTheUserIsBucketedIntoFeatureExperiment var expectedDecision = new FeatureDecision(expectedExperiment, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST); DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny(), It.IsAny(), - It.IsAny(), ProjectConfig, It.IsAny>(), It.IsAny())).Returns(expectedDecision); + It.IsAny(), ProjectConfig)).Returns(expectedDecision); var actualDecision = DecisionServiceMock.Object.GetVariationForFeature(featureFlag, "user1", ProjectConfig, new UserAttributes()); Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision)); @@ -827,9 +826,9 @@ public void TestGetVariationForFeatureWhenTheUserIsNotBucketedIntoFeatureExperim var expectedDecision = new FeatureDecision(expectedExperiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT); DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny(), It.IsAny(), - It.IsAny(), ProjectConfig, It.IsAny>(), It.IsAny())).Returns(null); + It.IsAny(), ProjectConfig)).Returns(null); DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureRollout(It.IsAny(), It.IsAny(), - It.IsAny(), ProjectConfig, It.IsAny())).Returns(expectedDecision); + It.IsAny(), ProjectConfig)).Returns(expectedDecision); var actualDecision = DecisionServiceMock.Object.GetVariationForFeature(featureFlag, "user1", ProjectConfig, new UserAttributes()); @@ -845,8 +844,8 @@ public void TestGetVariationForFeatureWhenTheUserIsNeitherBucketedIntoFeatureExp var featureFlag = ProjectConfig.GetFeatureFlagFromKey("string_single_variable_feature"); var expectedDecision = new FeatureDecision(null, null, FeatureDecision.DECISION_SOURCE_ROLLOUT); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny(), It.IsAny(), It.IsAny(), ProjectConfig, new List(), DefaultDecisionReasons.NewInstance())).Returns(null); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureRollout(It.IsAny(), It.IsAny(), It.IsAny(), ProjectConfig, DefaultDecisionReasons.NewInstance())).Returns(null); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny(), It.IsAny(), It.IsAny(), ProjectConfig)).Returns(null); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureRollout(It.IsAny(), It.IsAny(), It.IsAny(), ProjectConfig)).Returns(null); var actualDecision = DecisionServiceMock.Object.GetVariationForFeature(featureFlag, "user1", ProjectConfig, new UserAttributes()); Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision)); @@ -867,8 +866,8 @@ public void TestGetVariationForFeatureWhenTheUserIsBuckedtedInBothExperimentAndR { "browser_type", "chrome" } }; - DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, "user1", ProjectConfig, userAttributes, It.IsAny>(), It.IsAny())).Returns(variation); - var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig, new List(), DefaultDecisionReasons.NewInstance()); + DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, "user1", ProjectConfig, userAttributes)).Returns(variation); + var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig); // The user is bucketed into feature experiment's variation. Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision)); @@ -878,8 +877,8 @@ public void TestGetVariationForFeatureWhenTheUserIsBuckedtedInBothExperimentAndR var rolloutVariation = rolloutExperiment.Variations[0]; var expectedRolloutDecision = new FeatureDecision(rolloutExperiment, rolloutVariation, FeatureDecision.DECISION_SOURCE_ROLLOUT); - BucketerMock.Setup(bm => bm.Bucket(ProjectConfig, rolloutExperiment, It.IsAny(), It.IsAny(), It.IsAny())).Returns(rolloutVariation); - var actualRolloutDecision = DecisionServiceMock.Object.GetVariationForFeatureRollout(featureFlag, "user1", userAttributes, ProjectConfig, DefaultDecisionReasons.NewInstance()); + BucketerMock.Setup(bm => bm.Bucket(ProjectConfig, rolloutExperiment, It.IsAny(), It.IsAny())).Returns(rolloutVariation); + var actualRolloutDecision = DecisionServiceMock.Object.GetVariationForFeatureRollout(featureFlag, "user1", userAttributes, ProjectConfig); // The user is bucketed into feature rollout's variation. diff --git a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs deleted file mode 100644 index 029c8499..00000000 --- a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs +++ /dev/null @@ -1,201 +0,0 @@ -/** - * - * Copyright 2020, Optimizely 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. - */ - -using Castle.Core.Internal; -using Moq; -using NUnit.Framework; -using OptimizelySDK.Entity; -using OptimizelySDK.ErrorHandler; -using OptimizelySDK.Event.Dispatcher; -using OptimizelySDK.Logger; -using OptimizelySDK.OptimizelyDecisions; -using System; -using System.Collections.Generic; - -namespace OptimizelySDK.Tests -{ - [TestFixture] - public class OptimizelyUserContextTest - { - string UserID = "testUserID"; - private Optimizely Optimizely; - private Mock LoggerMock; - private Mock ErrorHandlerMock; - private Mock EventDispatcherMock; - - [SetUp] - public void SetUp() - { - LoggerMock = new Mock(); - LoggerMock.Setup(i => i.Log(It.IsAny(), It.IsAny())); - - ErrorHandlerMock = new Mock(); - ErrorHandlerMock.Setup(e => e.HandleError(It.IsAny())); - EventDispatcherMock = new Mock(); - - Optimizely = new Optimizely(TestData.Datafile, EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object); - } - - [Test] - public void OptimizelyUserContext_withAttributes() - { - var attributes = new UserAttributes() { { "house", "GRYFFINDOR" } }; - OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object); - - Assert.AreEqual(user.Optimizely, Optimizely); - Assert.AreEqual(user.UserId, UserID); - Assert.AreEqual(user.UserAttributes, attributes); - } - - [Test] - public void OptimizelyUserContext_noAttributes() - { - OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, null, ErrorHandlerMock.Object, LoggerMock.Object); - - Assert.AreEqual(user.Optimizely, Optimizely); - Assert.AreEqual(user.UserId, UserID); - Assert.True(user.UserAttributes.Count == 0); - } - - [Test] - public void SetAttribute() - { - var attributes = new UserAttributes() { { "house", "GRYFFINDOR" } }; - OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object); - - user.SetAttribute("k1", "v1"); - user.SetAttribute("k2", true); - user.SetAttribute("k3", 100); - user.SetAttribute("k4", 3.5); - - Assert.AreEqual(user.Optimizely, Optimizely); - Assert.AreEqual(user.UserId, UserID); - var newAttributes = user.UserAttributes; - Assert.AreEqual(newAttributes["house"], "GRYFFINDOR"); - Assert.AreEqual(newAttributes["k1"], "v1"); - Assert.AreEqual(newAttributes["k2"], true); - Assert.AreEqual(newAttributes["k3"], 100); - Assert.AreEqual(newAttributes["k4"], 3.5); - } - - [Test] - public void SetAttribute_noAttribute() - { - OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, null, ErrorHandlerMock.Object, LoggerMock.Object); - - user.SetAttribute("k1", "v1"); - user.SetAttribute("k2", true); - - Assert.AreEqual(user.Optimizely, Optimizely); - Assert.AreEqual(user.UserId, UserID); - var newAttributes = user.UserAttributes; - Assert.AreEqual(newAttributes["k1"], "v1"); - Assert.AreEqual(newAttributes["k2"], true); - } - - [Test] - public void SetAttribute_override() - { - var attributes = new UserAttributes() { { "house", "GRYFFINDOR" } }; - OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object); - - user.SetAttribute("k1", "v1"); - user.SetAttribute("house", "v2"); - - var newAttributes = user.UserAttributes; - Assert.AreEqual(newAttributes["k1"], "v1"); - Assert.AreEqual(newAttributes["house"], "v2"); - } - - [Test] - public void SetAttribute_nullValue() - { - var attributes = new UserAttributes() { { "k1", null } }; - OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object); - - var newAttributes = user.UserAttributes; - Assert.AreEqual(newAttributes["k1"], null); - - user.SetAttribute("k1", true); - newAttributes = user.UserAttributes; - Assert.AreEqual(newAttributes["k1"], true); - - user.SetAttribute("k1", null); - newAttributes = user.UserAttributes; - Assert.AreEqual(newAttributes["k1"], null); - } - - #region decide - - [Test] - public void Decide() - { - var flagKey = "multi_variate_feature"; - var variablesExpected = Optimizely.GetAllFeatureVariables(flagKey, UserID); - - var user = Optimizely.CreateUserContext(UserID); - user.SetAttribute("browser_type", "chrome"); - var decision = user.Decide(flagKey); - - Assert.AreEqual(decision.VariationKey, "Gred"); - Assert.False(decision.Enabled); - Assert.AreEqual(decision.Variables.ToDictionary(), variablesExpected.ToDictionary()); - Assert.AreEqual(decision.RuleKey, "test_experiment_multivariate"); - Assert.AreEqual(decision.FlagKey, flagKey); - Assert.AreEqual(decision.UserContext, user); - Assert.True(decision.Reasons.IsNullOrEmpty()); - } - - [Test] - public void DecideInvalidFlagKey() - { - var flagKey = "invalid_feature"; - - var user = Optimizely.CreateUserContext(UserID); - user.SetAttribute("browser_type", "chrome"); - var decision = user.Decide(flagKey); - - Assert.Null(decision.VariationKey); - Assert.False(decision.Enabled); - Assert.AreEqual(decision.Variables.ToDictionary(), new Dictionary()); - Assert.IsNull(decision.RuleKey); - Assert.AreEqual(decision.FlagKey, flagKey); - Assert.AreEqual(decision.UserContext, user); - Assert.True(decision.Reasons.IsNullOrEmpty()); - } - - [Test] - public void DecideInvalidConfig() - { - Optimizely optimizely = new Optimizely(TestData.UnsupportedVersionDatafile, EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object); - - var flagKey = "multi_variate_feature"; - var decisionExpected = OptimizelyDecision.NewErrorDecision( - flagKey, - new OptimizelyUserContext(optimizely, UserID, new UserAttributes(), ErrorHandlerMock.Object, LoggerMock.Object), - DecisionMessage.SDK_NOT_READY, - ErrorHandlerMock.Object, - LoggerMock.Object); - var user = optimizely.CreateUserContext(UserID); - var decision = user.Decide(flagKey); - - Assert.IsTrue(TestData.CompareObjects(decision, decisionExpected)); - } - #endregion - - } -} diff --git a/OptimizelySDK/Bucketing/Bucketer.cs b/OptimizelySDK/Bucketing/Bucketer.cs index be62727a..b8944687 100644 --- a/OptimizelySDK/Bucketing/Bucketer.cs +++ b/OptimizelySDK/Bucketing/Bucketer.cs @@ -15,7 +15,6 @@ */ using OptimizelySDK.Entity; using OptimizelySDK.Logger; -using OptimizelySDK.OptimizelyDecisions; using System; using System.Collections.Generic; using System.Text; @@ -103,9 +102,8 @@ private string FindBucket(string bucketingId, string userId, string parentId, IE /// Experiment Experiment in which user is to be bucketed /// A customer-assigned value used to create the key for the murmur hash. /// User identifier - /// Decision log messages. /// Variation which will be shown to the user - public virtual Variation Bucket(ProjectConfig config, Experiment experiment, string bucketingId, string userId, IDecisionReasons reasons) + public virtual Variation Bucket(ProjectConfig config, Experiment experiment, string bucketingId, string userId) { string message; Variation variation; @@ -124,26 +122,26 @@ public virtual Variation Bucket(ProjectConfig config, Experiment experiment, str if (string.IsNullOrEmpty(userExperimentId)) { message = $"User [{userId}] is in no experiment."; - Logger.Log(LogLevel.INFO, reasons.AddInfo(message)); + Logger.Log(LogLevel.INFO, message); return new Variation(); } if (userExperimentId != experiment.Id) { message = $"User [{userId}] is not in experiment [{experiment.Key}] of group [{experiment.GroupId}]."; - Logger.Log(LogLevel.INFO, reasons.AddInfo(message)); + Logger.Log(LogLevel.INFO, message); return new Variation(); } message = $"User [{userId}] is in experiment [{experiment.Key}] of group [{experiment.GroupId}]."; - Logger.Log(LogLevel.INFO, reasons.AddInfo(message)); + Logger.Log(LogLevel.INFO, message); } // Bucket user if not in whitelist and in group (if any). string variationId = FindBucket(bucketingId, userId, experiment.Id, experiment.TrafficAllocation); if (string.IsNullOrEmpty(variationId)) { - Logger.Log(LogLevel.INFO, reasons.AddInfo($"User [{userId}] is in no variation.")); + Logger.Log(LogLevel.INFO, $"User [{userId}] is in no variation."); return new Variation(); } @@ -151,7 +149,7 @@ public virtual Variation Bucket(ProjectConfig config, Experiment experiment, str // success! variation = config.GetVariationFromId(experiment.Key, variationId); message = $"User [{userId}] is in variation [{variation.Key}] of experiment [{experiment.Key}]."; - Logger.Log(LogLevel.INFO, reasons.AddInfo(message)); + Logger.Log(LogLevel.INFO, message); return variation; } } diff --git a/OptimizelySDK/Bucketing/DecisionService.cs b/OptimizelySDK/Bucketing/DecisionService.cs index 3973b13e..3959dbbd 100644 --- a/OptimizelySDK/Bucketing/DecisionService.cs +++ b/OptimizelySDK/Bucketing/DecisionService.cs @@ -19,7 +19,6 @@ using OptimizelySDK.Entity; using OptimizelySDK.ErrorHandler; using OptimizelySDK.Logger; -using OptimizelySDK.OptimizelyDecisions; using OptimizelySDK.Utils; namespace OptimizelySDK.Bucketing @@ -84,43 +83,20 @@ public DecisionService(Bucketer bucketer, IErrorHandler errorHandler, UserProfil /// The userId of the user. /// The user's attributes. This should be filtered to just attributes in the Datafile. /// The Variation the user is allocated into. - public virtual Variation GetVariation(Experiment experiment, - string userId, - ProjectConfig config, - UserAttributes filteredAttributes) - { - return GetVariation(experiment, userId, config, filteredAttributes, new List(), DefaultDecisionReasons.NewInstance()); - } - - /// - /// Get a Variation of an Experiment for a user to be allocated into. - /// - /// The Experiment the user will be bucketed into. - /// The userId of the user. - /// The user's attributes. This should be filtered to just attributes in the Datafile. - /// The Variation the user is allocated into. - public virtual Variation GetVariation(Experiment experiment, - string userId, - ProjectConfig config, - UserAttributes filteredAttributes, - List options, - IDecisionReasons reasons) + public virtual Variation GetVariation(Experiment experiment, string userId, ProjectConfig config, UserAttributes filteredAttributes) { if (!ExperimentUtils.IsExperimentActive(experiment, Logger)) return null; // check if a forced variation is set - var forcedVariation = GetForcedVariation(experiment.Key, userId, config, reasons); + var forcedVariation = GetForcedVariation(experiment.Key, userId, config); if (forcedVariation != null) return forcedVariation; - var variation = GetWhitelistedVariation(experiment, userId, reasons); + var variation = GetWhitelistedVariation(experiment, userId); if (variation != null) return variation; - // fetch the user profile map from the user profile service - var ignoreUPS = options.Contains(OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE); - UserProfile userProfile = null; - if (UserProfileService != null && !ignoreUPS) + if (UserProfileService != null) { try { @@ -128,21 +104,21 @@ public virtual Variation GetVariation(Experiment experiment, if (userProfileMap != null && UserProfileUtil.IsValidUserProfileMap(userProfileMap)) { userProfile = UserProfileUtil.ConvertMapToUserProfile(userProfileMap); - variation = GetStoredVariation(experiment, userProfile, config, reasons); + variation = GetStoredVariation(experiment, userProfile, config); if (variation != null) return variation; } else if (userProfileMap == null) { - Logger.Log(LogLevel.INFO, reasons.AddInfo("We were unable to get a user profile map from the UserProfileService.")); + Logger.Log(LogLevel.INFO, "We were unable to get a user profile map from the UserProfileService."); } else { - Logger.Log(LogLevel.ERROR, reasons.AddInfo("The UserProfileService returned an invalid map.")); + Logger.Log(LogLevel.ERROR, "The UserProfileService returned an invalid map."); } } catch (Exception exception) { - Logger.Log(LogLevel.ERROR, reasons.AddInfo(exception.Message)); + Logger.Log(LogLevel.ERROR, exception.Message); ErrorHandler.HandleError(new Exceptions.OptimizelyRuntimeException(exception.Message)); } } @@ -150,16 +126,16 @@ public virtual Variation GetVariation(Experiment experiment, if (ExperimentUtils.DoesUserMeetAudienceConditions(config, experiment, filteredAttributes, LOGGING_KEY_TYPE_EXPERIMENT, experiment.Key, Logger)) { // Get Bucketing ID from user attributes. - string bucketingId = GetBucketingId(userId, filteredAttributes, reasons); + string bucketingId = GetBucketingId(userId, filteredAttributes); - variation = Bucketer.Bucket(config, experiment, bucketingId, userId, reasons); + variation = Bucketer.Bucket(config, experiment, bucketingId, userId); if (variation != null && variation.Key != null) { if (UserProfileService != null) { var bucketerUserProfile = userProfile ?? new UserProfile(userId, new Dictionary()); - SaveVariation(experiment, variation, bucketerUserProfile, reasons); + SaveVariation(experiment, variation, bucketerUserProfile); } else @@ -168,7 +144,7 @@ public virtual Variation GetVariation(Experiment experiment, return variation; } - Logger.Log(LogLevel.INFO, reasons.AddInfo($"User \"{userId}\" does not meet conditions to be in experiment \"{experiment.Key}\".")); + Logger.Log(LogLevel.INFO, $"User \"{userId}\" does not meet conditions to be in experiment \"{experiment.Key}\"."); return null; } @@ -181,18 +157,6 @@ public virtual Variation GetVariation(Experiment experiment, /// Project Config /// Variation entity which the given user and experiment should be forced into. public Variation GetForcedVariation(string experimentKey, string userId, ProjectConfig config) - { - return GetForcedVariation(experimentKey, userId, config, DefaultDecisionReasons.NewInstance()); - } - - /// - /// Gets the forced variation for the given user and experiment. - /// - /// The experiment key - /// The user ID - /// Project Config - /// Variation entity which the given user and experiment should be forced into. - public Variation GetForcedVariation(string experimentKey, string userId, ProjectConfig config, IDecisionReasons reasons) { if (ForcedVariationMap.ContainsKey(userId) == false) { @@ -227,7 +191,8 @@ public Variation GetForcedVariation(string experimentKey, string userId, Project // this case is logged in getVariationFromKey if (string.IsNullOrEmpty(variationKey)) return null; - Logger.Log(LogLevel.DEBUG, reasons.AddInfo($@"Variation ""{variationKey}"" is mapped to experiment ""{experimentKey}"" and user ""{userId}"" in the forced variation map")); + + Logger.Log(LogLevel.DEBUG, $@"Variation ""{variationKey}"" is mapped to experiment ""{experimentKey}"" and user ""{userId}"" in the forced variation map"); Variation variation = config.GetVariationFromKey(experimentKey, variationKey); @@ -283,27 +248,16 @@ public bool SetForcedVariation(string experimentKey, string userId, string varia Logger.Log(LogLevel.DEBUG, $@"Set variation ""{variationId}"" for experiment ""{experimentId}"" and user ""{userId}"" in the forced variation map."); return true; } - /// - /// Get the variation the user has been whitelisted into. - /// - /// in which user is to be bucketed. - /// User Identifier - /// if the user is not whitelisted into any variation {@link Variation} - /// the user is bucketed into if the user has a specified whitelisted variation. - public Variation GetWhitelistedVariation(Experiment experiment, string userId) - { - return GetWhitelistedVariation(experiment, userId, DefaultDecisionReasons.NewInstance()); - } + /// /// Get the variation the user has been whitelisted into. /// /// in which user is to be bucketed. /// User Identifier - /// Decision log messages. /// if the user is not whitelisted into any variation {@link Variation} /// the user is bucketed into if the user has a specified whitelisted variation. - public Variation GetWhitelistedVariation(Experiment experiment, string userId, IDecisionReasons reasons) + public Variation GetWhitelistedVariation(Experiment experiment, string userId) { //if a user has a forced variation mapping, return the respective variation Dictionary userIdToVariationKeyMap = experiment.UserIdToKeyVariations; @@ -317,9 +271,9 @@ public Variation GetWhitelistedVariation(Experiment experiment, string userId, I : null; if (forcedVariation != null) - Logger.Log(LogLevel.INFO, reasons.AddInfo($"User \"{userId}\" is forced in variation \"{forcedVariationKey}\".")); + Logger.Log(LogLevel.INFO, $"User \"{userId}\" is forced in variation \"{forcedVariationKey}\"."); else - Logger.Log(LogLevel.ERROR, reasons.AddInfo($"Variation \"{forcedVariationKey}\" is not in the datafile. Not activating user \"{userId}\".")); + Logger.Log(LogLevel.ERROR, $"Variation \"{forcedVariationKey}\" is not in the datafile. Not activating user \"{userId}\"."); return forcedVariation; } @@ -330,7 +284,7 @@ public Variation GetWhitelistedVariation(Experiment experiment, string userId, I /// which the user was bucketed /// User profile of the user /// The user was previously bucketed into. - public Variation GetStoredVariation(Experiment experiment, UserProfile userProfile, ProjectConfig config, IDecisionReasons reasons) + public Variation GetStoredVariation(Experiment experiment, UserProfile userProfile, ProjectConfig config) { // ---------- Check User Profile for Sticky Bucketing ---------- // If a user profile instance is present then check it for a saved variation @@ -342,7 +296,7 @@ public Variation GetStoredVariation(Experiment experiment, UserProfile userProfi if (decision == null) { - Logger.Log(LogLevel.INFO, reasons.AddInfo($"No previously activated variation of experiment \"{experimentKey}\" for user \"{userProfile.UserId}\" found in user profile.")); + Logger.Log(LogLevel.INFO, $"No previously activated variation of experiment \"{experimentKey}\" for user \"{userProfile.UserId}\" found in user profile."); return null; } @@ -356,11 +310,11 @@ public Variation GetStoredVariation(Experiment experiment, UserProfile userProfi if (savedVariation == null) { - Logger.Log(LogLevel.INFO, reasons.AddInfo($"User \"{userProfile.UserId}\" was previously bucketed into variation with ID \"{variationId}\" for experiment \"{experimentId}\", but no matching variation was found for that user. We will re-bucket the user.")); + Logger.Log(LogLevel.INFO, $"User \"{userProfile.UserId}\" was previously bucketed into variation with ID \"{variationId}\" for experiment \"{experimentId}\", but no matching variation was found for that user. We will re-bucket the user."); return null; } - Logger.Log(LogLevel.INFO, reasons.AddInfo($"Returning previously activated variation \"{savedVariation.Key}\" of experiment \"{experimentKey}\" for user \"{userProfile.UserId}\" from user profile.")); + Logger.Log(LogLevel.INFO, $"Returning previously activated variation \"{savedVariation.Key}\" of experiment \"{experimentKey}\" for user \"{userProfile.UserId}\" from user profile."); return savedVariation; } catch (Exception) @@ -368,16 +322,6 @@ public Variation GetStoredVariation(Experiment experiment, UserProfile userProfi return null; } } - /// - /// Save a { @link Variation } of an { @link Experiment } for a user in the {@link UserProfileService}. - /// - /// The experiment the user was buck - /// The Variation to save. - /// instance of the user information. - public void SaveVariation(Experiment experiment, Variation variation, UserProfile userProfile) - { - SaveVariation(experiment, variation, userProfile, DefaultDecisionReasons.NewInstance()); - } /// /// Save a { @link Variation } of an { @link Experiment } for a user in the {@link UserProfileService}. @@ -385,7 +329,7 @@ public void SaveVariation(Experiment experiment, Variation variation, UserProfil /// The experiment the user was buck /// The Variation to save. /// instance of the user information. - public void SaveVariation(Experiment experiment, Variation variation, UserProfile userProfile, IDecisionReasons reasons) + public void SaveVariation(Experiment experiment, Variation variation, UserProfile userProfile) { //only save if the user has implemented a user profile service if (UserProfileService == null) @@ -407,15 +351,15 @@ public void SaveVariation(Experiment experiment, Variation variation, UserProfil try { UserProfileService.Save(userProfile.ToMap()); - Logger.Log(LogLevel.INFO, reasons.AddInfo($"Saved variation \"{variation.Id}\" of experiment \"{experiment.Id}\" for user \"{userProfile.UserId}\".")); + Logger.Log(LogLevel.INFO, $"Saved variation \"{variation.Id}\" of experiment \"{experiment.Id}\" for user \"{userProfile.UserId}\"."); } catch (Exception exception) { - Logger.Log(LogLevel.ERROR, reasons.AddInfo($"Failed to save variation \"{variation.Id}\" of experiment \"{experiment.Id}\" for user \"{userProfile.UserId}\".")); + Logger.Log(LogLevel.ERROR, $"Failed to save variation \"{variation.Id}\" of experiment \"{experiment.Id}\" for user \"{userProfile.UserId}\"."); ErrorHandler.HandleError(new Exceptions.OptimizelyRuntimeException(exception.Message)); } } - + /// /// Try to bucket the user into a rollout rule. /// Evaluate the user for rules in priority order by seeing if the user satisfies the audience. @@ -424,14 +368,9 @@ public void SaveVariation(Experiment experiment, Variation variation, UserProfil /// The feature flag the user wants to access. /// User Identifier /// The user's attributes. This should be filtered to just attributes in the Datafile. - /// Decision log messages. /// null if the user is not bucketed into the rollout or if the feature flag was not attached to a rollout. /// otherwise the FeatureDecision entity - public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag featureFlag, - string userId, - UserAttributes filteredAttributes, - ProjectConfig config, - IDecisionReasons reasons) + public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag featureFlag, string userId, UserAttributes filteredAttributes, ProjectConfig config) { if (featureFlag == null) { @@ -441,7 +380,7 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature if (string.IsNullOrEmpty(featureFlag.RolloutId)) { - Logger.Log(LogLevel.INFO, reasons.AddInfo($"The feature flag \"{featureFlag.Key}\" is not used in a rollout.")); + Logger.Log(LogLevel.INFO, $"The feature flag \"{featureFlag.Key}\" is not used in a rollout."); return null; } @@ -449,7 +388,7 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature if (string.IsNullOrEmpty(rollout.Id)) { - Logger.Log(LogLevel.ERROR, reasons.AddInfo($"The rollout with id \"{featureFlag.RolloutId}\" is not found in the datafile for feature flag \"{featureFlag.Key}\"")); + Logger.Log(LogLevel.ERROR, $"The rollout with id \"{featureFlag.RolloutId}\" is not found in the datafile for feature flag \"{featureFlag.Key}\""); return null; } @@ -461,16 +400,16 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature var rolloutRulesLength = rollout.Experiments.Count; // Get Bucketing ID from user attributes. - string bucketingId = GetBucketingId(userId, filteredAttributes, reasons); + string bucketingId = GetBucketingId(userId, filteredAttributes); // For all rules before the everyone else rule for (int i = 0; i < rolloutRulesLength - 1; i++) { string loggingKey = (i + 1).ToString(); var rolloutRule = rollout.Experiments[i]; - if (ExperimentUtils.DoesUserMeetAudienceConditions(config, rolloutRule, filteredAttributes, LOGGING_KEY_TYPE_RULE, loggingKey, reasons, Logger)) + if (ExperimentUtils.DoesUserMeetAudienceConditions(config, rolloutRule, filteredAttributes, LOGGING_KEY_TYPE_RULE, loggingKey, Logger)) { - variation = Bucketer.Bucket(config, rolloutRule, bucketingId, userId, reasons); + variation = Bucketer.Bucket(config, rolloutRule, bucketingId, userId); if (variation == null || string.IsNullOrEmpty(variation.Id)) break; @@ -485,9 +424,9 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature // Get the last rule which is everyone else rule. var everyoneElseRolloutRule = rollout.Experiments[rolloutRulesLength - 1]; - if (ExperimentUtils.DoesUserMeetAudienceConditions(config, everyoneElseRolloutRule, filteredAttributes, LOGGING_KEY_TYPE_RULE, "Everyone Else", reasons, Logger)) + if (ExperimentUtils.DoesUserMeetAudienceConditions(config, everyoneElseRolloutRule, filteredAttributes, LOGGING_KEY_TYPE_RULE, "Everyone Else", Logger)) { - variation = Bucketer.Bucket(config, everyoneElseRolloutRule, bucketingId, userId, reasons); + variation = Bucketer.Bucket(config, everyoneElseRolloutRule, bucketingId, userId); if (variation != null && !string.IsNullOrEmpty(variation.Id)) { Logger.Log(LogLevel.DEBUG, $"User \"{userId}\" meets conditions for targeting rule \"Everyone Else\"."); @@ -511,12 +450,7 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature /// The user's attributes. This should be filtered to just attributes in the Datafile. /// null if the user is not bucketed into the rollout or if the feature flag was not attached to a rollout. /// Otherwise the FeatureDecision entity - public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag featureFlag, - string userId, - UserAttributes filteredAttributes, - ProjectConfig config, - List options, - IDecisionReasons reasons) + public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag featureFlag, string userId, UserAttributes filteredAttributes, ProjectConfig config) { if (featureFlag == null) { @@ -526,7 +460,7 @@ public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag feat if (featureFlag.ExperimentIds == null || featureFlag.ExperimentIds.Count == 0) { - Logger.Log(LogLevel.INFO, reasons.AddInfo($"The feature flag \"{featureFlag.Key}\" is not used in any experiments.")); + Logger.Log(LogLevel.INFO, $"The feature flag \"{featureFlag.Key}\" is not used in any experiments."); return null; } @@ -537,16 +471,16 @@ public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag feat if (string.IsNullOrEmpty(experiment.Key)) continue; - var variation = GetVariation(experiment, userId, config, filteredAttributes, options, reasons); + var variation = GetVariation(experiment, userId, config, filteredAttributes); if (variation != null && !string.IsNullOrEmpty(variation.Id)) { - Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is bucketed into experiment \"{experiment.Key}\" of feature \"{featureFlag.Key}\".")); + Logger.Log(LogLevel.INFO, $"The user \"{userId}\" is bucketed into experiment \"{experiment.Key}\" of feature \"{featureFlag.Key}\"."); return new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST); } } - Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is not bucketed into any of the experiments on the feature \"{featureFlag.Key}\".")); + Logger.Log(LogLevel.INFO, $"The user \"{userId}\" is not bucketed into any of the experiments on the feature \"{featureFlag.Key}\"."); return null; } @@ -559,44 +493,23 @@ public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag feat /// null if the user is not bucketed into any variation or the FeatureDecision entity if the user is /// successfully bucketed. public virtual FeatureDecision GetVariationForFeature(FeatureFlag featureFlag, string userId, ProjectConfig config, UserAttributes filteredAttributes) - { - return GetVariationForFeature(featureFlag, userId, config, filteredAttributes, new List(), DefaultDecisionReasons.NewInstance()); - } - - /// - /// Get the variation the user is bucketed into for the FeatureFlag - /// - /// The feature flag the user wants to access. - /// User Identifier - /// The user's attributes. This should be filtered to just attributes in the Datafile. - /// The user's attributes. This should be filtered to just attributes in the Datafile. - /// An array of decision options. - /// Decision log messages. - /// null if the user is not bucketed into any variation or the FeatureDecision entity if the user is - /// successfully bucketed. - public virtual FeatureDecision GetVariationForFeature(FeatureFlag featureFlag, - string userId, - ProjectConfig config, - UserAttributes filteredAttributes, - List options, - IDecisionReasons reasons) { // Check if the feature flag has an experiment and the user is bucketed into that experiment. - var decision = GetVariationForFeatureExperiment(featureFlag, userId, filteredAttributes, config, options, reasons); + var decision = GetVariationForFeatureExperiment(featureFlag, userId, filteredAttributes, config); if (decision != null) return decision; // Check if the feature flag has rollout and the the user is bucketed into one of its rules. - decision = GetVariationForFeatureRollout(featureFlag, userId, filteredAttributes, config, reasons); + decision = GetVariationForFeatureRollout(featureFlag, userId, filteredAttributes, config); if (decision != null) { - Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is bucketed into a rollout for feature flag \"{featureFlag.Key}\".")); + Logger.Log(LogLevel.INFO, $"The user \"{userId}\" is bucketed into a rollout for feature flag \"{featureFlag.Key}\"."); return decision; } - Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is not bucketed into a rollout for feature flag \"{featureFlag.Key}\".")); + Logger.Log(LogLevel.INFO, $"The user \"{userId}\" is not bucketed into a rollout for feature flag \"{featureFlag.Key}\"."); return new FeatureDecision(null, null, FeatureDecision.DECISION_SOURCE_ROLLOUT); } @@ -606,7 +519,7 @@ public virtual FeatureDecision GetVariationForFeature(FeatureFlag featureFlag, /// User Identifier /// The user's attributes. /// Bucketing Id if it is a string type in attributes, user Id otherwise. - private string GetBucketingId(string userId, UserAttributes filteredAttributes, IDecisionReasons reasons) + private string GetBucketingId(string userId, UserAttributes filteredAttributes) { string bucketingId = userId; @@ -620,7 +533,7 @@ private string GetBucketingId(string userId, UserAttributes filteredAttributes, } else { - Logger.Log(LogLevel.WARN, reasons.AddInfo("BucketingID attribute is not a string. Defaulted to userId")); + Logger.Log(LogLevel.WARN, "BucketingID attribute is not a string. Defaulted to userId"); } } diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs index 49901a1c..b30d00ac 100644 --- a/OptimizelySDK/Optimizely.cs +++ b/OptimizelySDK/Optimizely.cs @@ -476,45 +476,45 @@ public virtual bool IsFeatureEnabled(string featureKey, string userId, UserAttri return false; bool featureEnabled = false; - var sourceInfo = new Dictionary(); - var decision = DecisionService.GetVariationForFeature(featureFlag, userId, config, userAttributes); - var variation = decision.Variation; - var decisionSource = decision?.Source ?? FeatureDecision.DECISION_SOURCE_ROLLOUT; + var sourceInfo = new Dictionary(); + var decision = DecisionService.GetVariationForFeature(featureFlag, userId, config, userAttributes); + var variation = decision.Variation; + var decisionSource = decision?.Source ?? FeatureDecision.DECISION_SOURCE_ROLLOUT; - SendImpressionEvent(decision.Experiment, variation, userId, userAttributes, config, featureKey, decisionSource); + SendImpressionEvent(decision.Experiment, variation, userId, userAttributes, config, featureKey, decisionSource); - if (variation != null) - { - featureEnabled = variation.FeatureEnabled.GetValueOrDefault(); + if (variation != null) + { + featureEnabled = variation.FeatureEnabled.GetValueOrDefault(); - // This information is only necessary for feature tests. - // For rollouts experiments and variations are an implementation detail only. - if (decision.Source == FeatureDecision.DECISION_SOURCE_FEATURE_TEST) - { - sourceInfo["experimentKey"] = decision.Experiment.Key; - sourceInfo["variationKey"] = variation.Key; - } - else - { - Logger.Log(LogLevel.INFO, $@"The user ""{userId}"" is not being experimented on feature ""{featureKey}""."); - } + // This information is only necessary for feature tests. + // For rollouts experiments and variations are an implementation detail only. + if (decision.Source == FeatureDecision.DECISION_SOURCE_FEATURE_TEST) + { + sourceInfo["experimentKey"] = decision.Experiment.Key; + sourceInfo["variationKey"] = variation.Key; } - - if (featureEnabled == true) - Logger.Log(LogLevel.INFO, $@"Feature flag ""{featureKey}"" is enabled for user ""{userId}""."); else - Logger.Log(LogLevel.INFO, $@"Feature flag ""{featureKey}"" is not enabled for user ""{userId}""."); - - var decisionInfo = new Dictionary { - { "featureKey", featureKey }, - { "featureEnabled", featureEnabled }, - { "source", decision.Source }, - { "sourceInfo", sourceInfo }, - }; - - NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, DecisionNotificationTypes.FEATURE, userId, - userAttributes ?? new UserAttributes(), decisionInfo); + Logger.Log(LogLevel.INFO, $@"The user ""{userId}"" is not being experimented on feature ""{featureKey}""."); + } + } + + if (featureEnabled == true) + Logger.Log(LogLevel.INFO, $@"Feature flag ""{featureKey}"" is enabled for user ""{userId}""."); + else + Logger.Log(LogLevel.INFO, $@"Feature flag ""{featureKey}"" is not enabled for user ""{userId}""."); + + var decisionInfo = new Dictionary + { + { "featureKey", featureKey }, + { "featureEnabled", featureEnabled }, + { "source", decision.Source }, + { "sourceInfo", sourceInfo }, + }; + + NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, DecisionNotificationTypes.FEATURE, userId, + userAttributes ?? new UserAttributes(), decisionInfo); return featureEnabled; } @@ -712,139 +712,6 @@ public OptimizelyUserContext CreateUserContext(string userId, return new OptimizelyUserContext(this, userId, userAttributes, ErrorHandler, Logger); } - public OptimizelyDecision Decide(OptimizelyUserContext user, - string key, - List options) - { - - var config = ProjectConfigManager?.GetConfig(); - if (config == null) - { - return OptimizelyDecision.NewErrorDecision(key, user, DecisionMessage.SDK_NOT_READY, ErrorHandler, Logger); - } - var userId = user.UserId; - var inputValues = new Dictionary - { - { USER_ID, userId }, - }; - - if (!ValidateStringInputs(inputValues)) - return null; - - var flag = config.GetFeatureFlagFromKey(key); - if (flag == null) - { - return OptimizelyDecision.NewErrorDecision(key, - user, - DecisionMessage.Reason(DecisionMessage.FLAG_KEY_INVALID, key), - ErrorHandler, Logger); - } - - var userAttributes = user.UserAttributes; - var decisionEventDispatched = false; - var allOptions = GetAllOptions(options); - var decisionReasons = DefaultDecisionReasons.NewInstance(allOptions); - - var flagDecision = DecisionService.GetVariationForFeature( - flag, - userId, - config, - userAttributes, - allOptions, - decisionReasons); - - var featureEnabled = false; - - var variation = flagDecision.Variation; - - if (variation != null) - { - featureEnabled = variation.FeatureEnabled.GetValueOrDefault(); - } - - if (featureEnabled) - { - Logger.Log(LogLevel.INFO, "Feature \"" + key + "\" is enabled for user \"" + userId + "\""); - } - else - { - Logger.Log(LogLevel.INFO, "Feature \"" + key + "\" is not enabled for user \"" + userId + "\""); - } - var variableMap = new Dictionary(); - if (flag?.Variables != null && !allOptions.Contains(OptimizelyDecideOption.EXCLUDE_VARIABLES)) - { - - foreach (var featureVariable in flag?.Variables) - { - string variableValue = featureVariable.DefaultValue; - if (featureEnabled) - { - var featureVariableUsageInstance = variation.GetFeatureVariableUsageFromId(featureVariable.Id); - if (featureVariableUsageInstance != null) - { - variableValue = featureVariableUsageInstance.Value; - } - } - - var typeCastedValue = GetTypeCastedVariableValue(variableValue, featureVariable.Type); - - if (typeCastedValue is OptimizelyJSON) - typeCastedValue = ((OptimizelyJSON)typeCastedValue).ToDictionary(); - - variableMap.Add(featureVariable.Key, typeCastedValue); - } - } - - var optimizelyJSON = new OptimizelyJSON(variableMap, ErrorHandler, Logger); - - var decisionSource = flagDecision?.Source ?? FeatureDecision.DECISION_SOURCE_ROLLOUT; - if (!allOptions.Contains(OptimizelyDecideOption.DISABLE_DECISION_EVENT)) - { - - SendImpressionEvent(flagDecision.Experiment, variation, userId, userAttributes, config, key, decisionSource); - decisionEventDispatched = true; - } - List reasonsToReport = decisionReasons.ToReport(); - string variationKey = flagDecision.Variation?.Key; - - // TODO: add ruleKey values when available later. use a copy of experimentKey until then. - string ruleKey = flagDecision.Experiment?.Key; - - var decisionInfo = new Dictionary - { - { "flagKey", key }, - { "enabled", featureEnabled }, - { "variables", variableMap }, - { "variationKey", variationKey }, - { "ruleKey", ruleKey }, - { "reasons", decisionReasons }, - { "decisionEventDispatched", decisionEventDispatched }, - { "featureEnabled", featureEnabled }, - }; - - NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, DecisionNotificationTypes.FEATURE, userId, - userAttributes ?? new UserAttributes(), decisionInfo); - - return new OptimizelyDecision( - variationKey, - featureEnabled, - optimizelyJSON, - ruleKey, - key, - user, - reasonsToReport); - } - - private List GetAllOptions(List options) - { - var copiedOptions = new List(DefaultDecideOptions); - if (options != null) - { - copiedOptions.AddRange(options); - } - return copiedOptions; - } - /// /// Sends impression event. /// diff --git a/OptimizelySDK/OptimizelyUserContext.cs b/OptimizelySDK/OptimizelyUserContext.cs index 07834546..e2621f20 100644 --- a/OptimizelySDK/OptimizelyUserContext.cs +++ b/OptimizelySDK/OptimizelyUserContext.cs @@ -46,7 +46,7 @@ public OptimizelyUserContext(Optimizely optimizely, string userId, UserAttribute /// value An attribute value public void SetAttribute(string key, object value) { - UserAttributes[key] = value; + UserAttributes.Add(key, value); } /// @@ -74,7 +74,7 @@ public OptimizelyDecision Decide(string key) public OptimizelyDecision Decide(string key, List options) { - return Optimizely.Decide(this, key, options); + return null; } /// diff --git a/OptimizelySDK/Utils/ExperimentUtils.cs b/OptimizelySDK/Utils/ExperimentUtils.cs index b6023480..470b014c 100644 --- a/OptimizelySDK/Utils/ExperimentUtils.cs +++ b/OptimizelySDK/Utils/ExperimentUtils.cs @@ -17,7 +17,6 @@ using OptimizelySDK.AudienceConditions; using OptimizelySDK.Entity; using OptimizelySDK.Logger; -using OptimizelySDK.OptimizelyDecisions; namespace OptimizelySDK.Utils { @@ -36,24 +35,6 @@ public static bool IsExperimentActive(Experiment experiment, ILogger logger) return true; } - /// - /// Check if the user meets audience conditions to be in experiment or not - /// - /// ProjectConfig Configuration for the project - /// Experiment Entity representing the experiment - /// Attributes of the user. Defaults to empty attributes array if not provided - /// It can be either experiment or rule. - /// In case loggingKeyType is experiment it will be experiment key or else it will be rule number. - /// true if the user meets audience conditions to be in experiment, false otherwise. - public static bool DoesUserMeetAudienceConditions(ProjectConfig config, - Experiment experiment, - UserAttributes userAttributes, - string loggingKeyType, - string loggingKey, - ILogger logger) - { - return DoesUserMeetAudienceConditions(config, experiment, userAttributes, loggingKeyType, loggingKey, DefaultDecisionReasons.NewInstance(), logger); - } /// /// Check if the user meets audience conditions to be in experiment or not @@ -63,14 +44,12 @@ public static bool DoesUserMeetAudienceConditions(ProjectConfig config, /// Attributes of the user. Defaults to empty attributes array if not provided /// It can be either experiment or rule. /// In case loggingKeyType is experiment it will be experiment key or else it will be rule number. - /// Decision log messages. /// true if the user meets audience conditions to be in experiment, false otherwise. public static bool DoesUserMeetAudienceConditions(ProjectConfig config, Experiment experiment, UserAttributes userAttributes, string loggingKeyType, string loggingKey, - IDecisionReasons reasons, ILogger logger) { if (userAttributes == null) @@ -94,7 +73,7 @@ public static bool DoesUserMeetAudienceConditions(ProjectConfig config, var result = expConditions.Evaluate(config, userAttributes, logger).GetValueOrDefault(); var resultText = result.ToString().ToUpper(); - logger.Log(LogLevel.INFO, reasons.AddInfo($@"Audiences for {loggingKeyType} ""{loggingKey}"" collectively evaluated to {resultText}")); + logger.Log(LogLevel.INFO, $@"Audiences for {loggingKeyType} ""{loggingKey}"" collectively evaluated to {resultText}"); return result; } } From 8c783301ee565b7e960b69bd0d1f9919ebfedef8 Mon Sep 17 00:00:00 2001 From: muhammadnoman Date: Wed, 11 Nov 2020 23:08:32 +0500 Subject: [PATCH 09/34] Added UserContext add reason logs BugFix of userAttributes Unit tests fix --- OptimizelySDK.Tests/BucketerTest.cs | 23 +- OptimizelySDK.Tests/DecisionServiceTest.cs | 93 ++++---- .../OptimizelyUserContextTest.cs | 201 ++++++++++++++++++ OptimizelySDK/Bucketing/Bucketer.cs | 14 +- OptimizelySDK/Bucketing/DecisionService.cs | 177 +++++++++++---- OptimizelySDK/Optimizely.cs | 199 ++++++++++++++--- OptimizelySDK/OptimizelyUserContext.cs | 4 +- OptimizelySDK/Utils/ExperimentUtils.cs | 23 +- 8 files changed, 590 insertions(+), 144 deletions(-) create mode 100644 OptimizelySDK.Tests/OptimizelyUserContextTest.cs diff --git a/OptimizelySDK.Tests/BucketerTest.cs b/OptimizelySDK.Tests/BucketerTest.cs index aeeeb96c..78b0feec 100644 --- a/OptimizelySDK.Tests/BucketerTest.cs +++ b/OptimizelySDK.Tests/BucketerTest.cs @@ -19,6 +19,7 @@ using OptimizelySDK.Config; using OptimizelySDK.Entity; using OptimizelySDK.Logger; +using OptimizelySDK.OptimizelyDecisions; namespace OptimizelySDK.Tests { @@ -98,7 +99,7 @@ public void TestBucketValidExperimentNotInGroup() // control Assert.AreEqual(new Variation { Id = "7722370027", Key = "control" }, - bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId)); + bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance())); LoggerMock.Verify(l => l.Log(It.IsAny(), It.IsAny()), Times.Exactly(2)); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [3000] to user [testUserId] with bucketing ID [testBucketingIdControl!].")); @@ -106,7 +107,7 @@ public void TestBucketValidExperimentNotInGroup() // variation Assert.AreEqual(new Variation { Id = "7721010009", Key = "variation" }, - bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId)); + bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance())); LoggerMock.Verify(l => l.Log(It.IsAny(), It.IsAny()), Times.Exactly(4)); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [7000] to user [testUserId] with bucketing ID [testBucketingIdControl!].")); @@ -114,7 +115,7 @@ public void TestBucketValidExperimentNotInGroup() // no variation Assert.AreEqual(new Variation { }, - bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId)); + bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance())); LoggerMock.Verify(l => l.Log(It.IsAny(), It.IsAny()), Times.Exactly(6)); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [9000] to user [testUserId] with bucketing ID [testBucketingIdControl!].")); @@ -130,7 +131,7 @@ public void TestBucketValidExperimentInGroup() // variation 1 bucketer.SetBucketValues(new[] { 1000, 4000 }); Assert.AreEqual(new Variation { Id = "7722260071", Key = "group_exp_1_var_1" }, - bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId)); + bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance())); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [1000] to user [testUserId] with bucketing ID [testBucketingIdControl!].")); LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [testUserId] is in experiment [group_experiment_1] of group [7722400015].")); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [4000] to user [testUserId] with bucketing ID [testBucketingIdControl!].")); @@ -139,7 +140,7 @@ public void TestBucketValidExperimentInGroup() // variation 2 bucketer.SetBucketValues(new[] { 1500, 7000 }); Assert.AreEqual(new Variation { Id = "7722360022", Key = "group_exp_1_var_2" }, - bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId)); + bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance())); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [1500] to user [testUserId] with bucketing ID [testBucketingIdControl!].")); LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [testUserId] is in experiment [group_experiment_1] of group [7722400015].")); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [7000] to user [testUserId] with bucketing ID [testBucketingIdControl!].")); @@ -148,7 +149,7 @@ public void TestBucketValidExperimentInGroup() // User not in experiment bucketer.SetBucketValues(new[] { 5000, 7000 }); Assert.AreEqual(new Variation { }, - bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId)); + bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance())); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [5000] to user [testUserId] with bucketing ID [testBucketingIdControl!].")); LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [testUserId] is not in experiment [group_experiment_1] of group [7722400015].")); @@ -161,7 +162,7 @@ public void TestBucketInvalidExperiment() var bucketer = new Bucketer(LoggerMock.Object); Assert.AreEqual(new Variation { }, - bucketer.Bucket(Config, new Experiment(), TestBucketingIdControl, TestUserId)); + bucketer.Bucket(Config, new Experiment(), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance())); LoggerMock.Verify(l => l.Log(It.IsAny(), It.IsAny()), Times.Never); } @@ -176,7 +177,7 @@ public void TestBucketWithBucketingId() // make sure that the bucketing ID is used for the variation bucketing and not the user ID Assert.AreEqual(expectedVariation, - bucketer.Bucket(Config, experiment, TestBucketingIdControl, TestUserIdBucketsToVariation)); + bucketer.Bucket(Config, experiment, TestBucketingIdControl, TestUserIdBucketsToVariation, DefaultDecisionReasons.NewInstance())); } // Test for invalid experiment keys, null variation should be returned @@ -187,7 +188,7 @@ public void TestBucketVariationInvalidExperimentsWithBucketingId() var expectedVariation = new Variation(); Assert.AreEqual(expectedVariation, - bucketer.Bucket(Config, Config.GetExperimentFromKey("invalid_experiment"), TestBucketingIdVariation, TestUserId)); + bucketer.Bucket(Config, Config.GetExperimentFromKey("invalid_experiment"), TestBucketingIdVariation, TestUserId, DefaultDecisionReasons.NewInstance())); } // Make sure that the bucketing ID is used to bucket the user into a group and not the user ID @@ -200,7 +201,7 @@ public void TestBucketVariationGroupedExperimentsWithBucketingId() Assert.AreEqual(expectedGroupVariation, bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_2"), - TestBucketingIdGroupExp2Var2, TestUserIdBucketsToNoGroup)); + TestBucketingIdGroupExp2Var2, TestUserIdBucketsToNoGroup, DefaultDecisionReasons.NewInstance())); } // Make sure that user gets bucketed into the rollout rule. @@ -213,7 +214,7 @@ public void TestBucketRolloutRule() var expectedVariation = Config.GetVariationFromId(rolloutRule.Key, "177773"); Assert.True(TestData.CompareObjects(expectedVariation, - bucketer.Bucket(Config, rolloutRule, "testBucketingId", TestUserId))); + bucketer.Bucket(Config, rolloutRule, "testBucketingId", TestUserId, DefaultDecisionReasons.NewInstance()))); } } } \ No newline at end of file diff --git a/OptimizelySDK.Tests/DecisionServiceTest.cs b/OptimizelySDK.Tests/DecisionServiceTest.cs index 0936a857..3c83b3db 100644 --- a/OptimizelySDK.Tests/DecisionServiceTest.cs +++ b/OptimizelySDK.Tests/DecisionServiceTest.cs @@ -24,6 +24,7 @@ using OptimizelySDK.Bucketing; using OptimizelySDK.Utils; using OptimizelySDK.Config; +using OptimizelySDK.OptimizelyDecisions; namespace OptimizelySDK.Tests { @@ -84,7 +85,7 @@ public void TestGetVariationForcedVariationPrecedesAudienceEval() LoggerMock.Verify(l => l.Log(LogLevel.INFO, string.Format("User \"{0}\" is forced in variation \"vtag5\".", WhitelistedUserId)), Times.Once); // no attributes provided for a experiment that has an audience Assert.IsTrue(TestData.CompareObjects(actualVariation, expectedVariation)); - BucketerMock.Verify(_ => _.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + BucketerMock.Verify(_ => _.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Test] @@ -111,7 +112,7 @@ public void TestGetVariationEvaluatesUserProfileBeforeAudienceTargeting() // ensure that a user with a saved user profile, sees the same variation regardless of audience evaluation decisionService.GetVariation(experiment, UserProfileId, ProjectConfig, new UserAttributes()); - BucketerMock.Verify(_ => _.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + BucketerMock.Verify(_ => _.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Test] @@ -123,7 +124,7 @@ public void TestGetForcedVariationReturnsForcedVariation() LoggerMock.Verify(l => l.Log(LogLevel.INFO, string.Format("User \"{0}\" is forced in variation \"{1}\".", WhitelistedUserId, WhitelistedVariation.Key)), Times.Once); - BucketerMock.Verify(_ => _.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + BucketerMock.Verify(_ => _.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Test] @@ -164,7 +165,7 @@ public void TestGetForcedVariationWithInvalidVariation() string.Format("Variation \"{0}\" is not in the datafile. Not activating user \"{1}\".", invalidVariationKey, userId)), Times.Once); - BucketerMock.Verify(_ => _.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + BucketerMock.Verify(_ => _.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Test] @@ -216,7 +217,7 @@ public void TestGetStoredVariationLogsWhenLookupReturnsNull() DecisionService decisionService = new DecisionService(bucketer, ErrorHandlerMock.Object, userProfileService, LoggerMock.Object); - Assert.IsNull(decisionService.GetStoredVariation(experiment, userProfile, ProjectConfig)); + Assert.IsNull(decisionService.GetStoredVariation(experiment, userProfile, ProjectConfig, DefaultDecisionReasons.NewInstance())); LoggerMock.Verify(l => l.Log(LogLevel.INFO, string.Format("No previously activated variation of experiment \"{0}\" for user \"{1}\" found in user profile." , experiment.Key, UserProfileId)), Times.Once); @@ -241,7 +242,7 @@ public void TestGetStoredVariationReturnsNullWhenVariationIsNoLongerInConfig() DecisionService decisionService = new DecisionService(bucketer, ErrorHandlerMock.Object, UserProfileServiceMock.Object, LoggerMock.Object); - Assert.IsNull(decisionService.GetStoredVariation(experiment, storedUserProfile, ProjectConfig)); + Assert.IsNull(decisionService.GetStoredVariation(experiment, storedUserProfile, ProjectConfig, DefaultDecisionReasons.NewInstance())); LoggerMock.Verify(l => l.Log(LogLevel.INFO, string.Format("User \"{0}\" was previously bucketed into variation with ID \"{1}\" for experiment \"{2}\", but no matching variation was found for that user. We will re-bucket the user." , UserProfileId, storedVariationId, experiment.Id)), Times.Once); } @@ -265,7 +266,7 @@ public void TestGetVariationSavesBucketedVariationIntoUserProfile() }); var mockBucketer = new Mock(LoggerMock.Object); - mockBucketer.Setup(m => m.Bucket(ProjectConfig, experiment, UserProfileId, UserProfileId)).Returns(variation); + mockBucketer.Setup(m => m.Bucket(ProjectConfig, experiment, UserProfileId, UserProfileId, It.IsAny())).Returns(variation); DecisionService decisionService = new DecisionService(mockBucketer.Object, ErrorHandlerMock.Object, UserProfileServiceMock.Object, LoggerMock.Object); @@ -317,7 +318,7 @@ public void TestGetVariationSavesANewUserProfile() }); var mockBucketer = new Mock(LoggerMock.Object); - mockBucketer.Setup(m => m.Bucket(ProjectConfig, experiment, UserProfileId, UserProfileId)).Returns(variation); + mockBucketer.Setup(m => m.Bucket(ProjectConfig, experiment, UserProfileId, UserProfileId, It.IsAny())).Returns(variation); Dictionary userProfile = null; @@ -468,7 +469,7 @@ public void TestGetVariationWithBucketingId() public void TestGetVariationForFeatureExperimentGivenNullExperimentIds() { var featureFlag = ProjectConfig.GetFeatureFlagFromKey("empty_feature"); - var decision = DecisionService.GetVariationForFeatureExperiment(featureFlag, GenericUserId, new UserAttributes() { }, ProjectConfig); + var decision = DecisionService.GetVariationForFeatureExperiment(featureFlag, GenericUserId, new UserAttributes() { }, ProjectConfig, new List(), DefaultDecisionReasons.NewInstance()); Assert.IsNull(decision); @@ -487,7 +488,7 @@ public void TestGetVariationForFeatureExperimentGivenExperimentNotInDataFile() ExperimentIds = new List { "29039203" } }; - var decision = DecisionService.GetVariationForFeatureExperiment(featureFlag, GenericUserId, new UserAttributes() { }, ProjectConfig); + var decision = DecisionService.GetVariationForFeatureExperiment(featureFlag, GenericUserId, new UserAttributes() { }, ProjectConfig, new List(), DefaultDecisionReasons.NewInstance()); Assert.IsNull(decision); LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Experiment ID \"29039203\" is not in datafile.")); @@ -502,7 +503,7 @@ public void TestGetVariationForFeatureExperimentGivenNonMutexGroupAndUserNotBuck DecisionServiceMock.Setup(ds => ds.GetVariation(multiVariateExp, "user1", ProjectConfig, null)).Returns(null); var featureFlag = ProjectConfig.GetFeatureFlagFromKey("multi_variate_feature"); - var decision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", new UserAttributes(), ProjectConfig); + var decision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", new UserAttributes(), ProjectConfig, new List(), DefaultDecisionReasons.NewInstance()); Assert.IsNull(decision); LoggerMock.Verify(l => l.Log(LogLevel.INFO, "The user \"user1\" is not bucketed into any of the experiments on the feature \"multi_variate_feature\".")); @@ -518,10 +519,10 @@ public void TestGetVariationForFeatureExperimentGivenNonMutexGroupAndUserIsBucke var userAttributes = new UserAttributes(); DecisionServiceMock.Setup(ds => ds.GetVariation(ProjectConfig.GetExperimentFromKey("test_experiment_multivariate"), - "user1", ProjectConfig, userAttributes)).Returns(variation); + "user1", ProjectConfig, userAttributes, It.IsAny>(), It.IsAny())).Returns(variation); var featureFlag = ProjectConfig.GetFeatureFlagFromKey("multi_variate_feature"); - var decision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig); + var decision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig, new List(), DefaultDecisionReasons.NewInstance()); Assert.IsTrue(TestData.CompareObjects(expectedDecision, decision)); @@ -541,7 +542,7 @@ public void TestGetVariationForFeatureExperimentGivenMutexGroupAndUserIsBucketed userAttributes)).Returns(variation); var featureFlag = ProjectConfig.GetFeatureFlagFromKey("boolean_feature"); - var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig); + var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig, new List(), DefaultDecisionReasons.NewInstance()); Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision)); @@ -553,11 +554,11 @@ public void TestGetVariationForFeatureExperimentGivenMutexGroupAndUserIsBucketed public void TestGetVariationForFeatureExperimentGivenMutexGroupAndUserNotBucketed() { var mutexExperiment = ProjectConfig.GetExperimentFromKey("group_experiment_1"); - DecisionServiceMock.Setup(ds => ds.GetVariation(It.IsAny(), It.IsAny(), ProjectConfig, It.IsAny())). + DecisionServiceMock.Setup(ds => ds.GetVariation(It.IsAny(), It.IsAny(), ProjectConfig, It.IsAny(), It.IsAny>(), It.IsAny())). Returns(null); var featureFlag = ProjectConfig.GetFeatureFlagFromKey("boolean_feature"); - var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", new UserAttributes(), ProjectConfig); + var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", new UserAttributes(), ProjectConfig, new List(), DefaultDecisionReasons.NewInstance()); Assert.IsNull(actualDecision); @@ -582,7 +583,7 @@ public void TestGetVariationForFeatureRolloutWhenNoRuleInRollouts() var decisionService = new DecisionService(new Bucketer(new NoOpLogger()), new NoOpErrorHandler(), null, new NoOpLogger()); - var variation = decisionService.GetVariationForFeatureRollout(featureFlag, "userId1", null, projectConfig); + var variation = decisionService.GetVariationForFeatureRollout(featureFlag, "userId1", null, projectConfig, DefaultDecisionReasons.NewInstance()); Assert.IsNull(variation); } @@ -600,9 +601,9 @@ public void TestGetVariationForFeatureRolloutWhenRolloutIsNotInDataFile() Variables = featureFlag.Variables }; - DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny(), It.IsAny(), It.IsAny(), ProjectConfig)).Returns(null); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny(), It.IsAny(), It.IsAny(), ProjectConfig, new List(), DefaultDecisionReasons.NewInstance())).Returns(null); - var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureRollout(featureFlag, "user1", new UserAttributes(), ProjectConfig); + var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureRollout(featureFlag, "user1", new UserAttributes(), ProjectConfig, DefaultDecisionReasons.NewInstance()); Assert.IsNull(actualDecision); LoggerMock.Verify(l => l.Log(LogLevel.INFO, "The feature flag \"boolean_feature\" is not used in a rollout.")); @@ -624,10 +625,10 @@ public void TestGetVariationForFeatureRolloutWhenUserIsBucketedInTheTargetingRul }; BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny())).Returns(variation); + It.IsAny(), It.IsAny())).Returns(variation); var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, null, LoggerMock.Object); - var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes, ProjectConfig); + var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes, ProjectConfig, DefaultDecisionReasons.NewInstance()); Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision)); } @@ -648,11 +649,11 @@ public void TestGetVariationForFeatureRolloutWhenUserIsNotBucketedInTheTargeting { "browser_type", "chrome" } }; - BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), experiment, It.IsAny(), It.IsAny())).Returns(null); - BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), everyoneElseRule, It.IsAny(), It.IsAny())).Returns(variation); + BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), experiment, It.IsAny(), It.IsAny(), It.IsAny())).Returns(null); + BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), everyoneElseRule, It.IsAny(), It.IsAny(), It.IsAny())).Returns(variation); var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, null, LoggerMock.Object); - var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes, ProjectConfig); + var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes, ProjectConfig, DefaultDecisionReasons.NewInstance()); Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision)); } @@ -668,10 +669,10 @@ public void TestGetVariationForFeatureRolloutWhenUserIsNeitherBucketedInTheTarge { "browser_type", "chrome" } }; - BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(null); + BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(null); var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, null, LoggerMock.Object); - var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes, ProjectConfig); + var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes, ProjectConfig, DefaultDecisionReasons.NewInstance()); Assert.IsNull(actualDecision); } @@ -689,11 +690,11 @@ public void TestGetVariationForFeatureRolloutWhenUserDoesNotQualifyForAnyTargeti var variation = everyoneElseRule.Variations[0]; var expectedDecision = new FeatureDecision(everyoneElseRule, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT); - BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), everyoneElseRule, It.IsAny(), It.IsAny())).Returns(variation); + BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), everyoneElseRule, It.IsAny(), It.IsAny(), It.IsAny())).Returns(variation); var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, null, LoggerMock.Object); // Provide null attributes so that user does not qualify for audience. - var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", null, ProjectConfig); + var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", null, ProjectConfig, DefaultDecisionReasons.NewInstance()); Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision)); @@ -723,7 +724,7 @@ public void TestGetVariationForFeatureRolloutAudienceAndTrafficeAllocationCheck( { { "device_type", "iPhone" }, { "location", "San Francisco" } - }, ProjectConfig); + }, ProjectConfig, DefaultDecisionReasons.NewInstance()); // Returned variation id should be '177773' because of audience 'iPhone users in San Francisco'. var expectedDecision = new FeatureDecision(expWithAudienceiPhoneUsers, varWithAudienceiPhoneUsers, FeatureDecision.DECISION_SOURCE_ROLLOUT); @@ -733,7 +734,7 @@ public void TestGetVariationForFeatureRolloutAudienceAndTrafficeAllocationCheck( actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, new UserAttributes { { "browser_type", "chrome" } - }, ProjectConfig); + }, ProjectConfig, DefaultDecisionReasons.NewInstance()); // Returned variation id should be '177771' because of audience 'Chrome users'. expectedDecision = new FeatureDecision(expWithAudienceChromeUsers, varWithAudienceChromeUsers, FeatureDecision.DECISION_SOURCE_ROLLOUT); @@ -741,7 +742,7 @@ public void TestGetVariationForFeatureRolloutAudienceAndTrafficeAllocationCheck( // Calling with no audience. mockBucketer.Setup(bm => bm.GenerateBucketValue(It.IsAny())).Returns(8000); - actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, new UserAttributes(), ProjectConfig); + actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, new UserAttributes(), ProjectConfig, DefaultDecisionReasons.NewInstance()); // Returned variation id should be of everyone else rule because of no audience. expectedDecision = new FeatureDecision(expWithNoAudience, varWithNoAudience, FeatureDecision.DECISION_SOURCE_ROLLOUT); @@ -752,7 +753,7 @@ public void TestGetVariationForFeatureRolloutAudienceAndTrafficeAllocationCheck( actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, new UserAttributes { { "browser_type", "chrome" } - }, ProjectConfig); + }, ProjectConfig, DefaultDecisionReasons.NewInstance()); // Returned decision entity should be null because bucket value exceeds traffice allocation of everyone else rule. Assert.Null(actualDecision); @@ -768,21 +769,21 @@ public void TestGetVariationForFeatureRolloutCheckAudienceInEveryoneElseRule() var variation = everyoneElseRule.Variations[0]; var expectedDecision = new FeatureDecision(everyoneElseRule, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT); - BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), everyoneElseRule, It.IsAny(), WhitelistedUserId)).Returns(variation); - BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), everyoneElseRule, It.IsAny(), GenericUserId)).Returns(null); + BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), everyoneElseRule, It.IsAny(), WhitelistedUserId, It.IsAny())).Returns(variation); + BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), everyoneElseRule, It.IsAny(), GenericUserId, It.IsAny())).Returns(null); var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, null, LoggerMock.Object); // Returned variation id should be of everyone else rule as it passes audience Id checking. - var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, WhitelistedUserId, null, ProjectConfig); + var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, WhitelistedUserId, null, ProjectConfig, DefaultDecisionReasons.NewInstance()); Assert.True(TestData.CompareObjects(expectedDecision, actualDecision)); // Returned variation id should be null. - actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, null, ProjectConfig); + actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, null, ProjectConfig, DefaultDecisionReasons.NewInstance()); Assert.Null(actualDecision); // Returned variation id should be null as it fails audience Id checking. everyoneElseRule.AudienceIds = new string[] { ProjectConfig.Audiences[0].Id }; - actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, null, ProjectConfig); + actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, null, ProjectConfig, DefaultDecisionReasons.NewInstance()); Assert.Null(actualDecision); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "User \"testUser1\" does not meet the conditions for targeting rule \"1\"."), Times.Once); @@ -807,7 +808,7 @@ public void TestGetVariationForFeatureWhenTheUserIsBucketedIntoFeatureExperiment var expectedDecision = new FeatureDecision(expectedExperiment, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST); DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny(), It.IsAny(), - It.IsAny(), ProjectConfig)).Returns(expectedDecision); + It.IsAny(), ProjectConfig, It.IsAny>(), It.IsAny())).Returns(expectedDecision); var actualDecision = DecisionServiceMock.Object.GetVariationForFeature(featureFlag, "user1", ProjectConfig, new UserAttributes()); Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision)); @@ -826,9 +827,9 @@ public void TestGetVariationForFeatureWhenTheUserIsNotBucketedIntoFeatureExperim var expectedDecision = new FeatureDecision(expectedExperiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT); DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny(), It.IsAny(), - It.IsAny(), ProjectConfig)).Returns(null); + It.IsAny(), ProjectConfig, It.IsAny>(), It.IsAny())).Returns(null); DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureRollout(It.IsAny(), It.IsAny(), - It.IsAny(), ProjectConfig)).Returns(expectedDecision); + It.IsAny(), ProjectConfig, It.IsAny())).Returns(expectedDecision); var actualDecision = DecisionServiceMock.Object.GetVariationForFeature(featureFlag, "user1", ProjectConfig, new UserAttributes()); @@ -844,8 +845,8 @@ public void TestGetVariationForFeatureWhenTheUserIsNeitherBucketedIntoFeatureExp var featureFlag = ProjectConfig.GetFeatureFlagFromKey("string_single_variable_feature"); var expectedDecision = new FeatureDecision(null, null, FeatureDecision.DECISION_SOURCE_ROLLOUT); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny(), It.IsAny(), It.IsAny(), ProjectConfig)).Returns(null); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureRollout(It.IsAny(), It.IsAny(), It.IsAny(), ProjectConfig)).Returns(null); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny(), It.IsAny(), It.IsAny(), ProjectConfig, new List(), DefaultDecisionReasons.NewInstance())).Returns(null); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureRollout(It.IsAny(), It.IsAny(), It.IsAny(), ProjectConfig, DefaultDecisionReasons.NewInstance())).Returns(null); var actualDecision = DecisionServiceMock.Object.GetVariationForFeature(featureFlag, "user1", ProjectConfig, new UserAttributes()); Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision)); @@ -866,8 +867,8 @@ public void TestGetVariationForFeatureWhenTheUserIsBuckedtedInBothExperimentAndR { "browser_type", "chrome" } }; - DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, "user1", ProjectConfig, userAttributes)).Returns(variation); - var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig); + DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, "user1", ProjectConfig, userAttributes, It.IsAny>(), It.IsAny())).Returns(variation); + var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig, new List(), DefaultDecisionReasons.NewInstance()); // The user is bucketed into feature experiment's variation. Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision)); @@ -877,8 +878,8 @@ public void TestGetVariationForFeatureWhenTheUserIsBuckedtedInBothExperimentAndR var rolloutVariation = rolloutExperiment.Variations[0]; var expectedRolloutDecision = new FeatureDecision(rolloutExperiment, rolloutVariation, FeatureDecision.DECISION_SOURCE_ROLLOUT); - BucketerMock.Setup(bm => bm.Bucket(ProjectConfig, rolloutExperiment, It.IsAny(), It.IsAny())).Returns(rolloutVariation); - var actualRolloutDecision = DecisionServiceMock.Object.GetVariationForFeatureRollout(featureFlag, "user1", userAttributes, ProjectConfig); + BucketerMock.Setup(bm => bm.Bucket(ProjectConfig, rolloutExperiment, It.IsAny(), It.IsAny(), It.IsAny())).Returns(rolloutVariation); + var actualRolloutDecision = DecisionServiceMock.Object.GetVariationForFeatureRollout(featureFlag, "user1", userAttributes, ProjectConfig, DefaultDecisionReasons.NewInstance()); // The user is bucketed into feature rollout's variation. diff --git a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs new file mode 100644 index 00000000..029c8499 --- /dev/null +++ b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs @@ -0,0 +1,201 @@ +/** + * + * Copyright 2020, Optimizely 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. + */ + +using Castle.Core.Internal; +using Moq; +using NUnit.Framework; +using OptimizelySDK.Entity; +using OptimizelySDK.ErrorHandler; +using OptimizelySDK.Event.Dispatcher; +using OptimizelySDK.Logger; +using OptimizelySDK.OptimizelyDecisions; +using System; +using System.Collections.Generic; + +namespace OptimizelySDK.Tests +{ + [TestFixture] + public class OptimizelyUserContextTest + { + string UserID = "testUserID"; + private Optimizely Optimizely; + private Mock LoggerMock; + private Mock ErrorHandlerMock; + private Mock EventDispatcherMock; + + [SetUp] + public void SetUp() + { + LoggerMock = new Mock(); + LoggerMock.Setup(i => i.Log(It.IsAny(), It.IsAny())); + + ErrorHandlerMock = new Mock(); + ErrorHandlerMock.Setup(e => e.HandleError(It.IsAny())); + EventDispatcherMock = new Mock(); + + Optimizely = new Optimizely(TestData.Datafile, EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object); + } + + [Test] + public void OptimizelyUserContext_withAttributes() + { + var attributes = new UserAttributes() { { "house", "GRYFFINDOR" } }; + OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object); + + Assert.AreEqual(user.Optimizely, Optimizely); + Assert.AreEqual(user.UserId, UserID); + Assert.AreEqual(user.UserAttributes, attributes); + } + + [Test] + public void OptimizelyUserContext_noAttributes() + { + OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, null, ErrorHandlerMock.Object, LoggerMock.Object); + + Assert.AreEqual(user.Optimizely, Optimizely); + Assert.AreEqual(user.UserId, UserID); + Assert.True(user.UserAttributes.Count == 0); + } + + [Test] + public void SetAttribute() + { + var attributes = new UserAttributes() { { "house", "GRYFFINDOR" } }; + OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object); + + user.SetAttribute("k1", "v1"); + user.SetAttribute("k2", true); + user.SetAttribute("k3", 100); + user.SetAttribute("k4", 3.5); + + Assert.AreEqual(user.Optimizely, Optimizely); + Assert.AreEqual(user.UserId, UserID); + var newAttributes = user.UserAttributes; + Assert.AreEqual(newAttributes["house"], "GRYFFINDOR"); + Assert.AreEqual(newAttributes["k1"], "v1"); + Assert.AreEqual(newAttributes["k2"], true); + Assert.AreEqual(newAttributes["k3"], 100); + Assert.AreEqual(newAttributes["k4"], 3.5); + } + + [Test] + public void SetAttribute_noAttribute() + { + OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, null, ErrorHandlerMock.Object, LoggerMock.Object); + + user.SetAttribute("k1", "v1"); + user.SetAttribute("k2", true); + + Assert.AreEqual(user.Optimizely, Optimizely); + Assert.AreEqual(user.UserId, UserID); + var newAttributes = user.UserAttributes; + Assert.AreEqual(newAttributes["k1"], "v1"); + Assert.AreEqual(newAttributes["k2"], true); + } + + [Test] + public void SetAttribute_override() + { + var attributes = new UserAttributes() { { "house", "GRYFFINDOR" } }; + OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object); + + user.SetAttribute("k1", "v1"); + user.SetAttribute("house", "v2"); + + var newAttributes = user.UserAttributes; + Assert.AreEqual(newAttributes["k1"], "v1"); + Assert.AreEqual(newAttributes["house"], "v2"); + } + + [Test] + public void SetAttribute_nullValue() + { + var attributes = new UserAttributes() { { "k1", null } }; + OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object); + + var newAttributes = user.UserAttributes; + Assert.AreEqual(newAttributes["k1"], null); + + user.SetAttribute("k1", true); + newAttributes = user.UserAttributes; + Assert.AreEqual(newAttributes["k1"], true); + + user.SetAttribute("k1", null); + newAttributes = user.UserAttributes; + Assert.AreEqual(newAttributes["k1"], null); + } + + #region decide + + [Test] + public void Decide() + { + var flagKey = "multi_variate_feature"; + var variablesExpected = Optimizely.GetAllFeatureVariables(flagKey, UserID); + + var user = Optimizely.CreateUserContext(UserID); + user.SetAttribute("browser_type", "chrome"); + var decision = user.Decide(flagKey); + + Assert.AreEqual(decision.VariationKey, "Gred"); + Assert.False(decision.Enabled); + Assert.AreEqual(decision.Variables.ToDictionary(), variablesExpected.ToDictionary()); + Assert.AreEqual(decision.RuleKey, "test_experiment_multivariate"); + Assert.AreEqual(decision.FlagKey, flagKey); + Assert.AreEqual(decision.UserContext, user); + Assert.True(decision.Reasons.IsNullOrEmpty()); + } + + [Test] + public void DecideInvalidFlagKey() + { + var flagKey = "invalid_feature"; + + var user = Optimizely.CreateUserContext(UserID); + user.SetAttribute("browser_type", "chrome"); + var decision = user.Decide(flagKey); + + Assert.Null(decision.VariationKey); + Assert.False(decision.Enabled); + Assert.AreEqual(decision.Variables.ToDictionary(), new Dictionary()); + Assert.IsNull(decision.RuleKey); + Assert.AreEqual(decision.FlagKey, flagKey); + Assert.AreEqual(decision.UserContext, user); + Assert.True(decision.Reasons.IsNullOrEmpty()); + } + + [Test] + public void DecideInvalidConfig() + { + Optimizely optimizely = new Optimizely(TestData.UnsupportedVersionDatafile, EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object); + + var flagKey = "multi_variate_feature"; + var decisionExpected = OptimizelyDecision.NewErrorDecision( + flagKey, + new OptimizelyUserContext(optimizely, UserID, new UserAttributes(), ErrorHandlerMock.Object, LoggerMock.Object), + DecisionMessage.SDK_NOT_READY, + ErrorHandlerMock.Object, + LoggerMock.Object); + var user = optimizely.CreateUserContext(UserID); + var decision = user.Decide(flagKey); + + Assert.IsTrue(TestData.CompareObjects(decision, decisionExpected)); + } + #endregion + + } +} diff --git a/OptimizelySDK/Bucketing/Bucketer.cs b/OptimizelySDK/Bucketing/Bucketer.cs index b8944687..be62727a 100644 --- a/OptimizelySDK/Bucketing/Bucketer.cs +++ b/OptimizelySDK/Bucketing/Bucketer.cs @@ -15,6 +15,7 @@ */ using OptimizelySDK.Entity; using OptimizelySDK.Logger; +using OptimizelySDK.OptimizelyDecisions; using System; using System.Collections.Generic; using System.Text; @@ -102,8 +103,9 @@ private string FindBucket(string bucketingId, string userId, string parentId, IE /// Experiment Experiment in which user is to be bucketed /// A customer-assigned value used to create the key for the murmur hash. /// User identifier + /// Decision log messages. /// Variation which will be shown to the user - public virtual Variation Bucket(ProjectConfig config, Experiment experiment, string bucketingId, string userId) + public virtual Variation Bucket(ProjectConfig config, Experiment experiment, string bucketingId, string userId, IDecisionReasons reasons) { string message; Variation variation; @@ -122,26 +124,26 @@ public virtual Variation Bucket(ProjectConfig config, Experiment experiment, str if (string.IsNullOrEmpty(userExperimentId)) { message = $"User [{userId}] is in no experiment."; - Logger.Log(LogLevel.INFO, message); + Logger.Log(LogLevel.INFO, reasons.AddInfo(message)); return new Variation(); } if (userExperimentId != experiment.Id) { message = $"User [{userId}] is not in experiment [{experiment.Key}] of group [{experiment.GroupId}]."; - Logger.Log(LogLevel.INFO, message); + Logger.Log(LogLevel.INFO, reasons.AddInfo(message)); return new Variation(); } message = $"User [{userId}] is in experiment [{experiment.Key}] of group [{experiment.GroupId}]."; - Logger.Log(LogLevel.INFO, message); + Logger.Log(LogLevel.INFO, reasons.AddInfo(message)); } // Bucket user if not in whitelist and in group (if any). string variationId = FindBucket(bucketingId, userId, experiment.Id, experiment.TrafficAllocation); if (string.IsNullOrEmpty(variationId)) { - Logger.Log(LogLevel.INFO, $"User [{userId}] is in no variation."); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"User [{userId}] is in no variation.")); return new Variation(); } @@ -149,7 +151,7 @@ public virtual Variation Bucket(ProjectConfig config, Experiment experiment, str // success! variation = config.GetVariationFromId(experiment.Key, variationId); message = $"User [{userId}] is in variation [{variation.Key}] of experiment [{experiment.Key}]."; - Logger.Log(LogLevel.INFO, message); + Logger.Log(LogLevel.INFO, reasons.AddInfo(message)); return variation; } } diff --git a/OptimizelySDK/Bucketing/DecisionService.cs b/OptimizelySDK/Bucketing/DecisionService.cs index 3959dbbd..3973b13e 100644 --- a/OptimizelySDK/Bucketing/DecisionService.cs +++ b/OptimizelySDK/Bucketing/DecisionService.cs @@ -19,6 +19,7 @@ using OptimizelySDK.Entity; using OptimizelySDK.ErrorHandler; using OptimizelySDK.Logger; +using OptimizelySDK.OptimizelyDecisions; using OptimizelySDK.Utils; namespace OptimizelySDK.Bucketing @@ -83,20 +84,43 @@ public DecisionService(Bucketer bucketer, IErrorHandler errorHandler, UserProfil /// The userId of the user. /// The user's attributes. This should be filtered to just attributes in the Datafile. /// The Variation the user is allocated into. - public virtual Variation GetVariation(Experiment experiment, string userId, ProjectConfig config, UserAttributes filteredAttributes) + public virtual Variation GetVariation(Experiment experiment, + string userId, + ProjectConfig config, + UserAttributes filteredAttributes) + { + return GetVariation(experiment, userId, config, filteredAttributes, new List(), DefaultDecisionReasons.NewInstance()); + } + + /// + /// Get a Variation of an Experiment for a user to be allocated into. + /// + /// The Experiment the user will be bucketed into. + /// The userId of the user. + /// The user's attributes. This should be filtered to just attributes in the Datafile. + /// The Variation the user is allocated into. + public virtual Variation GetVariation(Experiment experiment, + string userId, + ProjectConfig config, + UserAttributes filteredAttributes, + List options, + IDecisionReasons reasons) { if (!ExperimentUtils.IsExperimentActive(experiment, Logger)) return null; // check if a forced variation is set - var forcedVariation = GetForcedVariation(experiment.Key, userId, config); + var forcedVariation = GetForcedVariation(experiment.Key, userId, config, reasons); if (forcedVariation != null) return forcedVariation; - var variation = GetWhitelistedVariation(experiment, userId); + var variation = GetWhitelistedVariation(experiment, userId, reasons); if (variation != null) return variation; + // fetch the user profile map from the user profile service + var ignoreUPS = options.Contains(OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE); + UserProfile userProfile = null; - if (UserProfileService != null) + if (UserProfileService != null && !ignoreUPS) { try { @@ -104,21 +128,21 @@ public virtual Variation GetVariation(Experiment experiment, string userId, Proj if (userProfileMap != null && UserProfileUtil.IsValidUserProfileMap(userProfileMap)) { userProfile = UserProfileUtil.ConvertMapToUserProfile(userProfileMap); - variation = GetStoredVariation(experiment, userProfile, config); + variation = GetStoredVariation(experiment, userProfile, config, reasons); if (variation != null) return variation; } else if (userProfileMap == null) { - Logger.Log(LogLevel.INFO, "We were unable to get a user profile map from the UserProfileService."); + Logger.Log(LogLevel.INFO, reasons.AddInfo("We were unable to get a user profile map from the UserProfileService.")); } else { - Logger.Log(LogLevel.ERROR, "The UserProfileService returned an invalid map."); + Logger.Log(LogLevel.ERROR, reasons.AddInfo("The UserProfileService returned an invalid map.")); } } catch (Exception exception) { - Logger.Log(LogLevel.ERROR, exception.Message); + Logger.Log(LogLevel.ERROR, reasons.AddInfo(exception.Message)); ErrorHandler.HandleError(new Exceptions.OptimizelyRuntimeException(exception.Message)); } } @@ -126,16 +150,16 @@ public virtual Variation GetVariation(Experiment experiment, string userId, Proj if (ExperimentUtils.DoesUserMeetAudienceConditions(config, experiment, filteredAttributes, LOGGING_KEY_TYPE_EXPERIMENT, experiment.Key, Logger)) { // Get Bucketing ID from user attributes. - string bucketingId = GetBucketingId(userId, filteredAttributes); + string bucketingId = GetBucketingId(userId, filteredAttributes, reasons); - variation = Bucketer.Bucket(config, experiment, bucketingId, userId); + variation = Bucketer.Bucket(config, experiment, bucketingId, userId, reasons); if (variation != null && variation.Key != null) { if (UserProfileService != null) { var bucketerUserProfile = userProfile ?? new UserProfile(userId, new Dictionary()); - SaveVariation(experiment, variation, bucketerUserProfile); + SaveVariation(experiment, variation, bucketerUserProfile, reasons); } else @@ -144,7 +168,7 @@ public virtual Variation GetVariation(Experiment experiment, string userId, Proj return variation; } - Logger.Log(LogLevel.INFO, $"User \"{userId}\" does not meet conditions to be in experiment \"{experiment.Key}\"."); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"User \"{userId}\" does not meet conditions to be in experiment \"{experiment.Key}\".")); return null; } @@ -157,6 +181,18 @@ public virtual Variation GetVariation(Experiment experiment, string userId, Proj /// Project Config /// Variation entity which the given user and experiment should be forced into. public Variation GetForcedVariation(string experimentKey, string userId, ProjectConfig config) + { + return GetForcedVariation(experimentKey, userId, config, DefaultDecisionReasons.NewInstance()); + } + + /// + /// Gets the forced variation for the given user and experiment. + /// + /// The experiment key + /// The user ID + /// Project Config + /// Variation entity which the given user and experiment should be forced into. + public Variation GetForcedVariation(string experimentKey, string userId, ProjectConfig config, IDecisionReasons reasons) { if (ForcedVariationMap.ContainsKey(userId) == false) { @@ -191,8 +227,7 @@ public Variation GetForcedVariation(string experimentKey, string userId, Project // this case is logged in getVariationFromKey if (string.IsNullOrEmpty(variationKey)) return null; - - Logger.Log(LogLevel.DEBUG, $@"Variation ""{variationKey}"" is mapped to experiment ""{experimentKey}"" and user ""{userId}"" in the forced variation map"); + Logger.Log(LogLevel.DEBUG, reasons.AddInfo($@"Variation ""{variationKey}"" is mapped to experiment ""{experimentKey}"" and user ""{userId}"" in the forced variation map")); Variation variation = config.GetVariationFromKey(experimentKey, variationKey); @@ -248,8 +283,6 @@ public bool SetForcedVariation(string experimentKey, string userId, string varia Logger.Log(LogLevel.DEBUG, $@"Set variation ""{variationId}"" for experiment ""{experimentId}"" and user ""{userId}"" in the forced variation map."); return true; } - - /// /// Get the variation the user has been whitelisted into. /// @@ -258,6 +291,19 @@ public bool SetForcedVariation(string experimentKey, string userId, string varia /// if the user is not whitelisted into any variation {@link Variation} /// the user is bucketed into if the user has a specified whitelisted variation. public Variation GetWhitelistedVariation(Experiment experiment, string userId) + { + return GetWhitelistedVariation(experiment, userId, DefaultDecisionReasons.NewInstance()); + } + + /// + /// Get the variation the user has been whitelisted into. + /// + /// in which user is to be bucketed. + /// User Identifier + /// Decision log messages. + /// if the user is not whitelisted into any variation {@link Variation} + /// the user is bucketed into if the user has a specified whitelisted variation. + public Variation GetWhitelistedVariation(Experiment experiment, string userId, IDecisionReasons reasons) { //if a user has a forced variation mapping, return the respective variation Dictionary userIdToVariationKeyMap = experiment.UserIdToKeyVariations; @@ -271,9 +317,9 @@ public Variation GetWhitelistedVariation(Experiment experiment, string userId) : null; if (forcedVariation != null) - Logger.Log(LogLevel.INFO, $"User \"{userId}\" is forced in variation \"{forcedVariationKey}\"."); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"User \"{userId}\" is forced in variation \"{forcedVariationKey}\".")); else - Logger.Log(LogLevel.ERROR, $"Variation \"{forcedVariationKey}\" is not in the datafile. Not activating user \"{userId}\"."); + Logger.Log(LogLevel.ERROR, reasons.AddInfo($"Variation \"{forcedVariationKey}\" is not in the datafile. Not activating user \"{userId}\".")); return forcedVariation; } @@ -284,7 +330,7 @@ public Variation GetWhitelistedVariation(Experiment experiment, string userId) /// which the user was bucketed /// User profile of the user /// The user was previously bucketed into. - public Variation GetStoredVariation(Experiment experiment, UserProfile userProfile, ProjectConfig config) + public Variation GetStoredVariation(Experiment experiment, UserProfile userProfile, ProjectConfig config, IDecisionReasons reasons) { // ---------- Check User Profile for Sticky Bucketing ---------- // If a user profile instance is present then check it for a saved variation @@ -296,7 +342,7 @@ public Variation GetStoredVariation(Experiment experiment, UserProfile userProfi if (decision == null) { - Logger.Log(LogLevel.INFO, $"No previously activated variation of experiment \"{experimentKey}\" for user \"{userProfile.UserId}\" found in user profile."); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"No previously activated variation of experiment \"{experimentKey}\" for user \"{userProfile.UserId}\" found in user profile.")); return null; } @@ -310,11 +356,11 @@ public Variation GetStoredVariation(Experiment experiment, UserProfile userProfi if (savedVariation == null) { - Logger.Log(LogLevel.INFO, $"User \"{userProfile.UserId}\" was previously bucketed into variation with ID \"{variationId}\" for experiment \"{experimentId}\", but no matching variation was found for that user. We will re-bucket the user."); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"User \"{userProfile.UserId}\" was previously bucketed into variation with ID \"{variationId}\" for experiment \"{experimentId}\", but no matching variation was found for that user. We will re-bucket the user.")); return null; } - Logger.Log(LogLevel.INFO, $"Returning previously activated variation \"{savedVariation.Key}\" of experiment \"{experimentKey}\" for user \"{userProfile.UserId}\" from user profile."); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"Returning previously activated variation \"{savedVariation.Key}\" of experiment \"{experimentKey}\" for user \"{userProfile.UserId}\" from user profile.")); return savedVariation; } catch (Exception) @@ -322,7 +368,6 @@ public Variation GetStoredVariation(Experiment experiment, UserProfile userProfi return null; } } - /// /// Save a { @link Variation } of an { @link Experiment } for a user in the {@link UserProfileService}. /// @@ -330,6 +375,17 @@ public Variation GetStoredVariation(Experiment experiment, UserProfile userProfi /// The Variation to save. /// instance of the user information. public void SaveVariation(Experiment experiment, Variation variation, UserProfile userProfile) + { + SaveVariation(experiment, variation, userProfile, DefaultDecisionReasons.NewInstance()); + } + + /// + /// Save a { @link Variation } of an { @link Experiment } for a user in the {@link UserProfileService}. + /// + /// The experiment the user was buck + /// The Variation to save. + /// instance of the user information. + public void SaveVariation(Experiment experiment, Variation variation, UserProfile userProfile, IDecisionReasons reasons) { //only save if the user has implemented a user profile service if (UserProfileService == null) @@ -351,15 +407,15 @@ public void SaveVariation(Experiment experiment, Variation variation, UserProfil try { UserProfileService.Save(userProfile.ToMap()); - Logger.Log(LogLevel.INFO, $"Saved variation \"{variation.Id}\" of experiment \"{experiment.Id}\" for user \"{userProfile.UserId}\"."); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"Saved variation \"{variation.Id}\" of experiment \"{experiment.Id}\" for user \"{userProfile.UserId}\".")); } catch (Exception exception) { - Logger.Log(LogLevel.ERROR, $"Failed to save variation \"{variation.Id}\" of experiment \"{experiment.Id}\" for user \"{userProfile.UserId}\"."); + Logger.Log(LogLevel.ERROR, reasons.AddInfo($"Failed to save variation \"{variation.Id}\" of experiment \"{experiment.Id}\" for user \"{userProfile.UserId}\".")); ErrorHandler.HandleError(new Exceptions.OptimizelyRuntimeException(exception.Message)); } } - + /// /// Try to bucket the user into a rollout rule. /// Evaluate the user for rules in priority order by seeing if the user satisfies the audience. @@ -368,9 +424,14 @@ public void SaveVariation(Experiment experiment, Variation variation, UserProfil /// The feature flag the user wants to access. /// User Identifier /// The user's attributes. This should be filtered to just attributes in the Datafile. + /// Decision log messages. /// null if the user is not bucketed into the rollout or if the feature flag was not attached to a rollout. /// otherwise the FeatureDecision entity - public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag featureFlag, string userId, UserAttributes filteredAttributes, ProjectConfig config) + public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag featureFlag, + string userId, + UserAttributes filteredAttributes, + ProjectConfig config, + IDecisionReasons reasons) { if (featureFlag == null) { @@ -380,7 +441,7 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature if (string.IsNullOrEmpty(featureFlag.RolloutId)) { - Logger.Log(LogLevel.INFO, $"The feature flag \"{featureFlag.Key}\" is not used in a rollout."); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"The feature flag \"{featureFlag.Key}\" is not used in a rollout.")); return null; } @@ -388,7 +449,7 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature if (string.IsNullOrEmpty(rollout.Id)) { - Logger.Log(LogLevel.ERROR, $"The rollout with id \"{featureFlag.RolloutId}\" is not found in the datafile for feature flag \"{featureFlag.Key}\""); + Logger.Log(LogLevel.ERROR, reasons.AddInfo($"The rollout with id \"{featureFlag.RolloutId}\" is not found in the datafile for feature flag \"{featureFlag.Key}\"")); return null; } @@ -400,16 +461,16 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature var rolloutRulesLength = rollout.Experiments.Count; // Get Bucketing ID from user attributes. - string bucketingId = GetBucketingId(userId, filteredAttributes); + string bucketingId = GetBucketingId(userId, filteredAttributes, reasons); // For all rules before the everyone else rule for (int i = 0; i < rolloutRulesLength - 1; i++) { string loggingKey = (i + 1).ToString(); var rolloutRule = rollout.Experiments[i]; - if (ExperimentUtils.DoesUserMeetAudienceConditions(config, rolloutRule, filteredAttributes, LOGGING_KEY_TYPE_RULE, loggingKey, Logger)) + if (ExperimentUtils.DoesUserMeetAudienceConditions(config, rolloutRule, filteredAttributes, LOGGING_KEY_TYPE_RULE, loggingKey, reasons, Logger)) { - variation = Bucketer.Bucket(config, rolloutRule, bucketingId, userId); + variation = Bucketer.Bucket(config, rolloutRule, bucketingId, userId, reasons); if (variation == null || string.IsNullOrEmpty(variation.Id)) break; @@ -424,9 +485,9 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature // Get the last rule which is everyone else rule. var everyoneElseRolloutRule = rollout.Experiments[rolloutRulesLength - 1]; - if (ExperimentUtils.DoesUserMeetAudienceConditions(config, everyoneElseRolloutRule, filteredAttributes, LOGGING_KEY_TYPE_RULE, "Everyone Else", Logger)) + if (ExperimentUtils.DoesUserMeetAudienceConditions(config, everyoneElseRolloutRule, filteredAttributes, LOGGING_KEY_TYPE_RULE, "Everyone Else", reasons, Logger)) { - variation = Bucketer.Bucket(config, everyoneElseRolloutRule, bucketingId, userId); + variation = Bucketer.Bucket(config, everyoneElseRolloutRule, bucketingId, userId, reasons); if (variation != null && !string.IsNullOrEmpty(variation.Id)) { Logger.Log(LogLevel.DEBUG, $"User \"{userId}\" meets conditions for targeting rule \"Everyone Else\"."); @@ -450,7 +511,12 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature /// The user's attributes. This should be filtered to just attributes in the Datafile. /// null if the user is not bucketed into the rollout or if the feature flag was not attached to a rollout. /// Otherwise the FeatureDecision entity - public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag featureFlag, string userId, UserAttributes filteredAttributes, ProjectConfig config) + public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag featureFlag, + string userId, + UserAttributes filteredAttributes, + ProjectConfig config, + List options, + IDecisionReasons reasons) { if (featureFlag == null) { @@ -460,7 +526,7 @@ public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag feat if (featureFlag.ExperimentIds == null || featureFlag.ExperimentIds.Count == 0) { - Logger.Log(LogLevel.INFO, $"The feature flag \"{featureFlag.Key}\" is not used in any experiments."); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"The feature flag \"{featureFlag.Key}\" is not used in any experiments.")); return null; } @@ -471,16 +537,16 @@ public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag feat if (string.IsNullOrEmpty(experiment.Key)) continue; - var variation = GetVariation(experiment, userId, config, filteredAttributes); + var variation = GetVariation(experiment, userId, config, filteredAttributes, options, reasons); if (variation != null && !string.IsNullOrEmpty(variation.Id)) { - Logger.Log(LogLevel.INFO, $"The user \"{userId}\" is bucketed into experiment \"{experiment.Key}\" of feature \"{featureFlag.Key}\"."); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is bucketed into experiment \"{experiment.Key}\" of feature \"{featureFlag.Key}\".")); return new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST); } } - Logger.Log(LogLevel.INFO, $"The user \"{userId}\" is not bucketed into any of the experiments on the feature \"{featureFlag.Key}\"."); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is not bucketed into any of the experiments on the feature \"{featureFlag.Key}\".")); return null; } @@ -493,23 +559,44 @@ public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag feat /// null if the user is not bucketed into any variation or the FeatureDecision entity if the user is /// successfully bucketed. public virtual FeatureDecision GetVariationForFeature(FeatureFlag featureFlag, string userId, ProjectConfig config, UserAttributes filteredAttributes) + { + return GetVariationForFeature(featureFlag, userId, config, filteredAttributes, new List(), DefaultDecisionReasons.NewInstance()); + } + + /// + /// Get the variation the user is bucketed into for the FeatureFlag + /// + /// The feature flag the user wants to access. + /// User Identifier + /// The user's attributes. This should be filtered to just attributes in the Datafile. + /// The user's attributes. This should be filtered to just attributes in the Datafile. + /// An array of decision options. + /// Decision log messages. + /// null if the user is not bucketed into any variation or the FeatureDecision entity if the user is + /// successfully bucketed. + public virtual FeatureDecision GetVariationForFeature(FeatureFlag featureFlag, + string userId, + ProjectConfig config, + UserAttributes filteredAttributes, + List options, + IDecisionReasons reasons) { // Check if the feature flag has an experiment and the user is bucketed into that experiment. - var decision = GetVariationForFeatureExperiment(featureFlag, userId, filteredAttributes, config); + var decision = GetVariationForFeatureExperiment(featureFlag, userId, filteredAttributes, config, options, reasons); if (decision != null) return decision; // Check if the feature flag has rollout and the the user is bucketed into one of its rules. - decision = GetVariationForFeatureRollout(featureFlag, userId, filteredAttributes, config); + decision = GetVariationForFeatureRollout(featureFlag, userId, filteredAttributes, config, reasons); if (decision != null) { - Logger.Log(LogLevel.INFO, $"The user \"{userId}\" is bucketed into a rollout for feature flag \"{featureFlag.Key}\"."); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is bucketed into a rollout for feature flag \"{featureFlag.Key}\".")); return decision; } - Logger.Log(LogLevel.INFO, $"The user \"{userId}\" is not bucketed into a rollout for feature flag \"{featureFlag.Key}\"."); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is not bucketed into a rollout for feature flag \"{featureFlag.Key}\".")); return new FeatureDecision(null, null, FeatureDecision.DECISION_SOURCE_ROLLOUT); } @@ -519,7 +606,7 @@ public virtual FeatureDecision GetVariationForFeature(FeatureFlag featureFlag, s /// User Identifier /// The user's attributes. /// Bucketing Id if it is a string type in attributes, user Id otherwise. - private string GetBucketingId(string userId, UserAttributes filteredAttributes) + private string GetBucketingId(string userId, UserAttributes filteredAttributes, IDecisionReasons reasons) { string bucketingId = userId; @@ -533,7 +620,7 @@ private string GetBucketingId(string userId, UserAttributes filteredAttributes) } else { - Logger.Log(LogLevel.WARN, "BucketingID attribute is not a string. Defaulted to userId"); + Logger.Log(LogLevel.WARN, reasons.AddInfo("BucketingID attribute is not a string. Defaulted to userId")); } } diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs index b30d00ac..e64b38fe 100644 --- a/OptimizelySDK/Optimizely.cs +++ b/OptimizelySDK/Optimizely.cs @@ -476,45 +476,45 @@ public virtual bool IsFeatureEnabled(string featureKey, string userId, UserAttri return false; bool featureEnabled = false; - var sourceInfo = new Dictionary(); - var decision = DecisionService.GetVariationForFeature(featureFlag, userId, config, userAttributes); - var variation = decision.Variation; - var decisionSource = decision?.Source ?? FeatureDecision.DECISION_SOURCE_ROLLOUT; - - SendImpressionEvent(decision.Experiment, variation, userId, userAttributes, config, featureKey, decisionSource); + var sourceInfo = new Dictionary(); + var decision = DecisionService.GetVariationForFeature(featureFlag, userId, config, userAttributes); + var variation = decision.Variation; + var decisionSource = decision?.Source ?? FeatureDecision.DECISION_SOURCE_ROLLOUT; - if (variation != null) - { - featureEnabled = variation.FeatureEnabled.GetValueOrDefault(); + SendImpressionEvent(decision.Experiment, variation, userId, userAttributes, config, featureKey, decisionSource); - // This information is only necessary for feature tests. - // For rollouts experiments and variations are an implementation detail only. - if (decision.Source == FeatureDecision.DECISION_SOURCE_FEATURE_TEST) - { - sourceInfo["experimentKey"] = decision.Experiment.Key; - sourceInfo["variationKey"] = variation.Key; - } - else + if (variation != null) { - Logger.Log(LogLevel.INFO, $@"The user ""{userId}"" is not being experimented on feature ""{featureKey}""."); - } - } + featureEnabled = variation.FeatureEnabled.GetValueOrDefault(); - if (featureEnabled == true) - Logger.Log(LogLevel.INFO, $@"Feature flag ""{featureKey}"" is enabled for user ""{userId}""."); - else - Logger.Log(LogLevel.INFO, $@"Feature flag ""{featureKey}"" is not enabled for user ""{userId}""."); + // This information is only necessary for feature tests. + // For rollouts experiments and variations are an implementation detail only. + if (decision.Source == FeatureDecision.DECISION_SOURCE_FEATURE_TEST) + { + sourceInfo["experimentKey"] = decision.Experiment.Key; + sourceInfo["variationKey"] = variation.Key; + } + else + { + Logger.Log(LogLevel.INFO, $@"The user ""{userId}"" is not being experimented on feature ""{featureKey}""."); + } + } - var decisionInfo = new Dictionary - { - { "featureKey", featureKey }, - { "featureEnabled", featureEnabled }, - { "source", decision.Source }, - { "sourceInfo", sourceInfo }, - }; + if (featureEnabled == true) + Logger.Log(LogLevel.INFO, $@"Feature flag ""{featureKey}"" is enabled for user ""{userId}""."); + else + Logger.Log(LogLevel.INFO, $@"Feature flag ""{featureKey}"" is not enabled for user ""{userId}""."); - NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, DecisionNotificationTypes.FEATURE, userId, - userAttributes ?? new UserAttributes(), decisionInfo); + var decisionInfo = new Dictionary + { + { "featureKey", featureKey }, + { "featureEnabled", featureEnabled }, + { "source", decision.Source }, + { "sourceInfo", sourceInfo }, + }; + + NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, DecisionNotificationTypes.FEATURE, userId, + userAttributes ?? new UserAttributes(), decisionInfo); return featureEnabled; } @@ -712,6 +712,139 @@ public OptimizelyUserContext CreateUserContext(string userId, return new OptimizelyUserContext(this, userId, userAttributes, ErrorHandler, Logger); } + public OptimizelyDecision Decide(OptimizelyUserContext user, + string key, + List options) + { + + var config = ProjectConfigManager?.GetConfig(); + if (config == null) + { + return OptimizelyDecision.NewErrorDecision(key, user, DecisionMessage.SDK_NOT_READY, ErrorHandler, Logger); + } + var userId = user.UserId; + var inputValues = new Dictionary + { + { USER_ID, userId }, + }; + + if (!ValidateStringInputs(inputValues)) + return null; + + var flag = config.GetFeatureFlagFromKey(key); + if (flag == null) + { + return OptimizelyDecision.NewErrorDecision(key, + user, + DecisionMessage.Reason(DecisionMessage.FLAG_KEY_INVALID, key), + ErrorHandler, Logger); + } + + var userAttributes = user.UserAttributes; + var decisionEventDispatched = false; + var allOptions = GetAllOptions(options); + var decisionReasons = DefaultDecisionReasons.NewInstance(allOptions); + + var flagDecision = DecisionService.GetVariationForFeature( + flag, + userId, + config, + userAttributes, + allOptions, + decisionReasons); + + var featureEnabled = false; + + var variation = flagDecision.Variation; + + if (variation != null) + { + featureEnabled = variation.FeatureEnabled.GetValueOrDefault(); + } + + if (featureEnabled) + { + Logger.Log(LogLevel.INFO, "Feature \"" + key + "\" is enabled for user \"" + userId + "\""); + } + else + { + Logger.Log(LogLevel.INFO, "Feature \"" + key + "\" is not enabled for user \"" + userId + "\""); + } + var variableMap = new Dictionary(); + if (flag?.Variables != null && !allOptions.Contains(OptimizelyDecideOption.EXCLUDE_VARIABLES)) + { + + foreach (var featureVariable in flag?.Variables) + { + string variableValue = featureVariable.DefaultValue; + if (featureEnabled) + { + var featureVariableUsageInstance = variation.GetFeatureVariableUsageFromId(featureVariable.Id); + if (featureVariableUsageInstance != null) + { + variableValue = featureVariableUsageInstance.Value; + } + } + + var typeCastedValue = GetTypeCastedVariableValue(variableValue, featureVariable.Type); + + if (typeCastedValue is OptimizelyJSON) + typeCastedValue = ((OptimizelyJSON)typeCastedValue).ToDictionary(); + + variableMap.Add(featureVariable.Key, typeCastedValue); + } + } + + var optimizelyJSON = new OptimizelyJSON(variableMap, ErrorHandler, Logger); + + var decisionSource = flagDecision?.Source ?? FeatureDecision.DECISION_SOURCE_ROLLOUT; + if (!allOptions.Contains(OptimizelyDecideOption.DISABLE_DECISION_EVENT)) + { + + SendImpressionEvent(flagDecision.Experiment, variation, userId, userAttributes, config, key, decisionSource); + decisionEventDispatched = true; + } + var reasonsToReport = decisionReasons.ToReport(); + var variationKey = flagDecision.Variation?.Key; + + // TODO: add ruleKey values when available later. use a copy of experimentKey until then. + var ruleKey = flagDecision.Experiment?.Key; + + var decisionInfo = new Dictionary + { + { "flagKey", key }, + { "enabled", featureEnabled }, + { "variables", variableMap }, + { "variationKey", variationKey }, + { "ruleKey", ruleKey }, + { "reasons", decisionReasons }, + { "decisionEventDispatched", decisionEventDispatched }, + { "featureEnabled", featureEnabled }, + }; + + NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, DecisionNotificationTypes.FEATURE, userId, + userAttributes ?? new UserAttributes(), decisionInfo); + + return new OptimizelyDecision( + variationKey, + featureEnabled, + optimizelyJSON, + ruleKey, + key, + user, + reasonsToReport); + } + + private List GetAllOptions(List options) + { + var copiedOptions = new List(DefaultDecideOptions); + if (options != null) + { + copiedOptions.AddRange(options); + } + return copiedOptions; + } + /// /// Sends impression event. /// diff --git a/OptimizelySDK/OptimizelyUserContext.cs b/OptimizelySDK/OptimizelyUserContext.cs index e2621f20..07834546 100644 --- a/OptimizelySDK/OptimizelyUserContext.cs +++ b/OptimizelySDK/OptimizelyUserContext.cs @@ -46,7 +46,7 @@ public OptimizelyUserContext(Optimizely optimizely, string userId, UserAttribute /// value An attribute value public void SetAttribute(string key, object value) { - UserAttributes.Add(key, value); + UserAttributes[key] = value; } /// @@ -74,7 +74,7 @@ public OptimizelyDecision Decide(string key) public OptimizelyDecision Decide(string key, List options) { - return null; + return Optimizely.Decide(this, key, options); } /// diff --git a/OptimizelySDK/Utils/ExperimentUtils.cs b/OptimizelySDK/Utils/ExperimentUtils.cs index 470b014c..b6023480 100644 --- a/OptimizelySDK/Utils/ExperimentUtils.cs +++ b/OptimizelySDK/Utils/ExperimentUtils.cs @@ -17,6 +17,7 @@ using OptimizelySDK.AudienceConditions; using OptimizelySDK.Entity; using OptimizelySDK.Logger; +using OptimizelySDK.OptimizelyDecisions; namespace OptimizelySDK.Utils { @@ -35,6 +36,24 @@ public static bool IsExperimentActive(Experiment experiment, ILogger logger) return true; } + /// + /// Check if the user meets audience conditions to be in experiment or not + /// + /// ProjectConfig Configuration for the project + /// Experiment Entity representing the experiment + /// Attributes of the user. Defaults to empty attributes array if not provided + /// It can be either experiment or rule. + /// In case loggingKeyType is experiment it will be experiment key or else it will be rule number. + /// true if the user meets audience conditions to be in experiment, false otherwise. + public static bool DoesUserMeetAudienceConditions(ProjectConfig config, + Experiment experiment, + UserAttributes userAttributes, + string loggingKeyType, + string loggingKey, + ILogger logger) + { + return DoesUserMeetAudienceConditions(config, experiment, userAttributes, loggingKeyType, loggingKey, DefaultDecisionReasons.NewInstance(), logger); + } /// /// Check if the user meets audience conditions to be in experiment or not @@ -44,12 +63,14 @@ public static bool IsExperimentActive(Experiment experiment, ILogger logger) /// Attributes of the user. Defaults to empty attributes array if not provided /// It can be either experiment or rule. /// In case loggingKeyType is experiment it will be experiment key or else it will be rule number. + /// Decision log messages. /// true if the user meets audience conditions to be in experiment, false otherwise. public static bool DoesUserMeetAudienceConditions(ProjectConfig config, Experiment experiment, UserAttributes userAttributes, string loggingKeyType, string loggingKey, + IDecisionReasons reasons, ILogger logger) { if (userAttributes == null) @@ -73,7 +94,7 @@ public static bool DoesUserMeetAudienceConditions(ProjectConfig config, var result = expConditions.Evaluate(config, userAttributes, logger).GetValueOrDefault(); var resultText = result.ToString().ToUpper(); - logger.Log(LogLevel.INFO, $@"Audiences for {loggingKeyType} ""{loggingKey}"" collectively evaluated to {resultText}"); + logger.Log(LogLevel.INFO, reasons.AddInfo($@"Audiences for {loggingKeyType} ""{loggingKey}"" collectively evaluated to {resultText}")); return result; } } From ee465c37d367a344acdf48ecb1f4d43fba3178bf Mon Sep 17 00:00:00 2001 From: muhammadnoman Date: Thu, 12 Nov 2020 18:42:47 +0500 Subject: [PATCH 10/34] Added TrackEvent, DecideForKeys and DecideAllFunction Added Unit tests of above functions --- .../OptimizelyUserContextTest.cs | 225 ++++++++++++++++++ OptimizelySDK/Optimizely.cs | 54 +++++ OptimizelySDK/OptimizelyUserContext.cs | 17 +- 3 files changed, 292 insertions(+), 4 deletions(-) diff --git a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs index 029c8499..7c0f6a69 100644 --- a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs +++ b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs @@ -197,5 +197,230 @@ public void DecideInvalidConfig() } #endregion + #region decideAll + + [Test] + public void DecideAllOneFlag() + { + var flagKey = "multi_variate_feature"; + var flagKeys = new List() { flagKey }; + + var variablesExpected = Optimizely.GetAllFeatureVariables(flagKey, UserID); + + var user = Optimizely.CreateUserContext(UserID); + user.SetAttribute("browser_type", "chrome"); + + var decisions = user.DecideForKeys(flagKeys); + + Assert.True(decisions.Count == 1); + var decision = decisions[flagKey]; + + OptimizelyDecision expDecision = new OptimizelyDecision( + "Gred", + false, + variablesExpected, + "test_experiment_multivariate", + flagKey, + user, + new List()); + Assert.IsTrue(TestData.CompareObjects(decision, expDecision)); + } + + [Test] + public void DecideAllTwoFlag() + { + var flagKey1 = "multi_variate_feature"; + var flagKey2 = "string_single_variable_feature"; + var flagKeys = new List() { flagKey1, flagKey2 }; + + var variablesExpected1 = Optimizely.GetAllFeatureVariables(flagKey1, UserID); + var variablesExpected2 = Optimizely.GetAllFeatureVariables(flagKey2, UserID); + + var user = Optimizely.CreateUserContext(UserID); + user.SetAttribute("browser_type", "chrome"); + + var decisions = user.DecideForKeys(flagKeys); + + Assert.True(decisions.Count == 2); + + OptimizelyDecision expDecision1 = new OptimizelyDecision( + "Gred", + false, + variablesExpected1, + "test_experiment_multivariate", + flagKey1, + user, + new List()); + Assert.IsTrue(TestData.CompareObjects(decisions[flagKey1], expDecision1)); + + OptimizelyDecision expDecision2 = new OptimizelyDecision( + "control", + true, + variablesExpected2, + "test_experiment_with_feature_rollout", + flagKey2, + user, + new List()); + Assert.IsTrue(TestData.CompareObjects(decisions[flagKey2], expDecision2)); + } + + [Test] + public void DecideAllAllFlags() + { + var flagKey1 = "boolean_feature"; + var flagKey2 = "double_single_variable_feature"; + var flagKey3 = "integer_single_variable_feature"; + var flagKey4 = "boolean_single_variable_feature"; + var flagKey5 = "string_single_variable_feature"; + var flagKey6 = "multi_variate_feature"; + var flagKey7 = "mutex_group_feature"; + var flagKey8 = "empty_feature"; + var flagKey9 = "no_rollout_experiment_feature"; + var flagKey10 = "unsupported_variabletype"; + + var variablesExpected1 = Optimizely.GetAllFeatureVariables(flagKey1, UserID); + var variablesExpected2 = Optimizely.GetAllFeatureVariables(flagKey2, UserID); + var variablesExpected3 = Optimizely.GetAllFeatureVariables(flagKey3, UserID); + var variablesExpected4 = Optimizely.GetAllFeatureVariables(flagKey4, UserID); + var variablesExpected5 = Optimizely.GetAllFeatureVariables(flagKey5, UserID); + var variablesExpected6 = Optimizely.GetAllFeatureVariables(flagKey6, UserID); + var variablesExpected7 = Optimizely.GetAllFeatureVariables(flagKey7, UserID); + var variablesExpected8 = Optimizely.GetAllFeatureVariables(flagKey8, UserID); + var variablesExpected9 = Optimizely.GetAllFeatureVariables(flagKey9, UserID); + var variablesExpected10 = Optimizely.GetAllFeatureVariables(flagKey10, UserID); + + var user = Optimizely.CreateUserContext(UserID); + user.SetAttribute("browser_type", "chrome"); + + var decisions = user.DecideAll(); + + Assert.True(decisions.Count == 10); + OptimizelyDecision expDecision1 = new OptimizelyDecision( + null, + false, + variablesExpected1, + null, + flagKey1, + user, + new List()); + Assert.IsTrue(TestData.CompareObjects(decisions[flagKey1], expDecision1)); + + OptimizelyDecision expDecision2 = new OptimizelyDecision( + "variation", + false, + variablesExpected2, + "test_experiment_double_feature", + flagKey2, + user, + new List()); + Assert.IsTrue(TestData.CompareObjects(decisions[flagKey2], expDecision2)); + + OptimizelyDecision expDecision3 = new OptimizelyDecision( + "control", + false, + variablesExpected3, + "test_experiment_integer_feature", + flagKey3, + user, + new List()); + Assert.IsTrue(TestData.CompareObjects(decisions[flagKey3], expDecision3)); + + OptimizelyDecision expDecision4 = new OptimizelyDecision( + "188881", + false, + variablesExpected4, + "188880", + flagKey4, + user, + new List()); + Assert.IsTrue(TestData.CompareObjects(decisions[flagKey4], expDecision4)); + + OptimizelyDecision expDecision5 = new OptimizelyDecision( + "control", + true, + variablesExpected5, + "test_experiment_with_feature_rollout", + flagKey5, + user, + new List()); + Assert.IsTrue(TestData.CompareObjects(decisions[flagKey5], expDecision5)); + + OptimizelyDecision expDecision6 = new OptimizelyDecision( + "Gred", + false, + variablesExpected6, + "test_experiment_multivariate", + flagKey6, + user, + new List()); + Assert.IsTrue(TestData.CompareObjects(decisions[flagKey6], expDecision6)); + + OptimizelyDecision expDecision7 = new OptimizelyDecision( + null, + false, + variablesExpected7, + null, + flagKey7, + user, + new List()); + Assert.IsTrue(TestData.CompareObjects(decisions[flagKey7], expDecision7)); + + OptimizelyDecision expDecision8 = new OptimizelyDecision( + null, + false, + variablesExpected8, + null, + flagKey8, + user, + new List()); + Assert.IsTrue(TestData.CompareObjects(decisions[flagKey8], expDecision8)); + + OptimizelyDecision expDecision9 = new OptimizelyDecision( + null, + false, + variablesExpected9, + null, + flagKey9, + user, + new List()); + Assert.IsTrue(TestData.CompareObjects(decisions[flagKey9], expDecision9)); + + OptimizelyDecision expDecision10 = new OptimizelyDecision( + null, + false, + variablesExpected10, + null, + flagKey10, + user, + new List()); + Assert.IsTrue(TestData.CompareObjects(decisions[flagKey10], expDecision10)); + } + + [Test] + public void DecideAllEnabledFlagsOnlyDecideOptions() + { + var flagKey1 = "string_single_variable_feature"; + + var variablesExpected1 = Optimizely.GetAllFeatureVariables(flagKey1, UserID); + + var decideOptions = new List() { OptimizelyDecideOption.ENABLED_FLAGS_ONLY }; + var user = Optimizely.CreateUserContext(UserID); + user.SetAttribute("browser_type", "chrome"); + + var decisions = user.DecideAll(decideOptions); + + Assert.True(decisions.Count == 1); + + OptimizelyDecision expDecision1 = new OptimizelyDecision( + "control", + true, + variablesExpected1, + "test_experiment_with_feature_rollout", + flagKey1, + user, + new List()); + Assert.IsTrue(TestData.CompareObjects(decisions[flagKey1], expDecision1)); + } + #endregion } } diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs index e64b38fe..da9591c6 100644 --- a/OptimizelySDK/Optimizely.cs +++ b/OptimizelySDK/Optimizely.cs @@ -835,6 +835,60 @@ public OptimizelyDecision Decide(OptimizelyUserContext user, reasonsToReport); } + public Dictionary DecideAll(OptimizelyUserContext user, + List options) + { + var decisionMap = new Dictionary(); + + var projectConfig = ProjectConfigManager?.GetConfig(); + if (projectConfig == null) + { + Logger.Log(LogLevel.ERROR, "Optimizely instance is not valid, failing isFeatureEnabled call."); + return decisionMap; + } + + var allFlags = projectConfig.FeatureFlags; + var allFlagKeys = new List(); + for (int i = 0; i < allFlags.Length; i++) + { + allFlagKeys.Add(allFlags[i].Key); + } + return DecideForKeys(user, allFlagKeys, options); + } + + + public Dictionary DecideForKeys(OptimizelyUserContext user, + List keys, + List options) + { + var decisionDictionary = new Dictionary(); + + var projectConfig = ProjectConfigManager?.GetConfig(); + if (projectConfig == null) + { + Logger.Log(LogLevel.ERROR, "Optimizely instance is not valid, failing isFeatureEnabled call."); + return decisionDictionary; + } + + if (keys.Count == 0) + { + return decisionDictionary; + } + + var allOptions = GetAllOptions(options); + + foreach (string key in keys) + { + var decision = Decide(user, key, options); + if (!allOptions.Contains(OptimizelyDecideOption.ENABLED_FLAGS_ONLY) || decision.Enabled) + { + decisionDictionary.Add(key, decision); + } + } + + return decisionDictionary; + } + private List GetAllOptions(List options) { var copiedOptions = new List(DefaultDecideOptions); diff --git a/OptimizelySDK/OptimizelyUserContext.cs b/OptimizelySDK/OptimizelyUserContext.cs index 07834546..87c40adf 100644 --- a/OptimizelySDK/OptimizelyUserContext.cs +++ b/OptimizelySDK/OptimizelyUserContext.cs @@ -76,7 +76,17 @@ public OptimizelyDecision Decide(string key, { return Optimizely.Decide(this, key, options); } - + + /// + /// Returns a key-map of decision results for multiple flag keys and a user context. + /// + /// list of flag keys for which a decision will be made. + /// A dictionary of all decision results, mapped by flag keys. + public Dictionary DecideForKeys(List keys, List options) + { + return Optimizely.DecideForKeys(this, keys, options); + } + /// /// Returns a key-map of decision results for multiple flag keys and a user context. /// @@ -84,7 +94,7 @@ public OptimizelyDecision Decide(string key, /// A dictionary of all decision results, mapped by flag keys. public Dictionary DecideForKeys(List keys) { - return null; + return Optimizely.DecideForKeys(this, keys, new List()); } /// @@ -96,7 +106,6 @@ public Dictionary DecideAll() return DecideAll(new List()); } - /// /// Returns a key-map of decision results ({@link OptimizelyDecision}) for all active flag keys. /// @@ -104,7 +113,7 @@ public Dictionary DecideAll() /// All decision results mapped by flag keys. public Dictionary DecideAll(List options) { - return null; + return Optimizely.DecideAll(this, options); } /// From 591c770e14b8a336972fbce1ec276b4c938793cf Mon Sep 17 00:00:00 2001 From: muhammadnoman Date: Fri, 13 Nov 2020 20:41:31 +0500 Subject: [PATCH 11/34] Replaced list of reasons to array. Throwing not implemented exceptions for not implemented functions in optimizelyUserContext Added detailed documentation of optimizelyUserContext and Decision class --- .../OptimizelyDecisionTest.cs | 7 +- .../OptimizelyUserContextTest.cs | 108 ++++++++++++++++++ OptimizelySDK/Optimizely.cs | 12 +- .../DefaultDecisionReasons.cs | 1 - .../ErrorsDecisionReasons.cs | 3 + .../OptimizelyDecisions/OptimizelyDecision.cs | 22 +++- OptimizelySDK/OptimizelyUserContext.cs | 22 +++- 7 files changed, 157 insertions(+), 18 deletions(-) create mode 100644 OptimizelySDK.Tests/OptimizelyUserContextTest.cs diff --git a/OptimizelySDK.Tests/OptimizelyDecisions/OptimizelyDecisionTest.cs b/OptimizelySDK.Tests/OptimizelyDecisions/OptimizelyDecisionTest.cs index b430e4ea..f975e462 100644 --- a/OptimizelySDK.Tests/OptimizelyDecisions/OptimizelyDecisionTest.cs +++ b/OptimizelySDK.Tests/OptimizelyDecisions/OptimizelyDecisionTest.cs @@ -55,7 +55,7 @@ public void TestNewErrorDecision() [Test] public void TestNewDecision() { - var map = new Dictionary() { + var variableMap = new Dictionary() { { "strField", "john doe" }, { "intField", 12 }, { "objectField", new Dictionary () { @@ -63,7 +63,7 @@ public void TestNewDecision() } } }; - var optimizelyJSONUsingMap = new OptimizelyJSON(map, ErrorHandlerMock.Object, LoggerMock.Object); + var optimizelyJSONUsingMap = new OptimizelyJSON(variableMap, ErrorHandlerMock.Object, LoggerMock.Object); string expectedStringObj = "{\"strField\":\"john doe\",\"intField\":12,\"objectField\":{\"inner_field_int\":3}}"; var optimizelyDecision = new OptimizelyDecision("var_key", @@ -72,7 +72,7 @@ public void TestNewDecision() "experiment", "feature_key", null, - new List()); + new string[0]); Assert.AreEqual(optimizelyDecision.VariationKey, "var_key"); Assert.AreEqual(optimizelyDecision.FlagKey, "feature_key"); Assert.AreEqual(optimizelyDecision.Variables.ToString(), expectedStringObj); @@ -86,6 +86,7 @@ public void TestNewDecisionReasonWithDecideAllOptions() { var decisionReasons = DefaultDecisionReasons.NewInstance(new List() { OptimizelyDecideOption.INCLUDE_REASONS }); decisionReasons.AddError(DecisionMessage.Reason(DecisionMessage.FLAG_KEY_INVALID, "invalid_key")); + Assert.AreEqual(decisionReasons.ToReport()[0], "No flag was found for key \"invalid_key\"."); decisionReasons.AddError(DecisionMessage.Reason(DecisionMessage.VARIABLE_VALUE_INVALID, "invalid_key")); Assert.AreEqual(decisionReasons.ToReport()[1], "Variable value for key \"invalid_key\" is invalid or wrong type."); diff --git a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs new file mode 100644 index 00000000..7a0d67bf --- /dev/null +++ b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs @@ -0,0 +1,108 @@ +/** + * + * Copyright 2020, Optimizely 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. + */ + +using Moq; +using NUnit.Framework; +using OptimizelySDK.Entity; +using OptimizelySDK.ErrorHandler; +using OptimizelySDK.Event.Dispatcher; +using OptimizelySDK.Logger; +using System; + +namespace OptimizelySDK.Tests +{ + [TestFixture] + public class OptimizelyUserContextTest + { + string UserID = "testUserID"; + private Optimizely Optimizely; + private Mock LoggerMock; + private Mock ErrorHandlerMock; + private Mock EventDispatcherMock; + + [SetUp] + public void SetUp() + { + LoggerMock = new Mock(); + LoggerMock.Setup(i => i.Log(It.IsAny(), It.IsAny())); + + ErrorHandlerMock = new Mock(); + ErrorHandlerMock.Setup(e => e.HandleError(It.IsAny())); + EventDispatcherMock = new Mock(); + + Optimizely = new Optimizely(TestData.Datafile, EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object); + } + + [Test] + public void OptimizelyUserContext_withAttributes() + { + var attributes = new UserAttributes() { { "house", "GRYFFINDOR" } }; + OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object); + + Assert.AreEqual(user.Optimizely, Optimizely); + Assert.AreEqual(user.UserId, UserID); + Assert.AreEqual(user.UserAttributes, attributes); + } + + [Test] + public void OptimizelyUserContext_noAttributes() + { + OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, null, ErrorHandlerMock.Object, LoggerMock.Object); + + Assert.AreEqual(user.Optimizely, Optimizely); + Assert.AreEqual(user.UserId, UserID); + Assert.True(user.UserAttributes.Count == 0); + } + + [Test] + public void SetAttribute() + { + var attributes = new UserAttributes() { { "house", "GRYFFINDOR" } }; + OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object); + + user.SetAttribute("k1", "v1"); + user.SetAttribute("k2", true); + user.SetAttribute("k3", 100); + user.SetAttribute("k4", 3.5); + + Assert.AreEqual(user.Optimizely, Optimizely); + Assert.AreEqual(user.UserId, UserID); + var newAttributes = user.UserAttributes; + Assert.AreEqual(newAttributes["house"], "GRYFFINDOR"); + Assert.AreEqual(newAttributes["k1"], "v1"); + Assert.AreEqual(newAttributes["k2"], true); + Assert.AreEqual(newAttributes["k3"], 100); + Assert.AreEqual(newAttributes["k4"], 3.5); + } + + [Test] + public void SetAttribute_noAttribute() + { + OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, null, ErrorHandlerMock.Object, LoggerMock.Object); + + user.SetAttribute("k1", "v1"); + user.SetAttribute("k2", true); + + Assert.AreEqual(user.Optimizely, Optimizely); + Assert.AreEqual(user.UserId, UserID); + var newAttributes = user.UserAttributes; + Assert.AreEqual(newAttributes["k1"], "v1"); + Assert.AreEqual(newAttributes["k2"], true); + } + + } +} diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs index b30d00ac..9a05ebf3 100644 --- a/OptimizelySDK/Optimizely.cs +++ b/OptimizelySDK/Optimizely.cs @@ -60,7 +60,7 @@ public class Optimizely : IOptimizely, IDisposable private EventProcessor EventProcessor; - private List DefaultDecideOptions; + private OptimizelyDecideOption[] DefaultDecideOptions; /// /// It returns true if the ProjectConfig is valid otherwise false. @@ -162,7 +162,7 @@ public Optimizely(ProjectConfigManager configManager, IErrorHandler errorHandler = null, UserProfileService userProfileService = null, EventProcessor eventProcessor = null, - List defaultDecideOptions = null) + OptimizelyDecideOption[] defaultDecideOptions = null) { ProjectConfigManager = configManager; @@ -174,8 +174,8 @@ private void InitializeComponents(IEventDispatcher eventDispatcher = null, IErrorHandler errorHandler = null, UserProfileService userProfileService = null, NotificationCenter notificationCenter = null, - EventProcessor eventProcessor = null, - List defaultDecideOptions = null) + EventProcessor eventProcessor = null, + OptimizelyDecideOption[] defaultDecideOptions = null) { Logger = logger ?? new NoOpLogger(); EventDispatcher = eventDispatcher ?? new DefaultEventDispatcher(Logger); @@ -186,7 +186,7 @@ private void InitializeComponents(IEventDispatcher eventDispatcher = null, NotificationCenter = notificationCenter ?? new NotificationCenter(Logger); DecisionService = new DecisionService(Bucketer, ErrorHandler, userProfileService, Logger); EventProcessor = eventProcessor ?? new ForwardingEventProcessor(EventDispatcher, NotificationCenter, Logger); - DefaultDecideOptions = defaultDecideOptions ?? new List(); + DefaultDecideOptions = defaultDecideOptions ?? new OptimizelyDecideOption[0]; } /// @@ -688,8 +688,6 @@ public OptimizelyJSON GetFeatureVariableJSON(string featureKey, string variableK return GetFeatureVariableValueForType(featureKey, variableKey, userId, userAttributes, FeatureVariable.JSON_TYPE); } - //============ decide ============// - /// /// Create a context of the user for which decision APIs will be called. /// A user context will be created successfully even when the SDK is not fully configured yet. diff --git a/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs b/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs index fe1a38a8..95627330 100644 --- a/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs +++ b/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs @@ -15,7 +15,6 @@ */ using System.Collections.Generic; -using System.Linq; namespace OptimizelySDK.OptimizelyDecisions { diff --git a/OptimizelySDK/OptimizelyDecisions/ErrorsDecisionReasons.cs b/OptimizelySDK/OptimizelyDecisions/ErrorsDecisionReasons.cs index 890416ea..56e98d7e 100644 --- a/OptimizelySDK/OptimizelyDecisions/ErrorsDecisionReasons.cs +++ b/OptimizelySDK/OptimizelyDecisions/ErrorsDecisionReasons.cs @@ -18,6 +18,9 @@ namespace OptimizelySDK.OptimizelyDecisions { + /// + /// NewErrorDecision returns a decision with errors only + /// public class ErrorsDecisionReasons : IDecisionReasons { private readonly List errors = new List(); diff --git a/OptimizelySDK/OptimizelyDecisions/OptimizelyDecision.cs b/OptimizelySDK/OptimizelyDecisions/OptimizelyDecision.cs index 50334cf2..a4a934d3 100644 --- a/OptimizelySDK/OptimizelyDecisions/OptimizelyDecision.cs +++ b/OptimizelySDK/OptimizelyDecisions/OptimizelyDecision.cs @@ -20,15 +20,25 @@ namespace OptimizelySDK.OptimizelyDecisions { + /// + /// OptimizelyDecision defines the decision returned by decide api. + /// public class OptimizelyDecision { + // variation key for optimizely decision. public string VariationKey { get; private set; } + // boolean value indicating if the flag is enabled or not. public bool Enabled { get; private set; } + // collection of variables associated with the decision. public OptimizelyJSON Variables { get; private set; } + // rule key of the decision. public string RuleKey { get; private set; } + // flag key for which the decision was made. public string FlagKey { get; private set; } + // user context for which the decision was made. public OptimizelyUserContext UserContext { get; private set; } - public List Reasons { get; private set; } + // an array of error/info/debug messages describing why the decision has been made. + public string[] Reasons { get; private set; } public OptimizelyDecision(string variationKey, bool enabled, @@ -36,7 +46,7 @@ public OptimizelyDecision(string variationKey, string ruleKey, string flagKey, OptimizelyUserContext userContext, - List reasons) + string[] reasons) { VariationKey = variationKey; Enabled = enabled; @@ -47,6 +57,12 @@ public OptimizelyDecision(string variationKey, Reasons = reasons; } + /// + /// Static function to return OptimizelyDecision + /// when there are errors for example like OptimizelyConfig is not valid, etc. + /// OptimizelyDecision will have null variation key, false enabled, empty variables, null rule key + /// and error reason array + /// public static OptimizelyDecision NewErrorDecision(string key, OptimizelyUserContext optimizelyUserContext, string error, @@ -60,7 +76,7 @@ public static OptimizelyDecision NewErrorDecision(string key, null, key, optimizelyUserContext, - new List() { error }); + new string[] { error }); } } } diff --git a/OptimizelySDK/OptimizelyUserContext.cs b/OptimizelySDK/OptimizelyUserContext.cs index e2621f20..c0b49df7 100644 --- a/OptimizelySDK/OptimizelyUserContext.cs +++ b/OptimizelySDK/OptimizelyUserContext.cs @@ -19,15 +19,22 @@ using OptimizelySDK.ErrorHandler; using OptimizelySDK.Entity; using OptimizelySDK.OptimizelyDecisions; +using System; namespace OptimizelySDK { + /// + /// OptimizelyUserContext defines user contexts that the SDK will use to make decisions for + /// public class OptimizelyUserContext { private ILogger Logger; private IErrorHandler ErrorHandler; + // userID for Optimizely user context public string UserId { get; } + // user attributes for Optimizely user context. public UserAttributes UserAttributes { get; } + // Optimizely object to be used. public Optimizely Optimizely { get; } public OptimizelyUserContext(Optimizely optimizely, string userId, UserAttributes userAttributes, IErrorHandler errorHandler, ILogger logger) @@ -46,7 +53,14 @@ public OptimizelyUserContext(Optimizely optimizely, string userId, UserAttribute /// value An attribute value public void SetAttribute(string key, object value) { - UserAttributes.Add(key, value); + if (key == null) + { + Logger.Log(LogLevel.WARN, "Null attribute key."); + } + else + { + UserAttributes.Add(key, value); + } } /// @@ -74,7 +88,7 @@ public OptimizelyDecision Decide(string key) public OptimizelyDecision Decide(string key, List options) { - return null; + throw new NotImplementedException(); } /// @@ -84,7 +98,7 @@ public OptimizelyDecision Decide(string key, /// A dictionary of all decision results, mapped by flag keys. public Dictionary DecideForKeys(List keys) { - return null; + throw new NotImplementedException(); } /// @@ -104,7 +118,7 @@ public Dictionary DecideAll() /// All decision results mapped by flag keys. public Dictionary DecideAll(List options) { - return null; + throw new NotImplementedException(); } /// From c3db0771ff53f555d393f76a6974a041d7813c81 Mon Sep 17 00:00:00 2001 From: muhammadnoman Date: Fri, 13 Nov 2020 21:27:33 +0500 Subject: [PATCH 12/34] Bug fix --- OptimizelySDK.Tests/BucketerTest.cs | 2 +- .../OptimizelyUserContextTest.cs | 32 +++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/OptimizelySDK.Tests/BucketerTest.cs b/OptimizelySDK.Tests/BucketerTest.cs index 78b0feec..1bf4548b 100644 --- a/OptimizelySDK.Tests/BucketerTest.cs +++ b/OptimizelySDK.Tests/BucketerTest.cs @@ -217,4 +217,4 @@ public void TestBucketRolloutRule() bucketer.Bucket(Config, rolloutRule, "testBucketingId", TestUserId, DefaultDecisionReasons.NewInstance()))); } } -} \ No newline at end of file +} diff --git a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs index 7c0f6a69..f3897542 100644 --- a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs +++ b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs @@ -51,7 +51,7 @@ public void SetUp() } [Test] - public void OptimizelyUserContext_withAttributes() + public void OptimizelyUserContextWithAttributes() { var attributes = new UserAttributes() { { "house", "GRYFFINDOR" } }; OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object); @@ -62,7 +62,7 @@ public void OptimizelyUserContext_withAttributes() } [Test] - public void OptimizelyUserContext_noAttributes() + public void OptimizelyUserContextNoAttributes() { OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, null, ErrorHandlerMock.Object, LoggerMock.Object); @@ -222,7 +222,7 @@ public void DecideAllOneFlag() "test_experiment_multivariate", flagKey, user, - new List()); + new string[0]); Assert.IsTrue(TestData.CompareObjects(decision, expDecision)); } @@ -250,7 +250,7 @@ public void DecideAllTwoFlag() "test_experiment_multivariate", flagKey1, user, - new List()); + new string[0]); Assert.IsTrue(TestData.CompareObjects(decisions[flagKey1], expDecision1)); OptimizelyDecision expDecision2 = new OptimizelyDecision( @@ -260,7 +260,7 @@ public void DecideAllTwoFlag() "test_experiment_with_feature_rollout", flagKey2, user, - new List()); + new string[0]); Assert.IsTrue(TestData.CompareObjects(decisions[flagKey2], expDecision2)); } @@ -302,7 +302,7 @@ public void DecideAllAllFlags() null, flagKey1, user, - new List()); + new string[0]); Assert.IsTrue(TestData.CompareObjects(decisions[flagKey1], expDecision1)); OptimizelyDecision expDecision2 = new OptimizelyDecision( @@ -312,7 +312,7 @@ public void DecideAllAllFlags() "test_experiment_double_feature", flagKey2, user, - new List()); + new string[0]); Assert.IsTrue(TestData.CompareObjects(decisions[flagKey2], expDecision2)); OptimizelyDecision expDecision3 = new OptimizelyDecision( @@ -322,7 +322,7 @@ public void DecideAllAllFlags() "test_experiment_integer_feature", flagKey3, user, - new List()); + new string[0]); Assert.IsTrue(TestData.CompareObjects(decisions[flagKey3], expDecision3)); OptimizelyDecision expDecision4 = new OptimizelyDecision( @@ -332,7 +332,7 @@ public void DecideAllAllFlags() "188880", flagKey4, user, - new List()); + new string[0]); Assert.IsTrue(TestData.CompareObjects(decisions[flagKey4], expDecision4)); OptimizelyDecision expDecision5 = new OptimizelyDecision( @@ -342,7 +342,7 @@ public void DecideAllAllFlags() "test_experiment_with_feature_rollout", flagKey5, user, - new List()); + new string[0]); Assert.IsTrue(TestData.CompareObjects(decisions[flagKey5], expDecision5)); OptimizelyDecision expDecision6 = new OptimizelyDecision( @@ -352,7 +352,7 @@ public void DecideAllAllFlags() "test_experiment_multivariate", flagKey6, user, - new List()); + new string[0]); Assert.IsTrue(TestData.CompareObjects(decisions[flagKey6], expDecision6)); OptimizelyDecision expDecision7 = new OptimizelyDecision( @@ -362,7 +362,7 @@ public void DecideAllAllFlags() null, flagKey7, user, - new List()); + new string[0]); Assert.IsTrue(TestData.CompareObjects(decisions[flagKey7], expDecision7)); OptimizelyDecision expDecision8 = new OptimizelyDecision( @@ -372,7 +372,7 @@ public void DecideAllAllFlags() null, flagKey8, user, - new List()); + new string[0]); Assert.IsTrue(TestData.CompareObjects(decisions[flagKey8], expDecision8)); OptimizelyDecision expDecision9 = new OptimizelyDecision( @@ -382,7 +382,7 @@ public void DecideAllAllFlags() null, flagKey9, user, - new List()); + new string[0]); Assert.IsTrue(TestData.CompareObjects(decisions[flagKey9], expDecision9)); OptimizelyDecision expDecision10 = new OptimizelyDecision( @@ -392,7 +392,7 @@ public void DecideAllAllFlags() null, flagKey10, user, - new List()); + new string[0]); Assert.IsTrue(TestData.CompareObjects(decisions[flagKey10], expDecision10)); } @@ -418,7 +418,7 @@ public void DecideAllEnabledFlagsOnlyDecideOptions() "test_experiment_with_feature_rollout", flagKey1, user, - new List()); + new string[0]); Assert.IsTrue(TestData.CompareObjects(decisions[flagKey1], expDecision1)); } #endregion From 055bb93ded6e3fff94078a272664f96513f32d8f Mon Sep 17 00:00:00 2001 From: muhammadnoman Date: Fri, 13 Nov 2020 21:47:52 +0500 Subject: [PATCH 13/34] Resolved comments of adding null check before reasons --- OptimizelySDK.Tests/BucketerTest.cs | 2 +- .../OptimizelyUserContextTest.cs | 29 +++++++------- OptimizelySDK/Bucketing/Bucketer.cs | 10 ++--- OptimizelySDK/Bucketing/DecisionService.cs | 40 +++++++++---------- OptimizelySDK/Optimizely.cs | 20 +++++----- OptimizelySDK/Utils/ExperimentUtils.cs | 2 +- 6 files changed, 53 insertions(+), 50 deletions(-) diff --git a/OptimizelySDK.Tests/BucketerTest.cs b/OptimizelySDK.Tests/BucketerTest.cs index 78b0feec..1bf4548b 100644 --- a/OptimizelySDK.Tests/BucketerTest.cs +++ b/OptimizelySDK.Tests/BucketerTest.cs @@ -217,4 +217,4 @@ public void TestBucketRolloutRule() bucketer.Bucket(Config, rolloutRule, "testBucketingId", TestUserId, DefaultDecisionReasons.NewInstance()))); } } -} \ No newline at end of file +} diff --git a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs index 029c8499..5b1c9aa3 100644 --- a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs +++ b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs @@ -51,7 +51,7 @@ public void SetUp() } [Test] - public void OptimizelyUserContext_withAttributes() + public void OptimizelyUserContextWithAttributes() { var attributes = new UserAttributes() { { "house", "GRYFFINDOR" } }; OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object); @@ -62,7 +62,7 @@ public void OptimizelyUserContext_withAttributes() } [Test] - public void OptimizelyUserContext_noAttributes() + public void OptimizelyUserContextNoAttributes() { OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, null, ErrorHandlerMock.Object, LoggerMock.Object); @@ -93,7 +93,7 @@ public void SetAttribute() } [Test] - public void SetAttribute_noAttribute() + public void SetAttributeNoAttribute() { OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, null, ErrorHandlerMock.Object, LoggerMock.Object); @@ -108,7 +108,7 @@ public void SetAttribute_noAttribute() } [Test] - public void SetAttribute_override() + public void SetAttributeOverride() { var attributes = new UserAttributes() { { "house", "GRYFFINDOR" } }; OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object); @@ -122,7 +122,7 @@ public void SetAttribute_override() } [Test] - public void SetAttribute_nullValue() + public void SetAttributeNullValue() { var attributes = new UserAttributes() { { "k1", null } }; OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object); @@ -142,7 +142,7 @@ public void SetAttribute_nullValue() #region decide [Test] - public void Decide() + public void TestDecide() { var flagKey = "multi_variate_feature"; var variablesExpected = Optimizely.GetAllFeatureVariables(flagKey, UserID); @@ -167,19 +167,20 @@ public void DecideInvalidFlagKey() var user = Optimizely.CreateUserContext(UserID); user.SetAttribute("browser_type", "chrome"); + + var decisionExpected = OptimizelyDecision.NewErrorDecision( + flagKey, + user, + DecisionMessage.Reason(DecisionMessage.FLAG_KEY_INVALID, flagKey), + ErrorHandlerMock.Object, + LoggerMock.Object); var decision = user.Decide(flagKey); - Assert.Null(decision.VariationKey); - Assert.False(decision.Enabled); - Assert.AreEqual(decision.Variables.ToDictionary(), new Dictionary()); - Assert.IsNull(decision.RuleKey); - Assert.AreEqual(decision.FlagKey, flagKey); - Assert.AreEqual(decision.UserContext, user); - Assert.True(decision.Reasons.IsNullOrEmpty()); + Assert.IsTrue(TestData.CompareObjects(decision, decisionExpected)); } [Test] - public void DecideInvalidConfig() + public void DecideWhenConfigIsNull() { Optimizely optimizely = new Optimizely(TestData.UnsupportedVersionDatafile, EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object); diff --git a/OptimizelySDK/Bucketing/Bucketer.cs b/OptimizelySDK/Bucketing/Bucketer.cs index be62727a..1765453d 100644 --- a/OptimizelySDK/Bucketing/Bucketer.cs +++ b/OptimizelySDK/Bucketing/Bucketer.cs @@ -124,26 +124,26 @@ public virtual Variation Bucket(ProjectConfig config, Experiment experiment, str if (string.IsNullOrEmpty(userExperimentId)) { message = $"User [{userId}] is in no experiment."; - Logger.Log(LogLevel.INFO, reasons.AddInfo(message)); + Logger.Log(LogLevel.INFO, reasons?.AddInfo(message)); return new Variation(); } if (userExperimentId != experiment.Id) { message = $"User [{userId}] is not in experiment [{experiment.Key}] of group [{experiment.GroupId}]."; - Logger.Log(LogLevel.INFO, reasons.AddInfo(message)); + Logger.Log(LogLevel.INFO, reasons?.AddInfo(message)); return new Variation(); } message = $"User [{userId}] is in experiment [{experiment.Key}] of group [{experiment.GroupId}]."; - Logger.Log(LogLevel.INFO, reasons.AddInfo(message)); + Logger.Log(LogLevel.INFO, reasons?.AddInfo(message)); } // Bucket user if not in whitelist and in group (if any). string variationId = FindBucket(bucketingId, userId, experiment.Id, experiment.TrafficAllocation); if (string.IsNullOrEmpty(variationId)) { - Logger.Log(LogLevel.INFO, reasons.AddInfo($"User [{userId}] is in no variation.")); + Logger.Log(LogLevel.INFO, reasons?.AddInfo($"User [{userId}] is in no variation.")); return new Variation(); } @@ -151,7 +151,7 @@ public virtual Variation Bucket(ProjectConfig config, Experiment experiment, str // success! variation = config.GetVariationFromId(experiment.Key, variationId); message = $"User [{userId}] is in variation [{variation.Key}] of experiment [{experiment.Key}]."; - Logger.Log(LogLevel.INFO, reasons.AddInfo(message)); + Logger.Log(LogLevel.INFO, reasons?.AddInfo(message)); return variation; } } diff --git a/OptimizelySDK/Bucketing/DecisionService.cs b/OptimizelySDK/Bucketing/DecisionService.cs index 3973b13e..542d5d22 100644 --- a/OptimizelySDK/Bucketing/DecisionService.cs +++ b/OptimizelySDK/Bucketing/DecisionService.cs @@ -133,16 +133,16 @@ public virtual Variation GetVariation(Experiment experiment, } else if (userProfileMap == null) { - Logger.Log(LogLevel.INFO, reasons.AddInfo("We were unable to get a user profile map from the UserProfileService.")); + Logger.Log(LogLevel.INFO, reasons?.AddInfo("We were unable to get a user profile map from the UserProfileService.")); } else { - Logger.Log(LogLevel.ERROR, reasons.AddInfo("The UserProfileService returned an invalid map.")); + Logger.Log(LogLevel.ERROR, reasons?.AddInfo("The UserProfileService returned an invalid map.")); } } catch (Exception exception) { - Logger.Log(LogLevel.ERROR, reasons.AddInfo(exception.Message)); + Logger.Log(LogLevel.ERROR, reasons?.AddInfo(exception.Message)); ErrorHandler.HandleError(new Exceptions.OptimizelyRuntimeException(exception.Message)); } } @@ -168,7 +168,7 @@ public virtual Variation GetVariation(Experiment experiment, return variation; } - Logger.Log(LogLevel.INFO, reasons.AddInfo($"User \"{userId}\" does not meet conditions to be in experiment \"{experiment.Key}\".")); + Logger.Log(LogLevel.INFO, reasons?.AddInfo($"User \"{userId}\" does not meet conditions to be in experiment \"{experiment.Key}\".")); return null; } @@ -227,7 +227,7 @@ public Variation GetForcedVariation(string experimentKey, string userId, Project // this case is logged in getVariationFromKey if (string.IsNullOrEmpty(variationKey)) return null; - Logger.Log(LogLevel.DEBUG, reasons.AddInfo($@"Variation ""{variationKey}"" is mapped to experiment ""{experimentKey}"" and user ""{userId}"" in the forced variation map")); + Logger.Log(LogLevel.DEBUG, reasons?.AddInfo($@"Variation ""{variationKey}"" is mapped to experiment ""{experimentKey}"" and user ""{userId}"" in the forced variation map")); Variation variation = config.GetVariationFromKey(experimentKey, variationKey); @@ -317,9 +317,9 @@ public Variation GetWhitelistedVariation(Experiment experiment, string userId, I : null; if (forcedVariation != null) - Logger.Log(LogLevel.INFO, reasons.AddInfo($"User \"{userId}\" is forced in variation \"{forcedVariationKey}\".")); + Logger.Log(LogLevel.INFO, reasons?.AddInfo($"User \"{userId}\" is forced in variation \"{forcedVariationKey}\".")); else - Logger.Log(LogLevel.ERROR, reasons.AddInfo($"Variation \"{forcedVariationKey}\" is not in the datafile. Not activating user \"{userId}\".")); + Logger.Log(LogLevel.ERROR, reasons?.AddInfo($"Variation \"{forcedVariationKey}\" is not in the datafile. Not activating user \"{userId}\".")); return forcedVariation; } @@ -342,7 +342,7 @@ public Variation GetStoredVariation(Experiment experiment, UserProfile userProfi if (decision == null) { - Logger.Log(LogLevel.INFO, reasons.AddInfo($"No previously activated variation of experiment \"{experimentKey}\" for user \"{userProfile.UserId}\" found in user profile.")); + Logger.Log(LogLevel.INFO, reasons?.AddInfo($"No previously activated variation of experiment \"{experimentKey}\" for user \"{userProfile.UserId}\" found in user profile.")); return null; } @@ -356,11 +356,11 @@ public Variation GetStoredVariation(Experiment experiment, UserProfile userProfi if (savedVariation == null) { - Logger.Log(LogLevel.INFO, reasons.AddInfo($"User \"{userProfile.UserId}\" was previously bucketed into variation with ID \"{variationId}\" for experiment \"{experimentId}\", but no matching variation was found for that user. We will re-bucket the user.")); + Logger.Log(LogLevel.INFO, reasons?.AddInfo($"User \"{userProfile.UserId}\" was previously bucketed into variation with ID \"{variationId}\" for experiment \"{experimentId}\", but no matching variation was found for that user. We will re-bucket the user.")); return null; } - Logger.Log(LogLevel.INFO, reasons.AddInfo($"Returning previously activated variation \"{savedVariation.Key}\" of experiment \"{experimentKey}\" for user \"{userProfile.UserId}\" from user profile.")); + Logger.Log(LogLevel.INFO, reasons?.AddInfo($"Returning previously activated variation \"{savedVariation.Key}\" of experiment \"{experimentKey}\" for user \"{userProfile.UserId}\" from user profile.")); return savedVariation; } catch (Exception) @@ -407,11 +407,11 @@ public void SaveVariation(Experiment experiment, Variation variation, UserProfil try { UserProfileService.Save(userProfile.ToMap()); - Logger.Log(LogLevel.INFO, reasons.AddInfo($"Saved variation \"{variation.Id}\" of experiment \"{experiment.Id}\" for user \"{userProfile.UserId}\".")); + Logger.Log(LogLevel.INFO, reasons?.AddInfo($"Saved variation \"{variation.Id}\" of experiment \"{experiment.Id}\" for user \"{userProfile.UserId}\".")); } catch (Exception exception) { - Logger.Log(LogLevel.ERROR, reasons.AddInfo($"Failed to save variation \"{variation.Id}\" of experiment \"{experiment.Id}\" for user \"{userProfile.UserId}\".")); + Logger.Log(LogLevel.ERROR, reasons?.AddInfo($"Failed to save variation \"{variation.Id}\" of experiment \"{experiment.Id}\" for user \"{userProfile.UserId}\".")); ErrorHandler.HandleError(new Exceptions.OptimizelyRuntimeException(exception.Message)); } } @@ -441,7 +441,7 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature if (string.IsNullOrEmpty(featureFlag.RolloutId)) { - Logger.Log(LogLevel.INFO, reasons.AddInfo($"The feature flag \"{featureFlag.Key}\" is not used in a rollout.")); + Logger.Log(LogLevel.INFO, reasons?.AddInfo($"The feature flag \"{featureFlag.Key}\" is not used in a rollout.")); return null; } @@ -449,7 +449,7 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature if (string.IsNullOrEmpty(rollout.Id)) { - Logger.Log(LogLevel.ERROR, reasons.AddInfo($"The rollout with id \"{featureFlag.RolloutId}\" is not found in the datafile for feature flag \"{featureFlag.Key}\"")); + Logger.Log(LogLevel.ERROR, reasons?.AddInfo($"The rollout with id \"{featureFlag.RolloutId}\" is not found in the datafile for feature flag \"{featureFlag.Key}\"")); return null; } @@ -526,7 +526,7 @@ public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag feat if (featureFlag.ExperimentIds == null || featureFlag.ExperimentIds.Count == 0) { - Logger.Log(LogLevel.INFO, reasons.AddInfo($"The feature flag \"{featureFlag.Key}\" is not used in any experiments.")); + Logger.Log(LogLevel.INFO, reasons?.AddInfo($"The feature flag \"{featureFlag.Key}\" is not used in any experiments.")); return null; } @@ -541,12 +541,12 @@ public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag feat if (variation != null && !string.IsNullOrEmpty(variation.Id)) { - Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is bucketed into experiment \"{experiment.Key}\" of feature \"{featureFlag.Key}\".")); + Logger.Log(LogLevel.INFO, reasons?.AddInfo($"The user \"{userId}\" is bucketed into experiment \"{experiment.Key}\" of feature \"{featureFlag.Key}\".")); return new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST); } } - Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is not bucketed into any of the experiments on the feature \"{featureFlag.Key}\".")); + Logger.Log(LogLevel.INFO, reasons?.AddInfo($"The user \"{userId}\" is not bucketed into any of the experiments on the feature \"{featureFlag.Key}\".")); return null; } @@ -592,11 +592,11 @@ public virtual FeatureDecision GetVariationForFeature(FeatureFlag featureFlag, if (decision != null) { - Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is bucketed into a rollout for feature flag \"{featureFlag.Key}\".")); + Logger.Log(LogLevel.INFO, reasons?.AddInfo($"The user \"{userId}\" is bucketed into a rollout for feature flag \"{featureFlag.Key}\".")); return decision; } - Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is not bucketed into a rollout for feature flag \"{featureFlag.Key}\".")); + Logger.Log(LogLevel.INFO, reasons?.AddInfo($"The user \"{userId}\" is not bucketed into a rollout for feature flag \"{featureFlag.Key}\".")); return new FeatureDecision(null, null, FeatureDecision.DECISION_SOURCE_ROLLOUT); } @@ -620,7 +620,7 @@ private string GetBucketingId(string userId, UserAttributes filteredAttributes, } else { - Logger.Log(LogLevel.WARN, reasons.AddInfo("BucketingID attribute is not a string. Defaulted to userId")); + Logger.Log(LogLevel.WARN, reasons?.AddInfo("BucketingID attribute is not a string. Defaulted to userId")); } } diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs index 0e4f156c..df386559 100644 --- a/OptimizelySDK/Optimizely.cs +++ b/OptimizelySDK/Optimizely.cs @@ -712,6 +712,15 @@ public OptimizelyUserContext CreateUserContext(string userId, return new OptimizelyUserContext(this, userId, userAttributes, ErrorHandler, Logger); } + /// + /// Returns a decision result ({@link OptimizelyDecision}) for a given flag key and a user context, which contains all data required to deliver the flag. + ///
    + ///
  • If the SDK finds an error, it’ll return a decision with null for variationKey. The decision will include an error message in reasons. + ///
+ ///
+ /// A flag key for which a decision will be made. + /// A list of options for decision-making. + /// A decision result. public OptimizelyDecision Decide(OptimizelyUserContext user, string key, List options) @@ -723,16 +732,9 @@ public OptimizelyDecision Decide(OptimizelyUserContext user, return OptimizelyDecision.NewErrorDecision(key, user, DecisionMessage.SDK_NOT_READY, ErrorHandler, Logger); } var userId = user.UserId; - var inputValues = new Dictionary - { - { USER_ID, userId }, - }; - - if (!ValidateStringInputs(inputValues)) - return null; - + var flag = config.GetFeatureFlagFromKey(key); - if (flag == null) + if (flag.Key == null) { return OptimizelyDecision.NewErrorDecision(key, user, diff --git a/OptimizelySDK/Utils/ExperimentUtils.cs b/OptimizelySDK/Utils/ExperimentUtils.cs index b6023480..593718b1 100644 --- a/OptimizelySDK/Utils/ExperimentUtils.cs +++ b/OptimizelySDK/Utils/ExperimentUtils.cs @@ -94,7 +94,7 @@ public static bool DoesUserMeetAudienceConditions(ProjectConfig config, var result = expConditions.Evaluate(config, userAttributes, logger).GetValueOrDefault(); var resultText = result.ToString().ToUpper(); - logger.Log(LogLevel.INFO, reasons.AddInfo($@"Audiences for {loggingKeyType} ""{loggingKey}"" collectively evaluated to {resultText}")); + logger.Log(LogLevel.INFO, reasons?.AddInfo($@"Audiences for {loggingKeyType} ""{loggingKey}"" collectively evaluated to {resultText}")); return result; } } From 5ce14194f015d06b5fd8e99cbcdac384fed4f5fb Mon Sep 17 00:00:00 2001 From: muhammadnoman Date: Fri, 13 Nov 2020 21:54:30 +0500 Subject: [PATCH 14/34] removed underscores --- OptimizelySDK.Tests/OptimizelyUserContextTest.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs index 7a0d67bf..27e0f60a 100644 --- a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs +++ b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs @@ -48,7 +48,7 @@ public void SetUp() } [Test] - public void OptimizelyUserContext_withAttributes() + public void OptimizelyUserContextWithAttributes() { var attributes = new UserAttributes() { { "house", "GRYFFINDOR" } }; OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object); @@ -59,7 +59,7 @@ public void OptimizelyUserContext_withAttributes() } [Test] - public void OptimizelyUserContext_noAttributes() + public void OptimizelyUserContextNoAttributes() { OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, null, ErrorHandlerMock.Object, LoggerMock.Object); @@ -90,7 +90,7 @@ public void SetAttribute() } [Test] - public void SetAttribute_noAttribute() + public void SetAttributeNoAttribute() { OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, null, ErrorHandlerMock.Object, LoggerMock.Object); From a424fef6d776f5b8640196ac55acc4eb5e17db88 Mon Sep 17 00:00:00 2001 From: muhammadnoman Date: Mon, 16 Nov 2020 22:28:29 +0500 Subject: [PATCH 15/34] Added Decide Options unit tests --- .../OptimizelyUserContextTest.cs | 190 ++++++++++++++++++ OptimizelySDK/Optimizely.cs | 7 +- 2 files changed, 194 insertions(+), 3 deletions(-) diff --git a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs index f911bc60..facbc3dd 100644 --- a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs +++ b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs @@ -18,11 +18,17 @@ using Castle.Core.Internal; using Moq; using NUnit.Framework; +using OptimizelySDK.Bucketing; +using OptimizelySDK.Config; using OptimizelySDK.Entity; using OptimizelySDK.ErrorHandler; +using OptimizelySDK.Event; using OptimizelySDK.Event.Dispatcher; using OptimizelySDK.Logger; +using OptimizelySDK.Notifications; using OptimizelySDK.OptimizelyDecisions; +using OptimizelySDK.Tests.NotificationTests; +using OptimizelySDK.Utils; using System; using System.Collections.Generic; @@ -36,6 +42,7 @@ public class OptimizelyUserContextTest private Mock LoggerMock; private Mock ErrorHandlerMock; private Mock EventDispatcherMock; + private Mock NotificationCallbackMock; [SetUp] public void SetUp() @@ -43,6 +50,8 @@ public void SetUp() LoggerMock = new Mock(); LoggerMock.Setup(i => i.Log(It.IsAny(), It.IsAny())); + NotificationCallbackMock = new Mock(); + ErrorHandlerMock = new Mock(); ErrorHandlerMock.Setup(e => e.HandleError(It.IsAny())); EventDispatcherMock = new Mock(); @@ -422,6 +431,187 @@ public void DecideAllEnabledFlagsOnlyDecideOptions() new string[0]); Assert.IsTrue(TestData.CompareObjects(decisions[flagKey1], expDecision1)); } + + [Test] + public void DecideExcludeVariablesDecideOptions() + { + var flagKey = "multi_variate_feature"; + var variablesExpected = new Dictionary(); + var user = Optimizely.CreateUserContext(UserID); + user.SetAttribute("browser_type", "chrome"); + var decideOptions = new List() { OptimizelyDecideOption.EXCLUDE_VARIABLES }; + + var decision = user.Decide(flagKey, decideOptions); + + Assert.AreEqual(decision.VariationKey, "Gred"); + Assert.False(decision.Enabled); + Assert.AreEqual(decision.Variables.ToDictionary(), variablesExpected); + Assert.AreEqual(decision.RuleKey, "test_experiment_multivariate"); + Assert.AreEqual(decision.FlagKey, flagKey); + Assert.AreEqual(decision.UserContext, user); + Assert.True(decision.Reasons.IsNullOrEmpty()); + } + + [Test] + public void DecideIncludeReasonsDecideOptions() + { + var flagKey = "invalid_key"; + var user = Optimizely.CreateUserContext(UserID); + user.SetAttribute("browser_type", "chrome"); + + var decision = user.Decide(flagKey); + Assert.True(decision.Reasons.Length == 1); + Assert.AreEqual(decision.Reasons[0], DecisionMessage.Reason(DecisionMessage.FLAG_KEY_INVALID, flagKey)); + + var decideOptions = new List() { OptimizelyDecideOption.INCLUDE_REASONS }; + + decision = user.Decide(flagKey, decideOptions); + Assert.True(decision.Reasons.Length == 1); + Assert.AreEqual(decision.Reasons[0], DecisionMessage.Reason(DecisionMessage.FLAG_KEY_INVALID, flagKey)); + + flagKey = "multi_variate_feature"; + decision = user.Decide(flagKey); + Assert.True(decision.Reasons.Length == 0); + + Assert.AreEqual(decision.VariationKey, "Gred"); + Assert.False(decision.Enabled); + Assert.AreEqual(decision.RuleKey, "test_experiment_multivariate"); + Assert.AreEqual(decision.FlagKey, flagKey); + Assert.AreEqual(decision.UserContext, user); + Assert.True(decision.Reasons.IsNullOrEmpty()); + + decision = user.Decide(flagKey, decideOptions); + Assert.True(decision.Reasons.Length > 0); + Assert.AreEqual("User [testUserID] is in variation [Gred] of experiment [test_experiment_multivariate].", decision.Reasons[0]); + Assert.AreEqual("The user \"testUserID\" is bucketed into experiment \"test_experiment_multivariate\" of feature \"multi_variate_feature\".", decision.Reasons[1]); + } + + [Test] + public void TestDoNotSendEventDecide() + { + var flagKey = "multi_variate_feature"; + var variablesExpected = Optimizely.GetAllFeatureVariables(flagKey, UserID); + + var optimizely = new Optimizely(TestData.Datafile, EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object); + var user = optimizely.CreateUserContext(UserID); + user.SetAttribute("browser_type", "chrome"); + + var decideOptions = new List() { OptimizelyDecideOption.DISABLE_DECISION_EVENT }; + var decision = user.Decide(flagKey, decideOptions); + EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), Times.Never); + + decision = user.Decide(flagKey); + EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), Times.Once); + + Assert.AreEqual(decision.VariationKey, "Gred"); + Assert.False(decision.Enabled); + Assert.AreEqual(decision.Variables.ToDictionary(), variablesExpected.ToDictionary()); + Assert.AreEqual(decision.RuleKey, "test_experiment_multivariate"); + Assert.AreEqual(decision.FlagKey, flagKey); + Assert.AreEqual(decision.UserContext, user); + } + + [Test] + public void TestDefaultDecideOptions() + { + var flagKey = "multi_variate_feature"; + var variablesExpected = Optimizely.GetAllFeatureVariables(flagKey, UserID); + var decideOptions = new List() { OptimizelyDecideOption.DISABLE_DECISION_EVENT }; + + var optimizely = new Optimizely(TestData.Datafile, + EventDispatcherMock.Object, + LoggerMock.Object, + ErrorHandlerMock.Object, + defaultDecideOptions: decideOptions.ToArray()); + + var user = optimizely.CreateUserContext(UserID); + user.SetAttribute("browser_type", "chrome"); + + var decision = user.Decide(flagKey); + EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), Times.Never); + + Assert.AreEqual(decision.VariationKey, "Gred"); + Assert.False(decision.Enabled); + Assert.AreEqual(decision.Variables.ToDictionary(), variablesExpected.ToDictionary()); + Assert.AreEqual(decision.RuleKey, "test_experiment_multivariate"); + Assert.AreEqual(decision.FlagKey, flagKey); + Assert.AreEqual(decision.UserContext, user); + } + + [Test] + public void TestDecisionNotification() + { + var flagKey = "string_single_variable_feature"; + var variationKey = "control"; + var enabled = true; + var variables = Optimizely.GetAllFeatureVariables(flagKey, UserID); + var ruleKey = "test_experiment_with_feature_rollout"; + var reasons = new Dictionary(); + var user = Optimizely.CreateUserContext(UserID); + user.SetAttribute("browser_type", "chrome"); + + var decisionInfo = new Dictionary + { + { "flagKey", flagKey }, + { "enabled", enabled }, + { "variables", variables.ToDictionary() }, + { "variationKey", variationKey }, + { "ruleKey", ruleKey }, + { "reasons", reasons }, + { "decisionEventDispatched", true }, + { "featureEnabled", true }, + }; + + var userAttributes = new UserAttributes + { + { "browser_type", "chrome" } + }; + + // Mocking objects. + NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny>())); + + Optimizely.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); + + user.Decide(flagKey); + NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback(DecisionNotificationTypes.FEATURE, UserID, userAttributes, It.Is>(info => + TestData.CompareObjects(info, decisionInfo))), + Times.Once); + } + + [Test] + public void TestDecideOptionsByPassUPS() + { + var userProfileServiceMock = new Mock(); + var flagKey = "string_single_variable_feature"; + + var experimentId = "122235"; + var userId = "testUser3"; + var variationKey = "control"; + var fbVariationId = "122237"; + var fbVariationKey = "variation"; + + + var userProfile = new UserProfile(userId, new Dictionary + { + { experimentId, new Decision(fbVariationId)} + }); + + userProfileServiceMock.Setup(_ => _.Lookup(userId)).Returns(userProfile.ToMap()); + + var optimizely = new Optimizely(TestData.Datafile, EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object, userProfileServiceMock.Object); + + var user = optimizely.CreateUserContext(userId); + + var projectConfig = DatafileProjectConfig.Create(TestData.Datafile, LoggerMock.Object, ErrorHandlerMock.Object); + + var variationUserProfile = user.Decide(flagKey); + Assert.AreEqual(fbVariationKey, variationUserProfile.VariationKey); + + var decideOptions = new List() { OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE }; + variationUserProfile = user.Decide(flagKey, decideOptions); + Assert.AreEqual(variationKey, variationUserProfile.VariationKey); + } #endregion } } diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs index 3c09aad5..9a1dc6ef 100644 --- a/OptimizelySDK/Optimizely.cs +++ b/OptimizelySDK/Optimizely.cs @@ -122,10 +122,11 @@ public Optimizely(string datafile, IErrorHandler errorHandler = null, UserProfileService userProfileService = null, bool skipJsonValidation = false, - EventProcessor eventProcessor = null) + EventProcessor eventProcessor = null, + OptimizelyDecideOption[] defaultDecideOptions = null) { try { - InitializeComponents(eventDispatcher, logger, errorHandler, userProfileService, null, eventProcessor); + InitializeComponents(eventDispatcher, logger, errorHandler, userProfileService, null, eventProcessor, defaultDecideOptions); if (ValidateInputs(datafile, skipJsonValidation)) { var config = DatafileProjectConfig.Create(datafile, Logger, ErrorHandler); @@ -805,7 +806,7 @@ public OptimizelyDecision Decide(OptimizelyUserContext user, SendImpressionEvent(flagDecision.Experiment, variation, userId, userAttributes, config, key, decisionSource, featureEnabled); decisionEventDispatched = true; } - var reasonsToReport = decisionReasons.ToReport(); + List reasonsToReport = decisionReasons.ToReport(); var variationKey = flagDecision.Variation?.Key; // TODO: add ruleKey values when available later. use a copy of experimentKey until then. From 7eeb58875cebfb5ff4f2c1c8878b656cb7278ee8 Mon Sep 17 00:00:00 2001 From: muhammadnoman Date: Mon, 16 Nov 2020 23:12:57 +0500 Subject: [PATCH 16/34] Added track event tests and fixes defaultDecideOptions --- OptimizelySDK.Tests/OptimizelyTest.cs | 8 +++- .../OptimizelyUserContextTest.cs | 37 +++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/OptimizelySDK.Tests/OptimizelyTest.cs b/OptimizelySDK.Tests/OptimizelyTest.cs index e310571a..7f2b56b9 100644 --- a/OptimizelySDK.Tests/OptimizelyTest.cs +++ b/OptimizelySDK.Tests/OptimizelyTest.cs @@ -35,6 +35,7 @@ using System.Globalization; using System.Threading; using OptimizelySDK.Tests.Utils; +using OptimizelySDK.OptimizelyDecisions; namespace OptimizelySDK.Tests { @@ -98,7 +99,7 @@ public void Initialize() SkipJsonValidation = false, }; - OptimizelyMock = new Mock(TestData.Datafile, EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object, null, false, null) + OptimizelyMock = new Mock(TestData.Datafile, EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object, null, false, null, null) { CallBase = true }; @@ -166,6 +167,8 @@ private class OptimizelyHelper public bool SkipJsonValidation { get; set; } public EventProcessor EventProcessor { get; set; } + public OptimizelyDecideOption[] DefaultDecideOptions { get; set; } + public PrivateObject CreatePrivateOptimizely() { return new PrivateObject(typeof(Optimizely), ParameterTypes, @@ -177,7 +180,8 @@ public PrivateObject CreatePrivateOptimizely() ErrorHandler, UserProfileService, SkipJsonValidation, - EventProcessor + EventProcessor, + DefaultDecideOptions }); } } diff --git a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs index facbc3dd..763554a0 100644 --- a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs +++ b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs @@ -613,5 +613,42 @@ public void TestDecideOptionsByPassUPS() Assert.AreEqual(variationKey, variationUserProfile.VariationKey); } #endregion + + #region TrackEvent + [Test] + public void TestTrackEventWithAudienceConditions() + { + var OptimizelyWithTypedAudiences = new Optimizely(TestData.TypedAudienceDatafile, EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object); + var userAttributes = new UserAttributes + { + { "house", "Gryffindor" }, + { "should_do_it", false } + }; + + var user = OptimizelyWithTypedAudiences.CreateUserContext(UserID, userAttributes); + + // Should be excluded as exact match boolean audience with id '3468206643' does not match so the overall conditions fail. + user.TrackEvent("user_signed_up"); + + EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), Times.Once); + } + + [Test] + public void TrackEventEmptyAttributesWithEventTags() + { + var OptimizelyWithTypedAudiences = new Optimizely(TestData.TypedAudienceDatafile, EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object); + + var user = OptimizelyWithTypedAudiences.CreateUserContext(UserID); + + // Should be excluded as exact match boolean audience with id '3468206643' does not match so the overall conditions fail. + user.TrackEvent("user_signed_up", new EventTags + { + { "revenue", 42 }, + { "wont_send_null", null} + }); + + EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), Times.Once); + } + #endregion } } From 8e1750e3141d44f14aa5b76d626c64820dca558a Mon Sep 17 00:00:00 2001 From: muhammadnoman Date: Tue, 17 Nov 2020 21:50:55 +0500 Subject: [PATCH 17/34] Changed UserAttribute to Attributes --- OptimizelySDK.Tests/OptimizelyTest.cs | 12 ++++++------ OptimizelySDK.Tests/OptimizelyUserContextTest.cs | 16 ++++++++-------- OptimizelySDK/Optimizely.cs | 2 +- OptimizelySDK/OptimizelyUserContext.cs | 8 ++++---- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/OptimizelySDK.Tests/OptimizelyTest.cs b/OptimizelySDK.Tests/OptimizelyTest.cs index 7f2b56b9..b961a560 100644 --- a/OptimizelySDK.Tests/OptimizelyTest.cs +++ b/OptimizelySDK.Tests/OptimizelyTest.cs @@ -200,7 +200,7 @@ public void TestCreateUserContext() var optlyUserContext = Optimizely.CreateUserContext(TestUserId, attribute); Assert.AreEqual(TestUserId, optlyUserContext.UserId); Assert.AreEqual(Optimizely, optlyUserContext.Optimizely); - Assert.AreEqual(attribute, optlyUserContext.UserAttributes); + Assert.AreEqual(attribute, optlyUserContext.Attributes); } [Test] @@ -209,7 +209,7 @@ public void TestCreateUserContextWithoutAttributes() var optlyUserContext = Optimizely.CreateUserContext(TestUserId); Assert.AreEqual(TestUserId, optlyUserContext.UserId); Assert.AreEqual(Optimizely, optlyUserContext.Optimizely); - Assert.IsTrue(optlyUserContext.UserAttributes.Count == 0); + Assert.IsTrue(optlyUserContext.Attributes.Count == 0); } [Test] @@ -232,11 +232,11 @@ public void TestCreateUserContextMultipleAttribute() Assert.AreEqual("userId1", optlyUserContext1.UserId); Assert.AreEqual(Optimizely, optlyUserContext1.Optimizely); - Assert.AreEqual(attribute1, optlyUserContext1.UserAttributes); + Assert.AreEqual(attribute1, optlyUserContext1.Attributes); Assert.AreEqual("userId2", optlyUserContext2.UserId); Assert.AreEqual(Optimizely, optlyUserContext2.Optimizely); - Assert.AreEqual(attribute2, optlyUserContext2.UserAttributes); + Assert.AreEqual(attribute2, optlyUserContext2.Attributes); } [Test] @@ -251,7 +251,7 @@ public void TestChangeAttributeDoesNotEffectValues() var optlyUserContext = Optimizely.CreateUserContext(userId, attribute); Assert.AreEqual(TestUserId, optlyUserContext.UserId); Assert.AreEqual(Optimizely, optlyUserContext.Optimizely); - Assert.AreEqual(attribute, optlyUserContext.UserAttributes); + Assert.AreEqual(attribute, optlyUserContext.Attributes); attribute = new UserAttributes { @@ -262,7 +262,7 @@ public void TestChangeAttributeDoesNotEffectValues() userId = "InvalidUser"; Assert.AreEqual("testUserId", optlyUserContext.UserId); Assert.AreEqual(Optimizely, optlyUserContext.Optimizely); - Assert.AreNotEqual(attribute, optlyUserContext.UserAttributes); + Assert.AreNotEqual(attribute, optlyUserContext.Attributes); } #endregion diff --git a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs index 763554a0..52d5459e 100644 --- a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs +++ b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs @@ -67,7 +67,7 @@ public void OptimizelyUserContextWithAttributes() Assert.AreEqual(user.Optimizely, Optimizely); Assert.AreEqual(user.UserId, UserID); - Assert.AreEqual(user.UserAttributes, attributes); + Assert.AreEqual(user.Attributes, attributes); } [Test] @@ -77,7 +77,7 @@ public void OptimizelyUserContextNoAttributes() Assert.AreEqual(user.Optimizely, Optimizely); Assert.AreEqual(user.UserId, UserID); - Assert.True(user.UserAttributes.Count == 0); + Assert.True(user.Attributes.Count == 0); } [Test] @@ -93,7 +93,7 @@ public void SetAttribute() Assert.AreEqual(user.Optimizely, Optimizely); Assert.AreEqual(user.UserId, UserID); - var newAttributes = user.UserAttributes; + var newAttributes = user.Attributes; Assert.AreEqual(newAttributes["house"], "GRYFFINDOR"); Assert.AreEqual(newAttributes["k1"], "v1"); Assert.AreEqual(newAttributes["k2"], true); @@ -111,7 +111,7 @@ public void SetAttributeNoAttribute() Assert.AreEqual(user.Optimizely, Optimizely); Assert.AreEqual(user.UserId, UserID); - var newAttributes = user.UserAttributes; + var newAttributes = user.Attributes; Assert.AreEqual(newAttributes["k1"], "v1"); Assert.AreEqual(newAttributes["k2"], true); } @@ -125,7 +125,7 @@ public void SetAttributeOverride() user.SetAttribute("k1", "v1"); user.SetAttribute("house", "v2"); - var newAttributes = user.UserAttributes; + var newAttributes = user.Attributes; Assert.AreEqual(newAttributes["k1"], "v1"); Assert.AreEqual(newAttributes["house"], "v2"); } @@ -136,15 +136,15 @@ public void SetAttributeNullValue() var attributes = new UserAttributes() { { "k1", null } }; OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object); - var newAttributes = user.UserAttributes; + var newAttributes = user.Attributes; Assert.AreEqual(newAttributes["k1"], null); user.SetAttribute("k1", true); - newAttributes = user.UserAttributes; + newAttributes = user.Attributes; Assert.AreEqual(newAttributes["k1"], true); user.SetAttribute("k1", null); - newAttributes = user.UserAttributes; + newAttributes = user.Attributes; Assert.AreEqual(newAttributes["k1"], null); } diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs index 9a1dc6ef..9187c991 100644 --- a/OptimizelySDK/Optimizely.cs +++ b/OptimizelySDK/Optimizely.cs @@ -743,7 +743,7 @@ public OptimizelyDecision Decide(OptimizelyUserContext user, ErrorHandler, Logger); } - var userAttributes = user.UserAttributes; + var userAttributes = user.Attributes; var decisionEventDispatched = false; var allOptions = GetAllOptions(options); var decisionReasons = DefaultDecisionReasons.NewInstance(allOptions); diff --git a/OptimizelySDK/OptimizelyUserContext.cs b/OptimizelySDK/OptimizelyUserContext.cs index 634a6e90..07747438 100644 --- a/OptimizelySDK/OptimizelyUserContext.cs +++ b/OptimizelySDK/OptimizelyUserContext.cs @@ -32,7 +32,7 @@ public class OptimizelyUserContext // userID for Optimizely user context public string UserId { get; } // user attributes for Optimizely user context. - public UserAttributes UserAttributes { get; } + public UserAttributes Attributes { get; } // Optimizely object to be used. public Optimizely Optimizely { get; } @@ -41,7 +41,7 @@ public OptimizelyUserContext(Optimizely optimizely, string userId, UserAttribute ErrorHandler = errorHandler; Logger = logger; Optimizely = optimizely; - UserAttributes = userAttributes ?? new UserAttributes(); + Attributes = userAttributes ?? new UserAttributes(); UserId = userId; } @@ -58,7 +58,7 @@ public void SetAttribute(string key, object value) } else { - UserAttributes[key] = value; + Attributes[key] = value; } } @@ -146,7 +146,7 @@ public void TrackEvent(string eventName) public void TrackEvent(string eventName, EventTags eventTags) { - Optimizely.Track(eventName, UserId, UserAttributes, eventTags); + Optimizely.Track(eventName, UserId, Attributes, eventTags); } } } From 980abd0dcba8db0e99facf654ec55e2f9b541cc0 Mon Sep 17 00:00:00 2001 From: muhammadnoman Date: Wed, 18 Nov 2020 21:56:47 +0500 Subject: [PATCH 18/34] Added DecisionNotificationType flag --- OptimizelySDK/Optimizely.cs | 2 +- OptimizelySDK/Utils/DecisionInfoTypes.cs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs index 9187c991..b425b5d1 100644 --- a/OptimizelySDK/Optimizely.cs +++ b/OptimizelySDK/Optimizely.cs @@ -824,7 +824,7 @@ public OptimizelyDecision Decide(OptimizelyUserContext user, { "featureEnabled", featureEnabled }, }; - NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, DecisionNotificationTypes.FEATURE, userId, + NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, DecisionNotificationTypes.FLAG, userId, userAttributes ?? new UserAttributes(), decisionInfo); return new OptimizelyDecision( diff --git a/OptimizelySDK/Utils/DecisionInfoTypes.cs b/OptimizelySDK/Utils/DecisionInfoTypes.cs index 188522e8..6d504d67 100644 --- a/OptimizelySDK/Utils/DecisionInfoTypes.cs +++ b/OptimizelySDK/Utils/DecisionInfoTypes.cs @@ -1,5 +1,5 @@ /* - * Copyright 2019, Optimizely + * Copyright 2019-2020, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,5 +23,6 @@ public static class DecisionNotificationTypes public const string FEATURE_TEST = "feature-test"; public const string FEATURE_VARIABLE = "feature-variable"; public const string ALL_FEATURE_VARIABLE = "all-feature-variables"; + public const string FLAG = "flag"; } } From 14769f8194669a9ec94989189745dbf1dff0c0a9 Mon Sep 17 00:00:00 2001 From: muhammadnoman Date: Fri, 20 Nov 2020 21:27:14 +0500 Subject: [PATCH 19/34] Added check of Key != null in decide --- OptimizelySDK/Optimizely.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs index b425b5d1..4dc3eab1 100644 --- a/OptimizelySDK/Optimizely.cs +++ b/OptimizelySDK/Optimizely.cs @@ -732,8 +732,17 @@ public OptimizelyDecision Decide(OptimizelyUserContext user, { return OptimizelyDecision.NewErrorDecision(key, user, DecisionMessage.SDK_NOT_READY, ErrorHandler, Logger); } - var userId = user.UserId; + + if (key == null) + { + return OptimizelyDecision.NewErrorDecision(key, + user, + DecisionMessage.Reason(DecisionMessage.FLAG_KEY_INVALID, key), + ErrorHandler, Logger); + } + var userId = user?.UserId; + var flag = config.GetFeatureFlagFromKey(key); if (flag.Key == null) { From fafb14ce73fbbfcda44c2d0e889bb029e457cf2a Mon Sep 17 00:00:00 2001 From: muhammadnoman Date: Tue, 24 Nov 2020 19:28:52 +0500 Subject: [PATCH 20/34] Added Additional unit tests of optimizelyUserContext --- .../OptimizelyDecisionTest.cs | 14 +++++++++++++- OptimizelySDK.Tests/OptimizelySDK.Tests.csproj | 1 + OptimizelySDK.Tests/OptimizelyUserContextTest.cs | 16 ++++++++++++++++ .../DefaultDecisionReasons.cs | 6 +++--- OptimizelySDK/OptimizelyUserContext.cs | 4 ++-- 5 files changed, 35 insertions(+), 6 deletions(-) diff --git a/OptimizelySDK.Tests/OptimizelyDecisions/OptimizelyDecisionTest.cs b/OptimizelySDK.Tests/OptimizelyDecisions/OptimizelyDecisionTest.cs index f975e462..bf8f5f17 100644 --- a/OptimizelySDK.Tests/OptimizelyDecisions/OptimizelyDecisionTest.cs +++ b/OptimizelySDK.Tests/OptimizelyDecisions/OptimizelyDecisionTest.cs @@ -82,7 +82,7 @@ public void TestNewDecision() } [Test] - public void TestNewDecisionReasonWithDecideAllOptions() + public void TestNewDecisionReasonWithIncludeReasons() { var decisionReasons = DefaultDecisionReasons.NewInstance(new List() { OptimizelyDecideOption.INCLUDE_REASONS }); decisionReasons.AddError(DecisionMessage.Reason(DecisionMessage.FLAG_KEY_INVALID, "invalid_key")); @@ -94,5 +94,17 @@ public void TestNewDecisionReasonWithDecideAllOptions() Assert.AreEqual(decisionReasons.ToReport()[2], "Some info message."); } + [Test] + public void TestNewDecisionReasonWithoutIncludeReasons() + { + var decisionReasons = DefaultDecisionReasons.NewInstance(); + decisionReasons.AddError(DecisionMessage.Reason(DecisionMessage.FLAG_KEY_INVALID, "invalid_key")); + + Assert.AreEqual(decisionReasons.ToReport()[0], "No flag was found for key \"invalid_key\"."); + decisionReasons.AddError(DecisionMessage.Reason(DecisionMessage.VARIABLE_VALUE_INVALID, "invalid_key")); + Assert.AreEqual(decisionReasons.ToReport()[1], "Variable value for key \"invalid_key\" is invalid or wrong type."); + decisionReasons.AddInfo("Some info message."); + Assert.AreEqual(decisionReasons.ToReport().Count, 2); + } } } diff --git a/OptimizelySDK.Tests/OptimizelySDK.Tests.csproj b/OptimizelySDK.Tests/OptimizelySDK.Tests.csproj index 0f44902c..99a0995c 100644 --- a/OptimizelySDK.Tests/OptimizelySDK.Tests.csproj +++ b/OptimizelySDK.Tests/OptimizelySDK.Tests.csproj @@ -91,6 +91,7 @@ + diff --git a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs index 27e0f60a..1c18ff46 100644 --- a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs +++ b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs @@ -104,5 +104,21 @@ public void SetAttributeNoAttribute() Assert.AreEqual(newAttributes["k2"], true); } + [Test] + public void SetAttributeToOverrideAttribute() + { + OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, null, ErrorHandlerMock.Object, LoggerMock.Object); + + + Assert.AreEqual(user.Optimizely, Optimizely); + Assert.AreEqual(user.UserId, UserID); + + user.SetAttribute("k1", "v1"); + Assert.AreEqual(user.UserAttributes["k1"], "v1"); + + user.SetAttribute("k1", true); + Assert.AreEqual(user.UserAttributes["k1"], true); + } + } } diff --git a/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs b/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs index 95627330..0ded3470 100644 --- a/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs +++ b/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs @@ -21,7 +21,7 @@ namespace OptimizelySDK.OptimizelyDecisions public class DefaultDecisionReasons : IDecisionReasons { private List Errors = new List(); - private List Logs = new List(); + private List Infos = new List(); public static IDecisionReasons NewInstance(List options) { @@ -49,14 +49,14 @@ public void AddError(string format, params object[] args) public string AddInfo(string format, params object[] args) { string message = string.Format(format, args); - Logs.Add(message); + Infos.Add(message); return message; } public List ToReport() { List reasons = new List(Errors); - reasons.AddRange(Logs); + reasons.AddRange(Infos); return reasons; } } diff --git a/OptimizelySDK/OptimizelyUserContext.cs b/OptimizelySDK/OptimizelyUserContext.cs index c0b49df7..869571e8 100644 --- a/OptimizelySDK/OptimizelyUserContext.cs +++ b/OptimizelySDK/OptimizelyUserContext.cs @@ -58,8 +58,8 @@ public void SetAttribute(string key, object value) Logger.Log(LogLevel.WARN, "Null attribute key."); } else - { - UserAttributes.Add(key, value); + { + UserAttributes[key] = value; } } From 671aa294a2f9773015e3a436eb5a95e37536fc6d Mon Sep 17 00:00:00 2001 From: muhammadnoman Date: Tue, 24 Nov 2020 22:07:50 +0500 Subject: [PATCH 21/34] added notification type flag removed nullable from reasons and made sure its not null --- .../OptimizelyUserContextTest.cs | 2 +- OptimizelySDK/Bucketing/Bucketer.cs | 10 ++--- OptimizelySDK/Bucketing/DecisionService.cs | 42 +++++++++---------- OptimizelySDK/Optimizely.cs | 5 +-- OptimizelySDK/Utils/DecisionInfoTypes.cs | 1 + OptimizelySDK/Utils/ExperimentUtils.cs | 2 +- 6 files changed, 31 insertions(+), 31 deletions(-) diff --git a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs index 28334888..1238e73e 100644 --- a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs +++ b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs @@ -171,7 +171,7 @@ public void TestDecide() Assert.AreEqual(decision.RuleKey, "test_experiment_multivariate"); Assert.AreEqual(decision.FlagKey, flagKey); Assert.AreEqual(decision.UserContext, user); - Assert.True(decision.Reasons.IsNullOrEmpty()); + Assert.IsNotNull(decision.Reasons); } [Test] diff --git a/OptimizelySDK/Bucketing/Bucketer.cs b/OptimizelySDK/Bucketing/Bucketer.cs index 1765453d..be62727a 100644 --- a/OptimizelySDK/Bucketing/Bucketer.cs +++ b/OptimizelySDK/Bucketing/Bucketer.cs @@ -124,26 +124,26 @@ public virtual Variation Bucket(ProjectConfig config, Experiment experiment, str if (string.IsNullOrEmpty(userExperimentId)) { message = $"User [{userId}] is in no experiment."; - Logger.Log(LogLevel.INFO, reasons?.AddInfo(message)); + Logger.Log(LogLevel.INFO, reasons.AddInfo(message)); return new Variation(); } if (userExperimentId != experiment.Id) { message = $"User [{userId}] is not in experiment [{experiment.Key}] of group [{experiment.GroupId}]."; - Logger.Log(LogLevel.INFO, reasons?.AddInfo(message)); + Logger.Log(LogLevel.INFO, reasons.AddInfo(message)); return new Variation(); } message = $"User [{userId}] is in experiment [{experiment.Key}] of group [{experiment.GroupId}]."; - Logger.Log(LogLevel.INFO, reasons?.AddInfo(message)); + Logger.Log(LogLevel.INFO, reasons.AddInfo(message)); } // Bucket user if not in whitelist and in group (if any). string variationId = FindBucket(bucketingId, userId, experiment.Id, experiment.TrafficAllocation); if (string.IsNullOrEmpty(variationId)) { - Logger.Log(LogLevel.INFO, reasons?.AddInfo($"User [{userId}] is in no variation.")); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"User [{userId}] is in no variation.")); return new Variation(); } @@ -151,7 +151,7 @@ public virtual Variation Bucket(ProjectConfig config, Experiment experiment, str // success! variation = config.GetVariationFromId(experiment.Key, variationId); message = $"User [{userId}] is in variation [{variation.Key}] of experiment [{experiment.Key}]."; - Logger.Log(LogLevel.INFO, reasons?.AddInfo(message)); + Logger.Log(LogLevel.INFO, reasons.AddInfo(message)); return variation; } } diff --git a/OptimizelySDK/Bucketing/DecisionService.cs b/OptimizelySDK/Bucketing/DecisionService.cs index 542d5d22..c0ffd46c 100644 --- a/OptimizelySDK/Bucketing/DecisionService.cs +++ b/OptimizelySDK/Bucketing/DecisionService.cs @@ -133,16 +133,16 @@ public virtual Variation GetVariation(Experiment experiment, } else if (userProfileMap == null) { - Logger.Log(LogLevel.INFO, reasons?.AddInfo("We were unable to get a user profile map from the UserProfileService.")); + Logger.Log(LogLevel.INFO, reasons.AddInfo("We were unable to get a user profile map from the UserProfileService.")); } else { - Logger.Log(LogLevel.ERROR, reasons?.AddInfo("The UserProfileService returned an invalid map.")); + Logger.Log(LogLevel.ERROR, reasons.AddInfo("The UserProfileService returned an invalid map.")); } } catch (Exception exception) { - Logger.Log(LogLevel.ERROR, reasons?.AddInfo(exception.Message)); + Logger.Log(LogLevel.ERROR, reasons.AddInfo(exception.Message)); ErrorHandler.HandleError(new Exceptions.OptimizelyRuntimeException(exception.Message)); } } @@ -156,7 +156,7 @@ public virtual Variation GetVariation(Experiment experiment, if (variation != null && variation.Key != null) { - if (UserProfileService != null) + if (UserProfileService != null && !ignoreUPS) { var bucketerUserProfile = userProfile ?? new UserProfile(userId, new Dictionary()); SaveVariation(experiment, variation, bucketerUserProfile, reasons); @@ -168,7 +168,7 @@ public virtual Variation GetVariation(Experiment experiment, return variation; } - Logger.Log(LogLevel.INFO, reasons?.AddInfo($"User \"{userId}\" does not meet conditions to be in experiment \"{experiment.Key}\".")); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"User \"{userId}\" does not meet conditions to be in experiment \"{experiment.Key}\".")); return null; } @@ -227,7 +227,7 @@ public Variation GetForcedVariation(string experimentKey, string userId, Project // this case is logged in getVariationFromKey if (string.IsNullOrEmpty(variationKey)) return null; - Logger.Log(LogLevel.DEBUG, reasons?.AddInfo($@"Variation ""{variationKey}"" is mapped to experiment ""{experimentKey}"" and user ""{userId}"" in the forced variation map")); + Logger.Log(LogLevel.DEBUG, reasons.AddInfo($@"Variation ""{variationKey}"" is mapped to experiment ""{experimentKey}"" and user ""{userId}"" in the forced variation map")); Variation variation = config.GetVariationFromKey(experimentKey, variationKey); @@ -317,9 +317,9 @@ public Variation GetWhitelistedVariation(Experiment experiment, string userId, I : null; if (forcedVariation != null) - Logger.Log(LogLevel.INFO, reasons?.AddInfo($"User \"{userId}\" is forced in variation \"{forcedVariationKey}\".")); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"User \"{userId}\" is forced in variation \"{forcedVariationKey}\".")); else - Logger.Log(LogLevel.ERROR, reasons?.AddInfo($"Variation \"{forcedVariationKey}\" is not in the datafile. Not activating user \"{userId}\".")); + Logger.Log(LogLevel.ERROR, reasons.AddInfo($"Variation \"{forcedVariationKey}\" is not in the datafile. Not activating user \"{userId}\".")); return forcedVariation; } @@ -342,7 +342,7 @@ public Variation GetStoredVariation(Experiment experiment, UserProfile userProfi if (decision == null) { - Logger.Log(LogLevel.INFO, reasons?.AddInfo($"No previously activated variation of experiment \"{experimentKey}\" for user \"{userProfile.UserId}\" found in user profile.")); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"No previously activated variation of experiment \"{experimentKey}\" for user \"{userProfile.UserId}\" found in user profile.")); return null; } @@ -356,11 +356,11 @@ public Variation GetStoredVariation(Experiment experiment, UserProfile userProfi if (savedVariation == null) { - Logger.Log(LogLevel.INFO, reasons?.AddInfo($"User \"{userProfile.UserId}\" was previously bucketed into variation with ID \"{variationId}\" for experiment \"{experimentId}\", but no matching variation was found for that user. We will re-bucket the user.")); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"User \"{userProfile.UserId}\" was previously bucketed into variation with ID \"{variationId}\" for experiment \"{experimentId}\", but no matching variation was found for that user. We will re-bucket the user.")); return null; } - Logger.Log(LogLevel.INFO, reasons?.AddInfo($"Returning previously activated variation \"{savedVariation.Key}\" of experiment \"{experimentKey}\" for user \"{userProfile.UserId}\" from user profile.")); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"Returning previously activated variation \"{savedVariation.Key}\" of experiment \"{experimentKey}\" for user \"{userProfile.UserId}\" from user profile.")); return savedVariation; } catch (Exception) @@ -407,11 +407,11 @@ public void SaveVariation(Experiment experiment, Variation variation, UserProfil try { UserProfileService.Save(userProfile.ToMap()); - Logger.Log(LogLevel.INFO, reasons?.AddInfo($"Saved variation \"{variation.Id}\" of experiment \"{experiment.Id}\" for user \"{userProfile.UserId}\".")); + Logger.Log(LogLevel.INFO, $"Saved variation \"{variation.Id}\" of experiment \"{experiment.Id}\" for user \"{userProfile.UserId}\"."); } catch (Exception exception) { - Logger.Log(LogLevel.ERROR, reasons?.AddInfo($"Failed to save variation \"{variation.Id}\" of experiment \"{experiment.Id}\" for user \"{userProfile.UserId}\".")); + Logger.Log(LogLevel.ERROR, $"Failed to save variation \"{variation.Id}\" of experiment \"{experiment.Id}\" for user \"{userProfile.UserId}\"."); ErrorHandler.HandleError(new Exceptions.OptimizelyRuntimeException(exception.Message)); } } @@ -441,7 +441,7 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature if (string.IsNullOrEmpty(featureFlag.RolloutId)) { - Logger.Log(LogLevel.INFO, reasons?.AddInfo($"The feature flag \"{featureFlag.Key}\" is not used in a rollout.")); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"The feature flag \"{featureFlag.Key}\" is not used in a rollout.")); return null; } @@ -449,7 +449,7 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature if (string.IsNullOrEmpty(rollout.Id)) { - Logger.Log(LogLevel.ERROR, reasons?.AddInfo($"The rollout with id \"{featureFlag.RolloutId}\" is not found in the datafile for feature flag \"{featureFlag.Key}\"")); + Logger.Log(LogLevel.ERROR, reasons.AddInfo($"The rollout with id \"{featureFlag.RolloutId}\" is not found in the datafile for feature flag \"{featureFlag.Key}\"")); return null; } @@ -526,7 +526,7 @@ public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag feat if (featureFlag.ExperimentIds == null || featureFlag.ExperimentIds.Count == 0) { - Logger.Log(LogLevel.INFO, reasons?.AddInfo($"The feature flag \"{featureFlag.Key}\" is not used in any experiments.")); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"The feature flag \"{featureFlag.Key}\" is not used in any experiments.")); return null; } @@ -541,12 +541,12 @@ public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag feat if (variation != null && !string.IsNullOrEmpty(variation.Id)) { - Logger.Log(LogLevel.INFO, reasons?.AddInfo($"The user \"{userId}\" is bucketed into experiment \"{experiment.Key}\" of feature \"{featureFlag.Key}\".")); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is bucketed into experiment \"{experiment.Key}\" of feature \"{featureFlag.Key}\".")); return new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST); } } - Logger.Log(LogLevel.INFO, reasons?.AddInfo($"The user \"{userId}\" is not bucketed into any of the experiments on the feature \"{featureFlag.Key}\".")); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is not bucketed into any of the experiments on the feature \"{featureFlag.Key}\".")); return null; } @@ -592,11 +592,11 @@ public virtual FeatureDecision GetVariationForFeature(FeatureFlag featureFlag, if (decision != null) { - Logger.Log(LogLevel.INFO, reasons?.AddInfo($"The user \"{userId}\" is bucketed into a rollout for feature flag \"{featureFlag.Key}\".")); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is bucketed into a rollout for feature flag \"{featureFlag.Key}\".")); return decision; } - Logger.Log(LogLevel.INFO, reasons?.AddInfo($"The user \"{userId}\" is not bucketed into a rollout for feature flag \"{featureFlag.Key}\".")); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is not bucketed into a rollout for feature flag \"{featureFlag.Key}\".")); return new FeatureDecision(null, null, FeatureDecision.DECISION_SOURCE_ROLLOUT); } @@ -620,7 +620,7 @@ private string GetBucketingId(string userId, UserAttributes filteredAttributes, } else { - Logger.Log(LogLevel.WARN, reasons?.AddInfo("BucketingID attribute is not a string. Defaulted to userId")); + Logger.Log(LogLevel.WARN, reasons.AddInfo("BucketingID attribute is not a string. Defaulted to userId")); } } diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs index df386559..0b7209ed 100644 --- a/OptimizelySDK/Optimizely.cs +++ b/OptimizelySDK/Optimizely.cs @@ -819,11 +819,10 @@ public OptimizelyDecision Decide(OptimizelyUserContext user, { "variationKey", variationKey }, { "ruleKey", ruleKey }, { "reasons", decisionReasons }, - { "decisionEventDispatched", decisionEventDispatched }, - { "featureEnabled", featureEnabled }, + { "decisionEventDispatched", decisionEventDispatched } }; - NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, DecisionNotificationTypes.FEATURE, userId, + NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, DecisionNotificationTypes.FLAG, userId, userAttributes ?? new UserAttributes(), decisionInfo); return new OptimizelyDecision( diff --git a/OptimizelySDK/Utils/DecisionInfoTypes.cs b/OptimizelySDK/Utils/DecisionInfoTypes.cs index 188522e8..10cac1f7 100644 --- a/OptimizelySDK/Utils/DecisionInfoTypes.cs +++ b/OptimizelySDK/Utils/DecisionInfoTypes.cs @@ -20,6 +20,7 @@ public static class DecisionNotificationTypes { public const string AB_TEST = "ab-test"; public const string FEATURE = "feature"; + public const string FLAG = "flag"; public const string FEATURE_TEST = "feature-test"; public const string FEATURE_VARIABLE = "feature-variable"; public const string ALL_FEATURE_VARIABLE = "all-feature-variables"; diff --git a/OptimizelySDK/Utils/ExperimentUtils.cs b/OptimizelySDK/Utils/ExperimentUtils.cs index 593718b1..b6023480 100644 --- a/OptimizelySDK/Utils/ExperimentUtils.cs +++ b/OptimizelySDK/Utils/ExperimentUtils.cs @@ -94,7 +94,7 @@ public static bool DoesUserMeetAudienceConditions(ProjectConfig config, var result = expConditions.Evaluate(config, userAttributes, logger).GetValueOrDefault(); var resultText = result.ToString().ToUpper(); - logger.Log(LogLevel.INFO, reasons?.AddInfo($@"Audiences for {loggingKeyType} ""{loggingKey}"" collectively evaluated to {resultText}")); + logger.Log(LogLevel.INFO, reasons.AddInfo($@"Audiences for {loggingKeyType} ""{loggingKey}"" collectively evaluated to {resultText}")); return result; } } From 0628e68ef1c5cfa844f8038b7c21c4578caf1793 Mon Sep 17 00:00:00 2001 From: muhammadnoman Date: Wed, 25 Nov 2020 19:27:01 +0500 Subject: [PATCH 22/34] Added additional test cases --- .../OptimizelyUserContextTest.cs | 65 ++++++++++++++++++- OptimizelySDK/OptimizelyUserContext.cs | 2 +- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs index 6b75a1c4..2bfcdc8a 100644 --- a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs +++ b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs @@ -224,7 +224,7 @@ public void DecideWhenConfigIsNull() #region decideAll [Test] - public void DecideAllOneFlag() + public void DecideForKeysWithOneFlag() { var flagKey = "multi_variate_feature"; var flagKeys = new List() { flagKey }; @@ -446,6 +446,69 @@ public void DecideAllEnabledFlagsOnlyDecideOptions() Assert.IsTrue(TestData.CompareObjects(decisions[flagKey1], expDecision1)); } + [Test] + public void DecideAllEnabledFlagsDefaultDecideOptions() + { + var flagKey1 = "string_single_variable_feature"; + var decideOptions = new List() { OptimizelyDecideOption.ENABLED_FLAGS_ONLY }; + + var optimizely = new Optimizely(TestData.Datafile, + EventDispatcherMock.Object, + LoggerMock.Object, + ErrorHandlerMock.Object, + defaultDecideOptions: decideOptions.ToArray()); + + var variablesExpected1 = Optimizely.GetAllFeatureVariables(flagKey1, UserID); + + var user = optimizely.CreateUserContext(UserID); + user.SetAttribute("browser_type", "chrome"); + + var decisions = user.DecideAll(); + + Assert.True(decisions.Count == 1); + + OptimizelyDecision expDecision1 = new OptimizelyDecision( + "control", + true, + variablesExpected1, + "test_experiment_with_feature_rollout", + flagKey1, + user, + new string[0]); + Assert.IsTrue(TestData.CompareObjects(decisions[flagKey1], expDecision1)); + } + + [Test] + public void DecideAllEnabledFlagsDefaultDecideOptionsPlusApiOptions() + { + var flagKey1 = "string_single_variable_feature"; + var decideOptions = new List() { OptimizelyDecideOption.ENABLED_FLAGS_ONLY }; + + var optimizely = new Optimizely(TestData.Datafile, + EventDispatcherMock.Object, + LoggerMock.Object, + ErrorHandlerMock.Object, + defaultDecideOptions: decideOptions.ToArray()); + + var user = optimizely.CreateUserContext(UserID); + user.SetAttribute("browser_type", "chrome"); + decideOptions = new List() { OptimizelyDecideOption.EXCLUDE_VARIABLES }; + + var decisions = user.DecideAll(decideOptions); + + Assert.True(decisions.Count == 1); + var expectedOptlyJson = new Dictionary(); + OptimizelyDecision expDecision1 = new OptimizelyDecision( + "control", + true, + new OptimizelyJSON(dict: expectedOptlyJson, ErrorHandlerMock.Object, LoggerMock.Object), + "test_experiment_with_feature_rollout", + flagKey1, + user, + new string[0]); + Assert.IsTrue(TestData.CompareObjects(decisions[flagKey1], expDecision1)); + } + [Test] public void DecideExcludeVariablesDecideOptions() { diff --git a/OptimizelySDK/OptimizelyUserContext.cs b/OptimizelySDK/OptimizelyUserContext.cs index 07747438..4356b7d0 100644 --- a/OptimizelySDK/OptimizelyUserContext.cs +++ b/OptimizelySDK/OptimizelyUserContext.cs @@ -107,7 +107,7 @@ public Dictionary DecideForKeys(List keys, L /// A dictionary of all decision results, mapped by flag keys. public Dictionary DecideForKeys(List keys) { - return Optimizely.DecideForKeys(this, keys, new List()); + return DecideForKeys(keys, new List()); } /// From a0753a6bb1f3d8f48d6f00bad9bad7b903f52064 Mon Sep 17 00:00:00 2001 From: muhammadnoman Date: Wed, 25 Nov 2020 19:42:14 +0500 Subject: [PATCH 23/34] Added mutex lock in setting attributes value --- OptimizelySDK/OptimizelyUserContext.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/OptimizelySDK/OptimizelyUserContext.cs b/OptimizelySDK/OptimizelyUserContext.cs index 869571e8..e273c7c7 100644 --- a/OptimizelySDK/OptimizelyUserContext.cs +++ b/OptimizelySDK/OptimizelyUserContext.cs @@ -30,6 +30,7 @@ public class OptimizelyUserContext { private ILogger Logger; private IErrorHandler ErrorHandler; + private static object mutex = new object(); // userID for Optimizely user context public string UserId { get; } // user attributes for Optimizely user context. @@ -59,7 +60,10 @@ public void SetAttribute(string key, object value) } else { - UserAttributes[key] = value; + lock(mutex) + { + UserAttributes[key] = value; + } } } From 0e9faa18afea6818d6933a037075aaf17b6f4734 Mon Sep 17 00:00:00 2001 From: muhammadnoman Date: Thu, 26 Nov 2020 13:21:23 +0500 Subject: [PATCH 24/34] replaced list from array of decideOptions --- OptimizelySDK.Tests/DecisionServiceTest.cs | 28 +++++++++---------- .../OptimizelyDecisionTest.cs | 2 +- OptimizelySDK/Bucketing/DecisionService.cs | 12 ++++---- OptimizelySDK/Optimizely.cs | 12 ++++---- .../DefaultDecisionReasons.cs | 5 ++-- OptimizelySDK/OptimizelyUserContext.cs | 8 +++--- 6 files changed, 35 insertions(+), 32 deletions(-) diff --git a/OptimizelySDK.Tests/DecisionServiceTest.cs b/OptimizelySDK.Tests/DecisionServiceTest.cs index 3c83b3db..6513e6e5 100644 --- a/OptimizelySDK.Tests/DecisionServiceTest.cs +++ b/OptimizelySDK.Tests/DecisionServiceTest.cs @@ -469,7 +469,7 @@ public void TestGetVariationWithBucketingId() public void TestGetVariationForFeatureExperimentGivenNullExperimentIds() { var featureFlag = ProjectConfig.GetFeatureFlagFromKey("empty_feature"); - var decision = DecisionService.GetVariationForFeatureExperiment(featureFlag, GenericUserId, new UserAttributes() { }, ProjectConfig, new List(), DefaultDecisionReasons.NewInstance()); + var decision = DecisionService.GetVariationForFeatureExperiment(featureFlag, GenericUserId, new UserAttributes() { }, ProjectConfig, new OptimizelyDecideOption[]{}, DefaultDecisionReasons.NewInstance()); Assert.IsNull(decision); @@ -488,7 +488,7 @@ public void TestGetVariationForFeatureExperimentGivenExperimentNotInDataFile() ExperimentIds = new List { "29039203" } }; - var decision = DecisionService.GetVariationForFeatureExperiment(featureFlag, GenericUserId, new UserAttributes() { }, ProjectConfig, new List(), DefaultDecisionReasons.NewInstance()); + var decision = DecisionService.GetVariationForFeatureExperiment(featureFlag, GenericUserId, new UserAttributes() { }, ProjectConfig, new OptimizelyDecideOption[] { }, DefaultDecisionReasons.NewInstance()); Assert.IsNull(decision); LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Experiment ID \"29039203\" is not in datafile.")); @@ -503,7 +503,7 @@ public void TestGetVariationForFeatureExperimentGivenNonMutexGroupAndUserNotBuck DecisionServiceMock.Setup(ds => ds.GetVariation(multiVariateExp, "user1", ProjectConfig, null)).Returns(null); var featureFlag = ProjectConfig.GetFeatureFlagFromKey("multi_variate_feature"); - var decision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", new UserAttributes(), ProjectConfig, new List(), DefaultDecisionReasons.NewInstance()); + var decision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", new UserAttributes(), ProjectConfig, new OptimizelyDecideOption[] { }, DefaultDecisionReasons.NewInstance()); Assert.IsNull(decision); LoggerMock.Verify(l => l.Log(LogLevel.INFO, "The user \"user1\" is not bucketed into any of the experiments on the feature \"multi_variate_feature\".")); @@ -519,10 +519,10 @@ public void TestGetVariationForFeatureExperimentGivenNonMutexGroupAndUserIsBucke var userAttributes = new UserAttributes(); DecisionServiceMock.Setup(ds => ds.GetVariation(ProjectConfig.GetExperimentFromKey("test_experiment_multivariate"), - "user1", ProjectConfig, userAttributes, It.IsAny>(), It.IsAny())).Returns(variation); + "user1", ProjectConfig, userAttributes, It.IsAny (), It.IsAny())).Returns(variation); var featureFlag = ProjectConfig.GetFeatureFlagFromKey("multi_variate_feature"); - var decision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig, new List(), DefaultDecisionReasons.NewInstance()); + var decision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig, new OptimizelyDecideOption[] { }, DefaultDecisionReasons.NewInstance()); Assert.IsTrue(TestData.CompareObjects(expectedDecision, decision)); @@ -542,7 +542,7 @@ public void TestGetVariationForFeatureExperimentGivenMutexGroupAndUserIsBucketed userAttributes)).Returns(variation); var featureFlag = ProjectConfig.GetFeatureFlagFromKey("boolean_feature"); - var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig, new List(), DefaultDecisionReasons.NewInstance()); + var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig, new OptimizelyDecideOption[] { }, DefaultDecisionReasons.NewInstance()); Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision)); @@ -554,11 +554,11 @@ public void TestGetVariationForFeatureExperimentGivenMutexGroupAndUserIsBucketed public void TestGetVariationForFeatureExperimentGivenMutexGroupAndUserNotBucketed() { var mutexExperiment = ProjectConfig.GetExperimentFromKey("group_experiment_1"); - DecisionServiceMock.Setup(ds => ds.GetVariation(It.IsAny(), It.IsAny(), ProjectConfig, It.IsAny(), It.IsAny>(), It.IsAny())). + DecisionServiceMock.Setup(ds => ds.GetVariation(It.IsAny(), It.IsAny(), ProjectConfig, It.IsAny(), It.IsAny (), It.IsAny())). Returns(null); var featureFlag = ProjectConfig.GetFeatureFlagFromKey("boolean_feature"); - var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", new UserAttributes(), ProjectConfig, new List(), DefaultDecisionReasons.NewInstance()); + var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", new UserAttributes(), ProjectConfig, new OptimizelyDecideOption[] { }, DefaultDecisionReasons.NewInstance()); Assert.IsNull(actualDecision); @@ -601,7 +601,7 @@ public void TestGetVariationForFeatureRolloutWhenRolloutIsNotInDataFile() Variables = featureFlag.Variables }; - DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny(), It.IsAny(), It.IsAny(), ProjectConfig, new List(), DefaultDecisionReasons.NewInstance())).Returns(null); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny(), It.IsAny(), It.IsAny(), ProjectConfig, new OptimizelyDecideOption[] { }, DefaultDecisionReasons.NewInstance())).Returns(null); var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureRollout(featureFlag, "user1", new UserAttributes(), ProjectConfig, DefaultDecisionReasons.NewInstance()); Assert.IsNull(actualDecision); @@ -808,7 +808,7 @@ public void TestGetVariationForFeatureWhenTheUserIsBucketedIntoFeatureExperiment var expectedDecision = new FeatureDecision(expectedExperiment, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST); DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny(), It.IsAny(), - It.IsAny(), ProjectConfig, It.IsAny>(), It.IsAny())).Returns(expectedDecision); + It.IsAny(), ProjectConfig, It.IsAny(), It.IsAny())).Returns(expectedDecision); var actualDecision = DecisionServiceMock.Object.GetVariationForFeature(featureFlag, "user1", ProjectConfig, new UserAttributes()); Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision)); @@ -827,7 +827,7 @@ public void TestGetVariationForFeatureWhenTheUserIsNotBucketedIntoFeatureExperim var expectedDecision = new FeatureDecision(expectedExperiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT); DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny(), It.IsAny(), - It.IsAny(), ProjectConfig, It.IsAny>(), It.IsAny())).Returns(null); + It.IsAny(), ProjectConfig, It.IsAny(), It.IsAny())).Returns(null); DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureRollout(It.IsAny(), It.IsAny(), It.IsAny(), ProjectConfig, It.IsAny())).Returns(expectedDecision); @@ -845,7 +845,7 @@ public void TestGetVariationForFeatureWhenTheUserIsNeitherBucketedIntoFeatureExp var featureFlag = ProjectConfig.GetFeatureFlagFromKey("string_single_variable_feature"); var expectedDecision = new FeatureDecision(null, null, FeatureDecision.DECISION_SOURCE_ROLLOUT); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny(), It.IsAny(), It.IsAny(), ProjectConfig, new List(), DefaultDecisionReasons.NewInstance())).Returns(null); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny(), It.IsAny(), It.IsAny(), ProjectConfig, new OptimizelyDecideOption[] { }, DefaultDecisionReasons.NewInstance())).Returns(null); DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureRollout(It.IsAny(), It.IsAny(), It.IsAny(), ProjectConfig, DefaultDecisionReasons.NewInstance())).Returns(null); var actualDecision = DecisionServiceMock.Object.GetVariationForFeature(featureFlag, "user1", ProjectConfig, new UserAttributes()); @@ -867,8 +867,8 @@ public void TestGetVariationForFeatureWhenTheUserIsBuckedtedInBothExperimentAndR { "browser_type", "chrome" } }; - DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, "user1", ProjectConfig, userAttributes, It.IsAny>(), It.IsAny())).Returns(variation); - var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig, new List(), DefaultDecisionReasons.NewInstance()); + DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, "user1", ProjectConfig, userAttributes, It.IsAny(), It.IsAny())).Returns(variation); + var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig, new OptimizelyDecideOption[] { }, DefaultDecisionReasons.NewInstance()); // The user is bucketed into feature experiment's variation. Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision)); diff --git a/OptimizelySDK.Tests/OptimizelyDecisions/OptimizelyDecisionTest.cs b/OptimizelySDK.Tests/OptimizelyDecisions/OptimizelyDecisionTest.cs index bf8f5f17..d5248e7c 100644 --- a/OptimizelySDK.Tests/OptimizelyDecisions/OptimizelyDecisionTest.cs +++ b/OptimizelySDK.Tests/OptimizelyDecisions/OptimizelyDecisionTest.cs @@ -84,7 +84,7 @@ public void TestNewDecision() [Test] public void TestNewDecisionReasonWithIncludeReasons() { - var decisionReasons = DefaultDecisionReasons.NewInstance(new List() { OptimizelyDecideOption.INCLUDE_REASONS }); + var decisionReasons = DefaultDecisionReasons.NewInstance(new OptimizelyDecideOption[] { OptimizelyDecideOption.INCLUDE_REASONS }); decisionReasons.AddError(DecisionMessage.Reason(DecisionMessage.FLAG_KEY_INVALID, "invalid_key")); Assert.AreEqual(decisionReasons.ToReport()[0], "No flag was found for key \"invalid_key\"."); diff --git a/OptimizelySDK/Bucketing/DecisionService.cs b/OptimizelySDK/Bucketing/DecisionService.cs index c0ffd46c..68564470 100644 --- a/OptimizelySDK/Bucketing/DecisionService.cs +++ b/OptimizelySDK/Bucketing/DecisionService.cs @@ -89,7 +89,7 @@ public virtual Variation GetVariation(Experiment experiment, ProjectConfig config, UserAttributes filteredAttributes) { - return GetVariation(experiment, userId, config, filteredAttributes, new List(), DefaultDecisionReasons.NewInstance()); + return GetVariation(experiment, userId, config, filteredAttributes, new OptimizelyDecideOption[] { }, DefaultDecisionReasons.NewInstance()); } /// @@ -103,7 +103,7 @@ public virtual Variation GetVariation(Experiment experiment, string userId, ProjectConfig config, UserAttributes filteredAttributes, - List options, + OptimizelyDecideOption[] options, IDecisionReasons reasons) { if (!ExperimentUtils.IsExperimentActive(experiment, Logger)) return null; @@ -117,7 +117,7 @@ public virtual Variation GetVariation(Experiment experiment, if (variation != null) return variation; // fetch the user profile map from the user profile service - var ignoreUPS = options.Contains(OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE); + var ignoreUPS = Array.Exists(options, option => option == OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE); UserProfile userProfile = null; if (UserProfileService != null && !ignoreUPS) @@ -515,7 +515,7 @@ public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag feat string userId, UserAttributes filteredAttributes, ProjectConfig config, - List options, + OptimizelyDecideOption[] options, IDecisionReasons reasons) { if (featureFlag == null) @@ -560,7 +560,7 @@ public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag feat /// successfully bucketed. public virtual FeatureDecision GetVariationForFeature(FeatureFlag featureFlag, string userId, ProjectConfig config, UserAttributes filteredAttributes) { - return GetVariationForFeature(featureFlag, userId, config, filteredAttributes, new List(), DefaultDecisionReasons.NewInstance()); + return GetVariationForFeature(featureFlag, userId, config, filteredAttributes, new OptimizelyDecideOption[] { }, DefaultDecisionReasons.NewInstance()); } /// @@ -578,7 +578,7 @@ public virtual FeatureDecision GetVariationForFeature(FeatureFlag featureFlag, string userId, ProjectConfig config, UserAttributes filteredAttributes, - List options, + OptimizelyDecideOption[] options, IDecisionReasons reasons) { // Check if the feature flag has an experiment and the user is bucketed into that experiment. diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs index 0b7209ed..e36a6ec4 100644 --- a/OptimizelySDK/Optimizely.cs +++ b/OptimizelySDK/Optimizely.cs @@ -30,6 +30,7 @@ using OptimizelySDK.OptlyConfig; using System.Net; using OptimizelySDK.OptimizelyDecisions; +using System.Linq; namespace OptimizelySDK { @@ -721,9 +722,9 @@ public OptimizelyUserContext CreateUserContext(string userId, /// A flag key for which a decision will be made. /// A list of options for decision-making. /// A decision result. - public OptimizelyDecision Decide(OptimizelyUserContext user, + internal OptimizelyDecision Decide(OptimizelyUserContext user, string key, - List options) + OptimizelyDecideOption[] options) { var config = ProjectConfigManager?.GetConfig(); @@ -835,12 +836,13 @@ public OptimizelyDecision Decide(OptimizelyUserContext user, reasonsToReport.ToArray()); } - private List GetAllOptions(List options) + private OptimizelyDecideOption[] GetAllOptions(OptimizelyDecideOption[] options) { - var copiedOptions = new List(DefaultDecideOptions); + OptimizelyDecideOption[] copiedOptions = new OptimizelyDecideOption[DefaultDecideOptions.Length]; + Array.Copy(DefaultDecideOptions, copiedOptions, DefaultDecideOptions.Length); if (options != null) { - copiedOptions.AddRange(options); + copiedOptions.Concat(options); } return copiedOptions; } diff --git a/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs b/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs index 0ded3470..e02aa82d 100644 --- a/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs +++ b/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs @@ -14,6 +14,7 @@ * limitations under the License. */ +using System; using System.Collections.Generic; namespace OptimizelySDK.OptimizelyDecisions @@ -23,9 +24,9 @@ public class DefaultDecisionReasons : IDecisionReasons private List Errors = new List(); private List Infos = new List(); - public static IDecisionReasons NewInstance(List options) + public static IDecisionReasons NewInstance(OptimizelyDecideOption[] options) { - if (options != null && options.Contains(OptimizelyDecideOption.INCLUDE_REASONS)) + if (options != null && Array.Exists(options, option => option == OptimizelyDecideOption.INCLUDE_REASONS)) { return new DefaultDecisionReasons(); } diff --git a/OptimizelySDK/OptimizelyUserContext.cs b/OptimizelySDK/OptimizelyUserContext.cs index 32d035af..8f76c7a7 100644 --- a/OptimizelySDK/OptimizelyUserContext.cs +++ b/OptimizelySDK/OptimizelyUserContext.cs @@ -73,7 +73,7 @@ public void SetAttribute(string key, object value) /// A decision result. public OptimizelyDecision Decide(string key) { - return Decide(key, new List()); + return Decide(key, new OptimizelyDecideOption[] { }); } /// @@ -86,7 +86,7 @@ public OptimizelyDecision Decide(string key) /// A list of options for decision-making. /// A decision result. public OptimizelyDecision Decide(string key, - List options) + OptimizelyDecideOption[] options) { return Optimizely.Decide(this, key, options); } @@ -107,7 +107,7 @@ public Dictionary DecideForKeys(List keys) /// A dictionary of all decision results, mapped by flag keys. public Dictionary DecideAll() { - return DecideAll(new List()); + return DecideAll(new OptimizelyDecideOption[] { }); } @@ -116,7 +116,7 @@ public Dictionary DecideAll() /// /// A list of options for decision-making. /// All decision results mapped by flag keys. - public Dictionary DecideAll(List options) + public Dictionary DecideAll(OptimizelyDecideOption[] options) { throw new NotImplementedException(); } From 5d0028fb1f92acfbfe962e07451ce9504c3de37c Mon Sep 17 00:00:00 2001 From: muhammadnoman Date: Thu, 26 Nov 2020 19:00:12 +0500 Subject: [PATCH 25/34] changed decide decideall and decide for keys from public to internal --- OptimizelySDK/Optimizely.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs index 73706931..2abc0a34 100644 --- a/OptimizelySDK/Optimizely.cs +++ b/OptimizelySDK/Optimizely.cs @@ -846,7 +846,7 @@ internal OptimizelyDecision Decide(OptimizelyUserContext user, reasonsToReport.ToArray()); } - public Dictionary DecideAll(OptimizelyUserContext user, + internal Dictionary DecideAll(OptimizelyUserContext user, OptimizelyDecideOption[] options) { var decisionMap = new Dictionary(); @@ -868,7 +868,7 @@ public Dictionary DecideAll(OptimizelyUserContext us } - public Dictionary DecideForKeys(OptimizelyUserContext user, + internal Dictionary DecideForKeys(OptimizelyUserContext user, List keys, OptimizelyDecideOption[] options) { From 320ddc40819f00bf80f4ec9000d24d05ca237283 Mon Sep 17 00:00:00 2001 From: muhammadnoman Date: Fri, 27 Nov 2020 20:16:15 +0500 Subject: [PATCH 26/34] Resolved failing scenario --- OptimizelySDK.Tests/OptimizelyUserContextTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs index a18b7c0f..a3d65cbb 100644 --- a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs +++ b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs @@ -501,11 +501,11 @@ public void DecideAllEnabledFlagsDefaultDecideOptionsPlusApiOptions() OptimizelyDecision expDecision1 = new OptimizelyDecision( "control", true, - new OptimizelyJSON(dict: expectedOptlyJson, ErrorHandlerMock.Object, LoggerMock.Object), + new OptimizelyJSON(expectedOptlyJson, ErrorHandlerMock.Object, LoggerMock.Object), "test_experiment_with_feature_rollout", flagKey1, user, - new string[0]); + new string[] { }); Assert.IsTrue(TestData.CompareObjects(decisions[flagKey1], expDecision1)); } From 451e001418e0b88735eb66aaf7acc04f6fcaa9a6 Mon Sep 17 00:00:00 2001 From: muhammadnoman Date: Mon, 30 Nov 2020 20:55:44 +0500 Subject: [PATCH 27/34] Added unit test that byUPS will not call save in userprofile even once --- .../OptimizelyUserContextTest.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs index a3d65cbb..92e64f93 100644 --- a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs +++ b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs @@ -688,6 +688,25 @@ public void TestDecideOptionsByPassUPS() variationUserProfile = user.Decide(flagKey, decideOptions); Assert.AreEqual(variationKey, variationUserProfile.VariationKey); } + + [Test] + public void TestDecideOptionsByPassUPSNeverCallsSaveVariation() + { + var userProfileServiceMock = new Mock(); + var flagKey = "string_single_variable_feature"; + + var userId = "testUser3"; + var variationKey = "control"; + + var optimizely = new Optimizely(TestData.Datafile, EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object, userProfileServiceMock.Object); + var user = optimizely.CreateUserContext(userId); + + var decideOptions = new OptimizelyDecideOption[] { OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE }; + var variationUserProfile = user.Decide(flagKey, decideOptions); + userProfileServiceMock.Verify(l => l.Save(It.IsAny>()), Times.Never); + + Assert.AreEqual(variationKey, variationUserProfile.VariationKey); + } #endregion #region TrackEvent From 54e9e747ed33462f7d142c8d1d2f6be529dd7891 Mon Sep 17 00:00:00 2001 From: Muhammad Noman Date: Thu, 3 Dec 2020 03:59:25 +0500 Subject: [PATCH 28/34] Removed unnecessary methods from errorDecisionsReasons removed static from mutex object --- .../OptimizelyDecisions/DefaultDecisionReasons.cs | 6 +++--- .../OptimizelyDecisions/ErrorsDecisionReasons.cs | 15 ++------------- OptimizelySDK/OptimizelyUserContext.cs | 2 +- 3 files changed, 6 insertions(+), 17 deletions(-) diff --git a/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs b/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs index 0ded3470..2bce30c2 100644 --- a/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs +++ b/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs @@ -20,7 +20,7 @@ namespace OptimizelySDK.OptimizelyDecisions { public class DefaultDecisionReasons : IDecisionReasons { - private List Errors = new List(); + protected List Errors = new List(); private List Infos = new List(); public static IDecisionReasons NewInstance(List options) @@ -46,14 +46,14 @@ public void AddError(string format, params object[] args) Errors.Add(message); } - public string AddInfo(string format, params object[] args) + public virtual string AddInfo(string format, params object[] args) { string message = string.Format(format, args); Infos.Add(message); return message; } - public List ToReport() + public virtual List ToReport() { List reasons = new List(Errors); reasons.AddRange(Infos); diff --git a/OptimizelySDK/OptimizelyDecisions/ErrorsDecisionReasons.cs b/OptimizelySDK/OptimizelyDecisions/ErrorsDecisionReasons.cs index 56e98d7e..2c0b9348 100644 --- a/OptimizelySDK/OptimizelyDecisions/ErrorsDecisionReasons.cs +++ b/OptimizelySDK/OptimizelyDecisions/ErrorsDecisionReasons.cs @@ -21,25 +21,14 @@ namespace OptimizelySDK.OptimizelyDecisions /// /// NewErrorDecision returns a decision with errors only /// - public class ErrorsDecisionReasons : IDecisionReasons + public class ErrorsDecisionReasons : DefaultDecisionReasons { - private readonly List errors = new List(); - public void AddError(string format, params object[] args) - { - string message = string.Format(format, args); - errors.Add(message); - } - - public string AddInfo(string format, params object[] args) + public override string AddInfo(string format, params object[] args) { // skip tracking and pass-through reasons other than critical errors. return string.Format(format, args); } - public List ToReport() - { - return errors; - } } } diff --git a/OptimizelySDK/OptimizelyUserContext.cs b/OptimizelySDK/OptimizelyUserContext.cs index e273c7c7..ba53f747 100644 --- a/OptimizelySDK/OptimizelyUserContext.cs +++ b/OptimizelySDK/OptimizelyUserContext.cs @@ -30,7 +30,7 @@ public class OptimizelyUserContext { private ILogger Logger; private IErrorHandler ErrorHandler; - private static object mutex = new object(); + private object mutex = new object(); // userID for Optimizely user context public string UserId { get; } // user attributes for Optimizely user context. From b824b990beb840c61d8c549bfbd7a518122a85fc Mon Sep 17 00:00:00 2001 From: Muhammad Noman Date: Thu, 3 Dec 2020 04:19:18 +0500 Subject: [PATCH 29/34] Added ignoreUps check first in decision service added check of decisionReason in test that it should be zero if no error log occurs --- OptimizelySDK.Tests/BucketerTest.cs | 36 +++++++++++-------- .../OptimizelyUserContextTest.cs | 2 -- OptimizelySDK/Bucketing/DecisionService.cs | 2 +- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/OptimizelySDK.Tests/BucketerTest.cs b/OptimizelySDK.Tests/BucketerTest.cs index 1bf4548b..8aa94d0b 100644 --- a/OptimizelySDK.Tests/BucketerTest.cs +++ b/OptimizelySDK.Tests/BucketerTest.cs @@ -28,6 +28,7 @@ public class BucketerTest { private Mock LoggerMock; private ProjectConfig Config; + private IDecisionReasons DecisionReasons; private const string TestUserId = "testUserId"; public string TestBucketingIdControl { get; } = "testBucketingIdControl!"; // generates bucketing number 3741 public string TestBucketingIdVariation { get; } = "123456789'"; // generates bucketing number 4567 @@ -60,6 +61,7 @@ public override string ToString() public void Initialize() { LoggerMock = new Mock(); + DecisionReasons = DefaultDecisionReasons.NewInstance(); Config = DatafileProjectConfig.Create(TestData.Datafile, LoggerMock.Object, new ErrorHandler.NoOpErrorHandler()); } @@ -96,18 +98,16 @@ public void TestBucketValidExperimentNotInGroup() { TestBucketer bucketer = new TestBucketer(LoggerMock.Object); bucketer.SetBucketValues(new[] { 3000, 7000, 9000 }); - // control Assert.AreEqual(new Variation { Id = "7722370027", Key = "control" }, - bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance())); + bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId, DecisionReasons)); LoggerMock.Verify(l => l.Log(It.IsAny(), It.IsAny()), Times.Exactly(2)); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [3000] to user [testUserId] with bucketing ID [testBucketingIdControl!].")); LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [testUserId] is in variation [control] of experiment [test_experiment].")); - // variation Assert.AreEqual(new Variation { Id = "7721010009", Key = "variation" }, - bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance())); + bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId, DecisionReasons)); LoggerMock.Verify(l => l.Log(It.IsAny(), It.IsAny()), Times.Exactly(4)); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [7000] to user [testUserId] with bucketing ID [testBucketingIdControl!].")); @@ -115,23 +115,24 @@ public void TestBucketValidExperimentNotInGroup() // no variation Assert.AreEqual(new Variation { }, - bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance())); + bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId, DecisionReasons)); LoggerMock.Verify(l => l.Log(It.IsAny(), It.IsAny()), Times.Exactly(6)); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [9000] to user [testUserId] with bucketing ID [testBucketingIdControl!].")); LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [testUserId] is in no variation.")); + Assert.AreEqual(DecisionReasons.ToReport().Count, 0); } [Test] public void TestBucketValidExperimentInGroup() { TestBucketer bucketer = new TestBucketer(LoggerMock.Object); - + // group_experiment_1 (20% experiment) // variation 1 bucketer.SetBucketValues(new[] { 1000, 4000 }); Assert.AreEqual(new Variation { Id = "7722260071", Key = "group_exp_1_var_1" }, - bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance())); + bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId, DecisionReasons)); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [1000] to user [testUserId] with bucketing ID [testBucketingIdControl!].")); LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [testUserId] is in experiment [group_experiment_1] of group [7722400015].")); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [4000] to user [testUserId] with bucketing ID [testBucketingIdControl!].")); @@ -140,7 +141,7 @@ public void TestBucketValidExperimentInGroup() // variation 2 bucketer.SetBucketValues(new[] { 1500, 7000 }); Assert.AreEqual(new Variation { Id = "7722360022", Key = "group_exp_1_var_2" }, - bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance())); + bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId, DecisionReasons)); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [1500] to user [testUserId] with bucketing ID [testBucketingIdControl!].")); LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [testUserId] is in experiment [group_experiment_1] of group [7722400015].")); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [7000] to user [testUserId] with bucketing ID [testBucketingIdControl!].")); @@ -149,20 +150,21 @@ public void TestBucketValidExperimentInGroup() // User not in experiment bucketer.SetBucketValues(new[] { 5000, 7000 }); Assert.AreEqual(new Variation { }, - bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance())); + bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId, DecisionReasons)); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [5000] to user [testUserId] with bucketing ID [testBucketingIdControl!].")); LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [testUserId] is not in experiment [group_experiment_1] of group [7722400015].")); LoggerMock.Verify(l => l.Log(It.IsAny(), It.IsAny()), Times.Exactly(10)); + Assert.AreEqual(DecisionReasons.ToReport().Count, 0); } [Test] public void TestBucketInvalidExperiment() { var bucketer = new Bucketer(LoggerMock.Object); - + Assert.AreEqual(new Variation { }, - bucketer.Bucket(Config, new Experiment(), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance())); + bucketer.Bucket(Config, new Experiment(), TestBucketingIdControl, TestUserId, DecisionReasons)); LoggerMock.Verify(l => l.Log(It.IsAny(), It.IsAny()), Times.Never); } @@ -177,7 +179,8 @@ public void TestBucketWithBucketingId() // make sure that the bucketing ID is used for the variation bucketing and not the user ID Assert.AreEqual(expectedVariation, - bucketer.Bucket(Config, experiment, TestBucketingIdControl, TestUserIdBucketsToVariation, DefaultDecisionReasons.NewInstance())); + bucketer.Bucket(Config, experiment, TestBucketingIdControl, TestUserIdBucketsToVariation, DecisionReasons)); + Assert.AreEqual(DecisionReasons.ToReport().Count, 0); } // Test for invalid experiment keys, null variation should be returned @@ -188,7 +191,8 @@ public void TestBucketVariationInvalidExperimentsWithBucketingId() var expectedVariation = new Variation(); Assert.AreEqual(expectedVariation, - bucketer.Bucket(Config, Config.GetExperimentFromKey("invalid_experiment"), TestBucketingIdVariation, TestUserId, DefaultDecisionReasons.NewInstance())); + bucketer.Bucket(Config, Config.GetExperimentFromKey("invalid_experiment"), TestBucketingIdVariation, TestUserId, DecisionReasons)); + Assert.AreEqual(DecisionReasons.ToReport().Count, 0); } // Make sure that the bucketing ID is used to bucket the user into a group and not the user ID @@ -201,7 +205,8 @@ public void TestBucketVariationGroupedExperimentsWithBucketingId() Assert.AreEqual(expectedGroupVariation, bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_2"), - TestBucketingIdGroupExp2Var2, TestUserIdBucketsToNoGroup, DefaultDecisionReasons.NewInstance())); + TestBucketingIdGroupExp2Var2, TestUserIdBucketsToNoGroup, DecisionReasons)); + Assert.AreEqual(DecisionReasons.ToReport().Count, 0); } // Make sure that user gets bucketed into the rollout rule. @@ -214,7 +219,8 @@ public void TestBucketRolloutRule() var expectedVariation = Config.GetVariationFromId(rolloutRule.Key, "177773"); Assert.True(TestData.CompareObjects(expectedVariation, - bucketer.Bucket(Config, rolloutRule, "testBucketingId", TestUserId, DefaultDecisionReasons.NewInstance()))); + bucketer.Bucket(Config, rolloutRule, "testBucketingId", TestUserId, DecisionReasons))); + Assert.AreEqual(DecisionReasons.ToReport().Count, 0); } } } diff --git a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs index 1238e73e..cd976a54 100644 --- a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs +++ b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs @@ -15,7 +15,6 @@ * limitations under the License. */ -using Castle.Core.Internal; using Moq; using NUnit.Framework; using OptimizelySDK.Entity; @@ -24,7 +23,6 @@ using OptimizelySDK.Logger; using OptimizelySDK.OptimizelyDecisions; using System; -using System.Collections.Generic; namespace OptimizelySDK.Tests { diff --git a/OptimizelySDK/Bucketing/DecisionService.cs b/OptimizelySDK/Bucketing/DecisionService.cs index 68564470..8b6963b4 100644 --- a/OptimizelySDK/Bucketing/DecisionService.cs +++ b/OptimizelySDK/Bucketing/DecisionService.cs @@ -120,7 +120,7 @@ public virtual Variation GetVariation(Experiment experiment, var ignoreUPS = Array.Exists(options, option => option == OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE); UserProfile userProfile = null; - if (UserProfileService != null && !ignoreUPS) + if (!ignoreUPS && UserProfileService != null) { try { From 7eb41c7517fa184c4515bd8131b0da8ec5d73dcd Mon Sep 17 00:00:00 2001 From: Muhammad Noman Date: Thu, 3 Dec 2020 04:29:18 +0500 Subject: [PATCH 30/34] Converted a loop to lambda --- OptimizelySDK.Tests/OptimizelyUserContextTest.cs | 2 ++ OptimizelySDK/Optimizely.cs | 7 ++----- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs index 4dd3d261..92e64f93 100644 --- a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs +++ b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs @@ -15,6 +15,7 @@ * limitations under the License. */ +using Castle.Core.Internal; using Moq; using NUnit.Framework; using OptimizelySDK.Bucketing; @@ -29,6 +30,7 @@ using OptimizelySDK.Tests.NotificationTests; using OptimizelySDK.Utils; using System; +using System.Collections.Generic; namespace OptimizelySDK.Tests { diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs index 2abc0a34..37370102 100644 --- a/OptimizelySDK/Optimizely.cs +++ b/OptimizelySDK/Optimizely.cs @@ -859,11 +859,8 @@ internal Dictionary DecideAll(OptimizelyUserContext } var allFlags = projectConfig.FeatureFlags; - var allFlagKeys = new List(); - for (int i = 0; i < allFlags.Length; i++) - { - allFlagKeys.Add(allFlags[i].Key); - } + var allFlagKeys = allFlags.Select(v => v.Key).ToList(); + return DecideForKeys(user, allFlagKeys, options); } From eda27436b6605bda24967f4ed3d597f38ed066a3 Mon Sep 17 00:00:00 2001 From: Muhammad Noman Date: Mon, 7 Dec 2020 17:39:47 +0500 Subject: [PATCH 31/34] Nit Fix --- OptimizelySDK.Tests/OptimizelyUserContextTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs index cd976a54..08fb9a34 100644 --- a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs +++ b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs @@ -169,7 +169,7 @@ public void TestDecide() Assert.AreEqual(decision.RuleKey, "test_experiment_multivariate"); Assert.AreEqual(decision.FlagKey, flagKey); Assert.AreEqual(decision.UserContext, user); - Assert.IsNotNull(decision.Reasons); + Assert.AreEqual(decision.Reasons.Length, 0); } [Test] From f97e30290093392c5b3f32f0bc8723b3ffbeb83b Mon Sep 17 00:00:00 2001 From: muhammadnoman Date: Mon, 7 Dec 2020 22:03:27 +0500 Subject: [PATCH 32/34] Changed decide keys to array --- OptimizelySDK.Tests/OptimizelyUserContextTest.cs | 4 ++-- OptimizelySDK/Optimizely.cs | 6 +++--- OptimizelySDK/OptimizelyUserContext.cs | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs index 9a0c4357..07eee639 100644 --- a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs +++ b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs @@ -227,7 +227,7 @@ public void DecideWhenConfigIsNull() public void DecideForKeysWithOneFlag() { var flagKey = "multi_variate_feature"; - var flagKeys = new List() { flagKey }; + var flagKeys = new string[] { flagKey }; var variablesExpected = Optimizely.GetAllFeatureVariables(flagKey, UserID); @@ -255,7 +255,7 @@ public void DecideAllTwoFlag() { var flagKey1 = "multi_variate_feature"; var flagKey2 = "string_single_variable_feature"; - var flagKeys = new List() { flagKey1, flagKey2 }; + var flagKeys = new string[] { flagKey1, flagKey2 }; var variablesExpected1 = Optimizely.GetAllFeatureVariables(flagKey1, UserID); var variablesExpected2 = Optimizely.GetAllFeatureVariables(flagKey2, UserID); diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs index 37370102..63e07910 100644 --- a/OptimizelySDK/Optimizely.cs +++ b/OptimizelySDK/Optimizely.cs @@ -859,14 +859,14 @@ internal Dictionary DecideAll(OptimizelyUserContext } var allFlags = projectConfig.FeatureFlags; - var allFlagKeys = allFlags.Select(v => v.Key).ToList(); + var allFlagKeys = allFlags.Select(v => v.Key).ToArray(); return DecideForKeys(user, allFlagKeys, options); } internal Dictionary DecideForKeys(OptimizelyUserContext user, - List keys, + string[] keys, OptimizelyDecideOption[] options) { var decisionDictionary = new Dictionary(); @@ -878,7 +878,7 @@ internal Dictionary DecideForKeys(OptimizelyUserCont return decisionDictionary; } - if (keys.Count == 0) + if (keys.Length == 0) { return decisionDictionary; } diff --git a/OptimizelySDK/OptimizelyUserContext.cs b/OptimizelySDK/OptimizelyUserContext.cs index dcf7b269..7aca6751 100644 --- a/OptimizelySDK/OptimizelyUserContext.cs +++ b/OptimizelySDK/OptimizelyUserContext.cs @@ -99,7 +99,7 @@ public OptimizelyDecision Decide(string key, /// /// list of flag keys for which a decision will be made. /// A dictionary of all decision results, mapped by flag keys. - public Dictionary DecideForKeys(List keys, OptimizelyDecideOption[] options) + public Dictionary DecideForKeys(string[] keys, OptimizelyDecideOption[] options) { return Optimizely.DecideForKeys(this, keys, options); } @@ -109,7 +109,7 @@ public Dictionary DecideForKeys(List keys, O /// /// list of flag keys for which a decision will be made. /// A dictionary of all decision results, mapped by flag keys. - public Dictionary DecideForKeys(List keys) + public Dictionary DecideForKeys(string[] keys) { return DecideForKeys(keys, new OptimizelyDecideOption[] { }); } From 869b655c9256d64cdfff7a14097f264148f39aa8 Mon Sep 17 00:00:00 2001 From: muhammadnoman Date: Mon, 7 Dec 2020 22:16:40 +0500 Subject: [PATCH 33/34] Added additional test with count of Notfication --- .../OptimizelyUserContextTest.cs | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs index 07eee639..24b40edc 100644 --- a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs +++ b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs @@ -262,11 +262,22 @@ public void DecideAllTwoFlag() var user = Optimizely.CreateUserContext(UserID); user.SetAttribute("browser_type", "chrome"); + // Mocking objects. + NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny>())); + + Optimizely.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); var decisions = user.DecideForKeys(flagKeys); - Assert.True(decisions.Count == 2); + var userAttributes = new UserAttributes + { + { "browser_type", "chrome" } + }; + Assert.True(decisions.Count == 2); + NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback(DecisionNotificationTypes.FLAG, UserID, userAttributes, It.IsAny>()), + Times.Exactly(2)); OptimizelyDecision expDecision1 = new OptimizelyDecision( "Gred", false, @@ -315,10 +326,23 @@ public void DecideAllAllFlags() var user = Optimizely.CreateUserContext(UserID); user.SetAttribute("browser_type", "chrome"); + // Mocking objects. + NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny>())); + + Optimizely.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); var decisions = user.DecideAll(); + var userAttributes = new UserAttributes + { + { "browser_type", "chrome" } + }; + Assert.True(decisions.Count == 10); + NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback(DecisionNotificationTypes.FLAG, UserID, userAttributes, It.IsAny>()), + Times.Exactly(10)); + OptimizelyDecision expDecision1 = new OptimizelyDecision( null, false, From fa90e13c5ed84c4770cb09c7f6d6192e5cbbb1d5 Mon Sep 17 00:00:00 2001 From: Muhammad Noman Date: Wed, 9 Dec 2020 19:33:15 +0500 Subject: [PATCH 34/34] replaced copiedOptions = copiedOptions.Concat(options).Concat(DefaultDecideOptions).ToArray(); with copiedOptions = options.Union(DefaultDecideOptions).ToArray(); --- OptimizelySDK/Optimizely.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs index 63e07910..9485701f 100644 --- a/OptimizelySDK/Optimizely.cs +++ b/OptimizelySDK/Optimizely.cs @@ -898,11 +898,10 @@ internal Dictionary DecideForKeys(OptimizelyUserCont } private OptimizelyDecideOption[] GetAllOptions(OptimizelyDecideOption[] options) { - OptimizelyDecideOption[] copiedOptions = new OptimizelyDecideOption[DefaultDecideOptions.Length]; - Array.Copy(DefaultDecideOptions, copiedOptions, DefaultDecideOptions.Length); + OptimizelyDecideOption[] copiedOptions = DefaultDecideOptions; if (options != null) { - copiedOptions = copiedOptions.Concat(options).Concat(DefaultDecideOptions).ToArray(); + copiedOptions = options.Union(DefaultDecideOptions).ToArray(); } return copiedOptions; }