Skip to content

[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

Open
wants to merge 9 commits into
base: master
Choose a base branch
from

Conversation

junaed-optimizely
Copy link
Contributor

@junaed-optimizely junaed-optimizely commented Aug 12, 2025

Summary

Datafile Parsing to support holdout + ProjectConfig holdout change

Test plan

Tests have been added to support the changes

Issues

FSSDK-11544
FSSDK-11545

@junaed-optimizely junaed-optimizely marked this pull request as ready for review August 13, 2025 18:02
@junaed-optimizely junaed-optimizely changed the title [FSSDK-11544] experiment core and holdout introduction [FSSDK-11544] Parsing holdout configuration from datafile + project config holdout impl. Aug 14, 2025
Copy link

@Copilot Copilot AI left a 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];
Copy link
Preview

Copilot AI Aug 14, 2025

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.

Suggested change
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.

Copy link
Contributor

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];
Copy link
Preview

Copilot AI Aug 14, 2025

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.

Suggested change
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."));
Copy link
Preview

Copilot AI Aug 14, 2025

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.

Suggested change
new InvalidExperimentException("Provided holdout is not in datafile."));
new InvalidHoldoutException("Provided holdout is not in datafile."));

Copilot uses AI. Check for mistakes.

Copy link
Contributor

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.

/// </summary>
public Holdout()
{
Id = "";

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?

Copy link
Contributor Author

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!

/// 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>

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?

Copy link
Contributor Author

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);
}
}

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
}

Copy link
Contributor

@mikechu-optimizely mikechu-optimizely left a 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#. 😁

Comment on lines +219 to +225
/// <summary>
/// Associative array of Holdout ID to Holdout(s) in the datafile
/// </summary>
private Dictionary<string, Holdout> _HoldoutIdMap;

public Dictionary<string, Holdout> HoldoutIdMap => _HoldoutIdMap;

Copy link
Contributor

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."));
Copy link
Contributor

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];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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)
Copy link
Contributor

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];
Copy link
Contributor

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()
Copy link
Contributor

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)
Copy link
Contributor

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)

Copy link
Contributor

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;
Copy link
Contributor

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?

Suggested change
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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants