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