Skip to content

Commit efc6045

Browse files
committed
Feature Flags & Rollout - Implemented New API changes.
1 parent e3384b8 commit efc6045

File tree

7 files changed

+709
-20
lines changed

7 files changed

+709
-20
lines changed

OptimizelySDK.Tests/OptimizelyTest.cs

Lines changed: 391 additions & 5 deletions
Large diffs are not rendered by default.

OptimizelySDK/Bucketing/DecisionService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@ public virtual Variation GetVariationForFeatureExperiment(FeatureFlag featureFla
360360
/// <param name = "filteredAttributes" >The user's attributes. This should be filtered to just attributes in the Datafile.</param>
361361
/// <returns>null if the user is not bucketed into any variation or the Variation the user is bucketed into
362362
/// if the user is successfully bucketed.</returns>
363-
public Variation GetVariationForFeature(FeatureFlag featureFlag, string userId, UserAttributes filteredAttributes)
363+
public virtual Variation GetVariationForFeature(FeatureFlag featureFlag, string userId, UserAttributes filteredAttributes)
364364
{
365365
// Check if the feature flag has an experiment and the user is bucketed into that experiment.
366366
var variation = GetVariationForFeatureExperiment(featureFlag, userId, filteredAttributes);

OptimizelySDK/Entity/FeatureFlag.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,15 @@ public List<FeatureVariable> Variables
4343
}
4444

4545
public Dictionary<string, FeatureVariable> VariableKeyToFeatureVariableMap { get; set; }
46+
47+
public FeatureVariable GetFeatureVariableFromKey(string variableKey)
48+
{
49+
if (VariableKeyToFeatureVariableMap != null && VariableKeyToFeatureVariableMap.ContainsKey(variableKey))
50+
{
51+
return VariableKeyToFeatureVariableMap[variableKey];
52+
}
53+
54+
return null;
55+
}
4656
}
4757
}

OptimizelySDK/Entity/Variation.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,15 @@ public List<FeatureVariableUsage> FeatureVariableUsageInstances
3939
}
4040

4141
public Dictionary<string, FeatureVariableUsage> VariableIdToVariableUsageInstanceMap { get; set; }
42+
43+
public FeatureVariableUsage GetFeatureVariableUsageFromId(string variableId)
44+
{
45+
if (VariableIdToVariableUsageInstanceMap != null && VariableIdToVariableUsageInstanceMap.ContainsKey(variableId))
46+
{
47+
return VariableIdToVariableUsageInstanceMap[variableId];
48+
}
49+
50+
return null;
51+
}
4252
}
4353
}

OptimizelySDK/Optimizely.cs

Lines changed: 245 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -153,19 +153,7 @@ public string Activate(string experimentKey, string userId, UserAttributes userA
153153
userAttributes = userAttributes.FilterNullValues(Logger);
154154
}
155155

156-
var impressionEvent = EventBuilder.CreateImpressionEvent(Config, experiment, variation.Id, userId, userAttributes);
157-
Logger.Log(LogLevel.INFO, string.Format("Activating user {0} in experiment {1}.", userId, experimentKey));
158-
Logger.Log(LogLevel.DEBUG, string.Format("Dispatching impression event to URL {0} with params {1}.",
159-
impressionEvent, impressionEvent.GetParamsAsJson()));
160-
161-
try
162-
{
163-
EventDispatcher.DispatchEvent(impressionEvent);
164-
}
165-
catch (Exception exception)
166-
{
167-
Logger.Log(LogLevel.ERROR, string.Format("Unable to dispatch impression event. Error {0}", exception.Message));
168-
}
156+
SendImpressionEvent(experiment, variation.Id, userId, userAttributes);
169157

170158
return variation.Key;
171159
}
@@ -308,5 +296,249 @@ public Variation GetForcedVariation(string experimentKey, string userId)
308296

