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.
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 }