From 01bad8fbdd339440a445fb0e867d7ae281aa15cc Mon Sep 17 00:00:00 2001 From: muhammadnoman <muhammadnoman@folio3.com> Date: Thu, 5 Nov 2020 20:48:30 +0500 Subject: [PATCH 01/19] 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 +{ + /// <summary> + /// Interface implemented by all condition classes for audience evaluation. + /// </summary> + public interface IDecisionReasons + { + void AddError(string format, params object[] args); + string AddInfo(string format, params object[] args); + List<string> 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<string> Errors = new List<string>(); + private List<String> Logs = new List<string>(); + + public static IDecisionReasons NewInstance(List<OptimizelyDecideOption> 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<string> ToReport() + { + List<string> reasons = new List<string>(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<string> errors = new List<string>(); + + 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<string> 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<string> Reasons { get; set; } + + public OptimizelyDecision(string variationKey, + bool enabled, + OptimizelyJSON variables, + string ruleKey, + string flagKey, + OptimizelyUserContext userContext, + List<string> 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<string, object>(), errorHandler, logger), + null, + key, + optimizelyUserContext, + new List<string>() { 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; + } + + /// <summary> + /// Set an attribute for a given key. + /// </summary> + /// <param name="key">An attribute key</param> + /// <param name="value">value An attribute value</param> + public void SetAttribute(string key, object value) + { + UserAttributes.Add(key, value); + } + + /// <summary> + /// Returns a decision result ({@link OptimizelyDecision}) for a given flag key and a user context, which contains all data required to deliver the flag. + /// <ul> + /// <li>If the SDK finds an error, it’ll return a decision with <b>null</b> for <b>variationKey</b>. The decision will include an error message in <b>reasons</b>. + /// </ul> + /// </summary> + /// <param name="key">A flag key for which a decision will be made.</param> + /// <returns>A decision result.</returns> + public OptimizelyDecision Decide(string key) + { + return Decide(key, new List<OptimizelyDecideOption>()); + } + + /// <summary> + /// Returns a decision result ({@link OptimizelyDecision}) for a given flag key and a user context, which contains all data required to deliver the flag. + /// <ul> + /// <li>If the SDK finds an error, it’ll return a decision with <b>null</b> for <b>variationKey</b>. The decision will include an error message in <b>reasons</b>. + /// </ul> + /// </summary> + /// <param name="key">A flag key for which a decision will be made.</param> + /// <param name="options">A list of options for decision-making.</param> + /// <returns>A decision result.</returns> + public OptimizelyDecision Decide(string key, + List<OptimizelyDecideOption> options) + { + return null; + } + + /// <summary> + /// Returns a key-map of decision results for multiple flag keys and a user context. + /// </summary> + /// <param name="keys">list of flag keys for which a decision will be made.</param> + /// <returns>A dictionary of all decision results, mapped by flag keys.</returns> + public Dictionary<string, OptimizelyDecision> DecideForKeys(List<string> keys) + { + return null; + } + + /// <summary> + /// Returns a key-map of decision results ({@link OptimizelyDecision}) for all active flag keys. + /// </summary> + /// <returns>A dictionary of all decision results, mapped by flag keys.</returns> + public Dictionary<string, OptimizelyDecision> DecideAll() + { + return DecideAll(new List<OptimizelyDecideOption>()); + } + + + /// <summary> + /// Returns a key-map of decision results ({@link OptimizelyDecision}) for all active flag keys. + /// </summary> + /// <param name="options">A list of options for decision-making.</param> + /// <returns>All decision results mapped by flag keys.</returns> + public Dictionary<string, OptimizelyDecision> DecideAll(List<OptimizelyDecideOption> options) + { + return null; + } + + /// <summary> + /// Track an event. + /// </summary> + /// <param name="eventName">The event name.</param> + public void TrackEvent(string eventName) + { + TrackEvent(eventName, new EventTags()); + } + + /// <summary> + /// Track an event. + /// </summary> + /// <param name="eventName">The event name.</param> + /// <param name="eventTags">A map of event tag names to event tag values.</param> + public void TrackEvent(string eventName, + EventTags eventTags) + { + Optimizely.Track(eventName, UserId, UserAttributes, eventTags); + } + } +} From 35c5e76d087495df92ae2af4b16c1d2b85d457d6 Mon Sep 17 00:00:00 2001 From: muhammadnoman <muhammadnoman@folio3.com> Date: Fri, 6 Nov 2020 21:07:11 +0500 Subject: [PATCH 02/19] 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 @@ </Compile> <Compile Include="..\OptimizelySDK\OptlyConfig\IOptimizelyConfigManager.cs"> <Link>OptlyConfig\IOptimizelyConfigManager.cs</Link> + </Compile> + <Compile Include="..\OptimizelySDK\OptimizelyDecisions\DecisionMessage.cs"> + <Link>OptimizelyDecisions\DecisionMessage.cs</Link> + </Compile> + <Compile Include="..\OptimizelySDK\OptimizelyDecisions\IDecisionReasons.cs"> + <Link>OptimizelyDecisions\IDecisionReasons.cs</Link> + </Compile> + <Compile Include="..\OptimizelySDK\OptimizelyDecisions\DefaultDecisionReasons.cs"> + <Link>OptimizelyDecisions\DefaultDecisionReasons.cs</Link> + </Compile> + <Compile Include="..\OptimizelySDK\OptimizelyDecisions\ErrorsDecisionReasons.cs"> + <Link>OptimizelyDecisions\ErrorsDecisionReasons.cs</Link> + </Compile> + <Compile Include="..\OptimizelySDK\OptimizelyDecisions\OptimizelyDecideOption.cs"> + <Link>OptimizelyDecisions\OptimizelyDecideOption.cs</Link> + </Compile> + <Compile Include="..\OptimizelySDK\OptimizelyDecisions\OptimizelyDecision.cs"> + <Link>OptimizelyDecisions\OptimizelyDecision.cs</Link> + </Compile> + <Compile Include="..\OptimizelySDK\OptimizelyUserContext.cs"> + <Link>OptimizelyUserContext.cs</Link> </Compile> </ItemGroup> <ItemGroup> 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 @@ <Compile Include="..\OptimizelySDK\OptlyConfig\OptimizelyVariation.cs" /> <Compile Include="..\OptimizelySDK\OptlyConfig\OptimizelyConfigService.cs" /> <Compile Include="..\OptimizelySDK\OptlyConfig\IOptimizelyConfigManager.cs" /> + <Compile Include="..\OptimizelySDK\OptimizelyDecisions\DecisionMessage.cs" /> + <Compile Include="..\OptimizelySDK\OptimizelyDecisions\IDecisionReasons.cs" /> + <Compile Include="..\OptimizelySDK\OptimizelyDecisions\DefaultDecisionReasons.cs" /> + <Compile Include="..\OptimizelySDK\OptimizelyDecisions\ErrorsDecisionReasons.cs"/> + <Compile Include="..\OptimizelySDK\OptimizelyDecisions\OptimizelyDecideOption.cs" /> + <Compile Include="..\OptimizelySDK\OptimizelyDecisions\OptimizelyDecision.cs" /> + <Compile Include="..\OptimizelySDK\OptimizelyUserContext.cs" /> <Compile Include="..\OptimizelySDK\Config\FallbackProjectConfigManager.cs"> <Link>AtomicProjectConfigManager.cs</Link> </Compile> 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 @@ </Compile> <Compile Include="..\OptimizelySDK\OptlyConfig\IOptimizelyConfigManager.cs"> <Link>OptlyConfig\IOptimizelyConfigManager.cs</Link> + </Compile> + <Compile Include="..\OptimizelySDK\OptimizelyDecisions\DecisionMessage.cs"> + <Link>OptimizelyDecisions\DecisionMessage.cs</Link> + </Compile> + <Compile Include="..\OptimizelySDK\OptimizelyDecisions\IDecisionReasons.cs"> + <Link>OptimizelyDecisions\IDecisionReasons.cs</Link> + </Compile> + <Compile Include="..\OptimizelySDK\OptimizelyDecisions\DefaultDecisionReasons.cs"> + <Link>OptimizelyDecisions\DefaultDecisionReasons.cs</Link> + </Compile> + <Compile Include="..\OptimizelySDK\OptimizelyDecisions\ErrorsDecisionReasons.cs"> + <Link>OptimizelyDecisions\ErrorsDecisionReasons.cs</Link> + </Compile> + <Compile Include="..\OptimizelySDK\OptimizelyDecisions\OptimizelyDecideOption.cs"> + <Link>OptimizelyDecisions\OptimizelyDecideOption.cs</Link> + </Compile> + <Compile Include="..\OptimizelySDK\OptimizelyDecisions\OptimizelyDecision.cs"> + <Link>OptimizelyDecisions\OptimizelyDecision.cs</Link> + </Compile> + <Compile Include="..\OptimizelySDK\OptimizelyUserContext.cs"> + <Link>OptimizelyUserContext.cs</Link> </Compile> </ItemGroup> <ItemGroup> 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<ILogger> LoggerMock; + private Mock<IErrorHandler> ErrorHandlerMock; + + [SetUp] + public void Initialize() + { + ErrorHandlerMock = new Mock<IErrorHandler>(); + ErrorHandlerMock.Setup(e => e.HandleError(It.IsAny<Exception>())); + + LoggerMock = new Mock<ILogger>(); + LoggerMock.Setup(i => i.Log(It.IsAny<LogLevel>(), It.IsAny<string>())); + } + + [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<string, object>()); + Assert.AreEqual(optimizelyDecision.Reasons, new List<string>() { "some error message" }); + Assert.IsNull(optimizelyDecision.RuleKey); + Assert.False(optimizelyDecision.Enabled); + } + + [Test] + public void TestNewDecision() + { + var map = new Dictionary<string, object>() { + { "strField", "john doe" }, + { "intField", 12 }, + { "objectField", new Dictionary<string, object> () { + { "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<string>()); + Assert.AreEqual(optimizelyDecision.VariationKey, "var_key"); + Assert.AreEqual(optimizelyDecision.FlagKey, "feature_key"); + Assert.AreEqual(optimizelyDecision.Variables.ToString(), expectedStringObj); + Assert.AreEqual(optimizelyDecision.Reasons, new List<string>()); + 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 @@ <Compile Include="DecisionServiceTest.cs" /> <Compile Include="DefaultErrorHandlerTest.cs" /> <Compile Include="EventTests\EventProcessorProps.cs" /> + <Compile Include="OptimizelyDecisions\OptimizelyDecisionTest.cs" /> <Compile Include="OptimizelyJSONTest.cs" /> <Compile Include="EventTests\BatchEventProcessorTest.cs" /> <Compile Include="EventTests\DefaultEventDispatcherTest.cs" /> 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<string> 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<string> 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 @@ <Compile Include="Entity\Group.cs" /> <Compile Include="Entity\IdKeyEntity.cs" /> <Compile Include="Event\Entity\DecisionMetadata.cs" /> + <Compile Include="OptimizelyDecisions\DecisionMessage.cs" /> + <Compile Include="OptimizelyDecisions\IDecisionReasons.cs" /> + <Compile Include="OptimizelyDecisions\DefaultDecisionReasons.cs" /> + <Compile Include="OptimizelyDecisions\ErrorsDecisionReasons.cs" /> + <Compile Include="OptimizelyDecisions\OptimizelyDecideOption.cs" /> + <Compile Include="OptimizelyDecisions\OptimizelyDecision.cs" /> + <Compile Include="OptimizelyUserContext.cs" /> <Compile Include="OptimizelyJSON.cs" /> <Compile Include="Entity\Rollout.cs" /> <Compile Include="Entity\TrafficAllocation.cs" /> From f79974c976827faea40346dceb61fb66671f1b7a Mon Sep 17 00:00:00 2001 From: muhammadnoman <muhammadnoman@folio3.com> Date: Fri, 6 Nov 2020 21:11:13 +0500 Subject: [PATCH 03/19] 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 @@ <Compile Include="..\OptimizelySDK\OptlyConfig\IOptimizelyConfigManager.cs"> <Link>OptlyConfig\IOptimizelyConfigManager.cs</Link> </Compile> + <Compile Include="..\OptimizelySDK\OptimizelyDecisions\DecisionMessage.cs"> + <Link>OptimizelyDecisions\DecisionMessage.cs</Link> + </Compile> + <Compile Include="..\OptimizelySDK\OptimizelyDecisions\IDecisionReasons.cs"> + <Link>OptimizelyDecisions\IDecisionReasons.cs</Link> + </Compile> + <Compile Include="..\OptimizelySDK\OptimizelyDecisions\DefaultDecisionReasons.cs"> + <Link>OptimizelyDecisions\DefaultDecisionReasons.cs</Link> + </Compile> + <Compile Include="..\OptimizelySDK\OptimizelyDecisions\ErrorsDecisionReasons.cs"> + <Link>OptimizelyDecisions\ErrorsDecisionReasons.cs</Link> + </Compile> + <Compile Include="..\OptimizelySDK\OptimizelyDecisions\OptimizelyDecideOption.cs"> + <Link>OptimizelyDecisions\OptimizelyDecideOption.cs</Link> + </Compile> + <Compile Include="..\OptimizelySDK\OptimizelyDecisions\OptimizelyDecision.cs"> + <Link>OptimizelyDecisions\OptimizelyDecision.cs</Link> + </Compile> + <Compile Include="..\OptimizelySDK\OptimizelyUserContext.cs"> + <Link>OptimizelyUserContext.cs</Link> + </Compile> </ItemGroup> <ItemGroup> <None Include="..\OptimizelySDK\Utils\schema.json"> From 7531f3c327a5f226d6a5ca93b31b6394f72b67ee Mon Sep 17 00:00:00 2001 From: muhammadnoman <muhammadnoman@folio3.com> Date: Fri, 6 Nov 2020 22:28:14 +0500 Subject: [PATCH 04/19] 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>() { 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<string> Errors = new List<string>(); - private List<String> Logs = new List<string>(); + private List<string> Logs = new List<string>(); public static IDecisionReasons NewInstance(List<OptimizelyDecideOption> 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 <muhammadnoman@folio3.com> Date: Mon, 9 Nov 2020 19:14:51 +0500 Subject: [PATCH 05/19] 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>() { 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>() { 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<string> ToReport() { List<string> reasons = new List<string>(Errors); - reasons.Concat(Logs); + reasons.AddRange(Logs); return reasons; } } From ac0c6a513d771affdf894cb54c63c02724c7d764 Mon Sep 17 00:00:00 2001 From: muhammadnoman <muhammadnoman@folio3.com> Date: Mon, 9 Nov 2020 20:27:14 +0500 Subject: [PATCH 06/19] 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 /// <returns>null|Variation Representing variation</returns> Variation Activate(string experimentKey, string userId, UserAttributes userAttributes = null); + /// <summary> + /// 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. + /// </summary> + /// <param name="userId">The user ID to be used for bucketing.</param> + /// <param name="userAttributes">The user's attributes</param> + /// <returns>OptimizelyUserContext | An OptimizelyUserContext associated with this OptimizelyClient.</returns> + OptimizelyUserContext CreateUserContext(string userId, UserAttributes userAttributes = null); + /// <summary> /// Sends conversion event to Optimizely. /// </summary> 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<OptimizelyDecideOption> DefaultDecideOptions; + /// <summary> /// 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<OptimizelyDecideOption> 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<OptimizelyDecideOption> 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<OptimizelyDecideOption>(); } /// <summary> @@ -682,6 +688,30 @@ public OptimizelyJSON GetFeatureVariableJSON(string featureKey, string variableK return GetFeatureVariableValueForType<OptimizelyJSON>(featureKey, variableKey, userId, userAttributes, FeatureVariable.JSON_TYPE); } + //============ decide ============// + + /// <summary> + /// 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. + /// </summary> + /// <param name="userId">The user ID to be used for bucketing.</param> + /// <param name="userAttributes">The user's attributes</param> + /// <returns>OptimizelyUserContext | An OptimizelyUserContext associated with this OptimizelyClient.</returns> + public OptimizelyUserContext CreateUserContext(string userId, + UserAttributes userAttributes = null) + { + var inputValues = new Dictionary<string, string> + { + { USER_ID, userId }, + }; + + if (!ValidateStringInputs(inputValues)) + return null; + + + return new OptimizelyUserContext(this, userId, userAttributes, ErrorHandler, Logger); + } + /// <summary> /// Sends impression event. /// </summary> 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 <muhammadnoman@folio3.com> Date: Wed, 11 Nov 2020 22:48:46 +0500 Subject: [PATCH 07/19] 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<LogLevel>(), It.IsAny<string>()), 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<LogLevel>(), It.IsAny<string>()), 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<LogLevel>(), It.IsAny<string>()), 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<LogLevel>(), It.IsAny<string>()), 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<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never); + BucketerMock.Verify(_ => _.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>()), 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<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never); + BucketerMock.Verify(_ => _.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>()), 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<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never); + BucketerMock.Verify(_ => _.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>()), 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<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never); + BucketerMock.Verify(_ => _.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>()), 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<Bucketer>(LoggerMock.Object); - mockBucketer.Setup(m => m.Bucket(ProjectConfig, experiment, UserProfileId, UserProfileId)).Returns(variation); + mockBucketer.Setup(m => m.Bucket(ProjectConfig, experiment, UserProfileId, UserProfileId, It.IsAny<IDecisionReasons>())).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<Bucketer>(LoggerMock.Object); - mockBucketer.Setup(m => m.Bucket(ProjectConfig, experiment, UserProfileId, UserProfileId)).Returns(variation); + mockBucketer.Setup(m => m.Bucket(ProjectConfig, experiment, UserProfileId, UserProfileId, It.IsAny<IDecisionReasons>())).Returns(variation); Dictionary<string, object> 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<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance()); Assert.IsNull(decision); @@ -487,7 +488,7 @@ public void TestGetVariationForFeatureExperimentGivenExperimentNotInDataFile() ExperimentIds = new List<string> { "29039203" } }; - var decision = DecisionService.GetVariationForFeatureExperiment(featureFlag, GenericUserId, new UserAttributes() { }, ProjectConfig); + var decision = DecisionService.GetVariationForFeatureExperiment(featureFlag, GenericUserId, new UserAttributes() { }, ProjectConfig, new List<OptimizelyDecideOption>(), 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<Variation>(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<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\".")); @@ -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<List<OptimizelyDecideOption>>(), It.IsAny<IDecisionReasons>())).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<OptimizelyDecideOption>(), 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<OptimizelyDecideOption>(), 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<Experiment>(), It.IsAny<string>(), ProjectConfig, It.IsAny<UserAttributes>())). + DecisionServiceMock.Setup(ds => ds.GetVariation(It.IsAny<Experiment>(), It.IsAny<string>(), ProjectConfig, It.IsAny<UserAttributes>(), It.IsAny<List<OptimizelyDecideOption>>(), It.IsAny<IDecisionReasons>())). Returns<Variation>(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<OptimizelyDecideOption>(), 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<FeatureFlag>(), It.IsAny<string>(), It.IsAny<UserAttributes>(), ProjectConfig)).Returns<Variation>(null); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny<FeatureFlag>(), It.IsAny<string>(), It.IsAny<UserAttributes>(), ProjectConfig, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance())).Returns<Variation>(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<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), - It.IsAny<string>())).Returns(variation); + It.IsAny<string>(), It.IsAny<IDecisionReasons>())).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<ProjectConfig>(), experiment, It.IsAny<string>(), It.IsAny<string>())).Returns<Variation>(null); - BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), It.IsAny<string>())).Returns(variation); + BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), experiment, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>())).Returns<Variation>(null); + BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>())).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<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>())).Returns<Variation>(null); + BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>())).Returns<Variation>(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<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), It.IsAny<string>())).Returns(variation); + BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>())).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<string>())).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<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), WhitelistedUserId)).Returns(variation); - BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), GenericUserId)).Returns<Variation>(null); + BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), WhitelistedUserId, It.IsAny<IDecisionReasons>())).Returns(variation); + BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), GenericUserId, It.IsAny<IDecisionReasons>())).Returns<Variation>(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<FeatureFlag>(), It.IsAny<string>(), - It.IsAny<UserAttributes>(), ProjectConfig)).Returns(expectedDecision); + It.IsAny<UserAttributes>(), ProjectConfig, It.IsAny<List<OptimizelyDecideOption>>(), It.IsAny<IDecisionReasons>())).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<FeatureFlag>(), It.IsAny<string>(), - It.IsAny<UserAttributes>(), ProjectConfig)).Returns<Variation>(null); + It.IsAny<UserAttributes>(), ProjectConfig, It.IsAny<List<OptimizelyDecideOption>>(), It.IsAny<IDecisionReasons>())).Returns<Variation>(null); DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureRollout(It.IsAny<FeatureFlag>(), It.IsAny<string>(), - It.IsAny<UserAttributes>(), ProjectConfig)).Returns(expectedDecision); + It.IsAny<UserAttributes>(), ProjectConfig, It.IsAny<IDecisionReasons>())).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<FeatureFlag>(), It.IsAny<string>(), It.IsAny<UserAttributes>(), ProjectConfig)).Returns<Variation>(null); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureRollout(It.IsAny<FeatureFlag>(), It.IsAny<string>(), It.IsAny<UserAttributes>(), ProjectConfig)).Returns<Variation>(null); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny<FeatureFlag>(), It.IsAny<string>(), It.IsAny<UserAttributes>(), ProjectConfig, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance())).Returns<Variation>(null); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureRollout(It.IsAny<FeatureFlag>(), It.IsAny<string>(), It.IsAny<UserAttributes>(), ProjectConfig, DefaultDecisionReasons.NewInstance())).Returns<Variation>(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<List<OptimizelyDecideOption>>(), It.IsAny<IDecisionReasons>())).Returns(variation); + var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig, new List<OptimizelyDecideOption>(), 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<string>(), It.IsAny<string>())).Returns(rolloutVariation); - var actualRolloutDecision = DecisionServiceMock.Object.GetVariationForFeatureRollout(featureFlag, "user1", userAttributes, ProjectConfig); + BucketerMock.Setup(bm => bm.Bucket(ProjectConfig, rolloutExperiment, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>())).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<ILogger> LoggerMock; + private Mock<IErrorHandler> ErrorHandlerMock; + private Mock<IEventDispatcher> EventDispatcherMock; + + [SetUp] + public void SetUp() + { + LoggerMock = new Mock<ILogger>(); + LoggerMock.Setup(i => i.Log(It.IsAny<LogLevel>(), It.IsAny<string>())); + + ErrorHandlerMock = new Mock<IErrorHandler>(); + ErrorHandlerMock.Setup(e => e.HandleError(It.IsAny<Exception>())); + EventDispatcherMock = new Mock<IEventDispatcher>(); + + 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<string, object>()); + 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 /// <param name="experiment">Experiment Experiment in which user is to be bucketed</param> /// <param name="bucketingId">A customer-assigned value used to create the key for the murmur hash.</param> /// <param name="userId">User identifier</param> + /// <param name="reasons">Decision log messages.</param> /// <returns>Variation which will be shown to the user</returns> - 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 /// <param name = "userId" > The userId of the user. /// <param name = "filteredAttributes" > The user's attributes. This should be filtered to just attributes in the Datafile.</param> /// <returns>The Variation the user is allocated into.</returns> - 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<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance()); + } + + /// <summary> + /// Get a Variation of an Experiment for a user to be allocated into. + /// </summary> + /// <param name = "experiment" > The Experiment the user will be bucketed into.</param> + /// <param name = "userId" > The userId of the user. + /// <param name = "filteredAttributes" > The user's attributes. This should be filtered to just attributes in the Datafile.</param> + /// <returns>The Variation the user is allocated into.</returns> + public virtual Variation GetVariation(Experiment experiment, + string userId, + ProjectConfig config, + UserAttributes filteredAttributes, + List<OptimizelyDecideOption> 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<string, Decision>()); - 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 /// <param name="config">Project Config</param> /// <returns>Variation entity which the given user and experiment should be forced into.</returns> public Variation GetForcedVariation(string experimentKey, string userId, ProjectConfig config) + { + return GetForcedVariation(experimentKey, userId, config, DefaultDecisionReasons.NewInstance()); + } + + /// <summary> + /// Gets the forced variation for the given user and experiment. + /// </summary> + /// <param name="experimentKey">The experiment key</param> + /// <param name="userId">The user ID</param> + /// <param name="config">Project Config</param> + /// <returns>Variation entity which the given user and experiment should be forced into.</returns> + 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; } - - /// <summary> /// Get the variation the user has been whitelisted into. /// </summary> @@ -258,6 +291,19 @@ public bool SetForcedVariation(string experimentKey, string userId, string varia /// <returns>if the user is not whitelisted into any variation {@link Variation} /// the user is bucketed into if the user has a specified whitelisted variation.</returns> public Variation GetWhitelistedVariation(Experiment experiment, string userId) + { + return GetWhitelistedVariation(experiment, userId, DefaultDecisionReasons.NewInstance()); + } + + /// <summary> + /// Get the variation the user has been whitelisted into. + /// </summary> + /// <param name = "experiment" >in which user is to be bucketed.</param> + /// <param name = "userId" > User Identifier</param> + /// <param name = "reasons" > Decision log messages.</param> + /// <returns>if the user is not whitelisted into any variation {@link Variation} + /// the user is bucketed into if the user has a specified whitelisted variation.</returns> + public Variation GetWhitelistedVariation(Experiment experiment, string userId, IDecisionReasons reasons) { //if a user has a forced variation mapping, return the respective variation Dictionary<string, string> 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) /// <param name = "experiment" > which the user was bucketed</param> /// <param name = "userProfile" > User profile of the user</param> /// <returns>The user was previously bucketed into.</returns> - 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; } } - /// <summary> /// Save a { @link Variation } of an { @link Experiment } for a user in the {@link UserProfileService}. /// </summary> @@ -330,6 +375,17 @@ public Variation GetStoredVariation(Experiment experiment, UserProfile userProfi /// <param name = "variation" > The Variation to save.</param> /// <param name = "userProfile" > instance of the user information.</param> public void SaveVariation(Experiment experiment, Variation variation, UserProfile userProfile) + { + SaveVariation(experiment, variation, userProfile, DefaultDecisionReasons.NewInstance()); + } + + /// <summary> + /// Save a { @link Variation } of an { @link Experiment } for a user in the {@link UserProfileService}. + /// </summary> + /// <param name = "experiment" > The experiment the user was buck</param> + /// <param name = "variation" > The Variation to save.</param> + /// <param name = "userProfile" > instance of the user information.</param> + 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)); } } - + /// <summary> /// 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 /// <param name = "featureFlag" >The feature flag the user wants to access.</param> /// <param name = "userId" >User Identifier</param> /// <param name = "filteredAttributes" >The user's attributes. This should be filtered to just attributes in the Datafile.</param> + /// <param name = "reasons" >Decision log messages.</param> /// <returns>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</returns> - 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 /// <param name = "filteredAttributes" >The user's attributes. This should be filtered to just attributes in the Datafile.</param> /// <returns>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</returns> - 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<OptimizelyDecideOption> 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 /// <returns>null if the user is not bucketed into any variation or the FeatureDecision entity if the user is /// successfully bucketed.</returns> public virtual FeatureDecision GetVariationForFeature(FeatureFlag featureFlag, string userId, ProjectConfig config, UserAttributes filteredAttributes) + { + return GetVariationForFeature(featureFlag, userId, config, filteredAttributes, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance()); + } + + /// <summary> + /// Get the variation the user is bucketed into for the FeatureFlag + /// </summary> + /// <param name = "featureFlag" >The feature flag the user wants to access.</param> + /// <param name = "userId" >User Identifier</param> + /// <param name = "filteredAttributes" >The user's attributes. This should be filtered to just attributes in the Datafile.</param> + /// <param name = "filteredAttributes" >The user's attributes. This should be filtered to just attributes in the Datafile.</param> + /// <param name = "options" >An array of decision options.</param> + /// <param name = "reasons" >Decision log messages.</param> + /// <returns>null if the user is not bucketed into any variation or the FeatureDecision entity if the user is + /// successfully bucketed.</returns> + public virtual FeatureDecision GetVariationForFeature(FeatureFlag featureFlag, + string userId, + ProjectConfig config, + UserAttributes filteredAttributes, + List<OptimizelyDecideOption> 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 /// <param name = "userId" >User Identifier</param> /// <param name = "filteredAttributes" >The user's attributes.</param> /// <returns>Bucketing Id if it is a string type in attributes, user Id otherwise.</returns> - 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<string, string>(); - 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<string, string>(); + 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<string, object> - { - { "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<string, object> + { + { "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<OptimizelyDecideOption> 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<string, string> + { + { 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<string, object>(); + 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<string> 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<string, object> + { + { "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<OptimizelyDecideOption> GetAllOptions(List<OptimizelyDecideOption> options) + { + var copiedOptions = new List<OptimizelyDecideOption>(DefaultDecideOptions); + if (options != null) + { + copiedOptions.AddRange(options); + } + return copiedOptions; + } + /// <summary> /// Sends impression event. /// </summary> 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 /// <param name="value">value An attribute value</param> public void SetAttribute(string key, object value) { - UserAttributes.Add(key, value); + UserAttributes[key] = value; } /// <summary> @@ -74,7 +74,7 @@ public OptimizelyDecision Decide(string key) public OptimizelyDecision Decide(string key, List<OptimizelyDecideOption> options) { - return null; + return Optimizely.Decide(this, key, options); } /// <summary> 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; } + /// <summary> + /// Check if the user meets audience conditions to be in experiment or not + /// </summary> + /// <param name="config">ProjectConfig Configuration for the project</param> + /// <param name="experiment">Experiment Entity representing the experiment</param> + /// <param name="userAttributes">Attributes of the user. Defaults to empty attributes array if not provided</param> + /// <param name="loggingKeyType">It can be either experiment or rule.</param> + /// <param name="loggingKey">In case loggingKeyType is experiment it will be experiment key or else it will be rule number.</param> + /// <returns>true if the user meets audience conditions to be in experiment, false otherwise.</returns> + 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); + } /// <summary> /// 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) /// <param name="userAttributes">Attributes of the user. Defaults to empty attributes array if not provided</param> /// <param name="loggingKeyType">It can be either experiment or rule.</param> /// <param name="loggingKey">In case loggingKeyType is experiment it will be experiment key or else it will be rule number.</param> + /// <param name="reasons">Decision log messages.</param> /// <returns>true if the user meets audience conditions to be in experiment, false otherwise.</returns> 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 <muhammadnoman@folio3.com> Date: Wed, 11 Nov 2020 23:02:21 +0500 Subject: [PATCH 08/19] 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<LogLevel>(), It.IsAny<string>()), 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<LogLevel>(), It.IsAny<string>()), 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<LogLevel>(), It.IsAny<string>()), 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<LogLevel>(), It.IsAny<string>()), 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<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>()), Times.Never); + BucketerMock.Verify(_ => _.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>()), 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<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>()), Times.Never); + BucketerMock.Verify(_ => _.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>()), 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<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>()), Times.Never); + BucketerMock.Verify(_ => _.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>()), 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<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>()), Times.Never); + BucketerMock.Verify(_ => _.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>()), 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<Bucketer>(LoggerMock.Object); - mockBucketer.Setup(m => m.Bucket(ProjectConfig, experiment, UserProfileId, UserProfileId, It.IsAny<IDecisionReasons>())).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<Bucketer>(LoggerMock.Object); - mockBucketer.Setup(m => m.Bucket(ProjectConfig, experiment, UserProfileId, UserProfileId, It.IsAny<IDecisionReasons>())).Returns(variation); + mockBucketer.Setup(m => m.Bucket(ProjectConfig, experiment, UserProfileId, UserProfileId)).Returns(variation); Dictionary<string, object> 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<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance()); + var decision = DecisionService.GetVariationForFeatureExperiment(featureFlag, GenericUserId, new UserAttributes() { }, ProjectConfig); Assert.IsNull(decision); @@ -488,7 +487,7 @@ public void TestGetVariationForFeatureExperimentGivenExperimentNotInDataFile() ExperimentIds = new List<string> { "29039203" } }; - var decision = DecisionService.GetVariationForFeatureExperiment(featureFlag, GenericUserId, new UserAttributes() { }, ProjectConfig, new List<OptimizelyDecideOption>(), 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<Variation>(null); var featureFlag = ProjectConfig.GetFeatureFlagFromKey("multi_variate_feature"); - var decision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", new UserAttributes(), ProjectConfig, new List<OptimizelyDecideOption>(), 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<List<OptimizelyDecideOption>>(), It.IsAny<IDecisionReasons>())).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<OptimizelyDecideOption>(), 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<OptimizelyDecideOption>(), 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<Experiment>(), It.IsAny<string>(), ProjectConfig, It.IsAny<UserAttributes>(), It.IsAny<List<OptimizelyDecideOption>>(), It.IsAny<IDecisionReasons>())). + DecisionServiceMock.Setup(ds => ds.GetVariation(It.IsAny<Experiment>(), It.IsAny<string>(), ProjectConfig, It.IsAny<UserAttributes>())). Returns<Variation>(null); var featureFlag = ProjectConfig.GetFeatureFlagFromKey("boolean_feature"); - var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", new UserAttributes(), ProjectConfig, new List<OptimizelyDecideOption>(), 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<FeatureFlag>(), It.IsAny<string>(), It.IsAny<UserAttributes>(), ProjectConfig, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance())).Returns<Variation>(null); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny<FeatureFlag>(), It.IsAny<string>(), It.IsAny<UserAttributes>(), ProjectConfig)).Returns<Variation>(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<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), - It.IsAny<string>(), It.IsAny<IDecisionReasons>())).Returns(variation); + It.IsAny<string>())).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<ProjectConfig>(), experiment, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>())).Returns<Variation>(null); - BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>())).Returns(variation); + BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), experiment, It.IsAny<string>(), It.IsAny<string>())).Returns<Variation>(null); + BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), It.IsAny<string>())).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<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>())).Returns<Variation>(null); + BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>())).Returns<Variation>(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<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>())).Returns(variation); + BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), It.IsAny<string>())).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<string>())).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<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), WhitelistedUserId, It.IsAny<IDecisionReasons>())).Returns(variation); - BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), GenericUserId, It.IsAny<IDecisionReasons>())).Returns<Variation>(null); + BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), WhitelistedUserId)).Returns(variation); + BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), GenericUserId)).Returns<Variation>(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<FeatureFlag>(), It.IsAny<string>(), - It.IsAny<UserAttributes>(), ProjectConfig, It.IsAny<List<OptimizelyDecideOption>>(), It.IsAny<IDecisionReasons>())).Returns(expectedDecision); + It.IsAny<UserAttributes>(), 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<FeatureFlag>(), It.IsAny<string>(), - It.IsAny<UserAttributes>(), ProjectConfig, It.IsAny<List<OptimizelyDecideOption>>(), It.IsAny<IDecisionReasons>())).Returns<Variation>(null); + It.IsAny<UserAttributes>(), ProjectConfig)).Returns<Variation>(null); DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureRollout(It.IsAny<FeatureFlag>(), It.IsAny<string>(), - It.IsAny<UserAttributes>(), ProjectConfig, It.IsAny<IDecisionReasons>())).Returns(expectedDecision); + It.IsAny<UserAttributes>(), 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<FeatureFlag>(), It.IsAny<string>(), It.IsAny<UserAttributes>(), ProjectConfig, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance())).Returns<Variation>(null); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureRollout(It.IsAny<FeatureFlag>(), It.IsAny<string>(), It.IsAny<UserAttributes>(), ProjectConfig, DefaultDecisionReasons.NewInstance())).Returns<Variation>(null); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny<FeatureFlag>(), It.IsAny<string>(), It.IsAny<UserAttributes>(), ProjectConfig)).Returns<Variation>(null); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureRollout(It.IsAny<FeatureFlag>(), It.IsAny<string>(), It.IsAny<UserAttributes>(), ProjectConfig)).Returns<Variation>(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<List<OptimizelyDecideOption>>(), It.IsAny<IDecisionReasons>())).Returns(variation); - var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig, new List<OptimizelyDecideOption>(), 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<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>())).Returns(rolloutVariation); - var actualRolloutDecision = DecisionServiceMock.Object.GetVariationForFeatureRollout(featureFlag, "user1", userAttributes, ProjectConfig, DefaultDecisionReasons.NewInstance()); + BucketerMock.Setup(bm => bm.Bucket(ProjectConfig, rolloutExperiment, It.IsAny<string>(), It.IsAny<string>())).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<ILogger> LoggerMock; - private Mock<IErrorHandler> ErrorHandlerMock; - private Mock<IEventDispatcher> EventDispatcherMock; - - [SetUp] - public void SetUp() - { - LoggerMock = new Mock<ILogger>(); - LoggerMock.Setup(i => i.Log(It.IsAny<LogLevel>(), It.IsAny<string>())); - - ErrorHandlerMock = new Mock<IErrorHandler>(); - ErrorHandlerMock.Setup(e => e.HandleError(It.IsAny<Exception>())); - EventDispatcherMock = new Mock<IEventDispatcher>(); - - 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<string, object>()); - 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 /// <param name="experiment">Experiment Experiment in which user is to be bucketed</param> /// <param name="bucketingId">A customer-assigned value used to create the key for the murmur hash.</param> /// <param name="userId">User identifier</param> - /// <param name="reasons">Decision log messages.</param> /// <returns>Variation which will be shown to the user</returns> - 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 /// <param name = "userId" > The userId of the user. /// <param name = "filteredAttributes" > The user's attributes. This should be filtered to just attributes in the Datafile.</param> /// <returns>The Variation the user is allocated into.</returns> - public virtual Variation GetVariation(Experiment experiment, - string userId, - ProjectConfig config, - UserAttributes filteredAttributes) - { - return GetVariation(experiment, userId, config, filteredAttributes, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance()); - } - - /// <summary> - /// Get a Variation of an Experiment for a user to be allocated into. - /// </summary> - /// <param name = "experiment" > The Experiment the user will be bucketed into.</param> - /// <param name = "userId" > The userId of the user. - /// <param name = "filteredAttributes" > The user's attributes. This should be filtered to just attributes in the Datafile.</param> - /// <returns>The Variation the user is allocated into.</returns> - public virtual Variation GetVariation(Experiment experiment, - string userId, - ProjectConfig config, - UserAttributes filteredAttributes, - List<OptimizelyDecideOption> 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<string, Decision>()); - 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, /// <param name="config">Project Config</param> /// <returns>Variation entity which the given user and experiment should be forced into.</returns> public Variation GetForcedVariation(string experimentKey, string userId, ProjectConfig config) - { - return GetForcedVariation(experimentKey, userId, config, DefaultDecisionReasons.NewInstance()); - } - - /// <summary> - /// Gets the forced variation for the given user and experiment. - /// </summary> - /// <param name="experimentKey">The experiment key</param> - /// <param name="userId">The user ID</param> - /// <param name="config">Project Config</param> - /// <returns>Variation entity which the given user and experiment should be forced into.</returns> - 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; } - /// <summary> - /// Get the variation the user has been whitelisted into. - /// </summary> - /// <param name = "experiment" >in which user is to be bucketed.</param> - /// <param name = "userId" > User Identifier</param> - /// <returns>if the user is not whitelisted into any variation {@link Variation} - /// the user is bucketed into if the user has a specified whitelisted variation.</returns> - public Variation GetWhitelistedVariation(Experiment experiment, string userId) - { - return GetWhitelistedVariation(experiment, userId, DefaultDecisionReasons.NewInstance()); - } + /// <summary> /// Get the variation the user has been whitelisted into. /// </summary> /// <param name = "experiment" >in which user is to be bucketed.</param> /// <param name = "userId" > User Identifier</param> - /// <param name = "reasons" > Decision log messages.</param> /// <returns>if the user is not whitelisted into any variation {@link Variation} /// the user is bucketed into if the user has a specified whitelisted variation.</returns> - 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<string, string> 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 /// <param name = "experiment" > which the user was bucketed</param> /// <param name = "userProfile" > User profile of the user</param> /// <returns>The user was previously bucketed into.</returns> - 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; } } - /// <summary> - /// Save a { @link Variation } of an { @link Experiment } for a user in the {@link UserProfileService}. - /// </summary> - /// <param name = "experiment" > The experiment the user was buck</param> - /// <param name = "variation" > The Variation to save.</param> - /// <param name = "userProfile" > instance of the user information.</param> - public void SaveVariation(Experiment experiment, Variation variation, UserProfile userProfile) - { - SaveVariation(experiment, variation, userProfile, DefaultDecisionReasons.NewInstance()); - } /// <summary> /// 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 /// <param name = "experiment" > The experiment the user was buck</param> /// <param name = "variation" > The Variation to save.</param> /// <param name = "userProfile" > instance of the user information.</param> - 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)); } } - + /// <summary> /// 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 /// <param name = "featureFlag" >The feature flag the user wants to access.</param> /// <param name = "userId" >User Identifier</param> /// <param name = "filteredAttributes" >The user's attributes. This should be filtered to just attributes in the Datafile.</param> - /// <param name = "reasons" >Decision log messages.</param> /// <returns>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</returns> - 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 /// <param name = "filteredAttributes" >The user's attributes. This should be filtered to just attributes in the Datafile.</param> /// <returns>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</returns> - public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag featureFlag, - string userId, - UserAttributes filteredAttributes, - ProjectConfig config, - List<OptimizelyDecideOption> 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 /// <returns>null if the user is not bucketed into any variation or the FeatureDecision entity if the user is /// successfully bucketed.</returns> public virtual FeatureDecision GetVariationForFeature(FeatureFlag featureFlag, string userId, ProjectConfig config, UserAttributes filteredAttributes) - { - return GetVariationForFeature(featureFlag, userId, config, filteredAttributes, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance()); - } - - /// <summary> - /// Get the variation the user is bucketed into for the FeatureFlag - /// </summary> - /// <param name = "featureFlag" >The feature flag the user wants to access.</param> - /// <param name = "userId" >User Identifier</param> - /// <param name = "filteredAttributes" >The user's attributes. This should be filtered to just attributes in the Datafile.</param> - /// <param name = "filteredAttributes" >The user's attributes. This should be filtered to just attributes in the Datafile.</param> - /// <param name = "options" >An array of decision options.</param> - /// <param name = "reasons" >Decision log messages.</param> - /// <returns>null if the user is not bucketed into any variation or the FeatureDecision entity if the user is - /// successfully bucketed.</returns> - public virtual FeatureDecision GetVariationForFeature(FeatureFlag featureFlag, - string userId, - ProjectConfig config, - UserAttributes filteredAttributes, - List<OptimizelyDecideOption> 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, /// <param name = "userId" >User Identifier</param> /// <param name = "filteredAttributes" >The user's attributes.</param> /// <returns>Bucketing Id if it is a string type in attributes, user Id otherwise.</returns> - 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<string, string>(); - var decision = DecisionService.GetVariationForFeature(featureFlag, userId, config, userAttributes); - var variation = decision.Variation; - var decisionSource = decision?.Source ?? FeatureDecision.DECISION_SOURCE_ROLLOUT; + var sourceInfo = new Dictionary<string, string>(); + 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<string, object> { - { "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<string, object> + { + { "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<OptimizelyDecideOption> 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<string, string> - { - { 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<string, object>(); - 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<string> 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<string, object> - { - { "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<OptimizelyDecideOption> GetAllOptions(List<OptimizelyDecideOption> options) - { - var copiedOptions = new List<OptimizelyDecideOption>(DefaultDecideOptions); - if (options != null) - { - copiedOptions.AddRange(options); - } - return copiedOptions; - } - /// <summary> /// Sends impression event. /// </summary> 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 /// <param name="value">value An attribute value</param> public void SetAttribute(string key, object value) { - UserAttributes[key] = value; + UserAttributes.Add(key, value); } /// <summary> @@ -74,7 +74,7 @@ public OptimizelyDecision Decide(string key) public OptimizelyDecision Decide(string key, List<OptimizelyDecideOption> options) { - return Optimizely.Decide(this, key, options); + return null; } /// <summary> 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; } - /// <summary> - /// Check if the user meets audience conditions to be in experiment or not - /// </summary> - /// <param name="config">ProjectConfig Configuration for the project</param> - /// <param name="experiment">Experiment Entity representing the experiment</param> - /// <param name="userAttributes">Attributes of the user. Defaults to empty attributes array if not provided</param> - /// <param name="loggingKeyType">It can be either experiment or rule.</param> - /// <param name="loggingKey">In case loggingKeyType is experiment it will be experiment key or else it will be rule number.</param> - /// <returns>true if the user meets audience conditions to be in experiment, false otherwise.</returns> - 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); - } /// <summary> /// Check if the user meets audience conditions to be in experiment or not @@ -63,14 +44,12 @@ public static bool DoesUserMeetAudienceConditions(ProjectConfig config, /// <param name="userAttributes">Attributes of the user. Defaults to empty attributes array if not provided</param> /// <param name="loggingKeyType">It can be either experiment or rule.</param> /// <param name="loggingKey">In case loggingKeyType is experiment it will be experiment key or else it will be rule number.</param> - /// <param name="reasons">Decision log messages.</param> /// <returns>true if the user meets audience conditions to be in experiment, false otherwise.</returns> 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 <muhammadnoman@folio3.com> Date: Wed, 11 Nov 2020 23:08:32 +0500 Subject: [PATCH 09/19] 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<LogLevel>(), It.IsAny<string>()), 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<LogLevel>(), It.IsAny<string>()), 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<LogLevel>(), It.IsAny<string>()), 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<LogLevel>(), It.IsAny<string>()), 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<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never); + BucketerMock.Verify(_ => _.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>()), 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<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never); + BucketerMock.Verify(_ => _.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>()), 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<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never); + BucketerMock.Verify(_ => _.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>()), 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<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never); + BucketerMock.Verify(_ => _.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>()), 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<Bucketer>(LoggerMock.Object); - mockBucketer.Setup(m => m.Bucket(ProjectConfig, experiment, UserProfileId, UserProfileId)).Returns(variation); + mockBucketer.Setup(m => m.Bucket(ProjectConfig, experiment, UserProfileId, UserProfileId, It.IsAny<IDecisionReasons>())).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<Bucketer>(LoggerMock.Object); - mockBucketer.Setup(m => m.Bucket(ProjectConfig, experiment, UserProfileId, UserProfileId)).Returns(variation); + mockBucketer.Setup(m => m.Bucket(ProjectConfig, experiment, UserProfileId, UserProfileId, It.IsAny<IDecisionReasons>())).Returns(variation); Dictionary<string, object> 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<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance()); Assert.IsNull(decision); @@ -487,7 +488,7 @@ public void TestGetVariationForFeatureExperimentGivenExperimentNotInDataFile() ExperimentIds = new List<string> { "29039203" } }; - var decision = DecisionService.GetVariationForFeatureExperiment(featureFlag, GenericUserId, new UserAttributes() { }, ProjectConfig); + var decision = DecisionService.GetVariationForFeatureExperiment(featureFlag, GenericUserId, new UserAttributes() { }, ProjectConfig, new List<OptimizelyDecideOption>(), 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<Variation>(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<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\".")); @@ -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<List<OptimizelyDecideOption>>(), It.IsAny<IDecisionReasons>())).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<OptimizelyDecideOption>(), 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<OptimizelyDecideOption>(), 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<Experiment>(), It.IsAny<string>(), ProjectConfig, It.IsAny<UserAttributes>())). + DecisionServiceMock.Setup(ds => ds.GetVariation(It.IsAny<Experiment>(), It.IsAny<string>(), ProjectConfig, It.IsAny<UserAttributes>(), It.IsAny<List<OptimizelyDecideOption>>(), It.IsAny<IDecisionReasons>())). Returns<Variation>(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<OptimizelyDecideOption>(), 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<FeatureFlag>(), It.IsAny<string>(), It.IsAny<UserAttributes>(), ProjectConfig)).Returns<Variation>(null); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny<FeatureFlag>(), It.IsAny<string>(), It.IsAny<UserAttributes>(), ProjectConfig, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance())).Returns<Variation>(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<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), - It.IsAny<string>())).Returns(variation); + It.IsAny<string>(), It.IsAny<IDecisionReasons>())).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<ProjectConfig>(), experiment, It.IsAny<string>(), It.IsAny<string>())).Returns<Variation>(null); - BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), It.IsAny<string>())).Returns(variation); + BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), experiment, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>())).Returns<Variation>(null); + BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>())).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<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>())).Returns<Variation>(null); + BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>())).Returns<Variation>(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<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), It.IsAny<string>())).Returns(variation); + BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>())).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<string>())).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<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), WhitelistedUserId)).Returns(variation); - BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), GenericUserId)).Returns<Variation>(null); + BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), WhitelistedUserId, It.IsAny<IDecisionReasons>())).Returns(variation); + BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), GenericUserId, It.IsAny<IDecisionReasons>())).Returns<Variation>(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<FeatureFlag>(), It.IsAny<string>(), - It.IsAny<UserAttributes>(), ProjectConfig)).Returns(expectedDecision); + It.IsAny<UserAttributes>(), ProjectConfig, It.IsAny<List<OptimizelyDecideOption>>(), It.IsAny<IDecisionReasons>())).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<FeatureFlag>(), It.IsAny<string>(), - It.IsAny<UserAttributes>(), ProjectConfig)).Returns<Variation>(null); + It.IsAny<UserAttributes>(), ProjectConfig, It.IsAny<List<OptimizelyDecideOption>>(), It.IsAny<IDecisionReasons>())).Returns<Variation>(null); DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureRollout(It.IsAny<FeatureFlag>(), It.IsAny<string>(), - It.IsAny<UserAttributes>(), ProjectConfig)).Returns(expectedDecision); + It.IsAny<UserAttributes>(), ProjectConfig, It.IsAny<IDecisionReasons>())).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<FeatureFlag>(), It.IsAny<string>(), It.IsAny<UserAttributes>(), ProjectConfig)).Returns<Variation>(null); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureRollout(It.IsAny<FeatureFlag>(), It.IsAny<string>(), It.IsAny<UserAttributes>(), ProjectConfig)).Returns<Variation>(null); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny<FeatureFlag>(), It.IsAny<string>(), It.IsAny<UserAttributes>(), ProjectConfig, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance())).Returns<Variation>(null); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureRollout(It.IsAny<FeatureFlag>(), It.IsAny<string>(), It.IsAny<UserAttributes>(), ProjectConfig, DefaultDecisionReasons.NewInstance())).Returns<Variation>(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<List<OptimizelyDecideOption>>(), It.IsAny<IDecisionReasons>())).Returns(variation); + var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig, new List<OptimizelyDecideOption>(), 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<string>(), It.IsAny<string>())).Returns(rolloutVariation); - var actualRolloutDecision = DecisionServiceMock.Object.GetVariationForFeatureRollout(featureFlag, "user1", userAttributes, ProjectConfig); + BucketerMock.Setup(bm => bm.Bucket(ProjectConfig, rolloutExperiment, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>())).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<ILogger> LoggerMock; + private Mock<IErrorHandler> ErrorHandlerMock; + private Mock<IEventDispatcher> EventDispatcherMock; + + [SetUp] + public void SetUp() + { + LoggerMock = new Mock<ILogger>(); + LoggerMock.Setup(i => i.Log(It.IsAny<LogLevel>(), It.IsAny<string>())); + + ErrorHandlerMock = new Mock<IErrorHandler>(); + ErrorHandlerMock.Setup(e => e.HandleError(It.IsAny<Exception>())); + EventDispatcherMock = new Mock<IEventDispatcher>(); + + 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<string, object>()); + 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 /// <param name="experiment">Experiment Experiment in which user is to be bucketed</param> /// <param name="bucketingId">A customer-assigned value used to create the key for the murmur hash.</param> /// <param name="userId">User identifier</param> + /// <param name="reasons">Decision log messages.</param> /// <returns>Variation which will be shown to the user</returns> - 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 /// <param name = "userId" > The userId of the user. /// <param name = "filteredAttributes" > The user's attributes. This should be filtered to just attributes in the Datafile.</param> /// <returns>The Variation the user is allocated into.</returns> - 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<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance()); + } + + /// <summary> + /// Get a Variation of an Experiment for a user to be allocated into. + /// </summary> + /// <param name = "experiment" > The Experiment the user will be bucketed into.</param> + /// <param name = "userId" > The userId of the user. + /// <param name = "filteredAttributes" > The user's attributes. This should be filtered to just attributes in the Datafile.</param> + /// <returns>The Variation the user is allocated into.</returns> + public virtual Variation GetVariation(Experiment experiment, + string userId, + ProjectConfig config, + UserAttributes filteredAttributes, + List<OptimizelyDecideOption> 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<string, Decision>()); - 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 /// <param name="config">Project Config</param> /// <returns>Variation entity which the given user and experiment should be forced into.</returns> public Variation GetForcedVariation(string experimentKey, string userId, ProjectConfig config) + { + return GetForcedVariation(experimentKey, userId, config, DefaultDecisionReasons.NewInstance()); + } + + /// <summary> + /// Gets the forced variation for the given user and experiment. + /// </summary> + /// <param name="experimentKey">The experiment key</param> + /// <param name="userId">The user ID</param> + /// <param name="config">Project Config</param> + /// <returns>Variation entity which the given user and experiment should be forced into.</returns> + 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; } - - /// <summary> /// Get the variation the user has been whitelisted into. /// </summary> @@ -258,6 +291,19 @@ public bool SetForcedVariation(string experimentKey, string userId, string varia /// <returns>if the user is not whitelisted into any variation {@link Variation} /// the user is bucketed into if the user has a specified whitelisted variation.</returns> public Variation GetWhitelistedVariation(Experiment experiment, string userId) + { + return GetWhitelistedVariation(experiment, userId, DefaultDecisionReasons.NewInstance()); + } + + /// <summary> + /// Get the variation the user has been whitelisted into. + /// </summary> + /// <param name = "experiment" >in which user is to be bucketed.</param> + /// <param name = "userId" > User Identifier</param> + /// <param name = "reasons" > Decision log messages.</param> + /// <returns>if the user is not whitelisted into any variation {@link Variation} + /// the user is bucketed into if the user has a specified whitelisted variation.</returns> + public Variation GetWhitelistedVariation(Experiment experiment, string userId, IDecisionReasons reasons) { //if a user has a forced variation mapping, return the respective variation Dictionary<string, string> 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) /// <param name = "experiment" > which the user was bucketed</param> /// <param name = "userProfile" > User profile of the user</param> /// <returns>The user was previously bucketed into.</returns> - 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; } } - /// <summary> /// Save a { @link Variation } of an { @link Experiment } for a user in the {@link UserProfileService}. /// </summary> @@ -330,6 +375,17 @@ public Variation GetStoredVariation(Experiment experiment, UserProfile userProfi /// <param name = "variation" > The Variation to save.</param> /// <param name = "userProfile" > instance of the user information.</param> public void SaveVariation(Experiment experiment, Variation variation, UserProfile userProfile) + { + SaveVariation(experiment, variation, userProfile, DefaultDecisionReasons.NewInstance()); + } + + /// <summary> + /// Save a { @link Variation } of an { @link Experiment } for a user in the {@link UserProfileService}. + /// </summary> + /// <param name = "experiment" > The experiment the user was buck</param> + /// <param name = "variation" > The Variation to save.</param> + /// <param name = "userProfile" > instance of the user information.</param> + 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)); } } - + /// <summary> /// 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 /// <param name = "featureFlag" >The feature flag the user wants to access.</param> /// <param name = "userId" >User Identifier</param> /// <param name = "filteredAttributes" >The user's attributes. This should be filtered to just attributes in the Datafile.</param> + /// <param name = "reasons" >Decision log messages.</param> /// <returns>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</returns> - 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 /// <param name = "filteredAttributes" >The user's attributes. This should be filtered to just attributes in the Datafile.</param> /// <returns>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</returns> - 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<OptimizelyDecideOption> 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 /// <returns>null if the user is not bucketed into any variation or the FeatureDecision entity if the user is /// successfully bucketed.</returns> public virtual FeatureDecision GetVariationForFeature(FeatureFlag featureFlag, string userId, ProjectConfig config, UserAttributes filteredAttributes) + { + return GetVariationForFeature(featureFlag, userId, config, filteredAttributes, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance()); + } + + /// <summary> + /// Get the variation the user is bucketed into for the FeatureFlag + /// </summary> + /// <param name = "featureFlag" >The feature flag the user wants to access.</param> + /// <param name = "userId" >User Identifier</param> + /// <param name = "filteredAttributes" >The user's attributes. This should be filtered to just attributes in the Datafile.</param> + /// <param name = "filteredAttributes" >The user's attributes. This should be filtered to just attributes in the Datafile.</param> + /// <param name = "options" >An array of decision options.</param> + /// <param name = "reasons" >Decision log messages.</param> + /// <returns>null if the user is not bucketed into any variation or the FeatureDecision entity if the user is + /// successfully bucketed.</returns> + public virtual FeatureDecision GetVariationForFeature(FeatureFlag featureFlag, + string userId, + ProjectConfig config, + UserAttributes filteredAttributes, + List<OptimizelyDecideOption> 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 /// <param name = "userId" >User Identifier</param> /// <param name = "filteredAttributes" >The user's attributes.</param> /// <returns>Bucketing Id if it is a string type in attributes, user Id otherwise.</returns> - 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<string, string>(); - 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<string, string>(); + 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<string, object> - { - { "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<string, object> + { + { "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<OptimizelyDecideOption> 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<string, string> + { + { 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<string, object>(); + 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<string, object> + { + { "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<OptimizelyDecideOption> GetAllOptions(List<OptimizelyDecideOption> options) + { + var copiedOptions = new List<OptimizelyDecideOption>(DefaultDecideOptions); + if (options != null) + { + copiedOptions.AddRange(options); + } + return copiedOptions; + } + /// <summary> /// Sends impression event. /// </summary> 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 /// <param name="value">value An attribute value</param> public void SetAttribute(string key, object value) { - UserAttributes.Add(key, value); + UserAttributes[key] = value; } /// <summary> @@ -74,7 +74,7 @@ public OptimizelyDecision Decide(string key) public OptimizelyDecision Decide(string key, List<OptimizelyDecideOption> options) { - return null; + return Optimizely.Decide(this, key, options); } /// <summary> 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; } + /// <summary> + /// Check if the user meets audience conditions to be in experiment or not + /// </summary> + /// <param name="config">ProjectConfig Configuration for the project</param> + /// <param name="experiment">Experiment Entity representing the experiment</param> + /// <param name="userAttributes">Attributes of the user. Defaults to empty attributes array if not provided</param> + /// <param name="loggingKeyType">It can be either experiment or rule.</param> + /// <param name="loggingKey">In case loggingKeyType is experiment it will be experiment key or else it will be rule number.</param> + /// <returns>true if the user meets audience conditions to be in experiment, false otherwise.</returns> + 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); + } /// <summary> /// 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) /// <param name="userAttributes">Attributes of the user. Defaults to empty attributes array if not provided</param> /// <param name="loggingKeyType">It can be either experiment or rule.</param> /// <param name="loggingKey">In case loggingKeyType is experiment it will be experiment key or else it will be rule number.</param> + /// <param name="reasons">Decision log messages.</param> /// <returns>true if the user meets audience conditions to be in experiment, false otherwise.</returns> 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 591c770e14b8a336972fbce1ec276b4c938793cf Mon Sep 17 00:00:00 2001 From: muhammadnoman <muhammadnoman@folio3.com> Date: Fri, 13 Nov 2020 20:41:31 +0500 Subject: [PATCH 10/19] 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<string, object>() { + var variableMap = new Dictionary<string, object>() { { "strField", "john doe" }, { "intField", 12 }, { "objectField", new Dictionary<string, object> () { @@ -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<string>()); + 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>() { 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<ILogger> LoggerMock; + private Mock<IErrorHandler> ErrorHandlerMock; + private Mock<IEventDispatcher> EventDispatcherMock; + + [SetUp] + public void SetUp() + { + LoggerMock = new Mock<ILogger>(); + LoggerMock.Setup(i => i.Log(It.IsAny<LogLevel>(), It.IsAny<string>())); + + ErrorHandlerMock = new Mock<IErrorHandler>(); + ErrorHandlerMock.Setup(e => e.HandleError(It.IsAny<Exception>())); + EventDispatcherMock = new Mock<IEventDispatcher>(); + + 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<OptimizelyDecideOption> DefaultDecideOptions; + private OptimizelyDecideOption[] DefaultDecideOptions; /// <summary> /// 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<OptimizelyDecideOption> 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<OptimizelyDecideOption> 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<OptimizelyDecideOption>(); + DefaultDecideOptions = defaultDecideOptions ?? new OptimizelyDecideOption[0]; } /// <summary> @@ -688,8 +688,6 @@ public OptimizelyJSON GetFeatureVariableJSON(string featureKey, string variableK return GetFeatureVariableValueForType<OptimizelyJSON>(featureKey, variableKey, userId, userAttributes, FeatureVariable.JSON_TYPE); } - //============ decide ============// - /// <summary> /// 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 { + /// <summary> + /// NewErrorDecision returns a decision with errors only + /// </summary> public class ErrorsDecisionReasons : IDecisionReasons { private readonly List<string> errors = new List<string>(); 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 { + /// <summary> + /// OptimizelyDecision defines the decision returned by decide api. + /// </summary> 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<string> 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<string> reasons) + string[] reasons) { VariationKey = variationKey; Enabled = enabled; @@ -47,6 +57,12 @@ public OptimizelyDecision(string variationKey, Reasons = reasons; } + /// <summary> + /// 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 + /// </summary> 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<string>() { 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 { + /// <summary> + /// OptimizelyUserContext defines user contexts that the SDK will use to make decisions for + /// </summary> 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 /// <param name="value">value An attribute value</param> 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); + } } /// <summary> @@ -74,7 +88,7 @@ public OptimizelyDecision Decide(string key) public OptimizelyDecision Decide(string key, List<OptimizelyDecideOption> options) { - return null; + throw new NotImplementedException(); } /// <summary> @@ -84,7 +98,7 @@ public OptimizelyDecision Decide(string key, /// <returns>A dictionary of all decision results, mapped by flag keys.</returns> public Dictionary<string, OptimizelyDecision> DecideForKeys(List<string> keys) { - return null; + throw new NotImplementedException(); } /// <summary> @@ -104,7 +118,7 @@ public Dictionary<string, OptimizelyDecision> DecideAll() /// <returns>All decision results mapped by flag keys.</returns> public Dictionary<string, OptimizelyDecision> DecideAll(List<OptimizelyDecideOption> options) { - return null; + throw new NotImplementedException(); } /// <summary> From 055bb93ded6e3fff94078a272664f96513f32d8f Mon Sep 17 00:00:00 2001 From: muhammadnoman <muhammadnoman@folio3.com> Date: Fri, 13 Nov 2020 21:47:52 +0500 Subject: [PATCH 11/19] 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<string, object>()); - 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); } + /// <summary> + /// Returns a decision result ({@link OptimizelyDecision}) for a given flag key and a user context, which contains all data required to deliver the flag. + /// <ul> + /// <li>If the SDK finds an error, it’ll return a decision with <b>null</b> for <b>variationKey</b>. The decision will include an error message in <b>reasons</b>. + /// </ul> + /// </summary> + /// <param name="key">A flag key for which a decision will be made.</param> + /// <param name="options">A list of options for decision-making.</param> + /// <returns>A decision result.</returns> public OptimizelyDecision Decide(OptimizelyUserContext user, string key, List<OptimizelyDecideOption> 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<string, string> - { - { 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 <muhammadnoman@folio3.com> Date: Fri, 13 Nov 2020 21:54:30 +0500 Subject: [PATCH 12/19] 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 fafb14ce73fbbfcda44c2d0e889bb029e457cf2a Mon Sep 17 00:00:00 2001 From: muhammadnoman <muhammadnoman@folio3.com> Date: Tue, 24 Nov 2020 19:28:52 +0500 Subject: [PATCH 13/19] 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>() { 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 @@ <Compile Include="NotificationTests\NotificationCenterTests.cs" /> <Compile Include="ClientConfigHandlerTest.cs" /> <Compile Include="OptimizelyTest.cs" /> + <Compile Include="OptimizelyUserContextTest.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="TestBucketer.cs" /> <Compile Include="BucketerTest.cs" /> 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<string> Errors = new List<string>(); - private List<string> Logs = new List<string>(); + private List<string> Infos = new List<string>(); public static IDecisionReasons NewInstance(List<OptimizelyDecideOption> 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<string> ToReport() { List<string> reasons = new List<string>(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 <muhammadnoman@folio3.com> Date: Tue, 24 Nov 2020 22:07:50 +0500 Subject: [PATCH 14/19] 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<string, Decision>()); 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 a0753a6bb1f3d8f48d6f00bad9bad7b903f52064 Mon Sep 17 00:00:00 2001 From: muhammadnoman <muhammadnoman@folio3.com> Date: Wed, 25 Nov 2020 19:42:14 +0500 Subject: [PATCH 15/19] 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 <muhammadnoman@folio3.com> Date: Thu, 26 Nov 2020 13:21:23 +0500 Subject: [PATCH 16/19] 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<OptimizelyDecideOption>(), 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<string> { "29039203" } }; - var decision = DecisionService.GetVariationForFeatureExperiment(featureFlag, GenericUserId, new UserAttributes() { }, ProjectConfig, new List<OptimizelyDecideOption>(), 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<Variation>(null); var featureFlag = ProjectConfig.GetFeatureFlagFromKey("multi_variate_feature"); - var decision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", new UserAttributes(), ProjectConfig, new List<OptimizelyDecideOption>(), 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<List<OptimizelyDecideOption>>(), It.IsAny<IDecisionReasons>())).Returns(variation); + "user1", ProjectConfig, userAttributes, It.IsAny<OptimizelyDecideOption[]> (), It.IsAny<IDecisionReasons>())).Returns(variation); var featureFlag = ProjectConfig.GetFeatureFlagFromKey("multi_variate_feature"); - var decision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig, new List<OptimizelyDecideOption>(), 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<OptimizelyDecideOption>(), 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<Experiment>(), It.IsAny<string>(), ProjectConfig, It.IsAny<UserAttributes>(), It.IsAny<List<OptimizelyDecideOption>>(), It.IsAny<IDecisionReasons>())). + DecisionServiceMock.Setup(ds => ds.GetVariation(It.IsAny<Experiment>(), It.IsAny<string>(), ProjectConfig, It.IsAny<UserAttributes>(), It.IsAny<OptimizelyDecideOption[]> (), It.IsAny<IDecisionReasons>())). Returns<Variation>(null); var featureFlag = ProjectConfig.GetFeatureFlagFromKey("boolean_feature"); - var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", new UserAttributes(), ProjectConfig, new List<OptimizelyDecideOption>(), 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<FeatureFlag>(), It.IsAny<string>(), It.IsAny<UserAttributes>(), ProjectConfig, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance())).Returns<Variation>(null); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny<FeatureFlag>(), It.IsAny<string>(), It.IsAny<UserAttributes>(), ProjectConfig, new OptimizelyDecideOption[] { }, DefaultDecisionReasons.NewInstance())).Returns<Variation>(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<FeatureFlag>(), It.IsAny<string>(), - It.IsAny<UserAttributes>(), ProjectConfig, It.IsAny<List<OptimizelyDecideOption>>(), It.IsAny<IDecisionReasons>())).Returns(expectedDecision); + It.IsAny<UserAttributes>(), ProjectConfig, It.IsAny<OptimizelyDecideOption[]>(), It.IsAny<IDecisionReasons>())).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<FeatureFlag>(), It.IsAny<string>(), - It.IsAny<UserAttributes>(), ProjectConfig, It.IsAny<List<OptimizelyDecideOption>>(), It.IsAny<IDecisionReasons>())).Returns<Variation>(null); + It.IsAny<UserAttributes>(), ProjectConfig, It.IsAny<OptimizelyDecideOption[]>(), It.IsAny<IDecisionReasons>())).Returns<Variation>(null); DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureRollout(It.IsAny<FeatureFlag>(), It.IsAny<string>(), It.IsAny<UserAttributes>(), ProjectConfig, It.IsAny<IDecisionReasons>())).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<FeatureFlag>(), It.IsAny<string>(), It.IsAny<UserAttributes>(), ProjectConfig, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance())).Returns<Variation>(null); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny<FeatureFlag>(), It.IsAny<string>(), It.IsAny<UserAttributes>(), ProjectConfig, new OptimizelyDecideOption[] { }, DefaultDecisionReasons.NewInstance())).Returns<Variation>(null); DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureRollout(It.IsAny<FeatureFlag>(), It.IsAny<string>(), It.IsAny<UserAttributes>(), ProjectConfig, DefaultDecisionReasons.NewInstance())).Returns<Variation>(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<List<OptimizelyDecideOption>>(), It.IsAny<IDecisionReasons>())).Returns(variation); - var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance()); + DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, "user1", ProjectConfig, userAttributes, It.IsAny<OptimizelyDecideOption[]>(), It.IsAny<IDecisionReasons>())).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>() { 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<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance()); + return GetVariation(experiment, userId, config, filteredAttributes, new OptimizelyDecideOption[] { }, DefaultDecisionReasons.NewInstance()); } /// <summary> @@ -103,7 +103,7 @@ public virtual Variation GetVariation(Experiment experiment, string userId, ProjectConfig config, UserAttributes filteredAttributes, - List<OptimizelyDecideOption> 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<OptimizelyDecideOption> options, + OptimizelyDecideOption[] options, IDecisionReasons reasons) { if (featureFlag == null) @@ -560,7 +560,7 @@ public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag feat /// successfully bucketed.</returns> public virtual FeatureDecision GetVariationForFeature(FeatureFlag featureFlag, string userId, ProjectConfig config, UserAttributes filteredAttributes) { - return GetVariationForFeature(featureFlag, userId, config, filteredAttributes, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance()); + return GetVariationForFeature(featureFlag, userId, config, filteredAttributes, new OptimizelyDecideOption[] { }, DefaultDecisionReasons.NewInstance()); } /// <summary> @@ -578,7 +578,7 @@ public virtual FeatureDecision GetVariationForFeature(FeatureFlag featureFlag, string userId, ProjectConfig config, UserAttributes filteredAttributes, - List<OptimizelyDecideOption> 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, /// <param name="key">A flag key for which a decision will be made.</param> /// <param name="options">A list of options for decision-making.</param> /// <returns>A decision result.</returns> - public OptimizelyDecision Decide(OptimizelyUserContext user, + internal OptimizelyDecision Decide(OptimizelyUserContext user, string key, - List<OptimizelyDecideOption> options) + OptimizelyDecideOption[] options) { var config = ProjectConfigManager?.GetConfig(); @@ -835,12 +836,13 @@ public OptimizelyDecision Decide(OptimizelyUserContext user, reasonsToReport.ToArray()); } - private List<OptimizelyDecideOption> GetAllOptions(List<OptimizelyDecideOption> options) + private OptimizelyDecideOption[] GetAllOptions(OptimizelyDecideOption[] options) { - var copiedOptions = new List<OptimizelyDecideOption>(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<string> Errors = new List<string>(); private List<string> Infos = new List<string>(); - public static IDecisionReasons NewInstance(List<OptimizelyDecideOption> 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) /// <returns>A decision result.</returns> public OptimizelyDecision Decide(string key) { - return Decide(key, new List<OptimizelyDecideOption>()); + return Decide(key, new OptimizelyDecideOption[] { }); } /// <summary> @@ -86,7 +86,7 @@ public OptimizelyDecision Decide(string key) /// <param name="options">A list of options for decision-making.</param> /// <returns>A decision result.</returns> public OptimizelyDecision Decide(string key, - List<OptimizelyDecideOption> options) + OptimizelyDecideOption[] options) { return Optimizely.Decide(this, key, options); } @@ -107,7 +107,7 @@ public Dictionary<string, OptimizelyDecision> DecideForKeys(List<string> keys) /// <returns>A dictionary of all decision results, mapped by flag keys.</returns> public Dictionary<string, OptimizelyDecision> DecideAll() { - return DecideAll(new List<OptimizelyDecideOption>()); + return DecideAll(new OptimizelyDecideOption[] { }); } @@ -116,7 +116,7 @@ public Dictionary<string, OptimizelyDecision> DecideAll() /// </summary> /// <param name="options">A list of options for decision-making.</param> /// <returns>All decision results mapped by flag keys.</returns> - public Dictionary<string, OptimizelyDecision> DecideAll(List<OptimizelyDecideOption> options) + public Dictionary<string, OptimizelyDecision> DecideAll(OptimizelyDecideOption[] options) { throw new NotImplementedException(); } From 54e9e747ed33462f7d142c8d1d2f6be529dd7891 Mon Sep 17 00:00:00 2001 From: Muhammad Noman <muhammadnoman@folio3.com> Date: Thu, 3 Dec 2020 03:59:25 +0500 Subject: [PATCH 17/19] 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<string> Errors = new List<string>(); + protected List<string> Errors = new List<string>(); private List<string> Infos = new List<string>(); public static IDecisionReasons NewInstance(List<OptimizelyDecideOption> 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<string> ToReport() + public virtual List<string> ToReport() { List<string> reasons = new List<string>(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 /// <summary> /// NewErrorDecision returns a decision with errors only /// </summary> - public class ErrorsDecisionReasons : IDecisionReasons + public class ErrorsDecisionReasons : DefaultDecisionReasons { - private readonly List<string> errors = new List<string>(); - 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<string> 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 <muhammadnoman@folio3.com> Date: Thu, 3 Dec 2020 04:19:18 +0500 Subject: [PATCH 18/19] 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<ILogger> 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<ILogger>(); + 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<LogLevel>(), It.IsAny<string>()), 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<LogLevel>(), It.IsAny<string>()), 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<LogLevel>(), It.IsAny<string>()), 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<LogLevel>(), It.IsAny<string>()), 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<LogLevel>(), It.IsAny<string>()), 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 eda27436b6605bda24967f4ed3d597f38ed066a3 Mon Sep 17 00:00:00 2001 From: Muhammad Noman <muhammadnoman@folio3.com> Date: Mon, 7 Dec 2020 17:39:47 +0500 Subject: [PATCH 19/19] 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]