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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -0
  3. data/VERSION +1 -1
  4. data/compile_protos.sh +7 -0
  5. data/lib/prefab/client.rb +20 -46
  6. data/lib/prefab/config_client.rb +9 -12
  7. data/lib/prefab/config_resolver.rb +2 -1
  8. data/lib/prefab/config_value_unwrapper.rb +20 -9
  9. data/lib/prefab/context.rb +43 -7
  10. data/lib/prefab/context_shape_aggregator.rb +1 -1
  11. data/lib/prefab/criteria_evaluator.rb +24 -16
  12. data/lib/prefab/evaluation.rb +48 -0
  13. data/lib/prefab/evaluation_summary_aggregator.rb +85 -0
  14. data/lib/prefab/example_contexts_aggregator.rb +76 -0
  15. data/lib/prefab/exponential_backoff.rb +5 -0
  16. data/lib/prefab/feature_flag_client.rb +0 -2
  17. data/lib/prefab/log_path_aggregator.rb +1 -1
  18. data/lib/prefab/logger_client.rb +12 -13
  19. data/lib/prefab/options.rb +52 -43
  20. data/lib/prefab/periodic_sync.rb +30 -13
  21. data/lib/prefab/rate_limit_cache.rb +41 -0
  22. data/lib/prefab/resolved_config_presenter.rb +2 -4
  23. data/lib/prefab/weighted_value_resolver.rb +1 -1
  24. data/lib/prefab-cloud-ruby.rb +5 -5
  25. data/lib/prefab_pb.rb +11 -1
  26. data/prefab-cloud-ruby.gemspec +14 -9
  27. data/test/integration_test.rb +1 -3
  28. data/test/integration_test_helpers.rb +0 -1
  29. data/test/support/common_helpers.rb +174 -0
  30. data/test/support/mock_base_client.rb +44 -0
  31. data/test/support/mock_config_client.rb +19 -0
  32. data/test/support/mock_config_loader.rb +1 -0
  33. data/test/test_client.rb +354 -40
  34. data/test/test_config_client.rb +1 -0
  35. data/test/test_config_loader.rb +1 -0
  36. data/test/test_config_resolver.rb +25 -24
  37. data/test/test_config_value_unwrapper.rb +22 -32
  38. data/test/test_context.rb +1 -0
  39. data/test/test_context_shape_aggregator.rb +11 -1
  40. data/test/test_criteria_evaluator.rb +180 -133
  41. data/test/test_evaluation_summary_aggregator.rb +162 -0
  42. data/test/test_example_contexts_aggregator.rb +238 -0
  43. data/test/test_helper.rb +5 -131
  44. data/test/test_integration.rb +6 -4
  45. data/test/test_local_config_parser.rb +2 -2
  46. data/test/test_log_path_aggregator.rb +9 -1
  47. data/test/test_logger.rb +6 -5
  48. data/test/test_options.rb +33 -2
  49. data/test/test_rate_limit_cache.rb +44 -0
  50. data/test/test_weighted_value_resolver.rb +13 -7
  51. metadata +13 -8
  52. data/lib/prefab/evaluated_configs_aggregator.rb +0 -60
  53. data/lib/prefab/evaluated_keys_aggregator.rb +0 -41
  54. data/lib/prefab/noop_cache.rb +0 -15
  55. data/lib/prefab/noop_stats.rb +0 -8
  56. data/test/test_evaluated_configs_aggregator.rb +0 -254
  57. data/test/test_evaluated_keys_aggregator.rb +0 -54
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bf743673dab06e2777321f7fb867417478a383864dd6b982860abbb21375450b
4
- data.tar.gz: 47a0ba475095d9e773fafc684efc43bc4ac05bc9ec318b70ee321cf2c6b59428
3
+ metadata.gz: 318e4b03e0089cfe68fa20b00e3c400c1005b739f63ed4183b071418342e4bbe
4
+ data.tar.gz: 46e467acdeb1dea78cc2e5c97031c952fa6c2caf9be235a217d9341f54505389
5
5
  SHA512:
