diff --git a/OptimizelySDK.Tests/OptimizelyTest.cs b/OptimizelySDK.Tests/OptimizelyTest.cs index 5dab3aec..a0cca1d0 100644 --- a/OptimizelySDK.Tests/OptimizelyTest.cs +++ b/OptimizelySDK.Tests/OptimizelyTest.cs @@ -624,7 +624,7 @@ public void TestInvalidInstanceLogMessages() Times.Once); LoggerMock.Verify( log => log.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'Track'."), - Times.Once); + Times.Never); // Erroring on null or empty event key takes priority over invalid datafile. LoggerMock.Verify( log => log.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'IsFeatureEnabled'."), Times.Once); @@ -5668,20 +5668,41 @@ public void TestGetVariationValidateInputValues() [Test] public void TestTrackValidateInputValues() { - // Verify that ValidateStringInputs does not log error for valid values. - Optimizely.Track("purchase", "test_user"); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Provided User Id is in invalid format."), + const string GOOD_EVENT_KEY = "purchase"; + const string EXPECTED_EVENT_KEY_ERROR_MESSAGE = + "Event key cannot be null, empty, or whitespace string. Failing 'Track'."; + const string GOOD_USER = "test_user"; + const string EXPECTED_USER_ID_ERROR_MESSAGE = "Provided User Id is in invalid format."; + + Optimizely.Track(GOOD_EVENT_KEY, GOOD_USER); + LoggerMock.Verify(l => l.Log(LogLevel.ERROR, EXPECTED_USER_ID_ERROR_MESSAGE), Times.Never); LoggerMock.Verify( - l => l.Log(LogLevel.ERROR, "Provided Event Key is in invalid format."), + l => l.Log(LogLevel.ERROR, EXPECTED_EVENT_KEY_ERROR_MESSAGE), Times.Never); + LoggerMock.ResetCalls(); - // Verify that ValidateStringInputs logs error for invalid values. - Optimizely.Track("", null); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Provided User Id is in invalid format."), + Optimizely.Track("", GOOD_USER); + LoggerMock.Verify( + l => l.Log(LogLevel.ERROR, EXPECTED_EVENT_KEY_ERROR_MESSAGE), Times.Once); + LoggerMock.ResetCalls(); + + Optimizely.Track(" ", GOOD_USER); LoggerMock.Verify( - l => l.Log(LogLevel.ERROR, "Provided Event Key is in invalid format."), Times.Once); + l => l.Log(LogLevel.ERROR, EXPECTED_EVENT_KEY_ERROR_MESSAGE), + Times.Once); + LoggerMock.ResetCalls(); + + Optimizely.Track(null, GOOD_USER); + LoggerMock.Verify( + l => l.Log(LogLevel.ERROR, EXPECTED_EVENT_KEY_ERROR_MESSAGE), + Times.Once); + LoggerMock.ResetCalls(); + + Optimizely.Track(GOOD_EVENT_KEY, null); + LoggerMock.Verify(l => l.Log(LogLevel.ERROR, EXPECTED_USER_ID_ERROR_MESSAGE), + Times.Once); } #endregion Test ValidateStringInputs @@ -6139,8 +6160,10 @@ public static void SetCulture(string culture) [Test] public void TestSendOdpEventNullAction() { - var optly = new Optimizely(TestData.OdpIntegrationDatafile, logger: LoggerMock.Object, odpManager: OdpManagerMock.Object); - optly.SendOdpEvent(action: null, identifiers: new Dictionary(), type: "type"); + var optly = new Optimizely(TestData.OdpIntegrationDatafile, logger: LoggerMock.Object, + odpManager: OdpManagerMock.Object); + optly.SendOdpEvent(action: null, identifiers: new Dictionary(), + type: "type"); LoggerMock.Verify(l => l.Log(LogLevel.ERROR, Constants.ODP_INVALID_ACTION_MESSAGE), Times.Exactly(1)); @@ -6151,7 +6174,8 @@ public void TestSendOdpEventNullAction() public void TestSendOdpEventInvalidOptimizelyObject() { var optly = new Optimizely("Random datafile", null, LoggerMock.Object); - optly.SendOdpEvent("some_action", new Dictionary() { { "some_key", "some_value" } }, "some_event"); + optly.SendOdpEvent("some_action", + new Dictionary() { { "some_key", "some_value" } }, "some_event"); LoggerMock.Verify( l => l.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'SendOdpEvent'."), Times.Once); @@ -6160,8 +6184,10 @@ public void TestSendOdpEventInvalidOptimizelyObject() [Test] public void TestSendOdpEventEmptyStringAction() { - var optly = new Optimizely(TestData.OdpIntegrationDatafile, logger: LoggerMock.Object, odpManager: OdpManagerMock.Object); - optly.SendOdpEvent(action: "", identifiers: new Dictionary(), type: "type"); + var optly = new Optimizely(TestData.OdpIntegrationDatafile, logger: LoggerMock.Object, + odpManager: OdpManagerMock.Object); + optly.SendOdpEvent(action: "", identifiers: new Dictionary(), + type: "type"); LoggerMock.Verify(l => l.Log(LogLevel.ERROR, Constants.ODP_INVALID_ACTION_MESSAGE), Times.Exactly(1)); @@ -6172,7 +6198,8 @@ public void TestSendOdpEventEmptyStringAction() public void TestSendOdpEventNullType() { var identifiers = new Dictionary(); - var optly = new Optimizely(TestData.OdpIntegrationDatafile, logger: LoggerMock.Object, odpManager: OdpManagerMock.Object); + var optly = new Optimizely(TestData.OdpIntegrationDatafile, logger: LoggerMock.Object, + odpManager: OdpManagerMock.Object); optly.SendOdpEvent(action: "action", identifiers: identifiers, type: null); @@ -6188,7 +6215,8 @@ public void TestSendOdpEventNullType() public void TestSendOdpEventEmptyStringType() { var identifiers = new Dictionary(); - var optly = new Optimizely(TestData.OdpIntegrationDatafile, logger: LoggerMock.Object, odpManager: OdpManagerMock.Object); + var optly = new Optimizely(TestData.OdpIntegrationDatafile, logger: LoggerMock.Object, + odpManager: OdpManagerMock.Object); optly.SendOdpEvent(action: "action", identifiers: identifiers, type: ""); @@ -6210,7 +6238,8 @@ public void TestFetchQualifiedSegmentsInvalidOptimizelyObject() var optly = new Optimizely("Random datafile", null, LoggerMock.Object); optly.FetchQualifiedSegments("some_user", null); LoggerMock.Verify( - l => l.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'FetchQualifiedSegments'."), + l => l.Log(LogLevel.ERROR, + "Datafile has invalid format. Failing 'FetchQualifiedSegments'."), Times.Once); } diff --git a/OptimizelySDK/Config/HttpProjectConfigManager.cs b/OptimizelySDK/Config/HttpProjectConfigManager.cs index fecd0a92..ed9e5e29 100644 --- a/OptimizelySDK/Config/HttpProjectConfigManager.cs +++ b/OptimizelySDK/Config/HttpProjectConfigManager.cs @@ -19,6 +19,7 @@ #endif using System; +using System.Linq; using System.Net; using System.Threading.Tasks; using OptimizelySDK.ErrorHandler; diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs index 9da300f8..91503e03 100644 --- a/OptimizelySDK/Optimizely.cs +++ b/OptimizelySDK/Optimizely.cs @@ -35,6 +35,7 @@ using OptimizelySDK.OptlyConfig; using OptimizelySDK.OptimizelyDecisions; using System.Linq; +using System.Text.RegularExpressions; #if USE_ODP using OptimizelySDK.Odp; @@ -87,7 +88,7 @@ public static String SDK_VERSION { get { - // Example output: "2.1.0" . Should be kept in synch with NuGet package version. + // Example output: "2.1.0". Should be kept in sync with NuGet package version. #if NET35 || NET40 var assembly = Assembly.GetExecutingAssembly(); #else @@ -338,7 +339,7 @@ private bool ValidateInputs(string datafile, bool skipJsonValidation) /// /// Sends conversion event to Optimizely. /// - /// Event key representing the event which needs to be recorded + /// Event key representing the event (must not be null, empty, or whitespace) /// ID for user /// Attributes of the user /// eventTags array Hash representing metadata associated with the event. @@ -346,46 +347,52 @@ public void Track(string eventKey, string userId, UserAttributes userAttributes EventTags eventTags = null ) { - var config = ProjectConfigManager?.GetConfig(); - - if (config == null) + if (eventKey == null || Regex.IsMatch(eventKey, @"^\s*$")) { - Logger.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'Track'."); + Logger.Log(LogLevel.ERROR, + "Event key cannot be null, empty, or whitespace string. Failing 'Track'."); return; } var inputValues = new Dictionary - { { USER_ID, userId }, { EVENT_KEY, eventKey } }; + { + { USER_ID, userId }, + { EVENT_KEY, eventKey }, + }; if (!ValidateStringInputs(inputValues)) { return; } - var eevent = config.GetEvent(eventKey); - - if (eevent.Key == null) + var config = ProjectConfigManager?.GetConfig(); + if (config == null) { - Logger.Log(LogLevel.INFO, - string.Format("Not tracking user {0} for event {1}.", userId, eventKey)); + Logger.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'Track'."); return; } - if (eventTags != null) + var eventToTrack = config.GetEvent(eventKey); + if (eventToTrack.Key == null) { - eventTags = eventTags.FilterNullValues(Logger); + Logger.Log(LogLevel.INFO, $"Not tracking user {userId} for event {eventKey}."); + return; } + eventTags = eventTags?.FilterNullValues(Logger); + var userEvent = UserEventFactory.CreateConversionEvent(config, eventKey, userId, userAttributes, eventTags); + EventProcessor.Process(userEvent); - Logger.Log(LogLevel.INFO, - string.Format("Tracking event {0} for user {1}.", eventKey, userId)); + + Logger.Log(LogLevel.INFO, $"Tracking event {eventKey} for user {userId}."); if (NotificationCenter.GetNotificationCount(NotificationCenter.NotificationType.Track) > 0) { var conversionEvent = EventFactory.CreateLogEvent(userEvent, Logger); + NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Track, eventKey, userId, userAttributes, eventTags, conversionEvent); @@ -1347,7 +1354,8 @@ List segmentOptions if (config == null) { - Logger.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'FetchQualifiedSegments'."); + Logger.Log(LogLevel.ERROR, + "Datafile has invalid format. Failing 'FetchQualifiedSegments'."); return null; } @@ -1378,7 +1386,8 @@ internal void IdentifyUser(string userId) /// Dictionary for identifiers. The caller must provide at least one key-value pair. /// Type of event (defaults to `fullstack`) /// Optional event data in a key-value pair format - public void SendOdpEvent(string action, Dictionary identifiers, string type = Constants.ODP_EVENT_TYPE, + public void SendOdpEvent(string action, Dictionary identifiers, string type + = Constants.ODP_EVENT_TYPE, Dictionary data = null ) { diff --git a/OptimizelySDK/OptimizelyUserContext.cs b/OptimizelySDK/OptimizelyUserContext.cs index f7b37dac..0f095976 100644 --- a/OptimizelySDK/OptimizelyUserContext.cs +++ b/OptimizelySDK/OptimizelyUserContext.cs @@ -348,7 +348,7 @@ OptimizelyDecideOption[] options /// /// Track an event. /// - /// The event name. + /// The event name (must not be null, empty, or whitespace). public virtual void TrackEvent(string eventName) { TrackEvent(eventName, new EventTags()); @@ -357,7 +357,7 @@ public virtual void TrackEvent(string eventName) /// /// Track an event. /// - /// The event name. + /// The event name (must not be null, empty, or whitespace). /// A map of event tag names to event tag values. public virtual void TrackEvent(string eventName, EventTags eventTags