prefab-cloud-ruby 0.24.3 → 0.24.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +1 -1
  3. data/.rubocop.yml +13 -0
  4. data/CHANGELOG.md +78 -0
  5. data/Gemfile.lock +4 -4
  6. data/VERSION +1 -1
  7. data/bin/console +21 -0
  8. data/compile_protos.sh +6 -0
  9. data/lib/prefab/client.rb +25 -4
  10. data/lib/prefab/config_client.rb +17 -6
  11. data/lib/prefab/config_loader.rb +1 -1
  12. data/lib/prefab/config_resolver.rb +2 -4
  13. data/lib/prefab/config_value_wrapper.rb +18 -0
  14. data/lib/prefab/context.rb +22 -2
  15. data/lib/prefab/context_shape.rb +20 -0
  16. data/lib/prefab/context_shape_aggregator.rb +63 -0
  17. data/lib/prefab/criteria_evaluator.rb +61 -41
  18. data/lib/prefab/evaluated_configs_aggregator.rb +60 -0
  19. data/lib/prefab/evaluated_keys_aggregator.rb +41 -0
  20. data/lib/prefab/http_connection.rb +5 -1
  21. data/lib/prefab/local_config_parser.rb +13 -13
  22. data/lib/prefab/log_path_aggregator.rb +64 -0
  23. data/lib/prefab/logger_client.rb +11 -13
  24. data/lib/prefab/options.rb +37 -1
  25. data/lib/prefab/periodic_sync.rb +51 -0
  26. data/lib/prefab/time_helpers.rb +7 -0
  27. data/lib/prefab-cloud-ruby.rb +9 -2
  28. data/lib/prefab_pb.rb +33 -220
  29. data/prefab-cloud-ruby.gemspec +21 -5
  30. data/test/test_config_loader.rb +15 -15
  31. data/test/test_config_resolver.rb +102 -102
  32. data/test/test_config_value_unwrapper.rb +13 -13
  33. data/test/test_context.rb +42 -0
  34. data/test/test_context_shape.rb +51 -0
  35. data/test/test_context_shape_aggregator.rb +137 -0
  36. data/test/test_criteria_evaluator.rb +253 -150
  37. data/test/test_evaluated_configs_aggregator.rb +254 -0
  38. data/test/test_evaluated_keys_aggregator.rb +54 -0
  39. data/test/test_helper.rb +34 -2
  40. data/test/test_log_path_aggregator.rb +57 -0
  41. data/test/test_logger.rb +33 -33
  42. data/test/test_weighted_value_resolver.rb +2 -2
  43. metadata +21 -5
  44. data/lib/prefab/log_path_collector.rb +0 -102
  45. data/test/test_log_path_collector.rb +0 -58
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 29a3d9257f166fad534f81a44879cd82abc0cdd729714ab6feb2423d7b913a4d
4
- data.tar.gz: b11f8e29260760dd64a78f26c016bd936fe0a5e75100329c645915bddd094ebc
3
+ metadata.gz: bf743673dab06e2777321f7fb867417478a383864dd6b982860abbb21375450b
4
+ data.tar.gz: 47a0ba475095d9e773fafc684efc43bc4ac05bc9ec318b70ee321cf2c6b59428
5
5
  SHA512:
6
- metadata.gz: f74cda2cf6f657f72c15d118d9fb622d24d049261436691bff28e42ad5656beb53ec192e148e30e0f025a5becb094682fc68f4860c2cf5b3ed351b7954e3b587
7
- data.tar.gz: 90125b61cd874c12376eecc726aa2fef0d6158565150830f95f642f97fad849b53570238593ce617dc39b05960abb47d4c2e6cd4a974e30c69fc40ec2720958b
6
+ metadata.gz: 5ce8f48e9c4584a34deead09528e1b90cd33a3e80994433d999abb32ccfd31eae7883eedf18b99d8351c55cf00e1e1dc7ef6f0b5a7d45819b300f573ff068533
7
+ data.tar.gz: 416c9f5271b62cef7469db992dde3238c982d15f442f22d3c273c5dfe07935caa1777c26684c98b5ed3c92a67cb41bb10fb9b00d188d4cecaa34507b425fdbb4
@@ -22,7 +22,7 @@ jobs:
22
22
  runs-on: ubuntu-latest
