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 @@
+