309297
return forcedVariation;
310298
}
299+
300+
#region FeatureFlag APIs
301+
302+
/// <summary>
303+
/// Determine whether a feature is enabled.
304+
/// Send an impression event if the user is bucketed into an experiment using the feature.
305+
/// </summary>
306+
/// <param name="experimentKey">The experiment key</param>
307+
/// <param name="userId">The user ID</param>
308+
/// <param name="userAttributes">The user's attributes.</param>
309+
/// <returns>True if feature is enabled, false or null otherwise</returns>
310+
public bool? IsFeatureEnabled(string featureKey, string userId, UserAttributes userAttributes = null)
311+
{
312+
if (string.IsNullOrEmpty(userId))
313+
{
314+
Logger.Log(LogLevel.ERROR, "User ID must not be empty.");
315+
return null;
316+
}
317+
318+
if (string.IsNullOrEmpty(featureKey))
319+
{
320+
Logger.Log(LogLevel.ERROR, "Feature flag key must not be empty.");
321+
return null;
322+
}
323+
324+
var featureFlag = Config.GetFeatureFlagFromKey(featureKey);
325+
if (string.IsNullOrEmpty(featureFlag.Key))
326+
return null;
327+
328+
if (!Validator.IsFeatureFlagValid(Config, featureFlag))
329+
return false;
330+
331+
var variation = DecisionService.GetVariationForFeature(featureFlag, userId, userAttributes);
332+
if ( variation == null )
333+
{
334+
Logger.Log(LogLevel.INFO, $@"Feature flag ""{featureKey}"" is not enabled for user ""{userId}"".");
335+
return false;
336+
}
337+
338+
var experiment = Config.GetExperimentForVariationId(variation.Id);
339+
340+
if (!string.IsNullOrEmpty(experiment.Key))
341+
SendImpressionEvent(experiment, variation.Id, userId, userAttributes);
342+
else
343+
Logger.Log(LogLevel.INFO, $@"The user ""{userId}"" is not being experimented on feature ""{featureKey}"".");
344+
345+
Logger.Log(LogLevel.INFO, $@"Feature flag ""{featureKey}"" is enabled for user ""{userId}"".");
346+
return true;
347+
}
348+
349+
/// <summary>
350+
/// Gets the feature variable value for given type.
351+
/// </summary>
352+
/// <param name="featureKey">The feature flag key</param>
353+
/// <param name="variableKey">The variable key</param>
354+
/// <param name="userId">The user ID</param>
355+
/// <param name="userAttributes">The user's attributes</param>
356+
/// <param name="variableType">Variable type</param>
357+
/// <returns>string | null Feature variable value</returns>
358+
public virtual string GetFeatureVariableValueForType(string featureKey, string variableKey, string userId,
359+
UserAttributes userAttributes, FeatureVariable.VariableType variableType)
360+
{
361+
if (string.IsNullOrEmpty(featureKey))
362+
{
363+
Logger.Log(LogLevel.ERROR, "Feature flag key must not be empty.");
364+
return null;
365+
}
366+
367+
if (string.IsNullOrEmpty(variableKey))
368+
{
369+
Logger.Log(LogLevel.ERROR, "Variable key must not be empty.");
370+
return null;
371+
}
372+
373+
if (string.IsNullOrEmpty(userId))
374+
{
375+
Logger.Log(LogLevel.ERROR, "User ID must not be empty.");
376+
return null;
377+
}
378+
379+
var featureFlag = Config.GetFeatureFlagFromKey(featureKey);
380+
if (string.IsNullOrEmpty(featureFlag.Key))
381+
return null;
382+
383+
var featureVariable = featureFlag.GetFeatureVariableFromKey(variableKey);
384+
if (featureVariable == null)
385+
{
386+
Logger.Log(LogLevel.ERROR,
387+
$@"No feature variable was found for key ""{variableKey}"" in feature flag ""{featureKey}"".");
388+
return null;
389+
}
390+
else if (featureVariable.Type != variableType)
391+
{
392+
Logger.Log(LogLevel.ERROR,
393+
$@"Variable is of type ""{featureVariable.Type}"", but you requested it as type ""{variableType}"".");
394+
return null;
395+
}
396+
397+
var variableValue = featureVariable.DefaultValue;
398+
var variation = DecisionService.GetVariationForFeature(featureFlag, userId, userAttributes);
399+
400+
if (variation != null)
401+
{
402+
var featureVariableUsageInstance = variation.GetFeatureVariableUsageFromId(featureVariable.Id);
403+
if (featureVariableUsageInstance != null)
404+
{
405+
variableValue = featureVariableUsageInstance.Value;
406+
Logger.Log(LogLevel.INFO,
407+
$@"Returning variable value ""{variableValue}"" for variation ""{variation.Key}"" of feature flag ""{featureFlag.Key}"".");
408+
}
409+
else
410+
{
411+
Logger.Log(LogLevel.INFO,
412+
$@"Variable ""{variableKey}"" is not used in variation ""{variation.Key}"", returning default value ""{variableValue}"".");
413+
}
414+
}
415+
else
416+
{
417+
Logger.Log(LogLevel.INFO,
418+
$@"User ""{userId}"" is not in any variation for feature flag ""{featureFlag.Key}"", returning default value ""{variableValue}"".");
419+
}
420+
421+
return variableValue;
422+
}
423+
424+
/// <summary>
425+
/// Gets boolean feature variable value.
426+
/// </summary>
427+
/// <param name="featureKey">The feature flag key</param>
428+
/// <param name="variableKey">The variable key</param>
429+
/// <param name="userId">The user ID</param>
430+
/// <param name="userAttributes">The user's attributes</param>
431+
/// <returns>bool | Feature variable value or null</returns>
432+
public bool? GetFeatureVariableBoolean(string featureKey, string variableKey, string userId, UserAttributes userAttributes)
433+
{
434+
var variableType = FeatureVariable.VariableType.BOOLEAN;
435+
var variableValue = GetFeatureVariableValueForType(featureKey, variableKey, userId, userAttributes, variableType);
436+
437+
if (variableValue != null)
438+
{
439+
if (Boolean.TryParse(variableValue, out bool booleanValue))
440+
return booleanValue;
441+
else
442+
Logger.Log(LogLevel.ERROR, $@"Unable to cast variable value ""{variableValue}"" to type ""{variableType}"".");
443+
}
444+
445+
return null;
446+
}
447+
448+
/// <summary>
449+
/// Gets double feature variable value.
450+
/// </summary>
451+
/// <param name="featureKey">The feature flag key</param>
452+
/// <param name="variableKey">The variable key</param>
453+
/// <param name="userId">The user ID</param>
454+
/// <param name="userAttributes">The user's attributes</param>
455+
/// <returns>double | Feature variable value or null</returns>
456+
public double? GetFeatureVariableDouble(string featureKey, string variableKey, string userId, UserAttributes userAttributes)
457+
{
458+
var variableType = FeatureVariable.VariableType.DOUBLE;
459+
var variableValue = GetFeatureVariableValueForType(featureKey, variableKey, userId, userAttributes, variableType);
460+
461+
if (variableValue != null)
462+
{
463+
if (Double.TryParse(variableValue, out double doubleValue))
464+
return doubleValue;
465+
else
466+
Logger.Log(LogLevel.ERROR, $@"Unable to cast variable value ""{variableValue}"" to type ""{variableType}"".");
467+
}
468+
469+
return null;
470+
}
471+
472+
/// <summary>
473+
/// Gets integer feature variable value.
474+
/// </summary>
475+
/// <param name="featureKey">The feature flag key</param>
476+
/// <param name="variableKey">The variable key</param>
477+
/// <param name="userId">The user ID</param>
478+
/// <param name="userAttributes">The user's attributes</param>
479+
/// <returns>int | Feature variable value or null</returns>
480+
public int? GetFeatureVariableInteger(string featureKey, string variableKey, string userId, UserAttributes userAttributes)
481+
{
482+
var variableType = FeatureVariable.VariableType.INTEGER;
483+
var variableValue = GetFeatureVariableValueForType(featureKey, variableKey, userId, userAttributes, variableType);
484+
485+
if (variableValue != null)
486+
{
487+
if (Int32.TryParse(variableValue, out int intValue))
488+
return intValue;
489+
else
490+
Logger.Log(LogLevel.ERROR, $@"Unable to cast variable value ""{variableValue}"" to type ""{variableType}"".");
491+
}
492+
493+
return null;
494+
}
495+
496+
/// <summary>
497+
/// Gets string feature variable value.
498+
/// </summary>
499+
/// <param name="featureKey">The feature flag key</param>
500+
/// <param name="variableKey">The variable key</param>
501+
/// <param name="userId">The user ID</param>
502+
/// <param name="userAttributes">The user's attributes</param>
503+
/// <returns>string | Feature variable value or null</returns>
504+
public string GetFeatureVariableString(string featureKey, string variableKey, string userId, UserAttributes userAttributes)
505+
{
506+
return GetFeatureVariableValueForType(featureKey, variableKey, userId, userAttributes,
507+
FeatureVariable.VariableType.STRING);
508+
}
509+
510+
/// <summary>
511+
/// Sends impression event.
512+
/// </summary>
513+
/// <param name="experiment">The experiment</param>
514+
/// <param name="variationId">The variation Id</param>
515+
/// <param name="userId">The user ID</param>
516+
/// <param name="userAttributes">The user's attributes</param>
517+
private void SendImpressionEvent(Experiment experiment, string variationId, string userId,
518+
UserAttributes userAttributes)
519+
{
520+
if (experiment.IsExperimentRunning)
521+
{
522+
var impressionEvent = EventBuilder.CreateImpressionEvent(Config, experiment, variationId, userId, userAttributes);
523+
Logger.Log(LogLevel.INFO, string.Format("Activating user {0} in experiment {1}.", userId, experiment.Key));
524+
Logger.Log(LogLevel.DEBUG, string.Format("Dispatching impression event to URL {0} with params {1}.",
525+
impressionEvent.Url, impressionEvent.GetParamsAsJson()));
526+
527+
try
528+
{
529+
EventDispatcher.DispatchEvent(impressionEvent);
530+
}
531+
catch (Exception exception)
532+
{
533+
Logger.Log(LogLevel.ERROR, string.Format("Unable to dispatch impression event. Error {0}", exception.Message));
534+
}
535+
}
536+
else
537+
{
538+
Logger.Log(LogLevel.ERROR, @"Experiment has ""Launched"" status so not dispatching event during activation.");
539+
}
540+
}
541+
542+
#endregion // FeatureFlag APIs
311543
}
312544
}

