Skip to content

Commit d0f59b6

Browse files
feat: call UPS once during batch decideForKeys or decideAll
1 parent 963fc07 commit d0f59b6

File tree

2 files changed

+127
-77
lines changed

2 files changed

+127
-77
lines changed

OptimizelySDK/Bucketing/DecisionService.cs

Lines changed: 105 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
/*
2-
* Copyright 2017-2022, Optimizely
3-
*
4-
* Licensed under the Apache License, Version 2.0 (the "License");
5-
* you may not use this file except in compliance with the License.
6-
* You may obtain a copy of the License at
7-
*
8-
* https://www.apache.org/licenses/LICENSE-2.0
9-
*
10-
* Unless required by applicable law or agreed to in writing, software
11-
* distributed under the License is distributed on an "AS IS" BASIS,
12-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13-
* See the License for the specific language governing permissions and
14-
* limitations under the License.
15-
*/
2+
* Copyright 2017-2022, 2024 Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
1616

1717
using System;
1818
using System.Collections.Generic;
@@ -43,6 +43,22 @@ public class DecisionService
4343
private IErrorHandler ErrorHandler;
4444
private UserProfileService UserProfileService;
4545
private ILogger Logger;
46+
private UserProfile _userProfile;
47+
48+
private bool _decisionBatchInProgress = false;
49+
50+
public bool DecisionBatchInProgress
51+
{
52+
get => _decisionBatchInProgress;
53+
set
54+
{
55+
_decisionBatchInProgress = value;
56+
if (!_decisionBatchInProgress)
57+
{
58+
SaveToUserProfileService();
59+
}
60+
}
61+
}
4662

