Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ PATH
flagsmith (4.3.0)
faraday (>= 2.0.1)
faraday-retry
jsonpath (~> 1.1)
semantic

GEM
Expand All @@ -21,8 +22,11 @@ GEM
faraday (~> 2.0)
gem-release (2.2.2)
json (2.7.1)
jsonpath (1.1.5)
multi_json
language_server-protocol (3.17.0.3)
method_source (1.0.0)
multi_json (1.17.0)
net-http (0.4.1)
uri
parallel (1.24.0)
Expand Down
1 change: 1 addition & 0 deletions flagsmith.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Gem::Specification.new do |spec|

spec.add_dependency 'faraday', '>= 2.0.1'
spec.add_dependency 'faraday-retry'
spec.add_dependency 'jsonpath', '~> 1.1'
spec.add_dependency 'semantic'
spec.metadata['rubygems_mfa_required'] = 'true'
end
49 changes: 42 additions & 7 deletions lib/flagsmith.rb
Original file line number Diff line number Diff line change
Expand Up @@ -217,15 +217,40 @@ def get_identity_segments(identifier, traits = {})
end

identity_model = get_identity_model(identifier, traits)
segment_models = engine.get_identity_segments(environment, identity_model)
segment_models.map { |sm| Flagsmith::Segments::Segment.new(id: sm.id, name: sm.name) }.compact

context = Flagsmith::Engine::Evaluation::Mappers.get_evaluation_context(
environment, identity_model
)

unless context
raise Flagsmith::ClientError,
'Local evaluation required to obtain identity segments'
end

evaluation_result = Flagsmith::Engine::Evaluation::Core.get_evaluation_result(context)

evaluation_result[:segments].map do |segment_result|
flagsmith_id = segment_result.dig(:metadata, :flagsmith_id)
next unless flagsmith_id

Flagsmith::Segments::Segment.new(id: flagsmith_id, name: segment_result[:name])
end.compact
end

private

