Skip to content

Merging Feature/impressions properties #572

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Aug 1, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
CHANGES

8.7.0 (Aug 1, 2025)
- Added support for feature flag prerequisites. This allows customers to define dependency conditions between flags, which are evaluated before any allowlists or targeting rules.

8.6.0 (Jun 17, 2025)
- Added support for rule-based segments. These segments determine membership at runtime by evaluating their configured rules against the user attributes provided to the SDK.
- Added support for feature flag prerequisites. This allows customers to define dependency conditions between flags, which are evaluated before any allowlists or targeting rules.
Expand Down
1 change: 1 addition & 0 deletions lib/splitclient-rb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@
require 'splitclient-rb/engine/models/segment_type'
require 'splitclient-rb/engine/models/treatment'
require 'splitclient-rb/engine/models/split_http_response'
require 'splitclient-rb/engine/models/evaluation_options'
require 'splitclient-rb/engine/auth_api_client'
require 'splitclient-rb/engine/back_off'
require 'splitclient-rb/engine/push_manager'
Expand Down
34 changes: 24 additions & 10 deletions lib/splitclient-rb/cache/senders/impressions_formatter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,28 @@ def feature_impressions(filtered_impressions, feature)

def current_impressions(feature_impressions)
feature_impressions.map do |impression|
{
k: impression[:i][:k],
t: impression[:i][:t],
m: impression[:i][:m],
b: impression[:i][:b],
r: impression[:i][:r],
c: impression[:i][:c],
pt: impression[:i][:pt]
}
if impression[:i][:properties].nil?
impression = {
k: impression[:i][:k],
t: impression[:i][:t],
m: impression[:i][:m],
b: impression[:i][:b],
r: impression[:i][:r],
c: impression[:i][:c],
pt: impression[:i][:pt]
}
else
impression = {
k: impression[:i][:k],
t: impression[:i][:t],
m: impression[:i][:m],
b: impression[:i][:b],
r: impression[:i][:r],
c: impression[:i][:c],
pt: impression[:i][:pt],
properties: impression[:i][:properties].to_json.to_s
}
end
end
end

