Skip to content

Commit 6aa9c29

Browse files
[FSSDK-11547] adjustment
1 parent 8dbaeb1 commit 6aa9c29

File tree

3 files changed

+179
-1
lines changed

3 files changed

+179
-1
lines changed

OptimizelySDK.Tests/DecisionServiceHoldoutTest.cs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
using OptimizelySDK.Config;
2626
using OptimizelySDK.Entity;
2727
using OptimizelySDK.ErrorHandler;
28+
using OptimizelySDK.Event;
29+
using OptimizelySDK.Event.Entity;
2830
using OptimizelySDK.Logger;
2931
using OptimizelySDK.OptimizelyDecisions;
3032

@@ -34,6 +36,7 @@ namespace OptimizelySDK.Tests
3436
public class DecisionServiceHoldoutTest
3537
{
3638
private Mock<ILogger> LoggerMock;
39+
private Mock<EventProcessor> EventProcessorMock;
3740
private DecisionService DecisionService;
3841
private DatafileProjectConfig Config;
3942
private JObject TestData;
@@ -46,6 +49,7 @@ public class DecisionServiceHoldoutTest
4649
public void Initialize()
4750
{
4851
LoggerMock = new Mock<ILogger>();
52+
EventProcessorMock = new Mock<EventProcessor>();
4953

5054
// Load test data
5155
var testDataPath = Path.Combine(TestContext.CurrentContext.TestDirectory,
@@ -242,5 +246,90 @@ public void TestGetVariationsForFeatureList_Holdout_DecisionReasons()
242246
Assert.IsTrue(decisionWithReasons.DecisionReasons.ToReport().Count > 0, "Should have decision reasons");
243247
}
244248
}
249+
250+
[Test]
251+
public void TestImpressionEventForHoldout()
252+
{
253+
var featureFlag = Config.FeatureKeyMap["test_flag_1"];
254+
var userAttributes = new UserAttributes();
255+
256+
var eventDispatcher = new Event.Dispatcher.DefaultEventDispatcher(LoggerMock.Object);
257+
var optimizelyWithMockedEvents = new Optimizely(
258+
TestData["datafileWithHoldouts"].ToString(),
259+
eventDispatcher,
260+
LoggerMock.Object,
261+
new ErrorHandler.NoOpErrorHandler(),
262+
null, // userProfileService
263+
false, // skipJsonValidation
264+
EventProcessorMock.Object
265+
);
266+
267+
EventProcessorMock.Setup(ep => ep.Process(It.IsAny<ImpressionEvent>()));
268+
269+
var userContext = optimizelyWithMockedEvents.CreateUserContext(TestUserId, userAttributes);
270+
var decision = userContext.Decide(featureFlag.Key);
271+
272+
Assert.IsNotNull(decision, "Decision should not be null");
273+
Assert.IsNotNull(decision.RuleKey, "RuleKey should not be null");
274+
275+
var actualHoldout = Config.Holdouts?.FirstOrDefault(h => h.Key == decision.RuleKey);
276+
277+
Assert.IsNotNull(actualHoldout,
278+
$"RuleKey '{decision.RuleKey}' should correspond to a holdout experiment");
279+
Assert.AreEqual(featureFlag.Key, decision.FlagKey, "Flag key should match");
280+
281+
var holdoutVariation = actualHoldout.Variations.FirstOrDefault(v => v.Key == decision.VariationKey);
282+
283+
Assert.IsNotNull(holdoutVariation,
284+
$"Variation '{decision.VariationKey}' should be from the chosen holdout '{actualHoldout.Key}'");
285+
286+
Assert.AreEqual(holdoutVariation.FeatureEnabled, decision.Enabled,
287+
"Enabled flag should match holdout variation's featureEnabled value");
288+
289+
EventProcessorMock.Verify(ep => ep.Process(It.IsAny<ImpressionEvent>()), Times.Once,
290+
"Impression event should be processed exactly once for holdout decision");
291+
292+
EventProcessorMock.Verify(ep => ep.Process(It.Is<ImpressionEvent>(ie =>
293+
ie.Experiment.Key == actualHoldout.Key &&
294+
ie.Experiment.Id == actualHoldout.Id &&
295+
ie.Timestamp > 0 &&
296+
ie.UserId == TestUserId
297+
)), Times.Once, "Impression event should contain correct holdout experiment details");
298+
}
299+
300+
[Test]
301+
public void TestImpressionEventForHoldout_DisableDecisionEvent()
302+
{
303+
var featureFlag = Config.FeatureKeyMap["test_flag_1"];
304+
var userAttributes = new UserAttributes();
305+
306+
var eventDispatcher = new Event.Dispatcher.DefaultEventDispatcher(LoggerMock.Object);
307+
var optimizelyWithMockedEvents = new Optimizely(
308+
TestData["datafileWithHoldouts"].ToString(),
309+
eventDispatcher,
310+
LoggerMock.Object,
311+
new ErrorHandler.NoOpErrorHandler(),
312+
null, // userProfileService
313+
false, // skipJsonValidation
314+
EventProcessorMock.Object
315+
);
316+
317+
EventProcessorMock.Setup(ep => ep.Process(It.IsAny<ImpressionEvent>()));
318+
319+
var userContext = optimizelyWithMockedEvents.CreateUserContext(TestUserId, userAttributes);
320+
var decision = userContext.Decide(featureFlag.Key, new[] { OptimizelyDecideOption.DISABLE_DECISION_EVENT });
321+
322+
Assert.IsNotNull(decision, "Decision should not be null");
323+
Assert.IsNotNull(decision.RuleKey, "User should be bucketed into a holdout");
324+
325+
var chosenHoldout = Config.Holdouts?.FirstOrDefault(h => h.Key == decision.RuleKey);
326+
327+
Assert.IsNotNull(chosenHoldout, $"Holdout '{decision.RuleKey}' should exist in config");
328+
329+
Assert.AreEqual(featureFlag.Key, decision.FlagKey, "Flag key should match");
330+
331+
EventProcessorMock.Verify(ep => ep.Process(It.IsAny<ImpressionEvent>()), Times.Never,
332+
"No impression event should be processed when DISABLE_DECISION_EVENT option is used");
333+
}
245334
}
246335
}

