Skip to content

Commit 74d98f7

Browse files
feat: WIP
1 parent b5658b1 commit 74d98f7

File tree

2 files changed

+202
-44
lines changed

2 files changed

+202
-44
lines changed

OptimizelySDK/Bucketing/DecisionService.cs

Lines changed: 152 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2017-2022, Optimizely
2+
* Copyright 2017-2022, 2024 Optimizely
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
1616

1717
using System;
1818
using System.Collections.Generic;
19+
using System.Linq;
1920
using OptimizelySDK.Entity;
2021
using OptimizelySDK.ErrorHandler;
2122
using OptimizelySDK.Logger;
@@ -706,9 +707,9 @@ OptimizelyDecideOption[] options
706707
/// <summary>
707708
/// Get the variation the user is bucketed into for the FeatureFlag
708709
/// </summary>
709-
/// <param name = "featureFlag" >The feature flag the user wants to access.</param>
710-
/// <param name = "userId" >User Identifier</param>
711-
/// <param name = "filteredAttributes" >The user's attributes. This should be filtered to just attributes in the Datafile.</param>
710+
/// <param name="featureFlag">The feature flag the user wants to access.</param>
711+
/// <param name="user">The user context.</param>
712+
/// <param name="config">The project config.</param>
712713
/// <returns>null if the user is not bucketed into any variation or the FeatureDecision entity if the user is
713714
/// successfully bucketed.</returns>
714715
public virtual Result<FeatureDecision> GetVariationForFeature(FeatureFlag featureFlag,
@@ -719,53 +720,168 @@ public virtual Result<FeatureDecision> GetVariationForFeature(FeatureFlag featur
719720
new OptimizelyDecideOption[] { });
720721
}
721722

