prefab-cloud-ruby 0.8.0 → 0.11.0
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/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
|