Expand Down Expand Up @@ -73,7 +86,8 @@ def impression_hash(impression)
"#{impression[:i][:b]}:" \
"#{impression[:i][:c]}:" \
"#{impression[:i][:t]}:" \
"#{impression[:i][:pt]}"
"#{impression[:i][:pt]}" \
"#{impression[:i][:properties]}" \
end
end
end
Expand Down
96 changes: 63 additions & 33 deletions lib/splitclient-rb/clients/split_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,64 +35,67 @@ def initialize(sdk_key, repositories, status_manager, config, impressions_manage
end

def get_treatment(
key, split_name, attributes = {}, split_data = nil, store_impressions = true,
key, split_name, attributes = {}, evaluation_options = nil, split_data = nil, store_impressions = nil,
multiple = false, evaluator = nil
)
result = treatment(key, split_name, attributes, split_data, store_impressions, GET_TREATMENT, multiple)
log_deprecated_warning(GET_TREATMENT, evaluator, 'evaluator')

result = treatment(key, split_name, attributes, split_data, store_impressions, GET_TREATMENT, multiple, evaluation_options)
return result.tap { |t| t.delete(:config) } if multiple
result[:treatment]
end

def get_treatment_with_config(
key, split_name, attributes = {}, split_data = nil, store_impressions = true,
key, split_name, attributes = {}, evaluation_options = nil, split_data = nil, store_impressions = nil,
multiple = false, evaluator = nil
)
treatment(key, split_name, attributes, split_data, store_impressions, GET_TREATMENT_WITH_CONFIG, multiple)
log_deprecated_warning(GET_TREATMENT, evaluator, 'evaluator')
treatment(key, split_name, attributes, split_data, store_impressions, GET_TREATMENT_WITH_CONFIG, multiple, evaluation_options)
end

def get_treatments(key, split_names, attributes = {})
treatments = treatments(key, split_names, attributes)
def get_treatments(key, split_names, attributes = {}, evaluation_options = nil)
treatments = treatments(key, split_names, attributes, evaluation_options)

return treatments if treatments.nil?
keys = treatments.keys
treats = treatments.map { |_,t| t[:treatment] }
Hash[keys.zip(treats)]
end

def get_treatments_with_config(key, split_names, attributes = {})
treatments(key, split_names, attributes, GET_TREATMENTS_WITH_CONFIG)
def get_treatments_with_config(key, split_names, attributes = {}, evaluation_options = nil)
treatments(key, split_names, attributes, evaluation_options, GET_TREATMENTS_WITH_CONFIG)
end

def get_treatments_by_flag_set(key, flag_set, attributes = {})
def get_treatments_by_flag_set(key, flag_set, attributes = {}, evaluation_options = nil)
valid_flag_set = @split_validator.valid_flag_sets(GET_TREATMENTS_BY_FLAG_SET, [flag_set])
split_names = @splits_repository.get_feature_flags_by_sets(valid_flag_set)
treatments = treatments(key, split_names, attributes, GET_TREATMENTS_BY_FLAG_SET)
treatments = treatments(key, split_names, attributes, evaluation_options, GET_TREATMENTS_BY_FLAG_SET)
return treatments if treatments.nil?
keys = treatments.keys
treats = treatments.map { |_,t| t[:treatment] }
Hash[keys.zip(treats)]
end

def get_treatments_by_flag_sets(key, flag_sets, attributes = {})
def get_treatments_by_flag_sets(key, flag_sets, attributes = {}, evaluation_options = nil)
valid_flag_set = @split_validator.valid_flag_sets(GET_TREATMENTS_BY_FLAG_SETS, flag_sets)
split_names = @splits_repository.get_feature_flags_by_sets(valid_flag_set)
treatments = treatments(key, split_names, attributes, GET_TREATMENTS_BY_FLAG_SETS)
treatments = treatments(key, split_names, attributes, evaluation_options, GET_TREATMENTS_BY_FLAG_SETS)
return treatments if treatments.nil?
keys = treatments.keys
treats = treatments.map { |_,t| t[:treatment] }
Hash[keys.zip(treats)]
end

def get_treatments_with_config_by_flag_set(key, flag_set, attributes = {})
def get_treatments_with_config_by_flag_set(key, flag_set, attributes = {}, evaluation_options = nil)
valid_flag_set = @split_validator.valid_flag_sets(GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SET, [flag_set])
split_names = @splits_repository.get_feature_flags_by_sets(valid_flag_set)
treatments(key, split_names, attributes, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SET)
treatments(key, split_names, attributes, evaluation_options, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SET)
end

def get_treatments_with_config_by_flag_sets(key, flag_sets, attributes = {})
def get_treatments_with_config_by_flag_sets(key, flag_sets, attributes = {}, evaluation_options = nil)
valid_flag_set = @split_validator.valid_flag_sets(GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SETS, flag_sets)
split_names = @splits_repository.get_feature_flags_by_sets(valid_flag_set)
treatments(key, split_names, attributes, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SETS)
treatments(key, split_names, attributes, evaluation_options, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SETS)
end

def destroy
Expand Down Expand Up @@ -135,10 +138,7 @@ def track(key, traffic_type_name, event_type, value = nil, properties = nil)
if !properties.nil?
properties, size = validate_properties(properties)
properties_size += size
if (properties_size > EVENTS_SIZE_THRESHOLD)
@config.logger.error("The maximum size allowed for the properties is #{EVENTS_SIZE_THRESHOLD}. Current is #{properties_size}. Event not queued")
return false
end
return false unless check_properties_size(properties_size)
end

if ready? && [email protected]_mode && !@splits_repository.traffic_type_exists(traffic_type_name)
Expand All @@ -163,6 +163,14 @@ def block_until_ready(time = nil)

private

def check_properties_size(properties_size, msg = "Event not queued")
if (properties_size > EVENTS_SIZE_THRESHOLD)
@config.logger.error("The maximum size allowed for the properties is #{EVENTS_SIZE_THRESHOLD}. Current is #{properties_size}. #{msg}")
return false
end
return true
end

def keys_from_key(key)
case key
when Hash
Expand Down Expand Up @@ -206,7 +214,7 @@ def sanitize_split_names(calling_method, split_names)
end
end

def validate_properties(properties)
def validate_properties(properties, method = 'Event')
properties_count = 0
size = 0

Expand All @@ -225,11 +233,21 @@ def validate_properties(properties)
end
}