OptimizelySDK/ProjectConfig.cs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,13 @@ private Dictionary<string, Dictionary<string, Variation>> _VariationIdMap
125125
/// </summary>
126126
private Dictionary<string, Rollout> _RolloutIdMap;
127127
public Dictionary<string, Rollout> RolloutIdMap { get { return _RolloutIdMap; } }
128-
128+
129+
/// <summary>
130+
/// Associative array of Variation ID to Experiments(s) in the datafile
131+
/// </summary>
132+
private Dictionary<string, Experiment> _VariationIdToExperimentMap = new Dictionary<string, Experiment>();
133+
public Dictionary<string, Experiment> VariationIdToExperimentMap{ get { return _VariationIdToExperimentMap; } }
134+
129135
//========================= Callbacks ===========================
130136

131137
/// <summary>
@@ -236,6 +242,9 @@ private void Initialize()
236242
{
237243
_VariationKeyMap[experiment.Key][variation.Key] = variation;
238244
_VariationIdMap[experiment.Key][variation.Id] = variation;
245+
246+
// Generate Variation ID to Experiment map.
247+
_VariationIdToExperimentMap[variation.Id] = experiment;
239248
}
240249
}
241250
}
@@ -532,5 +541,21 @@ public Rollout GetRolloutFromId(string rolloutId)
532541
ErrorHandler.HandleError(new Exceptions.InvalidRolloutException("Provided rollout is not in datafile."));
533542
return new Rollout();
534543
}
544+
545+
/// <summary>
546+
/// Get experiment from variation ID.
547+
/// </summary>
548+
/// <param name="variationId">Variation ID</param>
549+
/// <returns>Experiment Entity corresponding to the variation ID or a dummy entity if ID is invalid</returns>
550+
public Experiment GetExperimentForVariationId(string variationId)
551+
{
552+
if (_VariationIdToExperimentMap.ContainsKey(variationId))
553+
return _VariationIdToExperimentMap[variationId];
554+
555+
Logger.Log(LogLevel.ERROR, $@"No experiment has been defined in datafile for variation ""{variationId}"".");
556+
ErrorHandler.HandleError(new Exceptions.InvalidVariationException("No experiment has been found for provided variation Id in the datafile."));
557+
558+
return new Experiment();
559+
}
535560
}
536561
}