def environment_flags_from_document
Flagsmith::Flags::Collection.from_feature_state_models(
engine.get_environment_feature_states(environment),
context = Flagsmith::Engine::Evaluation::Mappers.get_evaluation_context(environment)

unless context
raise Flagsmith::ClientError,
'Unable to get flags. No environment present.'
end

evaluation_result = Flagsmith::Engine::Evaluation::Core.get_evaluation_result(context)

Flagsmith::Flags::Collection.from_evaluation_result(
evaluation_result,
analytics_processor: analytics_processor,
default_flag_handler: default_flag_handler,
offline_handler: offline_handler
Expand All @@ -235,9 +260,19 @@ def environment_flags_from_document
def get_identity_flags_from_document(identifier, traits = {})
identity_model = get_identity_model(identifier, traits)

Flagsmith::Flags::Collection.from_feature_state_models(
engine.get_identity_feature_states(environment, identity_model),
identity_id: identity_model.composite_key,
context = Flagsmith::Engine::Evaluation::Mappers.get_evaluation_context(
environment, identity_model
)

unless context
raise Flagsmith::ClientError,
'Unable to get flags. No environment present.'
end

evaluation_result = Flagsmith::Engine::Evaluation::Core.get_evaluation_result(context)

Flagsmith::Flags::Collection.from_evaluation_result(
evaluation_result,
analytics_processor: analytics_processor,
default_flag_handler: default_flag_handler,
offline_handler: offline_handler
Expand Down
69 changes: 3 additions & 66 deletions lib/flagsmith/engine/core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,74 +17,11 @@
module Flagsmith
module Engine
# Flags engine methods
# NOTE: This class is kept for backwards compatibility but no longer contains
# the old model-based evaluation methods. Use the context-based evaluation
# via Flagsmith::Engine::Evaluation::Core.get_evaluation_result instead.
Comment thread
Zaimwa9 marked this conversation as resolved.
Outdated
class Engine
include Flagsmith::Engine::Segments::Evaluator

def get_identity_feature_state(environment, identity, feature_name, override_traits = nil)
feature_states = get_identity_feature_states_dict(environment, identity, override_traits).values

feature_state = feature_states.find { |f| f.feature.name == feature_name }

raise Flagsmith::FeatureStateNotFound, 'Feature State Not Found' if feature_state.nil?

feature_state
end

def get_identity_feature_states(environment, identity, override_traits = nil)
feature_states = get_identity_feature_states_dict(environment, identity, override_traits).values

return feature_states.select(&:enabled?) if environment.project.hide_disabled_flags

feature_states
end

def get_environment_feature_state(environment, feature_name)
features_state = environment.feature_states.find { |f| f.feature.name == feature_name }

raise Flagsmith::FeatureStateNotFound, 'Feature State Not Found' if features_state.nil?

features_state
end

def get_environment_feature_states(environment)
return environment.feature_states.select(&:enabled?) if environment.project.hide_disabled_flags

environment.feature_states
end

private

def get_identity_feature_states_dict(environment, identity, override_traits = nil)
# Get feature states from the environment
feature_states = {}
override = ->(fs) { feature_states[fs.feature.id] = fs }
environment.feature_states.each(&override)

override_by_matching_segments(environment, identity, override_traits) do |fs|
override.call(fs) unless higher_segment_priority?(feature_states, fs)
end

# Override with any feature states defined directly the identity
identity.identity_features.each(&override)
feature_states
end

# Override with any feature states defined by matching segments
def override_by_matching_segments(environment, identity, override_traits)
identity_segments = get_identity_segments(environment, identity, override_traits)
identity_segments.each do |matching_segment|
matching_segment.feature_states.each do |feature_state|
yield feature_state if block_given?
end
end
end

def higher_segment_priority?(collection, feature_state)
collection.key?(feature_state.feature.id) &&
collection[feature_state.feature.id].higher_segment_priority?(
feature_state
)
end
end
end
end
6 changes: 3 additions & 3 deletions lib/flagsmith/engine/evaluation/core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def get_evaluation_result(evaluation_context)
def evaluate_segments(evaluation_context)
return [], {} if evaluation_context[:identity].nil? || evaluation_context[:segments].nil?

identity_segments = get_identity_segments_from_context(evaluation_context)
identity_segments = get_identity_segments(evaluation_context)

segments = identity_segments.map do |segment|
result = {
Expand Down Expand Up @@ -116,7 +116,7 @@ def evaluate_feature_value(feature, identity_key = nil)
# Returns {value: any; reason?: string}
def get_multivariate_feature_value(feature, identity_key)
percentage_value = hashed_percentage_for_object_ids([feature[:key], identity_key])
sorted_variants = (feature[:variants] || []).sort_by { |v| v[:priority] || Float::INFINITY }
sorted_variants = (feature[:variants] || []).sort_by { |v| v[:priority] || WEAKEST_PRIORITY }

start_percentage = 0
sorted_variants.each do |variant|
Expand Down Expand Up @@ -154,7 +154,7 @@ def get_identity_key(evaluation_context)

# returns boolean
def higher_priority?(priority_a, priority_b)
(priority_a || Float::INFINITY) < (priority_b || Float::INFINITY)
(priority_a || WEAKEST_PRIORITY) < (priority_b || WEAKEST_PRIORITY)
end

def get_targeting_match_reason(match_object)
Expand Down
2 changes: 1 addition & 1 deletion lib/flagsmith/engine/features/models.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def multivariate_value(identity_id)
# but `self` does.
# 2. `other` have a feature segment with high priority
def higher_segment_priority?(other)
feature_segment.priority.to_i < (other&.feature_segment&.priority || Float::INFINITY)
feature_segment.priority.to_i < (other&.feature_segment&.priority || WEAKEST_PRIORITY)
rescue TypeError, NoMethodError
false
end
Expand Down
92 changes: 7 additions & 85 deletions lib/flagsmith/engine/segments/evaluator.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require 'json'
require 'jsonpath'
Comment thread
Zaimwa9 marked this conversation as resolved.
require_relative 'constants'
require_relative 'models'
require_relative '../utils/hash_func'
Expand All @@ -18,7 +20,7 @@ module Evaluator
#
# @param context [Hash] Evaluation context containing identity and segment definitions
# @return [Array<Hash>] Array of segments that the identity matches
def get_identity_segments_from_context(context)
def get_identity_segments(context)
return [] unless context[:identity] && context[:segments]

context[:segments].values.select do |segment|
Expand All @@ -29,67 +31,7 @@ def get_identity_segments_from_context(context)
end
end

# Model-based segment evaluation (existing approach)
def get_identity_segments(environment, identity, override_traits = nil)
environment.project.segments.select do |s|
evaluate_identity_in_segment(identity, s, override_traits)
end
end

# Evaluates whether a given identity is in the provided segment.
#
# :param identity: identity model object to evaluate
# :param segment: segment model object to evaluate
# :param override_traits: pass in a list of traits to use instead of those on the
# identity model itself
# :return: True if the identity is in the segment, False otherwise
def evaluate_identity_in_segment(identity, segment, override_traits = nil)
segment.rules&.length&.positive? &&
segment.rules.all? do |rule|
traits_match_segment_rule(
override_traits || identity.identity_traits,
rule,
segment.id,
identity.django_id || identity.composite_key
)
end
end

# rubocop:disable Metrics/MethodLength
def traits_match_segment_rule(identity_traits, rule, segment_id, identity_id)
matching_block = lambda { |condition|
traits_match_segment_condition(identity_traits, condition, segment_id, identity_id)
}

matches_conditions =
if rule.conditions&.length&.positive?
rule.conditions.send(rule.matching_function, &matching_block)
else
true
end

matches_conditions &&
rule.rules.all? { |r| traits_match_segment_rule(identity_traits, r, segment_id, identity_id) }
end
# rubocop:enable Metrics/MethodLength

def traits_match_segment_condition(identity_traits, condition, segment_id, identity_id)
if condition.operator == PERCENTAGE_SPLIT
return hashed_percentage_for_object_ids([segment_id,
identity_id]) <= condition.value.to_f
end

trait = identity_traits.find { |t| t.key.to_s == condition.property }

return handle_trait_existence_conditions(trait, condition.operator) if [IS_SET,
IS_NOT_SET].include?(condition.operator)

return condition.match_trait_value?(trait.trait_value) if trait

false
end

# Context-based helper functions (new approach)
# Context-based helper functions

# Evaluates whether a segment rule matches using context
#
Expand Down Expand Up @@ -148,9 +90,7 @@ def traits_match_segment_condition_from_context(condition, segment_key, context)
end

return false if condition[:property].nil?

trait_value = get_trait_value(condition[:property], context)

return !trait_value.nil? if condition[:operator] == IS_SET
return trait_value.nil? if condition[:operator] == IS_NOT_SET

Expand Down Expand Up @@ -200,25 +140,15 @@ def get_trait_value(property, context)
traits[property] || traits[property.to_sym]
end

# Get value from context using JSONPath-like syntax
# Get value from context using JSONPath syntax
#
# @param json_path [String] JSONPath expression (e.g., '$.identity.identifier')
# @param context [Hash] The evaluation context
# @return [Object, nil] The value at the path or nil
def get_context_value(json_path, context)
return nil unless context && json_path&.start_with?('$.')

# Simple JSONPath implementation - handle basic cases
path_parts = json_path.sub('$.', '').split('.')
current = context

path_parts.each do |part|
return nil unless current.is_a?(Hash)

current = current[part.to_sym]
end

current
results = JsonPath.new(json_path, use_symbols: true).on(context)
results.first
rescue StandardError
nil
end
Expand All @@ -243,14 +173,6 @@ def non_primitive?(value)

value.is_a?(Hash) || value.is_a?(Array)
end

private

def handle_trait_existence_conditions(matching_trait, operator)
return operator == IS_NOT_SET if matching_trait.nil?

operator == IS_SET
end
end
end
end
Expand Down
7 changes: 6 additions & 1 deletion lib/flagsmith/engine/segments/models.rb
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,12 @@ def match_modulo_value(trait_value)
def match_in_value(trait_value)
return false if trait_value.nil? || trait_value.is_a?(TrueClass) || trait_value.is_a?(FalseClass)

return @value.include?(trait_value.to_s) if @value.is_a?(Array)
(segments/models_spec.rb)
Comment thread
Zaimwa9 marked this conversation as resolved.
Outdated
return false unless ![true, false].include? trait_value

if @value.is_a?(Array)
return @value.include?(trait_value.to_s)
end

if @value.is_a?(String)
begin
Expand Down
Loading