23
23
  strategy:
24
24
  matrix:
25
- ruby-version: ['2.6', '2.7', '3.0', '3.1']
25
+ ruby-version: ['2.7', '3.0', '3.1']
26
26
 
27
27
  steps:
28
28
  - uses: actions/checkout@v3
data/.rubocop.yml ADDED
@@ -0,0 +1,13 @@
1
+ AllCops:
2
+ NewCops: enable
3
+ Exclude:
4
+ - prefab-cloud-ruby.gemspec
5
+ - lib/prefab_pb.rb
6
+
7
+ Metrics:
8
+ Exclude:
9
+ - 'test/*.rb'
10
+
11
+ Layout/LineLength:
12
+ Exclude:
13
+ - 'test/*.rb'
data/CHANGELOG.md ADDED
@@ -0,0 +1,78 @@
1
+ # Changelog
2
+
3
+ ## 0.24.5 - 2023-07-10
4
+
5
+ - Report Client Version (#121)
6
+
7
+ ## [0.24.4] - 2023-07-06
8
+
9
+ - Support Timed Loggers (#119)
10
+ - Added EvaluatedConfigsAggregator (disabled by default) (#118)
11
+ - Added EvaluatedKeysAggregator (disabled by default) (#117)
12
+ - Dropped Ruby 2.6 support (#116)
13
+ - Capture/report context shapes (#115)
14
+ - Added bin/console (#114)
15
+
16
+ ## [0.24.3] - 2023-05-15
17
+
18
+ - Add JSON log formatter (#106)
19
+
20
+ # [0.24.2] - 2023-05-12
21
+
22
+ - Fix bug in FF rollout eval consistency (#108)
23
+ - Simplify forking (#107)
24
+
25
+ # [0.24.1] - 2023-04-26
26
+
27
+ - Fix misleading deprecation warning (#105)
28
+
29
+ # [0.24.0] - 2023-04-26
30
+
31
+ - Backwards compatibility for JIT context (#104)
32
+ - Remove upsert (#103)
33
+ - Add resolver presenter and `on_update` callback (#102)
34
+ - Deprecate `lookup_key` and introduce Context (#99)
35
+
36
+ # [0.23.8] - 2023-04-21
37
+
38
+ - Update protobuf (#101)
39
+
40
+ # [0.23.7] - 2023-04-21
41
+
42
+ - Guard against ActiveJob not being loaded (#100)
43
+
44
+ # [0.23.6] - 2023-04-17
45
+
46
+ - Fix bug in FF rollout eval consistency (#98)
47
+ - Add tests for block-form of logging (#96)
48
+
49
+ # [0.23.5] - 2023-04-13
50
+
51
+ - Cast the value to string when checking presence in string list (#95)
52
+
53
+ # [0.23.4] - 2023-04-12
54
+
55
+ - Remove GRPC (#93)
56
+
57
+ # [0.23.3] - 2023-04-07
58
+
59
+ - Use exponential backoff for log level uploading (#92)
60
+
61
+ # [0.23.2] - 2023-04-04
62
+
63
+ - Move log collection logs from INFO to DEBUG (#91)
64
+ - Fix: Handle trailing slash in PREFAB_API_URL (#90)
65
+
66
+ # [0.23.1] - 2023-03-30
67
+
68
+ - ActiveStorage not defined in Rails < 5.2 (#87)
69
+
70
+ # [0.23.0] - 2023-03-28
71
+
72
+ - Convenience for setting Rails.logger (#85)
73
+ - Log evaluation according to rules (#81)
74
+
75
+ # [0.22.0] - 2023-03-15
76
+
77
+ - Report log paths and usages (#79)
78
+ - Accept hash or keyword args in `initialize` (#78)
data/Gemfile.lock CHANGED
@@ -66,7 +66,7 @@ GEM
66
66
  rake (~> 13.0)
67
67
  macaddr (1.7.2)
68
68
  systemu (~> 2.6.5)
69
- mini_portile2 (2.8.0)
69
+ mini_portile2 (2.8.2)
70
70
  minitest (5.16.2)
71
71
  minitest-focus (1.3.1)
72
72
  minitest (>= 4, < 6)
@@ -78,8 +78,8 @@ GEM
78
78
  multi_json (1.15.0)
79
79
  multi_xml (0.6.0)
80
80
  multipart-post (2.1.1)
81
- nokogiri (1.13.10)
82
- mini_portile2 (~> 2.8.0)
81
+ nokogiri (1.15.2)
82
+ mini_portile2 (~> 2.8.2)
83
83
  racc (~> 1.4)
84
84
  oauth2 (1.4.11)
85
85
  faraday (>= 0.17.3, < 3.0)
@@ -89,7 +89,7 @@ GEM
89
89
  rack (>= 1.2, < 4)
90
90
  psych (3.3.1)
91
91
  public_suffix (4.0.6)
92
- racc (1.6.1)
92
+ racc (1.7.0)
93
93
  rack (3.0.6.1)
94
94
  rake (13.0.6)
95
95
  rchardet (1.8.0)
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.24.3
1
+ 0.24.5
data/bin/console ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'irb'
5
+ require 'bundler/setup'
6
+
7
+ gemspec = Dir.glob(File.expand_path("../../*.gemspec", __FILE__)).first
8
+ spec = Gem::Specification.load(gemspec)
9
+
10
+ # Add the require paths to the $LOAD_PATH
11
+ spec.require_paths.each do |path|
12
+ full_path = File.expand_path("../" + path, __dir__)
13
+ $LOAD_PATH.unshift(full_path) unless $LOAD_PATH.include?(full_path)
14
+ end
15
+
16
+ spec.require_paths.each do |path|
17
+ require "./lib/prefab-cloud-ruby"
18
+ end
19
+
20
+ # Start an IRB session
21
+ IRB.start(__FILE__)
data/compile_protos.sh CHANGED
@@ -1,5 +1,11 @@
1
1
  #!/usr/bin/env bash
2
+
3
+ gem install grpc-tools
4
+
2
5
  grpc_tools_ruby_protoc -I ../prefab-cloud/ --ruby_out=lib --grpc_out=lib prefab.proto
6
+
7
+ gsed -i 's/^module Prefab$/module PrefabProto/g' lib/prefab_pb.rb
8
+
3
9
  # on M1 you need to
4
10
  # 1. run in rosetta
5
11
  # 2. mv gems/2.6.0/gems/grpc-tools-1.43.1/bin/x86_64-macos x86-macos
data/lib/prefab/client.rb CHANGED
@@ -53,17 +53,38 @@ module Prefab
53
53
  @feature_flag_client ||= Prefab::FeatureFlagClient.new(self)
54
54
  end
55
55
 
56
- def log_path_collector
56
+ def log_path_aggregator
57
57
  return nil if @options.collect_max_paths <= 0
58
58
 
59
- @log_path_collector ||= LogPathCollector.new(client: self, max_paths: @options.collect_max_paths,
60
- sync_interval: @options.collect_sync_interval)
59
+ @log_path_aggregator ||= LogPathAggregator.new(client: self, max_paths: @options.collect_max_paths,
60
+ sync_interval: @options.collect_sync_interval)
61
61
  end
62
62
 
63
63
  def log
64
64
  @logger_client ||= Prefab::LoggerClient.new(@options.logdev, formatter: @options.log_formatter,
65
65
  prefix: @options.log_prefix,
66
- log_path_collector: log_path_collector)
66
+ log_path_aggregator: log_path_aggregator)
67
+ end
68
+
69
+ def context_shape_aggregator
70
+ return nil if @options.collect_max_shapes <= 0
71
+
72
+ @context_shape_aggregator ||= ContextShapeAggregator.new(client: self, max_shapes: @options.collect_max_shapes,
73
+ sync_interval: @options.collect_sync_interval)
74
+ end
75
+
76
+ def evaluated_keys_aggregator
77
+ return nil if @options.collect_max_keys <= 0
78
+
79
+ @evaluated_keys_aggregator ||= EvaluatedKeysAggregator.new(client: self, max_keys: @options.collect_max_keys,
80
+ sync_interval: @options.collect_sync_interval)
81
+ end
82
+
83
+ def evaluated_configs_aggregator
84
+ return nil if @options.collect_max_evaluations <= 0
85
+
86
+ @evaluated_configs_aggregator ||= EvaluatedConfigsAggregator.new(client: self, max_configs: @options.collect_max_evaluations,
87
+ sync_interval: @options.collect_sync_interval)
67
88
  end
68
89
 
69
90
  def set_rails_loggers
@@ -6,6 +6,7 @@ module Prefab
6
6
  DEFAULT_CHECKPOINT_FREQ_SEC = 60
7
7
  SSE_READ_TIMEOUT = 300
8
8
  AUTH_USER = 'authuser'
9
+ LOGGING_KEY_PREFIX = "#{Prefab::LoggerClient::BASE_KEY}#{Prefab::LoggerClient::SEP}".freeze
9
10
 
10
11
  def initialize(base_client, timeout)
11
12
  @base_client = base_client
@@ -50,15 +51,24 @@ module Prefab
50
51
  end
51
52
 
52
53
  def self.value_to_delta(key, config_value, namespace = nil)
53
- Prefab::Config.new(key: [namespace, key].compact.join(':'),
54
- rows: [Prefab::ConfigRow.new(value: config_value)])
54
+ PrefabProto::Config.new(key: [namespace, key].compact.join(':'),
55
+ rows: [PrefabProto::ConfigRow.new(value: config_value)])
55
56
  end
56
57
 
57
58
  def get(key, default = NO_DEFAULT_PROVIDED, properties = NO_DEFAULT_PROVIDED)
58
59
  context = @config_resolver.make_context(properties)
60
+
59
61
  value = _get(key, context)
60
62
 
63
+ @base_client.context_shape_aggregator&.push(context)
64
+ @base_client.evaluated_keys_aggregator&.push(key)
65
+
61
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
71
+
62
72
  Prefab::ConfigValueUnwrapper.unwrap(value, key, context)
63
73
  else
64
74
  handle_default(key, default)
@@ -120,7 +130,7 @@ module Prefab
120
130
  def load_url(conn, source)
121
131
  resp = conn.get('')
122
132
  if resp.status == 200
123
- configs = Prefab::Configs.decode(resp.body)
133
+ configs = PrefabProto::Configs.decode(resp.body)
124
134
  load_configs(configs, source)
125
135
  true
126
136
  else
@@ -181,8 +191,9 @@ module Prefab
181
191
  auth = "#{AUTH_USER}:#{@base_client.api_key}"
182
192
  auth_string = Base64.strict_encode64(auth)
183
193
  headers = {
184
- 'x-prefab-start-at-id': start_at_id,
185
- 'Authorization': "Basic #{auth_string}"
194
+ 'x-prefab-start-at-id' => start_at_id,
195
+ 'Authorization' => "Basic #{auth_string}",
196
+ 'X-PrefabCloud-Client-Version' => "prefab-cloud-ruby-#{Prefab::VERSION}"
186
197
  }
187
198
  url = "#{@base_client.prefab_api_url}/api/v1/sse/config"
188
199
  @base_client.log_internal ::Logger::INFO, "SSE Streaming Connect to #{url} start_at #{start_at_id}"
@@ -191,7 +202,7 @@ module Prefab
191
202
  read_timeout: SSE_READ_TIMEOUT,
192
203
  logger: Prefab::SseLogger.new(@base_client.log)) do |client|
193
204
  client.on_event do |event|
194
- configs = Prefab::Configs.decode(Base64.decode64(event.data))
205
+ configs = PrefabProto::Configs.decode(Base64.decode64(event.data))
195
206
  load_configs(configs, :sse)
196
207
  end
197
208
  end
@@ -42,7 +42,7 @@ module Prefab
42
42
  end
43
43
 
44
44
  def get_api_deltas
45
- configs = Prefab::Configs.new
45
+ configs = PrefabProto::Configs.new
46
46
  @api_config.each_value do |config_value|
47
47
  configs.configs << config_value[:config]
48
48
  end
@@ -22,9 +22,7 @@ module Prefab
22
22
  end
23
23
 
24
24
  def raw(key)
25
- via_key = @local_store[key]
26
-
27
- via_key ? via_key[:config] : nil
25
+ @local_store.dig(key, :config)
28
26
  end
29
27
 
30
28
  def get(key, properties = NO_DEFAULT_PROVIDED)
@@ -33,7 +31,7 @@ module Prefab
33
31
 
34
32
  return nil unless raw_config
35
33
 
36
- evaluate(raw(key), properties)
34
+ evaluate(raw_config, properties)
37
35
  end
38
36
  end
39
37
 
@@ -0,0 +1,18 @@
1
+ module Prefab
2
+ class ConfigValueWrapper
3
+ def self.wrap(value)
4
+ case value
5
+ when Integer
6
+ PrefabProto::ConfigValue.new(int: value)
7
+ when Float
8
+ PrefabProto::ConfigValue.new(double: value)
9
+ when TrueClass, FalseClass
10
+ PrefabProto::ConfigValue.new(bool: value)
11
+ when Array
12
+ PrefabProto::ConfigValue.new(string_list: Prefab::StringList.new(values: value.map(&:to_s)))
13
+ else
14
+ PrefabProto::ConfigValue.new(string: value.to_s)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -89,11 +89,11 @@ module Prefab
89
89
  key = property_key
90
90
  end
91
91
 
92
- contexts[name] && contexts[name].get(key)
92
+ contexts[name]&.get(key)
93
93
  end
94
94
 
95
95
  def to_h
96
- contexts.map { |name, context| [name, context.to_h] }.to_h
96
+ contexts.transform_values(&:to_h)
97
97
  end
98
98
 
99
99
  def clear
@@ -103,5 +103,25 @@ module Prefab
103
103
  def context(name)
104
104
  contexts[name.to_s] || NamedContext.new(name, {})
105
105
  end
106
+
107
+ def to_proto(namespace)
108
+ prefab_context = {
109
+ 'current-time' => ConfigValueWrapper.wrap(Prefab::TimeHelpers.now_in_ms)
110
+ }
111
+
112
+ prefab_context['namespace'] = ConfigValueWrapper.wrap(namespace) if namespace&.length&.positive?
113
+
114
+ PrefabProto::ContextSet.new(
115
+ 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
+ )
122
+ end.concat([PrefabProto::Context.new(type: 'prefab',
123
+ values: prefab_context)])
124
+ )
125
+ end
106
126
  end
107
127
  end
@@ -0,0 +1,20 @@
1
+ module Prefab
2
+ class ContextShape
3
+ MAPPING = {
4
+ Integer => 1,
5
+ String => 2,
6
+ Float => 4,
7
+ TrueClass => 5,
8
+ FalseClass => 5,
9
+ Array => 10,
10
+ }.freeze
11
+
12
+ # We default to String if the type isn't a primitive we support.
13
+ # This is because we do a `to_s` in the CriteriaEvaluator.
14
+ DEFAULT = MAPPING[String]
15
+
16
+ def self.field_type_number(value)
17
+ MAPPING.fetch(value.class, DEFAULT)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'periodic_sync'
4
+
5
+ module Prefab
6
+ class ContextShapeAggregator
7
+ include Prefab::PeriodicSync
8
+
9
+ attr_reader :data
10
+
11
+ def initialize(client:, max_shapes:, sync_interval:)
12
+ @max_shapes = max_shapes
13
+ @client = client
14
+ @name = 'context_shape_aggregator'
15
+
16
+ @data = Concurrent::Set.new
17
+
18
+ start_periodic_sync(sync_interval)
19
+ end
20
+
21
+ def push(context)
22
+ return if @data.size >= @max_shapes
23
+
24
+ context.contexts.each_pair do |name, name_context|
25
+ name_context.to_h.each_pair do |key, value|
26
+ @data.add [name, key, Prefab::ContextShape.field_type_number(value)]
27
+ end
28
+ end
29
+ end
30
+
31
+ def prepare_data
32
+ duped = @data.dup
33
+ @data.clear
34
+
35
+ duped.inject({}) do |acc, (name, key, type)|
36
+ acc[name] ||= {}
37
+ acc[name][key] = type
38
+ acc
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def flush(to_ship, _)
45
+ @pool.post do
46
+ log_internal "Uploading context shapes for #{to_ship.values.size}"
47
+
48
+ shapes = PrefabProto::ContextShapes.new(
49
+ shapes: to_ship.map do |name, shape|
50
+ PrefabProto::ContextShape.new(
51
+ name: name,
52
+ field_types: shape
53
+ )
54
+ end
55
+ )
56
+
57
+ result = @client.post('/api/v1/context-shapes', shapes)
58
+
59
+ log_internal "Uploaded #{to_ship.values.size} shapes: #{result.status}"
60
+ end
61
+ end
62
+ end
63
+ end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # rubocop:disable Naming/MethodName
4
+ # We're intentionally keeping the UPCASED method names to match the protobuf
5
+ # and avoid wasting CPU cycles lowercasing things
3
6
  module Prefab
4
7
  class CriteriaEvaluator
5
8
  NAMESPACE_KEY = 'NAMESPACE'
@@ -27,45 +30,55 @@ module Prefab
27
30
 
28
31
  def all_criteria_match?(conditional_value, props)
29
32
  conditional_value.criteria.all? do |criterion|
30
- evaluate_criteron(criterion, props)
33
+ public_send(criterion.operator, criterion, props)
31
34
  end
32
35
  end
33
36
 
34
- def evaluate_criteron(criterion, properties)
35
- case criterion.operator
36
- when :IN_SEG
37
- return in_segment?(criterion, properties)
38
- when :NOT_IN_SEG
39
- return !in_segment?(criterion, properties)
40
- when :ALWAYS_TRUE
41
- return true
42
- end
37
+ def IN_SEG(criterion, properties)
38
+ in_segment?(criterion, properties)
39
+ end
43
40
 
44
- value_from_properties = criterion.property_name === NAMESPACE_KEY ? @namespace : properties.get(criterion.property_name)
45
-
46
- case criterion.operator
47
- when :PROP_IS_ONE_OF
48
- matches?(criterion, value_from_properties, properties)
49
- when :PROP_IS_NOT_ONE_OF
50
- !matches?(criterion, value_from_properties, properties)
51
- when :PROP_ENDS_WITH_ONE_OF
52
- return false unless value_from_properties
53
-
54
- criterion.value_to_match.string_list.values.any? do |ending|
55
- value_from_properties.end_with?(ending)
56
- end
57
- when :PROP_DOES_NOT_END_WITH_ONE_OF
58
- return true unless value_from_properties
59
-
60
- criterion.value_to_match.string_list.values.none? do |ending|
61
- value_from_properties.end_with?(ending)
62
- end
63
- when :HIERARCHICAL_MATCH
64
- value_from_properties.start_with?(criterion.value_to_match.string)
65
- else
66
- @base_client.log.info("Unknown Operator: #{criterion.operator}")
67
- false
68
- end
41
+ def NOT_IN_SEG(criterion, properties)
42
+ !in_segment?(criterion, properties)
43
+ end
44
+
45
+ def ALWAYS_TRUE(_criterion, _properties)
46
+ true
47
+ end
48
+
49
+ def PROP_IS_ONE_OF(criterion, properties)
50
+ matches?(criterion, value_from_properties(criterion, properties), properties)
51
+ end
52
+
53
+ def PROP_IS_NOT_ONE_OF(criterion, properties)
54
+ !matches?(criterion, value_from_properties(criterion, properties), properties)
55
+ end
56
+
57
+ def PROP_ENDS_WITH_ONE_OF(criterion, properties)
58
+ prop_ends_with_one_of?(criterion, value_from_properties(criterion, properties))
59
+ end
60
+
61
+ def PROP_DOES_NOT_END_WITH_ONE_OF(criterion, properties)
62
+ !prop_ends_with_one_of?(criterion, value_from_properties(criterion, properties))
63
+ end
64
+
65
+ def HIERARCHICAL_MATCH(criterion, properties)
66
+ value = value_from_properties(criterion, properties)
67
+ value&.start_with?(criterion.value_to_match.string)
68
+ end
69
+
70
+ def IN_INT_RANGE(criterion, properties)
71
+ value = if criterion.property_name == 'prefab.current-time'
72
+ Time.now.utc.to_i * 1000
73
+ else
74
+ value_from_properties(criterion, properties)
75
+ end
76
+
77
+ value && value >= criterion.value_to_match.int_range.start && value < criterion.value_to_match.int_range.end
78
+ end
79
+
80
+ def value_from_properties(criterion, properties)
81
+ criterion.property_name == NAMESPACE_KEY ? @namespace : properties.get(criterion.property_name)
69
82
  end
70
83
 
71
84
  private
@@ -81,24 +94,31 @@ module Prefab
81
94
  def in_segment?(criterion, properties)
82
95
  segment = @resolver.get(criterion.value_to_match.string, properties)
83
96
 
84
- if !segment
85
- @base_client.log.info( "Segment #{criterion.value_to_match.string} not found")
86
- end
97
+ @base_client.log.info("Segment #{criterion.value_to_match.string} not found") unless segment
87
98
 
88
99
  segment&.bool
89
100
  end
90
101
 
91
- def matches?(criterion, value_from_properties, properties)
102
+ def matches?(criterion, value, properties)
92
103
  criterion_value_or_values = Prefab::ConfigValueUnwrapper.unwrap(criterion.value_to_match, @config.key, properties)
93
104
 
94
105
  case criterion_value_or_values
95
106
  when Google::Protobuf::RepeatedField
96
107
  # we to_s the value from properties for comparison because the
97
108
  # criterion_value_or_values is a list of strings
98
- criterion_value_or_values.include?(value_from_properties.to_s)
109
+ criterion_value_or_values.include?(value.to_s)
99
110
  else
100
- criterion_value_or_values == value_from_properties
111
+ criterion_value_or_values == value
112
+ end
113
+ end
114
+
115
+ def prop_ends_with_one_of?(criterion, value)
116
+ return false unless value
117
+
118
+ criterion.value_to_match.string_list.values.any? do |ending|
119
+ value.end_with?(ending)
101
120
  end
102
121
  end
103
122
  end
104
123
  end
124
+ # rubocop:enable Naming/MethodName
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'periodic_sync'
4
+
5
+ module Prefab
6
+ class EvaluatedConfigsAggregator
7
+ include Prefab::PeriodicSync
8
+
9
+ attr_reader :data
10
+
11
+ def initialize(client:, max_configs:, sync_interval:)
12
+ @max_configs = max_configs
13
+ @client = client
14
+ @name = 'evaluated_configs_aggregator'
15
+
16
+ @data = Concurrent::Array.new
17
+
18
+ start_periodic_sync(sync_interval)
19
+ end
20
+
21
+ def push(evaluation)
22
+ return if @data.size >= @max_configs
23
+
24
+ @data.push(evaluation)
25
+ end
26
+
27
+ def prepare_data
28
+ to_ship = @data.dup
29
+ @data.clear
30
+
31
+ to_ship.map { |e| coerce_to_proto(e) }
32
+ end
33
+
34
+ def coerce_to_proto(evaluation)
35
+ config, result, context = evaluation
36
+
37
+ PrefabProto::EvaluatedConfig.new(
38
+ key: config.key,
39
+ config_version: config.id,
40
+ result: result,
41
+ context: context.to_proto(@client.namespace),
42
+ timestamp: Prefab::TimeHelpers.now_in_ms
43
+ )
44
+ end
45
+
46
+ private
47
+
48
+ def flush(to_ship, _)
49
+ @pool.post do
50
+ log_internal "Uploading evaluated configs for #{to_ship.size}"
51
+
52
+ configs = PrefabProto::EvaluatedConfigs.new(configs: to_ship)
53
+
54
+ result = @client.post('/api/v1/evaluated-configs', configs)
55
+
56
+ log_internal "Uploaded #{to_ship.size} configs: #{result.status}"
57
+ end
58
+ end
59
+ end
60
+ end