prefab-cloud-ruby 1.8.2 → 1.8.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bc28dd551eb4ba62f97902ae49cfa8f474c7a2883a8dce5ea43605d661420407
4
- data.tar.gz: f2bba00e271a6947d0394453f21964d4d849f22004747a434efa58f3a97d6f09
3
+ metadata.gz: ec8c47fb2fe1945e2d189198fbb2ecb8b2eee9d77537ac6569196659931badf4
4
+ data.tar.gz: 58afaf18e4541429db3e4204f7bc2c19340028d894b84a75d7195aac39fd6b37
5
5
  SHA512:
6
- metadata.gz: d9b2e0a3ce0aaa1812677c06744272f2ac52a92687420c10942f271261be1b9c7a4de64efc8e98ed2537df1597c00a2a1f359408c19114d64b3ba5838aeab917
7
- data.tar.gz: 22892c02a6570108400edc76af207e233125e4ec85df3d895db2be90c86e9dba7840a9ea4f8e46c2868e9ba6b1c642115677f47cb90db928be40e354a74d1e13
6
+ metadata.gz: 7ed0b65787d13a25a301a75481cff40db9b7dc1b036aea75c5e241b0bdddfd26577317b013830c7533ad0da51b7d087c3e59df5d8c38c07382509cfbc8d9eeb1
7
+ data.tar.gz: c57bb8881f6a61b252013c118240bc64b8ffaa248caafd234747f1897dd3d67aab6b02b72a3c1e8b164feb689590587475fe5d605f13c1ac4b5c0b426e45f137
@@ -40,7 +40,7 @@ jobs:
40
40
  - name: Install dependencies
41
41
  run: bundle install --without development --jobs 4 --retry 3
42
42
  - name: Run tests
43
- run: bundle exec rake
43
+ run: bundle exec rake --trace
44
44
  env:
45
45
  PREFAB_INTEGRATION_TEST_API_KEY: ${{ secrets.PREFAB_INTEGRATION_TEST_API_KEY }}
