Skip to content

Commit 91fb9a5

Browse files
[AI-FSSDK] [FSSDK-12337] Add Feature Rollout support (#392)
* [AI-FSSDK] [FSSDK-12337] Add Feature Rollout support Add Feature Rollout support to the Ruby SDK. During project config parsing, inject the "everyone else" variation from the flag's rollout into any experiment with type "feature_rollout", enabling correct evaluation without changes to decision logic. - Added config parsing logic to inject the everyone else rollout variation into feature_rollout experiments - Added traffic allocation entry (endOfRange=10000) for injected variation - Added get_everyone_else_variation helper to extract the last rollout rule's first variation - Added 6 unit tests covering injection, variation maps, edge cases, and backward compatibility * [AI-FSSDK] [FSSDK-12337] Add type field to experiment JSON schema Add optional 'type' string field to the experiment properties in the datafile JSON schema validation constants. * [AI-FSSDK] [FSSDK-12337] Add test for experiment type field parsing Verify that the type field from the datafile is correctly preserved on experiment hashes after config parsing. * [AI-FSSDK] [FSSDK-12337] Move flag_variation_map after rollout injection Move @flag_variation_map generation to after the feature rollout injection block so the everyone-else variation is included in get_variation_from_flag lookups used by forced decisions. * [AI-FSSDK] [FSSDK-12337] Add tests for featureEnabled and variables propagation Add tests verifying: - Injected everyone-else variation preserves featureEnabled=false - Variables from the rollout variation carry through to the injected variation and populate variation_id_to_variable_usage_map * [AI-FSSDK] [FSSDK-12337] Fix pre-existing rubocop Lint/Void offense in spec_params Remove redundant else clause in deep_clone that referenced a variable in void context (Lint/Void: Variable new_obj used in void context). * [AI-FSSDK] [FSSDK-12337] Add EXPERIMENT_TYPES constant with enum validation - Define EXPERIMENT_TYPES constant with valid experiment type values (ab, mab, cmab, td, fr) in Constants module - Use enum constraint in JSON schema to validate the type field - Reference constant in injection check instead of raw string literal - Add flag_variation_map assertion to variation maps test * [AI-FSSDK] [FSSDK-12337] Remove tests not in ticket spec Remove 3 tests not in updated Jira ticket test requirements: - LAST rollout rule selection - featureEnabled preservation - Variables propagation * [AI-FSSDK] [FSSDK-12337] Fix rubocop EmptyLinesAroundBlockBody offense * [FSSDK-12337] Fix experiment type values to match backend Update EXPERIMENT_TYPES to use actual backend values: 'multi_armed_bandit' and 'contextual_multi_armed_bandit' instead of shorthand 'mab' and 'cmab'. Restore accidentally removed else branch in spec_params deep_clone. * [AI-FSSDK] [FSSDK-12337] Fix rubocop Lint/Void offense in spec_params Remove redundant else clause in deep_clone that was accidentally restored in a previous commit. * [AI-FSSDK] [FSSDK-12337] Update experiment type values to short-form abbreviations
1 parent 0c74513 commit 91fb9a5

File tree

4 files changed

+372
-3
lines changed

4 files changed

+372
-3
lines changed

lib/optimizely/config/datafile_project_config.rb

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,6 @@ def initialize(datafile, logger, error_handler)
180180
@all_segments.concat Audience.get_segments(audience['conditions'])
181181
end
182182

183-
@flag_variation_map = generate_feature_variation_map(@feature_flags)
184183
@all_experiments = @experiment_id_map.merge(@rollout_experiment_id_map)
185184
@all_experiments.each do |id, exp|
186185
variations = exp.fetch('variations')
@@ -205,8 +204,33 @@ def initialize(datafile, logger, error_handler)
205204
feature_flag['experimentIds'].each do |experiment_id|
206205
@experiment_feature_map[experiment_id] = [feature_flag['id']]
207206
end
207+
208+
# Feature Rollout support: inject the "everyone else" variation
209+
# into any experiment with type == "feature_rollout"
210+
everyone_else_variation = get_everyone_else_variation(feature_flag)
211+
next if everyone_else_variation.nil?
212+
213+
feature_flag['experimentIds'].each do |exp_id|
214+
experiment = @experiment_id_map[exp_id]
215+
next unless experiment && experiment['type'] == Helpers::Constants::EXPERIMENT_TYPES['fr']
216+
217+
experiment['variations'].push(everyone_else_variation)
218+
experiment['trafficAllocation'].push(
219+
'entityId' => everyone_else_variation['id'],
220+
'endOfRange' => 10_000
221+
)
222+
@variation_key_map[experiment['key']][everyone_else_variation['key']] = everyone_else_variation
223+
@variation_id_map[experiment['key']][everyone_else_variation['id']] = everyone_else_variation
224+
@variation_id_map_by_experiment_id[exp_id][everyone_else_variation['id']] = everyone_else_variation
225+
@variation_key_map_by_experiment_id[exp_id][everyone_else_variation['key']] = everyone_else_variation
226+
variation_variables = everyone_else_variation['variables']
227+
@variation_id_to_variable_usage_map[everyone_else_variation['id']] = generate_key_map(variation_variables, 'id') if variation_variables
228+
end
208229
end
209230

231+
# Generate flag_variation_map after injection so it includes everyone-else variations
232+
@flag_variation_map = generate_feature_variation_map(@feature_flags)
233+
210234
# Adding Holdout variations in variation id and key maps
211235
return unless @holdouts && !@holdouts.empty?
212236

@@ -690,6 +714,38 @@ def get_holdout(holdout_id)
690714

691715
private
692716

717+
def get_everyone_else_variation(feature_flag)
718+
# Get the "everyone else" variation for a feature flag.
719+
#
720+
# The "everyone else" rule is the last experiment in the flag's rollout,
721+
# and its first variation is the "everyone else" variation.
722+
#
723+
# feature_flag - Feature flag hash
724+
#
725+
# Returns the "everyone else" variation hash, or nil if not available.
726+
727+
rollout_id = feature_flag['rolloutId']
728+
return nil if rollout_id.nil? || rollout_id.empty?
729+
730+
rollout = @rollout_id_map[rollout_id]
731+
return nil if rollout.nil?
732+
733+
experiments = rollout['experiments']
734+
return nil if experiments.nil? || experiments.empty?
735+
736+
everyone_else_rule = experiments.last
737+
variations = everyone_else_rule['variations']
738+
return nil if variations.nil? || variations.empty?
739+
740+
variation = variations.first
741+
{
742+
'id' => variation['id'],
743+
'key' => variation['key'],
744+
'featureEnabled' => variation['featureEnabled'] == true,
745+
'variables' => variation.fetch('variables', [])
746+
}
747+
end
748+
693749
def generate_feature_variation_map(feature_flags)
694750
flag_variation_map = {}
695751
feature_flags.each do |flag|

lib/optimizely/helpers/constants.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@
1818
module Optimizely
1919
module Helpers
2020
module Constants
21+
EXPERIMENT_TYPES = {
22+
'ab' => 'ab',
23+
'mab' => 'mab',
24+
'cmab' => 'cmab',
25+
'td' => 'td',
26+
'fr' => 'fr'
27+
}.freeze
28+
2129
JSON_SCHEMA_V2 = {
2230
'type' => 'object',
2331
'properties' => {
@@ -205,6 +213,10 @@ module Constants
205213
'cmab' => {
206214
'type' => 'object'
207215
},
216+
'type' => {
217+
'type' => %w[string null],
218+
'enum' => EXPERIMENT_TYPES.values + [nil]
219+
},
208220
'holdouts' => {
209221
'type' => 'array'
210222
}

0 commit comments

Comments
 (0)