Skip to content

[AI-FSSDK] [FSSDK-12369] Add local holdouts support to Go SDK#451

Open
Mat001 wants to merge 3 commits into
masterfrom
ai/mat001/FSSDK-12369-local-holdouts
Open

[AI-FSSDK] [FSSDK-12369] Add local holdouts support to Go SDK#451
Mat001 wants to merge 3 commits into
masterfrom
ai/mat001/FSSDK-12369-local-holdouts

Conversation

@Mat001
Copy link
Copy Markdown
Contributor

@Mat001 Mat001 commented May 14, 2026

Summary

Adds local holdouts support to the Go SDK. Local holdouts allow targeting specific experiment and delivery rules within a flag, while global holdouts continue to apply across all rules in all flags. This introduces a rule-level targeting system using an optional includedRules field on the holdout data model.

Changes

  • Added IncludedRules *[]string field to the Holdout entity with IsGlobal() helper method
  • Extended HoldoutConfig with GetGlobalHoldouts() and GetHoldoutsForRule(ruleID) APIs and updated the holdout mapper to build the rule-to-holdout index
  • Integrated local holdout evaluation per-rule in both FeatureExperimentService and RolloutService, evaluated after forced decisions and before audience/traffic checks
  • Added GetDecisionForRule method to HoldoutService for local holdout bucketing logic
  • Preserved backward compatibility: datafiles without includedRules default to global holdout behavior

Jira Ticket

FSSDK-12369

Copy link
Copy Markdown

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

Adds local holdout support so holdouts can target specific experiment or rollout rules while preserving existing global holdout behavior for datafiles without includedRules.

Changes:

  • Adds IncludedRules and global/local holdout classification.
  • Indexes local holdouts by rule ID in project config mapping.
  • Evaluates local holdouts in feature experiment and rollout decision flows after forced decisions and before normal rule evaluation.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
pkg/entities/experiment.go Adds local/global holdout metadata helper.
pkg/config/interface.go Extends project config holdout access APIs.
pkg/config/datafileprojectconfig/entities/entities.go Adds datafile includedRules parsing field.
pkg/config/datafileprojectconfig/config.go Stores local and global holdout indexes.
pkg/config/datafileprojectconfig/mappers/holdout.go Maps global holdouts by flag and local holdouts by rule.
pkg/config/datafileprojectconfig/mappers/holdout_test.go Adds mapper coverage for global/local holdout behavior.
pkg/decision/holdout_service.go Adds rule-level local holdout evaluation.
pkg/decision/holdout_service_test.go Adds local holdout decision service tests.
pkg/decision/feature_experiment_service.go Integrates local holdout checks into feature experiment evaluation.
pkg/decision/feature_experiment_service_test.go Updates constructor tests and forced-decision precedence coverage.
pkg/decision/rollout_service.go Integrates local holdout checks into rollout rule evaluation.
pkg/decision/composite_feature_service.go Shares holdout service with feature experiment service.
pkg/decision/helpers_test.go Updates decision test mock config interface.
pkg/decision/evaluator/audience_evaluator_test.go Updates evaluator test mock config interface.
pkg/cmab/service_test.go Updates CMAB test mock config interface.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pkg/decision/feature_experiment_service.go
Comment thread pkg/config/interface.go
@Mat001 Mat001 force-pushed the ai/mat001/FSSDK-12369-local-holdouts branch from a4fed68 to 9e39ee2 Compare May 20, 2026 21:37
Mat001 and others added 2 commits May 21, 2026 10:03
- Add rollout service tests for local holdout in regular and fallback rules,
  and FD-beats-local-holdout ordering enforcement
- Add feature experiment service FD-beats-100%-local-holdout enforcement test
- Add GetHoldoutsForRule config tests (previously untested)
- Extract checkForLocalHoldout closure to getLocalHoldoutDecision method
  to bring GetDecision cyclomatic complexity from 18 to 16 (lint fix)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@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.

Looks good to me. Added some comments.

Variations []Variation `json:"variations"`
TrafficAllocation []TrafficAllocation `json:"trafficAllocation"`
// IncludedRules is optional. nil = global holdout (applies to all rules across all flags).
// Non-nil array = local holdout (applies only to the specified rule IDs).
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Looks consistence with other sdks

if len(runningHoldouts) > 0 {
flagHoldoutsMap[feature.Key] = runningHoldouts
if len(globalHoldouts) > 0 {
flagHoldoutsMap[feature.Key] = globalHoldouts
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Do we still need flagHoldoutsMap? I think ruleHoldoutsMap is enough for targeting local HO

@@ -63,6 +63,10 @@ type DatafileProjectConfig struct {
holdouts []entities.Holdout
holdoutIDMap map[string]entities.Holdout
flagHoldoutsMap map[string][]entities.Holdout
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I think we can remove it, no longer needed.

}

// GetHoldoutsForFlag returns all holdouts applicable to the given feature flag
// GetHoldoutsForFlag returns all global holdouts applicable to the given feature flag.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I think we no longer need flag base method anymore.

audienceMap, audienceSegmentList := mappers.MapAudiences(append(datafile.TypedAudiences, datafile.Audiences...))
flagVariationsMap := mappers.MapFlagVariations(featureMap)
holdouts, holdoutIDMap, flagHoldoutsMap := mappers.MapHoldouts(datafile.Holdouts, featureMap)
holdouts, holdoutIDMap, flagHoldoutsMap, ruleHoldoutsMap := mappers.MapHoldouts(datafile.Holdouts, featureMap)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remove 'flagHoldoutsMap'

Comment thread pkg/config/interface.go
@@ -57,6 +57,12 @@ type ProjectConfig interface {
GetFlagVariationsMap() map[string][]entities.Variation
GetRegion() string
GetHoldoutsForFlag(featureKey string) []entities.Holdout
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remove it.

// GetDecisionForRule evaluates local holdouts targeting the given rule ID.
// It returns a non-empty FeatureDecision if the user is bucketed into a local holdout for this rule.
// Local holdouts are evaluated per-rule, after forced decisions, before audience/traffic checks.
func (h HoldoutService) GetDecisionForRule(ruleID string, projectConfig config.ProjectConfig, userContext entities.UserContext, options *decide.Options) (FeatureDecision, decide.DecisionReasons, error) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Can we update the methods name as like:

  • GetGlobalDecision
  • GetLocalDecisionForRule

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