splitclient-rb 7.0.0.pre.rc3-java → 7.0.1-java
Sign up to get free protection for your applications and to get access to all the features.
- 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],
|