prefab-cloud-ruby 0.24.3 → 0.24.4

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 (44) 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 +76 -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 +16 -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/local_config_parser.rb +13 -13
  21. data/lib/prefab/log_path_aggregator.rb +64 -0
  22. data/lib/prefab/logger_client.rb +11 -13
  23. data/lib/prefab/options.rb +37 -1
  24. data/lib/prefab/periodic_sync.rb +51 -0
  25. data/lib/prefab/time_helpers.rb +7 -0
  26. data/lib/prefab-cloud-ruby.rb +8 -1
  27. data/lib/prefab_pb.rb +33 -220
  28. data/prefab-cloud-ruby.gemspec +21 -5
  29. data/test/test_config_loader.rb +15 -15
  30. data/test/test_config_resolver.rb +102 -102
  31. data/test/test_config_value_unwrapper.rb +13 -13
  32. data/test/test_context.rb +42 -0
  33. data/test/test_context_shape.rb +51 -0
  34. data/test/test_context_shape_aggregator.rb +137 -0
  35. data/test/test_criteria_evaluator.rb +253 -150
  36. data/test/test_evaluated_configs_aggregator.rb +254 -0
  37. data/test/test_evaluated_keys_aggregator.rb +54 -0
  38. data/test/test_helper.rb +34 -2
  39. data/test/test_log_path_aggregator.rb +57 -0
  40. data/test/test_logger.rb +33 -33
  41. data/test/test_weighted_value_resolver.rb +2 -2
  42. metadata +21 -5
  43. data/lib/prefab/log_path_collector.rb +0 -102
  44. 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: a88b7d4c3a88af4b66a623208ec2673453ce763660d2cca7993e0623c39e35e5
4
+ data.tar.gz: 125b830fb204a7085fa6a3e5b365646f27d8bc4bcf42df0c2a6f2f0e0925dcd9
5
5
  SHA512:
6
- metadata.gz: f74cda2cf6f657f72c15d118d9fb622d24d049261436691bff28e42ad5656beb53ec192e148e30e0f025a5becb094682fc68f4860c2cf5b3ed351b7954e3b587
7
- data.tar.gz: 90125b61cd874c12376eecc726aa2fef0d6158565150830f95f642f97fad849b53570238593ce617dc39b05960abb47d4c2e6cd4a974e30c69fc40ec2720958b
6
+ metadata.gz: 7a73efdcad8211cd709f0b074db0bf46ae448362387ba6215c94314050087988909d3993a8377422a6a6ef5e09da703f3f40b017b751025d6dbbcbaffd35d012
7
+ data.tar.gz: 3e2b3e9cbf7686843abfc13f948a87840da068a823c9740eb0ceba7ea13a24502f249aebb1d1fa1931a735b2042ee989b291b8d513e1504ba534df30ab5658ea
@@ -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,76 @@
1
+ # Changelog
2
+
3
+ ## Unreleased
4
+
5
+ ## [0.24.4] - 2023-07-06
6
+
7
+ - Support Timed Loggers (#119)
8
+ - Added EvaluatedConfigsAggregator (disabled by default) (#118)
9
+ - Added EvaluatedKeysAggregator (disabled by default) (#117)
10
+ - Dropped Ruby 2.6 support (#116)
11
+ - Capture/report context shapes (#115)
12
+ - Added bin/console (#114)
13
+
14
+ ## [0.24.3] - 2023-05-15
15
+
16
+ - Add JSON log formatter (#106)
17
+
18
+ # [0.24.2] - 2023-05-12
19
+
20
+ - Fix bug in FF rollout eval consistency (#108)
21
+ - Simplify forking (#107)
22
+
23
+ # [0.24.1] - 2023-04-26
24
+
25
+ - Fix misleading deprecation warning (#105)
26
+
27
+ # [0.24.0] - 2023-04-26
28
+
29
+ - Backwards compatibility for JIT context (#104)
30
+ - Remove upsert (#103)
31
+ - Add resolver presenter and `on_update` callback (#102)
32
+ - Deprecate `lookup_key` and introduce Context (#99)
33
+
34
+ # [0.23.8] - 2023-04-21
35
+
36
+ - Update protobuf (#101)
37
+
38
+ # [0.23.7] - 2023-04-21
39
+
40
+ - Guard against ActiveJob not being loaded (#100)
41
+
42
+ # [0.23.6] - 2023-04-17
43
+
44
+ - Fix bug in FF rollout eval consistency (#98)
45
+ - Add tests for block-form of logging (#96)
46
+
47
+ # [0.23.5] - 2023-04-13
48
+
49
+ - Cast the value to string when checking presence in string list (#95)
50
+
51
+ # [0.23.4] - 2023-04-12
52
+
53
+ - Remove GRPC (#93)
54
+
55
+ # [0.23.3] - 2023-04-07
56
+
57
+ - Use exponential backoff for log level uploading (#92)
58
+
59
+ # [0.23.2] - 2023-04-04
60
+
61
+ - Move log collection logs from INFO to DEBUG (#91)
62
+ - Fix: Handle trailing slash in PREFAB_API_URL (#90)
63
+
64
+ # [0.23.1] - 2023-03-30
65
+
66
+ - ActiveStorage not defined in Rails < 5.2 (#87)
67
+
68
+ # [0.23.0] - 2023-03-28
69
+
70
+ - Convenience for setting Rails.logger (#85)
71
+ - Log evaluation according to rules (#81)
72
+
73
+ # [0.22.0] - 2023-03-15
74
+
75
+ - Report log paths and usages (#79)
76
+ - 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.4
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,8 @@ 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}"
186
196
  }
187
197
  url = "#{@base_client.prefab_api_url}/api/v1/sse/config"
188
198
  @base_client.log_internal ::Logger::INFO, "SSE Streaming Connect to #{url} start_at #{start_at_id}"
@@ -191,7 +201,7 @@ module Prefab
191
201
  read_timeout: SSE_READ_TIMEOUT,
192
202
  logger: Prefab::SseLogger.new(@base_client.log)) do |client|
193
203
  client.on_event do |event|
194
- configs = Prefab::Configs.decode(Base64.decode64(event.data))
204
+ configs = PrefabProto::Configs.decode(Base64.decode64(event.data))
195
205
  load_configs(configs, :sse)
196
206
  end
197
207
  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