Skip to content

Commit 830fc6d

Browse files
authored
Merge branch 'development' into update-ruby-version-of-ci
2 parents 948fd68 + 5d8f3cc commit 830fc6d

129 files changed

Lines changed: 3740 additions & 863 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGES.txt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
CHANGES
22

3+
8.8.0 (Sep 26, 2025)
4+
- Added a maximum size payload when posting unique keys telemetry in batches
5+
6+
8.7.0 (Aug 1, 2025)
7+
- Added a new optional argument to the client `getTreatment` methods to allow passing additional evaluation options, such as a map of properties to append to the generated impressions sent to Split backend. Read more in our docs.
8+
9+
8.6.0 (Jun 17, 2025)
10+
- 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.
11+
- Added support for feature flag prerequisites. This allows customers to define dependency conditions between flags, which are evaluated before any allowlists or targeting rules.
12+
13+
8.5.0 (Jan 17, 2025)
14+
- Fixed high cpu usage when unique keys are cleared every 24 hours.
15+
- Added support for the new impressions tracking toggle available on feature flags, both respecting the setting and including the new field being returned on SplitView type objects. Read more in our docs.
16+
317
8.4.0 (May 3, 2024)
418
- Fixed issue preventing Impressopns and Events posting if client.destroy is called before the post threads started
519
- Added support for targeting rules based on semantic versions (https://semver.org/).

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Copyright © 2024 Split Software, Inc.
1+
Copyright © 2025 Split Software, Inc.
22

33
Licensed under the Apache License, Version 2.0 (the "License");
44
you may not use this file except in compliance with the License.

lib/splitclient-rb.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
require 'splitclient-rb/cache/repositories/splits_repository'
2424
require 'splitclient-rb/cache/repositories/events_repository'
2525
require 'splitclient-rb/cache/repositories/impressions_repository'
26+
require 'splitclient-rb/cache/repositories/rule_based_segments_repository'
2627
require 'splitclient-rb/cache/repositories/events/memory_repository'
2728
require 'splitclient-rb/cache/repositories/events/redis_repository'
2829
require 'splitclient-rb/cache/repositories/flag_sets/memory_repository'
@@ -47,6 +48,7 @@
4748
require 'splitclient-rb/helpers/decryption_helper'
4849
require 'splitclient-rb/helpers/util'
4950
require 'splitclient-rb/helpers/repository_helper'
51+
require 'splitclient-rb/helpers/evaluator_helper'
5052
require 'splitclient-rb/split_factory'
5153
require 'splitclient-rb/split_factory_builder'
5254
require 'splitclient-rb/split_config'
@@ -96,13 +98,18 @@
9698
require 'splitclient-rb/engine/matchers/less_than_or_equal_to_semver_matcher'
9799
require 'splitclient-rb/engine/matchers/between_semver_matcher'
98100
require 'splitclient-rb/engine/matchers/in_list_semver_matcher'
101+
require 'splitclient-rb/engine/matchers/rule_based_segment_matcher'
102+
require 'splitclient-rb/engine/matchers/prerequisites_matcher'
99103
require 'splitclient-rb/engine/evaluator/splitter'
100104
require 'splitclient-rb/engine/impressions/noop_unique_keys_tracker'
101105
require 'splitclient-rb/engine/impressions/unique_keys_tracker'
102106
require 'splitclient-rb/engine/metrics/binary_search_latency_tracker'
103107
require 'splitclient-rb/engine/models/split'
104108
require 'splitclient-rb/engine/models/label'
109+
require 'splitclient-rb/engine/models/segment_type'
105110
require 'splitclient-rb/engine/models/treatment'
111+
require 'splitclient-rb/engine/models/split_http_response'
112+
require 'splitclient-rb/engine/models/evaluation_options'
106113
require 'splitclient-rb/engine/auth_api_client'
107114
require 'splitclient-rb/engine/back_off'
108115
require 'splitclient-rb/engine/push_manager'

lib/splitclient-rb/cache/fetchers/split_fetcher.rb

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ module SplitIoClient
22
module Cache
33
module Fetchers
44
class SplitFetcher
5-
attr_reader :splits_repository
5+
attr_reader :splits_repository, :rule_based_segments_repository
66

7-
def initialize(splits_repository, api_key, config, telemetry_runtime_producer)
7+
def initialize(splits_repository, rule_based_segments_repository, api_key, config, telemetry_runtime_producer)
88
@splits_repository = splits_repository
9+
@rule_based_segments_repository = rule_based_segments_repository
910
@api_key = api_key
1011
@config = config
1112
@semaphore = Mutex.new
@@ -23,10 +24,11 @@ def call
2324

2425
def fetch_splits(fetch_options = { cache_control_headers: false, till: nil })
2526
@semaphore.synchronize do
26-
data = splits_since(@splits_repository.get_change_number, fetch_options)
27-
28-
SplitIoClient::Helpers::RepositoryHelper.update_feature_flag_repository(@splits_repository, data[:splits], data[:till], @config)
27+
data = splits_since(@splits_repository.get_change_number, @rule_based_segments_repository.get_change_number, fetch_options)
28+
SplitIoClient::Helpers::RepositoryHelper.update_feature_flag_repository(@splits_repository, data[:ff][:d], data[:ff][:t], @config, @splits_api.clear_storage)
29+
SplitIoClient::Helpers::RepositoryHelper.update_rule_based_segment_repository(@rule_based_segments_repository, data[:rbs][:d], data[:rbs][:t], @config)
2930
@splits_repository.set_segment_names(data[:segment_names])
31+
@rule_based_segments_repository.set_segment_names(data[:segment_names])
3032
@config.logger.debug("segments seen(#{data[:segment_names].length}): #{data[:segment_names].to_a}") if @config.debug_enabled
3133

3234
{ segment_names: data[:segment_names], success: true }
@@ -55,8 +57,8 @@ def splits_thread
5557
end
5658
end
5759

58-
def splits_since(since, fetch_options = { cache_control_headers: false, till: nil })
59-
splits_api.since(since, fetch_options)
60+
def splits_since(since, since_rbs, fetch_options = { cache_control_headers: false, till: nil })
61+
splits_api.since(since, since_rbs, fetch_options)
6062
end
6163

6264
def splits_api

lib/splitclient-rb/cache/filter/bloom_filter.rb

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,31 +8,35 @@ module Filter
88
class BloomFilter
99
def initialize(capacity, false_positive_probability = 0.001)
1010
@capacity = capacity.round
11-
m = best_m(capacity, false_positive_probability)
12-
@ba = BitArray.new(m.round)
11+
@m = best_m(capacity, false_positive_probability)
12+
reset_filter
1313
@k = best_k(capacity)
1414
end
1515

1616
def add(string)
1717
return false if contains?(string)
1818

1919
positions = hashes(string)
20-
2120
positions.each { |position| @ba[position] = 1 }
2221

2322
true
2423
end
25-
24+
2625
def contains?(string)
2726
!hashes(string).any? { |ea| @ba[ea] == 0 }
2827
end
2928

3029
def clear
31-
@ba.size.times { |i| @ba[i] = 0 }
30+
@ba = nil
31+
reset_filter
3232
end
33-
33+
3434
private
3535

36+
def reset_filter
37+
@ba = BitArray.new(@m.round)
38+
end
39+
3640
# m is the required number of bits in the array
3741
def best_m(capacity, false_positive_probability)
3842
-(capacity * Math.log(false_positive_probability)) / (Math.log(2) ** 2)
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
require 'concurrent'
2+
3+
module SplitIoClient
4+
module Cache
5+
module Repositories
6+
class RuleBasedSegmentsRepository < Repository
7+
attr_reader :adapter
8+
DEFAULT_CONDITIONS_TEMPLATE = [{
9+
conditionType: "ROLLOUT",
10+
matcherGroup: {
11+
combiner: "AND",
12+
matchers: [
13+
{
14+
keySelector: nil,
15+
matcherType: "ALL_KEYS",
16+
negate: false,
17+
userDefinedSegmentMatcherData: nil,
18+
whitelistMatcherData: nil,
19+
unaryNumericMatcherData: nil,
20+
betweenMatcherData: nil,
21+
dependencyMatcherData: nil,
22+
booleanMatcherData: nil,
23+
stringMatcherData: nil
24+
}]
25+
}
26+
}]
27+
TILL_PREFIX = '.rbsegments.till'
28+
RB_SEGMENTS_PREFIX = '.rbsegment.'
29+
REGISTERED_PREFIX = '.segments.registered'
30+
31+
def initialize(config)
32+
super(config)
33+
@adapter = case @config.cache_adapter.class.to_s
34+
when 'SplitIoClient::Cache::Adapters::RedisAdapter'
35+
SplitIoClient::Cache::Adapters::CacheAdapter.new(@config)
36+
else
37+
@config.cache_adapter
38+
end
39+
unless @config.mode.equal?(:consumer)
40+
@adapter.set_string(namespace_key(TILL_PREFIX), '-1')
41+
@adapter.initialize_map(namespace_key(REGISTERED_PREFIX))
42+
end
43+
end
44+
45+
def update(to_add, to_delete, new_change_number)
46+
to_add.each{ |rule_based_segment| add_rule_based_segment(rule_based_segment) }
47+
to_delete.each{ |rule_based_segment| remove_rule_based_segment(rule_based_segment) }
48+
set_change_number(new_change_number)
49+
end
50+
51+
def get_rule_based_segment(name)
52+
rule_based_segment = @adapter.string(namespace_key("#{RB_SEGMENTS_PREFIX}#{name}"))
53+
54+
JSON.parse(rule_based_segment, symbolize_names: true) if rule_based_segment
55+
end
56+
57+
def rule_based_segment_names
58+
@adapter.find_strings_by_prefix(namespace_key(RB_SEGMENTS_PREFIX))
59+
.map { |rule_based_segment_names| rule_based_segment_names.gsub(namespace_key(RB_SEGMENTS_PREFIX), '') }
60+
end
61+
62+
def set_change_number(since)
63+
@adapter.set_string(namespace_key(TILL_PREFIX), since)
64+
end
65+
66+
def get_change_number
67+
@adapter.string(namespace_key(TILL_PREFIX))
68+
end
69+
70+
def set_segment_names(names)
71+
return if names.nil? || names.empty?
72+
73+
names.each do |name|
74+
@adapter.add_to_set(namespace_key(REGISTERED_PREFIX), name)
75+
end
76+
end
77+
78+
def exists?(name)
79+
@adapter.exists?(namespace_key("#{RB_SEGMENTS_PREFIX}#{name}"))
80+
end
81+
82+
def clear
83+
@adapter.clear(namespace_key)
84+
end
85+
86+
def contains?(segment_names)
87+
return false if rule_based_segment_names.empty?
88+
89+
return segment_names.to_set.subset?(rule_based_segment_names.to_set)
90+
end
91+
92+
private
93+
94+
def add_rule_based_segment(rule_based_segment)
95+
return unless rule_based_segment[:name]
96+
97+
if check_undefined_matcher(rule_based_segment)
98+
@config.logger.warn("Rule based segment #{rule_based_segment[:name]} has undefined matcher, setting conditions to default template.")
99+
rule_based_segment[:conditions] = RuleBasedSegmentsRepository::DEFAULT_CONDITIONS_TEMPLATE
100+
end
101+
102+
@adapter.set_string(namespace_key("#{RB_SEGMENTS_PREFIX}#{rule_based_segment[:name]}"), rule_based_segment.to_json)
103+
end
104+
105+
def check_undefined_matcher(rule_based_segment)
106+
for condition in rule_based_segment[:conditions]
107+
for matcher in condition[:matcherGroup][:matchers]
108+
if !SplitIoClient::Condition.instance_methods(false).map(&:to_s).include?("matcher_#{matcher[:matcherType].downcase}")
109+
@config.logger.error("Detected undefined matcher #{matcher[:matcherType].downcase} in feature flag #{rule_based_segment[:name]}")
110+
return true
111+
end
112+
end
113+
end
114+
return false
115+
end
116+
117+
def remove_rule_based_segment(rule_based_segment)
118+
@adapter.delete(namespace_key("#{RB_SEGMENTS_PREFIX}#{rule_based_segment[:name]}"))
119+
end
120+
end
121+
end
122+
end
123+
end

lib/splitclient-rb/cache/repositories/segments_repository.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,13 @@ def segment_keys_count
8383
0
8484
end
8585

86+
def contains?(segment_names)
87+
if segment_names.empty?
88+
return false
89+
end
90+
return segment_names.to_set.subset?(used_segment_names.to_set)
91+
end
92+
8693
private
8794

8895
def segment_data(name)

lib/splitclient-rb/cache/repositories/splits_repository.rb

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ class SplitsRepository < Repository
3131
],
3232
label: "targeting rule type unsupported by sdk"
3333
}]
34+
TILL_PREFIX = '.splits.till'
35+
SPLIT_PREFIX = '.split.'
36+
READY_PREFIX = '.splits.ready'
3437

