Skip to content

Commit a866266

Browse files
feat: WIP implementing solution. Tests are
still in progress too.
1 parent 74d98f7 commit a866266

File tree

3 files changed

+310
-245
lines changed

3 files changed

+310
-245
lines changed

OptimizelySDK.sln.DotSettings

+5
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,15 @@
4343
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=Interfaces/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="I" Suffix="" Style="AaBb" /&gt;</s:String>
4444
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=LocalConstants/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /&gt;</s:String>
4545
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticReadonly/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
46+
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=15b5b1f1_002D457c_002D4ca6_002Db278_002D5615aedc07d3/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static readonly fields (private)"&gt;&lt;ElementKinds&gt;&lt;Kind Name="READONLY_FIELD" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;&lt;/Policy&gt;</s:String>
47+
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=8b8504e3_002Df0be_002D4c14_002D9103_002Dc732f2bddc15/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Any" AccessRightKinds="Any" Description="Enum members"&gt;&lt;ElementKinds&gt;&lt;Kind Name="ENUM_MEMBER" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"&gt;&lt;ExtraRule Prefix="" Suffix="" Style="AaBb" /&gt;&lt;/Policy&gt;&lt;/Policy&gt;</s:String>
48+
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=a4f433b8_002Dabcd_002D4e55_002Da08f_002D82e78cef0f0c/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Any" AccessRightKinds="Any" Description="Local constants"&gt;&lt;ElementKinds&gt;&lt;Kind Name="LOCAL_CONSTANT" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /&gt;&lt;/Policy&gt;</s:String>
49+
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=a7a3339e_002D4e89_002D4319_002D9735_002Da9dc4cb74cc7/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Any" AccessRightKinds="Any" Description="Interfaces"&gt;&lt;ElementKinds&gt;&lt;Kind Name="INTERFACE" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="I" Suffix="" Style="AaBb" /&gt;&lt;/Policy&gt;</s:String>
4650
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue">True</s:Boolean>
4751
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
4852
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue">True</s:Boolean>
4953
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
54+
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EPredefinedNamingRulesToUserRulesUpgrade/@EntryIndexedValue">True</s:Boolean>
5055
<s:Boolean x:Key="/Default/UserDictionary/Words/=Bucketer/@EntryIndexedValue">True</s:Boolean>
5156
<s:Boolean x:Key="/Default/UserDictionary/Words/=ODP_0027s/@EntryIndexedValue">True</s:Boolean>
5257
<s:Boolean x:Key="/Default/UserDictionary/Words/=Optly/@EntryIndexedValue">True</s:Boolean>

OptimizelySDK/Bucketing/DecisionService.cs

+120-84
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public class DecisionService
4343
private Bucketer Bucketer;
4444
private IErrorHandler ErrorHandler;
4545
private UserProfileService UserProfileService;
46-
private ILogger Logger;
46+
private static ILogger Logger;
4747