OptimizelySDK/Utils/Validator.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,32 @@ public static bool AreEventTagsValid(Dictionary<string, object> eventTags) {
7676
return eventTags.All(tag => !int.TryParse(tag.Key, out key));
7777

7878
}
79+
80+
/// <summary>
81+
/// Determines whether all the experiments in the feature flag
82+
/// belongs to the same mutex group
83+
/// </summary>
84+
/// <param name="projectConfig">The project config object</param>
85+
/// <param name="featureFlag">Feature flag to validate</param>
86+
/// <returns>true if feature flag is valid.</returns>
87+
public static bool IsFeatureFlagValid(ProjectConfig projectConfig, FeatureFlag featureFlag)
88+
{
89+
var experimentIds = featureFlag.ExperimentIds;
90+
91+
if (experimentIds == null || experimentIds.Count <= 1)
92+
return true;
93+
94+
var groupId = projectConfig.GetExperimentFromId(experimentIds[0]).GroupId;
95+
96+
for (int i = 1; i < experimentIds.Count; i++)
97+
{
98+
// Every experiment should have the same group Id.
99+
if (projectConfig.GetExperimentFromId(experimentIds[i]).GroupId != groupId)
100+
return false;
101+
}
102+
103+
return true;
104+
}
79105
}
80106
}
81107

0 commit comments

Comments
 (0)