diff --git a/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj b/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj index 0b479154..326fa803 100644 --- a/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj +++ b/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj @@ -119,6 +119,9 @@ Logger\NoOpLogger.cs + + + Notifications\NotificationCenter.cs Optimizely.cs diff --git a/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj b/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj index 2adbe146..3b1290d0 100644 --- a/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj +++ b/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj @@ -120,6 +120,9 @@ Logger\NoOpLogger.cs + + + Notifications\NotificationCenter.cs Optimizely.cs diff --git a/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj b/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj index 101e9828..73917fc5 100644 --- a/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj +++ b/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj @@ -38,6 +38,7 @@ + diff --git a/OptimizelySDK.Tests/DecisionServiceTest.cs b/OptimizelySDK.Tests/DecisionServiceTest.cs index bf743c8f..f766cd62 100644 --- a/OptimizelySDK.Tests/DecisionServiceTest.cs +++ b/OptimizelySDK.Tests/DecisionServiceTest.cs @@ -277,7 +277,6 @@ public void TestGetVariationSavesBucketedVariationIntoUserProfile() } [Test] - [ExpectedException] public void TestBucketLogsCorrectlyWhenUserProfileFailsToSave() { Experiment experiment = NoAudienceProjectConfig.Experiments[0]; @@ -300,8 +299,9 @@ public void TestBucketLogsCorrectlyWhenUserProfileFailsToSave() decisionService.SaveVariation(experiment, variation, saveUserProfile); LoggerMock.Verify(l => l.Log(LogLevel.ERROR, string.Format - ("Failed to save variation \"{0}\" of experiment \"{1}\" for user \"{2}\".", UserProfileId, variation.Id, experiment.Id)) + ("Failed to save variation \"{0}\" of experiment \"{1}\" for user \"{2}\".", variation.Id, experiment.Id, UserProfileId)) , Times.Once); + ErrorHandlerMock.Verify(er => er.HandleError(It.IsAny()), Times.Once); } [Test] diff --git a/OptimizelySDK.Tests/DefaultErrorHandlerTest.cs b/OptimizelySDK.Tests/DefaultErrorHandlerTest.cs new file mode 100644 index 00000000..07db4493 --- /dev/null +++ b/OptimizelySDK.Tests/DefaultErrorHandlerTest.cs @@ -0,0 +1,77 @@ +/** + * + * Copyright 2017, 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 System; +using System.Collections.Generic; +using Moq; +using OptimizelySDK.Logger; +using OptimizelySDK.ErrorHandler; +using OptimizelySDK.Entity; +using NUnit.Framework; +using OptimizelySDK.Bucketing; +using OptimizelySDK.Exceptions; + +namespace OptimizelySDK.Tests +{ + public class DefaultErrorHandlerTest + { + private DefaultErrorHandler DefaultErrorHandler; + private Mock LoggerMock; + + [SetUp] + public void Setup() + { + LoggerMock = new Mock(); + } + + [Test] + public void TestErrorHandlerMessage() + { + DefaultErrorHandler = new DefaultErrorHandler(LoggerMock.Object, false); + string testingException = "Testing exception"; + try + { + throw new OptimizelyException("Testing exception"); + } + catch(OptimizelyException ex) + { + DefaultErrorHandler.HandleError(ex); + } + + LoggerMock.Verify(log => log.Log(LogLevel.ERROR, testingException), Times.Once); + } + + [Test] + [ExpectedException] + public void TestErrorHandlerMessageWithThrowException() + { + DefaultErrorHandler = new DefaultErrorHandler(LoggerMock.Object, true); + string testingException = "Testing and throwing exception"; + try + { + throw new OptimizelyException("Testing exception"); + } + catch (OptimizelyException ex) + { + //have to throw exception. + DefaultErrorHandler.HandleError(ex); + } + + LoggerMock.Verify(log => log.Log(LogLevel.ERROR, testingException), Times.Once); + } + + } +} diff --git a/OptimizelySDK.Tests/NotificationTests/NotificationCenterTests.cs b/OptimizelySDK.Tests/NotificationTests/NotificationCenterTests.cs new file mode 100644 index 00000000..20eea771 --- /dev/null +++ b/OptimizelySDK.Tests/NotificationTests/NotificationCenterTests.cs @@ -0,0 +1,241 @@ +/* + * Copyright 2017, 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.Entity; +using OptimizelySDK.ErrorHandler; +using OptimizelySDK.Event; +using OptimizelySDK.Logger; +using OptimizelySDK.Notifications; +using System.Collections.Generic; +using NotificationType = OptimizelySDK.Notifications.NotificationCenter.NotificationType; + +namespace OptimizelySDK.Tests.NotificationTests +{ + public class NotificationCenterTests + { + private Mock LoggerMock; + private NotificationCenter NotificationCenter; + private TestNotificationCallbacks TestNotificationCallbacks; + + private NotificationType NotificationTypeActivate = NotificationType.Activate; + private NotificationType NotificationTypeTrack = NotificationType.Track; + + [SetUp] + public void Setup() + { + LoggerMock = new Mock(); + LoggerMock.Setup(i => i.Log(It.IsAny(), It.IsAny())); + + NotificationCenter = new NotificationCenter(LoggerMock.Object); + TestNotificationCallbacks = new TestNotificationCallbacks(); + } + + [Test] + public void TestAddAndRemoveNotificationListener() + { + // Verify that callback added successfully. + Assert.AreEqual(1, NotificationCenter.AddNotification(NotificationTypeActivate, TestNotificationCallbacks.TestActivateCallback)); + Assert.AreEqual(1, NotificationCenter.NotificationsCount); + + // Verify that callback removed successfully. + Assert.AreEqual(true, NotificationCenter.RemoveNotification(1)); + Assert.AreEqual(0, NotificationCenter.NotificationsCount); + + //Verify return false with invalid ID. + Assert.AreEqual(false, NotificationCenter.RemoveNotification(1)); + + // Verify that callback added successfully and return right notification ID. + Assert.AreEqual(NotificationCenter.NotificationId, NotificationCenter.AddNotification(NotificationTypeActivate, TestNotificationCallbacks.TestActivateCallback)); + Assert.AreEqual(1, NotificationCenter.NotificationsCount); + } + + [Test] + public void TestAddMultipleNotificationListeners() + { + NotificationCenter.AddNotification(NotificationTypeActivate, TestNotificationCallbacks.TestActivateCallback); + NotificationCenter.AddNotification(NotificationTypeActivate, TestNotificationCallbacks.TestAnotherActivateCallback); + + // Verify that multiple notifications will be added for same notification type. + Assert.AreEqual(2, NotificationCenter.NotificationsCount); + + // Verify that notifications of other types will also gets added successfully. + NotificationCenter.AddNotification(NotificationTypeTrack, TestNotificationCallbacks.TestTrackCallback); + Assert.AreEqual(3, NotificationCenter.NotificationsCount); + } + + [Test] + public void TestAddSameNotificationListenerMultipleTimes() + { + NotificationCenter.AddNotification(NotificationTypeActivate, TestNotificationCallbacks.TestActivateCallback); + + // Verify that adding same callback multiple times will gets failed. + Assert.AreEqual(-1, NotificationCenter.AddNotification(NotificationTypeActivate, TestNotificationCallbacks.TestActivateCallback)); + Assert.AreEqual(1, NotificationCenter.NotificationsCount); + LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "The notification callback already exists."), Times.Once); + } + + [Test] + public void TestAddInvalidNotificationListeners() + { + // Verify that AddNotification gets failed on adding invalid notification listeners. + Assert.AreEqual(0, NotificationCenter.AddNotification(NotificationTypeTrack, + TestNotificationCallbacks.TestActivateCallback)); + + + LoggerMock.Verify(l => l.Log(LogLevel.ERROR, $@"Invalid notification type provided for ""{NotificationTypeActivate}"" callback."), + Times.Once); + + // Verify that no notifion has been added. + Assert.AreEqual(0, NotificationCenter.NotificationsCount); + } + + [Test] + public void TestClearNotifications() + { + // Add decision notifications. + NotificationCenter.AddNotification(NotificationTypeActivate, TestNotificationCallbacks.TestActivateCallback); + NotificationCenter.AddNotification(NotificationTypeActivate, TestNotificationCallbacks.TestAnotherActivateCallback); + + // Add track notification. + NotificationCenter.AddNotification(NotificationTypeTrack, TestNotificationCallbacks.TestTrackCallback); + + // Verify that callbacks added successfully. + Assert.AreEqual(3, NotificationCenter.NotificationsCount); + + // Verify that only decision callbacks are removed. + NotificationCenter.ClearNotifications(NotificationTypeActivate); + Assert.AreEqual(1, NotificationCenter.NotificationsCount); + + // Verify that ClearNotifications does not break on calling twice for same type. + NotificationCenter.ClearNotifications(NotificationTypeActivate); + NotificationCenter.ClearNotifications(NotificationTypeActivate); + + // Verify that ClearNotifications does not break after calling ClearAllNotifications. + NotificationCenter.ClearAllNotifications(); + NotificationCenter.ClearNotifications(NotificationTypeTrack); + } + + [Test] + public void TestClearAllNotifications() + { + // Add decision notifications. + NotificationCenter.AddNotification(NotificationTypeActivate, TestNotificationCallbacks.TestActivateCallback); + NotificationCenter.AddNotification(NotificationTypeActivate, TestNotificationCallbacks.TestAnotherActivateCallback); + + // Add track notification. + NotificationCenter.AddNotification(NotificationTypeTrack, TestNotificationCallbacks.TestTrackCallback); + + // Verify that callbacks added successfully. + Assert.AreEqual(3, NotificationCenter.NotificationsCount); + + // Verify that ClearAllNotifications remove all the callbacks. + NotificationCenter.ClearAllNotifications(); + Assert.AreEqual(0, NotificationCenter.NotificationsCount); + + // Verify that ClearAllNotifications does not break on calling twice or after ClearNotifications. + NotificationCenter.ClearNotifications(NotificationTypeActivate); + NotificationCenter.ClearAllNotifications(); + NotificationCenter.ClearAllNotifications(); + } + + [Test] + public void TestSendNotifications() + { + var config = ProjectConfig.Create(TestData.Datafile, LoggerMock.Object, new NoOpErrorHandler()); + var logEventMocker = new Mock("http://mockedurl", new Dictionary(), "POST", new Dictionary()); + // Mocking notification callbacks. + var notificationCallbackMock = new Mock(); + + notificationCallbackMock.Setup(nc => nc.TestActivateCallback(It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny())); + + notificationCallbackMock.Setup(nc => nc.TestAnotherActivateCallback(It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); + + + // Adding decision notifications. + NotificationCenter.AddNotification(NotificationTypeActivate, notificationCallbackMock.Object.TestActivateCallback); + NotificationCenter.AddNotification(NotificationTypeActivate, notificationCallbackMock.Object.TestAnotherActivateCallback); + + + // Adding track notifications. + NotificationCenter.AddNotification(NotificationTypeTrack, notificationCallbackMock.Object.TestTrackCallback); + + // Fire decision type notifications. + NotificationCenter.SendNotifications(NotificationTypeActivate, config.GetExperimentFromKey("test_experiment"), + "testUser", new UserAttributes(), config.GetVariationFromId("test_experiment", "7722370027"), logEventMocker.Object); + + // Verify that only the registered notifications of decision type are called. + notificationCallbackMock.Verify(nc => nc.TestActivateCallback(It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + + notificationCallbackMock.Verify(nc => nc.TestAnotherActivateCallback(It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + + notificationCallbackMock.Verify(nc => nc.TestTrackCallback(It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + + + + + // Verify that after clearing notifications, SendNotification should not call any notification + // which were previously registered. + NotificationCenter.ClearAllNotifications(); + notificationCallbackMock.ResetCalls(); + + NotificationCenter.SendNotifications(NotificationTypeActivate, config.GetExperimentFromKey("test_experiment"), + "testUser", new UserAttributes(), config.GetVariationFromId("test_experiment", "7722370027"), null); + + + // Again verify notifications which were registered are not called. + notificationCallbackMock.Verify(nc => nc.TestActivateCallback(It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + + notificationCallbackMock.Verify(nc => nc.TestAnotherActivateCallback(It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + + notificationCallbackMock.Verify(nc => nc.TestTrackCallback(It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } + } + + #region Test Notification callbacks class. + + /// + /// Test class containing dummy notification callbacks. + /// + public class TestNotificationCallbacks + { + public virtual void TestActivateCallback(Experiment experiment, string userId, UserAttributes userAttributes, + Variation variation, LogEvent logEvent) { + } + + public virtual void TestAnotherActivateCallback(Experiment experiment, string userId, UserAttributes userAttributes, + Variation variation, LogEvent logEvent) { + } + + public virtual void TestTrackCallback(string eventKey, string userId, UserAttributes userAttributes, + EventTags eventTags, LogEvent logEvent) { + } + + public virtual void TestAnotherTrackCallback(string eventKey, string userId, UserAttributes userAttributes, + EventTags eventTags, LogEvent logEvent) { + } + } + #endregion // Test Notification callbacks class. +} diff --git a/OptimizelySDK.Tests/OptimizelySDK.Tests.csproj b/OptimizelySDK.Tests/OptimizelySDK.Tests.csproj index efd4162c..e9edfc4a 100644 --- a/OptimizelySDK.Tests/OptimizelySDK.Tests.csproj +++ b/OptimizelySDK.Tests/OptimizelySDK.Tests.csproj @@ -71,10 +71,12 @@ + + diff --git a/OptimizelySDK.Tests/OptimizelyTest.cs b/OptimizelySDK.Tests/OptimizelyTest.cs index dc5d6ec4..6746459f 100644 --- a/OptimizelySDK.Tests/OptimizelyTest.cs +++ b/OptimizelySDK.Tests/OptimizelyTest.cs @@ -26,6 +26,8 @@ using NUnit.Framework; using OptimizelySDK.Tests.UtilsTests; using OptimizelySDK.Bucketing; +using OptimizelySDK.Notifications; +using OptimizelySDK.Tests.NotificationTests; namespace OptimizelySDK.Tests { @@ -42,6 +44,8 @@ public class OptimizelyTest private OptimizelyHelper Helper; private Mock OptimizelyMock; private Mock DecisionServiceMock; + private NotificationCenter NotificationCenter; + private Mock NotificationCallbackMock; #region Test Life Cycle [SetUp] @@ -85,6 +89,9 @@ public void Initialize() DecisionServiceMock = new Mock(new Bucketer(LoggerMock.Object), ErrorHandlerMock.Object, Config, null, LoggerMock.Object); + + NotificationCenter = new NotificationCenter(LoggerMock.Object); + NotificationCallbackMock = new Mock(); } [TestFixtureTearDown] @@ -1495,5 +1502,145 @@ public void TestIsFeatureEnabledGivenFeatureFlagIsEnabledAndUserIsBeingExperimen } #endregion // Test IsFeatureEnabled method + + #region Test NotificationCenter + + [Test] + public void TestActivateListenerWithoutAttributes() + { + TestActivateListener(null); + } + + [Test] + public void TestActivateListenerWithAttributes() + { + var userAttributes = new UserAttributes + { + { "device_type", "iPhone" }, + { "company", "Optimizely" }, + { "location", "San Francisco" } + }; + + TestActivateListener(userAttributes); + } + + public void TestActivateListener(UserAttributes userAttributes) + { + var experimentKey = "test_experiment"; + var variationKey = "control"; + var featureKey = "boolean_feature"; + var experiment = Config.GetExperimentFromKey(experimentKey); + var variation = Config.GetVariationFromKey(experimentKey, variationKey); + var featureFlag = Config.GetFeatureFlagFromKey(featureKey); + var logEvent = new LogEvent("https://logx.optimizely.com/v1/events", OptimizelyHelper.SingleParameter, + "POST", new Dictionary { }); + + // Mocking objects. + NotificationCallbackMock.Setup(nc => nc.TestActivateCallback(It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny())); + NotificationCallbackMock.Setup(nc => nc.TestAnotherActivateCallback(It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny())); + NotificationCallbackMock.Setup(nc => nc.TestTrackCallback(It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny())); + EventBuilderMock.Setup(ebm => ebm.CreateImpressionEvent(It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny())).Returns(logEvent); + DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, TestUserId, userAttributes)).Returns(variation); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, userAttributes)).Returns(variation); + + // Adding notification listeners. + var notificationType = NotificationCenter.NotificationType.Activate; + NotificationCenter.AddNotification(notificationType, NotificationCallbackMock.Object.TestActivateCallback); + NotificationCenter.AddNotification(notificationType, NotificationCallbackMock.Object.TestAnotherActivateCallback); + + var optly = Helper.CreatePrivateOptimizely(); + optly.SetFieldOrProperty("NotificationCenter", NotificationCenter); + optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); + optly.SetFieldOrProperty("EventBuilder", EventBuilderMock.Object); + + // Calling Activate and IsFeatureEnabled. + optly.Invoke("Activate", experimentKey, TestUserId, userAttributes); + optly.Invoke("IsFeatureEnabled", featureKey, TestUserId, userAttributes); + + // Verify that all the registered callbacks are called once for both Activate and IsFeatureEnabled. + NotificationCallbackMock.Verify(nc => nc.TestActivateCallback(experiment, TestUserId, userAttributes, variation, logEvent), Times.Exactly(2)); + NotificationCallbackMock.Verify(nc => nc.TestAnotherActivateCallback(experiment, TestUserId, userAttributes, variation, logEvent), Times.Exactly(2)); + } + + [Test] + public void TestTrackListenerWithoutAttributesAndEventTags() + { + TestTrackListener(null, null); + } + + [Test] + public void TestTrackListenerWithAttributesWithoutEventTags() + { + var userAttributes = new UserAttributes + { + { "device_type", "iPhone" }, + { "company", "Optimizely" }, + { "location", "San Francisco" } + }; + + TestTrackListener(userAttributes, null); + } + + [Test] + public void TestTrackListenerWithAttributesAndEventTags() + { + var userAttributes = new UserAttributes + { + { "device_type", "iPhone" }, + { "company", "Optimizely" }, + { "location", "San Francisco" } + }; + + var eventTags = new EventTags + { + { "revenue", 42 } + }; + + TestTrackListener(userAttributes, eventTags); + } + + public void TestTrackListener(UserAttributes userAttributes, EventTags eventTags) + { + var experimentKey = "test_experiment"; + var variationKey = "control"; + var eventKey = "purchase"; + var experiment = Config.GetExperimentFromKey(experimentKey); + var variation = Config.GetVariationFromKey(experimentKey, variationKey); + var logEvent = new LogEvent("https://logx.optimizely.com/v1/events", OptimizelyHelper.SingleParameter, + "POST", new Dictionary { }); + + // Mocking objects. + NotificationCallbackMock.Setup(nc => nc.TestTrackCallback(It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny())); + NotificationCallbackMock.Setup(nc => nc.TestAnotherTrackCallback(It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny())); + EventBuilderMock.Setup(ebm => ebm.CreateConversionEvent(It.IsAny(), It.IsAny(), + It.IsAny>(), It.IsAny(), It.IsAny(), + It.IsAny())).Returns(logEvent); + DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, TestUserId, userAttributes)).Returns(variation); + + // Adding notification listeners. + var notificationType = NotificationCenter.NotificationType.Track; + NotificationCenter.AddNotification(notificationType, NotificationCallbackMock.Object.TestTrackCallback); + NotificationCenter.AddNotification(notificationType, NotificationCallbackMock.Object.TestAnotherTrackCallback); + + var optly = Helper.CreatePrivateOptimizely(); + optly.SetFieldOrProperty("NotificationCenter", NotificationCenter); + optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); + optly.SetFieldOrProperty("EventBuilder", EventBuilderMock.Object); + + // Calling Track. + optly.Invoke("Track", eventKey, TestUserId, userAttributes, eventTags); + + // Verify that all the registered callbacks for Track are called. + NotificationCallbackMock.Verify(nc => nc.TestTrackCallback(eventKey, TestUserId, userAttributes, eventTags, logEvent), Times.Exactly(1)); + NotificationCallbackMock.Verify(nc => nc.TestAnotherTrackCallback(eventKey, TestUserId, userAttributes, eventTags, logEvent), Times.Exactly(1)); + } + + #endregion // Test NotificationCenter } } diff --git a/OptimizelySDK/Bucketing/DecisionService.cs b/OptimizelySDK/Bucketing/DecisionService.cs index 8fc8cbbc..c3ea6638 100644 --- a/OptimizelySDK/Bucketing/DecisionService.cs +++ b/OptimizelySDK/Bucketing/DecisionService.cs @@ -241,8 +241,8 @@ public void SaveVariation(Experiment experiment, Variation variation, UserProfil } catch (Exception exception) { - Logger.Log(LogLevel.ERROR, string.Format("Failed to save variation \"{0}\" of experiment \"{1}\" for user \"{2}\": {3}.", - variation.Id, experiment.Id, userProfile.UserId, exception.Message)); + Logger.Log(LogLevel.ERROR, string.Format("Failed to save variation \"{0}\" of experiment \"{1}\" for user \"{2}\".", + variation.Id, experiment.Id, userProfile.UserId)); ErrorHandler.HandleError(new Exceptions.OptimizelyRuntimeException(exception.Message)); } } diff --git a/OptimizelySDK/Notifications/NotificationCenter.cs b/OptimizelySDK/Notifications/NotificationCenter.cs new file mode 100644 index 00000000..bdd6e7dc --- /dev/null +++ b/OptimizelySDK/Notifications/NotificationCenter.cs @@ -0,0 +1,242 @@ +/* + * Copyright 2017, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using OptimizelySDK.Entity; +using OptimizelySDK.Event; +using OptimizelySDK.Logger; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace OptimizelySDK.Notifications +{ + /// + /// NotificationCenter class for sending notifications. + /// + public class NotificationCenter + { + /// + /// Enum representing notification types. + /// + public enum NotificationType + { + Activate, // Activate called. + Track + }; + + /// + /// Delegate for activate notifcations. + /// + /// The experiment entity + /// The user identifier + /// Associative array of attributes for the user + /// The variation entity + /// The impression event + public delegate void ActivateCallback(Experiment experiment, string userId, UserAttributes userAttributes, + Variation variation, LogEvent logEvent); + + /// + /// Delegate for track notifcations. + /// + /// The event key + /// The user identifier + /// Associative array of attributes for the user + /// Associative array of EventTags representing metadata associated with the event + /// The conversion event + public delegate void TrackCallback(string eventKey, string userId, UserAttributes userAttributes, EventTags eventTags, + LogEvent logEvent); + + private ILogger Logger; + + // Notification Id represeting number of notifications. + public int NotificationId { get; private set; } = 1; + + // Associative array of notification type to notification id and notification pair. + private Dictionary> Notifications = + new Dictionary>(); + + /// + /// Property representing total notifications count. + /// + public int NotificationsCount + { + get + { + int notificationsCount = 0; + foreach (var notificationsMap in Notifications.Values) + { + notificationsCount += notificationsMap.Count; + } + + return notificationsCount; + } + } + + /// + /// NotificationCenter constructor + /// + /// The logger object + public NotificationCenter(ILogger logger = null) + { + Logger = logger ?? new NoOpLogger(); + + foreach (NotificationType notificationType in Enum.GetValues(typeof(NotificationType))) + { + Notifications[notificationType] = new Dictionary(); + } + } + + /// + /// Add a notification callback of decision type to the notification center. + /// + /// Notification type + /// Callback function to call when event gets triggered + /// int | 0 for invalid notification type, -1 for adding existing notification + /// or the notification id of newly added notification. + public int AddNotification(NotificationType notificationType, ActivateCallback activateCallback) + { + if (!IsNotificationTypeValid(notificationType, NotificationType.Activate)) + return 0; + + return AddNotification(notificationType, (object)activateCallback); + } + + /// + /// Add a notification callback of track type to the notification center. + /// + /// Notification type + /// Callback function to call when event gets triggered + /// int | 0 for invalid notification type, -1 for adding existing notification + /// or the notification id of newly added notification. + public int AddNotification(NotificationType notificationType, TrackCallback trackCallback) + { + if (!IsNotificationTypeValid(notificationType, NotificationType.Track)) + return 0; + + return AddNotification(notificationType, (object)trackCallback); + } + + + /// + /// Validate notification type. + /// + /// Provided notification type + /// expected notification type + /// true if notification type is valid, false otherwise + private bool IsNotificationTypeValid(NotificationType providedNotificationType, NotificationType expectedNotificationType) + { + if (providedNotificationType != expectedNotificationType) + { + Logger.Log(LogLevel.ERROR, $@"Invalid notification type provided for ""{expectedNotificationType}"" callback."); + return false; + } + + return true; + } + + /// + /// Add a notification callback to the notification center. + /// + /// Notification type + /// Callback function to call when event gets triggered + /// -1 for adding existing notification or the notification id of newly added notification. + private int AddNotification(NotificationType notificationType, object notificationCallback) + { + var notificationHoldersList = Notifications[notificationType]; + + if (!Notifications.ContainsKey(notificationType) || Notifications[notificationType].Count == 0) + Notifications[notificationType][NotificationId] = notificationCallback; + else + { + foreach(var notification in this.Notifications[notificationType]) + { + if ((Delegate)notification.Value == (Delegate)notificationCallback) + { + Logger.Log(LogLevel.ERROR, "The notification callback already exists."); + return -1; + } + } + + Notifications[notificationType][NotificationId] = notificationCallback; + } + + int retVal = NotificationId; + NotificationId += 1; + + return retVal; + } + + /// + /// Remove a previously added notification callback. + /// + /// Id of notification + /// Returns true if found and removed, false otherwise. + public bool RemoveNotification(int notificationId) + { + foreach(var key in Notifications.Keys) + { + if (Notifications[key] != null && Notifications[key].Any(notification => notification.Key == notificationId)) + { + Notifications[key].Remove(notificationId); + return true; + } + } + + return false; + } + + /// + /// Remove all notifications for the specified notification type. + /// + /// The notification type + public void ClearNotifications(NotificationType notificationType) + { + Notifications[notificationType].Clear(); + } + + /// + /// Removes all notifications. + /// + public void ClearAllNotifications() + { + foreach (var notificationsMap in Notifications.Values) + { + notificationsMap.Clear(); + } + } + + /// + /// Fire notifications of specified notification type when the event gets triggered. + /// + /// The notification type + /// Arguments to pass in notification callbacks + public void SendNotifications(NotificationType notificationType, params object[] args) + { + foreach (var notification in Notifications[notificationType]) + { + try + { + Delegate d = notification.Value as Delegate; + d.DynamicInvoke(args); + } + catch (Exception exception) + { + Logger.Log(LogLevel.ERROR, "Problem calling notify callback. Error: " + exception.Message); + } + } + } + } +} diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs index 9027f7e7..4a510238 100644 --- a/OptimizelySDK/Optimizely.cs +++ b/OptimizelySDK/Optimizely.cs @@ -20,6 +20,7 @@ using OptimizelySDK.Event.Dispatcher; using OptimizelySDK.Logger; using OptimizelySDK.Utils; +using OptimizelySDK.Notifications; using System; using System.Collections.Generic; using System.Reflection; @@ -28,7 +29,6 @@ namespace OptimizelySDK { public class Optimizely { - private Bucketer Bucketer; private EventBuilder EventBuilder; @@ -45,6 +45,8 @@ public class Optimizely private DecisionService DecisionService; + private NotificationCenter NotificationCenter; + public bool IsValid { get; private set; } public static String SDK_VERSION { @@ -91,6 +93,7 @@ public Optimizely(string datafile, Bucketer = new Bucketer(Logger); EventBuilder = new EventBuilder(Bucketer); UserProfileService = userProfileService; + NotificationCenter = new NotificationCenter(Logger); try { @@ -175,7 +178,7 @@ public string Activate(string experimentKey, string userId, UserAttributes userA userAttributes = userAttributes.FilterNullValues(Logger); } - SendImpressionEvent(experiment, variation.Id, userId, userAttributes); + SendImpressionEvent(experiment, variation, userId, userAttributes); return variation.Key; } @@ -262,6 +265,8 @@ public void Track(string eventKey, string userId, UserAttributes userAttributes Logger.Log(LogLevel.ERROR, string.Format("Unable to dispatch conversion event. Error {0}", exception.Message)); } + NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Track, eventKey, userId, + userAttributes, eventTags, conversionEvent); } else { @@ -360,9 +365,23 @@ public Variation GetForcedVariation(string experimentKey, string userId) var experiment = Config.GetExperimentForVariationId(variation.Id); if (!string.IsNullOrEmpty(experiment.Key)) - SendImpressionEvent(experiment, variation.Id, userId, userAttributes); + { + SendImpressionEvent(experiment, variation, userId, userAttributes); + } else + { + var audiences = new Audience[1]; + var rolloutRule = Config.GetRolloutRuleForVariationId(variation.Id); + + if (!string.IsNullOrEmpty(rolloutRule.Key) + && rolloutRule.AudienceIds != null + && rolloutRule.AudienceIds.Length > 0) + { + audiences[0] = Config.GetAudience(rolloutRule.AudienceIds[0]); + } + Logger.Log(LogLevel.INFO, $@"The user ""{userId}"" is not being experimented on feature ""{featureKey}""."); + } Logger.Log(LogLevel.INFO, $@"Feature flag ""{featureKey}"" is enabled for user ""{userId}""."); return true; @@ -533,15 +552,15 @@ public string GetFeatureVariableString(string featureKey, string variableKey, st /// Sends impression event. /// /// The experiment - /// The variation Id + /// The variation entity /// The user ID /// The user's attributes - private void SendImpressionEvent(Experiment experiment, string variationId, string userId, + private void SendImpressionEvent(Experiment experiment, Variation variation, string userId, UserAttributes userAttributes) { if (experiment.IsExperimentRunning) { - var impressionEvent = EventBuilder.CreateImpressionEvent(Config, experiment, variationId, userId, userAttributes); + var impressionEvent = EventBuilder.CreateImpressionEvent(Config, experiment, variation.Id, userId, userAttributes); Logger.Log(LogLevel.INFO, string.Format("Activating user {0} in experiment {1}.", userId, experiment.Key)); Logger.Log(LogLevel.DEBUG, string.Format("Dispatching impression event to URL {0} with params {1}.", impressionEvent.Url, impressionEvent.GetParamsAsJson())); @@ -554,6 +573,9 @@ private void SendImpressionEvent(Experiment experiment, string variationId, stri { Logger.Log(LogLevel.ERROR, string.Format("Unable to dispatch impression event. Error {0}", exception.Message)); } + + NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Activate, experiment, userId, + userAttributes, variation, impressionEvent); } else { diff --git a/OptimizelySDK/OptimizelySDK.csproj b/OptimizelySDK/OptimizelySDK.csproj index 5a072fc8..8e21fb92 100644 --- a/OptimizelySDK/OptimizelySDK.csproj +++ b/OptimizelySDK/OptimizelySDK.csproj @@ -93,6 +93,7 @@ + @@ -121,4 +122,4 @@ --> - + \ No newline at end of file diff --git a/OptimizelySDK/ProjectConfig.cs b/OptimizelySDK/ProjectConfig.cs index 539ebcc9..2e928a5b 100644 --- a/OptimizelySDK/ProjectConfig.cs +++ b/OptimizelySDK/ProjectConfig.cs @@ -132,6 +132,12 @@ private Dictionary> _VariationIdMap private Dictionary _VariationIdToExperimentMap = new Dictionary(); public Dictionary VariationIdToExperimentMap{ get { return _VariationIdToExperimentMap; } } + /// + /// Associative array of Variation ID to Rollout Rules(s) in the datafile + /// + private Dictionary _VariationIdToRolloutRuleMap = new Dictionary(); + public Dictionary VariationIdToRolloutRuleMap { get { return _VariationIdToRolloutRuleMap; } } + //========================= Callbacks =========================== /// @@ -248,6 +254,19 @@ private void Initialize() } } } + + // Generate Variation ID to Rollout Rule map. + foreach (var rollout in Rollouts) + { + foreach (var rolloutRule in rollout.Experiments) + { + if (rolloutRule.Variations != null) + { + foreach (var variation in rolloutRule.Variations) + _VariationIdToRolloutRuleMap[variation.Id] = rolloutRule; + } + } + } } public static ProjectConfig Create(string content, ILogger logger, IErrorHandler errorHandler) @@ -557,5 +576,21 @@ public Experiment GetExperimentForVariationId(string variationId) return new Experiment(); } + + /// + /// Get rollout rule for variation. + /// + /// Variation ID + /// Rollout Rule corresponding to the variation ID or a dummy entity if ID is invalid + public Experiment GetRolloutRuleForVariationId(string variationId) + { + if (_VariationIdToRolloutRuleMap.ContainsKey(variationId)) + return _VariationIdToRolloutRuleMap[variationId]; + + Logger.Log(LogLevel.ERROR, $@"No rollout rule has been defined in datafile for variation ""{variationId}""."); + ErrorHandler.HandleError(new Exceptions.InvalidVariationException("No rollout rule has been found for provided variation Id in the datafile.")); + + return new Experiment(); + } } } diff --git a/OptimizelySDK/Utils/ConfigParser.cs b/OptimizelySDK/Utils/ConfigParser.cs index 454a1237..1d1b73c0 100644 --- a/OptimizelySDK/Utils/ConfigParser.cs +++ b/OptimizelySDK/Utils/ConfigParser.cs @@ -32,17 +32,5 @@ public static Dictionary GenerateMap(IEnumerable entities, Func getKey(e), e => clone ? (T)e.Clone() : e); } - - /// - /// Creates an array of entities from the entity - /// (not sure this is really needed) - /// - /// Original Entities - /// Whether or not to clone the original entity - /// array of entities - public static T[] GenerateMap(IEnumerable entities, bool clone) - { - return entities.Select(e => clone ? (T)e.Clone() : e).ToArray(); - } } } \ No newline at end of file