Skip to content

Commit f1026a2

Browse files
Zaimwa9gagantrivedikhvn26
authored
feat: get evaluation get result (#88)
* feat: added-engine-function-signatures * feat: moved-engine-to-core * feat: implemented-process-segment-overrides * feat: implemented-evalute-segments-partially * feat: implemented-should-apply-override * feat: implemented-get-identity-segments * feat: implemented-new-in-and-fixed-remaining-tests * feat: run-lint * feat: misc * feat: json-path-lib-implementation * remove dup * feat: made-legacy-functions-public * feat: updated-tests-to-match-engine-in-operator-accepting-numbers * feat: engine-agnostic-to-empty-identity-in-segment-evaluation * feat: renamed-to-is-higher-priority * feat: renamed-get-identity-segments-func * feat: reverted-to-is-primitive * feat: use-weakest-priority-constant * feat: upgraded-engine-test-data-and-fixed-mv-evaluation-bug * feat: removed-targeting-reason-func * feat: linter-rubocop-autocorrect * feat: linter * feat: linter * feat: moved-mappers-to-engine-namespace * feat: enrich-context-with-identity-key * feat: run-ci-on-all-branches * feat: removed-comments * feat!: sdk consumes context engine (#89) * fix: sdk-uses-new-engine-methods * feat: introduced-jsonpath-library * feat: fixed-conflict * Update lib/flagsmith/engine/segments/models.rb Co-authored-by: Kim Gustyr <kim.gustyr@flagsmith.com> * Update lib/flagsmith/engine/segments/models.rb * feat: removed-normalize * feat: linter * feat: replaced-flagsmith-id-with-id * feat: removed-comments --------- Co-authored-by: Kim Gustyr <kim.gustyr@flagsmith.com> --------- Co-authored-by: Gagan Trivedi <gagandeeptrivedi47@gmail.com> Co-authored-by: Kim Gustyr <kim.gustyr@flagsmith.com>
1 parent 4b106a7 commit f1026a2

File tree

19 files changed

+543
-279
lines changed

19 files changed

+543
-279
lines changed

.github/workflows/pull_request.yaml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,6 @@ on:
1414
- synchronize
1515
- reopened
1616
- ready_for_review
17-
branches:
18-
- main
19-
- release/**
2017

2118
push:
2219
branches:

Gemfile.lock

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ PATH
44
flagsmith (4.3.0)
55
faraday (>= 2.0.1)
66
faraday-retry
7+
jsonpath (~> 1.1)
78
semantic
89

910
GEM
@@ -21,8 +22,11 @@ GEM
2122
faraday (~> 2.0)
2223
gem-release (2.2.2)
2324
json (2.7.1)
25+
jsonpath (1.1.5)
26+
multi_json
2427
language_server-protocol (3.17.0.3)
2528
method_source (1.0.0)
29+
multi_json (1.17.0)
2630
net-http (0.4.1)
2731
uri
2832
parallel (1.24.0)

flagsmith.gemspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ Gem::Specification.new do |spec|
3434

3535
spec.add_dependency 'faraday', '>= 2.0.1'
3636
spec.add_dependency 'faraday-retry'
37+
spec.add_dependency 'jsonpath', '~> 1.1'
3738
spec.add_dependency 'semantic'
3839
spec.metadata['rubygems_mfa_required'] = 'true'
3940
end

lib/flagsmith.rb

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,6 @@ def initialize(config)
6969
api_client
7070
analytics_processor
7171
environment_data_polling_manager
72-
engine
7372
load_offline_handler
7473
end
7574

@@ -99,10 +98,6 @@ def realtime_client
9998
@realtime_client ||= Flagsmith::RealtimeClient.new(@config)
10099
end
101100

102-
def engine
103-
@engine ||= Flagsmith::Engine::Engine.new
104-
end
105-
106101
def analytics_processor
107102
return nil unless @config.enable_analytics?
108103

@@ -211,21 +206,33 @@ def get_value_for_identity(feature_name, user_id = nil, default: nil)
211206
end
212207

213208
def get_identity_segments(identifier, traits = {})
214-
unless environment
215-
raise Flagsmith::ClientError,
216-
'Local evaluation or offline handler is required to obtain identity segments.'
217-
end
209+
raise Flagsmith::ClientError, 'Local evaluation or offline handler is required to obtain identity segments.' unless environment
218210

219211
identity_model = get_identity_model(identifier, traits)
220-
segment_models = engine.get_identity_segments(environment, identity_model)
221-
segment_models.map { |sm| Flagsmith::Segments::Segment.new(id: sm.id, name: sm.name) }.compact
212+
context = Flagsmith::Engine::Mappers.get_evaluation_context(environment, identity_model)
213+
raise Flagsmith::ClientError, 'Local evaluation required to obtain identity segments' unless context
214+
215+
evaluation_result = Flagsmith::Engine.get_evaluation_result(context)
216+
evaluation_result[:segments].filter_map do |segment_result|
217+
id = segment_result.dig(:metadata, :id)
218+
Flagsmith::Segments::Segment.new(id: id, name: segment_result[:name]) if id
219+
end
222220
end
223221

224222
private
225223

226-
def environment_flags_from_document
227-
Flagsmith::Flags::Collection.from_feature_state_models(
228-
engine.get_environment_feature_states(environment),
224+
def environment_flags_from_document # rubocop:disable Metrics/MethodLength
225+
context = Flagsmith::Engine::Mappers.get_evaluation_context(environment)
226+
227+
unless context
228+
raise Flagsmith::ClientError,
229+
'Unable to get flags. No environment present.'
230+
end
231+
232+
evaluation_result = Flagsmith::Engine.get_evaluation_result(context)
233+
234+
Flagsmith::Flags::Collection.from_evaluation_result(
235+
evaluation_result,
229236
analytics_processor: analytics_processor,
230237
default_flag_handler: default_flag_handler,
231238
offline_handler: offline_handler
@@ -234,12 +241,13 @@ def environment_flags_from_document
234241

235242
def get_identity_flags_from_document(identifier, traits = {})
236243
identity_model = get_identity_model(identifier, traits)
244+
context = Flagsmith::Engine::Mappers.get_evaluation_context(environment, identity_model)
245+
raise Flagsmith::ClientError, 'Unable to get flags. No environment present.' unless context
237246

238-
Flagsmith::Flags::Collection.from_feature_state_models(
239-
engine.get_identity_feature_states(environment, identity_model),
240-
identity_id: identity_model.composite_key,
241-
analytics_processor: analytics_processor,
242-
default_flag_handler: default_flag_handler,
247+
evaluation_result = Flagsmith::Engine.get_evaluation_result(context)
248+
Flagsmith::Flags::Collection.from_evaluation_result(
249+
evaluation_result,
250+
analytics_processor: analytics_processor, default_flag_handler: default_flag_handler,
243251
offline_handler: offline_handler
244252
)
245253
end

lib/flagsmith/engine/core.rb

Lines changed: 106 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -5,86 +5,143 @@
55

66
require_relative 'environments/models'
77
require_relative 'features/models'
8+
require_relative 'features/constants'
89
require_relative 'identities/models'
910
require_relative 'organisations/models'
1011
require_relative 'projects/models'
1112
require_relative 'segments/evaluator'
1213
require_relative 'segments/models'
1314
require_relative 'utils/hash_func'
1415
require_relative 'mappers'
15-
require_relative 'evaluation/core'
1616

1717
module Flagsmith
18+
# Core evaluation logic for feature flags
1819
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
2538

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?
2742

28-
raise Flagsmith::FeatureStateNotFound, 'Feature State Not Found' if feature_state.nil?
43+
identity_segments = get_segments_from_context(evaluation_context)
2944

30-
feature_state
45+
segments = identity_segments.map do |segment|
46+
{ name: segment[:name], metadata: segment[:metadata] }.compact
3147
end
3248

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
3553

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 = {}
3757

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
3967
end
4068

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
4371

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)
4574

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
4781
end
82+
end
4883

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
5187

52-
environment.feature_states
53-
end
88+
{ value: feature[:value], reason: nil }
89+
end
5490

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 }
5695

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
6299

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
66105

67-
# Override with any feature states defined directly the identity
68-
identity.identity_features.each(&override)
69-
feature_states
106+
start_percentage = limit
70107
end
108+
nil
109+
end
71110

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
81116

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)
88145
end
89146
end
90147
end

0 commit comments

Comments
 (0)