722-
/// <summary>
723-
/// Get the variation the user is bucketed into for the FeatureFlag
724-
/// </summary>
725-
/// <param name = "featureFlag" >The feature flag the user wants to access.</param>
726-
/// <param name = "userId" >User Identifier</param>
727-
/// <param name = "filteredAttributes" >The user's attributes. This should be filtered to just attributes in the Datafile.</param>
728-
/// <param name = "filteredAttributes" >The user's attributes. This should be filtered to just attributes in the Datafile.</param>
729-
/// <param name = "options" >An array of decision options.</param>
730-
/// <returns>null if the user is not bucketed into any variation or the FeatureDecision entity if the user is
731-
/// successfully bucketed.</returns>
732-
public virtual Result<FeatureDecision> GetVariationForFeature(FeatureFlag featureFlag,
723+
private class UserProfileTracker
724+
{
725+
public UserProfile UserProfile { get; set; }
726+
public bool ProfileUpdated { get; set; }
727+
728+
public UserProfileTracker(UserProfile userProfile, bool profileUpdated)
729+
{
730+
UserProfile = userProfile;
731+
ProfileUpdated = profileUpdated;
732+
}
733+
}
734+
735+
void SaveUserProfile(UserProfile userProfile)
736+
{
737+
if (UserProfileService == null)
738+
{
739+
return;
740+
}
741+
742+
try
743+
{
744+
UserProfileService.Save(userProfile.ToMap());
745+
Logger.Log(LogLevel.INFO,
746+
$"Saved user profile of user \"{userProfile.UserId}\".");
747+
}
748+
catch (Exception exception)
749+
{
750+
Logger.Log(LogLevel.WARN,
751+
$"Failed to save user profile of user \"{userProfile.UserId}\".");
752+
ErrorHandler.HandleError(new Exceptions.OptimizelyRuntimeException(exception.Message));
753+
}
754+
}
755+
756+
private UserProfile GetUserProfile(String userId, DecisionReasons reasons)
757+
{
758+
UserProfile userProfile = null;
759+
760+
try
761+
{
762+
var userProfileMap = UserProfileService.Lookup(userId);
763+
if (userProfileMap == null)
764+
{
765+
Logger.Log(LogLevel.INFO,
766+
reasons.AddInfo(
767+
"We were unable to get a user profile map from the UserProfileService."));
768+
}
769+
else if (UserProfileUtil.IsValidUserProfileMap(userProfileMap))
770+
{
771+
userProfile = UserProfileUtil.ConvertMapToUserProfile(userProfileMap);
772+
}
773+
else
774+
{
775+
Logger.Log(LogLevel.WARN,
776+
reasons.AddInfo("The UserProfileService returned an invalid map."));
777+
}
778+
}
779+
catch (Exception exception)
780+
{
781+
Logger.Log(LogLevel.ERROR, reasons.AddInfo(exception.Message));
782+
ErrorHandler.HandleError(
783+
new Exceptions.OptimizelyRuntimeException(exception.Message));
784+
}
785+
786+
if (userProfile == null)
787+
{
788+
userProfile = new UserProfile(userId, new Dictionary<string, Decision>());
789+
}
790+
791+
return userProfile;
792+
}
793+
794+
public virtual List<Result<FeatureDecision>> GetVariationsForFeatureList(
795+
List<FeatureFlag> featureFlags,
733796
OptimizelyUserContext user,
734-
ProjectConfig config,
797+
ProjectConfig projectConfig,
735798
UserAttributes filteredAttributes,
736799
OptimizelyDecideOption[] options
737800
)
738801
{
739-
var reasons = new DecisionReasons();
740-
var userId = user.GetUserId();
741-
// Check if the feature flag has an experiment and the user is bucketed into that experiment.
742-
var decisionResult = GetVariationForFeatureExperiment(featureFlag, user,
743-
filteredAttributes, config, options);
744-
reasons += decisionResult.DecisionReasons;
802+
var upsReasons = new DecisionReasons();
745803

746-
if (decisionResult.ResultObject != null)
804+
var ignoreUPS = options.Contains(OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE);
805+
UserProfileTracker userProfileTracker = null;
806+
807+
if (UserProfileService != null && !ignoreUPS)
747808
{
748-
return Result<FeatureDecision>.NewResult(decisionResult.ResultObject, reasons);
809+
var userProfile = GetUserProfile(user.GetUserId(), upsReasons);
810+
userProfileTracker = new UserProfileTracker(userProfile, false);
749811
}
750812

751-
// Check if the feature flag has rollout and the the user is bucketed into one of its rules.
752-
decisionResult = GetVariationForFeatureRollout(featureFlag, user, config);
753-
reasons += decisionResult.DecisionReasons;
813+
var userId = user.GetUserId();
814+
var decisions = new List<Result<FeatureDecision>>();
754815

755-
if (decisionResult.ResultObject != null)
816+
foreach (var featureFlag in featureFlags)
756817
{
818+
var reasons = new DecisionReasons();
819+
reasons += upsReasons;
820+
821+
// Check if the feature flag has an experiment and the user is bucketed into that experiment.
822+
var decisionResult = GetVariationForFeatureExperiment(featureFlag, user,
823+
filteredAttributes, projectConfig, options);
824+
reasons += decisionResult.DecisionReasons;
825+
826+
if (decisionResult.ResultObject != null)
827+
{
828+
decisions.Add(
829+
Result<FeatureDecision>.NewResult(decisionResult.ResultObject, reasons));
830+
continue;
831+
}
832+
833+
// Check if the feature flag has rollout and the the user is bucketed into one of its rules.
834+
decisionResult = GetVariationForFeatureRollout(featureFlag, user, projectConfig);
835+
reasons += decisionResult.DecisionReasons;
836+
837+
if (decisionResult.ResultObject != null)
838+
{
839+
Logger.Log(LogLevel.INFO,
840+
reasons.AddInfo(
841+
$"The user \"{userId}\" is bucketed into a rollout for feature flag \"{featureFlag.Key}\"."));
842+
decisions.Add(
843+
Result<FeatureDecision>.NewResult(decisionResult.ResultObject, reasons));
844+
continue;
845+
}
846+
757847
Logger.Log(LogLevel.INFO,
758848
reasons.AddInfo(
759-
$"The user \"{userId}\" is bucketed into a rollout for feature flag \"{featureFlag.Key}\"."));
760-
return Result<FeatureDecision>.NewResult(decisionResult.ResultObject, reasons);
849+
$"The user \"{userId}\" is not bucketed into a rollout for feature flag \"{featureFlag.Key}\"."));
850+
decisions.Add(Result<FeatureDecision>.NewResult(
851+
new FeatureDecision(null, null, FeatureDecision.DECISION_SOURCE_ROLLOUT),
852+
reasons));
761853
}
762854

763-
Logger.Log(LogLevel.INFO,
764-
reasons.AddInfo(
765-
$"The user \"{userId}\" is not bucketed into a rollout for feature flag \"{featureFlag.Key}\"."));
766-
return Result<FeatureDecision>.NewResult(
767-
new FeatureDecision(null, null, FeatureDecision.DECISION_SOURCE_ROLLOUT), reasons);
768-
;
855+
if (UserProfileService != null && !ignoreUPS && userProfileTracker?.ProfileUpdated == true)
856+
{
857+
SaveUserProfile(userProfileTracker.UserProfile);
858+
}
859+
860+
return decisions;
861+
}
862+
863+
/// <summary>
864+
/// Get the variation the user is bucketed into for the FeatureFlag
865+
/// </summary>
866+
/// <param name="featureFlag">The feature flag the user wants to access.</param>
867+
/// <param name="user">The user context.</param>
868+
/// <param name="config">The project config.</param>
869+
/// <param name="filteredAttributes">The user's attributes. This should be filtered to just attributes in the Datafile.</param>
870+
/// <param name="options">An array of decision options.</param>
871+
/// <returns>null if the user is not bucketed into any variation or the FeatureDecision entity if the user is
872+
/// successfully bucketed.</returns>
873+
public virtual Result<FeatureDecision> GetVariationForFeature(FeatureFlag featureFlag,
874+
OptimizelyUserContext user,
875+
ProjectConfig config,
876+
UserAttributes filteredAttributes,
877+
OptimizelyDecideOption[] options
878+
)
879+
{
880+
return GetVariationsForFeatureList(new List<FeatureFlag> { featureFlag },
881+
user,
882+
config,
883+
filteredAttributes,
884+
options).First();
769885
}
770886