@config.logger.warn('Event has more than 300 properties. Some of them will be trimmed when processed') if properties_count > 300
@config.logger.warn("#{method} has more than 300 properties. Some of them will be trimmed when processed") if properties_count > 300

return fixed_properties, size
end

def validate_evaluation_options(evaluation_options)
if !evaluation_options.is_a?(SplitIoClient::Engine::Models::EvaluationOptions)
@config.logger.warn("Option #{evaluation_options} should be a EvaluationOptions type. Setting value to nil")
return nil, 0
end
evaluation_options.properties = evaluation_options.properties.transform_keys(&:to_sym)
evaluation_options.properties, size = validate_properties(evaluation_options.properties, 'Treatment')
return evaluation_options, size
end

def valid_client
if @destroyed
@config.logger.error('Client has already been destroyed - no calls possible')
Expand All @@ -238,8 +256,7 @@ def valid_client
@config.valid_mode
end

def treatments(key, feature_flag_names, attributes = {}, calling_method = 'get_treatments')
attributes = {} if attributes.nil?
def treatments(key, feature_flag_names, attributes = {}, evaluation_options = nil, calling_method = 'get_treatments')
sanitized_feature_flag_names = sanitize_split_names(calling_method, feature_flag_names)

if sanitized_feature_flag_names.nil?
Expand All @@ -255,6 +272,7 @@ def treatments(key, feature_flag_names, attributes = {}, calling_method = 'get_t
bucketing_key, matching_key = keys_from_key(key)
bucketing_key = bucketing_key ? bucketing_key.to_s : nil
matching_key = matching_key ? matching_key.to_s : nil
attributes = parsed_attributes(attributes)

if [email protected]_validator.valid_get_treatments_parameters(calling_method, key, sanitized_feature_flag_names, matching_key, bucketing_key, attributes)
to_return = Hash.new
Expand All @@ -269,7 +287,9 @@ def treatments(key, feature_flag_names, attributes = {}, calling_method = 'get_t
to_return = Hash.new
sanitized_feature_flag_names.each {|name|
to_return[name.to_sym] = control_treatment_with_config
impressions << { :impression => @impressions_manager.build_impression(matching_key, bucketing_key, name.to_sym, control_treatment_with_config.merge({ :label => Engine::Models::Label::NOT_READY }), false, { attributes: attributes, time: nil }), :disabled => false }
impressions << { :impression => @impressions_manager.build_impression(matching_key, bucketing_key, name.to_sym,
control_treatment_with_config.merge({ :label => Engine::Models::Label::NOT_READY }), false, { attributes: attributes, time: nil },
evaluation_options), :disabled => false }
}
@impressions_manager.track(impressions)
return to_return
Expand All @@ -291,7 +311,7 @@ def treatments(key, feature_flag_names, attributes = {}, calling_method = 'get_t
invalid_treatments[key] = control_treatment_with_config
next
end
treatments_labels_change_numbers, impressions = evaluate_treatment(feature_flag, key, bucketing_key, matching_key, attributes, calling_method)
treatments_labels_change_numbers, impressions = evaluate_treatment(feature_flag, key, bucketing_key, matching_key, attributes, calling_method, false, evaluation_options)
treatments[key] =
{
treatment: treatments_labels_change_numbers[:treatment],
Expand All @@ -313,8 +333,12 @@ def treatments(key, feature_flag_names, attributes = {}, calling_method = 'get_t
# @param split_data [Hash] split data, when provided this method doesn't fetch splits_repository for the data
# @param store_impressions [Boolean] impressions aren't stored if this flag is false
# @return [String/Hash] Treatment as String or Hash of treatments in case of array of features
def treatment(key, feature_flag_name, attributes = {}, split_data = nil, store_impressions = true,
calling_method = 'get_treatment', multiple = false)
def treatment(key, feature_flag_name, attributes = {}, split_data = nil, store_impressions = nil,
calling_method = 'get_treatment', multiple = false, evaluation_options = nil)

log_deprecated_warning(calling_method, split_data, 'split_data')
log_deprecated_warning(calling_method, store_impressions, 'store_impressions')

impressions = []
bucketing_key, matching_key = keys_from_key(key)

Expand All @@ -332,13 +356,17 @@ def treatment(key, feature_flag_name, attributes = {}, split_data = nil, store_i
end

feature_flag = @splits_repository.get_split(feature_flag_name)
treatments, impressions_decorator = evaluate_treatment(feature_flag, feature_flag_name, bucketing_key, matching_key, attributes, calling_method, multiple)
treatments, impressions_decorator = evaluate_treatment(feature_flag, feature_flag_name, bucketing_key, matching_key, attributes, calling_method, multiple, evaluation_options)

@impressions_manager.track(impressions_decorator) unless impressions_decorator.nil?
treatments
end

def evaluate_treatment(feature_flag, feature_flag_name, bucketing_key, matching_key, attributes, calling_method, multiple = false)
def log_deprecated_warning(calling_method, parameter, parameter_name)
@config.logger.warn("#{calling_method}: detected #{parameter_name} parameter used, this parameter is deprecated and its value is ignored.") unless parameter.nil?
end

def evaluate_treatment(feature_flag, feature_flag_name, bucketing_key, matching_key, attributes, calling_method, multiple = false, evaluation_options = nil)
impressions_decorator = []
begin
start = Time.now
Expand All @@ -359,18 +387,20 @@ def evaluate_treatment(feature_flag, feature_flag_name, bucketing_key, matching_
impressions_disabled = false
end

evaluation_options, size = validate_evaluation_options(evaluation_options) unless evaluation_options.nil?
evaluation_options.properties = nil unless evaluation_options.nil? or check_properties_size((EVENT_AVERAGE_SIZE + size), "Properties are ignored")

record_latency(calling_method, start)
impression_decorator = { :impression => @impressions_manager.build_impression(matching_key, bucketing_key, feature_flag_name, treatment_data, impressions_disabled, { attributes: attributes, time: nil }), :disabled => impressions_disabled }
impression_decorator = { :impression => @impressions_manager.build_impression(matching_key, bucketing_key, feature_flag_name, treatment_data, impressions_disabled, { attributes: attributes, time: nil }, evaluation_options), :disabled => impressions_disabled }
impressions_decorator << impression_decorator unless impression_decorator.nil?
rescue StandardError => e
@config.log_found_exception(__method__.to_s, e)
record_exception(calling_method)
impression_decorator = { :impression => @impressions_manager.build_impression(matching_key, bucketing_key, feature_flag_name, control_treatment, false, { attributes: attributes, time: nil }), :disabled => false }
impression_decorator = { :impression => @impressions_manager.build_impression(matching_key, bucketing_key, feature_flag_name, control_treatment, false, { attributes: attributes, time: nil }, evaluation_options), :disabled => false }
impressions_decorator << impression_decorator unless impression_decorator.nil?

return parsed_treatment(control_treatment.merge({ :label => Engine::Models::Label::EXCEPTION }), multiple), impressions_decorator
end

return parsed_treatment(treatment_data, multiple), impressions_decorator
end

Expand Down
Loading