prefab-cloud-ruby 1.7.2 → 1.8.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 90c54906938e69580ab5b2aad4407560268e01d90ed4f2d3f595c51a1a308b9a
4
- data.tar.gz: 632d29a45418a731cf6bb536dacc32aadcc2f8559326f8b195069250a71ac4ff
3
+ metadata.gz: b8b27e64dfd37c521a1d0397cbfa40d0f3c1ce5d1f42e339b116cfb4da7c9f58
4
+ data.tar.gz: 9b4f432823d5653b73cd6f33ccf0a6630f0d69d6ba7ea6e9155254ca3da3e26e
5
5
  SHA512:
6
- metadata.gz: 1921b09367845ee76bd84604f002bffc735139c3765417053ab8e5b73f059a1e909dec105249e38933f0dcf1f1d2bab9374c33c7703c79952da3617a22108817
7
- data.tar.gz: acfdbd82dbef635e9e3703ed3d3b9111d5a47e5382465406cd688b80c1104e6479fcb80ef2dc243be0fc9f6c8d3b2ab265a1541345ca7017e92dbbff6b7511ca
6
+ metadata.gz: df2e756faf5af90410e442d87d861ee5d6507fb9dd681201bb2c56418f8ddc662cb87d100dde617a501c563a04452d8f712e56ea784a94bc2a67c380a992dfc5
7
+ data.tar.gz: 2883ed43fb3c579d60efed468c7011ac6a76eb40e2f58737b5f8a7525c337ee5cbbbdbc0b5e3cad160e15c0c13c62ae22cdf1bd14827515f58347e5f13b4aa3b
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.8.1 - 2024-09-03
4
+
5
+ - Fix SSE reconnection bug (#197)
6
+
7
+ ## 1.8.0 - 2024-08-22
8
+
9
+ - Load config from belt and failover to suspenders (#195)
10
+
3
11
  ## 1.7.2 - 2024-06-24
4
12
 
5
13
  - Support JSON config values (#194)
data/Gemfile.lock CHANGED
@@ -101,7 +101,7 @@ GEM
101
101
  rake (~> 13.0)
102
102
  macaddr (1.7.2)
103
103
  systemu (~> 2.6.5)
104
- mini_portile2 (2.8.5)
104
+ mini_portile2 (2.8.7)
105
105
  minitest (5.22.3)
106
106
  minitest-focus (1.4.0)
107
107
  minitest (>= 4, < 6)
@@ -114,7 +114,7 @@ GEM
114
114
  multi_xml (0.6.0)
115
115
  multipart-post (2.4.0)
116
116
  mutex_m (0.2.0)
117
- nokogiri (1.16.3)
117
+ nokogiri (1.16.7)
118
118
  mini_portile2 (~> 2.8.2)
119
119
  racc (~> 1.4)
120
120
  oauth2 (1.4.11)
@@ -126,7 +126,7 @@ GEM
126
126
  psych (5.1.2)
127
127
  stringio
128
128
  public_suffix (5.0.4)
129
- racc (1.7.3)
129
+ racc (1.8.1)
130
130
  rack (3.0.10)
131
131
  rake (13.1.0)
132
132
  rchardet (1.8.0)
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.7.2
1
+ 1.8.1
data/dev/console CHANGED
@@ -17,5 +17,11 @@ spec.require_paths.each do |path|
17
17
  require "./lib/prefab-cloud-ruby"
18
18
  end
19
19
 
20
+ SemanticLogger.add_appender(io: $stdout)
21
+
22
+ if !ENV['PREFAB_LOG_CLIENT_BOOTSTRAP_LOG_LEVEL']
23
+ puts "run with PREFAB_LOG_CLIENT_BOOTSTRAP_LOG_LEVEL=debug (or trace) for more output"
24
+ end
25
+
20
26
  # Start an IRB session
21
27
  IRB.start(__FILE__)
data/lib/prefab/client.rb CHANGED
@@ -8,7 +8,7 @@ module Prefab
8
8
  MAX_SLEEP_SEC = 10
9
9
  BASE_SLEEP_SEC = 0.5
10
10
 
11
- attr_reader :namespace, :interceptor, :api_key, :prefab_api_url, :options, :instance_hash
11
+ attr_reader :namespace, :interceptor, :api_key, :options, :instance_hash
12
12
 
13
13
  def initialize(options = Prefab::Options.new)
14
14
  @options = options.is_a?(Prefab::Options) ? options : Prefab::Options.new(options)
@@ -23,9 +23,6 @@ module Prefab
23
23
  else
24
24
  @api_key = @options.api_key
25
25
  raise Prefab::Errors::InvalidApiKeyError, @api_key if @api_key.nil? || @api_key.empty? || api_key.count('-') < 1
26
-
27
- @prefab_api_url = @options.prefab_api_url
28
- LOG.debug "Prefab Connecting to: #{@prefab_api_url}"
29
26
  end
30
27
 
31
28
  context.clear
@@ -48,6 +45,10 @@ module Prefab
48
45
  @config_client ||= Prefab::ConfigClient.new(self, timeout)
49
46
  end
50
47
 
48
+ def stop
49
+ @config_client.stop
50
+ end
51
+
51
52
  def feature_flag_client
52
53
  @feature_flag_client ||= Prefab::FeatureFlagClient.new(self)
53
54
  end
@@ -111,7 +112,7 @@ module Prefab
111
112
  end
112
113
 
113
114
  def post(path, body)
114
- Prefab::HttpConnection.new(@options.prefab_api_url, @api_key).post(path, body)
115
+ Prefab::HttpConnection.new(@options.telemetry_destination, @api_key).post(path, body)
115
116
  end
116
117
 
117
118
  def inspect
@@ -3,19 +3,15 @@
3
3
  module Prefab
4
4
  class ConfigClient
5
5
  LOG = Prefab::InternalLogger.new(self)
6
- RECONNECT_WAIT = 5
7
6
  DEFAULT_CHECKPOINT_FREQ_SEC = 60
8
- SSE_READ_TIMEOUT = 300
9
7
  STALE_CACHE_WARN_HOURS = 5
10
- AUTH_USER = 'authuser'
8
+
11
9
  def initialize(base_client, timeout)
12
10
  @base_client = base_client
13
11
  @options = base_client.options
14
12
  LOG.debug 'Initialize ConfigClient'
15
13
  @timeout = timeout
16
14
 
17
- @stream_lock = Concurrent::ReadWriteLock.new
18
-
19
15
  @checkpoint_freq_secs = DEFAULT_CHECKPOINT_FREQ_SEC
20
16
 
21
17
  @config_loader = Prefab::ConfigLoader.new(@base_client)
@@ -34,9 +30,27 @@ module Prefab
34
30
  end
35
31
  end
36
32
 
33
+ def stop
34
+ @sse_config_client&.close
35
+ end
36
+
37
37
  def start_streaming
38
- @stream_lock.with_write_lock do
39
- start_sse_streaming_connection_thread(@config_loader.highwater_mark) if @streaming_thread.nil?
38
+ Thread.new do
39
+ # wait for the config loader to have a highwater mark before we connect the SSE
40
+ loop do
41
+ break if @config_loader.highwater_mark > 0
42
+
43
+ sleep 0.1
44
+ end
45
+
46
+ stream_lock = Concurrent::ReadWriteLock.new
47
+ @sse_config_client = Prefab::SSEConfigClient.new(@options, @config_loader)
48
+
49
+ @sse_config_client.start do |configs|
50
+ stream_lock.with_write_lock do
51
+ load_configs(configs, :sse)
52
+ end
53
+ end
40
54
  end
41
55
  end
42
56
 
@@ -104,29 +118,24 @@ module Prefab
104
118
  end
105
119
 
106
120
  def load_checkpoint
107
- success = load_checkpoint_api_cdn
108
-
109
- return if success
110
-
111
- success = load_checkpoint_api
112
-
121
+ success = load_source_checkpoint
113
122
  return if success
114
123
 
115
124
  success = load_cache
116
-
117
125
  return if success
118
126
 
119
127
  LOG.warn 'No success loading checkpoints'
120
128
  end
121
129
 
122
- def load_checkpoint_api_cdn
123
- conn = Prefab::HttpConnection.new("#{@options.url_for_api_cdn}/api/v1/configs/0", @base_client.api_key)
124
- load_url(conn, :remote_cdn_api)
125
- end
130
+ def load_source_checkpoint
131
+ @options.config_sources.each do |source|
132
+ conn = Prefab::HttpConnection.new("#{source}/api/v1/configs/0", @base_client.api_key)
133
+ result = load_url(conn, :remote_api)
126
134
 
127
- def load_checkpoint_api
128
- conn = Prefab::HttpConnection.new("#{@options.prefab_api_url}/api/v1/configs/0", @base_client.api_key)
129
- load_url(conn, :remote_api)
135
+ return true if result
136
+ end
137
+
138
+ return false
130
139
  end
131
140
 
132
141
  def load_url(conn, source)
@@ -263,26 +272,5 @@ module Prefab
263
272
  LOG.info presenter.to_s
264
273
  LOG.debug to_s
265
274
  end
266
-
267
- def start_sse_streaming_connection_thread(start_at_id)
268
- auth = "#{AUTH_USER}:#{@base_client.api_key}"
269
- auth_string = Base64.strict_encode64(auth)
270
- headers = {
271
- 'x-prefab-start-at-id' => start_at_id,
272
- 'Authorization' => "Basic #{auth_string}",
273
- 'X-PrefabCloud-Client-Version' => "prefab-cloud-ruby-#{Prefab::VERSION}"
274
- }
275
- url = "#{@base_client.prefab_api_url}/api/v1/sse/config"
276
- LOG.debug "SSE Streaming Connect to #{url} start_at #{start_at_id}"
277
- @streaming_thread = SSE::Client.new(url,
278
- headers: headers,
279
- read_timeout: SSE_READ_TIMEOUT,
280
- logger: Prefab::InternalLogger.new(SSE::Client)) do |client|
281
- client.on_event do |event|
282
- configs = PrefabProto::Configs.decode(Base64.decode64(event.data))
283
- load_configs(configs, :sse)
284
- end
285
- end
286
- end
287
275
  end
288
276
  end
@@ -46,16 +46,22 @@ module Prefab
46
46
  pool.post do
47
47
  LOG.debug "Uploading context shapes for #{to_ship.values.size}"
48
48
 
49
- shapes = PrefabProto::ContextShapes.new(
50
- shapes: to_ship.map do |name, shape|
51
- PrefabProto::ContextShape.new(
52
- name: name,
53
- field_types: shape
54
- )
55
- end
49
+ events = PrefabProto::TelemetryEvents.new(
50
+ instance_hash: instance_hash,
51
+ events: [
52
+ PrefabProto::TelemetryEvent.new(context_shapes:
53
+ PrefabProto::ContextShapes.new(
54
+ shapes: to_ship.map do |name, shape|
55
+ PrefabProto::ContextShape.new(
56
+ name: name,
57
+ field_types: shape
58
+ )
59
+ end
60
+ ))
61
+ ]
56
62
  )
57
63
 
58
- result = post('/api/v1/context-shapes', shapes)
64
+ result = post('/api/v1/telemetry', events)
59
65
 
60
66
  LOG.debug "Uploaded #{to_ship.values.size} shapes: #{result.status}"
61
67
  end
@@ -45,11 +45,13 @@ module Prefab
45
45
  end
46
46
 
47
47
  def PROP_IS_ONE_OF(criterion, properties)
48
- matches?(criterion, value_from_properties(criterion, properties), properties)
48
+ Array(value_from_properties(criterion, properties)).any? do |prop|
49
+ matches?(criterion, prop, properties)
50
+ end
49
51
  end
50
52
 
51
53
  def PROP_IS_NOT_ONE_OF(criterion, properties)
52
- !matches?(criterion, value_from_properties(criterion, properties), properties)
54
+ !PROP_IS_ONE_OF(criterion, properties)
53
55
  end
54
56
 
55
57
  def PROP_ENDS_WITH_ONE_OF(criterion, properties)
@@ -57,7 +59,7 @@ module Prefab
57
59
  end
58
60
 
59
61
  def PROP_DOES_NOT_END_WITH_ONE_OF(criterion, properties)
60
- !prop_ends_with_one_of?(criterion, value_from_properties(criterion, properties))
62
+ !PROP_ENDS_WITH_ONE_OF(criterion, properties)
61
63
  end
62
64
 
63
65
  def HIERARCHICAL_MATCH(criterion, properties)
@@ -2,7 +2,8 @@ module Prefab
2
2
  class InternalLogger < SemanticLogger::Logger
3
3
 
4
4
  def initialize(klass)
5
- super(klass, :warn)
5
+ default_level = ENV['PREFAB_LOG_CLIENT_BOOTSTRAP_LOG_LEVEL'] ? ENV['PREFAB_LOG_CLIENT_BOOTSTRAP_LOG_LEVEL'].downcase.to_sym : :warn
6
+ super(klass, default_level)
6
7
  instances << self
7
8
  end
8
9
 
@@ -51,15 +51,16 @@ module Prefab
51
51
  aggregate[path]['logger_name'] = path
52
52
  end
53
53
 
54
- loggers = PrefabProto::Loggers.new(
55
- loggers: aggregate.values,
56
- start_at: start_at_was,
57
- end_at: Prefab::TimeHelpers.now_in_ms,
58
- instance_hash: @client.instance_hash,
59
- namespace: @client.namespace
54
+ loggers = PrefabProto::LoggersTelemetryEvent.new(loggers: aggregate.values,
55
+ start_at: start_at_was,
56
+ end_at: Prefab::TimeHelpers.now_in_ms)
57
+
58
+ events = PrefabProto::TelemetryEvents.new(
59
+ instance_hash: instance_hash,
60
+ events: [PrefabProto::TelemetryEvent.new(loggers: loggers)]
60
61
  )
61
62
 
62
- result = post('/api/v1/known-loggers', loggers)
63
+ result = post('/api/v1/telemetry', events)
63
64
 
64
65
  LOG.debug "Uploaded #{to_ship.size} paths: #{result.status}"
65
66
  end
@@ -5,7 +5,10 @@ module Prefab
5
5
  class Options
6
6
  attr_reader :api_key
7
7
  attr_reader :namespace
8
- attr_reader :prefab_api_url
8
+ attr_reader :sources
9
+ attr_reader :sse_sources
10
+ attr_reader :telemetry_destination
11
+ attr_reader :config_sources
9
12
  attr_reader :on_no_default
10
13
  attr_reader :initialization_timeout_sec
11
14
  attr_reader :on_init_failure
@@ -38,10 +41,16 @@ module Prefab
38
41
  DEFAULT_MAX_EXAMPLE_CONTEXTS = 100_000
39
42
  DEFAULT_MAX_EVAL_SUMMARIES = 100_000
40
43
 
44
+ DEFAULT_SOURCES = [
45
+ "https://belt.prefab.cloud",
46
+ "https://suspenders.prefab.cloud",
47
+ ].freeze
48
+
41
49
  private def init(
50
+ sources: nil,
42
51
  api_key: ENV['PREFAB_API_KEY'],
43
52
  namespace: '',
44
- prefab_api_url: ENV['PREFAB_API_URL'] || 'https://api.prefab.cloud',
53
+ prefab_api_url: nil,
45
54
  on_no_default: ON_NO_DEFAULT::RAISE, # options :raise, :warn_and_return_nil,
46
55
  initialization_timeout_sec: 10, # how long to wait before on_init_failure
47
56
  on_init_failure: ON_INITIALIZATION_FAILURE::RAISE,
@@ -63,7 +72,6 @@ module Prefab
63
72
  )
64
73
  @api_key = api_key
65
74
  @namespace = namespace
66
- @prefab_api_url = remove_trailing_slash(prefab_api_url)
67
75
  @on_no_default = on_no_default
68
76
  @initialization_timeout_sec = initialization_timeout_sec
69
77
  @on_init_failure = on_init_failure
@@ -88,6 +96,25 @@ module Prefab
88
96
  @collect_example_contexts = false
89
97
  @collect_max_example_contexts = 0
90
98
 
99
+ if ENV['PREFAB_API_URL_OVERRIDE'] && ENV['PREFAB_API_URL_OVERRIDE'].length > 0
100
+ sources = ENV['PREFAB_API_URL_OVERRIDE']
101
+ end
102
+
103
+ @sources = Array(sources || DEFAULT_SOURCES).map {|source| remove_trailing_slash(source) }
104
+
105
+ @sse_sources = @sources
106
+ @config_sources = @sources
107
+
108
+ @telemetry_destination = @sources.select do |source|
109
+ source.start_with?('https://') && (source.include?("belt") || source.include?("suspenders"))
110
+ end.map do |source|
111
+ source.sub(/(belt|suspenders)\./, 'telemetry.')
112
+ end[0]
113
+
114
+ if prefab_api_url
115
+ warn '[DEPRECATION] prefab_api_url is deprecated. Please provide `sources` if you need to override the default sources'
116
+ end
117
+
91
118
  case context_upload_mode
92
119
  when :none
93
120
  # do nothing
@@ -140,11 +167,6 @@ module Prefab
140
167
  @collect_max_evaluation_summaries
141
168
  end
142
169
 
143
- # https://api.prefab.cloud -> https://api-prefab-cloud.global.ssl.fastly.net
144
- def url_for_api_cdn
145
- ENV['PREFAB_CDN_URL'] || "#{@prefab_api_url.gsub(/\./, '-')}.global.ssl.fastly.net"
146
- end
147
-
148
170
  def api_key_id
149
171
  @api_key&.split("-")&.first
150
172
  end
@@ -32,6 +32,10 @@ module Prefab
32
32
  @client.post(url, data)
33
33
  end
34
34
 
35
+ def instance_hash
36
+ @client.instance_hash
37
+ end
38
+
35
39
  def start_periodic_sync(sync_interval)
36
40
  @start_at = Prefab::TimeHelpers.now_in_ms
37
41
 
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prefab
4
+ class SSEConfigClient
5
+ SSE_READ_TIMEOUT = 300
6
+ SECONDS_BETWEEN_RECONNECT = 5
7
+ AUTH_USER = 'authuser'
8
+ LOG = Prefab::InternalLogger.new(self)
9
+
10
+ def initialize(options, config_loader)
11
+ @options = options
12
+ @config_loader = config_loader
13
+ @connected = false
14
+ end
15
+
16
+ def close
17
+ @retry_thread&.kill
18
+ @client&.close
19
+ end
20
+
21
+ def start(&load_configs)
22
+ if @options.sse_sources.empty?
23
+ LOG.debug 'No SSE sources configured'
24
+ return
25
+ end
26
+
27
+ @client = connect(&load_configs)
28
+
29
+ closed_count = 0
30
+
31
+ @retry_thread = Thread.new do
32
+ loop do
33
+ sleep 1
34
+
35
+ if @client.closed?
36
+ closed_count += 1
37
+
38
+ if closed_count > SECONDS_BETWEEN_RECONNECT
39
+ closed_count = 0
40
+ @client = connect(&load_configs)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ def connect(&load_configs)
48
+ url = "#{source}/api/v1/sse/config"
49
+ LOG.debug "SSE Streaming Connect to #{url} start_at #{@config_loader.highwater_mark}"
50
+
51
+ SSE::Client.new(url,
52
+ headers: headers,
53
+ read_timeout: SSE_READ_TIMEOUT,
54
+ logger: Prefab::InternalLogger.new(SSE::Client)) do |client|
55
+ client.on_event do |event|
56
+ configs = PrefabProto::Configs.decode(Base64.decode64(event.data))
57
+ load_configs.call(configs, :sse)
58
+ end
59
+
60
+ client.on_error do |error|
61
+ LOG.error "SSE Streaming Error: #{error.inspect} for url #{url}"
62
+
63
+ if error.is_a?(HTTP::ConnectionError)
64
+ LOG.debug "Closing SSE connection for url #{url}"
65
+ client.close
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ def headers
72
+ auth = "#{AUTH_USER}:#{@options.api_key}"
73
+ auth_string = Base64.strict_encode64(auth)
74
+ return {
75
+ 'x-prefab-start-at-id' => @config_loader.highwater_mark,
76
+ 'Authorization' => "Basic #{auth_string}",
77
+ 'Accept' => 'text/event-stream',
78
+ 'X-PrefabCloud-Client-Version' => "prefab-cloud-ruby-#{Prefab::VERSION}"
79
+ }
80
+ end
81
+
82
+ def source
83
+ @source_index = @source_index.nil? ? 0 : @source_index + 1
84
+
85
+ if @source_index >= @options.sse_sources.size
86
+ @source_index = 0
87
+ end
88
+
89
+ return @options.sse_sources[@source_index]
90
+ end
91
+ end
92
+ end
@@ -46,6 +46,7 @@ require 'prefab/context'
46
46
  require 'prefab/logger_client'
47
47
  require 'active_support/deprecation'
48
48
  require 'active_support'
49
+ require 'prefab/sse_config_client'
49
50
  require 'prefab/client'
50
51
  require 'prefab/config_client_presenter'
51
52
  require 'prefab/config_client'
@@ -2,16 +2,16 @@
2
2
  # DO NOT EDIT THIS FILE DIRECTLY
3
3
  # Instead, edit Juwelier::Tasks in Rakefile, and run 'rake gemspec'
4
4
  # -*- encoding: utf-8 -*-
5
- # stub: prefab-cloud-ruby 1.7.2 ruby lib
5
+ # stub: prefab-cloud-ruby 1.8.1 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "prefab-cloud-ruby".freeze
9
- s.version = "1.7.2"
9
+ s.version = "1.8.1"
10
10
 
11
11
  s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
12
12
  s.require_paths = ["lib".freeze]
13
13
  s.authors = ["Jeff Dwyer".freeze]
14
- s.date = "2024-06-24"
14
+ s.date = "2024-09-03"
15
15
  s.description = "Feature Flags, Live Config, and Dynamic Log Levels as a service".freeze
16
16
  s.email = "jdwyer@prefab.cloud".freeze
17
17
  s.extra_rdoc_files = [
@@ -74,6 +74,7 @@ Gem::Specification.new do |s|
74
74
  "lib/prefab/prefab.rb",
75
75
  "lib/prefab/rate_limit_cache.rb",
76
76
  "lib/prefab/resolved_config_presenter.rb",
77
+ "lib/prefab/sse_config_client.rb",
77
78
  "lib/prefab/time_helpers.rb",
78
79
  "lib/prefab/weighted_value_resolver.rb",
79
80
  "lib/prefab/yaml_config_parser.rb",
@@ -113,6 +114,7 @@ Gem::Specification.new do |s|
113
114
  "test/test_options.rb",
114
115
  "test/test_prefab.rb",
115
116
  "test/test_rate_limit_cache.rb",
117
+ "test/test_sse_config_client.rb",
116
118
  "test/test_weighted_value_resolver.rb"
117
119
  ]
118
120
  s.homepage = "http://github.com/prefab-cloud/prefab-cloud-ruby".freeze
@@ -20,6 +20,10 @@ class IntegrationTest
20
20
  @test_client = capture_telemetry(base_client)
21
21
  end
22
22
 
23
+ def teardown
24
+ test_client.stop
25
+ end
26
+
23
27
  def test_type
24
28
  if @data
25
29
  :telemetry
@@ -53,7 +57,11 @@ class IntegrationTest
53
57
  def parse_client_overrides(overrides)
54
58
  Hash[
55
59
  (overrides || {}).map do |(k, v)|
56
- [k.to_sym, v]
60
+ if k.to_s == "prefab_api_url"
61
+ [:sources, [v]]
62
+ else
63
+ [k.to_sym, v]
64
+ end
57
65
  end
58
66
  ]
59
67
  end
@@ -127,17 +135,22 @@ class IntegrationTest
127
135
  prefab_envs: ['unit_tests'],
128
136
  prefab_datasources: Prefab::Options::DATASOURCES::ALL,
129
137
  api_key: ENV['PREFAB_INTEGRATION_TEST_API_KEY'],
130
- prefab_api_url: 'https://api.staging-prefab.cloud',
138
+ sources: [
139
+ 'https://belt.staging-prefab.cloud',
140
+ 'https://suspenders.staging-prefab.cloud',
141
+ ],
131
142
  global_context: @global_context || {},
132
143
  }.merge(@client_overrides))
133
144
  end
134
145
 
135
146
  def capture_telemetry(client)
147
+ super_method = client.method(:post)
148
+
136
149
  client.define_singleton_method(:post) do |url, data|
137
150
  client.instance_variable_set(:@last_data_sent, data)
138
151
  client.instance_variable_set(:@last_post_endpoint, url)
139
152
 
140
- result = super(url, data)
153
+ result = super_method.call(url, data)
141
154
 
142
155
  client.instance_variable_set(:@last_post_result, result)
143
156
 
@@ -2,16 +2,12 @@
2
2
 
3
3
  module IntegrationTestHelpers
4
4
  SUBMODULE_PATH = 'test/prefab-cloud-integration-test-data'
5
- RAISE_IF_NO_TESTS_FOUND = ENV['PREFAB_INTEGRATION_TEST_RAISE'] == 'true'
6
5
 
7
6
  def self.find_integration_tests
8
7
  files = find_test_files
9
8
 
10
9
  if files.none?
11
- message = "No integration tests found"
12
- raise message if RAISE_IF_NO_TESTS_FOUND
13
-
14
- puts message
10
+ raise "No integration tests found"
15
11
  end
16
12
 
17
13
  files
@@ -44,7 +40,7 @@ module IntegrationTestHelpers
44
40
  end
45
41
  end
46
42
 
47
- [aggregator, ->(data) { data.loggers }, expected_loggers.values]
43
+ [aggregator, ->(data) { data.events[0].loggers.loggers }, expected_loggers.values]
48
44
  when "context_shape"
49
45
  aggregator = it.test_client.context_shape_aggregator
50
46
 
@@ -59,7 +55,7 @@ module IntegrationTestHelpers
59
55
  )
60
56
  end
61
57
 
62
- [aggregator, ->(data) { data.shapes }, expected]
58
+ [aggregator, ->(data) { data.events[0].context_shapes.shapes }, expected]
63
59
  when "evaluation_summary"
64
60
  aggregator = it.test_client.evaluation_summary_aggregator
65
61
 
@@ -47,11 +47,14 @@ module CommonHelpers
47
47
  ENV[key] = old_value
48
48
  end
49
49
 
50
+ EFFECTIVELY_NEVER = 99_999 # we sync manually
51
+
50
52
  DEFAULT_NEW_CLIENT_OPTIONS = {
51
53
  prefab_config_override_dir: 'none',
52
54
  prefab_config_classpath_dir: 'test',
53
55
  prefab_envs: ['unit_tests'],
54
- prefab_datasources: Prefab::Options::DATASOURCES::LOCAL_ONLY
56
+ prefab_datasources: Prefab::Options::DATASOURCES::LOCAL_ONLY,
57
+ collect_sync_interval: EFFECTIVELY_NEVER,
55
58
  }.freeze
56
59
 
57
60
  def new_client(overrides = {})
@@ -98,30 +98,36 @@ class TestContextShapeAggregator < Minitest::Test
98
98
 
99
99
  assert_equal [
100
100
  [
101
- '/api/v1/context-shapes',
102
- PrefabProto::ContextShapes.new(shapes: [
103
- PrefabProto::ContextShape.new(
104
- name: 'user', field_types: {
105
- 'age' => 4, 'dob' => 2, 'email' => 2, 'name' => 2
106
- }
107
- ),
108
- PrefabProto::ContextShape.new(
109
- name: 'subscription', field_types: {
110
- 'plan' => 2, 'free' => 5, 'trial' => 5
111
- }
112
- ),
113
- PrefabProto::ContextShape.new(
114
- name: 'device', field_types: {
115
- 'version' => 1, 'os' => 2, 'name' => 2
116
- }
117
- )
118
- ])
101
+ '/api/v1/telemetry',
102
+ PrefabProto::TelemetryEvents.new(
103
+ instance_hash: client.instance_hash,
104
+ events: [
105
+ PrefabProto::TelemetryEvent.new(context_shapes:
106
+
107
+ PrefabProto::ContextShapes.new(shapes: [
108
+ PrefabProto::ContextShape.new(
109
+ name: 'user', field_types: {
110
+ 'age' => 4, 'dob' => 2, 'email' => 2, 'name' => 2
111
+ }
112
+ ),
113
+ PrefabProto::ContextShape.new(
114
+ name: 'subscription', field_types: {
115
+ 'plan' => 2, 'free' => 5, 'trial' => 5
116
+ }
117
+ ),
118
+ PrefabProto::ContextShape.new(
119
+ name: 'device', field_types: {
120
+ 'version' => 1, 'os' => 2, 'name' => 2
121
+ }
122
+ )
123
+ ]))
124
+ ]
125
+ )
119
126
  ]
120
127
  ], requests
121
128
 
122
-
123
129
  assert_logged [
124
- "No success loading checkpoints",
130
+ 'No success loading checkpoints',
125
131
  "Couldn't Initialize In 0. Key some.key. Returning what we have"
126
132
  ]
127
133
  end
@@ -4,8 +4,6 @@ require 'test_helper'
4
4
  require 'timecop'
5
5
 
6
6
  class TestExampleContextsAggregator < Minitest::Test
7
- EFFECTIVELY_NEVER = 99_999 # we sync manually
8
-
9
7
  def test_record
10
8
  aggregator = Prefab::ExampleContextsAggregator.new(client: MockBaseClient.new, max_contexts: 2,
11
9
  sync_interval: EFFECTIVELY_NEVER)
@@ -8,10 +8,19 @@ require 'yaml'
8
8
  class TestIntegration < Minitest::Test
9
9
  IntegrationTestHelpers.find_integration_tests.map do |test_file|
10
10
  tests = YAML.load(File.read(test_file))['tests']
11
+ test_names = []
11
12
 
12
13
  tests.each do |test|
13
14
  test['cases'].each do |test_case|
14
- define_method(:"test_#{test['name']}_#{test_case['name']}") do
15
+ new_name = "test_#{test['name']}_#{test_case['name']}"
16
+
17
+ if test_names.include?(new_name)
18
+ raise "Duplicate test name: #{new_name}"
19
+ end
20
+
21
+ test_names << new_name
22
+
23
+ define_method(:"#{new_name}") do
15
24
  it = IntegrationTest.new(test_case)
16
25
 
17
26
  IntegrationTestHelpers.with_block_context_maybe(it.block_context) do
@@ -38,7 +47,7 @@ class TestIntegration < Minitest::Test
38
47
 
39
48
  wait_for -> { it.last_post_result&.status == 200 }
40
49
 
41
- assert it.endpoint == it.last_post_endpoint
50
+ assert_equal "/api/v1/telemetry", it.last_post_endpoint
42
51
 
43
52
  actual = get_actual_data[it.last_data_sent]
44
53
 
@@ -56,6 +65,8 @@ class TestIntegration < Minitest::Test
56
65
  "Prefab::ConfigClient -- Couldn't Initialize In 0.01. Key any-key. Returning what we have"
57
66
  ]
58
67
  end
68
+ ensure
69
+ it.teardown
59
70
  end
60
71
  end
61
72
  end
@@ -32,15 +32,27 @@ class TestLogPathAggregator < Minitest::Test
32
32
  client.log_path_aggregator.send(:sync)
33
33
  end
34
34
 
35
- assert_equal '/api/v1/known-loggers', requests[0][0]
36
- sent_logger = requests[0][1]
37
- assert_equal 'this.is.a.namespace', sent_logger.namespace
38
- assert_equal Prefab::TimeHelpers.now_in_ms, sent_logger.start_at
39
- assert_equal Prefab::TimeHelpers.now_in_ms, sent_logger.end_at
40
- assert_equal client.instance_hash, sent_logger.instance_hash
41
- assert_includes sent_logger.loggers,
42
- PrefabProto::Logger.new(logger_name: 'test.test_log_path_aggregator.test_sync', infos: 2,
43
- errors: 3)
35
+ assert_equal [
36
+ [
37
+ '/api/v1/telemetry',
38
+ PrefabProto::TelemetryEvents.new(
39
+ instance_hash: client.instance_hash,
40
+ events: [
41
+ PrefabProto::TelemetryEvent.new(loggers:
42
+
43
+ PrefabProto::LoggersTelemetryEvent.new(loggers: [
44
+ PrefabProto::Logger.new(
45
+ logger_name: 'test.test_log_path_aggregator.test_sync',
46
+ infos: 2,
47
+ errors: 3
48
+ )
49
+ ],
50
+ start_at: Prefab::TimeHelpers.now_in_ms,
51
+ end_at: Prefab::TimeHelpers.now_in_ms,))
52
+ ]
53
+ )
54
+ ]
55
+ ], requests
44
56
  end
45
57
  end
46
58
 
data/test/test_options.rb CHANGED
@@ -5,18 +5,31 @@ require 'test_helper'
5
5
  class TestOptions < Minitest::Test
6
6
  API_KEY = 'abcdefg'
7
7
 
8
- def test_prefab_api_url
9
- assert_equal 'https://api.prefab.cloud', Prefab::Options.new.prefab_api_url
8
+ def test_api_override_env_var
9
+ assert_equal Prefab::Options::DEFAULT_SOURCES, Prefab::Options.new.sources
10
10
 
11
- with_env 'PREFAB_API_URL', 'https://api.prefab.cloud' do
12
- assert_equal 'https://api.prefab.cloud', Prefab::Options.new.prefab_api_url
11
+ # blank doesn't take effect
12
+ with_env('PREFAB_API_URL_OVERRIDE', '') do
13
+ assert_equal Prefab::Options::DEFAULT_SOURCES, Prefab::Options.new.sources
13
14
  end
14
15
 
15
- with_env 'PREFAB_API_URL', 'https://api.prefab.cloud/' do
16
- assert_equal 'https://api.prefab.cloud', Prefab::Options.new.prefab_api_url
16
+ # non-blank does take effect
17
+ with_env('PREFAB_API_URL_OVERRIDE', 'https://override.example.com') do
18
+ assert_equal ["https://override.example.com"], Prefab::Options.new.sources
17
19
  end
18
20
  end
19
21
 
22
+ def test_overriding_sources
23
+ assert_equal Prefab::Options::DEFAULT_SOURCES, Prefab::Options.new.sources
24
+
25
+ # a plain string ends up wrapped in an array
26
+ source = 'https://example.com'
27
+ assert_equal [source], Prefab::Options.new(sources: source).sources
28
+
29
+ sources = ['https://example.com', 'https://example2.com']
30
+ assert_equal sources, Prefab::Options.new(sources: sources).sources
31
+ end
32
+
20
33
  def test_works_with_named_arguments
21
34
  assert_equal API_KEY, Prefab::Options.new(api_key: API_KEY).api_key
22
35
  end
@@ -0,0 +1,65 @@
1
+ require 'test_helper'
2
+
3
+ class TestSSEConfigClient < Minitest::Test
4
+ def test_client
5
+ sources = [
6
+ "https://api.staging-prefab.cloud/",
7
+ ]
8
+
9
+ options = Prefab::Options.new(sources: sources, api_key: ENV['PREFAB_INTEGRATION_TEST_API_KEY'])
10
+
11
+ config_loader = OpenStruct.new(highwater_mark: 4)
12
+
13
+ client = Prefab::SSEConfigClient.new(options, config_loader)
14
+
15
+ assert_equal 4, client.headers['x-prefab-start-at-id']
16
+
17
+ result = nil
18
+
19
+ # fake our load_configs block
20
+ client.start do |c, source|
21
+ result = c
22
+ assert_equal :sse, source
23
+ end
24
+
25
+ wait_for -> { !result.nil? }
26
+
27
+ assert result.configs.size > 30
28
+ ensure
29
+ client.close
30
+ end
31
+
32
+ def test_failing_over
33
+ sources = [
34
+ "https://does.not.exist.staging-prefab.cloud/",
35
+ "https://api.staging-prefab.cloud/",
36
+ ]
37
+
38
+ options = Prefab::Options.new(sources: sources, api_key: ENV['PREFAB_INTEGRATION_TEST_API_KEY'])
39
+
40
+ config_loader = OpenStruct.new(highwater_mark: 4)
41
+
42
+ client = Prefab::SSEConfigClient.new(options, config_loader)
43
+
44
+ assert_equal 4, client.headers['x-prefab-start-at-id']
45
+
46
+ result = nil
47
+
48
+ # fake our load_configs block
49
+ client.start do |c, source|
50
+ result = c
51
+ assert_equal :sse, source
52
+ end
53
+
54
+ wait_for -> { !result.nil? }, max_wait: 10
55
+
56
+ assert result.configs.size > 30
57
+ ensure
58
+ client.close
59
+
60
+ assert_logged [
61
+ /failed to connect: .*https:\/\/does.not.exist/,
62
+ /HTTP::ConnectionError/,
63
+ ]
64
+ end
65
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: prefab-cloud-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.7.2
4
+ version: 1.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeff Dwyer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-06-24 00:00:00.000000000 Z
11
+ date: 2024-09-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -261,6 +261,7 @@ files:
261
261
  - lib/prefab/prefab.rb
262
262
  - lib/prefab/rate_limit_cache.rb
263
263
  - lib/prefab/resolved_config_presenter.rb
264
+ - lib/prefab/sse_config_client.rb
264
265
  - lib/prefab/time_helpers.rb
265
266
  - lib/prefab/weighted_value_resolver.rb
266
267
  - lib/prefab/yaml_config_parser.rb
@@ -300,6 +301,7 @@ files:
300
301
  - test/test_options.rb
301
302
  - test/test_prefab.rb
302
303
  - test/test_rate_limit_cache.rb
304
+ - test/test_sse_config_client.rb
303
305
  - test/test_weighted_value_resolver.rb
304
306
  homepage: http://github.com/prefab-cloud/prefab-cloud-ruby
305
307
  licenses: