|
5 | 5 |
|
6 | 6 | require_relative 'environments/models' |
7 | 7 | require_relative 'features/models' |
| 8 | +require_relative 'features/constants' |
8 | 9 | require_relative 'identities/models' |
9 | 10 | require_relative 'organisations/models' |
10 | 11 | require_relative 'projects/models' |
11 | 12 | require_relative 'segments/evaluator' |
12 | 13 | require_relative 'segments/models' |
13 | 14 | require_relative 'utils/hash_func' |
14 | 15 | require_relative 'mappers' |
15 | | -require_relative 'evaluation/core' |
16 | 16 |
|
17 | 17 | module Flagsmith |
| 18 | + # Core evaluation logic for feature flags |
18 | 19 | module Engine |
19 | | - # Flags engine methods |
20 | | - class Engine |
21 | | - include Flagsmith::Engine::Segments::Evaluator |
22 | | - |
23 | | - def get_identity_feature_state(environment, identity, feature_name, override_traits = nil) |
24 | | - feature_states = get_identity_feature_states_dict(environment, identity, override_traits).values |
| 20 | + extend self |
| 21 | + include Flagsmith::Engine::Utils::HashFunc |
| 22 | + include Flagsmith::Engine::Features::TargetingReasons |
| 23 | + include Flagsmith::Engine::Segments::Evaluator |
| 24 | + |
| 25 | + # Get evaluation result from evaluation context |
| 26 | + # |
| 27 | + # @param evaluation_context [Hash] The evaluation context |
| 28 | + # @return [Hash] Evaluation result with flags and segments |
| 29 | + def get_evaluation_result(evaluation_context) |
| 30 | + evaluation_context = get_enriched_context(evaluation_context) |
| 31 | + segments, segment_overrides = evaluate_segments(evaluation_context) |
| 32 | + flags = evaluate_features(evaluation_context, segment_overrides) |
| 33 | + { |
| 34 | + flags: flags, |
| 35 | + segments: segments |
| 36 | + } |
| 37 | + end |
25 | 38 |
|
26 | | - feature_state = feature_states.find { |f| f.feature.name == feature_name } |
| 39 | + # Returns { segments: EvaluationResultSegments; segmentOverrides: Record<string, SegmentOverride>; } |
| 40 | + def evaluate_segments(evaluation_context) |
| 41 | + return [], {} if evaluation_context[:segments].nil? |
27 | 42 |
|
28 | | - raise Flagsmith::FeatureStateNotFound, 'Feature State Not Found' if feature_state.nil? |
| 43 | + identity_segments = get_segments_from_context(evaluation_context) |
29 | 44 |
|
30 | | - feature_state |
| 45 | + segments = identity_segments.map do |segment| |
| 46 | + { name: segment[:name], metadata: segment[:metadata] }.compact |
31 | 47 | end |
32 | 48 |
|
33 | | - def get_identity_feature_states(environment, identity, override_traits = nil) |
34 | | - feature_states = get_identity_feature_states_dict(environment, identity, override_traits).values |
| 49 | + segment_overrides = process_segment_overrides(identity_segments) |
| 50 | + |
| 51 | + [segments, segment_overrides] |
| 52 | + end |
35 | 53 |
|
36 | | - return feature_states.select(&:enabled?) if environment.project.hide_disabled_flags |
| 54 | + # Returns Record<string: override.name, SegmentOverride> |
| 55 | + def process_segment_overrides(identity_segments) # rubocop:disable Metrics/MethodLength |
| 56 | + segment_overrides = {} |
37 | 57 |
|
38 | | - feature_states |
| 58 | + identity_segments.each do |segment| |
| 59 | + Array(segment[:overrides]).each do |override| |
| 60 | + next unless should_apply_override(override, segment_overrides) |
| 61 | + |
| 62 | + segment_overrides[override[:name]] = { |
| 63 | + feature: override, |
| 64 | + segment_name: segment[:name] |
| 65 | + } |
| 66 | + end |
39 | 67 | end |
40 | 68 |
|
41 | | - def get_environment_feature_state(environment, feature_name) |
42 | | - features_state = environment.feature_states.find { |f| f.feature.name == feature_name } |
| 69 | + segment_overrides |
| 70 | + end |
43 | 71 |
|
44 | | - raise Flagsmith::FeatureStateNotFound, 'Feature State Not Found' if features_state.nil? |
| 72 | + def evaluate_features(evaluation_context, segment_overrides) |
| 73 | + identity_key = get_identity_key(evaluation_context) |
45 | 74 |
|
46 | | - features_state |
| 75 | + (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 |
| 78 | + |
| 79 | + flag_result = build_flag_result(final_feature, identity_key, segment_override) |
| 80 | + flags[final_feature[:name].to_sym] = flag_result |
47 | 81 | end |
| 82 | + end |
48 | 83 |
|
49 | | - def get_environment_feature_states(environment) |
50 | | - return environment.feature_states.select(&:enabled?) if environment.project.hide_disabled_flags |
| 84 | + # Returns {value: any; reason?: string} |
| 85 | + def evaluate_feature_value(feature, identity_key = nil) |
| 86 | + return get_multivariate_feature_value(feature, identity_key) if feature[:variants]&.any? && identity_key |
51 | 87 |
|
52 | | - environment.feature_states |
53 | | - end |
| 88 | + { value: feature[:value], reason: nil } |
| 89 | + end |
54 | 90 |
|
55 | | - private |
| 91 | + # Returns {value: any; reason?: string} |
| 92 | + def get_multivariate_feature_value(feature, identity_key) |
| 93 | + percentage_value = hashed_percentage_for_object_ids([feature[:key], identity_key]) |
| 94 | + sorted_variants = (feature[:variants] || []).sort_by { |v| v[:priority] || WEAKEST_PRIORITY } |
56 | 95 |
|
57 | | - def get_identity_feature_states_dict(environment, identity, override_traits = nil) |
58 | | - # Get feature states from the environment |
59 | | - feature_states = {} |
60 | | - override = ->(fs) { feature_states[fs.feature.id] = fs } |
61 | | - environment.feature_states.each(&override) |
| 96 | + variant = find_matching_variant(sorted_variants, percentage_value) |
| 97 | + variant || { value: feature[:value], reason: nil } |
| 98 | + end |
62 | 99 |
|
63 | | - override_by_matching_segments(environment, identity, override_traits) do |fs| |
64 | | - override.call(fs) unless higher_segment_priority?(feature_states, fs) |
65 | | - end |
| 100 | + def find_matching_variant(sorted_variants, percentage_value) |
| 101 | + start_percentage = 0 |
| 102 | + sorted_variants.each do |variant| |
| 103 | + 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 |
66 | 105 |
|
67 | | - # Override with any feature states defined directly the identity |
68 | | - identity.identity_features.each(&override) |
69 | | - feature_states |
| 106 | + start_percentage = limit |
70 | 107 | end |
| 108 | + nil |
| 109 | + end |
71 | 110 |
|
72 | | - # Override with any feature states defined by matching segments |
73 | | - def override_by_matching_segments(environment, identity, override_traits) |
74 | | - identity_segments = get_identity_segments(environment, identity, override_traits) |
75 | | - identity_segments.each do |matching_segment| |
76 | | - matching_segment.feature_states.each do |feature_state| |
77 | | - yield feature_state if block_given? |
78 | | - end |
79 | | - end |
80 | | - end |
| 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]) |
| 115 | + end |
81 | 116 |
|
82 | | - def higher_segment_priority?(collection, feature_state) |
83 | | - collection.key?(feature_state.feature.id) && |
84 | | - collection[feature_state.feature.id].higher_segment_priority?( |
85 | | - feature_state |
86 | | - ) |
87 | | - end |
| 117 | + private |
| 118 | + |
| 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 | + |
| 133 | + # Extract identity key from evaluation context |
| 134 | + # |
| 135 | + # @param evaluation_context [Hash] The evaluation context |
| 136 | + # @return [String, nil] The identity key or nil if no identity |
| 137 | + def get_identity_key(evaluation_context) |
| 138 | + return nil unless evaluation_context[:identity] |
| 139 | + |
| 140 | + evaluation_context[:identity][:key] |
| 141 | + end |
| 142 | + |
| 143 | + def stronger_priority?(priority_a, priority_b) |
| 144 | + (priority_a || WEAKEST_PRIORITY) < (priority_b || WEAKEST_PRIORITY) |
88 | 145 | end |
89 | 146 | end |
90 | 147 | end |
0 commit comments