diff --git a/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj b/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj index 67870037..66204977 100644 --- a/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj +++ b/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj @@ -10,6 +10,9 @@ IOptimizely.cs + + Notifications\NotificationCenterRegistry.cs + Odp\Constants.cs @@ -40,9 +43,6 @@ Odp\Entity\OdpEvent.cs - - Odp\Entity\OptimizelySdkSettings.cs - Odp\Entity\Response.cs diff --git a/OptimizelySDK.Tests/OdpTests/OdpEventManagerTests.cs b/OptimizelySDK.Tests/OdpTests/OdpEventManagerTests.cs index 0fea8a82..2fd54753 100644 --- a/OptimizelySDK.Tests/OdpTests/OdpEventManagerTests.cs +++ b/OptimizelySDK.Tests/OdpTests/OdpEventManagerTests.cs @@ -1,5 +1,5 @@ /* - * Copyright 2022, Optimizely + * Copyright 2022-2023, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -155,12 +155,14 @@ public void Setup() [Test] public void ShouldLogAndDiscardEventsWhenEventManagerNotRunning() { - var eventManager = new OdpEventManager.Builder().WithOdpConfig(_odpConfig). + var eventManager = new OdpEventManager.Builder(). WithOdpEventApiManager(_mockApiManager.Object). WithLogger(_mockLogger.Object). - Build(startImmediately: false); + WithAutoStart(false). + Build(); + eventManager.UpdateSettings(_odpConfig); - // since we've not called start() then... + // since we've not called Start() then... eventManager.SendEvent(_testEvents[0]); // ...we should get a notice after trying to send an event @@ -173,11 +175,13 @@ public void ShouldLogAndDiscardEventsWhenEventManagerNotRunning() public void ShouldLogAndDiscardEventsWhenEventManagerConfigNotReady() { var mockOdpConfig = new Mock(API_KEY, API_HOST, _emptySegmentsToCheck); - mockOdpConfig.Setup(o => o.IsReady()).Returns(false); - var eventManager = new OdpEventManager.Builder().WithOdpConfig(mockOdpConfig.Object). + mockOdpConfig.Setup(o => o.IsReady()).Returns(false); // stay not ready + var eventManager = new OdpEventManager.Builder(). WithOdpEventApiManager(_mockApiManager.Object). WithLogger(_mockLogger.Object). - Build(startImmediately: false); // doing it manually in Act next + WithAutoStart(false). // start manually in Act + Build(); + eventManager.UpdateSettings(mockOdpConfig.Object); eventManager.Start(); // Log when Start() called eventManager.SendEvent(_testEvents[0]); // Log when enqueue attempted @@ -191,30 +195,33 @@ public void ShouldLogAndDiscardEventsWhenEventManagerConfigNotReady() public void ShouldLogWhenOdpNotIntegratedAndIdentifyUserCalled() { var mockOdpConfig = new Mock(API_KEY, API_HOST, _emptySegmentsToCheck); - mockOdpConfig.Setup(o => o.IsReady()).Returns(false); - var eventManager = new OdpEventManager.Builder().WithOdpConfig(mockOdpConfig.Object). + mockOdpConfig.Setup(o => o.IsReady()).Returns(false); // never ready + var eventManager = new OdpEventManager.Builder(). WithOdpEventApiManager(_mockApiManager.Object). WithLogger(_mockLogger.Object). - Build(); + Build(); // assumed AutoStart true; Logs 1x here + eventManager.UpdateSettings(mockOdpConfig.Object); // auto-start after update; Logs 1x here - eventManager.IdentifyUser(FS_USER_ID); + eventManager.IdentifyUser(FS_USER_ID); // Logs 1x here too _mockLogger.Verify( l => l.Log(LogLevel.WARN, Constants.ODP_NOT_INTEGRATED_MESSAGE), - Times.Exactly(2)); // during Start() and SendEvent() + Times.Exactly(3)); // during Start() and SendEvent() } [Test] public void ShouldLogWhenOdpNotIntegratedAndStartCalled() { var mockOdpConfig = new Mock(API_KEY, API_HOST, _emptySegmentsToCheck); - mockOdpConfig.Setup(o => o.IsReady()).Returns(false); - var eventManager = new OdpEventManager.Builder().WithOdpConfig(mockOdpConfig.Object). + mockOdpConfig.Setup(o => o.IsReady()).Returns(false); // since never ready + var eventManager = new OdpEventManager.Builder(). WithOdpEventApiManager(_mockApiManager.Object). WithLogger(_mockLogger.Object). - Build(startImmediately: false); // doing it manually in Act next + WithAutoStart(false). // doing it manually in Act next + Build(); + eventManager.UpdateSettings(mockOdpConfig.Object); - eventManager.Start(); + eventManager.Start(); // Log 1x here too _mockLogger.Verify(l => l.Log(LogLevel.WARN, Constants.ODP_NOT_INTEGRATED_MESSAGE), Times.Once); @@ -250,10 +257,11 @@ public void ShouldDiscardEventsWithInvalidData() "key-3", new DateTime() }, }); - var eventManager = new OdpEventManager.Builder().WithOdpConfig(_odpConfig). + var eventManager = new OdpEventManager.Builder(). WithOdpEventApiManager(_mockApiManager.Object). WithLogger(_mockLogger.Object). Build(); + eventManager.UpdateSettings(_odpConfig); eventManager.SendEvent(eventWithAnArray); eventManager.SendEvent(eventWithADate); @@ -271,16 +279,17 @@ public void ShouldAddAdditionalInformationToEachEvent() _mockApiManager.Setup(api => api.SendEvents(It.IsAny(), It.IsAny(), Capture.In(eventsCollector))). Callback(() => cde.Signal()); - var eventManager = new OdpEventManager.Builder().WithOdpConfig(_odpConfig). + var eventManager = new OdpEventManager.Builder(). WithOdpEventApiManager(_mockApiManager.Object). WithLogger(_mockLogger.Object). WithEventQueue(new BlockingCollection(10)). // max capacity of 10 WithBatchSize(10). WithFlushInterval(TimeSpan.FromMilliseconds(100)). Build(); + eventManager.UpdateSettings(_odpConfig); eventManager.SendEvent(_testEvents[0]); - cde.Wait(); + cde.Wait(MAX_COUNT_DOWN_EVENT_WAIT_MS); var eventsSentToApi = eventsCollector.FirstOrDefault(); var actualEvent = eventsSentToApi?.FirstOrDefault(); @@ -304,13 +313,14 @@ public void ShouldAddAdditionalInformationToEachEvent() [Test] public void ShouldAttemptToFlushAnEmptyQueueAtFlushInterval() { - var eventManager = new OdpEventManager.Builder().WithOdpConfig(_odpConfig). + var eventManager = new OdpEventManager.Builder(). WithOdpEventApiManager(_mockApiManager.Object). WithLogger(_mockLogger.Object). WithEventQueue(new BlockingCollection(10)). WithBatchSize(10). WithFlushInterval(TimeSpan.FromMilliseconds(100)). Build(); + eventManager.UpdateSettings(_odpConfig); // do not add events to the queue, but allow for // at least 3 flush intervals executions @@ -328,13 +338,14 @@ public void ShouldDispatchEventsInCorrectNumberOfBatches() a.SendEvents(It.IsAny(), It.IsAny(), It.IsAny>())). Returns(false); - var eventManager = new OdpEventManager.Builder().WithOdpConfig(_odpConfig). + var eventManager = new OdpEventManager.Builder(). WithOdpEventApiManager(_mockApiManager.Object). WithLogger(_mockLogger.Object). WithEventQueue(new BlockingCollection(10)). WithBatchSize(10). WithFlushInterval(TimeSpan.FromMilliseconds(500)). Build(); + eventManager.UpdateSettings(_odpConfig); for (int i = 0; i < 25; i++) { @@ -362,12 +373,13 @@ public void ShouldDispatchEventsWithCorrectPayload() Capture.In(eventCollector))). Callback(() => cde.Signal()). Returns(false); - var eventManager = new OdpEventManager.Builder().WithOdpConfig(_odpConfig). + var eventManager = new OdpEventManager.Builder(). WithOdpEventApiManager(_mockApiManager.Object). WithLogger(_mockLogger.Object). WithBatchSize(10). WithFlushInterval(TimeSpan.FromSeconds(1)). Build(); + eventManager.UpdateSettings(_odpConfig); _testEvents.ForEach(e => eventManager.SendEvent(e)); cde.Wait(MAX_COUNT_DOWN_EVENT_WAIT_MS); @@ -395,13 +407,14 @@ public void ShouldRetryFailedEvents() It.IsAny>())). Callback(() => cde.Signal()). Returns(true); - var eventManager = new OdpEventManager.Builder().WithOdpConfig(_odpConfig). + var eventManager = new OdpEventManager.Builder(). WithOdpEventApiManager(_mockApiManager.Object). WithLogger(_mockLogger.Object). WithEventQueue(new BlockingCollection(10)). WithBatchSize(2). WithFlushInterval(TimeSpan.FromMilliseconds(100)). Build(); + eventManager.UpdateSettings(_odpConfig); for (int i = 0; i < 4; i++) { @@ -419,13 +432,14 @@ public void ShouldRetryFailedEvents() [Test] public void ShouldFlushAllScheduledEventsBeforeStopping() { - var eventManager = new OdpEventManager.Builder().WithOdpConfig(_odpConfig). + var eventManager = new OdpEventManager.Builder(). WithOdpEventApiManager(_mockApiManager.Object). WithLogger(_mockLogger.Object). WithEventQueue(new BlockingCollection(100)). WithBatchSize(2). // small batch size WithFlushInterval(TimeSpan.FromSeconds(2)). // long flush interval Build(); + eventManager.UpdateSettings(_odpConfig); for (int i = 0; i < 25; i++) { @@ -453,12 +467,13 @@ public void ShouldPrepareCorrectPayloadForIdentifyUser() _mockApiManager.Setup(api => api.SendEvents(It.IsAny(), It.IsAny(), Capture.In(eventsCollector))). Callback(() => cde.Signal()); - var eventManager = new OdpEventManager.Builder().WithOdpConfig(_odpConfig). + var eventManager = new OdpEventManager.Builder(). WithOdpEventApiManager(_mockApiManager.Object). WithLogger(_mockLogger.Object). WithEventQueue(new BlockingCollection(1)). WithBatchSize(1). Build(); + eventManager.UpdateSettings(_odpConfig); eventManager.IdentifyUser(USER_ID); cde.Wait(MAX_COUNT_DOWN_EVENT_WAIT_MS); @@ -488,10 +503,11 @@ public void ShouldApplyUpdatedOdpConfigurationWhenAvailable() "1-item-cart", }; var differentOdpConfig = new OdpConfig(apiKey, apiHost, segmentsToCheck); - var eventManager = new OdpEventManager.Builder().WithOdpConfig(_odpConfig). + var eventManager = new OdpEventManager.Builder(). WithOdpEventApiManager(_mockApiManager.Object). WithLogger(_mockLogger.Object). Build(); + eventManager.UpdateSettings(_odpConfig); eventManager.UpdateSettings(differentOdpConfig); diff --git a/OptimizelySDK.Tests/OdpTests/OdpManagerTest.cs b/OptimizelySDK.Tests/OdpTests/OdpManagerTest.cs index 2f943a0c..d9d89c24 100644 --- a/OptimizelySDK.Tests/OdpTests/OdpManagerTest.cs +++ b/OptimizelySDK.Tests/OdpTests/OdpManagerTest.cs @@ -1,5 +1,5 @@ /* - * Copyright 2022 Optimizely + * Copyright 2022-2023 Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -67,7 +67,6 @@ public class OdpManagerTest private readonly List _emptySegmentsToCheck = new List(0); - private OdpConfig _odpConfig; private Mock _mockLogger; private Mock _mockOdpEventManager; private Mock _mockSegmentManager; @@ -75,7 +74,6 @@ public class OdpManagerTest [SetUp] public void Setup() { - _odpConfig = new OdpConfig(API_KEY, API_HOST, _emptySegmentsToCheck); _mockLogger = new Mock(); _mockOdpEventManager = new Mock(); _mockSegmentManager = new Mock(); @@ -86,7 +84,7 @@ public void ShouldStartEventManagerWhenOdpManagerIsInitialized() { _mockOdpEventManager.Setup(e => e.Start()); - _ = new OdpManager.Builder().WithOdpConfig(_odpConfig). + _ = new OdpManager.Builder(). WithSegmentManager(_mockSegmentManager.Object). WithEventManager(_mockOdpEventManager.Object). WithLogger(_mockLogger.Object). @@ -99,11 +97,12 @@ public void ShouldStartEventManagerWhenOdpManagerIsInitialized() public void ShouldStopEventManagerWhenCloseIsCalled() { _mockOdpEventManager.Setup(e => e.Stop()); - var manager = new OdpManager.Builder().WithOdpConfig(_odpConfig). + var manager = new OdpManager.Builder(). WithSegmentManager(_mockSegmentManager.Object). WithEventManager(_mockOdpEventManager.Object). WithLogger(_mockLogger.Object). Build(); + manager.UpdateSettings(API_KEY, API_HOST, _emptySegmentsToCheck); manager.Dispose(); @@ -116,7 +115,7 @@ public void ShouldUseNewSettingsInEventManagerWhenOdpConfigIsUpdated() var eventManagerParameterCollector = new List(); _mockOdpEventManager.Setup(e => e.UpdateSettings(Capture.In(eventManagerParameterCollector))); - var manager = new OdpManager.Builder().WithOdpConfig(_odpConfig). + var manager = new OdpManager.Builder(). WithSegmentManager(_mockSegmentManager.Object). WithEventManager(_mockOdpEventManager.Object). WithLogger(_mockLogger.Object). @@ -138,7 +137,7 @@ public void ShouldUseNewSettingsInSegmentManagerWhenOdpConfigIsUpdated() var segmentManagerParameterCollector = new List(); _mockSegmentManager.Setup(s => s.UpdateSettings(Capture.In(segmentManagerParameterCollector))); - var manager = new OdpManager.Builder().WithOdpConfig(_odpConfig). + var manager = new OdpManager.Builder(). WithSegmentManager(_mockSegmentManager.Object). WithEventManager(_mockOdpEventManager.Object). WithLogger(_mockLogger.Object). @@ -159,14 +158,17 @@ public void ShouldHandleOdpConfigSettingsNoChange() { _mockSegmentManager.Setup(s => s.UpdateSettings(It.IsAny())); _mockOdpEventManager.Setup(e => e.UpdateSettings(It.IsAny())); - var manager = new OdpManager.Builder().WithOdpConfig(_odpConfig). + var manager = new OdpManager.Builder(). WithSegmentManager(_mockSegmentManager.Object). WithEventManager(_mockOdpEventManager.Object). WithLogger(_mockLogger.Object). Build(); + manager.UpdateSettings(API_KEY, API_HOST, _emptySegmentsToCheck); // initial set + _mockOdpEventManager.ResetCalls(); + _mockSegmentManager.ResetCalls(); - var wasUpdated = manager.UpdateSettings(_odpConfig.ApiKey, _odpConfig.ApiHost, - _odpConfig.SegmentsToCheck); + // attempt to set with the same config + var wasUpdated = manager.UpdateSettings(API_KEY, API_HOST, _emptySegmentsToCheck); Assert.IsFalse(wasUpdated); _mockSegmentManager.Verify(s => s.UpdateSettings(It.IsAny()), Times.Never); @@ -176,9 +178,10 @@ public void ShouldHandleOdpConfigSettingsNoChange() [Test] public void ShouldUpdateSettingsWithReset() { - _mockSegmentManager.Setup(s => - s.UpdateSettings(It.IsAny())); - var manager = new OdpManager.Builder().WithOdpConfig(_odpConfig). + _mockOdpEventManager.Setup(e => e.UpdateSettings(It.IsAny())); + _mockSegmentManager.Setup(s => s.ResetCache()); + _mockSegmentManager.Setup(s => s.UpdateSettings(It.IsAny())); + var manager = new OdpManager.Builder(). WithSegmentManager(_mockSegmentManager.Object). WithEventManager(_mockOdpEventManager.Object). WithLogger(_mockLogger.Object). @@ -188,22 +191,31 @@ public void ShouldUpdateSettingsWithReset() _updatedSegmentsToCheck); Assert.IsTrue(wasUpdated); + _mockOdpEventManager.Verify(e=>e.UpdateSettings(It.IsAny()), Times.Once); _mockSegmentManager.Verify(s => s.ResetCache(), Times.Once); + _mockSegmentManager.Verify(s=>s.UpdateSettings(It.IsAny()), Times.Once); } [Test] public void ShouldDisableOdpThroughConfiguration() { - _mockOdpEventManager.Setup(e => e.SendEvent(It.IsAny())); + _mockOdpEventManager.Setup(e => e.Start()); _mockOdpEventManager.Setup(e => e.IsStarted).Returns(true); - var manager = new OdpManager.Builder().WithOdpConfig(_odpConfig). + _mockOdpEventManager.Setup(e => e.SendEvent(It.IsAny())); + _mockOdpEventManager.Setup(e => e.UpdateSettings(It.IsAny())); + var manager = new OdpManager.Builder(). WithEventManager(_mockOdpEventManager.Object). WithLogger(_mockLogger.Object). - Build(); + Build(); // auto-start event manager attempted, but no config + manager.UpdateSettings(API_KEY, API_HOST, + _emptySegmentsToCheck); // event manager config added + auto-start + // should send event manager.SendEvent(TEST_EVENT_TYPE, TEST_EVENT_ACTION, _testEventIdentifiers, _testEventData); + _mockOdpEventManager.Verify(e => e.Start(), Times.Once); + _mockOdpEventManager.Verify(e => e.UpdateSettings(It.IsAny()), Times.Once); _mockOdpEventManager.Verify(e => e.SendEvent(It.IsAny()), Times.Once); _mockLogger.Verify(l => l.Log(LogLevel.ERROR, "ODP event not dispatched (ODP disabled)."), Times.Never); @@ -211,12 +223,13 @@ public void ShouldDisableOdpThroughConfiguration() _mockOdpEventManager.ResetCalls(); _mockLogger.ResetCalls(); + // remove config and try sending again manager.UpdateSettings(string.Empty, string.Empty, _emptySegmentsToCheck); - manager.SendEvent(TEST_EVENT_TYPE, TEST_EVENT_ACTION, _testEventIdentifiers, _testEventData); manager.Dispose(); + // should not try to send and provide a log message _mockOdpEventManager.Verify(e => e.SendEvent(It.IsAny()), Times.Never); _mockLogger.Verify(l => l.Log(LogLevel.ERROR, "ODP event not dispatched (ODP disabled)."), Times.Once); @@ -225,11 +238,12 @@ public void ShouldDisableOdpThroughConfiguration() [Test] public void ShouldGetEventManager() { - var manager = new OdpManager.Builder().WithOdpConfig(_odpConfig). + var manager = new OdpManager.Builder(). WithSegmentManager(_mockSegmentManager.Object). WithEventManager(_mockOdpEventManager.Object). WithLogger(_mockLogger.Object). Build(); + manager.UpdateSettings(API_KEY, API_HOST, _emptySegmentsToCheck); Assert.IsNotNull(manager.EventManager); } @@ -237,11 +251,12 @@ public void ShouldGetEventManager() [Test] public void ShouldGetSegmentManager() { - var manager = new OdpManager.Builder().WithOdpConfig(_odpConfig). + var manager = new OdpManager.Builder(). WithSegmentManager(_mockSegmentManager.Object). WithEventManager(_mockOdpEventManager.Object). WithLogger(_mockLogger.Object). Build(); + manager.UpdateSettings(API_KEY, API_HOST, _emptySegmentsToCheck); Assert.IsNotNull(manager.SegmentManager); } @@ -251,10 +266,11 @@ public void ShouldIdentifyUserWhenOdpIsIntegrated() { _mockOdpEventManager.Setup(e => e.IdentifyUser(It.IsAny())); _mockOdpEventManager.Setup(e => e.IsStarted).Returns(true); - var manager = new OdpManager.Builder().WithOdpConfig(_odpConfig). + var manager = new OdpManager.Builder(). WithEventManager(_mockOdpEventManager.Object). WithLogger(_mockLogger.Object). Build(); + manager.UpdateSettings(API_KEY, API_HOST, _emptySegmentsToCheck); manager.IdentifyUser(VALID_FS_USER_ID); manager.Dispose(); @@ -268,10 +284,11 @@ public void ShouldNotIdentifyUserWhenOdpDisabled() { _mockOdpEventManager.Setup(e => e.IdentifyUser(It.IsAny())); _mockOdpEventManager.Setup(e => e.IsStarted).Returns(true); - var manager = new OdpManager.Builder().WithOdpConfig(_odpConfig). + var manager = new OdpManager.Builder(). WithEventManager(_mockOdpEventManager.Object). WithLogger(_mockLogger.Object). Build(false); + manager.UpdateSettings(API_KEY, API_HOST, _emptySegmentsToCheck); manager.IdentifyUser(VALID_FS_USER_ID); manager.Dispose(); @@ -286,10 +303,11 @@ public void ShouldSendEventWhenOdpIsIntegrated() { _mockOdpEventManager.Setup(e => e.SendEvent(It.IsAny())); _mockOdpEventManager.Setup(e => e.IsStarted).Returns(true); - var manager = new OdpManager.Builder().WithOdpConfig(_odpConfig). + var manager = new OdpManager.Builder(). WithEventManager(_mockOdpEventManager.Object). WithLogger(_mockLogger.Object). Build(); + manager.UpdateSettings(API_KEY, API_HOST, _emptySegmentsToCheck); manager.SendEvent(TEST_EVENT_TYPE, TEST_EVENT_ACTION, _testEventIdentifiers, _testEventData); @@ -301,11 +319,12 @@ public void ShouldSendEventWhenOdpIsIntegrated() [Test] public void ShouldNotSendEventOdpNotIntegrated() { - var odpConfig = new OdpConfig(string.Empty, string.Empty, _emptySegmentsToCheck); _mockOdpEventManager.Setup(e => e.SendEvent(It.IsAny())); - var manager = new OdpManager.Builder().WithOdpConfig(odpConfig). + var manager = new OdpManager.Builder(). + WithEventManager(_mockOdpEventManager.Object). WithLogger(_mockLogger.Object). Build(false); // do not enable + manager.UpdateSettings(string.Empty, string.Empty, _emptySegmentsToCheck); manager.SendEvent(TEST_EVENT_TYPE, TEST_EVENT_ACTION, _testEventIdentifiers, _testEventData); diff --git a/OptimizelySDK.Tests/OdpTests/OdpSegmentManagerTest.cs b/OptimizelySDK.Tests/OdpTests/OdpSegmentManagerTest.cs index c8abe650..78d6b56e 100644 --- a/OptimizelySDK.Tests/OdpTests/OdpSegmentManagerTest.cs +++ b/OptimizelySDK.Tests/OdpTests/OdpSegmentManagerTest.cs @@ -69,8 +69,9 @@ public void ShouldFetchSegmentsOnCacheMiss() _mockApiManager.Setup(a => a.FetchSegments(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())). Returns(segmentsToCheck.ToArray()); - var manager = new OdpSegmentManager(_odpConfig, _mockApiManager.Object, - Constants.DEFAULT_MAX_CACHE_SIZE, null, _mockLogger.Object, _mockCache.Object); + var manager = new OdpSegmentManager(_mockApiManager.Object, _mockCache.Object, + _mockLogger.Object); + manager.UpdateSettings(_odpConfig); var segments = manager.FetchQualifiedSegments(FS_USER_ID); @@ -98,8 +99,9 @@ public void ShouldFetchSegmentsSuccessOnCacheHit() _mockCache.Setup(c => c.Lookup(Capture.In(keyCollector))).Returns(segmentsToCheck); _mockApiManager.Setup(a => a.FetchSegments(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())); - var manager = new OdpSegmentManager(_odpConfig, _mockApiManager.Object, - Constants.DEFAULT_MAX_CACHE_SIZE, null, _mockLogger.Object, _mockCache.Object); + var manager = new OdpSegmentManager(_mockApiManager.Object, _mockCache.Object, + _mockLogger.Object); + manager.UpdateSettings(_odpConfig); var segments = manager.FetchQualifiedSegments(FS_USER_ID); @@ -124,8 +126,9 @@ public void ShouldHandleFetchSegmentsWithError() _mockApiManager.Setup(a => a.FetchSegments(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())). Returns(null as string[]); - var manager = new OdpSegmentManager(_odpConfig, _mockApiManager.Object, - Constants.DEFAULT_MAX_CACHE_SIZE, null, _mockLogger.Object, _mockCache.Object); + var manager = new OdpSegmentManager(_mockApiManager.Object, _mockCache.Object, + _mockLogger.Object); + manager.UpdateSettings(_odpConfig); var segments = manager.FetchQualifiedSegments(FS_USER_ID); @@ -143,8 +146,9 @@ public void ShouldHandleFetchSegmentsWithError() public void ShouldLogAndReturnAnEmptySetWhenNoSegmentsToCheck() { var odpConfig = new OdpConfig(API_KEY, API_HOST, new List(0)); - var manager = new OdpSegmentManager(odpConfig, _mockApiManager.Object, - Constants.DEFAULT_MAX_CACHE_SIZE, null, _mockLogger.Object, _mockCache.Object); + var manager = new OdpSegmentManager(_mockApiManager.Object, _mockCache.Object, + _mockLogger.Object); + manager.UpdateSettings(odpConfig); var segments = manager.FetchQualifiedSegments(FS_USER_ID); @@ -160,9 +164,10 @@ public void ShouldLogAndReturnNullWhenOdpConfigNotReady() { var mockOdpConfig = new Mock(API_KEY, API_HOST, new List(0)); mockOdpConfig.Setup(o => o.IsReady()).Returns(false); - var manager = new OdpSegmentManager(mockOdpConfig.Object, _mockApiManager.Object, - Constants.DEFAULT_MAX_CACHE_SIZE, null, _mockLogger.Object, _mockCache.Object); - + var manager = new OdpSegmentManager(_mockApiManager.Object, _mockCache.Object, + _mockLogger.Object); + manager.UpdateSettings(mockOdpConfig.Object); + var segments = manager.FetchQualifiedSegments(FS_USER_ID); Assert.IsNull(segments); @@ -174,8 +179,8 @@ public void ShouldLogAndReturnNullWhenOdpConfigNotReady() [Test] public void ShouldIgnoreCache() { - var manager = new OdpSegmentManager(_odpConfig, _mockApiManager.Object, - Constants.DEFAULT_MAX_CACHE_SIZE, null, _mockLogger.Object, _mockCache.Object); + var manager = new OdpSegmentManager(_mockApiManager.Object, _mockCache.Object, _mockLogger.Object); + manager.UpdateSettings(_odpConfig); manager.FetchQualifiedSegments(FS_USER_ID, new List { @@ -194,8 +199,8 @@ public void ShouldIgnoreCache() [Test] public void ShouldResetCache() { - var manager = new OdpSegmentManager(_odpConfig, _mockApiManager.Object, - Constants.DEFAULT_MAX_CACHE_SIZE, null, _mockLogger.Object, _mockCache.Object); + var manager = new OdpSegmentManager(_mockApiManager.Object, _mockCache.Object, _mockLogger.Object); + manager.UpdateSettings(_odpConfig); manager.FetchQualifiedSegments(FS_USER_ID, new List { @@ -216,8 +221,8 @@ public void ShouldMakeValidCacheKey() { var keyCollector = new List(); _mockCache.Setup(c => c.Lookup(Capture.In(keyCollector))); - var manager = new OdpSegmentManager(_odpConfig, _mockApiManager.Object, - Constants.DEFAULT_MAX_CACHE_SIZE, null, _mockLogger.Object, _mockCache.Object); + var manager = new OdpSegmentManager(_mockApiManager.Object, _mockCache.Object, _mockLogger.Object); + manager.UpdateSettings(_odpConfig); manager.FetchQualifiedSegments(FS_USER_ID); diff --git a/OptimizelySDK/Config/FallbackProjectConfigManager.cs b/OptimizelySDK/Config/FallbackProjectConfigManager.cs index b1044e45..fcd68709 100644 --- a/OptimizelySDK/Config/FallbackProjectConfigManager.cs +++ b/OptimizelySDK/Config/FallbackProjectConfigManager.cs @@ -1,5 +1,5 @@ /* - * Copyright 2019, 2022 Optimizely + * Copyright 2019, 2022-2023 Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,6 +45,17 @@ public ProjectConfig GetConfig() return ProjectConfig; } + /// + /// SDK Key for Fallback is not used and always null + /// + public string SdkKey + { + get + { + return null; + } + } + /// /// Access to current cached project configuration /// diff --git a/OptimizelySDK/Config/HttpProjectConfigManager.cs b/OptimizelySDK/Config/HttpProjectConfigManager.cs index 8b909755..54f82a83 100644 --- a/OptimizelySDK/Config/HttpProjectConfigManager.cs +++ b/OptimizelySDK/Config/HttpProjectConfigManager.cs @@ -1,11 +1,11 @@ /* - * Copyright 2019-2021, Optimizely + * Copyright 2019-2021, 2023 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 + * https://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, @@ -14,6 +14,10 @@ * limitations under the License. */ +#if !(NET35 || NET40 || NETSTANDARD1_6) +#define USE_ODP +#endif + using OptimizelySDK.ErrorHandler; using OptimizelySDK.Logger; using OptimizelySDK.Notifications; @@ -29,18 +33,37 @@ public class HttpProjectConfigManager : PollingProjectConfigManager private string Url; private string LastModifiedSince = string.Empty; private string DatafileAccessToken = string.Empty; - private HttpProjectConfigManager(TimeSpan period, string url, TimeSpan blockingTimeout, bool autoUpdate, ILogger logger, IErrorHandler errorHandler) + private string _sdkKey = string.Empty; + + private HttpProjectConfigManager(TimeSpan period, string url, TimeSpan blockingTimeout, + bool autoUpdate, ILogger logger, IErrorHandler errorHandler, string sdkKey + ) : base(period, blockingTimeout, autoUpdate, logger, errorHandler) { Url = url; + _sdkKey = sdkKey; } - private HttpProjectConfigManager(TimeSpan period, string url, TimeSpan blockingTimeout, bool autoUpdate, ILogger logger, IErrorHandler errorHandler, string datafileAccessToken) - : this(period, url, blockingTimeout, autoUpdate, logger, errorHandler) + private HttpProjectConfigManager(TimeSpan period, string url, TimeSpan blockingTimeout, + bool autoUpdate, ILogger logger, IErrorHandler errorHandler, string datafileAccessToken, + string sdkKey + ) + : this(period, url, blockingTimeout, autoUpdate, logger, errorHandler, sdkKey) { DatafileAccessToken = datafileAccessToken; } + /// + /// SDK key in use for this project + /// + public string SdkKey + { + get + { + return _sdkKey; + } + } + public Task OnReady() { return CompletableConfigManager.Task; @@ -59,21 +82,26 @@ public HttpClient() public HttpClient(System.Net.Http.HttpClient httpClient) : this() { - if (httpClient != null) { + if (httpClient != null) + { Client = httpClient; } } public static System.Net.Http.HttpClientHandler GetHttpClientHandler() { - var handler = new System.Net.Http.HttpClientHandler() { - AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate + var handler = new System.Net.Http.HttpClientHandler() + { + AutomaticDecompression = System.Net.DecompressionMethods.GZip | + System.Net.DecompressionMethods.Deflate }; return handler; } - public virtual Task SendAsync(System.Net.Http.HttpRequestMessage httpRequestMessage) + public virtual Task SendAsync( + System.Net.Http.HttpRequestMessage httpRequestMessage + ) { return Client.SendAsync(httpRequestMessage); } @@ -84,12 +112,13 @@ public static System.Net.Http.HttpClientHandler GetHttpClientHandler() static HttpProjectConfigManager() { Client = new HttpClient(); - } + } private string GetRemoteDatafileResponse() { Logger.Log(LogLevel.DEBUG, $"Making datafile request to url \"{Url}\""); - var request = new System.Net.Http.HttpRequestMessage { + var request = new System.Net.Http.HttpRequestMessage + { RequestUri = new Uri(Url), Method = System.Net.Http.HttpMethod.Get, }; @@ -98,16 +127,20 @@ private string GetRemoteDatafileResponse() if (!string.IsNullOrEmpty(LastModifiedSince)) request.Headers.Add("If-Modified-Since", LastModifiedSince); - if (!string.IsNullOrEmpty(DatafileAccessToken)) { - request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", DatafileAccessToken); + if (!string.IsNullOrEmpty(DatafileAccessToken)) + { + request.Headers.Authorization = + new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", + DatafileAccessToken); } - var httpResponse = Client.SendAsync(request); + var httpResponse = Client.SendAsync(request); httpResponse.Wait(); // Return from here if datafile is not modified. var result = httpResponse.Result; - if (!result.IsSuccessStatusCode) { + if (!result.IsSuccessStatusCode) + { Logger.Log(LogLevel.ERROR, $"Error fetching datafile \"{result.StatusCode}\""); return null; } @@ -122,9 +155,9 @@ private string GetRemoteDatafileResponse() var content = result.Content.ReadAsStringAsync(); content.Wait(); - return content.Result; + return content.Result; } -#elif NET40 +#elif NET40 private string GetRemoteDatafileResponse() { var request = (System.Net.HttpWebRequest)System.Net.WebRequest.Create(Url); @@ -164,19 +197,24 @@ protected override ProjectConfig Poll() return DatafileProjectConfig.Create(datafile, Logger, ErrorHandler); } - + public class Builder { private const long MAX_MILLISECONDS_LIMIT = 4294967294; private readonly TimeSpan DEFAULT_PERIOD = TimeSpan.FromMinutes(5); private readonly TimeSpan DEFAULT_BLOCKINGOUT_PERIOD = TimeSpan.FromSeconds(15); - private readonly string DEFAULT_FORMAT = "https://cdn.optimizely.com/datafiles/{0}.json"; - private readonly string DEFAULT_AUTHENTICATED_DATAFILE_FORMAT = "https://config.optimizely.com/datafiles/auth/{0}.json"; + + private readonly string DEFAULT_FORMAT = + "https://cdn.optimizely.com/datafiles/{0}.json"; + + private readonly string DEFAULT_AUTHENTICATED_DATAFILE_FORMAT = + "https://config.optimizely.com/datafiles/auth/{0}.json"; + private string Datafile; - private string DatafileAccessToken; + private string DatafileAccessToken; private string SdkKey; private string Url; - private string Format; + private string Format; private ILogger Logger; private IErrorHandler ErrorHandler; private TimeSpan Period; @@ -197,6 +235,7 @@ public Builder WithBlockingTimeoutPeriod(TimeSpan blockingTimeoutSpan) return this; } + public Builder WithDatafile(string datafile) { Datafile = datafile; @@ -241,7 +280,7 @@ public Builder WithFormat(string format) return this; } - + public Builder WithLogger(ILogger logger) { Logger = logger; @@ -263,7 +302,7 @@ public Builder WithAutoUpdate(bool autoUpdate) return this; } - public Builder WithStartByDefault(bool startByDefault=true) + public Builder WithStartByDefault(bool startByDefault = true) { StartByDefault = startByDefault; @@ -303,41 +342,62 @@ public HttpProjectConfigManager Build(bool defer) if (ErrorHandler == null) ErrorHandler = new DefaultErrorHandler(Logger, false); - if (string.IsNullOrEmpty(Format)) { - - if (string.IsNullOrEmpty(DatafileAccessToken)) { + if (string.IsNullOrEmpty(Format)) + { + if (string.IsNullOrEmpty(DatafileAccessToken)) + { Format = DEFAULT_FORMAT; - } else { + } + else + { Format = DEFAULT_AUTHENTICATED_DATAFILE_FORMAT; } } - if (string.IsNullOrEmpty(Url)) { - if (string.IsNullOrEmpty(SdkKey)) { + if (string.IsNullOrEmpty(Url)) + { + if (string.IsNullOrEmpty(SdkKey)) + { ErrorHandler.HandleError(new Exception("SdkKey cannot be null")); } + Url = string.Format(Format, SdkKey); } - if (IsPollingIntervalProvided && (Period.TotalMilliseconds <= 0 || Period.TotalMilliseconds > MAX_MILLISECONDS_LIMIT)) { - Logger.Log(LogLevel.DEBUG, $"Polling interval is not valid for periodic calls, using default period {DEFAULT_PERIOD.TotalMilliseconds}ms"); + if (IsPollingIntervalProvided && (Period.TotalMilliseconds <= 0 || + Period.TotalMilliseconds > + MAX_MILLISECONDS_LIMIT)) + { + Logger.Log(LogLevel.DEBUG, + $"Polling interval is not valid for periodic calls, using default period {DEFAULT_PERIOD.TotalMilliseconds}ms"); Period = DEFAULT_PERIOD; - } else if(!IsPollingIntervalProvided) { - Logger.Log(LogLevel.DEBUG, $"No polling interval provided, using default period {DEFAULT_PERIOD.TotalMilliseconds}ms"); + } + else if (!IsPollingIntervalProvided) + { + Logger.Log(LogLevel.DEBUG, + $"No polling interval provided, using default period {DEFAULT_PERIOD.TotalMilliseconds}ms"); Period = DEFAULT_PERIOD; } - - if (IsBlockingTimeoutProvided && (BlockingTimeoutSpan.TotalMilliseconds <= 0 || BlockingTimeoutSpan.TotalMilliseconds > MAX_MILLISECONDS_LIMIT)) { - Logger.Log(LogLevel.DEBUG, $"Blocking timeout is not valid, using default blocking timeout {DEFAULT_BLOCKINGOUT_PERIOD.TotalMilliseconds}ms"); + + if (IsBlockingTimeoutProvided && (BlockingTimeoutSpan.TotalMilliseconds <= 0 || + BlockingTimeoutSpan.TotalMilliseconds > + MAX_MILLISECONDS_LIMIT)) + { + Logger.Log(LogLevel.DEBUG, + $"Blocking timeout is not valid, using default blocking timeout {DEFAULT_BLOCKINGOUT_PERIOD.TotalMilliseconds}ms"); BlockingTimeoutSpan = DEFAULT_BLOCKINGOUT_PERIOD; - } else if(!IsBlockingTimeoutProvided) { - Logger.Log(LogLevel.DEBUG, $"No Blocking timeout provided, using default blocking timeout {DEFAULT_BLOCKINGOUT_PERIOD.TotalMilliseconds}ms"); + } + else if (!IsBlockingTimeoutProvided) + { + Logger.Log(LogLevel.DEBUG, + $"No Blocking timeout provided, using default blocking timeout {DEFAULT_BLOCKINGOUT_PERIOD.TotalMilliseconds}ms"); BlockingTimeoutSpan = DEFAULT_BLOCKINGOUT_PERIOD; } - - configManager = new HttpProjectConfigManager(Period, Url, BlockingTimeoutSpan, AutoUpdate, Logger, ErrorHandler, DatafileAccessToken); + + configManager = new HttpProjectConfigManager(Period, Url, BlockingTimeoutSpan, + AutoUpdate, Logger, ErrorHandler, DatafileAccessToken, SdkKey); if (Datafile != null) { @@ -351,11 +411,18 @@ public HttpProjectConfigManager Build(bool defer) Logger.Log(LogLevel.WARN, "Error parsing fallback datafile." + ex.Message); } } - - configManager.NotifyOnProjectConfigUpdate += () => { - NotificationCenter?.SendNotifications(NotificationCenter.NotificationType.OptimizelyConfigUpdate); + + configManager.NotifyOnProjectConfigUpdate += () => + { +#if USE_ODP + NotificationCenterRegistry.GetNotificationCenter(SdkKey). + SendNotifications( + NotificationCenter.NotificationType.OptimizelyConfigUpdate); +#endif + NotificationCenter?.SendNotifications(NotificationCenter.NotificationType. + OptimizelyConfigUpdate); }; - + if (StartByDefault) configManager.Start(); @@ -363,7 +430,7 @@ public HttpProjectConfigManager Build(bool defer) // Optionally block until config is available. if (!defer) configManager.GetConfig(); - + return configManager; } } diff --git a/OptimizelySDK/Config/PollingProjectConfigManager.cs b/OptimizelySDK/Config/PollingProjectConfigManager.cs index e21319fa..b7022a3b 100644 --- a/OptimizelySDK/Config/PollingProjectConfigManager.cs +++ b/OptimizelySDK/Config/PollingProjectConfigManager.cs @@ -1,5 +1,5 @@ /* - * Copyright 2019-2020, 2022 Optimizely + * Copyright 2019-2020, 2022-2023 Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,6 +46,7 @@ public abstract class PollingProjectConfigManager : ProjectConfigManager, protected IErrorHandler ErrorHandler { get; set; } protected TimeSpan BlockingTimeout; + public virtual string SdkKey { get; } protected TaskCompletionSource CompletableConfigManager = new TaskCompletionSource(); diff --git a/OptimizelySDK/Config/ProjectConfigManager.cs b/OptimizelySDK/Config/ProjectConfigManager.cs index 7fe9753e..bdd4e839 100644 --- a/OptimizelySDK/Config/ProjectConfigManager.cs +++ b/OptimizelySDK/Config/ProjectConfigManager.cs @@ -1,5 +1,5 @@ /* - * Copyright 2019, 2022 Optimizely + * Copyright 2019, 2022-2023 Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,11 @@ public interface ProjectConfigManager /// ProjectConfig instance ProjectConfig GetConfig(); + /// + /// SDK key in use for this project + /// + string SdkKey { get; } + /// /// Access to current cached project configuration /// diff --git a/OptimizelySDK/Notifications/NotificationCenterRegistry.cs b/OptimizelySDK/Notifications/NotificationCenterRegistry.cs new file mode 100644 index 00000000..8389926f --- /dev/null +++ b/OptimizelySDK/Notifications/NotificationCenterRegistry.cs @@ -0,0 +1,82 @@ +/* + * Copyright 2023, 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 + * + * https://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; + +namespace OptimizelySDK.Notifications +{ + internal static class NotificationCenterRegistry + { + private static readonly object _mutex = new object(); + + private static Dictionary _notificationCenters = + new Dictionary(); + + /// + /// Thread-safe access to the NotificationCenter + /// + /// Retrieve NotificationCenter based on SDK key + /// Logger to record events + /// NotificationCenter instance per SDK key + public static NotificationCenter GetNotificationCenter(string sdkKey, ILogger logger = null) + { + if (sdkKey == null) + { + logger?.Log(LogLevel.INFO, "No SDK key provided to GetNotificationCenter"); + return default; + } + + NotificationCenter notificationCenter; + lock (_mutex) + { + if (_notificationCenters.ContainsKey(sdkKey)) + { + notificationCenter = _notificationCenters[sdkKey]; + } + else + { + notificationCenter = new NotificationCenter(logger); + _notificationCenters[sdkKey] = notificationCenter; + } + } + + return notificationCenter; + } + + /// + /// Thread-safe removal of a NotificationCenter from the Registry + /// + /// SDK key identifying the target + public static void RemoveNotificationCenter(string sdkKey) + { + if (sdkKey == null) + { + return; + } + + lock (_mutex) + { + if (_notificationCenters.TryGetValue(sdkKey, + out NotificationCenter notificationCenter)) + { + notificationCenter.ClearAllNotifications(); + _notificationCenters.Remove(sdkKey); + } + } + } + } +} diff --git a/OptimizelySDK/Odp/Entity/OptimizelySdkSettings.cs b/OptimizelySDK/Odp/Entity/OptimizelySdkSettings.cs deleted file mode 100644 index 8e38d002..00000000 --- a/OptimizelySDK/Odp/Entity/OptimizelySdkSettings.cs +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2022 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 - * - * https://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.Odp.Entity -{ - public class OptimizelySdkSettings - { - /// - /// The maximum size of audience segments cache - cache is disabled if this is set to zero - /// - public int SegmentsCacheSize { get; set; } - - /// - /// The timeout in seconds of audience segments cache - timeout is disabled if this is set to zero - /// - public int SegmentsCacheTimeout { get; set; } - - /// - /// ODP features are disabled if this is set to true. - /// - public bool DisableOdp { get; set; } - - /// - /// Optimizely SDK Settings - /// - /// The maximum size of audience segments cache (optional. default = 100). Set to zero to disable caching - /// The timeout in seconds of audience segments cache (optional. default = 600). Set to zero to disable timeout - /// Set this flag to true (default = false) to disable ODP features - public OptimizelySdkSettings(int segmentsCacheSize = 100, int segmentsCacheTimeout = 600, - bool disableOdp = false - ) - { - SegmentsCacheSize = segmentsCacheSize; - SegmentsCacheTimeout = segmentsCacheTimeout; - DisableOdp = disableOdp; - } - } -} diff --git a/OptimizelySDK/Odp/LruCache.cs b/OptimizelySDK/Odp/LruCache.cs index a475c117..e6f3dd5c 100644 --- a/OptimizelySDK/Odp/LruCache.cs +++ b/OptimizelySDK/Odp/LruCache.cs @@ -1,5 +1,5 @@ /* - * Copyright 2022, Optimizely + * Copyright 2022-2023, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,8 +22,7 @@ namespace OptimizelySDK.Odp { - public class LruCache : ICache - where T : class + public class LruCache : ICache where T : class { /// /// The maximum number of elements that should be stored @@ -62,7 +61,7 @@ public LruCache(int? maxSize = null, ) { _mutex = new object(); - + _maxSize = Math.Max(0, maxSize ?? Constants.DEFAULT_MAX_CACHE_SIZE); _logger = logger ?? new DefaultLogger(); diff --git a/OptimizelySDK/Odp/OdpEventManager.cs b/OptimizelySDK/Odp/OdpEventManager.cs index 5fe3a3c7..1aa87069 100644 --- a/OptimizelySDK/Odp/OdpEventManager.cs +++ b/OptimizelySDK/Odp/OdpEventManager.cs @@ -1,5 +1,5 @@ /* - * Copyright 2022, Optimizely + * Copyright 2022-2023, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -91,6 +91,11 @@ public class OdpEventManager : IOdpEventManager, IDisposable /// private Dictionary _commonData; + /// + /// Indicates if OdpEventManager should start upon Build() and UpdateSettings() + /// + private bool _autoStart; + /// /// Clear all entries from the queue /// @@ -107,7 +112,7 @@ private void DropQueue() /// public void Start() { - if (!_odpConfig.IsReady()) + if (_odpConfig == null || !_odpConfig.IsReady()) { _logger.Log(LogLevel.WARN, Constants.ODP_NOT_INTEGRATED_MESSAGE); @@ -266,7 +271,7 @@ public void Stop() /// Event to enqueue public void SendEvent(OdpEvent odpEvent) { - if (!_odpConfig.IsReady()) + if (_odpConfig == null || !_odpConfig.IsReady()) { _logger.Log(LogLevel.WARN, Constants.ODP_NOT_INTEGRATED_MESSAGE); return; @@ -349,7 +354,17 @@ public void IdentifyUser(string userId) /// Configuration object containing new values public void UpdateSettings(OdpConfig odpConfig) { + if (odpConfig == null) + { + return; + } + _odpConfig = odpConfig; + + if (_autoStart) + { + Start(); + } } /// @@ -402,23 +417,28 @@ public class Builder private BlockingCollection _eventQueue = new BlockingCollection(Constants.DEFAULT_QUEUE_CAPACITY); - private OdpConfig _odpConfig; private IOdpEventApiManager _odpEventApiManager; private int _batchSize; private TimeSpan _flushInterval; private TimeSpan _timeoutInterval; private ILogger _logger; private IErrorHandler _errorHandler; + private bool? _autoStart; - public Builder WithEventQueue(BlockingCollection eventQueue) + /// + /// Indicates if OdpEventManager should start upon Build() and UpdateSettings() + /// + /// + /// + public Builder WithAutoStart(bool autoStart) { - _eventQueue = eventQueue; + _autoStart = autoStart; return this; } - public Builder WithOdpConfig(OdpConfig odpConfig) + public Builder WithEventQueue(BlockingCollection eventQueue) { - _odpConfig = odpConfig; + _eventQueue = eventQueue; return this; } @@ -461,13 +481,11 @@ public Builder WithErrorHandler(IErrorHandler errorHandler = null) /// /// Build OdpEventManager instance using collected parameters /// - /// Should start event processor upon initialization /// OdpEventProcessor instance - public OdpEventManager Build(bool startImmediately = true) + public OdpEventManager Build() { var manager = new OdpEventManager(); manager._eventQueue = _eventQueue; - manager._odpConfig = _odpConfig; manager._odpEventApiManager = _odpEventApiManager; manager._batchSize = _batchSize < 1 ? Constants.DEFAULT_BATCH_SIZE : _batchSize; @@ -479,6 +497,7 @@ public OdpEventManager Build(bool startImmediately = true) _timeoutInterval; manager._logger = _logger ?? new NoOpLogger(); manager._errorHandler = _errorHandler ?? new NoOpErrorHandler(); + manager._autoStart = _autoStart ?? true; manager._validOdpDataTypes = new List() { @@ -510,7 +529,7 @@ public OdpEventManager Build(bool startImmediately = true) }, }; - if (startImmediately) + if (manager._autoStart) { manager.Start(); } diff --git a/OptimizelySDK/Odp/OdpManager.cs b/OptimizelySDK/Odp/OdpManager.cs index ddaf79e0..740c5a87 100644 --- a/OptimizelySDK/Odp/OdpManager.cs +++ b/OptimizelySDK/Odp/OdpManager.cs @@ -1,5 +1,5 @@ /* - * Copyright 2022 Optimizely + * Copyright 2022-2023 Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -64,7 +64,7 @@ private OdpManager() { } public bool UpdateSettings(string apiKey, string apiHost, List segmentsToCheck) { var newConfig = new OdpConfig(apiKey, apiHost, segmentsToCheck); - if (_odpConfig.Equals(newConfig)) + if (_odpConfig != null && _odpConfig.Equals(newConfig)) { return false; } @@ -149,14 +149,13 @@ public void Dispose() /// public class Builder { - private OdpConfig _odpConfig; private IOdpEventManager _eventManager; private IOdpSegmentManager _segmentManager; - private int _cacheSize; - private int _cacheTimeoutSeconds; private ILogger _logger; private IErrorHandler _errorHandler; private ICache> _cache; + private int? _maxSize; + private TimeSpan? _itemTimeout; public Builder WithSegmentManager(IOdpSegmentManager segmentManager) { @@ -170,24 +169,6 @@ public Builder WithEventManager(IOdpEventManager eventManager) return this; } - public Builder WithOdpConfig(OdpConfig odpConfig) - { - _odpConfig = odpConfig; - return this; - } - - public Builder WithCacheSize(int cacheSize) - { - _cacheSize = cacheSize; - return this; - } - - public Builder WithCacheTimeout(int seconds) - { - _cacheTimeoutSeconds = seconds; - return this; - } - public Builder WithLogger(ILogger logger = null) { _logger = logger; @@ -200,12 +181,22 @@ public Builder WithErrorHandler(IErrorHandler errorHandler = null) return this; } - public Builder WithCacheImplementation(ICache> cache) + public Builder WithCache(ICache> cache) { _cache = cache; return this; } + public Builder WithCache(int? maxSize = null, + TimeSpan? itemTimeout = null + ) + { + _maxSize = maxSize; + _itemTimeout = itemTimeout; + + return this; + } + /// /// Build OdpManager instance using collected parameters /// @@ -215,28 +206,21 @@ public OdpManager Build(bool asEnabled = true) { _logger = _logger ?? new DefaultLogger(); _errorHandler = _errorHandler ?? new NoOpErrorHandler(); - _odpConfig = _odpConfig ?? new OdpConfig(); var manager = new OdpManager { - _odpConfig = _odpConfig, _logger = _logger, _enabled = asEnabled, }; - if (!manager._enabled) - { - return manager; - } - if (_eventManager == null) { var eventApiManager = new OdpEventApiManager(_logger, _errorHandler); manager.EventManager = new OdpEventManager.Builder(). - WithOdpConfig(_odpConfig). - WithBatchSize(_cacheSize). - WithTimeoutInterval(TimeSpan.FromMilliseconds(_cacheTimeoutSeconds)). + WithBatchSize(Constants.DEFAULT_MAX_CACHE_SIZE). + WithTimeoutInterval( + TimeSpan.FromMilliseconds(Constants.DEFAULT_CACHE_SECONDS)). WithOdpEventApiManager(eventApiManager). WithLogger(_logger). WithErrorHandler(_errorHandler). @@ -251,13 +235,9 @@ public OdpManager Build(bool asEnabled = true) if (_segmentManager == null) { - var cacheTimeout = TimeSpan.FromSeconds(_cacheTimeoutSeconds <= 0 ? - Constants.DEFAULT_CACHE_SECONDS : - _cacheTimeoutSeconds); var apiManager = new OdpSegmentApiManager(_logger, _errorHandler); - manager.SegmentManager = new OdpSegmentManager(_odpConfig, apiManager, - _cacheSize, cacheTimeout, _logger, _cache); + manager.SegmentManager = new OdpSegmentManager(apiManager, GetCache(), _logger); } else { @@ -266,6 +246,11 @@ public OdpManager Build(bool asEnabled = true) return manager; } + + private ICache> GetCache() + { + return _cache ?? new LruCache>(_maxSize, _itemTimeout, _logger); + } } /// @@ -274,7 +259,7 @@ public OdpManager Build(bool asEnabled = true) /// True if EventManager can process events otherwise False private bool EventManagerOrConfigNotReady() { - return EventManager == null || !_enabled || !_odpConfig.IsReady(); + return EventManager == null || !_enabled || _odpConfig == null || !_odpConfig.IsReady(); } /// @@ -283,7 +268,8 @@ private bool EventManagerOrConfigNotReady() /// True if SegmentManager can fetch audience segments otherwise False private bool SegmentManagerOrConfigNotReady() { - return SegmentManager == null || !_enabled || !_odpConfig.IsReady(); + return SegmentManager == null || !_enabled || _odpConfig == null || + !_odpConfig.IsReady(); } } } diff --git a/OptimizelySDK/Odp/OdpSegmentManager.cs b/OptimizelySDK/Odp/OdpSegmentManager.cs index c155a755..162c0212 100644 --- a/OptimizelySDK/Odp/OdpSegmentManager.cs +++ b/OptimizelySDK/Odp/OdpSegmentManager.cs @@ -47,13 +47,27 @@ public class OdpSegmentManager : IOdpSegmentManager /// private readonly ICache> _segmentsCache; - public OdpSegmentManager(OdpConfig odpConfig, IOdpSegmentApiManager apiManager, - int? cacheSize = null, TimeSpan? itemTimeout = null, - ILogger logger = null, ICache> cache = null + public OdpSegmentManager(IOdpSegmentApiManager apiManager, + ICache> cache = null, + ILogger logger = null + ) + { + _apiManager = apiManager; + _logger = logger ?? new DefaultLogger(); + + _segmentsCache = + cache ?? new LruCache>(Constants.DEFAULT_MAX_CACHE_SIZE, + TimeSpan.FromSeconds(Constants.DEFAULT_CACHE_SECONDS), logger); + } + + public OdpSegmentManager(IOdpSegmentApiManager apiManager, + int? cacheSize = null, + TimeSpan? itemTimeout = null, + ICache> cache = null, + ILogger logger = null ) { _apiManager = apiManager; - _odpConfig = odpConfig; _logger = logger ?? new DefaultLogger(); itemTimeout = itemTimeout ?? TimeSpan.FromSeconds(Constants.DEFAULT_CACHE_SECONDS); @@ -81,7 +95,7 @@ public List FetchQualifiedSegments(string fsUserId, List options = null ) { - if (!_odpConfig.IsReady()) + if (_odpConfig == null || !_odpConfig.IsReady()) { _logger.Log(LogLevel.WARN, Constants.ODP_NOT_INTEGRATED_MESSAGE); return null; diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs index 7504b822..29fa46aa 100644 --- a/OptimizelySDK/Optimizely.cs +++ b/OptimizelySDK/Optimizely.cs @@ -1,5 +1,5 @@ /* - * Copyright 2017-2022, Optimizely + * Copyright 2017-2023, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use file except in compliance with the License. @@ -38,7 +38,6 @@ #if USE_ODP using OptimizelySDK.Odp; -using OptimizelySDK.Odp.Entity; #endif namespace OptimizelySDK @@ -75,8 +74,6 @@ public class Optimizely : IOptimizely, IDisposable #if USE_ODP private OdpManager OdpManager; - - private OptimizelySdkSettings SdkSettings; #endif /// @@ -138,7 +135,7 @@ public static String SDK_TYPE /// boolean representing whether JSON schema validation needs to be performed /// EventProcessor /// Default Decide options - /// Optional settings for SDK configuration + /// Optional ODP Manager public Optimizely(string datafile, IEventDispatcher eventDispatcher = null, ILogger logger = null, @@ -148,21 +145,29 @@ public Optimizely(string datafile, EventProcessor eventProcessor = null, OptimizelyDecideOption[] defaultDecideOptions = null #if USE_ODP - , OptimizelySdkSettings sdkSettings = null + , OdpManager odpManager = null #endif ) { try { +#if USE_ODP InitializeComponents(eventDispatcher, logger, errorHandler, userProfileService, - null, eventProcessor, defaultDecideOptions); + null, eventProcessor, defaultDecideOptions, odpManager); +#else + InitializeComponents(eventDispatcher, logger, errorHandler, userProfileService, + null, eventProcessor, defaultDecideOptions); +#endif if (ValidateInputs(datafile, skipJsonValidation)) { var config = DatafileProjectConfig.Create(datafile, Logger, ErrorHandler); ProjectConfigManager = new FallbackProjectConfigManager(config); #if USE_ODP - SetupOdp(config, sdkSettings); + // No need to setup notification for datafile updates. This constructor + // is for hardcoded datafile which should not be changed using this method. + OdpManager.UpdateSettings(config.PublicKeyForOdp, config.HostForOdp, + config.Segments.ToList()); #endif } else @@ -194,7 +199,7 @@ public Optimizely(string datafile, /// User profile service. /// EventProcessor /// Default Decide options - /// Optional settings for SDK configuration + /// Optional ODP Manager public Optimizely(ProjectConfigManager configManager, NotificationCenter notificationCenter = null, IEventDispatcher eventDispatcher = null, @@ -204,16 +209,35 @@ public Optimizely(ProjectConfigManager configManager, EventProcessor eventProcessor = null, OptimizelyDecideOption[] defaultDecideOptions = null #if USE_ODP - , OptimizelySdkSettings sdkSettings = null + , OdpManager odpManager = null #endif ) { ProjectConfigManager = configManager; +#if USE_ODP + InitializeComponents(eventDispatcher, logger, errorHandler, userProfileService, + notificationCenter, eventProcessor, defaultDecideOptions, odpManager); + + var projectConfig = ProjectConfigManager.CachedProjectConfig; + if (projectConfig != null) + { + NotificationCenterRegistry.GetNotificationCenter(configManager.SdkKey, logger)?. + AddNotification(NotificationCenter.NotificationType.OptimizelyConfigUpdate, + () => + { + OdpManager?.UpdateSettings(projectConfig.PublicKeyForOdp, + projectConfig.HostForOdp, + projectConfig.Segments.ToList()); + }); + + // in case if notification is lost. + OdpManager?.UpdateSettings(projectConfig.PublicKeyForOdp, projectConfig.HostForOdp, + projectConfig.Segments.ToList()); + } +#else InitializeComponents(eventDispatcher, logger, errorHandler, userProfileService, notificationCenter, eventProcessor, defaultDecideOptions); -#if USE_ODP - SetupOdp(ProjectConfigManager.CachedProjectConfig, sdkSettings); #endif } @@ -224,6 +248,9 @@ private void InitializeComponents(IEventDispatcher eventDispatcher = null, NotificationCenter notificationCenter = null, EventProcessor eventProcessor = null, OptimizelyDecideOption[] defaultDecideOptions = null +#if USE_ODP + , OdpManager odpManager = null +#endif ) { Logger = logger ?? new NoOpLogger(); @@ -240,36 +267,10 @@ private void InitializeComponents(IEventDispatcher eventDispatcher = null, Logger); DefaultDecideOptions = defaultDecideOptions ?? new OptimizelyDecideOption[] { }; - } - #if USE_ODP - private void SetupOdp(ProjectConfig config, OptimizelySdkSettings sdkSettings = null) - { - if (config == null || sdkSettings?.DisableOdp == true) - { - return; - } - - SdkSettings = sdkSettings ?? new OptimizelySdkSettings(); - - var odpConfig = new OdpConfig(config.PublicKeyForOdp, config.HostForOdp, - config.Segments.ToList()); - - OdpManager = new OdpManager.Builder(). - WithOdpConfig(odpConfig). - WithCacheSize(SdkSettings.SegmentsCacheSize). - WithCacheTimeout(SdkSettings.SegmentsCacheTimeout). - WithLogger(Logger). - WithErrorHandler(ErrorHandler). - Build(!SdkSettings.DisableOdp); - - NotificationCenter.AddNotification( - NotificationCenter.NotificationType.OptimizelyConfigUpdate, - () => OdpManager?.UpdateSettings(config.PublicKeyForOdp, config.HostForOdp, - config.Segments.ToList()) - ); - } + OdpManager = odpManager ?? new OdpManager.Builder().Build(); #endif + } /// /// Buckets visitor and sends impression event to Optimizely. @@ -1490,15 +1491,26 @@ private object GetTypeCastedVariableValue(string value, string type) public void Dispose() { - if (Disposed) return; + if (Disposed) + { + return; + } Disposed = true; - (ProjectConfigManager as IDisposable)?.Dispose(); - (EventProcessor as IDisposable)?.Dispose(); + if (ProjectConfigManager is IDisposable) + { +#if USE_ODP + NotificationCenterRegistry.RemoveNotificationCenter(ProjectConfigManager.SdkKey); +#endif + + (ProjectConfigManager as IDisposable)?.Dispose(); + } ProjectConfigManager = null; + (EventProcessor as IDisposable)?.Dispose(); + #if USE_ODP OdpManager?.Dispose(); #endif diff --git a/OptimizelySDK/OptimizelySDK.csproj b/OptimizelySDK/OptimizelySDK.csproj index 15a6fcfb..b27a1f8a 100644 --- a/OptimizelySDK/OptimizelySDK.csproj +++ b/OptimizelySDK/OptimizelySDK.csproj @@ -99,7 +99,6 @@ - @@ -196,6 +195,7 @@ +