22
33require 'semantic'
44require 'securerandom'
5+ require 'set'
56
67require_relative 'environments/models'
78require_relative 'features/models'
89require_relative 'features/constants'
910require_relative 'identities/models'
11+ require_relative 'lazy_flags'
1012require_relative 'organisations/models'
1113require_relative 'projects/models'
1214require_relative 'segments/evaluator'
1618
1719module Flagsmith
1820 # Core evaluation logic for feature flags
19- module Engine
21+ module Engine # rubocop:disable Metrics/ModuleLength
2022 extend self
2123 include Flagsmith ::Engine ::Utils ::HashFunc
2224 include Flagsmith ::Engine ::Features ::TargetingReasons
@@ -27,37 +29,42 @@ module Engine
2729 # @param evaluation_context [Hash] The evaluation context
2830 # @return [Hash] Evaluation result with flags and segments
2931 def get_evaluation_result ( evaluation_context )
30- evaluation_context = get_enriched_context ( evaluation_context )
31- segments , segment_overrides = evaluate_segments ( evaluation_context )
32+ lazy_flags = LazyFlags . new ( get_enriched_context ( evaluation_context ) )
33+ evaluation_context = lazy_flags . context
34+ segments , segment_overrides = evaluate_segments ( evaluation_context , lazy_flags )
3235 flags = evaluate_features ( evaluation_context , segment_overrides )
36+ lazy_flags . apply_to ( flags ) if lazy_flags . used?
3337 {
3438 flags : flags ,
3539 segments : segments
3640 }
3741 end
3842
3943 # Returns { segments: EvaluationResultSegments; segmentOverrides: Record<string, SegmentOverride>; }
40- def evaluate_segments ( evaluation_context )
44+ def evaluate_segments ( evaluation_context , lazy_flags )
4145 return [ ] , { } if evaluation_context [ :segments ] . nil?
4246
43- identity_segments = get_segments_from_context ( evaluation_context )
47+ matching_segments = evaluation_context [ :segments ] . each_value . select do |segment |
48+ cycle_hits = lazy_flags . cycle_hits
49+ context_in_segment? ( evaluation_context , segment ) && lazy_flags . cycle_hits == cycle_hits
50+ end
4451
45- segments = identity_segments . map do |segment |
52+ segments = matching_segments . map do |segment |
4653 { name : segment [ :name ] , metadata : segment [ :metadata ] } . compact
4754 end
4855
49- segment_overrides = process_segment_overrides ( identity_segments )
56+ segment_overrides = process_segment_overrides ( matching_segments )
5057
5158 [ segments , segment_overrides ]
5259 end
5360
5461 # Returns Record<string: override.name, SegmentOverride>
55- def process_segment_overrides ( identity_segments ) # rubocop:disable Metrics/MethodLength
62+ def process_segment_overrides ( matching_segments ) # rubocop:disable Metrics/MethodLength
5663 segment_overrides = { }
5764
58- identity_segments . each do |segment |
65+ matching_segments . each do |segment |
5966 Array ( segment [ :overrides ] ) . each do |override |
60- next unless should_apply_override ( override , segment_overrides )
67+ next unless wins_over? ( override , segment_overrides [ override [ :name ] ] &. dig ( :feature ) )
6168
6269 segment_overrides [ override [ :name ] ] = {
6370 feature : override ,
@@ -71,65 +78,94 @@ def process_segment_overrides(identity_segments) # rubocop:disable Metrics/Metho
7178
7279 def evaluate_features ( evaluation_context , segment_overrides )
7380 identity_key = get_identity_key ( evaluation_context )
81+ targeted_feature_names = get_targeted_feature_names ( evaluation_context )
7482
7583 ( evaluation_context [ :features ] || { } ) . each_with_object ( { } ) do |( _ , feature ) , flags |
76- segment_override = segment_overrides [ feature [ :name ] ]
77- final_feature = segment_override ? segment_override [ :feature ] : feature
84+ flags [ feature [ :name ] . to_sym ] =
85+ get_feature_flag_result ( feature , segment_overrides [ feature [ :name ] ] , identity_key , targeted_feature_names )
86+ end
87+ end
88+
89+ # Returns the flag result of a feature, served by its segment override if any, or else by its environment default
90+ def get_feature_flag_result ( feature , segment_override , identity_key , targeted_feature_names )
91+ return get_flag_result ( segment_override [ :feature ] , identity_key , targeting_match_reason ( segment_override ) ) if segment_override
92+
93+ get_flag_result ( feature , identity_key , fallback_reason ( feature [ :name ] , targeted_feature_names ) )
94+ end
7895
79- flag_result = build_flag_result ( final_feature , identity_key , segment_override )
80- flags [ final_feature [ :name ] . to_sym ] = flag_result
96+ # Returns the names of the features overridden by at least one segment
97+ def get_targeted_feature_names ( evaluation_context )
98+ names = { }
99+ evaluation_context [ :segments ] &.each_value do |segment |
100+ segment [ :overrides ] &.each { |override | names [ override [ :name ] ] = true }
81101 end
102+ names
103+ end
104+
105+ # Returns the reason for a flag served by a segment override
106+ def targeting_match_reason ( segment_override )
107+ "#{ TARGETING_REASON_TARGETING_MATCH } ; segment=#{ segment_override [ :segment_name ] } "
108+ end
109+
110+ # Returns the reason for a flag served by its environment default:
111+ # DEFAULT if the feature has segment overrides that did not match, STATIC if it has none
112+ def fallback_reason ( feature_name , targeted_feature_names )
113+ targeted_feature_names . key? ( feature_name ) ? TARGETING_REASON_DEFAULT : TARGETING_REASON_STATIC
82114 end
83115
84- # Returns {value: any; reason?: string}
116+ # Returns {name: string; enabled: boolean; value: any; reason: string; variant?: string; metadata?: any}
117+ def get_flag_result ( feature , identity_key , reason )
118+ evaluated = evaluate_feature_value ( feature , identity_key )
119+
120+ flag_result = {
121+ name : feature [ :name ] ,
122+ enabled : feature [ :enabled ] ,
123+ value : evaluated [ :value ] ,
124+ reason : evaluated [ :reason ] || reason ,
125+ variant : evaluated [ :variant ]
126+ }
127+
128+ flag_result [ :metadata ] = feature [ :metadata ] if feature [ :metadata ]
129+ flag_result
130+ end
131+
132+ # Returns {value: any; reason?: string; variant?: string}
85133 def evaluate_feature_value ( feature , identity_key = nil )
86134 return get_multivariate_feature_value ( feature , identity_key ) if feature [ :variants ] &.any? && identity_key
87135
88- { value : feature [ :value ] , reason : nil }
136+ { value : feature [ :value ] , reason : nil , variant : nil }
89137 end
90138
91- # Returns {value: any; reason?: string}
139+ # Returns {value: any; reason?: string; variant: string }
92140 def get_multivariate_feature_value ( feature , identity_key )
93141 percentage_value = hashed_percentage_for_object_ids ( [ feature [ :key ] , identity_key ] )
94- sorted_variants = ( feature [ :variants ] || [ ] ) . sort_by { |v | v [ :priority ] || WEAKEST_PRIORITY }
142+ sorted_variants = ( feature [ :variants ] || [ ] ) . sort_by { |v | v [ :priority ] || DEFAULT_PRIORITY }
95143
96144 variant = find_matching_variant ( sorted_variants , percentage_value )
97- variant || { value : feature [ :value ] , reason : nil }
145+ variant || { value : feature [ :value ] , reason : nil , variant : 'control' }
98146 end
99147
100148 def find_matching_variant ( sorted_variants , percentage_value )
101149 start_percentage = 0
102150 sorted_variants . each do |variant |
103151 limit = start_percentage + variant [ :weight ]
104- return { value : variant [ :value ] , reason : "#{ TARGETING_REASON_SPLIT } ; weight=#{ variant [ :weight ] } " } if start_percentage <= percentage_value && percentage_value < limit
152+ if start_percentage <= percentage_value && percentage_value < limit
153+ return { value : variant [ :value ] , reason : "#{ TARGETING_REASON_SPLIT } ; weight=#{ variant [ :weight ] } " , variant : variant [ :key ] }
154+ end
105155
106156 start_percentage = limit
107157 end
108158 nil
109159 end
110160
111- # returns boolean
112- def should_apply_override ( override , existing_overrides )
113- current_override = existing_overrides [ override [ :name ] ]
114- ! current_override || stronger_priority? ( override [ :priority ] , current_override [ :feature ] [ : priority] )
161+ # Whether a segment override takes precedence over the one held so far.
162+ # The lowest priority wins, and the first seen wins a tie.
163+ def wins_over? ( candidate , incumbent )
164+ incumbent . nil? || ( candidate [ :priority ] || DEFAULT_PRIORITY ) < ( incumbent [ : priority] || DEFAULT_PRIORITY )
115165 end
116166
117167 private
118168
119- def build_flag_result ( feature , identity_key , segment_override )
120- evaluated = evaluate_feature_value ( feature , identity_key )
121-
122- flag_result = {
123- name : feature [ :name ] ,
124- enabled : feature [ :enabled ] ,
125- value : evaluated [ :value ] ,
126- reason : evaluated [ :reason ] || ( segment_override ? "#{ TARGETING_REASON_TARGETING_MATCH } ; segment=#{ segment_override [ :segment_name ] } " : TARGETING_REASON_DEFAULT )
127- }
128-
129- flag_result [ :metadata ] = feature [ :metadata ] if feature [ :metadata ]
130- flag_result
131- end
132-
133169 # Extract identity key from evaluation context
134170 #
135171 # @param evaluation_context [Hash] The evaluation context
@@ -139,9 +175,5 @@ def get_identity_key(evaluation_context)
139175
140176 evaluation_context [ :identity ] [ :key ]
141177 end
142-
143- def stronger_priority? ( priority_a , priority_b )
144- ( priority_a || WEAKEST_PRIORITY ) < ( priority_b || WEAKEST_PRIORITY )
145- end
146178 end
147179end
0 commit comments