4848
/// <summary>
4949
/// Associative array of user IDs to an associative array
@@ -85,9 +85,9 @@ public DecisionService(Bucketer bucketer, IErrorHandler errorHandler,
8585
/// <summary>
8686
/// Get a Variation of an Experiment for a user to be allocated into.
8787
/// </summary>
88-
/// <param name = "experiment" > The Experiment the user will be bucketed into.</param>
89-
/// <param name = "user" > Optimizely user context.
90-
/// <param name = "config" > Project config.</param>
88+
/// <param name="experiment">The Experiment the user will be bucketed into.</param>
89+
/// <param name="user">Optimizely user context.</param>
90+
/// <param name="config">Project config.</param>
9191
/// <returns>The Variation the user is allocated into.</returns>
9292
public virtual Result<Variation> GetVariation(Experiment experiment,
9393
OptimizelyUserContext user,
@@ -100,24 +100,72 @@ ProjectConfig config
100100
/// <summary>
101101
/// Get a Variation of an Experiment for a user to be allocated into.
102102
/// </summary>
103-
/// <param name = "experiment" > The Experiment the user will be bucketed into.</param>
104-
/// <param name = "user" > optimizely user context.
105-
/// <param name = "config" > Project Config.</param>
106-
/// <param name = "options" >An array of decision options.</param>
107-
/// <returns>The Variation the user is allocated into.</returns>
103+
/// <param name="experiment">The Experiment the user will be bucketed into.</param>
104+
/// <param name="user">Optimizely user context.</param>
105+
/// <param name="config">Project Config.</param>
106+
/// <param name="options">An array of decision options.</param>
107+
/// <returns></returns>
108108
public virtual Result<Variation> GetVariation(Experiment experiment,
109109
OptimizelyUserContext user,
110110
ProjectConfig config,
111111
OptimizelyDecideOption[] options
112112
)
113113
{
114114
var reasons = new DecisionReasons();
115-
var userId = user.GetUserId();
115+
116+
var ignoreUps = options.Contains(OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE);
117+
UserProfileTracker userProfileTracker = null;
118+
119+
if (UserProfileService != null && !ignoreUps)
120+
{
121+
var userProfile = GetUserProfile(user.GetUserId(), reasons);
122+
userProfileTracker = new UserProfileTracker(userProfile, false);
123+
}
124+
125+
var response = GetVariation(experiment, user, config, options, userProfileTracker,
126+
reasons);
127+
128+
if (UserProfileService != null && !ignoreUps &&
129+
userProfileTracker?.ProfileUpdated == true)
130+
{
131+
SaveUserProfile(userProfileTracker.UserProfile);
132+
}
133+
134+
return response;
135+
}
136+
137+
/// <summary>
138+
/// Get a Variation of an Experiment for a user to be allocated into.
139+
/// </summary>
140+
/// <param name="experiment">The Experiment the user will be bucketed into.</param>
141+
/// <param name="user">Optimizely user context.</param>
142+
/// <param name="config">Project Config.</param>
143+
/// <param name="options">An array of decision options.</param>
144+
/// <param name="userProfileTracker">A UserProfileTracker object.</param>
145+
/// <param name="reasons">Set of reasons for the decision.</param>
146+
/// <returns>The Variation the user is allocated into.</returns>
147+
public virtual Result<Variation> GetVariation(Experiment experiment,
148+
OptimizelyUserContext user,
149+
ProjectConfig config,
150+
OptimizelyDecideOption[] options,
151+
UserProfileTracker userProfileTracker,
152+
DecisionReasons reasons = null
153+
)
154+
{
155+
if (reasons == null)
156+
{
157+
reasons = new DecisionReasons();
158+
}
159+
116160
if (!ExperimentUtils.IsExperimentActive(experiment, Logger))
117161
{
162+
var message = reasons.AddInfo($"Experiment {experiment.Key} is not running.");
163+
Logger.Log(LogLevel.INFO, message);
118164
return Result<Variation>.NullResult(reasons);
119165
}
120166

167+
var userId = user.GetUserId();
168+
121169
// check if a forced variation is set
122170
var decisionVariationResult = GetForcedVariation(experiment.Key, userId, config);
123171
reasons += decisionVariationResult.DecisionReasons;
@@ -137,76 +185,41 @@ OptimizelyDecideOption[] options
137185
return decisionVariationResult;
138186
}
139187

140-
// fetch the user profile map from the user profile service
141-
var ignoreUPS = Array.Exists(options,
142-
option => option == OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE);
143-
144-
UserProfile userProfile = null;
145-
if (!ignoreUPS && UserProfileService != null)
188+
if (userProfileTracker != null)
146189
{
147-
try
148-
{
149-
var userProfileMap = UserProfileService.Lookup(user.GetUserId());
150-
if (userProfileMap != null &&
151-
UserProfileUtil.IsValidUserProfileMap(userProfileMap))
152-
{
153-
userProfile = UserProfileUtil.ConvertMapToUserProfile(userProfileMap);
154-
decisionVariationResult =
155-
GetStoredVariation(experiment, userProfile, config);
156-
reasons += decisionVariationResult.DecisionReasons;
157-
if (decisionVariationResult.ResultObject != null)
158-
{
159-
return decisionVariationResult.SetReasons(reasons);
160-
}
161-
}
162-
else if (userProfileMap == null)
163-
{
164-
Logger.Log(LogLevel.INFO,
165-
reasons.AddInfo(
166-
"We were unable to get a user profile map from the UserProfileService."));
167-
}
168-
else
169-
{
170-
Logger.Log(LogLevel.ERROR,
171-
reasons.AddInfo("The UserProfileService returned an invalid map."));
172-
}
173-
}
174-
catch (Exception exception)
190+
decisionVariationResult =
191+
GetStoredVariation(experiment, userProfileTracker.UserProfile, config);
192+
reasons += decisionVariationResult.DecisionReasons;
193+
variation = decisionVariationResult.ResultObject;
194+
if (variation != null)
175195
{
176-
Logger.Log(LogLevel.ERROR, reasons.AddInfo(exception.Message));
177-
ErrorHandler.HandleError(
178-
new Exceptions.OptimizelyRuntimeException(exception.Message));
196+
return decisionVariationResult;
179197
}
180198
}
181199

182-
var filteredAttributes = user.GetAttributes();
183-
var doesUserMeetAudienceConditionsResult =
184-
ExperimentUtils.DoesUserMeetAudienceConditions(config, experiment, user,
185-
LOGGING_KEY_TYPE_EXPERIMENT, experiment.Key, Logger);
186-
reasons += doesUserMeetAudienceConditionsResult.DecisionReasons;
187-
if (doesUserMeetAudienceConditionsResult.ResultObject)
200+
var decisionMeetAudience = ExperimentUtils.DoesUserMeetAudienceConditions(config,
201+
experiment, user,
202+
LOGGING_KEY_TYPE_EXPERIMENT, experiment.Key, Logger);
203+
reasons += decisionMeetAudience.DecisionReasons;
204+
if (decisionMeetAudience.ResultObject)
188205
{
189-
// Get Bucketing ID from user attributes.
190-
var bucketingIdResult = GetBucketingId(userId, filteredAttributes);
206+
var bucketingIdResult = GetBucketingId(userId, user.GetAttributes());
191207
reasons += bucketingIdResult.DecisionReasons;
192208

193209
decisionVariationResult = Bucketer.Bucket(config, experiment,
194210
bucketingIdResult.ResultObject, userId);
195211
reasons += decisionVariationResult.DecisionReasons;
212+
variation = decisionVariationResult.ResultObject;
196213

197-
if (decisionVariationResult.ResultObject?.Key != null)
214+
if (variation != null)
198215
{
199-
if (UserProfileService != null && !ignoreUPS)
216+
if (userProfileTracker != null)
200217
{
201-
var bucketerUserProfile = userProfile ??
202-
new UserProfile(userId,
203-
new Dictionary<string, Decision>());
204-
SaveVariation(experiment, decisionVariationResult.ResultObject,
205-
bucketerUserProfile);
218+
userProfileTracker.UpdateUserProfile(experiment, variation);
206219
}
207220
else
208221
{
209-
Logger.Log(LogLevel.INFO,
222+
Logger.Log(LogLevel.DEBUG,
210223
"This decision will not be saved since the UserProfileService is null.");
211224
}
212225
}
@@ -720,18 +733,6 @@ public virtual Result<FeatureDecision> GetVariationForFeature(FeatureFlag featur
720733
new OptimizelyDecideOption[] { });
721734
}
722735

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-
735736
void SaveUserProfile(UserProfile userProfile)
736737
{
737738
if (UserProfileService == null)
@@ -791,6 +792,40 @@ private UserProfile GetUserProfile(String userId, DecisionReasons reasons)
791792
return userProfile;
792793
}
793794

795+
public class UserProfileTracker
796+
{
797+
public UserProfile UserProfile { get; set; }
798+
public bool ProfileUpdated { get; set; }
799+
800+
public UserProfileTracker(UserProfile userProfile, bool profileUpdated)
801+
{
802+
UserProfile = userProfile;
803+
ProfileUpdated = profileUpdated;
804+
}
805+
806+
public void UpdateUserProfile(Experiment experiment, Variation variation)
807+
{
808+
var experimentId = experiment.Id;
809+
var variationId = variation.Id;
810+
Decision decision;
811+
if (UserProfile.ExperimentBucketMap.ContainsKey(experimentId))
812+
{
813+
decision = UserProfile.ExperimentBucketMap[experimentId];
814+
decision.VariationId = variationId;
815+
}
816+
else
817+
{
818+
decision = new Decision(variationId);
819+
}
820+
821+
UserProfile.ExperimentBucketMap[experimentId] = decision;
822+
ProfileUpdated = true;
823+
824+
Logger.Log(LogLevel.INFO,
825+
$"Updated variation \"{variationId}\" of experiment \"{experimentId}\" for user \"{UserProfile.UserId}\".");
826+
}
827+
}
828+
794829
public virtual List<Result<FeatureDecision>> GetVariationsForFeatureList(
795830
List<FeatureFlag> featureFlags,
796831
OptimizelyUserContext user,
@@ -834,22 +869,23 @@ OptimizelyDecideOption[] options
834869
decisionResult = GetVariationForFeatureRollout(featureFlag, user, projectConfig);
835870
reasons += decisionResult.DecisionReasons;
836871

837-
if (decisionResult.ResultObject != null)
872+
if (decisionResult.ResultObject == null)
873+
{
874+
Logger.Log(LogLevel.INFO,
875+
reasons.AddInfo(
876+
$"The user \"{userId}\" is not bucketed into a rollout for feature flag \"{featureFlag.Key}\"."));
877+
decisions.Add(Result<FeatureDecision>.NewResult(
878+
new FeatureDecision(null, null, FeatureDecision.DECISION_SOURCE_ROLLOUT),
879+
reasons));
880+
}
881+
else
838882
{
839883
Logger.Log(LogLevel.INFO,
840884
reasons.AddInfo(
841885
$"The user \"{userId}\" is bucketed into a rollout for feature flag \"{featureFlag.Key}\"."));
842886
decisions.Add(
843887
Result<FeatureDecision>.NewResult(decisionResult.ResultObject, reasons));
844-
continue;
845888
}
846-
847-
Logger.Log(LogLevel.INFO,
848-
reasons.AddInfo(
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));
853889
}
854890

855891
if (UserProfileService != null && !ignoreUPS && userProfileTracker?.ProfileUpdated == true)

0 commit comments

Comments
 (0)