771887
/// <summary>

OptimizelySDK/Optimizely.cs

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -869,6 +869,8 @@ internal OptimizelyDecision Decide(OptimizelyUserContext user,
869869
OptimizelyDecideOption[] options
870870
)
871871
{
872+
return DecideForKeys(user, new[] { key }, options)[key];
873+
872874
var config = ProjectConfigManager?.GetConfig();
873875

874876
if (config == null)
@@ -1024,7 +1026,7 @@ OptimizelyDecideOption[] options
10241026
if (projectConfig == null)
10251027
{
10261028
Logger.Log(LogLevel.ERROR,
1027-
"Optimizely instance is not valid, failing isFeatureEnabled call.");
1029+
"Optimizely instance is not valid, failing DecideAll call.");
10281030
return decisionMap;
10291031
}
10301032

@@ -1041,23 +1043,61 @@ OptimizelyDecideOption[] options
10411043
{
10421044
var decisionDictionary = new Dictionary<string, OptimizelyDecision>();
10431045

1044-
var projectConfig = ProjectConfigManager?.GetConfig();
1045-
if (projectConfig == null)
1046+
if (keys.Length == 0)
10461047
{
1047-
Logger.Log(LogLevel.ERROR,
1048-
"Optimizely instance is not valid, failing isFeatureEnabled call.");
10491048
return decisionDictionary;
10501049
}
10511050

1052-
if (keys.Length == 0)
1051+
var projectConfig = ProjectConfigManager?.GetConfig();
1052+
if (projectConfig == null)
10531053
{
1054+
Logger.Log(LogLevel.ERROR,
1055+
"Optimizely instance is not valid, failing DecideForKeys call.");
10541056
return decisionDictionary;
10551057
}
10561058

10571059
var allOptions = GetAllOptions(options);
10581060

1061+
var flagDecisions = new Dictionary<string, FeatureDecision>();
1062+
var decisionReasons = new Dictionary<string, DecisionReasons>();
1063+
1064+
var flagsWithoutForcedDecisions = new List<FeatureFlag>();
1065+
10591066
foreach (var key in keys)
10601067
{
1068+
var flag = projectConfig.GetFeatureFlagFromKey(key);
1069+
if (flag.Key == null)
1070+
{
1071+
decisionDictionary.Add(key,
1072+
OptimizelyDecision.NewErrorDecision(key, user,
1073+
DecisionMessage.Reason(DecisionMessage.FLAG_KEY_INVALID, key),
1074+
ErrorHandler, Logger));
1075+
continue;
1076+
}
1077+
1078+
var decisionContext = new OptimizelyDecisionContext(flag.Key);
1079+
var forcedDecisionVariation =
1080+
DecisionService.ValidatedForcedDecision(decisionContext, projectConfig, user);
1081+
decisionReasons.Add(key, forcedDecisionVariation.DecisionReasons);
1082+
1083+
if (forcedDecisionVariation.ResultObject != null)
1084+
{
1085+
var experiment = projectConfig.GetExperimentFromKey(flag.Key);
1086+
var featureDecision = Result<FeatureDecision>.NewResult(
1087+
new FeatureDecision(experiment, forcedDecisionVariation.ResultObject,
1088+
FeatureDecision.DECISION_SOURCE_FEATURE_TEST),
1089+
forcedDecisionVariation.DecisionReasons);
1090+
flagDecisions.Add(key, featureDecision.ResultObject);
1091+
}
1092+
else
1093+
{
1094+
flagsWithoutForcedDecisions.Add(flag);
1095+
}
1096+
1097+
var decisionsList = DecisionService.GetVariationsForFeatureList(
1098+
flagsWithoutForcedDecisions, user, projectConfig, user.GetAttributes(),
1099+
options);
1100+
10611101
var decision = Decide(user, key, options);
10621102
if (!allOptions.Contains(OptimizelyDecideOption.ENABLED_FLAGS_ONLY) ||
10631103
decision.Enabled)
@@ -1347,7 +1387,8 @@ List<OdpSegmentOption> segmentOptions
13471387

13481388
if (config == null)
13491389
{
1350-
Logger.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'FetchQualifiedSegments'.");
1390+
Logger.Log(LogLevel.ERROR,
1391+
"Datafile has invalid format. Failing 'FetchQualifiedSegments'.");
13511392
return null;
13521393
}
13531394

@@ -1378,7 +1419,8 @@ internal void IdentifyUser(string userId)
13781419
/// <param name="identifiers">Dictionary for identifiers. The caller must provide at least one key-value pair.</param>
13791420
/// <param name="type">Type of event (defaults to `fullstack`)</param>
13801421
/// <param name="data">Optional event data in a key-value pair format</param>
1381-
public void SendOdpEvent(string action, Dictionary<string, string> identifiers, string type = Constants.ODP_EVENT_TYPE,
1422+
public void SendOdpEvent(string action, Dictionary<string, string> identifiers,
1423+
string type = Constants.ODP_EVENT_TYPE,
13821424
Dictionary<string, object> data = null
13831425
)
13841426
{

0 commit comments

Comments
 (0)