-
Notifications
You must be signed in to change notification settings - Fork 20
[FSSDK-11544] Parsing holdout configuration from datafile + project config holdout impl. #384
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
[FSSDK-11544] Parsing holdout configuration from datafile + project config holdout impl. #384
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
Implements holdout functionality for the Optimizely SDK by adding datafile parsing support and project configuration integration. This enables holdout experiments that can be applied globally or targeted to specific feature flags.
- Added comprehensive holdout entity model with flag inclusion/exclusion logic
- Introduced holdout configuration manager for efficient flag-to-holdout relationship mapping
- Extended project configuration interface to support holdout operations and queries
Reviewed Changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 3 comments.
Show a summary per file
File | Description |
---|---|
OptimizelySDK/Utils/HoldoutConfig.cs | Core holdout configuration manager with caching and flag relationship logic |
OptimizelySDK/Entity/Holdout.cs | Complete holdout entity implementation with audience and variation support |
OptimizelySDK/Entity/IExperimentCore.cs | Shared interface for common experiment/holdout functionality |
OptimizelySDK/Entity/ExperimentCoreExtensions.cs | Extension methods for shared experiment/holdout operations |
OptimizelySDK/Entity/Experiment.cs | Updated to implement IExperimentCore interface |
OptimizelySDK/ProjectConfig.cs | Added holdout-related interface methods |
OptimizelySDK/Config/DatafileProjectConfig.cs | Integrated holdout parsing and configuration management |
Various project files | Added new entity and utility files to compilation |
Test files | Comprehensive test coverage for holdout functionality |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
/// <summary> | ||
/// Flags excluded from this holdout | ||
/// </summary> | ||
public string[] ExcludedFlags { get; set; } = new string[0]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using new string[0]
is less efficient than Array.Empty<string>()
in .NET Core 2.1+ or declaring a static readonly empty array. Consider using a more efficient approach for empty array initialization.
public string[] ExcludedFlags { get; set; } = new string[0]; | |
public string[] IncludedFlags { get; set; } = Array.Empty<string>(); | |
/// <summary> | |
/// Flags excluded from this holdout | |
/// </summary> | |
public string[] ExcludedFlags { get; set; } = Array.Empty<string>(); |
Copilot uses AI. Check for mistakes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is a good suggestion.
/// <summary> | ||
/// Flags excluded from this holdout | ||
/// </summary> | ||
public string[] ExcludedFlags { get; set; } = new string[0]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using new string[0]
is less efficient than Array.Empty<string>()
in .NET Core 2.1+ or declaring a static readonly empty array. Consider using a more efficient approach for empty array initialization.
public string[] ExcludedFlags { get; set; } = new string[0]; | |
public string[] ExcludedFlags { get; set; } = Array.Empty<string>(); |
Copilot uses AI. Check for mistakes.
var message = $@"Holdout ID ""{holdoutId}"" is not in datafile."; | ||
Logger.Log(LogLevel.ERROR, message); | ||
ErrorHandler.HandleError( | ||
new InvalidExperimentException("Provided holdout is not in datafile.")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error message uses InvalidExperimentException
for a holdout operation, which is misleading. Consider creating a more specific exception type like InvalidHoldoutException
or using a more generic exception type.
new InvalidExperimentException("Provided holdout is not in datafile.")); | |
new InvalidHoldoutException("Provided holdout is not in datafile.")); |
Copilot uses AI. Check for mistakes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a good idea.
OptimizelySDK/Entity/Holdout.cs
Outdated
/// </summary> | ||
public Holdout() | ||
{ | ||
Id = ""; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah. This should not be here. I guess I put this while implementing tests, and forget to adjust later!
OptimizelySDK/ProjectConfig.cs
Outdated
/// Get the holdout from the ID | ||
/// </summary> | ||
/// <param name="holdoutId">ID for holdout</param> | ||
/// <returns>Holdout Entity corresponding to the holdout ID or a dummy entity if ID is invalid</returns> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Curious to know csharp doesn't support null value?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My bad. I guess I lost track of the default case. Will update promptly!
activeHoldouts.Add(globalHoldout); | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we could improve this logic. If excluded list is empty we don't need to iterate the global ho list.
if (excludefForFlag.Count > 0) {
/ / do iteration
} else {
activeHoldouts = globalHoldout
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've reviewed and provided comments around performance and consistency. I do not see any security problems. I'm providing a non-blocking approval but suggest consideration of the comments from me and Copilot. Good work. Welcome to C#. 😁
/// <summary> | ||
/// Associative array of Holdout ID to Holdout(s) in the datafile | ||
/// </summary> | ||
private Dictionary<string, Holdout> _HoldoutIdMap; | ||
|
||
public Dictionary<string, Holdout> HoldoutIdMap => _HoldoutIdMap; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for following existing convention. I hope we'll go to more modern syntax public Dictionary<string, Holdout> HoldoutIdMap { get; private set; }
var message = $@"Holdout ID ""{holdoutId}"" is not in datafile."; | ||
Logger.Log(LogLevel.ERROR, message); | ||
ErrorHandler.HandleError( | ||
new InvalidExperimentException("Provided holdout is not in datafile.")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a good idea.
public Holdout[] GetHoldoutsForFlag(string flagKey) | ||
{ | ||
var holdouts = _holdoutConfig?.GetHoldoutsForFlag(flagKey); | ||
return holdouts?.ToArray() ?? new Holdout[0]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return holdouts?.ToArray() ?? new Holdout[0]; | |
return holdouts?.ToArray() ?? Array.Empty<Holdout>(); |
Consider what Copilot pointed out too.
var collect = false; | ||
var replaced = string.Empty; | ||
|
||
foreach (var ch in conditionString) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would this accomplish the goal in lieu of looping?
return Regex.Replace(
conditionString,
@"AUDIENCE\(""([^""]+)""\)",
match =>
{
var id = match.Groups[1].Value;
var name = audiencesMap.TryGetValue(id, out var audienceName) ? audienceName : id;
return $"\"{name}\"";
});
/// <summary> | ||
/// Flags excluded from this holdout | ||
/// </summary> | ||
public string[] ExcludedFlags { get; set; } = new string[0]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is a good suggestion.
/// <summary> | ||
/// Generate variation key maps for performance optimization | ||
/// </summary> | ||
public void GenerateVariationKeyMap() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NIT: Does GenerateVariationMaps
sound more aligned with the method generating an id map and a key map?
/// </summary> | ||
/// <param name="audiencesMap">Map of audience ID to audience name</param> | ||
/// <returns>Serialized audience string with names</returns> | ||
public string SerializeAudiences(Dictionary<string, string> audiencesMap) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this method being used? (just double-checking)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
...or maybe it will be? …or perhaps we're following a reference implementation for consistency across other SDKs. 😄
{ | ||
if (AudienceConditions == null) | ||
{ | ||
return null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we also set the backing field to null
to ensure the value is always consistent with the source data?
return null; | |
_audienceConditionsString = null; | |
return null; |
/// <returns>The Holdout object if found, null otherwise</returns> | ||
public Holdout GetHoldout(string holdoutId) | ||
{ | ||
return _holdoutIdMap.ContainsKey(holdoutId) ? _holdoutIdMap[holdoutId] : null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return _holdoutIdMap.ContainsKey(holdoutId) ? _holdoutIdMap[holdoutId] : null; | |
if (string.IsNullOrEmpty(holdoutId)) | |
{ | |
return null; | |
} | |
_holdoutIdMap.TryGetValue(holdoutId, out var holdout); | |
return holdout; |
...for better performance and null/empty input check
Summary
Datafile Parsing to support holdout + ProjectConfig holdout change
Test plan
Tests have been added to support the changes
Issues
FSSDK-11544
FSSDK-11545