OptimizelySDK.Tests/OptimizelyUserContextHoldoutTest.cs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@
2828
using OptimizelySDK.Event;
2929
using OptimizelySDK.Event.Dispatcher;
3030
using OptimizelySDK.Logger;
31+
using OptimizelySDK.Notifications;
3132
using OptimizelySDK.OptimizelyDecisions;
33+
using OptimizelySDK.Utils;
3234

3335
namespace OptimizelySDK.Tests
3436
{
@@ -572,6 +574,92 @@ public void TestDecideReasons_HoldoutDecisionContainsRelevantReasons()
572574
"Should contain holdout-related decision reasoning");
573575
}
574576

577+
578+
#endregion
579+
580+
# region Notification test
581+
582+
[Test]
583+
public void TestDecide_HoldoutNotificationContent()
584+
{
585+
var capturedNotifications = new List<Dictionary<string, object>>();
586+
587+
NotificationCenter.DecisionCallback notificationCallback =
588+
(decisionType, userId, userAttributes, decisionInfo) =>
589+
{
590+
capturedNotifications.Add(new Dictionary<string, object>(decisionInfo));
591+
};
592+
593+
OptimizelyInstance.NotificationCenter.AddNotification(
594+
NotificationCenter.NotificationType.Decision,
595+
notificationCallback);
596+
597+
var userContext = OptimizelyInstance.CreateUserContext(TestUserId,
598+
new UserAttributes { { "country", "us" } });
599+
var decision = userContext.Decide("test_flag_1");
600+
601+
Assert.AreEqual(1, capturedNotifications.Count,
602+
"Should have captured exactly one decision notification");
603+
604+
var notification = capturedNotifications.First();
605+
606+
Assert.IsTrue(notification.ContainsKey("ruleKey"),
607+
"Notification should contain ruleKey");
608+
609+
var ruleKey = notification["ruleKey"]?.ToString();
610+
611+
Assert.IsNotNull(ruleKey, "RuleKey should not be null");
612+
613+
var holdoutExperiment = Config.Holdouts?.FirstOrDefault(h => h.Key == ruleKey);
614+
615+
Assert.IsNotNull(holdoutExperiment,
616+
$"RuleKey '{ruleKey}' should correspond to a holdout experiment");
617+
Assert.IsTrue(notification.ContainsKey("flagKey"),
618+
"Holdout notification should contain flagKey");
619+
Assert.IsTrue(notification.ContainsKey("enabled"),
620+
"Holdout notification should contain enabled flag");
621+
Assert.IsTrue(notification.ContainsKey("variationKey"),
622+
"Holdout notification should contain variationKey");
623+
Assert.IsTrue(notification.ContainsKey("experimentId"),
624+
"Holdout notification should contain experimentId");
625+
Assert.IsTrue(notification.ContainsKey("variationId"),
626+
"Holdout notification should contain variationId");
627+
628+
var flagKey = notification["flagKey"]?.ToString();
629+
630+
Assert.AreEqual("test_flag_1", flagKey, "FlagKey should match the requested flag");
631+
632+
var experimentId = notification["experimentId"]?.ToString();
633+
Assert.AreEqual(holdoutExperiment.Id, experimentId,
634+
"ExperimentId in notification should match holdout experiment ID");
635+
636+
var variationId = notification["variationId"]?.ToString();
637+
var holdoutVariation = holdoutExperiment.Variations?.FirstOrDefault(v => v.Id == variationId);
638+
639+
Assert.IsNotNull(holdoutVariation,
640+
$"VariationId '{variationId}' should correspond to a holdout variation");
641+
642+
var variationKey = notification["variationKey"]?.ToString();
643+
644+
Assert.AreEqual(holdoutVariation.Key, variationKey,
645+
"VariationKey in notification should match holdout variation key");
646+
647+
var enabled = notification["enabled"];
648+
649+
Assert.IsNotNull(enabled, "Enabled flag should be present in notification");
650+
Assert.AreEqual(holdoutVariation.FeatureEnabled, (bool)enabled,
651+
"Enabled flag should match holdout variation's featureEnabled value");
652+
653+
Assert.IsTrue(Config.FeatureKeyMap.ContainsKey(flagKey),
654+
$"FlagKey '{flagKey}' should exist in config");
655+
656+
Assert.IsTrue(notification.ContainsKey("variables"),
657+
"Notification should contain variables");
658+
Assert.IsTrue(notification.ContainsKey("reasons"),
659+
"Notification should contain reasons");
660+
Assert.IsTrue(notification.ContainsKey("decisionEventDispatched"),
661+
"Notification should contain decisionEventDispatched");
662+
}
575663
#endregion
576664
}
577665
}

OptimizelySDK/Optimizely.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -564,7 +564,8 @@ public virtual bool IsFeatureEnabled(string featureKey, string userId,
564564

565565
// This information is only necessary for feature tests.
566566
// For rollouts experiments and variations are an implementation detail only.
567-
if (decision?.Source == FeatureDecision.DECISION_SOURCE_FEATURE_TEST)
567+
if (decision?.Source == FeatureDecision.DECISION_SOURCE_FEATURE_TEST ||
568+
decision?.Source == FeatureDecision.DECISION_SOURCE_HOLDOUT)
568569
{
569570
decisionSource = decision.Source;
570571
sourceInfo["experimentKey"] = decision.Experiment.Key;

0 commit comments

Comments
 (0)