3538
def initialize(config, flag_sets_repository, flag_set_filter)
3639
super(config)
@@ -43,10 +46,7 @@ def initialize(config, flag_sets_repository, flag_set_filter)
4346
end
4447
@flag_sets = flag_sets_repository
4548
@flag_set_filter = flag_set_filter
46-
unless @config.mode.equal?(:consumer)
47-
@adapter.set_string(namespace_key('.splits.till'), '-1')
48-
@adapter.initialize_map(namespace_key('.segments.registered'))
49-
end
49+
initialize_keys
5050
end
5151

5252
def update(to_add, to_delete, new_change_number)
@@ -87,16 +87,16 @@ def traffic_type_exists(tt_name)
8787

8888
# Return an array of Split Names excluding control keys like splits.till
8989
def split_names
90-
@adapter.find_strings_by_prefix(namespace_key('.split.'))
91-
.map { |split| split.gsub(namespace_key('.split.'), '') }
90+
@adapter.find_strings_by_prefix(namespace_key(SPLIT_PREFIX))
91+
.map { |split| split.gsub(namespace_key(SPLIT_PREFIX), '') }
9292
end
9393

9494
def set_change_number(since)
95-
@adapter.set_string(namespace_key('.splits.till'), since)
95+
@adapter.set_string(namespace_key(TILL_PREFIX), since)
9696
end
9797

