splitclient-rb 7.0.0.pre.rc3-java → 7.0.1-java
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/pull_request_template.md +10 -0
- data/.gitignore +2 -0
- data/.travis.yml +9 -0
- data/CHANGES.txt +18 -11
- data/CONTRIBUTORS-GUIDE.md +49 -0
- data/README.md +75 -41
- data/lib/splitclient-rb.rb +4 -4
- data/lib/splitclient-rb/cache/adapters/memory_adapters/map_adapter.rb +4 -0
- data/lib/splitclient-rb/cache/adapters/memory_adapters/queue_adapter.rb +4 -0
- data/lib/splitclient-rb/cache/repositories/impressions/memory_repository.rb +4 -0
- data/lib/splitclient-rb/cache/repositories/impressions_repository.rb +2 -2
- data/lib/splitclient-rb/cache/repositories/metrics/memory_repository.rb +1 -1
- data/lib/splitclient-rb/cache/repositories/metrics_repository.rb +2 -2
- data/lib/splitclient-rb/cache/senders/impressions_sender.rb +0 -5
- data/lib/splitclient-rb/cache/senders/localhost_repo_cleaner.rb +49 -0
- data/lib/splitclient-rb/cache/stores/localhost_split_builder.rb +94 -0
- data/lib/splitclient-rb/cache/stores/localhost_split_store.rb +112 -0
- data/lib/splitclient-rb/cache/stores/segment_store.rb +1 -7
- data/lib/splitclient-rb/cache/stores/split_store.rb +1 -7
- data/lib/splitclient-rb/cache/stores/store_utils.rb +13 -0
- data/lib/splitclient-rb/clients/split_client.rb +16 -13
- data/lib/splitclient-rb/engine/api/client.rb +6 -11
- data/lib/splitclient-rb/engine/api/events.rb +3 -5
- data/lib/splitclient-rb/engine/api/impressions.rb +1 -1
- data/lib/splitclient-rb/engine/parser/split_adapter.rb +18 -1
- data/lib/splitclient-rb/managers/split_manager.rb +10 -9
- data/lib/splitclient-rb/split_config.rb +79 -17
- data/lib/splitclient-rb/split_factory.rb +11 -4
- data/lib/splitclient-rb/split_factory_builder.rb +1 -21
- data/lib/splitclient-rb/version.rb +1 -1
- data/sonar-scanner.sh +42 -0
- metadata +11 -10
- data/Detailed-README.md +0 -576
- data/NEWS +0 -144
- data/lib/splitclient-rb/clients/localhost_split_client.rb +0 -184
- data/lib/splitclient-rb/localhost_split_factory.rb +0 -14
- data/lib/splitclient-rb/localhost_utils.rb +0 -59
- data/lib/splitclient-rb/managers/localhost_split_manager.rb +0 -60
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SplitIoClient
|
4
|
+
module Cache
|
5
|
+
module Stores
|
6
|
+
class LocalhostSplitBuilder
|
7
|
+
class << self
|
8
|
+
def build_splits(splits)
|
9
|
+
splits.map do |feature, treatments|
|
10
|
+
build_split(feature, treatments)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def build_split(feature, treatments)
|
17
|
+
{
|
18
|
+
name: feature,
|
19
|
+
status: 'ACTIVE',
|
20
|
+
killed: false,
|
21
|
+
trafficAllocation: 100,
|
22
|
+
seed: 2_089_907_429,
|
23
|
+
defaultTreatment: 'control_treatment',
|
24
|
+
configurations: build_configurations(treatments),
|
25
|
+
conditions: build_conditions(treatments)
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
def build_configurations(treatments)
|
30
|
+
treatments.reduce({}) do |hash, treatment|
|
31
|
+
hash.merge(treatment[:treatment].to_sym => treatment[:config])
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def build_conditions(treatments)
|
36
|
+
conditions = treatments.map do |treatment|
|
37
|
+
if treatment[:keys]
|
38
|
+
build_whitelist_treatment(treatment[:treatment], Array(treatment[:keys]))
|
39
|
+
else
|
40
|
+
build_rollout_treatment
|
41
|
+
end
|
42
|
+
.merge(partitions: build_partitions(treatment[:treatment], treatments))
|
43
|
+
end
|
44
|
+
|
45
|
+
conditions.sort_by { |condition| condition[:conditionType] }.reverse!
|
46
|
+
end
|
47
|
+
|
48
|
+
def build_whitelist_treatment(treatment_name, whitelist_keys)
|
49
|
+
{
|
50
|
+
conditionType: 'WHITELIST',
|
51
|
+
matcherGroup: {
|
52
|
+
combiner: 'AND',
|
53
|
+
matchers: [{
|
54
|
+
keySelector: nil,
|
55
|
+
matcherType: 'WHITELIST',
|
56
|
+
negate: false,
|
57
|
+
whitelistMatcherData: {
|
58
|
+
whitelist: whitelist_keys
|
59
|
+
}
|
60
|
+
}]
|
61
|
+
},
|
62
|
+
label: "whitelisted #{treatment_name}"
|
63
|
+
}
|
64
|
+
end
|
65
|
+
|
66
|
+
def build_rollout_treatment
|
67
|
+
{
|
68
|
+
conditionType: 'ROLLOUT',
|
69
|
+
matcherGroup: {
|
70
|
+
combiner: 'AND',
|
71
|
+
matchers: [
|
72
|
+
{
|
73
|
+
matcherType: 'ALL_KEYS',
|
74
|
+
negate: false
|
75
|
+
}
|
76
|
+
]
|
77
|
+
},
|
78
|
+
label: 'default rule'
|
79
|
+
}
|
80
|
+
end
|
81
|
+
|
82
|
+
def build_partitions(current_treatment_name, treatments)
|
83
|
+
treatments.map do |treatment|
|
84
|
+
{
|
85
|
+
treatment: treatment[:treatment],
|
86
|
+
size: treatment[:treatment] == current_treatment_name ? 100 : 0
|
87
|
+
}
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SplitIoClient
|
4
|
+
module Cache
|
5
|
+
module Stores
|
6
|
+
class LocalhostSplitStore
|
7
|
+
attr_reader :splits_repository
|
8
|
+
|
9
|
+
def initialize(splits_repository, config, sdk_blocker = nil)
|
10
|
+
@splits_repository = splits_repository
|
11
|
+
@config = config
|
12
|
+
@sdk_blocker = sdk_blocker
|
13
|
+
end
|
14
|
+
|
15
|
+
def call
|
16
|
+
if ENV['SPLITCLIENT_ENV'] == 'test'
|
17
|
+
store_splits
|
18
|
+
else
|
19
|
+
splits_thread
|
20
|
+
|
21
|
+
if defined?(PhusionPassenger)
|
22
|
+
PhusionPassenger.on_event(:starting_worker_process) do |forked|
|
23
|
+
splits_thread if forked
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def splits_thread
|
32
|
+
@config.threads[:split_store] = Thread.new do
|
33
|
+
@config.logger.info('Starting splits fetcher service')
|
34
|
+
loop do
|
35
|
+
store_splits
|
36
|
+
|
37
|
+
sleep(StoreUtils.random_interval(@config.features_refresh_rate))
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def store_splits
|
43
|
+
load_features.each do |split|
|
44
|
+
store_split(split)
|
45
|
+
end
|
46
|
+
|
47
|
+
if @sdk_blocker
|
48
|
+
@sdk_blocker.splits_ready!
|
49
|
+
@sdk_blocker.segments_ready!
|
50
|
+
end
|
51
|
+
rescue StandardError => error
|
52
|
+
@config.logger.error('Error while parsing the split file. ' \
|
53
|
+
'Check that the input file matches the expected format')
|
54
|
+
@config.log_found_exception(__method__.to_s, error)
|
55
|
+
end
|
56
|
+
|
57
|
+
def store_split(split)
|
58
|
+
@config.logger.debug("storing split (#{split[:name]})") if @config.debug_enabled
|
59
|
+
|
60
|
+
@splits_repository.add_split(split)
|
61
|
+
end
|
62
|
+
|
63
|
+
def load_features
|
64
|
+
yaml_extensions = ['.yml', '.yaml']
|
65
|
+
if yaml_extensions.include? File.extname(@config.split_file)
|
66
|
+
parse_yaml_features
|
67
|
+
else
|
68
|
+
@config.logger.warn('Localhost mode: .split mocks ' \
|
69
|
+
'will be deprecated soon in favor of YAML files, which provide more ' \
|
70
|
+
'targeting power. Take a look in our documentation.')
|
71
|
+
|
72
|
+
parse_plain_text_features
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def parse_plain_text_features
|
77
|
+
splits = File.open(@config.split_file).each_with_object({}) do |line, memo|
|
78
|
+
feature, treatment = line.strip.split(' ')
|
79
|
+
|
80
|
+
next if line.start_with?('#') || line.strip.empty?
|
81
|
+
|
82
|
+
memo[feature] = [{ treatment: treatment }]
|
83
|
+
end
|
84
|
+
|
85
|
+
LocalhostSplitBuilder.build_splits(splits)
|
86
|
+
end
|
87
|
+
|
88
|
+
def parse_yaml_features
|
89
|
+
splits = YAML.safe_load(File.read(@config.split_file)).each_with_object({}) do |feature, memo|
|
90
|
+
feat_symbolized_keys = symbolize_feat_keys(feature)
|
91
|
+
|
92
|
+
feat_name = feature.keys.first
|
93
|
+
|
94
|
+
if memo[feat_name]
|
95
|
+
memo[feat_name] << feat_symbolized_keys
|
96
|
+
else
|
97
|
+
memo[feat_name] = [feat_symbolized_keys]
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
LocalhostSplitBuilder.build_splits(splits)
|
102
|
+
end
|
103
|
+
|
104
|
+
def symbolize_feat_keys(yaml_feature)
|
105
|
+
yaml_feature.values.first.each_with_object({}) do |(k, v), memo|
|
106
|
+
memo[k.to_sym] = k == 'config' ? v.to_json : v
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -38,7 +38,7 @@ module SplitIoClient
|
|
38
38
|
store_segments
|
39
39
|
@config.logger.debug("Segment names: #{@segments_repository.used_segment_names.to_a}") if @config.debug_enabled
|
40
40
|
|
41
|
-
sleep_for = random_interval(@config.segments_refresh_rate)
|
41
|
+
sleep_for = StoreUtils.random_interval(@config.segments_refresh_rate)
|
42
42
|
@config.logger.debug("Segments store is sleeping for: #{sleep_for} seconds") if @config.debug_enabled
|
43
43
|
sleep(sleep_for)
|
44
44
|
end
|
@@ -53,12 +53,6 @@ module SplitIoClient
|
|
53
53
|
@config.log_found_exception(__method__.to_s, error)
|
54
54
|
end
|
55
55
|
|
56
|
-
def random_interval(interval)
|
57
|
-
random_factor = Random.new.rand(50..100) / 100.0
|
58
|
-
|
59
|
-
interval * random_factor
|
60
|
-
end
|
61
|
-
|
62
56
|
def segments_api
|
63
57
|
@segments_api ||= SplitIoClient::Api::Segments.new(@api_key, @metrics, @segments_repository, @config)
|
64
58
|
end
|
@@ -34,7 +34,7 @@ module SplitIoClient
|
|
34
34
|
loop do
|
35
35
|
store_splits
|
36
36
|
|
37
|
-
sleep(random_interval(@config.features_refresh_rate))
|
37
|
+
sleep(StoreUtils.random_interval(@config.features_refresh_rate))
|
38
38
|
end
|
39
39
|
end
|
40
40
|
end
|
@@ -57,12 +57,6 @@ module SplitIoClient
|
|
57
57
|
@config.log_found_exception(__method__.to_s, error)
|
58
58
|
end
|
59
59
|
|
60
|
-
def random_interval(interval)
|
61
|
-
random_factor = Random.new.rand(50..100) / 100.0
|
62
|
-
|
63
|
-
interval * random_factor
|
64
|
-
end
|
65
|
-
|
66
60
|
def splits_since(since)
|
67
61
|
splits_api.since(since)
|
68
62
|
end
|
@@ -74,11 +74,9 @@ module SplitIoClient
|
|
74
74
|
@destroyed = true
|
75
75
|
end
|
76
76
|
|
77
|
-
def store_impression(split_name, matching_key, bucketing_key, treatment,
|
77
|
+
def store_impression(split_name, matching_key, bucketing_key, treatment, attributes)
|
78
78
|
time = (Time.now.to_f * 1000.0).to_i
|
79
79
|
|
80
|
-
return if @config.disable_impressions || !store_impressions
|
81
|
-
|
82
80
|
@impressions_repository.add(
|
83
81
|
matching_key,
|
84
82
|
bucketing_key,
|
@@ -133,7 +131,7 @@ module SplitIoClient
|
|
133
131
|
end
|
134
132
|
end
|
135
133
|
|
136
|
-
if ready? && !@splits_repository.traffic_type_exists(traffic_type_name)
|
134
|
+
if ready? && !@config.localhost_mode && !@splits_repository.traffic_type_exists(traffic_type_name)
|
137
135
|
@config.logger.warn("track: Traffic Type #{traffic_type_name} " \
|
138
136
|
"does not have any corresponding Splits in this environment, make sure you're tracking " \
|
139
137
|
'your events to a valid traffic type defined in the Split console')
|
@@ -249,14 +247,12 @@ module SplitIoClient
|
|
249
247
|
# Measure
|
250
248
|
@adapter.metrics.time('sdk.' + calling_method, latency)
|
251
249
|
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
)
|
250
|
+
time = (Time.now.to_f * 1000.0).to_i
|
251
|
+
@impressions_repository.add_bulk(
|
252
|
+
matching_key, bucketing_key, treatments_labels_change_numbers, time
|
253
|
+
)
|
257
254
|
|
258
|
-
|
259
|
-
end
|
255
|
+
route_impressions(sanitized_split_names, matching_key, bucketing_key, time, treatments_labels_change_numbers, attributes)
|
260
256
|
|
261
257
|
split_names_keys = treatments_labels_change_numbers.keys
|
262
258
|
treatments = treatments_labels_change_numbers.values.map do |v|
|
@@ -291,6 +287,8 @@ module SplitIoClient
|
|
291
287
|
|
292
288
|
bucketing_key, matching_key = keys_from_key(key)
|
293
289
|
|
290
|
+
attributes = parsed_attributes(attributes)
|
291
|
+
|
294
292
|
return parsed_control_exception unless valid_client && @config.split_validator.valid_get_treatment_parameters(calling_method, key, split_name, matching_key, bucketing_key, attributes)
|
295
293
|
|
296
294
|
bucketing_key = bucketing_key ? bucketing_key.to_s : nil
|
@@ -328,14 +326,15 @@ module SplitIoClient
|
|
328
326
|
end
|
329
327
|
|
330
328
|
latency = (Time.now - start) * 1000.0
|
331
|
-
|
329
|
+
|
330
|
+
store_impression(split_name, matching_key, bucketing_key, treatment_data, attributes) if store_impressions
|
332
331
|
|
333
332
|
# Measure
|
334
333
|
@adapter.metrics.time('sdk.' + calling_method, latency) unless multiple
|
335
334
|
rescue StandardError => error
|
336
335
|
@config.log_found_exception(__method__.to_s, error)
|
337
336
|
|
338
|
-
store_impression(split_name, matching_key, bucketing_key, control_treatment,
|
337
|
+
store_impression(split_name, matching_key, bucketing_key, control_treatment, attributes) if store_impressions
|
339
338
|
|
340
339
|
return parsed_control_exception
|
341
340
|
end
|
@@ -351,5 +350,9 @@ module SplitIoClient
|
|
351
350
|
return @sdk_blocker.ready? if @sdk_blocker
|
352
351
|
true
|
353
352
|
end
|
353
|
+
|
354
|
+
def parsed_attributes(attributes)
|
355
|
+
return attributes || attributes.to_h
|
356
|
+
end
|
354
357
|
end
|
355
358
|
end
|
@@ -30,6 +30,12 @@ module SplitIoClient
|
|
30
30
|
req.headers = common_headers(api_key)
|
31
31
|
.merge('Content-Type' => 'application/json')
|
32
32
|
.merge(headers)
|
33
|
+
|
34
|
+
machine_ip = @config.machine_ip
|
35
|
+
machine_name = @config.machine_name
|
36
|
+
|
37
|
+
req.headers = req.headers.merge('SplitSDKMachineIP' => machine_ip) unless machine_ip.empty? || machine_ip == 'unknown'
|
38
|
+
req.headers = req.headers.merge('SplitSDKMachineName' => machine_name) unless machine_name.empty? || machine_name == 'unknown'
|
33
39
|
|
34
40
|
req.body = data.to_json
|
35
41
|
|
@@ -78,19 +84,8 @@ module SplitIoClient
|
|
78
84
|
{
|
79
85
|
'Authorization' => "Bearer #{api_key}",
|
80
86
|
'SplitSDKVersion' => "#{@config.language}-#{@config.version}",
|
81
|
-
'SplitSDKMachineName' => @config.machine_name,
|
82
|
-
'SplitSDKMachineIP' => @config.machine_ip,
|
83
|
-
'Referer' => referer
|
84
87
|
}
|
85
88
|
end
|
86
|
-
|
87
|
-
def referer
|
88
|
-
result = "#{@config.language}-#{@config.version}"
|
89
|
-
|
90
|
-
result = "#{result}::#{SplitIoClient::SplitConfig.machine_hostname}" unless SplitIoClient::SplitConfig.machine_hostname == 'localhost'
|
91
|
-
|
92
|
-
result
|
93
|
-
end
|
94
89
|
end
|
95
90
|
end
|
96
91
|
end
|
@@ -18,10 +18,7 @@ module SplitIoClient
|
|
18
18
|
response = post_api(
|
19
19
|
"#{@config.events_uri}/events/bulk",
|
20
20
|
@api_key,
|
21
|
-
events_slice.map { |event| formatted_event(event[:e]) }
|
22
|
-
'SplitSDKMachineIP' => events_slice[0][:m][:i],
|
23
|
-
'SplitSDKMachineName' => events_slice[0][:m][:n],
|
24
|
-
'SplitSDKVersion' => events_slice[0][:m][:s]
|
21
|
+
events_slice.map { |event| formatted_event(event[:e]) }
|
25
22
|
)
|
26
23
|
|
27
24
|
if response.success?
|
@@ -42,7 +39,8 @@ module SplitIoClient
|
|
42
39
|
trafficTypeName: event[:trafficTypeName],
|
43
40
|
eventTypeId: event[:eventTypeId],
|
44
41
|
value: event[:value].to_f,
|
45
|
-
timestamp: event[:timestamp].to_i
|
42
|
+
timestamp: event[:timestamp].to_i,
|
43
|
+
properties: event[:properties]
|
46
44
|
}
|
47
45
|
end
|
48
46
|
end
|
@@ -15,7 +15,7 @@ module SplitIoClient
|
|
15
15
|
end
|
16
16
|
|
17
17
|
impressions_by_ip(impressions).each do |ip, impressions_ip|
|
18
|
-
response = post_api("#{@config.events_uri}/testImpressions/bulk", @api_key, impressions_ip
|
18
|
+
response = post_api("#{@config.events_uri}/testImpressions/bulk", @api_key, impressions_ip)
|
19
19
|
|
20
20
|
if response.success?
|
21
21
|
@config.split_logger.log_if_debug("Impressions reported: #{total_impressions(impressions)}")
|
@@ -44,7 +44,9 @@ module SplitIoClient
|
|
44
44
|
@sdk_blocker = sdk_blocker
|
45
45
|
@config = config
|
46
46
|
|
47
|
-
|
47
|
+
start_localhost_components if @config.localhost_mode
|
48
|
+
|
49
|
+
start_standalone_components if @config.standalone? && !@config.localhost_mode
|
48
50
|
end
|
49
51
|
|
50
52
|
def start_standalone_components
|
@@ -55,6 +57,21 @@ module SplitIoClient
|
|
55
57
|
events_sender
|
56
58
|
end
|
57
59
|
|
60
|
+
def start_localhost_components
|
61
|
+
localhost_split_store
|
62
|
+
localhost_repo_cleaner
|
63
|
+
end
|
64
|
+
|
65
|
+
# Starts thread which loops constantly and retrieves splits from a file source
|
66
|
+
def localhost_split_store
|
67
|
+
LocalhostSplitStore.new(@splits_repository, @config, @sdk_blocker).call
|
68
|
+
end
|
69
|
+
|
70
|
+
# Starts thread which loops constantly and cleans up repositories to avoid memory issues in localhost mode
|
71
|
+
def localhost_repo_cleaner
|
72
|
+
LocalhostRepoCleaner.new(@impressions_repository, @metrics_repository, @events_repository, @config).call
|
73
|
+
end
|
74
|
+
|
58
75
|
# Starts thread which loops constantly and stores splits in the splits_repository of choice
|
59
76
|
def split_store
|
60
77
|
SplitStore.new(@splits_repository, @api_key, @metrics, @config, @sdk_blocker).call
|
@@ -3,13 +3,9 @@ module SplitIoClient
|
|
3
3
|
#
|
4
4
|
# Creates a new split manager instance that connects to split.io API.
|
5
5
|
#
|
6
|
-
# @param api_key [String] the API key for your split account
|
7
|
-
#
|
8
6
|
# @return [SplitIoManager] split.io client instance
|
9
|
-
def initialize(
|
10
|
-
@localhost_mode_features = []
|
7
|
+
def initialize(splits_repository = nil, sdk_blocker, config)
|
11
8
|
@splits_repository = splits_repository
|
12
|
-
@adapter = adapter
|
13
9
|
@sdk_blocker = sdk_blocker
|
14
10
|
@config = config
|
15
11
|
end
|
@@ -91,13 +87,18 @@ module SplitIoClient
|
|
91
87
|
return {} unless split
|
92
88
|
|
93
89
|
begin
|
94
|
-
|
95
|
-
|
96
|
-
|
90
|
+
if @config.localhost_mode
|
91
|
+
treatments = split[:conditions]
|
92
|
+
.first[:partitions]
|
93
|
+
.map { |partition| partition[:treatment] }
|
94
|
+
else
|
95
|
+
treatments = split[:conditions]
|
96
|
+
.detect { |c| c[:conditionType] == 'ROLLOUT' }[:partitions]
|
97
|
+
.map { |partition| partition[:treatment] }
|
98
|
+
end
|
97
99
|
rescue StandardError
|
98
100
|
treatments = []
|
99
101
|
end
|
100
|
-
|
101
102
|
{
|
102
103
|
name: name,
|
103
104
|
traffic_type_name: split[:trafficTypeName],
|