Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
a67d321
fix: main-mappers-and-tests
Zaimwa9 Oct 23, 2025
dc19da6
fix: added-metadata
Zaimwa9 Oct 23, 2025
d536c81
fix: renaming
Zaimwa9 Oct 23, 2025
79b3176
fix: comments
Zaimwa9 Oct 23, 2025
45b926e
fix: remoevd-redundant-test
Zaimwa9 Oct 23, 2025
9ea952d
feat: reviewed-engine-test-todos
Zaimwa9 Oct 23, 2025
b9d14a8
Update lib/flagsmith/engine/evaluation/mappers.rb
Zaimwa9 Oct 24, 2025
cb3558b
feat: use-overrides-key-hash
Zaimwa9 Oct 24, 2025
58bfe8b
feat: removed-identifiers-join
Zaimwa9 Oct 24, 2025
3d7fa00
feat: flagsmith-id-in-snake-case
Zaimwa9 Oct 24, 2025
075a8a4
feat: run-lint
Zaimwa9 Oct 28, 2025
73b841c
feat: renamed-module-evaluation
Zaimwa9 Oct 28, 2025
51894ad
feat: fixed-priority-0-being-skipped
Zaimwa9 Oct 28, 2025
e1eabd2
feat: reverted-to-hash
Zaimwa9 Oct 30, 2025
0386836
feat: linter
Zaimwa9 Nov 10, 2025
d86ba90
feat: split-functions-and-module-for-linting
Zaimwa9 Nov 10, 2025
f8ea862
feat: get-rid-of-extra-map-nested-rule-function
Zaimwa9 Nov 10, 2025
b2c3d30
feat: renaming-identity-methods
Zaimwa9 Nov 10, 2025
7de46c2
feat: fixed-forgotten-func-renaming
Zaimwa9 Nov 10, 2025
10b256c
Update lib/flagsmith/engine/evaluation/mappers/environment.rb
Zaimwa9 Nov 10, 2025
6c8dc0b
feat: added-name-env-model-and-fixture
Zaimwa9 Nov 10, 2025
1a908fe
feat: moved-mappers-to-engine-namespace
Zaimwa9 Nov 10, 2025
0c9650a
feat: removed-feature-key
Zaimwa9 Nov 11, 2025
4b106a7
feat: linter
Zaimwa9 Nov 11, 2025
f1026a2
feat: get evaluation get result (#88)
Zaimwa9 Nov 12, 2025
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
3 changes: 0 additions & 3 deletions .github/workflows/pull_request.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@ on:
- synchronize
- reopened
- ready_for_review
branches:
- main
- release/**

push:
branches:
Expand Down
2 changes: 1 addition & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[submodule "spec/engine-test-data"]
path = spec/engine-test-data
url = [email protected]:Flagsmith/engine-test-data.git
branch = v1.0.0
branch = main
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
46 changes: 27 additions & 19 deletions lib/flagsmith.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ def initialize(config)
api_client
analytics_processor
environment_data_polling_manager
engine
load_offline_handler
end

Expand Down Expand Up @@ -99,10 +98,6 @@ def realtime_client
@realtime_client ||= Flagsmith::RealtimeClient.new(@config)
end

def engine
@engine ||= Flagsmith::Engine::Engine.new
end

def analytics_processor
return nil unless @config.enable_analytics?

Expand Down Expand Up @@ -211,21 +206,33 @@ def get_value_for_identity(feature_name, user_id = nil, default: nil)
end

def get_identity_segments(identifier, traits = {})
unless environment
raise Flagsmith::ClientError,
'Local evaluation or offline handler is required to obtain identity segments.'
end
raise Flagsmith::ClientError, 'Local evaluation or offline handler is required to obtain identity segments.' unless environment

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::Mappers.get_evaluation_context(environment, identity_model)
raise Flagsmith::ClientError, 'Local evaluation required to obtain identity segments' unless context

evaluation_result = Flagsmith::Engine.get_evaluation_result(context)
evaluation_result[:segments].filter_map do |segment_result|
id = segment_result.dig(:metadata, :id)
Flagsmith::Segments::Segment.new(id: id, name: segment_result[:name]) if id
end
end

private

def environment_flags_from_document
Flagsmith::Flags::Collection.from_feature_state_models(
engine.get_environment_feature_states(environment),
def environment_flags_from_document # rubocop:disable Metrics/MethodLength
context = Flagsmith::Engine::Mappers.get_evaluation_context(environment)

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

evaluation_result = Flagsmith::Engine.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 @@ -234,12 +241,13 @@ def environment_flags_from_document

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

Flagsmith::Flags::Collection.from_feature_state_models(
engine.get_identity_feature_states(environment, identity_model),
identity_id: identity_model.composite_key,
analytics_processor: analytics_processor,
default_flag_handler: default_flag_handler,
evaluation_result = Flagsmith::Engine.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
)
end
Expand Down
155 changes: 107 additions & 48 deletions lib/flagsmith/engine/core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,84 +5,143 @@

require_relative 'environments/models'
require_relative 'features/models'
require_relative 'features/constants'
require_relative 'identities/models'
require_relative 'organisations/models'
require_relative 'projects/models'
require_relative 'segments/evaluator'
require_relative 'segments/models'
require_relative 'utils/hash_func'
require_relative 'mappers'

module Flagsmith
# Core evaluation logic for feature flags
module Engine
# Flags engine methods
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
extend self
include Flagsmith::Engine::Utils::HashFunc
include Flagsmith::Engine::Features::TargetingReasons
include Flagsmith::Engine::Segments::Evaluator

# Get evaluation result from evaluation context
#
# @param evaluation_context [Hash] The evaluation context
# @return [Hash] Evaluation result with flags and segments
def get_evaluation_result(evaluation_context)
evaluation_context = get_enriched_context(evaluation_context)
segments, segment_overrides = evaluate_segments(evaluation_context)
flags = evaluate_features(evaluation_context, segment_overrides)
{
flags: flags,
segments: segments
}
end

feature_state = feature_states.find { |f| f.feature.name == feature_name }
# Returns { segments: EvaluationResultSegments; segmentOverrides: Record<string, SegmentOverride>; }
def evaluate_segments(evaluation_context)
return [], {} if evaluation_context[:segments].nil?

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

feature_state
segments = identity_segments.map do |segment|
{ name: segment[:name], metadata: segment[:metadata] }.compact
end

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

[segments, segment_overrides]
end

return feature_states.select(&:enabled?) if environment.project.hide_disabled_flags
# Returns Record<string: override.name, SegmentOverride>
def process_segment_overrides(identity_segments) # rubocop:disable Metrics/MethodLength
segment_overrides = {}

feature_states
identity_segments.each do |segment|
Array(segment[:overrides]).each do |override|
next unless should_apply_override(override, segment_overrides)

segment_overrides[override[:name]] = {
feature: override,
segment_name: segment[:name]
}
end
end

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

raise Flagsmith::FeatureStateNotFound, 'Feature State Not Found' if features_state.nil?
def evaluate_features(evaluation_context, segment_overrides)
identity_key = get_identity_key(evaluation_context)

features_state
(evaluation_context[:features] || {}).each_with_object({}) do |(_, feature), flags|
segment_override = segment_overrides[feature[:name]]
final_feature = segment_override ? segment_override[:feature] : feature

flag_result = build_flag_result(final_feature, identity_key, segment_override)
flags[final_feature[:name].to_sym] = flag_result
end
end

def get_environment_feature_states(environment)
return environment.feature_states.select(&:enabled?) if environment.project.hide_disabled_flags
# Returns {value: any; reason?: string}
def evaluate_feature_value(feature, identity_key = nil)
return get_multivariate_feature_value(feature, identity_key) if feature[:variants]&.any? && identity_key

environment.feature_states
end
{ value: feature[:value], reason: nil }
end

private
# 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] || WEAKEST_PRIORITY }

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)
variant = find_matching_variant(sorted_variants, percentage_value)
variant || { value: feature[:value], reason: nil }
end

override_by_matching_segments(environment, identity, override_traits) do |fs|
override.call(fs) unless higher_segment_priority?(feature_states, fs)
end
def find_matching_variant(sorted_variants, percentage_value)
start_percentage = 0
sorted_variants.each do |variant|
limit = start_percentage + variant[:weight]
return { value: variant[:value], reason: "#{TARGETING_REASON_SPLIT}; weight=#{variant[:weight]}" } if start_percentage <= percentage_value && percentage_value < limit

# Override with any feature states defined directly the identity
identity.identity_features.each(&override)
feature_states
start_percentage = limit
end
nil
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
# returns boolean
def should_apply_override(override, existing_overrides)
current_override = existing_overrides[override[:name]]
!current_override || stronger_priority?(override[:priority], current_override[:feature][:priority])
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
private

def build_flag_result(feature, identity_key, segment_override)
evaluated = evaluate_feature_value(feature, identity_key)

flag_result = {
name: feature[:name],
enabled: feature[:enabled],
value: evaluated[:value],
reason: evaluated[:reason] || (segment_override ? "#{TARGETING_REASON_TARGETING_MATCH}; segment=#{segment_override[:segment_name]}" : TARGETING_REASON_DEFAULT)
}

flag_result[:metadata] = feature[:metadata] if feature[:metadata]
flag_result
end

# Extract identity key from evaluation context
#
# @param evaluation_context [Hash] The evaluation context
# @return [String, nil] The identity key or nil if no identity
def get_identity_key(evaluation_context)
return nil unless evaluation_context[:identity]

evaluation_context[:identity][:key]
end

def stronger_priority?(priority_a, priority_b)
(priority_a || WEAKEST_PRIORITY) < (priority_b || WEAKEST_PRIORITY)
end
end
end
9 changes: 6 additions & 3 deletions lib/flagsmith/engine/environments/models.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,20 @@ module Flagsmith
module Engine
# EnvironmentModel
class Environment
attr_reader :id, :api_key
attr_reader :id, :api_key, :name
attr_accessor :project, :feature_states, :amplitude_config, :segment_config,
:mixpanel_config, :heap_config, :identity_overrides

def initialize(id:, api_key:, project:, feature_states: [], identity_overrides: [])
# rubocop:disable Metrics/ParameterLists
def initialize(id:, api_key:, project:, name: nil, feature_states: [], identity_overrides: [])
@id = id
@api_key = api_key
@name = name
@project = project
@feature_states = feature_states
@identity_overrides = identity_overrides
end
# rubocop:enable Metrics/ParameterLists

class << self
# rubocop:disable Metrics/MethodLength
Expand All @@ -28,7 +31,7 @@ def build(json)
Flagsmith::Engine::Identity.build(io)
end

new(**json.slice(:id, :api_key).merge(
new(**json.slice(:id, :api_key, :name).merge(
project: project,
feature_states: feature_states,
identity_overrides: identity_overrides
Expand Down
Loading