Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
2aeb65a
feat: added-engine-function-signatures
Zaimwa9 Oct 24, 2025
ba057cb
feat: moved-engine-to-core
Zaimwa9 Oct 24, 2025
bb96eb1
feat: implemented-process-segment-overrides
Zaimwa9 Oct 24, 2025
a2fd5a8
feat: implemented-evalute-segments-partially
Zaimwa9 Oct 24, 2025
0f08fb1
feat: implemented-should-apply-override
Zaimwa9 Oct 24, 2025
da08e8e
feat: implemented-get-identity-segments
Zaimwa9 Oct 27, 2025
af51bf7
feat: implemented-new-in-and-fixed-remaining-tests
Zaimwa9 Oct 27, 2025
0fb7587
feat: run-lint
Zaimwa9 Oct 28, 2025
741ceeb
feat: rebased
Zaimwa9 Oct 28, 2025
6a6a129
feat: misc
Zaimwa9 Oct 28, 2025
3bad95b
Merge branch 'feat/evaluation-context-mappers' of github.com:Flagsmit…
Zaimwa9 Oct 30, 2025
f0a53b7
feat: json-path-lib-implementation
Zaimwa9 Oct 30, 2025
ef1274a
remove dup
gagantrivedi Nov 5, 2025
962c01c
feat: made-legacy-functions-public
Zaimwa9 Nov 10, 2025
771a1a1
feat: updated-tests-to-match-engine-in-operator-accepting-numbers
Zaimwa9 Nov 10, 2025
561cd71
feat: rebased
Zaimwa9 Nov 10, 2025
21d7e74
feat: engine-agnostic-to-empty-identity-in-segment-evaluation
Zaimwa9 Nov 10, 2025
0baa0b4
feat: renamed-to-is-higher-priority
Zaimwa9 Nov 10, 2025
97cfdfb
feat: renamed-get-identity-segments-func
Zaimwa9 Nov 10, 2025
7188b95
feat: reverted-to-is-primitive
Zaimwa9 Nov 10, 2025
418ae33
feat: use-weakest-priority-constant
Zaimwa9 Nov 10, 2025
12144a3
feat: upgraded-engine-test-data-and-fixed-mv-evaluation-bug
Zaimwa9 Nov 10, 2025
fe63b45
feat: removed-targeting-reason-func
Zaimwa9 Nov 10, 2025
6420d15
feat: linter-rubocop-autocorrect
Zaimwa9 Nov 10, 2025
cbbd3c2
feat: linter
Zaimwa9 Nov 10, 2025
3b729fc
feat: linter
Zaimwa9 Nov 10, 2025
7c15dd5
Merge branch 'feat/evaluation-context-mappers' of github.com:Flagsmit…
Zaimwa9 Nov 10, 2025
13057f9
feat: moved-mappers-to-engine-namespace
Zaimwa9 Nov 10, 2025
7cee757
feat: rebased
Zaimwa9 Nov 11, 2025
2f3850c
Merge branch 'feat/evaluation-context-mappers' of github.com:Flagsmit…
Zaimwa9 Nov 11, 2025
37c424f
feat: enrich-context-with-identity-key
Zaimwa9 Nov 11, 2025
29e6bf6
feat: run-ci-on-all-branches
Zaimwa9 Nov 11, 2025
080f191
feat: removed-comments
Zaimwa9 Nov 11, 2025
fc1369a
feat!: sdk consumes context engine (#89)
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
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: 106 additions & 49 deletions lib/flagsmith/engine/core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,86 +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'
require_relative 'evaluation/core'

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
Loading