prefab-cloud-ruby 0.8.0 → 0.11.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +1 -0
- data/Gemfile.lock +24 -0
- data/README.md +3 -0
- data/VERSION +1 -1
- data/lib/prefab/client.rb +12 -8
- data/lib/prefab/config_client.rb +51 -19
- data/lib/prefab/config_loader.rb +12 -10
- data/lib/prefab/config_resolver.rb +36 -38
- data/lib/prefab/feature_flag_client.rb +40 -27
- data/lib/prefab-cloud-ruby.rb +2 -0
- data/lib/prefab_pb.rb +47 -44
- data/lib/prefab_services_pb.rb +17 -3
- data/prefab-cloud-ruby.gemspec +5 -3
- data/run_test_harness_server.sh +1 -1
- data/test/harness_server.rb +12 -4
- data/test/test_config_loader.rb +20 -20
- data/test/test_config_resolver.rb +72 -52
- data/test/test_feature_flag_client.rb +195 -108
- data/test/test_helper.rb +42 -6
- data/test/test_logger.rb +0 -10
- metadata +16 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 22d564bcc76424b3308623b4d2e27a4acaf4217c285bcca02542291d8cc15827
|
4
|
+
data.tar.gz: c097169d379d9f477a66d20fc378f693173de044baecad08059263fd239257e5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cdf6ca3b1b28bf83caa629193b21e2d26e4d22d64ee4cd30b7a88fdce623adc7f3feb3605df7107cdca00e7823709f9eeac2a68176db1d10b6616196d2bcb580
|
7
|
+
data.tar.gz: c61b171a2be217c016fad1cc0a1ce9faed159c6f894065db4202ec200ec1f8ac22caabb880fae0f358fcced0930ce49d4e273f800a0fb72c39c6d6f19cc59d07
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -14,12 +14,18 @@ GEM
|
|
14
14
|
descendants_tracker (0.0.4)
|
15
15
|
thread_safe (~> 0.3, >= 0.3.1)
|
16
16
|
docile (1.3.5)
|
17
|
+
domain_name (0.5.20190701)
|
18
|
+
unf (>= 0.0.5, < 1.0.0)
|
17
19
|
eventmachine (1.2.7)
|
18
20
|
faraday (1.3.0)
|
19
21
|
faraday-net_http (~> 1.0)
|
20
22
|
multipart-post (>= 1.2, < 3)
|
21
23
|
ruby2_keywords
|
22
24
|
faraday-net_http (1.0.1)
|
25
|
+
ffi (1.15.5)
|
26
|
+
ffi-compiler (1.0.1)
|
27
|
+
ffi (>= 1.0.0)
|
28
|
+
rake
|
23
29
|
git (1.8.1)
|
24
30
|
rchardet (~> 1.8)
|
25
31
|
github_api (0.19.0)
|
@@ -37,6 +43,14 @@ GEM
|
|
37
43
|
grpc-tools (1.43.1)
|
38
44
|
hashie (3.6.0)
|
39
45
|
highline (2.0.3)
|
46
|
+
http (5.0.1)
|
47
|
+
addressable (~> 2.3)
|
48
|
+
http-cookie (~> 1.0)
|
49
|
+
http-form_data (~> 2.2)
|
50
|
+
llhttp-ffi (~> 0.3.0)
|
51
|
+
http-cookie (1.0.4)
|
52
|
+
domain_name (~> 0.5)
|
53
|
+
http-form_data (2.3.0)
|
40
54
|
i18n (1.8.9)
|
41
55
|
concurrent-ruby (~> 1.0)
|
42
56
|
json (1.8.6)
|
@@ -55,6 +69,12 @@ GEM
|
|
55
69
|
jwt (2.2.2)
|
56
70
|
kamelcase (0.0.2)
|
57
71
|
semver2 (~> 3)
|
72
|
+
ld-eventsource (2.2.0)
|
73
|
+
concurrent-ruby (~> 1.0)
|
74
|
+
http (>= 4.4.1, < 6.0.0)
|
75
|
+
llhttp-ffi (0.3.1)
|
76
|
+
ffi-compiler (~> 1.0)
|
77
|
+
rake (~> 13.0)
|
58
78
|
mini_portile2 (2.7.1)
|
59
79
|
minitest (5.14.4)
|
60
80
|
multi_json (1.15.0)
|
@@ -96,6 +116,9 @@ GEM
|
|
96
116
|
thread_safe (0.3.6)
|
97
117
|
tzinfo (1.2.9)
|
98
118
|
thread_safe (~> 0.1)
|
119
|
+
unf (0.1.4)
|
120
|
+
unf_ext
|
121
|
+
unf_ext (0.0.8)
|
99
122
|
|
100
123
|
PLATFORMS
|
101
124
|
ruby
|
@@ -109,6 +132,7 @@ DEPENDENCIES
|
|
109
132
|
grpc
|
110
133
|
grpc-tools
|
111
134
|
juwelier (~> 2.4.9)
|
135
|
+
ld-eventsource
|
112
136
|
rdoc (~> 3.12)
|
113
137
|
shoulda
|
114
138
|
simplecov
|
data/README.md
CHANGED
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.11.0
|
data/lib/prefab/client.rb
CHANGED
@@ -8,7 +8,7 @@ module Prefab
|
|
8
8
|
}
|
9
9
|
|
10
10
|
|
11
|
-
attr_reader :project_id, :shared_cache, :stats, :namespace, :interceptor, :api_key, :
|
11
|
+
attr_reader :project_id, :shared_cache, :stats, :namespace, :interceptor, :api_key, :prefab_api_url
|
12
12
|
|
13
13
|
def initialize(api_key: ENV['PREFAB_API_KEY'],
|
14
14
|
logdev: nil,
|
@@ -26,22 +26,22 @@ module Prefab
|
|
26
26
|
@stats = (stats || NoopStats.new)
|
27
27
|
@shared_cache = (shared_cache || NoopCache.new)
|
28
28
|
@api_key = api_key
|
29
|
-
@project_id = api_key.split("-")[0].to_i
|
30
|
-
@environment = api_key.split("-")[1]
|
29
|
+
@project_id = api_key.split("-")[0].to_i # unvalidated, but that's ok. APIs only listen to the actual passwd
|
31
30
|
@namespace = namespace
|
32
31
|
@interceptor = Prefab::AuthInterceptor.new(api_key)
|
33
32
|
@stubs = {}
|
34
|
-
|
33
|
+
@prefab_api_url = ENV["PREFAB_API_URL"] || 'https://api.prefab.cloud'
|
34
|
+
@prefab_grpc_url = ENV["PREFAB_GRPC_URL"] || 'grpc.prefab.cloud:443'
|
35
|
+
log_internal Logger::INFO, "Prefab Connecting to: #{@prefab_api_url} and #{@prefab_grpc_url} Secure: #{http_secure?}"
|
35
36
|
at_exit do
|
36
37
|
channel.destroy
|
37
38
|
end
|
38
39
|
end
|
39
40
|
|
40
41
|
def channel
|
41
|
-
credentials =
|
42
|
-
|
43
|
-
|
44
|
-
@_channel ||= GRPC::Core::Channel.new(url, nil, credentials)
|
42
|
+
credentials = http_secure? ? creds : :this_channel_is_insecure
|
43
|
+
log_internal Logger::DEBUG, "GRPC Channel #{@prefab_grpc_url} #{credentials}"
|
44
|
+
@_channel ||= GRPC::Core::Channel.new(@prefab_grpc_url, nil, credentials)
|
45
45
|
end
|
46
46
|
|
47
47
|
def config_client(timeout: 5.0)
|
@@ -101,6 +101,10 @@ module Prefab
|
|
101
101
|
|
102
102
|
private
|
103
103
|
|
104
|
+
def http_secure?
|
105
|
+
ENV["PREFAB_CLOUD_HTTP"] != "true"
|
106
|
+
end
|
107
|
+
|
104
108
|
def stub_for(service, timeout)
|
105
109
|
@stubs["#{service}_#{timeout}"] ||= service::Stub.new(nil,
|
106
110
|
nil,
|
data/lib/prefab/config_client.rb
CHANGED
@@ -29,7 +29,8 @@ module Prefab
|
|
29
29
|
|
30
30
|
def start_streaming
|
31
31
|
@streaming = true
|
32
|
-
|
32
|
+
# start_grpc_streaming_connection_thread(@config_loader.highwater_mark)
|
33
|
+
start_sse_streaming_connection_thread(@config_loader.highwater_mark)
|
33
34
|
end
|
34
35
|
|
35
36
|
def get(key)
|
@@ -62,8 +63,14 @@ module Prefab
|
|
62
63
|
end
|
63
64
|
|
64
65
|
def self.value_to_delta(key, config_value, namespace = nil)
|
65
|
-
Prefab::
|
66
|
-
|
66
|
+
Prefab::Config.new(key: [namespace, key].compact.join(":"),
|
67
|
+
rows: [Prefab::ConfigRow.new(value: config_value)])
|
68
|
+
end
|
69
|
+
|
70
|
+
def get_config_obj(key)
|
71
|
+
@initialization_lock.with_read_lock do
|
72
|
+
@config_resolver.get_config(key)
|
73
|
+
end
|
67
74
|
end
|
68
75
|
|
69
76
|
private
|
@@ -92,14 +99,11 @@ module Prefab
|
|
92
99
|
def load_checkpoint_from_config
|
93
100
|
@base_client.log_internal Logger::DEBUG, "Load Checkpoint From Config"
|
94
101
|
|
95
|
-
config_req = Prefab::ConfigServicePointer.new(
|
96
|
-
|
102
|
+
config_req = Prefab::ConfigServicePointer.new(start_at_id: @config_loader.highwater_mark)
|
103
|
+
|
97
104
|
resp = stub.get_all_config(config_req)
|
98
105
|
@base_client.log_internal Logger::DEBUG, "Got Response #{resp}"
|
99
|
-
|
100
|
-
resp.deltas.each do |delta|
|
101
|
-
@config_loader.set(delta)
|
102
|
-
end
|
106
|
+
load_configs(resp, :api)
|
103
107
|
@config_resolver.update
|
104
108
|
finish_init!(:api)
|
105
109
|
true
|
@@ -112,16 +116,20 @@ module Prefab
|
|
112
116
|
url = "#{@s3_cloud_front}/#{@base_client.api_key.gsub("|", "/")}"
|
113
117
|
resp = Faraday.get url
|
114
118
|
if resp.status == 200
|
115
|
-
|
116
|
-
|
119
|
+
configs = Prefab::Configs.decode(resp.body)
|
120
|
+
load_configs(configs, :s3)
|
117
121
|
else
|
118
122
|
@base_client.log_internal Logger::INFO, "No S3 checkpoint. Response #{resp.status} Plan may not support this."
|
119
123
|
end
|
120
124
|
end
|
121
125
|
|
122
|
-
def
|
123
|
-
|
124
|
-
|
126
|
+
def load_configs(configs, source)
|
127
|
+
project_env_id = configs.config_service_pointer.project_env_id
|
128
|
+
@config_resolver.project_env_id = project_env_id
|
129
|
+
|
130
|
+
@base_client.log_internal Logger::INFO, "Prefab Initializing in project: #{@base_client.project_id} environment: #{project_env_id} and namespace: '#{@namespace}'"
|
131
|
+
configs.configs.each do |config|
|
132
|
+
@config_loader.set(config)
|
125
133
|
end
|
126
134
|
@base_client.log_internal Logger::INFO, "Found checkpoint with highwater id #{@config_loader.highwater_mark} from #{source}"
|
127
135
|
@base_client.stats.increment("prefab.config.checkpoint.load")
|
@@ -156,11 +164,35 @@ module Prefab
|
|
156
164
|
end
|
157
165
|
end
|
158
166
|
|
167
|
+
|
168
|
+
def start_sse_streaming_connection_thread(start_at_id)
|
169
|
+
auth = "#{@base_client.project_id}:#{@base_client.api_key}"
|
170
|
+
|
171
|
+
auth_string = Base64.strict_encode64(auth)
|
172
|
+
headers = {
|
173
|
+
"x-prefab-start-at-id": start_at_id,
|
174
|
+
"Authorization": "Basic #{auth_string}",
|
175
|
+
}
|
176
|
+
url = "#{@base_client.prefab_api_url}/api/v1/sse/config"
|
177
|
+
@base_client.log_internal Logger::INFO, "SSE Streaming Connect to #{url}"
|
178
|
+
SSE::Client.new(url, headers: headers) do |client|
|
179
|
+
client.on_event do |event|
|
180
|
+
configs = Prefab::Configs.decode(Base64.decode64(event.data))
|
181
|
+
@base_client.log_internal Logger::INFO, "SSE received configs."
|
182
|
+
@base_client.log_internal Logger::DEBUG, "SSE received configs: #{configs}"
|
183
|
+
configs.configs.each do |config|
|
184
|
+
@config_loader.set(config)
|
185
|
+
end
|
186
|
+
@config_resolver.update
|
187
|
+
finish_init!(:streaming)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
159
192
|
# Setup a streaming connection to the API
|
160
193
|
# Save new config values into the loader
|
161
|
-
def
|
162
|
-
config_req = Prefab::ConfigServicePointer.new(
|
163
|
-
start_at_id: start_at_id)
|
194
|
+
def start_grpc_streaming_connection_thread(start_at_id)
|
195
|
+
config_req = Prefab::ConfigServicePointer.new(start_at_id: start_at_id)
|
164
196
|
@base_client.log_internal Logger::DEBUG, "start api connection thread #{start_at_id}"
|
165
197
|
@base_client.stats.increment("prefab.config.api.start")
|
166
198
|
|
@@ -174,8 +206,8 @@ module Prefab
|
|
174
206
|
begin
|
175
207
|
resp = stub.get_config(config_req)
|
176
208
|
resp.each do |r|
|
177
|
-
r.
|
178
|
-
@config_loader.set(
|
209
|
+
r.configs.each do |config|
|
210
|
+
@config_loader.set(config)
|
179
211
|
end
|
180
212
|
@config_resolver.update
|
181
213
|
finish_init!(:streaming)
|
data/lib/prefab/config_loader.rb
CHANGED
@@ -20,18 +20,18 @@ module Prefab
|
|
20
20
|
rtn
|
21
21
|
end
|
22
22
|
|
23
|
-
def set(
|
23
|
+
def set(config)
|
24
24
|
# don't overwrite newer values
|
25
|
-
if @api_config[
|
25
|
+
if @api_config[config.key] && @api_config[config.key].id > config.id
|
26
26
|
return
|
27
27
|
end
|
28
28
|
|
29
|
-
if
|
30
|
-
@api_config.delete(
|
29
|
+
if config.rows.empty?
|
30
|
+
@api_config.delete(config.key)
|
31
31
|
else
|
32
|
-
@api_config[
|
32
|
+
@api_config[config.key] = config
|
33
33
|
end
|
34
|
-
@highwater_mark = [
|
34
|
+
@highwater_mark = [config.id, @highwater_mark].max
|
35
35
|
end
|
36
36
|
|
37
37
|
def rm(key)
|
@@ -39,11 +39,11 @@ module Prefab
|
|
39
39
|
end
|
40
40
|
|
41
41
|
def get_api_deltas
|
42
|
-
|
42
|
+
configs = Prefab::Configs.new
|
43
43
|
@api_config.each_value do |config_value|
|
44
|
-
|
44
|
+
configs.configs << config_value
|
45
45
|
end
|
46
|
-
|
46
|
+
configs
|
47
47
|
end
|
48
48
|
|
49
49
|
private
|
@@ -63,7 +63,9 @@ module Prefab
|
|
63
63
|
Dir.glob(glob).each do |file|
|
64
64
|
yaml = load(file)
|
65
65
|
yaml.each do |k, v|
|
66
|
-
rtn[k] = Prefab::
|
66
|
+
rtn[k] = Prefab::Config.new(key: k, rows: [
|
67
|
+
Prefab::ConfigRow.new(value: Prefab::ConfigValue.new(value_from(v)))
|
68
|
+
])
|
67
69
|
end
|
68
70
|
end
|
69
71
|
rtn
|
@@ -3,12 +3,14 @@ module Prefab
|
|
3
3
|
include Prefab::ConfigHelper
|
4
4
|
NAMESPACE_DELIMITER = ".".freeze
|
5
5
|
|
6
|
+
attr_accessor :project_env_id # this will be set by the config_client when it gets an API response
|
7
|
+
|
6
8
|
def initialize(base_client, config_loader)
|
7
9
|
@lock = Concurrent::ReadWriteLock.new
|
8
10
|
@local_store = {}
|
9
|
-
@environment = base_client.environment
|
10
11
|
@namespace = base_client.namespace
|
11
12
|
@config_loader = config_loader
|
13
|
+
@project_env_id = 0
|
12
14
|
make_local
|
13
15
|
end
|
14
16
|
|
@@ -16,20 +18,27 @@ module Prefab
|
|
16
18
|
str = ""
|
17
19
|
@lock.with_read_lock do
|
18
20
|
@local_store.each do |k, v|
|
19
|
-
|
20
|
-
|
21
|
+
if v.nil?
|
22
|
+
str<< "|#{k}| tombstone\n"
|
23
|
+
else
|
24
|
+
value = v[:value]
|
25
|
+
str << "|#{k}| from #{v[:match]} |#{value_of(value)}|#{value_of(value).class}\n"
|
26
|
+
end
|
21
27
|
end
|
22
28
|
end
|
23
29
|
str
|
24
30
|
end
|
25
31
|
|
26
32
|
def get(property)
|
27
|
-
config =
|
28
|
-
@local_store[property]
|
29
|
-
end
|
33
|
+
config = _get(property)
|
30
34
|
config ? value_of(config[:value]) : nil
|
31
35
|
end
|
32
36
|
|
37
|
+
def get_config(property)
|
38
|
+
config = _get(property)
|
39
|
+
config ? config[:config] : nil
|
40
|
+
end
|
41
|
+
|
33
42
|
def update
|
34
43
|
make_local
|
35
44
|
end
|
@@ -55,46 +64,35 @@ module Prefab
|
|
55
64
|
|
56
65
|
def make_local
|
57
66
|
store = {}
|
58
|
-
@config_loader.calc_config.each do |key,
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
# override the top level default with env default
|
69
|
-
to_store = { match: "env_default", env: env_value.environment, value: env_value.default }
|
70
|
-
|
71
|
-
if env_value.namespace_values.any?
|
72
|
-
# check all namespace_values for match
|
73
|
-
env_value.namespace_values.each do |namespace_value|
|
74
|
-
(starts_with, count) = starts_with_ns?(namespace_value.namespace, @namespace)
|
75
|
-
if starts_with
|
76
|
-
# is this match the best match?
|
77
|
-
if count > (to_store[:match_depth_count] || 0)
|
78
|
-
to_store = { match: namespace_value.namespace, count: count, value: namespace_value.config_value }
|
79
|
-
end
|
80
|
-
end
|
67
|
+
@config_loader.calc_config.each do |key, config|
|
68
|
+
sortable = config.rows.map do |row|
|
69
|
+
if row.project_env_id != 0
|
70
|
+
if row.project_env_id == @project_env_id
|
71
|
+
if !row.namespace.empty?
|
72
|
+
(starts_with, count) = starts_with_ns?(row.namespace, @namespace)
|
73
|
+
# rubocop:disable BlockNesting
|
74
|
+
{ sortable: 2 + count, match: row.namespace, value: row.value, config: config} if starts_with
|
75
|
+
else
|
76
|
+
{ sortable: 1, match: row.project_env_id, value: row.value, config: config}
|
81
77
|
end
|
82
78
|
end
|
79
|
+
else
|
80
|
+
{ sortable: 0, match: "default", value: row.value, config: config}
|
83
81
|
end
|
84
|
-
end
|
85
|
-
|
86
|
-
# feature flags are a funny case
|
87
|
-
# we only define the variants in the default in order to be DRY
|
88
|
-
# but we want to access them in environments, clone them over
|
89
|
-
if to_store[:value].type == :feature_flag
|
90
|
-
to_store[:value].feature_flag.variants = delta.default.feature_flag.variants
|
91
|
-
end
|
92
|
-
|
82
|
+
end.compact
|
83
|
+
to_store = sortable.sort_by { |h| h[:sortable] }.last
|
93
84
|
store[key] = to_store
|
94
85
|
end
|
86
|
+
|
95
87
|
@lock.with_write_lock do
|
96
88
|
@local_store = store
|
97
89
|
end
|
98
90
|
end
|
91
|
+
|
92
|
+
def _get(property)
|
93
|
+
@lock.with_read_lock do
|
94
|
+
@local_store[property]
|
95
|
+
end
|
96
|
+
end
|
99
97
|
end
|
100
98
|
end
|
@@ -2,7 +2,6 @@ module Prefab
|
|
2
2
|
class FeatureFlagClient
|
3
3
|
include Prefab::ConfigHelper
|
4
4
|
MAX_32_FLOAT = 4294967294.0
|
5
|
-
DISTRIBUTION_SPACE = 1000
|
6
5
|
|
7
6
|
def initialize(base_client)
|
8
7
|
@base_client = base_client
|
@@ -22,13 +21,16 @@ module Prefab
|
|
22
21
|
return is_on?(get(feature_name, lookup_key, attributes))
|
23
22
|
end
|
24
23
|
|
25
|
-
def get(feature_name, lookup_key, attributes)
|
24
|
+
def get(feature_name, lookup_key=nil, attributes={})
|
26
25
|
feature_obj = @base_client.config_client.get(feature_name)
|
27
|
-
|
26
|
+
config_obj = @base_client.config_client.get_config_obj(feature_name)
|
27
|
+
return nil if feature_obj.nil? || config_obj.nil?
|
28
|
+
variants = config_obj.variants
|
29
|
+
evaluate(feature_name, lookup_key, attributes, feature_obj, variants)
|
28
30
|
end
|
29
31
|
|
30
|
-
def evaluate(feature_name, lookup_key, attributes, feature_obj)
|
31
|
-
value_of(get_variant(feature_name, lookup_key, attributes, feature_obj))
|
32
|
+
def evaluate(feature_name, lookup_key, attributes, feature_obj, variants)
|
33
|
+
value_of(get_variant(feature_name, lookup_key, attributes, feature_obj, variants))
|
32
34
|
end
|
33
35
|
|
34
36
|
private
|
@@ -38,50 +40,54 @@ module Prefab
|
|
38
40
|
return false
|
39
41
|
end
|
40
42
|
variant.bool
|
43
|
+
rescue
|
44
|
+
@base_client.log.info("is_on? methods only work for boolean feature flags variants. This feature flags variant is '#{variant}'. Returning false")
|
45
|
+
false
|
41
46
|
end
|
42
47
|
|
43
|
-
def get_variant(feature_name, lookup_key, attributes, feature_obj)
|
48
|
+
def get_variant(feature_name, lookup_key, attributes, feature_obj, variants)
|
44
49
|
if !feature_obj.active
|
45
|
-
return get_variant_obj(
|
50
|
+
return get_variant_obj(variants, feature_obj.inactive_variant_idx)
|
46
51
|
end
|
47
52
|
|
48
|
-
variant_distribution = feature_obj.default
|
49
|
-
|
50
53
|
# if user_targets.match
|
51
54
|
feature_obj.user_targets.each do |target|
|
52
55
|
if (target.identifiers.include? lookup_key)
|
53
|
-
return get_variant_obj(
|
56
|
+
return get_variant_obj(variants, target.variant_idx)
|
54
57
|
end
|
55
58
|
end
|
56
59
|
|
60
|
+
#default to inactive
|
61
|
+
variant_weights = [Prefab::VariantWeight.new(variant_idx: feature_obj.inactive_variant_idx, weight: 1)]
|
62
|
+
|
57
63
|
# if rules.match
|
58
64
|
feature_obj.rules.each do |rule|
|
59
65
|
if criteria_match?(rule, lookup_key, attributes)
|
60
|
-
|
66
|
+
variant_weights = rule.variant_weights
|
67
|
+
break
|
61
68
|
end
|
62
69
|
end
|
63
70
|
|
64
|
-
if variant_distribution.type == :variant_idx
|
65
|
-
variant_idx = variant_distribution.variant_idx
|
66
|
-
else
|
67
|
-
percent_through_distribution = rand()
|
68
|
-
if lookup_key
|
69
|
-
percent_through_distribution = get_user_pct(feature_name, lookup_key)
|
70
|
-
end
|
71
|
-
distribution_bucket = DISTRIBUTION_SPACE * percent_through_distribution
|
72
71
|
|
73
|
-
|
72
|
+
percent_through_distribution = rand()
|
73
|
+
if lookup_key
|
74
|
+
percent_through_distribution = get_user_pct(feature_name, lookup_key)
|
74
75
|
end
|
75
76
|
|
76
|
-
|
77
|
+
variant_idx = get_variant_idx_from_weights(variant_weights, percent_through_distribution, feature_name)
|
78
|
+
|
79
|
+
return get_variant_obj(variants, variant_idx)
|
77
80
|
end
|
78
81
|
|
79
|
-
def get_variant_obj(
|
80
|
-
|
82
|
+
def get_variant_obj(variants, idx)
|
83
|
+
# our array is 0 based, but the idx are 1 based so the protos are clearly set
|
84
|
+
return variants[idx - 1] if variants.length >= idx
|
81
85
|
nil
|
82
86
|
end
|
83
87
|
|
84
|
-
def get_variant_idx_from_weights(variant_weights,
|
88
|
+
def get_variant_idx_from_weights(variant_weights, percent_through_distribution, feature_name)
|
89
|
+
distrubution_space = variant_weights.inject(0) { |sum, v| sum + v.weight }
|
90
|
+
bucket = distrubution_space * percent_through_distribution
|
85
91
|
sum = 0
|
86
92
|
variant_weights.each do |variant_weight|
|
87
93
|
if bucket < sum + variant_weight.weight
|
@@ -92,7 +98,7 @@ module Prefab
|
|
92
98
|
end
|
93
99
|
# variants didn't add up to 100%
|
94
100
|
@base_client.log.info("Variants of #{feature_name} did not add to 100%")
|
95
|
-
return variant_weights.last.
|
101
|
+
return variant_weights.last.variant_idx
|
96
102
|
end
|
97
103
|
|
98
104
|
def get_user_pct(feature, lookup_key)
|
@@ -102,14 +108,21 @@ module Prefab
|
|
102
108
|
end
|
103
109
|
|
104
110
|
def criteria_match?(rule, lookup_key, attributes)
|
105
|
-
|
111
|
+
|
112
|
+
if rule.criteria.operator == :ALWAYS_TRUE
|
113
|
+
return true
|
114
|
+
elsif rule.criteria.operator == :LOOKUP_KEY_IN
|
106
115
|
return rule.criteria.values.include?(lookup_key)
|
107
|
-
elsif rule.criteria.operator == :
|
116
|
+
elsif rule.criteria.operator == :LOOKUP_KEY_NOT_IN
|
108
117
|
return !rule.criteria.values.include?(lookup_key)
|
109
118
|
elsif rule.criteria.operator == :IN_SEG
|
110
119
|
return segment_matches(rule.criteria.values, lookup_key, attributes).any?
|
111
120
|
elsif rule.criteria.operator == :NOT_IN_SEG
|
112
121
|
return segment_matches(rule.criteria.values, lookup_key, attributes).none?
|
122
|
+
elsif rule.criteria.operator == :PROP_IS_ONE_OF
|
123
|
+
return rule.criteria.values.include?(attributes[rule.criteria.property]) || rule.criteria.values.include?(attributes[rule.criteria.property.to_sym])
|
124
|
+
elsif rule.criteria.operator == :PROP_IS_NOT_ONE_OF
|
125
|
+
return !(rule.criteria.values.include?(attributes[rule.criteria.property]) || rule.criteria.values.include?(attributes[rule.criteria.property.to_sym]))
|
113
126
|
end
|
114
127
|
@base_client.log.info("Unknown Operator")
|
115
128
|
false
|