Skip to content

[AI-FSSDK] [FSSDK-12369] Add local holdouts support with includedRules field#397

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

[AI-FSSDK] [FSSDK-12369] Add local holdouts support with includedRules field#397
Mat001 wants to merge 2 commits into
masterfrom
ai/mat001/FSSDK-12369-local-holdouts

Conversation

@Mat001
Copy link
Copy Markdown
Contributor

@Mat001 Mat001 commented May 13, 2026

Summary

This PR adds local holdouts support to the Ruby SDK, allowing holdouts to target specific rules within a flag via an includedRules field.

Jira Ticket: FSSDK-12369

Changes

Configuration (lib/optimizely/config/datafile_project_config.rb)

  • Added @global_holdouts (Array) and @rule_holdouts_map (Hash) instance variables
  • Added global_holdouts and rule_holdouts_map to attr_reader
  • During holdout initialization: classify each holdout as global (includedRules absent/nil) or local (includedRules is an array), populating the respective data structures
  • Added get_global_holdouts method: returns all global holdouts (nil includedRules)
  • Added get_holdouts_for_rule(rule_id) method: returns local holdouts targeting a specific rule

Decision Logic (lib/optimizely/decision_service.rb)

  • Updated get_decision_for_flag to use get_global_holdouts instead of holdout_id_map.values for flag-level holdout evaluation
  • Added local holdout checks in get_variation_from_experiment_rule (after forced decisions, before regular bucketing)
  • Added local holdout checks in get_variation_from_delivery_rule (after forced decisions, before audience/bucketing evaluation)

Tests

  • spec/local_holdouts_spec.rb (new): 10 tests covering:
    • Backward compatibility (holdouts without includedRules field treated as global)
    • Global/local holdout classification
    • rule_holdouts_map population
    • get_global_holdouts and get_holdouts_for_rule helper methods
    • Edge cases (empty includedRules array, no global holdouts)
  • spec/spec_params.rb: Added CONFIG_BODY_WITH_LOCAL_HOLDOUTS fixture with both global and local holdouts

Decision Flow

Flag evaluation
  └── Global holdouts (includedRules == nil) → checked at flag level
        └── If not in global holdout:
              └── Forced decisions → checked per rule
                    └── Local holdouts (includedRules is array) → checked per rule
                          └── Regular bucketing / audience evaluation

Backward Compatibility

Old datafiles without includedRules key: Ruby JSON parsing returns nil for missing keys → treated as global holdout → behavior unchanged.

🤖 Generated with Claude Code

…s field

- Add global_holdouts and rule_holdouts_map to DatafileProjectConfig initialization
- Classify holdouts as global (includedRules nil) or local (includedRules array) during config parsing
- Add get_global_holdouts and get_holdouts_for_rule methods to DatafileProjectConfig
- Update get_decision_for_flag to use get_global_holdouts instead of holdout_id_map.values
- Add local holdout checks in get_variation_from_experiment_rule (after forced decisions)
- Add local holdout checks in get_variation_from_delivery_rule (after forced decisions)
- Add CONFIG_BODY_WITH_LOCAL_HOLDOUTS test fixture with mixed global and local holdouts
- Add local_holdouts_spec.rb with 10 tests covering config classification and helper methods

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove duplicate get_global_holdouts def (attr_reader already exists)
- Use Style/Next (next unless) in local holdout loops
- Fix Layout/SpaceInsideHashLiteralBraces in spec

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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.

1 participant