prefab-cloud-ruby 0.24.5 → 1.0.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/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)
|