Skip to content

[FSSDK-11546] holdout + decision service impl. #386

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 12 commits into
base: master
Choose a base branch
from

Conversation

junaed-optimizely
Copy link
Contributor

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

Summary

Added holdout support in decision service.

Test plan

Tests: Comprehensive tests addition

Issues

FSSDK-11546

@junaed-optimizely junaed-optimizely marked this pull request as ready for review August 22, 2025 15:52
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

This PR adds holdout support to the Optimizely .NET SDK's decision service, enabling the SDK to evaluate holdouts (experiments that exclude users from treatment) alongside existing experiments and rollouts.

  • Refactored the inheritance hierarchy to use ExperimentCore as the base class for both experiments and holdouts
  • Implemented holdout evaluation logic in the decision service with proper priority ordering (holdouts → experiments → rollouts)
  • Added comprehensive test coverage for holdout functionality including bucketing, decision reasons, and user context operations

Reviewed Changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
ExperimentCore.cs Added base class with common experiment/holdout properties and moved shared logic from Experiment class
Holdout.cs Modified to override LayerId property to always return empty string as holdouts don't belong to layers
Experiment.cs Removed duplicate properties now inherited from ExperimentCore
DecisionService.cs Added GetDecisionForFlag method implementing holdout evaluation with proper priority ordering
Bucketer.cs Updated to accept ExperimentCore instead of Experiment for holdout compatibility
FeatureDecision.cs Added holdout decision source constant and updated to use ExperimentCore
Various other files Updated method signatures to use ExperimentCore for holdout compatibility
Test files Added comprehensive holdout test coverage including bucketing, decision service, and user context tests

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

public bool IsExperimentRunning =>
!string.IsNullOrEmpty(Status) && Status == STATUS_RUNNING;

public bool IsActivated => IsExperimentRunning;
Copy link
Preview

Copilot AI Aug 22, 2025

Choose a reason for hiding this comment

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

The IsActivated property delegates to IsExperimentRunning, but this creates an indirect dependency on a misleadingly named property. Consider implementing this directly as !string.IsNullOrEmpty(Status) && Status == STATUS_RUNNING for clarity.

Suggested change
public bool IsActivated => IsExperimentRunning;
public bool IsActivated => !string.IsNullOrEmpty(Status) && Status == STATUS_RUNNING;

Copilot uses AI. Check for mistakes.

Copy link
Contributor Author

@junaed-optimizely junaed-optimizely Aug 22, 2025

Choose a reason for hiding this comment

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

Would it be ok if we use existing isExperimentRunning for holdouts as well ? I think we dont need this newly introduced prop then in C# ? @muzahidul-opti ?

Choose a reason for hiding this comment

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

Yah, we can do that since only running ho will be in the datafile.

Choose a reason for hiding this comment

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

We can keep it generic, isRunning what do you think?

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. Since Experiment, ExperimentCore and Holdout all are internal, we can change this to isRunning and it should not be breaking! Unless I am getting this completely wrong!

CC: @mikechu-optimizely!

Copy link

@muzahidul-opti muzahidul-opti left a comment

Choose a reason for hiding this comment

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

LGTM, few comments added

{
_VariationKeyMap[holdout.Key][variation.Key] = variation;
_VariationIdMap[holdout.Key][variation.Id] = variation;
_VariationKeyMapByExperimentId[holdout.Id][variation.Key] = variation;

Choose a reason for hiding this comment

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

Looks good to me. Since the ho should have only one variation, we may add instance method to HO like this:

func getVariation(id: String) -> Variation? {
        return variations.filter { $0.id == id }.first
    }

Copy link
Contributor Author

Choose a reason for hiding this comment

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

C# Uses Variation key map for bucketing and this fits well with that pattern. I am not sure about the instance method and its implication. Could you clarify please ?

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.

2 participants