prefab-cloud-ruby 0.6.0 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.tool-versions +1 -0
- data/CODEOWNERS +1 -0
- data/Gemfile +6 -3
- data/Gemfile.lock +22 -11
- data/README.md +1 -1
- data/VERSION +1 -1
- data/compile_protos.sh +3 -0
- data/lib/prefab/client.rb +7 -4
- data/lib/prefab/config_client.rb +11 -5
- data/lib/prefab/config_helper.rb +20 -0
- data/lib/prefab/config_loader.rb +3 -3
- data/lib/prefab/config_resolver.rb +38 -34
- data/lib/prefab/feature_flag_client.rb +99 -11
- data/lib/prefab-cloud-ruby.rb +1 -0
- data/lib/prefab_pb.rb +206 -132
- data/lib/prefab_services_pb.rb +6 -6
- data/prefab-cloud-ruby.gemspec +29 -29
- data/run_test_harness_server.sh +2 -0
- data/test/harness_server.rb +46 -0
- data/test/test_config_loader.rb +12 -12
- data/test/test_config_resolver.rb +107 -37
- data/test/test_feature_flag_client.rb +191 -35
- data/test/test_helper.rb +13 -3
- metadata +63 -18
- data/.ruby-version +0 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: a36d24a94355b3b7a2b3b889734e1f6f6ccafc7dcebfb11ff11d77ba92ea7828
|
4
|
+
data.tar.gz: 326b3166d4fc2589ebb2550114b2cc198617620daa70ecfb6b9fe394c9dd79a5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fb73839190f23e05540688572b8f47c1300ae9181bdecef0f24fdecb9a9e73ace1c88937e7c228601c20ff68405876c21b977e45bd691ba33f59d71dd4811718
|
7
|
+
data.tar.gz: 792f04aa032c2f9975444a191ddbb8f727e256d12ec2fc4b3c26819bff4a425deabffb3ff262221e7fe90ed4c1d1422d7a2cb447530264cde494b4e149de7e78
|
data/.tool-versions
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby 3.0.3
|
data/CODEOWNERS
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
* @prefab-cloud/prefabdevs @prefab-cloud/prefabmaintainers @prefab-cloud/prefabadmins
|
data/Gemfile
CHANGED
@@ -2,13 +2,16 @@ source "https://rubygems.org"
|
|
2
2
|
|
3
3
|
gem 'concurrent-ruby', '~> 1.0', '>= 1.0.5'
|
4
4
|
gem 'faraday'
|
5
|
-
gem 'grpc'
|
5
|
+
gem 'grpc', :platforms => :ruby
|
6
|
+
gem 'google-protobuf', :platforms => :ruby
|
7
|
+
gem 'googleapis-common-protos-types', :platforms => :ruby
|
6
8
|
|
7
9
|
group :development do
|
8
|
-
gem 'grpc-tools',
|
10
|
+
gem 'grpc-tools', :platforms => :ruby
|
9
11
|
gem "shoulda", ">= 0"
|
10
12
|
gem "rdoc", "~> 3.12"
|
11
|
-
gem "bundler"
|
13
|
+
gem "bundler"
|
12
14
|
gem "juwelier", "~> 2.4.9"
|
13
15
|
gem "simplecov", ">= 0"
|
16
|
+
gem 'thin'
|
14
17
|
end
|
data/Gemfile.lock
CHANGED
@@ -10,9 +10,11 @@ GEM
|
|
10
10
|
public_suffix (>= 2.0.2, < 5.0)
|
11
11
|
builder (3.2.4)
|
12
12
|
concurrent-ruby (1.1.8)
|
13
|
+
daemons (1.4.1)
|
13
14
|
descendants_tracker (0.0.4)
|
14
15
|
thread_safe (~> 0.3, >= 0.3.1)
|
15
16
|
docile (1.3.5)
|
17
|
+
eventmachine (1.2.7)
|
16
18
|
faraday (1.3.0)
|
17
19
|
faraday-net_http (~> 1.0)
|
18
20
|
multipart-post (>= 1.2, < 3)
|
@@ -26,13 +28,13 @@ GEM
|
|
26
28
|
faraday (>= 0.8, < 2)
|
27
29
|
hashie (~> 3.5, >= 3.5.2)
|
28
30
|
oauth2 (~> 1.0)
|
29
|
-
google-protobuf (3.
|
30
|
-
googleapis-common-protos-types (1.0
|
31
|
-
google-protobuf (~> 3.14)
|
32
|
-
grpc (1.36.0)
|
31
|
+
google-protobuf (3.19.3)
|
32
|
+
googleapis-common-protos-types (1.3.0)
|
33
33
|
google-protobuf (~> 3.14)
|
34
|
+
grpc (1.43.1)
|
35
|
+
google-protobuf (~> 3.18)
|
34
36
|
googleapis-common-protos-types (~> 1.0)
|
35
|
-
grpc-tools (1.
|
37
|
+
grpc-tools (1.43.1)
|
36
38
|
hashie (3.6.0)
|
37
39
|
highline (2.0.3)
|
38
40
|
i18n (1.8.9)
|
@@ -53,13 +55,14 @@ GEM
|
|
53
55
|
jwt (2.2.2)
|
54
56
|
kamelcase (0.0.2)
|
55
57
|
semver2 (~> 3)
|
56
|
-
mini_portile2 (2.
|
58
|
+
mini_portile2 (2.7.1)
|
57
59
|
minitest (5.14.4)
|
58
60
|
multi_json (1.15.0)
|
59
61
|
multi_xml (0.6.0)
|
60
62
|
multipart-post (2.1.1)
|
61
|
-
nokogiri (1.
|
62
|
-
mini_portile2 (~> 2.
|
63
|
+
nokogiri (1.13.1)
|
64
|
+
mini_portile2 (~> 2.7.0)
|
65
|
+
racc (~> 1.4)
|
63
66
|
oauth2 (1.4.7)
|
64
67
|
faraday (>= 0.8, < 2.0)
|
65
68
|
jwt (>= 1.0, < 3.0)
|
@@ -68,6 +71,7 @@ GEM
|
|
68
71
|
rack (>= 1.2, < 3)
|
69
72
|
psych (3.3.1)
|
70
73
|
public_suffix (4.0.6)
|
74
|
+
racc (1.6.0)
|
71
75
|
rack (2.2.3)
|
72
76
|
rake (13.0.3)
|
73
77
|
rchardet (1.8.0)
|
@@ -85,6 +89,10 @@ GEM
|
|
85
89
|
docile (~> 1.1)
|
86
90
|
simplecov-html (~> 0.11)
|
87
91
|
simplecov-html (0.12.3)
|
92
|
+
thin (1.8.1)
|
93
|
+
daemons (~> 1.0, >= 1.0.9)
|
94
|
+
eventmachine (~> 1.0, >= 1.0.4)
|
95
|
+
rack (>= 1, < 3)
|
88
96
|
thread_safe (0.3.6)
|
89
97
|
tzinfo (1.2.9)
|
90
98
|
thread_safe (~> 0.1)
|
@@ -93,15 +101,18 @@ PLATFORMS
|
|
93
101
|
ruby
|
94
102
|
|
95
103
|
DEPENDENCIES
|
96
|
-
bundler
|
104
|
+
bundler
|
97
105
|
concurrent-ruby (~> 1.0, >= 1.0.5)
|
98
106
|
faraday
|
107
|
+
google-protobuf
|
108
|
+
googleapis-common-protos-types
|
99
109
|
grpc
|
100
|
-
grpc-tools
|
110
|
+
grpc-tools
|
101
111
|
juwelier (~> 2.4.9)
|
102
112
|
rdoc (~> 3.12)
|
103
113
|
shoulda
|
104
114
|
simplecov
|
115
|
+
thin
|
105
116
|
|
106
117
|
BUNDLED WITH
|
107
|
-
|
118
|
+
2.3.5
|
data/README.md
CHANGED
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.7.0
|
data/compile_protos.sh
CHANGED
data/lib/prefab/client.rb
CHANGED
@@ -8,7 +8,7 @@ module Prefab
|
|
8
8
|
}
|
9
9
|
|
10
10
|
|
11
|
-
attr_reader :
|
11
|
+
attr_reader :project_id, :shared_cache, :stats, :namespace, :interceptor, :api_key, :environment
|
12
12
|
|
13
13
|
def initialize(api_key: ENV['PREFAB_API_KEY'],
|
14
14
|
logdev: nil,
|
@@ -19,13 +19,15 @@ module Prefab
|
|
19
19
|
log_formatter: DEFAULT_LOG_FORMATTER
|
20
20
|
)
|
21
21
|
raise "No API key. Set PREFAB_API_KEY env var" if api_key.nil? || api_key.empty?
|
22
|
+
raise "PREFAB_API_KEY format invalid. Expecting 123-development-yourapikey" unless api_key.count("-") == 2
|
22
23
|
@logdev = (logdev || $stdout)
|
23
24
|
@log_formatter = log_formatter
|
24
25
|
@local = local
|
25
26
|
@stats = (stats || NoopStats.new)
|
26
27
|
@shared_cache = (shared_cache || NoopCache.new)
|
27
28
|
@api_key = api_key
|
28
|
-
@
|
29
|
+
@project_id = api_key.split("-")[0].to_i
|
30
|
+
@environment = api_key.split("-")[1]
|
29
31
|
@namespace = namespace
|
30
32
|
@interceptor = Prefab::AuthInterceptor.new(api_key)
|
31
33
|
@stubs = {}
|
@@ -37,7 +39,8 @@ module Prefab
|
|
37
39
|
|
38
40
|
def channel
|
39
41
|
credentials = ENV["PREFAB_CLOUD_HTTP"] == "true" ? :this_channel_is_insecure : creds
|
40
|
-
url = ENV["PREFAB_API_URL"] || '
|
42
|
+
url = ENV["PREFAB_API_URL"] || 'grpc.prefab.cloud:443'
|
43
|
+
log_internal Logger::DEBUG, "GRPC Channel #{url} #{credentials}"
|
41
44
|
@_channel ||= GRPC::Core::Channel.new(url, nil, credentials)
|
42
45
|
end
|
43
46
|
|
@@ -88,7 +91,7 @@ module Prefab
|
|
88
91
|
end
|
89
92
|
|
90
93
|
def cache_key(post_fix)
|
91
|
-
"prefab:#{
|
94
|
+
"prefab:#{project_id}:#{post_fix}"
|
92
95
|
end
|
93
96
|
|
94
97
|
def reset!
|
data/lib/prefab/config_client.rb
CHANGED
@@ -6,6 +6,7 @@ module Prefab
|
|
6
6
|
|
7
7
|
def initialize(base_client, timeout)
|
8
8
|
@base_client = base_client
|
9
|
+
@base_client.log_internal Logger::DEBUG, "Initialize ConfigClient"
|
9
10
|
@timeout = timeout
|
10
11
|
@initialization_lock = Concurrent::ReadWriteLock.new
|
11
12
|
|
@@ -14,11 +15,14 @@ module Prefab
|
|
14
15
|
@config_loader = Prefab::ConfigLoader.new(@base_client)
|
15
16
|
@config_resolver = Prefab::ConfigResolver.new(@base_client, @config_loader)
|
16
17
|
|
18
|
+
@base_client.log_internal Logger::DEBUG, "Initialize ConfigClient: AcquireWriteLock"
|
17
19
|
@initialization_lock.acquire_write_lock
|
20
|
+
@base_client.log_internal Logger::DEBUG, "Initialize ConfigClient: AcquiredWriteLock"
|
18
21
|
|
19
22
|
@cancellable_interceptor = Prefab::CancellableInterceptor.new(@base_client)
|
20
23
|
|
21
24
|
@s3_cloud_front = ENV["PREFAB_S3CF_BUCKET"] || DEFAULT_S3CF_BUCKET
|
25
|
+
|
22
26
|
load_checkpoint
|
23
27
|
start_checkpointing_thread
|
24
28
|
end
|
@@ -28,9 +32,9 @@ module Prefab
|
|
28
32
|
start_api_connection_thread(@config_loader.highwater_mark)
|
29
33
|
end
|
30
34
|
|
31
|
-
def get(
|
35
|
+
def get(key)
|
32
36
|
@initialization_lock.with_read_lock do
|
33
|
-
@config_resolver.get(
|
37
|
+
@config_resolver.get(key)
|
34
38
|
end
|
35
39
|
end
|
36
40
|
|
@@ -86,9 +90,12 @@ module Prefab
|
|
86
90
|
end
|
87
91
|
|
88
92
|
def load_checkpoint_from_config
|
89
|
-
|
93
|
+
@base_client.log_internal Logger::DEBUG, "Load Checkpoint From Config"
|
94
|
+
|
95
|
+
config_req = Prefab::ConfigServicePointer.new(project_id: @base_client.project_id,
|
90
96
|
start_at_id: @config_loader.highwater_mark)
|
91
97
|
resp = stub.get_all_config(config_req)
|
98
|
+
@base_client.log_internal Logger::DEBUG, "Got Response #{resp}"
|
92
99
|
load_deltas(resp, :api)
|
93
100
|
resp.deltas.each do |delta|
|
94
101
|
@config_loader.set(delta)
|
@@ -112,7 +119,6 @@ module Prefab
|
|
112
119
|
end
|
113
120
|
end
|
114
121
|
|
115
|
-
|
116
122
|
def load_deltas(deltas, source)
|
117
123
|
deltas.deltas.each do |delta|
|
118
124
|
@config_loader.set(delta)
|
@@ -153,7 +159,7 @@ module Prefab
|
|
153
159
|
# Setup a streaming connection to the API
|
154
160
|
# Save new config values into the loader
|
155
161
|
def start_api_connection_thread(start_at_id)
|
156
|
-
config_req = Prefab::ConfigServicePointer.new(
|
162
|
+
config_req = Prefab::ConfigServicePointer.new(project_id: @base_client.project_id,
|
157
163
|
start_at_id: start_at_id)
|
158
164
|
@base_client.log_internal Logger::DEBUG, "start api connection thread #{start_at_id}"
|
159
165
|
@base_client.stats.increment("prefab.config.api.start")
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Prefab
|
2
|
+
module ConfigHelper
|
3
|
+
def value_of(config_value)
|
4
|
+
case config_value.type
|
5
|
+
when :string
|
6
|
+
config_value.string
|
7
|
+
when :int
|
8
|
+
config_value.int
|
9
|
+
when :double
|
10
|
+
config_value.double
|
11
|
+
when :bool
|
12
|
+
config_value.bool
|
13
|
+
when :feature_flag
|
14
|
+
config_value.feature_flag
|
15
|
+
when :segment
|
16
|
+
config_value.segment
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/prefab/config_loader.rb
CHANGED
@@ -14,7 +14,7 @@ module Prefab
|
|
14
14
|
def calc_config
|
15
15
|
rtn = @classpath_config.clone
|
16
16
|
@api_config.each_key do |k|
|
17
|
-
rtn[k] = @api_config[k]
|
17
|
+
rtn[k] = @api_config[k]
|
18
18
|
end
|
19
19
|
rtn = rtn.merge(@local_overrides)
|
20
20
|
rtn
|
@@ -26,7 +26,7 @@ module Prefab
|
|
26
26
|
return
|
27
27
|
end
|
28
28
|
|
29
|
-
if delta.
|
29
|
+
if delta.default.nil?
|
30
30
|
@api_config.delete(delta.key)
|
31
31
|
else
|
32
32
|
@api_config[delta.key] = delta
|
@@ -63,7 +63,7 @@ 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::ConfigValue.new(value_from(v))
|
66
|
+
rtn[k] = Prefab::ConfigDelta.new(key: k, default: Prefab::ConfigValue.new(value_from(v)))
|
67
67
|
end
|
68
68
|
end
|
69
69
|
rtn
|
@@ -1,11 +1,12 @@
|
|
1
1
|
module Prefab
|
2
2
|
class ConfigResolver
|
3
|
+
include Prefab::ConfigHelper
|
3
4
|
NAMESPACE_DELIMITER = ".".freeze
|
4
|
-
NAME_KEY_DELIMITER = ":".freeze
|
5
5
|
|
6
6
|
def initialize(base_client, config_loader)
|
7
7
|
@lock = Concurrent::ReadWriteLock.new
|
8
8
|
@local_store = {}
|
9
|
+
@environment = base_client.environment
|
9
10
|
@namespace = base_client.namespace
|
10
11
|
@config_loader = config_loader
|
11
12
|
make_local
|
@@ -16,7 +17,7 @@ module Prefab
|
|
16
17
|
@lock.with_read_lock do
|
17
18
|
@local_store.each do |k, v|
|
18
19
|
value = v[:value]
|
19
|
-
str << "|#{k}|
|
20
|
+
str << "|#{k}| from #{v[:match]} |#{value_of(value)}|#{value_of(value).class}\n"
|
20
21
|
end
|
21
22
|
end
|
22
23
|
str
|
@@ -39,21 +40,6 @@ module Prefab
|
|
39
40
|
|
40
41
|
private
|
41
42
|
|
42
|
-
def value_of(config_value)
|
43
|
-
case config_value.type
|
44
|
-
when :string
|
45
|
-
config_value.string
|
46
|
-
when :int
|
47
|
-
config_value.int
|
48
|
-
when :double
|
49
|
-
config_value.double
|
50
|
-
when :bool
|
51
|
-
config_value.bool
|
52
|
-
when :feature_flag
|
53
|
-
config_value.feature_flag
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
43
|
# Should client a.b.c see key in namespace a.b? yes
|
58
44
|
# Should client a.b.c see key in namespace a.b.c? yes
|
59
45
|
# Should client a.b.c see key in namespace a.b.d? no
|
@@ -61,32 +47,50 @@ module Prefab
|
|
61
47
|
#
|
62
48
|
def starts_with_ns?(key_namespace, client_namespace)
|
63
49
|
zipped = key_namespace.split(NAMESPACE_DELIMITER).zip(client_namespace.split(NAMESPACE_DELIMITER))
|
64
|
-
zipped.map do |k, c|
|
65
|
-
(k.nil? || k.empty?) ||
|
66
|
-
end
|
50
|
+
mapped = zipped.map do |k, c|
|
51
|
+
(k.nil? || k.empty?) || k == c
|
52
|
+
end
|
53
|
+
[mapped.all?, mapped.size]
|
67
54
|
end
|
68
55
|
|
69
56
|
def make_local
|
70
57
|
store = {}
|
71
|
-
@config_loader.calc_config.each do |
|
72
|
-
|
73
|
-
|
58
|
+
@config_loader.calc_config.each do |key, delta|
|
59
|
+
# start with the top level default
|
60
|
+
to_store = { match: "default", value: delta.default }
|
61
|
+
if delta.envs.any?
|
62
|
+
env_values = delta.envs.select { |e| e.environment == @environment }
|
74
63
|
|
75
|
-
|
64
|
+
# do we have and env_values that match our env?
|
65
|
+
if env_values.any?
|
66
|
+
env_value = env_values.first
|
76
67
|
|
77
|
-
|
78
|
-
|
79
|
-
key_namespace = split[0]
|
80
|
-
end
|
68
|
+
# override the top level default with env default
|
69
|
+
to_store = { match: "env_default", env: env_value.environment, value: env_value.default }
|
81
70
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
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
|
81
|
+
end
|
82
|
+
end
|
88
83
|
end
|
89
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
|
+
|
93
|
+
store[key] = to_store
|
90
94
|
end
|
91
95
|
@lock.with_write_lock do
|
92
96
|
@local_store = store
|
@@ -1,6 +1,8 @@
|
|
1
1
|
module Prefab
|
2
2
|
class FeatureFlagClient
|
3
|
+
include Prefab::ConfigHelper
|
3
4
|
MAX_32_FLOAT = 4294967294.0
|
5
|
+
DISTRIBUTION_SPACE = 1000
|
4
6
|
|
5
7
|
def initialize(base_client)
|
6
8
|
@base_client = base_client
|
@@ -14,38 +16,124 @@ module Prefab
|
|
14
16
|
feature_is_on_for?(feature_name, nil)
|
15
17
|
end
|
16
18
|
|
17
|
-
def feature_is_on_for?(feature_name, lookup_key, attributes:
|
19
|
+
def feature_is_on_for?(feature_name, lookup_key, attributes: {})
|
18
20
|
@base_client.stats.increment("prefab.featureflag.on", tags: ["feature:#{feature_name}"])
|
19
21
|
|
22
|
+
return is_on?(get(feature_name, lookup_key, attributes))
|
23
|
+
end
|
24
|
+
|
25
|
+
def get(feature_name, lookup_key, attributes)
|
20
26
|
feature_obj = @base_client.config_client.get(feature_name)
|
21
|
-
|
27
|
+
evaluate(feature_name, lookup_key, attributes, feature_obj)
|
28
|
+
end
|
29
|
+
|
30
|
+
def evaluate(feature_name, lookup_key, attributes, feature_obj)
|
31
|
+
value_of(get_variant(feature_name, lookup_key, attributes, feature_obj))
|
22
32
|
end
|
23
33
|
|
24
34
|
private
|
25
35
|
|
26
|
-
def is_on?(
|
27
|
-
if
|
36
|
+
def is_on?(variant)
|
37
|
+
if variant.nil?
|
28
38
|
return false
|
29
39
|
end
|
40
|
+
variant.bool
|
41
|
+
end
|
42
|
+
|
43
|
+
def get_variant(feature_name, lookup_key, attributes, feature_obj)
|
44
|
+
if !feature_obj.active
|
45
|
+
return get_variant_obj(feature_obj, feature_obj.inactive_variant_idx)
|
46
|
+
end
|
47
|
+
|
48
|
+
variant_distribution = feature_obj.default
|
49
|
+
|
50
|
+
# if user_targets.match
|
51
|
+
feature_obj.user_targets.each do |target|
|
52
|
+
if (target.identifiers.include? lookup_key)
|
53
|
+
return get_variant_obj(feature_obj, target.variant_idx)
|
54
|
+
end
|
55
|
+
end
|
30
56
|
|
31
|
-
|
32
|
-
|
33
|
-
|
57
|
+
# if rules.match
|
58
|
+
feature_obj.rules.each do |rule|
|
59
|
+
if criteria_match?(rule, lookup_key, attributes)
|
60
|
+
variant_distribution = rule.distribution
|
61
|
+
end
|
34
62
|
end
|
35
63
|
|
36
|
-
if
|
37
|
-
|
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
|
+
|
73
|
+
variant_idx = get_variant_idx_from_weights(variant_distribution.variant_weights.weights, distribution_bucket, feature_name)
|
38
74
|
end
|
39
75
|
|
40
|
-
return feature_obj
|
76
|
+
return get_variant_obj(feature_obj, variant_idx)
|
77
|
+
end
|
78
|
+
|
79
|
+
def get_variant_obj(feature_obj, idx)
|
80
|
+
return feature_obj.variants[idx] if feature_obj.variants.length >= idx
|
81
|
+
nil
|
82
|
+
end
|
83
|
+
|
84
|
+
def get_variant_idx_from_weights(variant_weights, bucket, feature_name)
|
85
|
+
sum = 0
|
86
|
+
variant_weights.each do |variant_weight|
|
87
|
+
if bucket < sum + variant_weight.weight
|
88
|
+
return variant_weight.variant_idx
|
89
|
+
else
|
90
|
+
sum += variant_weight.weight
|
91
|
+
end
|
92
|
+
end
|
93
|
+
# variants didn't add up to 100%
|
94
|
+
@base_client.log.info("Variants of #{feature_name} did not add to 100%")
|
95
|
+
return variant_weights.last.variant
|
41
96
|
end
|
42
97
|
|
43
98
|
def get_user_pct(feature, lookup_key)
|
44
|
-
to_hash = "#{@base_client.
|
99
|
+
to_hash = "#{@base_client.project_id}#{feature}#{lookup_key}"
|
45
100
|
int_value = Murmur3.murmur3_32(to_hash)
|
46
101
|
int_value / MAX_32_FLOAT
|
47
102
|
end
|
48
103
|
|
104
|
+
def criteria_match?(rule, lookup_key, attributes)
|
105
|
+
if rule.criteria.operator == :IN
|
106
|
+
return rule.criteria.values.include?(lookup_key)
|
107
|
+
elsif rule.criteria.operator == :NOT_IN
|
108
|
+
return !rule.criteria.values.include?(lookup_key)
|
109
|
+
elsif rule.criteria.operator == :IN_SEG
|
110
|
+
return segment_matches(rule.criteria.values, lookup_key, attributes).any?
|
111
|
+
elsif rule.criteria.operator == :NOT_IN_SEG
|
112
|
+
return segment_matches(rule.criteria.values, lookup_key, attributes).none?
|
113
|
+
end
|
114
|
+
@base_client.log.info("Unknown Operator")
|
115
|
+
false
|
116
|
+
end
|
117
|
+
|
118
|
+
# evaluate each segment key and return whether each one matches
|
119
|
+
# there should be an associated segment available as a standard config obj
|
120
|
+
def segment_matches(segment_keys, lookup_key, attributes)
|
121
|
+
segment_keys.map do |segment_key|
|
122
|
+
segment = @base_client.config_client.get(segment_key)
|
123
|
+
if segment.nil?
|
124
|
+
@base_client.log.info("Missing Segment")
|
125
|
+
false
|
126
|
+
else
|
127
|
+
segment_match?(segment, lookup_key, attributes)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def segment_match?(segment, lookup_key, attributes)
|
133
|
+
includes = segment.includes.include?(lookup_key)
|
134
|
+
excludes = segment.excludes.include?(lookup_key)
|
135
|
+
includes && !excludes
|
136
|
+
end
|
49
137
|
end
|
50
138
|
end
|
51
139
|
|