4763
/// <summary>
4864
/// Associative array of user IDs to an associative array
@@ -60,10 +76,10 @@ public class DecisionService
6076
/// <summary>
6177
/// Initialize a decision service for the Optimizely client.
6278
/// </summary>
63-
/// <param name = "bucketer" > Base bucketer to allocate new users to an experiment.</param>
64-
/// <param name = "errorHandler" > The error handler of the Optimizely client.</param>
65-
/// <param name = "userProfileService" ></ param >
66-
/// < param name= "logger" > UserProfileService implementation for storing user info.</param>
79+
/// <param name="bucketer"> Base bucketer to allocate new users to an experiment.</param>
80+
/// <param name="errorHandler"> The error handler of the Optimizely client.</param>
81+
/// <param name="userProfileService">The injected implementation providing control over the bucketing.</param >
82+
/// < param name="logger"> UserProfileService implementation for storing user info.</param>
6783
public DecisionService(Bucketer bucketer, IErrorHandler errorHandler,
6884
UserProfileService userProfileService, ILogger logger
6985
)
@@ -85,7 +101,7 @@ public DecisionService(Bucketer bucketer, IErrorHandler errorHandler,
85101
/// Get a Variation of an Experiment for a user to be allocated into.
86102
/// </summary>
87103
/// <param name = "experiment" > The Experiment the user will be bucketed into.</param>
88-
/// <param name = "user" > Optimizely user context.
104+
/// <param name = "user" > Optimizely user context.</param>
89105
/// <param name = "config" > Project config.</param>
90106
/// <returns>The Variation the user is allocated into.</returns>
91107
public virtual Result<Variation> GetVariation(Experiment experiment,
@@ -99,10 +115,10 @@ ProjectConfig config
99115
/// <summary>
100116
/// Get a Variation of an Experiment for a user to be allocated into.
101117
/// </summary>
102-
/// <param name = "experiment" > The Experiment the user will be bucketed into.</param>
103-
/// <param name = "user" > optimizely user context.
104-
/// <param name = "config" > Project Config.</param>
105-
/// <param name = "options" >An array of decision options.</param>
118+
/// <param name="experiment">The Experiment the user will be bucketed into.</param>
119+
/// <param name="user">optimizely user context.</param>
120+
/// <param name="config">Project Config.</param>
121+
/// <param name="options">An array of decision options.</param>
106122
/// <returns>The Variation the user is allocated into.</returns>
107123
public virtual Result<Variation> GetVariation(Experiment experiment,
108124
OptimizelyUserContext user,
@@ -140,35 +156,42 @@ OptimizelyDecideOption[] options
140156
var ignoreUPS = Array.Exists(options,
141157
option => option == OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE);
142158

143-
UserProfile userProfile = null;
159+
144160
if (!ignoreUPS && UserProfileService != null)
145161
{
146162
try
147163
{
148-
var userProfileMap = UserProfileService.Lookup(user.GetUserId());
149-
if (userProfileMap != null &&
150-
UserProfileUtil.IsValidUserProfileMap(userProfileMap))
164+
if (_userProfile == null)
165+
{
166+
var userProfileMap = UserProfileService.Lookup(user.GetUserId());
167+
if (userProfileMap != null &&
168+
UserProfileUtil.IsValidUserProfileMap(userProfileMap))
169+
{
170+
_userProfile = UserProfileUtil.ConvertMapToUserProfile(userProfileMap);
171+
}
172+
else if (userProfileMap == null)
173+
{
174+
Logger.Log(LogLevel.INFO,
175+
reasons.AddInfo(
176+
"We were unable to get a user profile map from the UserProfileService."));
177+
}
178+
else
179+
{
180+
Logger.Log(LogLevel.ERROR,
181+
reasons.AddInfo("The UserProfileService returned an invalid map."));
182+
}
183+
}
184+
185+
if (_userProfile != null)
151186
{
152-
userProfile = UserProfileUtil.ConvertMapToUserProfile(userProfileMap);
153187
decisionVariationResult =
154-
GetStoredVariation(experiment, userProfile, config);
188+
GetStoredVariation(experiment, _userProfile, config);
155189
reasons += decisionVariationResult.DecisionReasons;
156190
if (decisionVariationResult.ResultObject != null)
157191
{
158192
return decisionVariationResult.SetReasons(reasons);
159193
}
160194
}
161-
else if (userProfileMap == null)
162-
{
163-
Logger.Log(LogLevel.INFO,
164-
reasons.AddInfo(
165-
"We were unable to get a user profile map from the UserProfileService."));
166-
}
167-
else
168-
{
169-
Logger.Log(LogLevel.ERROR,
170-
reasons.AddInfo("The UserProfileService returned an invalid map."));
171-
}
172195
}
173196
catch (Exception exception)
174197
{
@@ -197,11 +220,7 @@ OptimizelyDecideOption[] options
197220
{
198221
if (UserProfileService != null && !ignoreUPS)
199222
{
200-
var bucketerUserProfile = userProfile ??
201-
new UserProfile(userId,
202-
new Dictionary<string, Decision>());
203-
SaveVariation(experiment, decisionVariationResult.ResultObject,
204-
bucketerUserProfile);
223+
SaveVariation(experiment, decisionVariationResult.ResultObject);
205224
}
206225
else
207226
{
@@ -454,12 +473,9 @@ ProjectConfig config
454473
/// <summary>
455474
/// Save a { @link Variation } of an { @link Experiment } for a user in the {@link UserProfileService}.
456475
/// </summary>
457-
/// <param name = "experiment" > The experiment the user was buck</param>
458-
/// <param name = "variation" > The Variation to save.</param>
459-
/// <param name = "userProfile" > instance of the user information.</param>
460-
public void SaveVariation(Experiment experiment, Variation variation,
461-
UserProfile userProfile
462-
)
476+
/// <param name="experiment">The experiment the user was buck</param>
477+
/// <param name="variation">The Variation to save.</param>
478+
public void SaveVariation(Experiment experiment, Variation variation)
463479
{
464480
//only save if the user has implemented a user profile service
465481
if (UserProfileService == null)
@@ -468,28 +484,58 @@ UserProfile userProfile
468484
}
469485

470486
Decision decision;
471-
if (userProfile.ExperimentBucketMap.ContainsKey(experiment.Id))
487+
if (_userProfile.ExperimentBucketMap.ContainsKey(experiment.Id))
472488
{
473-
decision = userProfile.ExperimentBucketMap[experiment.Id];
489+
decision = _userProfile.ExperimentBucketMap[experiment.Id];
474490
decision.VariationId = variation.Id;
475491
}
476492
else
477493
{
478494
decision = new Decision(variation.Id);
479495
}
480496

481-
userProfile.ExperimentBucketMap[experiment.Id] = decision;
497+
_userProfile.ExperimentBucketMap[experiment.Id] = decision;
498+
499+
if (!_decisionBatchInProgress)
500+
{
501+
SaveToUserProfileService(experiment, variation);
502+
}
503+
}
482504

505+
private void SaveToUserProfileService(Experiment experiment = null,
506+
Variation variation = null
507+
)
508+
{
509+
var useSpecificLogEntry = experiment != null && variation != null &&
510+
!string.IsNullOrEmpty(_userProfile?.UserId);
511+
483512
try
484513
{
485-
UserProfileService.Save(userProfile.ToMap());
486-
Logger.Log(LogLevel.INFO,
487-
$"Saved variation \"{variation.Id}\" of experiment \"{experiment.Id}\" for user \"{userProfile.UserId}\".");
514+
if (_userProfile != null)
515+
{
516+
UserProfileService.Save(_userProfile.ToMap());
517+
if (useSpecificLogEntry)
518+
{
519+
Logger.Log(LogLevel.INFO,
520+
$"Saved variation \"{variation.Id}\" of experiment \"{experiment.Id}\" for user \"{_userProfile.UserId}\".");
521+
}
522+
else
523+
{
524+
Logger.Log(LogLevel.INFO, "Saved user profile after batch decision.");
525+
}
526+
}
488527
}
489528
catch (Exception exception)
490529
{
491-
Logger.Log(LogLevel.ERROR,
492-
$"Failed to save variation \"{variation.Id}\" of experiment \"{experiment.Id}\" for user \"{userProfile.UserId}\".");
530+
if (useSpecificLogEntry)
531+
{
532+
Logger.Log(LogLevel.ERROR,
533+
$"Failed to save variation \"{variation.Id}\" of experiment \"{experiment.Id}\" for user \"{_userProfile.UserId}\".");
534+
}
535+
else
536+
{
537+
Logger.Log(LogLevel.ERROR, "Failed to save user profile after batch decision.");
538+
}
493539
ErrorHandler.HandleError(
494540
new Exceptions.OptimizelyRuntimeException(exception.Message));
495541
}

OptimizelySDK/Optimizely.cs

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2017-2023, Optimizely
2+
* Copyright 2017-2024, Optimizely
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use file except in compliance with the License.
@@ -223,8 +223,8 @@ public Optimizely(ProjectConfigManager configManager,
223223

224224
if (ProjectConfigManager.SdkKey != null)
225225
{
226-
NotificationCenterRegistry.GetNotificationCenter(configManager.SdkKey, logger)
227-
?.AddNotification(NotificationCenter.NotificationType.OptimizelyConfigUpdate,
226+
NotificationCenterRegistry.GetNotificationCenter(configManager.SdkKey, logger)?.
227+
AddNotification(NotificationCenter.NotificationType.OptimizelyConfigUpdate,
228228
() =>
229229
{
230230
projectConfig = ProjectConfigManager.CachedProjectConfig;
@@ -268,10 +268,9 @@ private void InitializeComponents(IEventDispatcher eventDispatcher = null,
268268
Logger);
269269
DefaultDecideOptions = defaultDecideOptions ?? new OptimizelyDecideOption[] { };
270270
#if USE_ODP
271-
OdpManager = odpManager ?? new OdpManager.Builder()
272-
.WithErrorHandler(errorHandler)
273-
.WithLogger(logger)
274-
.Build();
271+
OdpManager = odpManager ?? new OdpManager.Builder().WithErrorHandler(errorHandler).
272+
WithLogger(logger).
273+
Build();
275274
#endif
276275
}
277276

@@ -442,8 +441,8 @@ private Variation GetVariation(string experimentKey, string userId, ProjectConfi
442441
userAttributes = userAttributes ?? new UserAttributes();
443442

444443
var userContext = CreateUserContextCopy(userId, userAttributes);
445-
var variation = DecisionService.GetVariation(experiment, userContext, config)
446-
?.ResultObject;
444+
var variation = DecisionService.GetVariation(experiment, userContext, config)?.
445+
ResultObject;
447446
var decisionInfo = new Dictionary<string, object>
448447
{
449448
{ "experimentKey", experimentKey }, { "variationKey", variation?.Key },
@@ -553,8 +552,8 @@ public virtual bool IsFeatureEnabled(string featureKey, string userId,
553552
var sourceInfo = new Dictionary<string, string>();
554553
var decision = DecisionService.GetVariationForFeature(featureFlag,
555554
CreateUserContextCopy(userId, userAttributes),
556-
config)
557-
.ResultObject;
555+
config).
556+
ResultObject;
558557
var variation = decision?.Variation;
559558
var decisionSource = decision?.Source ?? FeatureDecision.DECISION_SOURCE_ROLLOUT;
560559

@@ -665,8 +664,8 @@ public virtual T GetFeatureVariableValueForType<T>(string featureKey, string var
665664
var variableValue = featureVariable.DefaultValue;
666665
var decision = DecisionService.GetVariationForFeature(featureFlag,
667666
CreateUserContextCopy(userId, userAttributes),
668-
config)
669-
.ResultObject;
667+
config).
668+
ResultObject;
670669

671670
if (decision?.Variation != null)
672671
{
@@ -981,9 +980,9 @@ OptimizelyDecideOption[] options
981980
featureEnabled);
982981
}
983982

984-
var reasonsToReport = decisionReasons
985-
.ToReport(allOptions.Contains(OptimizelyDecideOption.INCLUDE_REASONS))
986-
.ToArray();
983+
var reasonsToReport = decisionReasons.
984+
ToReport(allOptions.Contains(OptimizelyDecideOption.INCLUDE_REASONS)).
985+
ToArray();
987986
var variationKey = decision?.Variation?.Key;
988987

989988
// TODO: add ruleKey values when available later. use a copy of experimentKey until then.
@@ -1056,6 +1055,7 @@ OptimizelyDecideOption[] options
10561055

10571056
var allOptions = GetAllOptions(options);
10581057

1058+
DecisionService.DecisionBatchInProgress = true;
10591059
foreach (var key in keys)
10601060
{
10611061
var decision = Decide(user, key, options);
@@ -1066,6 +1066,8 @@ OptimizelyDecideOption[] options
10661066
}
10671067
}
10681068

1069+
DecisionService.DecisionBatchInProgress = false;
1070+
10691071
return decisionDictionary;
10701072
}
10711073

@@ -1347,7 +1349,8 @@ List<OdpSegmentOption> segmentOptions
13471349

13481350
if (config == null)
13491351
{
1350-
Logger.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'FetchQualifiedSegments'.");
1352+
Logger.Log(LogLevel.ERROR,
1353+
"Datafile has invalid format. Failing 'FetchQualifiedSegments'.");
13511354
return null;
13521355
}
13531356

@@ -1378,7 +1381,8 @@ internal void IdentifyUser(string userId)
13781381
/// <param name="identifiers">Dictionary for identifiers. The caller must provide at least one key-value pair.</param>
13791382
/// <param name="type">Type of event (defaults to `fullstack`)</param>
13801383
/// <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,
1384+
public void SendOdpEvent(string action, Dictionary<string, string> identifiers,
1385+
string type = Constants.ODP_EVENT_TYPE,
13821386
Dictionary<string, object> data = null
13831387
)
13841388
{

0 commit comments

Comments
 (0)