9898
def get_change_number
99-
@adapter.string(namespace_key('.splits.till'))
99+
@adapter.string(namespace_key(TILL_PREFIX))
100100
end
101101

102102
def set_segment_names(names)
@@ -112,21 +112,22 @@ def exists?(name)
112112
end
113113

114114
def ready?
115-
@adapter.string(namespace_key('.splits.ready')).to_i != -1
115+
@adapter.string(namespace_key(READY_PREFIX)).to_i != -1
116116
end
117117

118118
def not_ready!
119-
@adapter.set_string(namespace_key('.splits.ready'), -1)
119+
@adapter.set_string(namespace_key(READY_PREFIX), -1)
120120
end
121121

122122
def ready!
123-
@adapter.set_string(namespace_key('.splits.ready'), Time.now.utc.to_i)
123+
@adapter.set_string(namespace_key(READY_PREFIX), Time.now.utc.to_i)
124124
end
125125

126126
def clear
127127
@tt_cache.clear
128128

129129
@adapter.clear(namespace_key)
130+
initialize_keys
130131
end
131132

132133
def kill(change_number, split_name, default_treatment)
@@ -167,6 +168,13 @@ def flag_set_filter
167168

168169
private
169170

171+
def initialize_keys
172+
unless @config.mode.equal?(:consumer)
173+
@adapter.set_string(namespace_key(TILL_PREFIX), '-1')
174+
@adapter.initialize_map(namespace_key('.segments.registered'))
175+
end
176+
end
177+
170178
def add_feature_flag(split)
171179
return unless split[:name]
172180
existing_split = get_split(split[:name])

0 commit comments

Comments
 (0)