6
- metadata.gz: 5ce8f48e9c4584a34deead09528e1b90cd33a3e80994433d999abb32ccfd31eae7883eedf18b99d8351c55cf00e1e1dc7ef6f0b5a7d45819b300f573ff068533
7
- data.tar.gz: 416c9f5271b62cef7469db992dde3238c982d15f442f22d3c273c5dfe07935caa1777c26684c98b5ed3c92a67cb41bb10fb9b00d188d4cecaa34507b425fdbb4
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.24.5
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 :shared_cache, :stats, :namespace, :interceptor, :api_key, :prefab_api_url, :options, :instance_hash
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 evaluated_keys_aggregator
77
- return nil if @options.collect_max_keys <= 0
69
+ def example_contexts_aggregator
70
+ return nil if @options.collect_max_example_contexts <= 0
78
71
 
79
- @evaluated_keys_aggregator ||= EvaluatedKeysAggregator.new(client: self, max_keys: @options.collect_max_keys,
80
- sync_interval: @options.collect_sync_interval)
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 evaluated_configs_aggregator
84
- return nil if @options.collect_max_evaluations <= 0
79
+ def evaluation_summary_aggregator
80
+ return nil if @options.collect_max_evaluation_summaries <= 0
85
81
 
86
- @evaluated_configs_aggregator ||= EvaluatedConfigsAggregator.new(client: self, max_configs: @options.collect_max_evaluations,
87
- sync_interval: @options.collect_sync_interval)
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, lookup_key = NO_DEFAULT_PROVIDED, properties = NO_DEFAULT_PROVIDED)
108
- _, properties = handle_positional_arguments(lookup_key, properties, :enabled?)
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, default_or_lookup_key = NO_DEFAULT_PROVIDED, properties = NO_DEFAULT_PROVIDED, ff_default = nil)
110
+ def get(key, default = NO_DEFAULT_PROVIDED, jit_context = NO_DEFAULT_PROVIDED)
114
111
  if is_ff?(key)
115
- default, properties = handle_positional_arguments(default_or_lookup_key, properties, :get)
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, default_or_lookup_key, properties)
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
@@ -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
- value = _get(key, context)
61
+ if !context.blank? && @base_client.example_contexts_aggregator
62
+ @base_client.example_contexts_aggregator.record(context)
63
+ end
62
64
 
63
- @base_client.context_shape_aggregator&.push(context)
64
- @base_client.evaluated_keys_aggregator&.push(key)
65
+ evaluation = _get(key, context)
65
66
 
66
- if value
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
- Prefab::ConfigValueUnwrapper.unwrap(value, key, context)
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 initalization to be complete
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.set_config_client(self)
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
- def self.unwrap(config_value, config_key, context)
6
- return nil unless config_value
5
+ attr_reader :value, :weighted_value_index
7
6
 
8
- case config_value.type
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
- config_value.public_send(config_value.type)
15
+ value.public_send(value.type)
11
16
  when :string_list
12
- config_value.string_list.values
13
- when :weighted_values
14
- value = Prefab::WeightedValueResolver.new(
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
- unwrap(value.value, config_key, context)
31
+ new(deepest_value(value.value, config_key, context).value, index)
21
32
  else
22
- raise "Unknown type: #{config_value.type}"
33
+ new(config_value)
23
34
  end
24
35
  end
25
36
  end
@@ -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
- PrefabProto::Context.new(
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
@@ -42,7 +42,7 @@ module Prefab
42
42
  private
43
43
 
44
44
  def flush(to_ship, _)
45
- @pool.post do
45
+ pool.post do
46
46
  log_internal "Uploading context shapes for #{to_ship.values.size}"
47
47
 
48
48
  shapes = PrefabProto::ContextShapes.new(
@@ -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
- matching_environment_row_values.each do |conditional_value|
21
- return conditional_value.value if all_criteria_match?(conditional_value, properties)
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 matching_environment_row_values
87
- @config.rows.find { |row| row.project_env_id == @project_env_id }&.values || NO_MATCHING_ROWS
88
- end
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
- def default_row_values
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&.bool
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.unwrap(criterion.value_to_match, @config.key, properties)
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)
@@ -37,7 +37,7 @@ module Prefab
37
37
  private
38
38
 
39
39
  def flush(to_ship, start_at_was)
40
- @pool.post do
40
+ pool.post do
41
41
  log_internal "Uploading stats for #{to_ship.size} paths"
42
42
 
43
43
  aggregate = Hash.new { |h, k| h[k] = PrefabProto::Logger.new }