46
46
  PREFAB_INTEGRATION_TEST_ENCRYPTION_KEY: ${{ secrets.PREFAB_INTEGRATION_TEST_ENCRYPTION_KEY }}
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.8.4 - 2024-09-19
4
+
5
+ - Use `stream` subdomain for SSE (#203)
6
+
7
+ ## 1.8.3 - 2024-09-16
8
+
9
+ - Add JavaScript stub & bootstrapping (#200)
10
+
3
11
  ## 1.8.2 - 2024-09-03
4
12
 
5
13
  - Forbid bad semantic_logger version (#198)
data/Gemfile CHANGED
@@ -12,6 +12,7 @@ gem 'activesupport', '>= 4'
12
12
  gem 'semantic_logger', '!= 4.16.0', require: "semantic_logger/sync"
13
13
 
14
14
  group :development do
15
+ gem 'allocation_stats'
15
16
  gem 'benchmark-ips'
16
17
  gem 'bundler'
17
18
  gem 'juwelier', '~> 2.4.9'
@@ -24,4 +25,5 @@ group :test do
24
25
  gem 'minitest-focus'
25
26
  gem 'minitest-reporters'
26
27
  gem 'timecop'
28
+ gem 'webrick'
27
29
  end
data/Gemfile.lock CHANGED
@@ -13,6 +13,7 @@ GEM
13
13
  tzinfo (~> 2.0)
14
14
  addressable (2.8.6)
15
15
  public_suffix (>= 2.0.2, < 6.0)
16
+ allocation_stats (0.1.5)
16
17
  ansi (1.5.0)
17
18
  base64 (0.2.0)
18
19
  benchmark-ips (2.13.0)
@@ -151,12 +152,14 @@ GEM
151
152
  concurrent-ruby (~> 1.0)
152
153
  uuid (2.3.9)
153
154
  macaddr (~> 1.0)
155
+ webrick (1.8.1)
154
156
 
155
157
  PLATFORMS
156
158
  ruby
157
159
 
158
160
  DEPENDENCIES
159
161
  activesupport (>= 4)
162
+ allocation_stats
160
163
  benchmark-ips
161
164
  bundler
162
165
  concurrent-ruby (~> 1.0, >= 1.0.5)
@@ -173,6 +176,7 @@ DEPENDENCIES
173
176
  simplecov
174
177
  timecop
175
178
  uuid
179
+ webrick
176
180
 
177
181
  BUNDLED WITH
178
182
  2.3.5
data/Rakefile CHANGED
@@ -11,6 +11,14 @@ rescue Bundler::BundlerError => e
11
11
  end
12
12
 
13
13
  require 'rake'
14
+
15
+ require 'rake/testtask'
16
+ Rake::TestTask.new(:test) do |test|
17
+ test.libs << 'lib' << 'test'
18
+ test.pattern = 'test/**/test_*.rb'
19
+ test.verbose = true
20
+ end
21
+
14
22
  task default: :test
15
23
 
16
24
  unless ENV['CI']
@@ -28,12 +36,6 @@ unless ENV['CI']
28
36
  # dependencies defined in Gemfile
29
37
  end
30
38
  Juwelier::RubygemsDotOrgTasks.new
31
- require 'rake/testtask'
32
- Rake::TestTask.new(:test) do |test|
33
- test.libs << 'lib' << 'test'
34
- test.pattern = 'test/**/test_*.rb'
35
- test.verbose = true
36
- end
37
39
 
38
40
  desc 'Code coverage detail'
39
41
  task :simplecov do
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.8.2
1
+ 1.8.4
data/dev/allocation_stats CHANGED
@@ -20,7 +20,7 @@ require 'prefab-cloud-ruby'
20
20
 
21
21
  $prefab = Prefab::Client.new(collect_logger_counts: false, collect_evaluation_summaries: false,
22
22
  context_upload_mode: :none)
23
- $prefab.get('prefab.auth.allowed_origins')
23
+ $prefab.get('a.live.integer')
24
24
 
25
25
  puts '-' * 80
26
26
 
@@ -50,11 +50,11 @@ def measure(description)
50
50
  end
51
51
 
52
52
  measure "no-JIT context (#{$runs} runs)" do
53
- $prefab.get('prefab.auth.allowed_origins')
53
+ $prefab.get('a.live.integer')
54
54
  end
55
55
 
56
56
  puts "\n\n"
57
57
 
58
58
  measure "with JIT context (#{$runs} runs)" do
59
- $prefab.get('prefab.auth.allowed_origins', { a: { b: "c" } })
59
+ $prefab.get('a.live.integer', { a: { b: "c" } })
60
60
  end
data/dev/console CHANGED
@@ -1,23 +1,8 @@
1
- #!/usr/bin/env ruby
1
+ #!/usr/bin/env bundle exec ruby
2
2
  # frozen_string_literal: true
3
3
 
4
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
- SemanticLogger.add_appender(io: $stdout)
5
+ require_relative "./script_setup"
21
6
 
22
7
  if !ENV['PREFAB_LOG_CLIENT_BOOTSTRAP_LOG_LEVEL']
23
8
  puts "run with PREFAB_LOG_CLIENT_BOOTSTRAP_LOG_LEVEL=debug (or trace) for more output"
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+
5
+ gemspec = Dir.glob(File.expand_path("../../*.gemspec", __FILE__)).first
6
+ spec = Gem::Specification.load(gemspec)
7
+
8
+ # Add the require paths to the $LOAD_PATH
9
+ spec.require_paths.each do |path|
10
+ full_path = File.expand_path("../" + path, __dir__)
11
+ $LOAD_PATH.unshift(full_path) unless $LOAD_PATH.include?(full_path)
12
+ end
13
+
14
+ spec.require_paths.each do |path|
15
+ require "./lib/prefab-cloud-ruby"
16
+ end
17
+
18
+ SemanticLogger.add_appender(io: $stdout)
@@ -46,9 +46,9 @@ module Prefab
46
46
  stream_lock = Concurrent::ReadWriteLock.new
47
47
  @sse_config_client = Prefab::SSEConfigClient.new(@options, @config_loader)
48
48
 
49
- @sse_config_client.start do |configs|
49
+ @sse_config_client.start do |configs, _event, source|
50
50
  stream_lock.with_write_lock do
51
- load_configs(configs, :sse)
51
+ load_configs(configs, source)
52
52
  end
53
53
  end
54
54
  end
@@ -23,6 +23,10 @@ module Prefab
23
23
  Prefab::ResolvedConfigPresenter.new(self, @lock, @local_store)
24
24
  end
25
25
 
26
+ def keys
27
+ @local_store.keys
28
+ end
29
+
26
30
  def raw(key)
27
31
  @local_store.dig(key, :config)
28
32
  end
@@ -21,8 +21,10 @@ module Prefab
21
21
 
22
22
  def evaluate(properties)
23
23
  rtn = evaluate_for_env(@project_env_id, properties) ||
24
- evaluate_for_env(0, properties)
25
- LOG.trace "Eval Key #{@config.key} Result #{rtn&.reportable_value} with #{properties.to_h}" unless @config.config_type == :LOG_LEVEL
24
+ evaluate_for_env(0, properties)
25
+ LOG.trace {
26
+ "Eval Key #{@config.key} Result #{rtn&.reportable_value} with #{properties.to_h}"
27
+ } unless @config.config_type == :LOG_LEVEL
26
28
  rtn
27
29
  end
28
30
 
@@ -50,5 +50,9 @@ module Prefab
50
50
  def to_f
51
51
  in_seconds.to_f
52
52
  end
53
+
54
+ def as_json
55
+ { ms: in_seconds * 1000, seconds: in_seconds }
56
+ end
53
57
  end
54
58
  end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prefab
4
+ class JavaScriptStub
5
+ LOG = Prefab::InternalLogger.new(self)
6
+
7
+ def initialize(client = nil)
8
+ @client = client || Prefab.instance
9
+ end
10
+
11
+ def bootstrap(context)
12
+ configs, warnings = data(context)
13
+ <<~JS
14
+ window._prefabBootstrap = {
15
+ configs: #{JSON.dump(configs)},
16
+ context: #{JSON.dump(context)}
17
+ }
18
+ #{log_warnings(warnings)}
19
+ JS
20
+ end
21
+
22
+ def generate_stub(context, callback = nil)
23
+ configs, warnings = data(context)
24
+ <<~JS
25
+ window.prefab = window.prefab || {};
26
+ window.prefab.config = #{JSON.dump(configs)};
27
+ window.prefab.get = function(key) {
28
+ var value = window.prefab.config[key];
29
+ #{callback && " #{callback}(key, value);"}
30
+ return value;
31
+ };
32
+ window.prefab.isEnabled = function(key) {
33
+ var value = window.prefab.config[key] === true;
34
+ #{callback && " #{callback}(key, value);"}
35
+ return value;
36
+ };
37
+ #{log_warnings(warnings)}
38
+ JS
39
+ end
40
+
41
+ private
42
+
43
+ def underlying_value(value)
44
+ v = Prefab::ConfigValueUnwrapper.new(value, @client.resolver).unwrap
45
+ case v
46
+ when Google::Protobuf::RepeatedField
47
+ v.to_a
48
+ when Prefab::Duration
49
+ v.as_json
50
+ else
51
+ v
52
+ end
53
+ end
54
+
55
+ def log_warnings(warnings)
56
+ return '' if warnings.empty?
57
+
58
+ <<~JS
59
+ console.warn('The following keys could not be resolved:', #{JSON.dump(warnings)});
60
+ JS
61
+ end
62
+
63
+ def data(context)
64
+ permitted = {}
65
+ warnings = []
66
+ resolver_keys = @client.resolver.keys
67
+
68
+ resolver_keys.each do |key|
69
+ begin
70
+ config = @client.resolver.raw(key)
71
+
72
+ if config.config_type == :FEATURE_FLAG || config.send_to_client_sdk || config.config_type == :LOG_LEVEL
73
+ permitted[key] = underlying_value(@client.resolver.get(key, context).value)
74
+ end
75
+ rescue StandardError => e
76
+ LOG.warn("Could not resolve key #{key}: #{e}")
77
+
78
+ warnings << key
79
+ end
80
+ end
81
+
82
+ [permitted, warnings]
83
+ end
84
+ end
85
+ end
data/lib/prefab/prefab.rb CHANGED
@@ -78,6 +78,42 @@ module Prefab
78
78
  @singleton.is_ff?(key)
79
79
  end
80
80
 
81
+ # Generate the JavaScript snippet to bootstrap the client SDK. This will
82
+ # include the configuration values that are permitted to be sent to the
83
+ # client SDK.
84
+ #
85
+ # If the context provided to the client SDK is not the same as the context
86
+ # used to generate the configuration values, the client SDK will still
87
+ # generate a fetch to get the correct values for the context.
88
+ #
89
+ # Any keys that could not be resolved will be logged as a warning to the
90
+ # console.
91
+ def self.bootstrap_javascript(context)
92
+ ensure_initialized
93
+ Prefab::JavaScriptStub.new(@singleton).bootstrap(context)
94
+ end
95
+
96
+ # Generate the JavaScript snippet to *replace* the client SDK. Use this to
97
+ # get `prefab.get` and `prefab.isEnabled` functions on the window object.
98
+ #
99
+ # Only use this if you are not using the client SDK and do not need
100
+ # client-side context.
101
+ #
102
+ # Any keys that could not be resolved will be logged as a warning to the
103
+ # console.
104
+ #
105
+ # You can pass an optional callback function to be called with the key and
106
+ # value of each configuration value. This can be useful for logging,
107
+ # tracking experiment exposure, etc.
108
+ #
109
+ # e.g.
110
+ # - `Prefab.generate_javascript_stub(context, "reportExperimentExposure")`
111
+ # - `Prefab.generate_javascript_stub(context, "(key,value)=>{console.log({eval: 'eval', key,value})}")`
112
+ def self.generate_javascript_stub(context, callback = nil)
113
+ ensure_initialized
114
+ Prefab::JavaScriptStub.new(@singleton).generate_stub(context, callback)
115
+ end
116
+
81
117
  private
82
118
 
83
119
  def self.ensure_initialized(key = nil)
@@ -2,15 +2,33 @@
2
2
 
3
3
  module Prefab
4
4
  class SSEConfigClient
5
- SSE_READ_TIMEOUT = 300
6
- SECONDS_BETWEEN_RECONNECT = 5
5
+ class Options
6
+ attr_reader :sse_read_timeout, :seconds_between_new_connection,
7
+ :sse_default_reconnect_time, :sleep_delay_for_new_connection_check,
8
+ :errors_to_close_connection
9
+
10
+ def initialize(sse_read_timeout: 300,
11
+ seconds_between_new_connection: 5,
12
+ sleep_delay_for_new_connection_check: 1,
13
+ sse_default_reconnect_time: SSE::Client::DEFAULT_RECONNECT_TIME,
14
+ errors_to_close_connection: [HTTP::ConnectionError])
15
+ @sse_read_timeout = sse_read_timeout
16
+ @seconds_between_new_connection = seconds_between_new_connection
17
+ @sse_default_reconnect_time = sse_default_reconnect_time
18
+ @sleep_delay_for_new_connection_check = sleep_delay_for_new_connection_check
19
+ @errors_to_close_connection = errors_to_close_connection
20
+ end
21
+ end
22
+
7
23
  AUTH_USER = 'authuser'
8
24
  LOG = Prefab::InternalLogger.new(self)
9
25
 
10
- def initialize(options, config_loader)
11
- @options = options
26
+ def initialize(prefab_options, config_loader, options = nil, logger = nil)
27
+ @prefab_options = prefab_options
28
+ @options = options || Options.new
12
29
  @config_loader = config_loader
13
30
  @connected = false
31
+ @logger = logger || LOG
14
32
  end
15
33
 
16
34
  def close
@@ -19,8 +37,8 @@ module Prefab
19
37
  end
20
38
 
21
39
  def start(&load_configs)
22
- if @options.sse_sources.empty?
23
- LOG.debug 'No SSE sources configured'
40
+ if @prefab_options.sse_sources.empty?
41
+ @logger.debug 'No SSE sources configured'
24
42
  return
25
43
  end
26
44
 
@@ -30,13 +48,14 @@ module Prefab
30
48
 
31
49
  @retry_thread = Thread.new do
32
50
  loop do
33
- sleep 1
51
+ sleep @options.sleep_delay_for_new_connection_check
34
52
 
35
53
  if @client.closed?
36
- closed_count += 1
54
+ closed_count += @options.sleep_delay_for_new_connection_check
37
55
 
38
- if closed_count > SECONDS_BETWEEN_RECONNECT
56
+ if closed_count > @options.seconds_between_new_connection
39
57
  closed_count = 0
58
+ @logger.debug 'Reconnecting SSE client'
40
59
  @client = connect(&load_configs)
41
60
  end
42
61
  end
@@ -46,22 +65,23 @@ module Prefab
46
65
 
47
66
  def connect(&load_configs)
48
67
  url = "#{source}/api/v1/sse/config"
49
- LOG.debug "SSE Streaming Connect to #{url} start_at #{@config_loader.highwater_mark}"
68
+ @logger.debug "SSE Streaming Connect to #{url} start_at #{@config_loader.highwater_mark}"
50
69
 
51
70
  SSE::Client.new(url,
52
71
  headers: headers,
53
- read_timeout: SSE_READ_TIMEOUT,
72
+ read_timeout: @options.sse_read_timeout,
73
+ reconnect_time: @options.sse_default_reconnect_time,
54
74
  logger: Prefab::InternalLogger.new(SSE::Client)) do |client|
55
75
  client.on_event do |event|
56
76
  configs = PrefabProto::Configs.decode(Base64.decode64(event.data))
57
- load_configs.call(configs, :sse)
77
+ load_configs.call(configs, event, :sse)
58
78
  end
59
79
 
60
80
  client.on_error do |error|
61
- LOG.error "SSE Streaming Error: #{error.inspect} for url #{url}"
81
+ @logger.error "SSE Streaming Error: #{error.inspect} for url #{url}"
62
82
 
63
- if error.is_a?(HTTP::ConnectionError)
64
- LOG.debug "Closing SSE connection for url #{url}"
83
+ if @options.errors_to_close_connection.any? { |klass| error.is_a?(klass) }
84
+ @logger.debug "Closing SSE connection for url #{url}"
65
85
  client.close
66
86
  end
67
87
  end
@@ -69,7 +89,7 @@ module Prefab
69
89
  end
70
90
 
71
91
  def headers
72
- auth = "#{AUTH_USER}:#{@options.api_key}"
92
+ auth = "#{AUTH_USER}:#{@prefab_options.api_key}"
73
93
  auth_string = Base64.strict_encode64(auth)
74
94
  return {
75
95
  'x-prefab-start-at-id' => @config_loader.highwater_mark,
@@ -82,11 +102,11 @@ module Prefab
82
102
  def source
83
103
  @source_index = @source_index.nil? ? 0 : @source_index + 1
84
104
 
85
- if @source_index >= @options.sse_sources.size
105
+ if @source_index >= @prefab_options.sse_sources.size
86
106
  @source_index = 0
87
107
  end
88
108
 
89
- return @options.sse_sources[@source_index]
109
+ return @prefab_options.sse_sources[@source_index].sub(/(belt|suspenders)\./, 'stream.')
90
110
  end
91
111
  end
92
112
  end
@@ -53,3 +53,4 @@ require 'prefab/config_client'
53
53
  require 'prefab/feature_flag_client'
54
54
  require 'prefab/prefab'
55
55
  require 'prefab/murmer3'
56
+ require 'prefab/javascript_stub'
@@ -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.8.2 ruby lib
5
+ # stub: prefab-cloud-ruby 1.8.4 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "prefab-cloud-ruby".freeze
9
- s.version = "1.8.2"
9
+ s.version = "1.8.4"
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-09-03"
14
+ s.date = "2024-09-19"
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 = [
@@ -37,6 +37,7 @@ Gem::Specification.new do |s|
37
37
  "dev/allocation_stats",
38
38
  "dev/benchmark",
39
39
  "dev/console",
40
+ "dev/script_setup.rb",
40
41
  "lib/prefab-cloud-ruby.rb",
41
42
  "lib/prefab/client.rb",
42
43
  "lib/prefab/config_client.rb",
@@ -65,6 +66,7 @@ Gem::Specification.new do |s|
65
66
  "lib/prefab/feature_flag_client.rb",
66
67
  "lib/prefab/http_connection.rb",
67
68
  "lib/prefab/internal_logger.rb",
69
+ "lib/prefab/javascript_stub.rb",
68
70
  "lib/prefab/local_config_parser.rb",
69
71
  "lib/prefab/log_path_aggregator.rb",
70
72
  "lib/prefab/logger_client.rb",
@@ -107,6 +109,7 @@ Gem::Specification.new do |s|
107
109
  "test/test_helper.rb",
108
110
  "test/test_integration.rb",
109
111
  "test/test_internal_logger.rb",
112
+ "test/test_javascript_stub.rb",
110
113
  "test/test_local_config_parser.rb",
111
114
  "test/test_log_path_aggregator.rb",
112
115
  "test/test_logger.rb",
@@ -135,6 +138,7 @@ Gem::Specification.new do |s|
135
138
  s.add_runtime_dependency(%q<uuid>.freeze, [">= 0"])
136
139
  s.add_runtime_dependency(%q<activesupport>.freeze, [">= 4"])
137
140
  s.add_runtime_dependency(%q<semantic_logger>.freeze, ["!= 4.16.0"])
141
+ s.add_development_dependency(%q<allocation_stats>.freeze, [">= 0"])
138
142
  s.add_development_dependency(%q<benchmark-ips>.freeze, [">= 0"])
139
143
  s.add_development_dependency(%q<bundler>.freeze, [">= 0"])
140
144
  s.add_development_dependency(%q<juwelier>.freeze, ["~> 2.4.9"])
@@ -149,6 +153,7 @@ Gem::Specification.new do |s|
149
153
  s.add_dependency(%q<uuid>.freeze, [">= 0"])
150
154
  s.add_dependency(%q<activesupport>.freeze, [">= 4"])
151
155
  s.add_dependency(%q<semantic_logger>.freeze, ["!= 4.16.0"])
156
+ s.add_dependency(%q<allocation_stats>.freeze, [">= 0"])
152
157
  s.add_dependency(%q<benchmark-ips>.freeze, [">= 0"])
153
158
  s.add_dependency(%q<bundler>.freeze, [">= 0"])
154
159
  s.add_dependency(%q<juwelier>.freeze, ["~> 2.4.9"])
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ class JavascriptStubTest < Minitest::Test
6
+ PROJECT_ENV_ID = 1
7
+ DEFAULT_VALUE = 'default_value'
8
+ DEFAULT_VALUE_CONFIG = PrefabProto::ConfigValue.new(string: DEFAULT_VALUE)
9
+ TRUE_CONFIG = PrefabProto::ConfigValue.new(bool: true)
10
+ FALSE_CONFIG = PrefabProto::ConfigValue.new(bool: false)
11
+ DEFAULT_ROW = PrefabProto::ConfigRow.new(
12
+ values: [
13
+ PrefabProto::ConditionalValue.new(value: DEFAULT_VALUE_CONFIG)
14
+ ]
15
+ )
16
+
17
+ def setup
18
+ super
19
+
20
+ log_level = PrefabProto::Config.new(
21
+ id: 999,
22
+ key: 'log-level',
23
+ config_type: PrefabProto::ConfigType::LOG_LEVEL,
24
+ rows: [
25
+ PrefabProto::ConfigRow.new(
26
+ values: [
27
+ PrefabProto::ConditionalValue.new(
28
+ criteria: [],
29
+ value: PrefabProto::ConfigValue.new(log_level: PrefabProto::LogLevel::INFO)
30
+ )
31
+ ]
32
+ )
33
+ ]
34
+ )
35
+
36
+ config_for_sdk = PrefabProto::Config.new(
37
+ id: 123,
38
+ key: 'basic-config',
39
+ config_type: PrefabProto::ConfigType::CONFIG,
40
+ rows: [DEFAULT_ROW],
41
+ send_to_client_sdk: true
42
+ )
43
+
44
+ config_not_for_sdk = PrefabProto::Config.new(
45
+ id: 787,
46
+ key: 'non-sdk-basic-config',
47
+ config_type: PrefabProto::ConfigType::CONFIG,
48
+ rows: [DEFAULT_ROW]
49
+ )
50
+
51
+ ff = PrefabProto::Config.new(
52
+ id: 456,
53
+ key: 'feature-flag',
54
+ config_type: PrefabProto::ConfigType::FEATURE_FLAG,
55
+ rows: [
56
+ PrefabProto::ConfigRow.new(
57
+ values: [
58
+ PrefabProto::ConditionalValue.new(
59
+ value: TRUE_CONFIG,
60
+ criteria: [
61
+ PrefabProto::Criterion.new(
62
+ operator: PrefabProto::Criterion::CriterionOperator::PROP_ENDS_WITH_ONE_OF,
63
+ value_to_match: string_list(['hotmail.com', 'gmail.com']),
64
+ property_name: 'user.email'
65
+ )
66
+ ]
67
+ ),
68
+ PrefabProto::ConditionalValue.new(value: FALSE_CONFIG)
69
+ ]
70
+ )
71
+ ]
72
+ )
73
+
74
+ @client = new_client(
75
+ config: [log_level, config_for_sdk, config_not_for_sdk, ff],
76
+ project_env_id: PROJECT_ENV_ID,
77
+ collect_evaluation_summaries: true,
78
+ prefab_config_override_dir: '/tmp',
79
+ prefab_config_classpath_dir: '/tmp',
80
+ context_upload_mode: :periodic_example,
81
+ allow_telemetry_in_local_mode: true
82
+ )
83
+ end
84
+
85
+ def test_bootstrap
86
+ result = Prefab::JavaScriptStub.new(@client).bootstrap({})
87
+
88
+ assert_equal %(
89
+ window._prefabBootstrap = {
90
+ configs: {"log-level":"INFO","basic-config":"default_value","feature-flag":false},
91
+ context: {}
92
+ }
93
+ ).strip, result.strip
94
+
95
+ result = Prefab::JavaScriptStub.new(@client).bootstrap({ user: { email: 'gmail.com' } })
96
+
97
+ assert_equal %(
98
+ window._prefabBootstrap = {
99
+ configs: {"log-level":"INFO","basic-config":"default_value","feature-flag":true},
100
+ context: {"user":{"email":"gmail.com"}}
101
+ }
102
+ ).strip, result.strip
103
+ end
104
+
105
+ def test_generate_stub
106
+ result = Prefab::JavaScriptStub.new(@client).generate_stub({})
107
+
108
+ assert_equal %(
109
+ window.prefab = window.prefab || {};
110
+ window.prefab.config = {"log-level":"INFO","basic-config":"default_value","feature-flag":false};
111
+ window.prefab.get = function(key) {
112
+ var value = window.prefab.config[key];
113
+
114
+ return value;
115
+ };
116
+ window.prefab.isEnabled = function(key) {
117
+ var value = window.prefab.config[key] === true;
118
+
119
+ return value;
120
+ };
121
+ ).strip, result.strip
122
+
123
+ result = Prefab::JavaScriptStub.new(@client).generate_stub({ user: { email: 'gmail.com' } }, "myEvalCallback")
124
+
125
+ assert_equal %(
126
+ window.prefab = window.prefab || {};
127
+ window.prefab.config = {"log-level":"INFO","basic-config":"default_value","feature-flag":true};
128
+ window.prefab.get = function(key) {
129
+ var value = window.prefab.config[key];
130
+ myEvalCallback(key, value);
131
+ return value;
132
+ };
133
+ window.prefab.isEnabled = function(key) {
134
+ var value = window.prefab.config[key] === true;
135
+ myEvalCallback(key, value);
136
+ return value;
137
+ };
138
+
139
+ ).strip, result.strip
140
+ end
141
+ end
@@ -1,23 +1,27 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'test_helper'
4
+ require 'webrick'
2
5
 
3
6
  class TestSSEConfigClient < Minitest::Test
4
7
  def test_client
5
8
  sources = [
6
- "https://api.staging-prefab.cloud/",
9
+ 'https://belt.staging-prefab.cloud/'
7
10
  ]
8
11
 
9
- options = Prefab::Options.new(sources: sources, api_key: ENV['PREFAB_INTEGRATION_TEST_API_KEY'])
12
+ options = Prefab::Options.new(sources: sources, api_key: ENV.fetch('PREFAB_INTEGRATION_TEST_API_KEY', nil))
10
13
 
11
14
  config_loader = OpenStruct.new(highwater_mark: 4)
12
15
 
13
16
  client = Prefab::SSEConfigClient.new(options, config_loader)
14
17
 
15
18
  assert_equal 4, client.headers['x-prefab-start-at-id']
19
+ assert_equal "https://stream.staging-prefab.cloud", client.source
16
20
 
17
21
  result = nil
18
22
 
19
23
  # fake our load_configs block
20
- client.start do |c, source|
24
+ client.start do |c, _event, source|
21
25
  result = c
22
26
  assert_equal :sse, source
23
27
  end
@@ -31,35 +35,176 @@ class TestSSEConfigClient < Minitest::Test
31
35
 
32
36
  def test_failing_over
33
37
  sources = [
34
- "https://does.not.exist.staging-prefab.cloud/",
35
- "https://api.staging-prefab.cloud/",
38
+ 'https://does.not.exist.staging-prefab.cloud/',
39
+ 'https://api.staging-prefab.cloud/'
36
40
  ]
37
41
 
38
- options = Prefab::Options.new(sources: sources, api_key: ENV['PREFAB_INTEGRATION_TEST_API_KEY'])
42
+ prefab_options = Prefab::Options.new(sources: sources, api_key: ENV.fetch('PREFAB_INTEGRATION_TEST_API_KEY', nil))
39
43
 
40
44
  config_loader = OpenStruct.new(highwater_mark: 4)
41
45
 
42
- client = Prefab::SSEConfigClient.new(options, config_loader)
46
+ sse_options = Prefab::SSEConfigClient::Options.new(seconds_between_new_connection: 0.01, sleep_delay_for_new_connection_check: 0.01)
47
+
48
+ client = Prefab::SSEConfigClient.new(prefab_options, config_loader, sse_options)
43
49
 
44
50
  assert_equal 4, client.headers['x-prefab-start-at-id']
45
51
 
46
52
  result = nil
47
53
 
48
54
  # fake our load_configs block
49
- client.start do |c, source|
55
+ client.start do |c, _event, source|
50
56
  result = c
51
57
  assert_equal :sse, source
52
58
  end
53
59
 
54
- wait_for -> { !result.nil? }, max_wait: 10
60
+ wait_for -> { !result.nil? }
55
61
 
56
62
  assert result.configs.size > 30
57
63
  ensure
58
64
  client.close
59
65
 
60
66
  assert_logged [
61
- /failed to connect: .*https:\/\/does.not.exist/,
62
- /HTTP::ConnectionError/,
67
+ %r{failed to connect: .*https://does.not.exist},
68
+ /HTTP::ConnectionError/
63
69
  ]
64
70
  end
71
+
72
+ def test_recovering_from_disconnection
73
+ server, = start_webrick_server(4567, DisconnectingEndpoint)
74
+
75
+ config_loader = OpenStruct.new(highwater_mark: 4)
76
+
77
+ prefab_options = OpenStruct.new(sse_sources: ['http://localhost:4567'], api_key: 'test')
78
+ last_event_id = nil
79
+ client = nil
80
+
81
+ begin
82
+ Thread.new do
83
+ server.start
84
+ end
85
+
86
+ sse_options = Prefab::SSEConfigClient::Options.new(
87
+ sse_default_reconnect_time: 0.1
88
+ )
89
+ client = Prefab::SSEConfigClient.new(prefab_options, config_loader, sse_options)
90
+
91
+ client.start do |_configs, event, _source|
92
+ last_event_id = event.id.to_i
93
+ end
94
+
95
+ wait_for -> { last_event_id && last_event_id > 1 }
96
+ ensure
97
+ client.close
98
+ server.stop
99
+
100
+ refute_nil last_event_id, 'Expected to have received an event'
101
+ assert last_event_id > 1, 'Expected to have received multiple events (indicating a retry)'
102
+ end
103
+ end
104
+
105
+ def test_recovering_from_an_error
106
+ log_output = StringIO.new
107
+ logger = Logger.new(log_output)
108
+
109
+ server, = start_webrick_server(4568, ErroringEndpoint)
110
+
111
+ config_loader = OpenStruct.new(highwater_mark: 4)
112
+
113
+ prefab_options = OpenStruct.new(sse_sources: ['http://localhost:4568'], api_key: 'test')
114
+ last_event_id = nil
115
+ client = nil
116
+
117
+ begin
118
+ Thread.new do
119
+ server.start
120
+ end
121
+
122
+ sse_options = Prefab::SSEConfigClient::Options.new(
123
+ sse_default_reconnect_time: 0.1,
124
+ seconds_between_new_connection: 0.1,
125
+ sleep_delay_for_new_connection_check: 0.1,
126
+ errors_to_close_connection: [SSE::Errors::HTTPStatusError]
127
+ )
128
+ client = Prefab::SSEConfigClient.new(prefab_options, config_loader, sse_options, logger)
129
+
130
+ client.start do |_configs, event, _source|
131
+ last_event_id = event.id.to_i
132
+ end
133
+
134
+ wait_for -> { last_event_id && last_event_id > 2 }
135
+ ensure
136
+ server.stop
137
+ client.close
138
+
139
+ refute_nil last_event_id, 'Expected to have received an event'
140
+ assert last_event_id > 2, 'Expected to have received multiple events (indicating a reconnect)'
141
+ end
142
+
143
+ log_lines = log_output.string.split("\n")
144
+
145
+ assert_match(/SSE Streaming Connect/, log_lines[0])
146
+ assert_match(/SSE Streaming Error/, log_lines[1], 'Expected to have logged an error. If this starts failing after an ld-eventsource upgrade, you might need to tweak NUMBER_OF_FAILURES below')
147
+ assert_match(/Closing SSE connection/, log_lines[2])
148
+ assert_match(/Reconnecting SSE client/, log_lines[3])
149
+ assert_match(/SSE Streaming Connect/, log_lines[4])
150
+ end
151
+
152
+ def start_webrick_server(port, endpoint_class)
153
+ log_string = StringIO.new
154
+ logger = WEBrick::Log.new(log_string)
155
+ server = WEBrick::HTTPServer.new(Port: port, Logger: logger, AccessLog: [])
156
+ server.mount '/api/v1/sse/config', endpoint_class
157
+
158
+ [server, log_string]
159
+ end
160
+
161
+ module SharedEndpointLogic
162
+ def event_id
163
+ @@event_id ||= 0
164
+ @@event_id += 1
165
+ end
166
+
167
+ def setup_response(response)
168
+ response.status = 200
169
+ response['Content-Type'] = 'text/event-stream'
170
+ response['Cache-Control'] = 'no-cache'
171
+ response['Connection'] = 'keep-alive'
172
+
173
+ response.chunked = false
174
+ end
175
+ end
176
+
177
+ class DisconnectingEndpoint < WEBrick::HTTPServlet::AbstractServlet
178
+ include SharedEndpointLogic
179
+
180
+ def do_GET(_request, response)
181
+ setup_response(response)
182
+
183
+ output = response.body
184
+
185
+ output << "id: #{event_id}\n"
186
+ output << "event: message\n"
187
+ output << "data: CmYIu8fh4YaO0x4QZBo0bG9nLWxldmVsLmNsb3VkLnByZWZhYi5zZXJ2ZXIubG9nZ2luZy5FdmVudFByb2Nlc3NvciIfCAESG2phbWVzLmtlYmluZ2VyQHByZWZhYi5jbG91ZDgGSAkSDQhkELvH4eGGjtMeGGU=\n\n"
188
+ end
189
+ end
190
+
191
+ class ErroringEndpoint < WEBrick::HTTPServlet::AbstractServlet
192
+ include SharedEndpointLogic
193
+ NUMBER_OF_FAILURES = 5
194
+
195
+ def do_GET(_request, response)
196
+ setup_response(response)
197
+
198
+ output = response.body
199
+
200
+ output << "id: #{event_id}\n"
201
+
202
+ if event_id < NUMBER_OF_FAILURES
203
+ raise 'ErroringEndpoint' # This manifests as an SSE::Errors::HTTPStatusError
204
+ end
205
+
206
+ output << "event: message\n"
207
+ output << "data: CmYIu8fh4YaO0x4QZBo0bG9nLWxldmVsLmNsb3VkLnByZWZhYi5zZXJ2ZXIubG9nZ2luZy5FdmVudFByb2Nlc3NvciIfCAESG2phbWVzLmtlYmluZ2VyQHByZWZhYi5jbG91ZDgGSAkSDQhkELvH4eGGjtMeGGU=\n\n"
208
+ end
209
+ end
65
210
  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.8.2
4
+ version: 1.8.4
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-09-03 00:00:00.000000000 Z
11
+ date: 2024-09-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -128,6 +128,20 @@ dependencies:
128
128
  - - "!="
129
129
  - !ruby/object:Gem::Version
130
130
  version: 4.16.0
131
+ - !ruby/object:Gem::Dependency
132
+ name: allocation_stats
133
+ requirement: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ type: :development
139
+ prerelease: false
140
+ version_requirements: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
131
145
  - !ruby/object:Gem::Dependency
132
146
  name: benchmark-ips
133
147
  requirement: !ruby/object:Gem::Requirement
@@ -224,6 +238,7 @@ files:
224
238
  - dev/allocation_stats
225
239
  - dev/benchmark
226
240
  - dev/console
241
+ - dev/script_setup.rb
227
242
  - lib/prefab-cloud-ruby.rb
228
243
  - lib/prefab/client.rb
229
244
  - lib/prefab/config_client.rb
@@ -252,6 +267,7 @@ files:
252
267
  - lib/prefab/feature_flag_client.rb
253
268
  - lib/prefab/http_connection.rb
254
269
  - lib/prefab/internal_logger.rb
270
+ - lib/prefab/javascript_stub.rb
255
271
  - lib/prefab/local_config_parser.rb
256
272
  - lib/prefab/log_path_aggregator.rb
257
273
  - lib/prefab/logger_client.rb
@@ -294,6 +310,7 @@ files:
294
310
  - test/test_helper.rb
295
311
  - test/test_integration.rb
296
312
  - test/test_internal_logger.rb
313
+ - test/test_javascript_stub.rb
297
314
  - test/test_local_config_parser.rb
298
315
  - test/test_log_path_aggregator.rb
299
316
  - test/test_logger.rb