prefab-cloud-ruby 0.24.5 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +15 -0
- data/VERSION +1 -1
- data/compile_protos.sh +7 -0
- data/lib/prefab/client.rb +20 -46
- data/lib/prefab/config_client.rb +9 -12
- data/lib/prefab/config_resolver.rb +2 -1
- data/lib/prefab/config_value_unwrapper.rb +20 -9
- data/lib/prefab/context.rb +43 -7
- data/lib/prefab/context_shape_aggregator.rb +1 -1
- data/lib/prefab/criteria_evaluator.rb +24 -16
- data/lib/prefab/evaluation.rb +48 -0
- data/lib/prefab/evaluation_summary_aggregator.rb +85 -0
- data/lib/prefab/example_contexts_aggregator.rb +76 -0
- data/lib/prefab/exponential_backoff.rb +5 -0
- data/lib/prefab/feature_flag_client.rb +0 -2
- data/lib/prefab/log_path_aggregator.rb +1 -1
- data/lib/prefab/logger_client.rb +12 -13
- data/lib/prefab/options.rb +52 -43
- data/lib/prefab/periodic_sync.rb +30 -13
- data/lib/prefab/rate_limit_cache.rb +41 -0
- data/lib/prefab/resolved_config_presenter.rb +2 -4
- data/lib/prefab/weighted_value_resolver.rb +1 -1
- data/lib/prefab-cloud-ruby.rb +5 -5
- data/lib/prefab_pb.rb +11 -1
- data/prefab-cloud-ruby.gemspec +14 -9
- data/test/integration_test.rb +1 -3
- data/test/integration_test_helpers.rb +0 -1
- data/test/support/common_helpers.rb +174 -0
- data/test/support/mock_base_client.rb +44 -0
- data/test/support/mock_config_client.rb +19 -0
- data/test/support/mock_config_loader.rb +1 -0
- data/test/test_client.rb +354 -40
- data/test/test_config_client.rb +1 -0
- data/test/test_config_loader.rb +1 -0
- data/test/test_config_resolver.rb +25 -24
- data/test/test_config_value_unwrapper.rb +22 -32
- data/test/test_context.rb +1 -0
- data/test/test_context_shape_aggregator.rb +11 -1
- data/test/test_criteria_evaluator.rb +180 -133
- data/test/test_evaluation_summary_aggregator.rb +162 -0
- data/test/test_example_contexts_aggregator.rb +238 -0
- data/test/test_helper.rb +5 -131
- data/test/test_integration.rb +6 -4
- data/test/test_local_config_parser.rb +2 -2
- data/test/test_log_path_aggregator.rb +9 -1
- data/test/test_logger.rb +6 -5
- data/test/test_options.rb +33 -2
- data/test/test_rate_limit_cache.rb +44 -0
- data/test/test_weighted_value_resolver.rb +13 -7
- metadata +13 -8
- data/lib/prefab/evaluated_configs_aggregator.rb +0 -60
- data/lib/prefab/evaluated_keys_aggregator.rb +0 -41
- data/lib/prefab/noop_cache.rb +0 -15
- data/lib/prefab/noop_stats.rb +0 -8
- data/test/test_evaluated_configs_aggregator.rb +0 -254
- data/test/test_evaluated_keys_aggregator.rb +0 -54
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 318e4b03e0089cfe68fa20b00e3c400c1005b739f63ed4183b071418342e4bbe
|
4
|
+
data.tar.gz: 46e467acdeb1dea78cc2e5c97031c952fa6c2caf9be235a217d9341f54505389
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fac2499f7fc5f4ba46eb6bb0f8a8671ac6fa8412600e8f3fbdeb61db68b60f491def31bd7faf1ee78711644ff4e1c253386c54e08d40f075404ab0b22d5977db
|
7
|
+
data.tar.gz: 9b112dcdbe22b2cadc6c20e5d10df94b1f1e269f49e3a12ada73550294a55154db098cc3662c5e25f2d569111b870f09df6abf2fc7af1cbc447983b3b3378bce
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,20 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## 1.0.0 - 2023-08-10
|
4
|
+
|
5
|
+
- Removed EvaluatedKeysAggregator (#137)
|
6
|
+
- Change `collect_evaluation_summaries` default to true (#136)
|
7
|
+
- Removed some backwards compatibility shims (#133)
|
8
|
+
- Standardizing options (#132)
|
9
|
+
- Note that the default value for `context_upload_mode` is `:periodic_example` which means example contexts will be collected.
|
10
|
+
This enables easy variant override assignment in our UI. More at https://prefab.cloud/blog/feature-flag-variant-assignment/
|
11
|
+
|
12
|
+
## 0.24.6 - 2023-07-31
|
13
|
+
|
14
|
+
- Logger Client compatibility (#129)
|
15
|
+
- Replace EvaluatedConfigs with ExampleContexts (#128)
|
16
|
+
- Add ConfigEvaluationSummaries (opt-in for now) (#123)
|
17
|
+
|
3
18
|
## 0.24.5 - 2023-07-10
|
4
19
|
|
5
20
|
- Report Client Version (#121)
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
1.0.0
|
data/compile_protos.sh
CHANGED
@@ -1,7 +1,14 @@
|
|
1
1
|
#!/usr/bin/env bash
|
2
2
|
|
3
|
+
set -e
|
4
|
+
|
3
5
|
gem install grpc-tools
|
4
6
|
|
7
|
+
(
|
8
|
+
cd ../prefab-cloud
|
9
|
+
git pull --rebase
|
10
|
+
)
|
11
|
+
|
5
12
|
grpc_tools_ruby_protoc -I ../prefab-cloud/ --ruby_out=lib --grpc_out=lib prefab.proto
|
6
13
|
|
7
14
|
gsed -i 's/^module Prefab$/module PrefabProto/g' lib/prefab_pb.rb
|
data/lib/prefab/client.rb
CHANGED
@@ -7,12 +7,10 @@ module Prefab
|
|
7
7
|
MAX_SLEEP_SEC = 10
|
8
8
|
BASE_SLEEP_SEC = 0.5
|
9
9
|
|
10
|
-
attr_reader :
|
10
|
+
attr_reader :namespace, :interceptor, :api_key, :prefab_api_url, :options, :instance_hash
|
11
11
|
|
12
12
|
def initialize(options = Prefab::Options.new)
|
13
13
|
@options = options.is_a?(Prefab::Options) ? options : Prefab::Options.new(options)
|
14
|
-
@shared_cache = @options.shared_cache
|
15
|
-
@stats = @options.stats
|
16
14
|
@namespace = @options.namespace
|
17
15
|
@stubs = {}
|
18
16
|
@instance_hash = UUID.new.generate
|
@@ -32,11 +30,6 @@ module Prefab
|
|
32
30
|
config_client
|
33
31
|
end
|
34
32
|
|
35
|
-
def with_log_context(_lookup_key, properties, &block)
|
36
|
-
warn '[DEPRECATION] `$prefab.with_log_context` is deprecated. Please use `with_context` instead.'
|
37
|
-
with_context(properties, &block)
|
38
|
-
end
|
39
|
-
|
40
33
|
def with_context(properties, &block)
|
41
34
|
Prefab::Context.with_context(properties, &block)
|
42
35
|
end
|
@@ -73,18 +66,24 @@ module Prefab
|
|
73
66
|
sync_interval: @options.collect_sync_interval)
|
74
67
|
end
|
75
68
|
|
76
|
-
def
|
77
|
-
return nil if @options.
|
69
|
+
def example_contexts_aggregator
|
70
|
+
return nil if @options.collect_max_example_contexts <= 0
|
78
71
|
|
79
|
-
@
|
80
|
-
|
72
|
+
@example_contexts_aggregator ||= ExampleContextsAggregator.new(
|
73
|
+
client: self,
|
74
|
+
max_contexts: @options.collect_max_example_contexts,
|
75
|
+
sync_interval: @options.collect_sync_interval
|
76
|
+
)
|
81
77
|
end
|
82
78
|
|
83
|
-
def
|
84
|
-
return nil if @options.
|
79
|
+
def evaluation_summary_aggregator
|
80
|
+
return nil if @options.collect_max_evaluation_summaries <= 0
|
85
81
|
|
86
|
-
@
|
87
|
-
|
82
|
+
@evaluation_summary_aggregator ||= EvaluationSummaryAggregator.new(
|
83
|
+
client: self,
|
84
|
+
max_keys: @options.collect_max_evaluation_summaries,
|
85
|
+
sync_interval: @options.collect_sync_interval
|
86
|
+
)
|
88
87
|
end
|
89
88
|
|
90
89
|
def set_rails_loggers
|
@@ -104,19 +103,15 @@ module Prefab
|
|
104
103
|
log.log_internal msg, path, nil, level
|
105
104
|
end
|
106
105
|
|
107
|
-
def enabled?(feature_name,
|
108
|
-
|
109
|
-
|
110
|
-
feature_flag_client.feature_is_on_for?(feature_name, properties)
|
106
|
+
def enabled?(feature_name, jit_context = NO_DEFAULT_PROVIDED)
|
107
|
+
feature_flag_client.feature_is_on_for?(feature_name, jit_context)
|
111
108
|
end
|
112
109
|
|
113
|
-
def get(key,
|
110
|
+
def get(key, default = NO_DEFAULT_PROVIDED, jit_context = NO_DEFAULT_PROVIDED)
|
114
111
|
if is_ff?(key)
|
115
|
-
|
116
|
-
|
117
|
-
feature_flag_client.get(key, properties, default: ff_default)
|
112
|
+
feature_flag_client.get(key, jit_context, default: default)
|
118
113
|
else
|
119
|
-
config_client.get(key,
|
114
|
+
config_client.get(key, default, jit_context)
|
120
115
|
end
|
121
116
|
end
|
122
117
|
|
@@ -148,26 +143,5 @@ module Prefab
|
|
148
143
|
|
149
144
|
raw && raw.allowable_values.any?
|
150
145
|
end
|
151
|
-
|
152
|
-
# The goal here is to ease transition from the old API to the new one. The
|
153
|
-
# old API had a lookup_key parameter that is deprecated. This method
|
154
|
-
# handles the transition by checking if the first parameter is a string and
|
155
|
-
# if so, it is assumed to be the lookup_key and a deprecation warning is
|
156
|
-
# issued and we know the second argument is the properties. If the first
|
157
|
-
# parameter is a hash, you're on the new API and no further action is
|
158
|
-
# required.
|
159
|
-
def handle_positional_arguments(lookup_key, properties, method)
|
160
|
-
# handle JIT context
|
161
|
-
if lookup_key.is_a?(Hash) && properties == NO_DEFAULT_PROVIDED
|
162
|
-
properties = lookup_key
|
163
|
-
lookup_key = nil
|
164
|
-
end
|
165
|
-
|
166
|
-
if lookup_key.is_a?(String)
|
167
|
-
warn "[DEPRECATION] `$prefab.#{method}`'s lookup_key argument is deprecated. Please remove it or use context instead."
|
168
|
-
end
|
169
|
-
|
170
|
-
[lookup_key, properties]
|
171
|
-
end
|
172
146
|
end
|
173
147
|
end
|
data/lib/prefab/config_client.rb
CHANGED
@@ -58,18 +58,16 @@ module Prefab
|
|
58
58
|
def get(key, default = NO_DEFAULT_PROVIDED, properties = NO_DEFAULT_PROVIDED)
|
59
59
|
context = @config_resolver.make_context(properties)
|
60
60
|
|
61
|
-
|
61
|
+
if !context.blank? && @base_client.example_contexts_aggregator
|
62
|
+
@base_client.example_contexts_aggregator.record(context)
|
63
|
+
end
|
62
64
|
|
63
|
-
|
64
|
-
@base_client.evaluated_keys_aggregator&.push(key)
|
65
|
+
evaluation = _get(key, context)
|
65
66
|
|
66
|
-
|
67
|
-
# NOTE: we don't &.push here because some of the args aren't already available
|
68
|
-
if @base_client.evaluated_configs_aggregator && key != Prefab::LoggerClient::BASE_KEY && !key.start_with?(LOGGING_KEY_PREFIX)
|
69
|
-
@base_client.evaluated_configs_aggregator.push([raw(key), value, context])
|
70
|
-
end
|
67
|
+
@base_client.context_shape_aggregator&.push(context)
|
71
68
|
|
72
|
-
|
69
|
+
if evaluation
|
70
|
+
evaluation.report_and_return(@base_client.evaluation_summary_aggregator)
|
73
71
|
else
|
74
72
|
handle_default(key, default)
|
75
73
|
end
|
@@ -90,7 +88,7 @@ module Prefab
|
|
90
88
|
end
|
91
89
|
|
92
90
|
def _get(key, properties)
|
93
|
-
# wait timeout sec for the
|
91
|
+
# wait timeout sec for the initialization to be complete
|
94
92
|
@initialized_future.value(@options.initialization_timeout_sec)
|
95
93
|
if @initialized_future.incomplete?
|
96
94
|
unless @options.on_init_failure == Prefab::Options::ON_INITIALIZATION_FAILURE::RETURN
|
@@ -158,7 +156,6 @@ module Prefab
|
|
158
156
|
@base_client.log_internal ::Logger::DEBUG,
|
159
157
|
"Checkpoint with highwater id #{@config_loader.highwater_mark} from #{source}. No changes.", 'load_configs'
|
160
158
|
end
|
161
|
-
@base_client.stats.increment('prefab.config.checkpoint.load')
|
162
159
|
@config_resolver.update
|
163
160
|
finish_init!(source)
|
164
161
|
end
|
@@ -183,7 +180,7 @@ module Prefab
|
|
183
180
|
|
184
181
|
@base_client.log_internal ::Logger::INFO, "Unlocked Config via #{source}"
|
185
182
|
@initialization_lock.release_write_lock
|
186
|
-
@base_client.log.
|
183
|
+
@base_client.log.config_client = self
|
187
184
|
@base_client.log_internal ::Logger::INFO, to_s
|
188
185
|
end
|
189
186
|
|
@@ -10,6 +10,7 @@ module Prefab
|
|
10
10
|
@config_loader = config_loader
|
11
11
|
@project_env_id = 0 # we don't know this yet, it is set from the API results
|
12
12
|
@base_client = base_client
|
13
|
+
@on_update = nil
|
13
14
|
make_local
|
14
15
|
end
|
15
16
|
|
@@ -54,7 +55,7 @@ module Prefab
|
|
54
55
|
end
|
55
56
|
|
56
57
|
def make_context(properties)
|
57
|
-
if properties == NO_DEFAULT_PROVIDED
|
58
|
+
if properties == NO_DEFAULT_PROVIDED || properties.nil?
|
58
59
|
Context.current
|
59
60
|
elsif properties.is_a?(Context)
|
60
61
|
properties
|
@@ -2,24 +2,35 @@
|
|
2
2
|
|
3
3
|
module Prefab
|
4
4
|
class ConfigValueUnwrapper
|
5
|
-
|
6
|
-
return nil unless config_value
|
5
|
+
attr_reader :value, :weighted_value_index
|
7
6
|
|
8
|
-
|
7
|
+
def initialize(value, weighted_value_index = nil)
|
8
|
+
@value = value
|
9
|
+
@weighted_value_index = weighted_value_index
|
10
|
+
end
|
11
|
+
|
12
|
+
def unwrap
|
13
|
+
case value.type
|
9
14
|
when :int, :string, :double, :bool, :log_level
|
10
|
-
|
15
|
+
value.public_send(value.type)
|
11
16
|
when :string_list
|
12
|
-
|
13
|
-
|
14
|
-
|
17
|
+
value.string_list.values
|
18
|
+
else
|
19
|
+
raise "Unknown type: #{config_value.type}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.deepest_value(config_value, config_key, context)
|
24
|
+
if config_value&.type == :weighted_values
|
25
|
+
value, index = Prefab::WeightedValueResolver.new(
|
15
26
|
config_value.weighted_values.weighted_values,
|
16
27
|
config_key,
|
17
28
|
context.get(config_value.weighted_values.hash_by_property_name)
|
18
29
|
).resolve
|
19
30
|
|
20
|
-
|
31
|
+
new(deepest_value(value.value, config_key, context).value, index)
|
21
32
|
else
|
22
|
-
|
33
|
+
new(config_value)
|
23
34
|
end
|
24
35
|
end
|
25
36
|
end
|
data/lib/prefab/context.rb
CHANGED
@@ -25,10 +25,23 @@ module Prefab
|
|
25
25
|
def to_h
|
26
26
|
@hash
|
27
27
|
end
|
28
|
+
|
29
|
+
def key
|
30
|
+
"#{@name}:#{get('key')}"
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_proto
|
34
|
+
PrefabProto::Context.new(
|
35
|
+
type: name,
|
36
|
+
values: to_h.transform_values do |value|
|
37
|
+
ConfigValueWrapper.wrap(value)
|
38
|
+
end
|
39
|
+
)
|
40
|
+
end
|
28
41
|
end
|
29
42
|
|
30
43
|
THREAD_KEY = :prefab_context
|
31
|
-
attr_reader :contexts
|
44
|
+
attr_reader :contexts, :seen_at
|
32
45
|
|
33
46
|
class << self
|
34
47
|
def current=(context)
|
@@ -58,6 +71,7 @@ module Prefab
|
|
58
71
|
|
59
72
|
def initialize(context = {})
|
60
73
|
@contexts = {}
|
74
|
+
@seen_at = Time.now.utc.to_i
|
61
75
|
|
62
76
|
if context.is_a?(NamedContext)
|
63
77
|
@contexts[context.name] = context
|
@@ -77,6 +91,10 @@ module Prefab
|
|
77
91
|
end
|
78
92
|
end
|
79
93
|
|
94
|
+
def blank?
|
95
|
+
contexts.empty?
|
96
|
+
end
|
97
|
+
|
80
98
|
def set(name, hash)
|
81
99
|
@contexts[name.to_s] = NamedContext.new(name, hash)
|
82
100
|
end
|
@@ -113,15 +131,33 @@ module Prefab
|
|
113
131
|
|
114
132
|
PrefabProto::ContextSet.new(
|
115
133
|
contexts: contexts.map do |name, context|
|
116
|
-
|
117
|
-
type: name,
|
118
|
-
values: context.to_h.transform_values do |value|
|
119
|
-
ConfigValueWrapper.wrap(value)
|
120
|
-
end
|
121
|
-
)
|
134
|
+
context.to_proto
|
122
135
|
end.concat([PrefabProto::Context.new(type: 'prefab',
|
123
136
|
values: prefab_context)])
|
124
137
|
)
|
125
138
|
end
|
139
|
+
|
140
|
+
def slim_proto
|
141
|
+
PrefabProto::ContextSet.new(
|
142
|
+
contexts: contexts.map do |_, context|
|
143
|
+
context.to_proto
|
144
|
+
end
|
145
|
+
)
|
146
|
+
end
|
147
|
+
|
148
|
+
def grouped_key
|
149
|
+
contexts.map do |_, context|
|
150
|
+
context.key
|
151
|
+
end.sort.join('|')
|
152
|
+
end
|
153
|
+
|
154
|
+
include Comparable
|
155
|
+
def <=>(other)
|
156
|
+
if other.is_a?(Prefab::Context)
|
157
|
+
to_h <=> other.to_h
|
158
|
+
else
|
159
|
+
super
|
160
|
+
end
|
161
|
+
end
|
126
162
|
end
|
127
163
|
end
|
@@ -4,6 +4,8 @@
|
|
4
4
|
# We're intentionally keeping the UPCASED method names to match the protobuf
|
5
5
|
# and avoid wasting CPU cycles lowercasing things
|
6
6
|
module Prefab
|
7
|
+
# This class evaluates a config's criteria. `evaluate` returns the value of
|
8
|
+
# the first match based on the provided properties.
|
7
9
|
class CriteriaEvaluator
|
8
10
|
NAMESPACE_KEY = 'NAMESPACE'
|
9
11
|
NO_MATCHING_ROWS = [].freeze
|
@@ -17,15 +19,8 @@ module Prefab
|
|
17
19
|
end
|
18
20
|
|
19
21
|
def evaluate(properties)
|
20
|
-
|
21
|
-
|
22
|
-
end
|
23
|
-
|
24
|
-
default_row_values.each do |conditional_value|
|
25
|
-
return conditional_value.value if all_criteria_match?(conditional_value, properties)
|
26
|
-
end
|
27
|
-
|
28
|
-
nil
|
22
|
+
evaluate_for_env(@project_env_id, properties) ||
|
23
|
+
evaluate_for_env(0, properties)
|
29
24
|
end
|
30
25
|
|
31
26
|
def all_criteria_match?(conditional_value, props)
|
@@ -83,12 +78,24 @@ module Prefab
|
|
83
78
|
|
84
79
|
private
|
85
80
|
|
86
|
-
def
|
87
|
-
@config.rows.
|
88
|
-
|
81
|
+
def evaluate_for_env(env_id, properties)
|
82
|
+
@config.rows.each_with_index do |row, index|
|
83
|
+
next unless row.project_env_id == env_id
|
84
|
+
|
85
|
+
row.values.each_with_index do |conditional_value, value_index|
|
86
|
+
next unless all_criteria_match?(conditional_value, properties)
|
87
|
+
|
88
|
+
return Prefab::Evaluation.new(
|
89
|
+
config: @config,
|
90
|
+
value: conditional_value.value,
|
91
|
+
value_index: value_index,
|
92
|
+
config_row_index: index,
|
93
|
+
context: properties
|
94
|
+
)
|
95
|
+
end
|
96
|
+
end
|
89
97
|
|
90
|
-
|
91
|
-
@config.rows.find { |row| row.project_env_id != @project_env_id }&.values || NO_MATCHING_ROWS
|
98
|
+
nil
|
92
99
|
end
|
93
100
|
|
94
101
|
def in_segment?(criterion, properties)
|
@@ -96,11 +103,12 @@ module Prefab
|
|
96
103
|
|
97
104
|
@base_client.log.info("Segment #{criterion.value_to_match.string} not found") unless segment
|
98
105
|
|
99
|
-
segment&.
|
106
|
+
segment&.report_and_return(@base_client.evaluation_summary_aggregator)
|
100
107
|
end
|
101
108
|
|
102
109
|
def matches?(criterion, value, properties)
|
103
|
-
criterion_value_or_values = Prefab::ConfigValueUnwrapper.
|
110
|
+
criterion_value_or_values = Prefab::ConfigValueUnwrapper.deepest_value(criterion.value_to_match, @config.key,
|
111
|
+
properties).unwrap
|
104
112
|
|
105
113
|
case criterion_value_or_values
|
106
114
|
when Google::Protobuf::RepeatedField
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Prefab
|
4
|
+
# Records the result of evaluating a config's criteria and forensics for reporting
|
5
|
+
class Evaluation
|
6
|
+
attr_reader :value
|
7
|
+
|
8
|
+
def initialize(config:, value:, value_index:, config_row_index:, context:)
|
9
|
+
@config = config
|
10
|
+
@value = value
|
11
|
+
@value_index = value_index
|
12
|
+
@config_row_index = config_row_index
|
13
|
+
@context = context
|
14
|
+
end
|
15
|
+
|
16
|
+
def unwrapped_value
|
17
|
+
deepest_value.unwrap
|
18
|
+
end
|
19
|
+
|
20
|
+
def report_and_return(evaluation_summary_aggregator)
|
21
|
+
report(evaluation_summary_aggregator)
|
22
|
+
|
23
|
+
unwrapped_value
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def report(evaluation_summary_aggregator)
|
29
|
+
return if @config.config_type == :LOG_LEVEL
|
30
|
+
|
31
|
+
evaluation_summary_aggregator&.record(
|
32
|
+
config_key: @config.key,
|
33
|
+
config_type: @config.config_type,
|
34
|
+
counter: {
|
35
|
+
config_id: @config.id,
|
36
|
+
config_row_index: @config_row_index,
|
37
|
+
conditional_value_index: @value_index,
|
38
|
+
selected_value: deepest_value.value,
|
39
|
+
weighted_value_index: deepest_value.weighted_value_index,
|
40
|
+
selected_index: nil # TODO
|
41
|
+
})
|
42
|
+
end
|
43
|
+
|
44
|
+
def deepest_value
|
45
|
+
@deepest_value ||= Prefab::ConfigValueUnwrapper.deepest_value(@value, @config.key, @context)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'periodic_sync'
|
4
|
+
|
5
|
+
module Prefab
|
6
|
+
# This class aggregates the number of times each config is evaluated, and
|
7
|
+
# details about how the config is evaluated This data is reported to the
|
8
|
+
# server at a regular interval defined by `sync_interval`.
|
9
|
+
class EvaluationSummaryAggregator
|
10
|
+
include Prefab::PeriodicSync
|
11
|
+
|
12
|
+
attr_reader :data
|
13
|
+
|
14
|
+
def initialize(client:, max_keys:, sync_interval:)
|
15
|
+
@client = client
|
16
|
+
@max_keys = max_keys
|
17
|
+
@name = 'evaluation_summary_aggregator'
|
18
|
+
|
19
|
+
@data = Concurrent::Hash.new
|
20
|
+
|
21
|
+
start_periodic_sync(sync_interval)
|
22
|
+
end
|
23
|
+
|
24
|
+
def record(config_key:, config_type:, counter:)
|
25
|
+
return if @data.size >= @max_keys
|
26
|
+
|
27
|
+
key = [config_key, config_type]
|
28
|
+
@data[key] ||= Concurrent::Hash.new
|
29
|
+
|
30
|
+
@data[key][counter] ||= 0
|
31
|
+
@data[key][counter] += 1
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def counter_proto(counter, count)
|
37
|
+
PrefabProto::ConfigEvaluationCounter.new(
|
38
|
+
config_id: counter[:config_id],
|
39
|
+
selected_index: counter[:selected_index],
|
40
|
+
config_row_index: counter[:config_row_index],
|
41
|
+
conditional_value_index: counter[:conditional_value_index],
|
42
|
+
weighted_value_index: counter[:weighted_value_index],
|
43
|
+
selected_value: counter[:selected_value],
|
44
|
+
count: count
|
45
|
+
)
|
46
|
+
end
|
47
|
+
|
48
|
+
def flush(to_ship, start_at_was)
|
49
|
+
pool.post do
|
50
|
+
log_internal "Flushing #{to_ship.size} summaries"
|
51
|
+
|
52
|
+
summaries_proto = PrefabProto::ConfigEvaluationSummaries.new(
|
53
|
+
start: start_at_was,
|
54
|
+
end: Prefab::TimeHelpers.now_in_ms,
|
55
|
+
summaries: summaries(to_ship)
|
56
|
+
)
|
57
|
+
|
58
|
+
result = @client.post('/api/v1/telemetry', events(summaries_proto))
|
59
|
+
|
60
|
+
log_internal "Uploaded #{to_ship.size} summaries: #{result.status}"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def events(summaries)
|
65
|
+
event = PrefabProto::TelemetryEvent.new(summaries: summaries)
|
66
|
+
|
67
|
+
PrefabProto::TelemetryEvents.new(
|
68
|
+
instance_hash: @client.instance_hash,
|
69
|
+
events: [event]
|
70
|
+
)
|
71
|
+
end
|
72
|
+
|
73
|
+
def summaries(data)
|
74
|
+
data.map do |(config_key, config_type), counters|
|
75
|
+
counter_protos = counters.map { |counter, count| counter_proto(counter, count) }
|
76
|
+
|
77
|
+
PrefabProto::ConfigEvaluationSummary.new(
|
78
|
+
key: config_key,
|
79
|
+
type: config_type,
|
80
|
+
counters: counter_protos
|
81
|
+
)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'periodic_sync'
|
4
|
+
|
5
|
+
module Prefab
|
6
|
+
# This class aggregates example contexts. It dedupes based on the
|
7
|
+
# concatenation of the keys of the contexts.
|
8
|
+
#
|
9
|
+
# It shouldn't send the same context more than once per hour.
|
10
|
+
class ExampleContextsAggregator
|
11
|
+
include Prefab::PeriodicSync
|
12
|
+
|
13
|
+
attr_reader :data, :cache
|
14
|
+
|
15
|
+
ONE_HOUR = 60 * 60
|
16
|
+
|
17
|
+
def initialize(client:, max_contexts:, sync_interval:)
|
18
|
+
@client = client
|
19
|
+
@max_contexts = max_contexts
|
20
|
+
@name = 'example_contexts_aggregator'
|
21
|
+
|
22
|
+
@data = Concurrent::Array.new
|
23
|
+
@cache = Prefab::RateLimitCache.new(ONE_HOUR)
|
24
|
+
|
25
|
+
start_periodic_sync(sync_interval)
|
26
|
+
end
|
27
|
+
|
28
|
+
def record(contexts)
|
29
|
+
key = contexts.grouped_key
|
30
|
+
|
31
|
+
return unless @data.size < @max_contexts && !@cache.fresh?(key)
|
32
|
+
|
33
|
+
@cache.set(key)
|
34
|
+
|
35
|
+
@data.push(contexts)
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def on_prepare_data
|
41
|
+
@cache.prune
|
42
|
+
end
|
43
|
+
|
44
|
+
def flush(to_ship, _)
|
45
|
+
pool.post do
|
46
|
+
log_internal "Flushing #{to_ship.size} examples"
|
47
|
+
|
48
|
+
result = @client.post('/api/v1/telemetry', events(to_ship))
|
49
|
+
|
50
|
+
log_internal "Uploaded #{to_ship.size} examples: #{result.status}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def example_contexts(to_ship)
|
55
|
+
to_ship.map do |contexts|
|
56
|
+
PrefabProto::ExampleContext.new(
|
57
|
+
timestamp: contexts.seen_at * 1000,
|
58
|
+
contextSet: contexts.slim_proto
|
59
|
+
)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def events(to_ship)
|
64
|
+
event = PrefabProto::TelemetryEvent.new(
|
65
|
+
example_contexts: PrefabProto::ExampleContexts.new(
|
66
|
+
examples: example_contexts(to_ship)
|
67
|
+
)
|
68
|
+
)
|
69
|
+
|
70
|
+
PrefabProto::TelemetryEvents.new(
|
71
|
+
instance_hash: @client.instance_hash,
|
72
|
+
events: [event]
|
73
|
+
)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -1,4 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Prefab
|
4
|
+
# This class implements exponential backoff with a maximum delay.
|
5
|
+
#
|
6
|
+
# This is the default sync interval for aggregators.
|
2
7
|
class ExponentialBackoff
|
3
8
|
def initialize(max_delay:, initial_delay: 2, multiplier: 2)
|
4
9
|
@initial_delay = initial_delay
|
@@ -11,8 +11,6 @@ module Prefab
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def feature_is_on_for?(feature_name, properties)
|
14
|
-
@base_client.stats.increment('prefab.featureflag.on', tags: ["feature:#{feature_name}"])
|
15
|
-
|
16
14
|
variant = @base_client.config_client.get(feature_name, false, properties)
|
17
15
|
|
18
16
|
is_on?(variant)
|