prefab-cloud-ruby 1.8.1 → 1.8.3

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: b8b27e64dfd37c521a1d0397cbfa40d0f3c1ce5d1f42e339b116cfb4da7c9f58
4
- data.tar.gz: 9b4f432823d5653b73cd6f33ccf0a6630f0d69d6ba7ea6e9155254ca3da3e26e
3
+ metadata.gz: ddab427174fc8ee9580364b7f55becb58806d98b94968ee3d903ce67e9b01268
4
+ data.tar.gz: 181cdf62f18a7c9b09846746597273230f6b13100b302ef4531693f0a3d07085
5
5
  SHA512:
6
- metadata.gz: df2e756faf5af90410e442d87d861ee5d6507fb9dd681201bb2c56418f8ddc662cb87d100dde617a501c563a04452d8f712e56ea784a94bc2a67c380a992dfc5
7
- data.tar.gz: 2883ed43fb3c579d60efed468c7011ac6a76eb40e2f58737b5f8a7525c337ee5cbbbdbc0b5e3cad160e15c0c13c62ae22cdf1bd14827515f58347e5f13b4aa3b
6
+ metadata.gz: 784b5d8e0229ec1e953d16a2442561a1cc67e18fc4f398f4878ac4672256892a0d6c314b06b9cd835a8309d184d47a2280d348b8afd396f977c6c57248d7c2b9
7
+ data.tar.gz: '0865454d8eab0548aff23e3caa338d037babc8d9a49165424e6b8822d6a5f5b5ba67e8395752bebedb9666ff5cd3f1eac46f4d4df66abc1c8f2274770e4d634f'
@@ -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.3 - 2024-09-16
4
+
5
+ - Add JavaScript stub & bootstrapping (#200)
6
+
7
+ ## 1.8.2 - 2024-09-03
8
+
9
+ - Forbid bad semantic_logger version (#198)
10
+
3
11
  ## 1.8.1 - 2024-09-03
4
12
 
5
13
  - Fix SSE reconnection bug (#197)
data/Gemfile CHANGED
@@ -9,9 +9,10 @@ gem 'uuid'
9
9
 
10
10
  gem 'activesupport', '>= 4'
11
11
 
12
- gem 'semantic_logger', require: "semantic_logger/sync"
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)
@@ -169,10 +172,11 @@ DEPENDENCIES
169
172
  minitest-focus
170
173
  minitest-reporters
171
174
  rdoc
172
- semantic_logger
175
+ semantic_logger (!= 4.16.0)
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.1
1
+ 1.8.3
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,99 @@
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
+ # Generate the JavaScript snippet to bootstrap the client SDK. This will
12
+ # include the configuration values that are permitted to be sent to the
13
+ # client SDK.
14
+ #
15
+ # If the context provided to the client SDK is not the same as the context
16
+ # used to generate the configuration values, the client SDK will still
17
+ # generate a fetch to get the correct values for the context.
18
+ #
19
+ # Any keys that could not be resolved will be logged as a warning to the
20
+ # console.
21
+ def bootstrap(context)
22
+ configs, warnings = data(context)
23
+ <<~JS
24
+ window._prefabBootstrap = {
25
+ configs: #{JSON.dump(configs)},
26
+ context: #{JSON.dump(context)}
27
+ }
28
+ #{log_warnings(warnings)}
29
+ JS
30
+ end
31
+
32
+ # Generate the JavaScript snippet to *replace* the client SDK. Use this to
33
+ # get `prefab.get` and `prefab.isEnabled` functions on the window object.
34
+ #
35
+ # Only use this if you are not using the client SDK and do not need
36
+ # client-side context.
37
+ #
38
+ # Any keys that could not be resolved will be logged as a warning to the
39
+ # console.
40
+ def generate_stub(context)
41
+ configs, warnings = data(context)
42
+ <<~JS
43
+ window.prefab = window.prefab || {};
44
+ window.prefab.config = #{JSON.dump(configs)};
45
+ window.prefab.get = function(key) {
46
+ return window.prefab.config[key];
47
+ };
48
+ window.prefab.isEnabled = function(key) {
49
+ return window.prefab.config[key] === true;
50
+ };
51
+ #{log_warnings(warnings)}
52
+ JS
53
+ end
54
+
55
+ private
56
+
57
+ def underlying_value(value)
58
+ v = Prefab::ConfigValueUnwrapper.new(value, @client.resolver).unwrap
59
+ case v
60
+ when Google::Protobuf::RepeatedField
61
+ v.to_a
62
+ when Prefab::Duration
63
+ v.as_json
64
+ else
65
+ v
66
+ end
67
+ end
68
+
69
+ def log_warnings(warnings)
70
+ return '' if warnings.empty?
71
+
72
+ <<~JS
73
+ console.warn('The following keys could not be resolved:', #{JSON.dump(@warnings)});
74
+ JS
75
+ end
76
+
77
+ def data(context)
78
+ permitted = {}
79
+ warnings = []
80
+ resolver_keys = @client.resolver.keys
81
+
82
+ resolver_keys.each do |key|
83
+ begin
84
+ config = @client.resolver.raw(key)
85
+
86
+ if config.config_type == :FEATURE_FLAG || config.send_to_client_sdk
87
+ permitted[key] = underlying_value(@client.resolver.get(key, context).value)
88
+ end
89
+ rescue StandardError => e
90
+ LOG.warn("Could not resolve key #{key}: #{e}")
91
+
92
+ warnings << key
93
+ end
94
+ end
95
+
96
+ [permitted, warnings]
97
+ end
98
+ end
99
+ end
data/lib/prefab/prefab.rb CHANGED
@@ -78,6 +78,16 @@ module Prefab
78
78
  @singleton.is_ff?(key)
79
79
  end
80
80
 
81
+ def self.bootstrap_javascript(context)
82
+ ensure_initialized
83
+ Prefab::JavaScriptStub.new(@singleton).bootstrap(context)
84
+ end
85
+
86
+ def self.generate_javascript_stub(context)
87
+ ensure_initialized
88
+ Prefab::JavaScriptStub.new(@singleton).generate_stub(context)
89
+ end
90
+
81
91
  private
82
92
 
83
93
  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]
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.1 ruby lib
5
+ # stub: prefab-cloud-ruby 1.8.3 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "prefab-cloud-ruby".freeze
9
- s.version = "1.8.1"
9
+ s.version = "1.8.3"
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-16"
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",
@@ -134,7 +137,8 @@ Gem::Specification.new do |s|
134
137
  s.add_runtime_dependency(%q<ld-eventsource>.freeze, [">= 0"])
135
138
  s.add_runtime_dependency(%q<uuid>.freeze, [">= 0"])
136
139
  s.add_runtime_dependency(%q<activesupport>.freeze, [">= 4"])
137
- s.add_runtime_dependency(%q<semantic_logger>.freeze, [">= 0"])
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"])
@@ -148,7 +152,8 @@ Gem::Specification.new do |s|
148
152
  s.add_dependency(%q<ld-eventsource>.freeze, [">= 0"])
149
153
  s.add_dependency(%q<uuid>.freeze, [">= 0"])
150
154
  s.add_dependency(%q<activesupport>.freeze, [">= 4"])
151
- s.add_dependency(%q<semantic_logger>.freeze, [">= 0"])
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,133 @@
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: {"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: {"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 = {"basic-config":"default_value","feature-flag":false};
111
+ window.prefab.get = function(key) {
112
+ return window.prefab.config[key];
113
+ };
114
+ window.prefab.isEnabled = function(key) {
115
+ return window.prefab.config[key] === true;
116
+ };
117
+ ).strip, result.strip
118
+
119
+ result = Prefab::JavaScriptStub.new(@client).generate_stub({ user: { email: 'gmail.com' } })
120
+
121
+ assert_equal %(
122
+ window.prefab = window.prefab || {};
123
+ window.prefab.config = {"basic-config":"default_value","feature-flag":true};
124
+ window.prefab.get = function(key) {
125
+ return window.prefab.config[key];
126
+ };
127
+ window.prefab.isEnabled = function(key) {
128
+ return window.prefab.config[key] === true;
129
+ };
130
+
131
+ ).strip, result.strip
132
+ end
133
+ end
@@ -1,12 +1,15 @@
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://api.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
 
@@ -17,7 +20,7 @@ class TestSSEConfigClient < Minitest::Test
17
20
  result = nil
18
21
 
19
22
  # fake our load_configs block
20
- client.start do |c, source|
23
+ client.start do |c, _event, source|
21
24
  result = c
22
25
  assert_equal :sse, source
23
26
  end
@@ -31,35 +34,176 @@ class TestSSEConfigClient < Minitest::Test
31
34
 
32
35
  def test_failing_over
33
36
  sources = [
34
- "https://does.not.exist.staging-prefab.cloud/",
35
- "https://api.staging-prefab.cloud/",
37
+ 'https://does.not.exist.staging-prefab.cloud/',
38
+ 'https://api.staging-prefab.cloud/'
36
39
  ]
37
40
 
38
- options = Prefab::Options.new(sources: sources, api_key: ENV['PREFAB_INTEGRATION_TEST_API_KEY'])
41
+ prefab_options = Prefab::Options.new(sources: sources, api_key: ENV.fetch('PREFAB_INTEGRATION_TEST_API_KEY', nil))
39
42
 
40
43
  config_loader = OpenStruct.new(highwater_mark: 4)
41
44
 
42
- client = Prefab::SSEConfigClient.new(options, config_loader)
45
+ sse_options = Prefab::SSEConfigClient::Options.new(seconds_between_new_connection: 0.01, sleep_delay_for_new_connection_check: 0.01)
46
+
47
+ client = Prefab::SSEConfigClient.new(prefab_options, config_loader, sse_options)
43
48
 
44
49
  assert_equal 4, client.headers['x-prefab-start-at-id']
45
50
 
46
51
  result = nil
47
52
 
48
53
  # fake our load_configs block
49
- client.start do |c, source|
54
+ client.start do |c, _event, source|
50
55
  result = c
51
56
  assert_equal :sse, source
52
57
  end
53
58
 
54
- wait_for -> { !result.nil? }, max_wait: 10
59
+ wait_for -> { !result.nil? }
55
60
 
56
61
  assert result.configs.size > 30
57
62
  ensure
58
63
  client.close
59
64
 
60
65
  assert_logged [
61
- /failed to connect: .*https:\/\/does.not.exist/,
62
- /HTTP::ConnectionError/,
66
+ %r{failed to connect: .*https://does.not.exist},
67
+ /HTTP::ConnectionError/
63
68
  ]
64
69
  end
70
+
71
+ def test_recovering_from_disconnection
72
+ server, = start_webrick_server(4567, DisconnectingEndpoint)
73
+
74
+ config_loader = OpenStruct.new(highwater_mark: 4)
75
+
76
+ prefab_options = OpenStruct.new(sse_sources: ['http://localhost:4567'], api_key: 'test')
77
+ last_event_id = nil
78
+ client = nil
79
+
80
+ begin
81
+ Thread.new do
82
+ server.start
83
+ end
84
+
85
+ sse_options = Prefab::SSEConfigClient::Options.new(
86
+ sse_default_reconnect_time: 0.1
87
+ )
88
+ client = Prefab::SSEConfigClient.new(prefab_options, config_loader, sse_options)
89
+
90
+ client.start do |_configs, event, _source|
91
+ last_event_id = event.id.to_i
92
+ end
93
+
94
+ wait_for -> { last_event_id && last_event_id > 1 }
95
+ ensure
96
+ client.close
97
+ server.stop
98
+
99
+ refute_nil last_event_id, 'Expected to have received an event'
100
+ assert last_event_id > 1, 'Expected to have received multiple events (indicating a retry)'
101
+ end
102
+ end
103
+
104
+ def test_recovering_from_an_error
105
+ log_output = StringIO.new
106
+ logger = Logger.new(log_output)
107
+
108
+ server, = start_webrick_server(4568, ErroringEndpoint)
109
+
110
+ config_loader = OpenStruct.new(highwater_mark: 4)
111
+
112
+ prefab_options = OpenStruct.new(sse_sources: ['http://localhost:4568'], api_key: 'test')
113
+ last_event_id = nil
114
+ client = nil
115
+
116
+ begin
117
+ Thread.new do
118
+ server.start
119
+ end
120
+
121
+ sse_options = Prefab::SSEConfigClient::Options.new(
122
+ sse_default_reconnect_time: 0.1,
123
+ seconds_between_new_connection: 0.1,
124
+ sleep_delay_for_new_connection_check: 0.1,
125
+ errors_to_close_connection: [SSE::Errors::HTTPStatusError]
126
+ )
127
+ client = Prefab::SSEConfigClient.new(prefab_options, config_loader, sse_options, logger)
128
+
129
+ client.start do |_configs, event, _source|
130
+ last_event_id = event.id.to_i
131
+ end
132
+
133
+ wait_for -> { last_event_id && last_event_id > 2 }
134
+ ensure
135
+ server.stop
136
+ client.close
137
+
138
+ refute_nil last_event_id, 'Expected to have received an event'
139
+ assert last_event_id > 2, 'Expected to have received multiple events (indicating a reconnect)'
140
+ end
141
+
142
+ log_lines = log_output.string.split("\n")
143
+
144
+ assert_match(/SSE Streaming Connect/, log_lines[0])
145
+ 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')
146
+ assert_match(/Closing SSE connection/, log_lines[2])
147
+ assert_match(/Reconnecting SSE client/, log_lines[3])
148
+ assert_match(/SSE Streaming Connect/, log_lines[4])
149
+ end
150
+
151
+ def start_webrick_server(port, endpoint_class)
152
+ log_string = StringIO.new
153
+ logger = WEBrick::Log.new(log_string)
154
+ server = WEBrick::HTTPServer.new(Port: port, Logger: logger, AccessLog: [])
155
+ server.mount '/api/v1/sse/config', endpoint_class
156
+
157
+ [server, log_string]
158
+ end
159
+
160
+ module SharedEndpointLogic
161
+ def event_id
162
+ @@event_id ||= 0
163
+ @@event_id += 1
164
+ end
165
+
166
+ def setup_response(response)
167
+ response.status = 200
168
+ response['Content-Type'] = 'text/event-stream'
169
+ response['Cache-Control'] = 'no-cache'
170
+ response['Connection'] = 'keep-alive'
171
+
172
+ response.chunked = false
173
+ end
174
+ end
175
+
176
+ class DisconnectingEndpoint < WEBrick::HTTPServlet::AbstractServlet
177
+ include SharedEndpointLogic
178
+
179
+ def do_GET(_request, response)
180
+ setup_response(response)
181
+
182
+ output = response.body
183
+
184
+ output << "id: #{event_id}\n"
185
+ output << "event: message\n"
186
+ output << "data: CmYIu8fh4YaO0x4QZBo0bG9nLWxldmVsLmNsb3VkLnByZWZhYi5zZXJ2ZXIubG9nZ2luZy5FdmVudFByb2Nlc3NvciIfCAESG2phbWVzLmtlYmluZ2VyQHByZWZhYi5jbG91ZDgGSAkSDQhkELvH4eGGjtMeGGU=\n\n"
187
+ end
188
+ end
189
+
190
+ class ErroringEndpoint < WEBrick::HTTPServlet::AbstractServlet
191
+ include SharedEndpointLogic
192
+ NUMBER_OF_FAILURES = 5
193
+
194
+ def do_GET(_request, response)
195
+ setup_response(response)
196
+
197
+ output = response.body
198
+
199
+ output << "id: #{event_id}\n"
200
+
201
+ if event_id < NUMBER_OF_FAILURES
202
+ raise 'ErroringEndpoint' # This manifests as an SSE::Errors::HTTPStatusError
203
+ end
204
+
205
+ output << "event: message\n"
206
+ output << "data: CmYIu8fh4YaO0x4QZBo0bG9nLWxldmVsLmNsb3VkLnByZWZhYi5zZXJ2ZXIubG9nZ2luZy5FdmVudFByb2Nlc3NvciIfCAESG2phbWVzLmtlYmluZ2VyQHByZWZhYi5jbG91ZDgGSAkSDQhkELvH4eGGjtMeGGU=\n\n"
207
+ end
208
+ end
65
209
  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.1
4
+ version: 1.8.3
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-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -116,12 +116,26 @@ dependencies:
116
116
  version: '4'
117
117
  - !ruby/object:Gem::Dependency
118
118
  name: semantic_logger
119
+ requirement: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "!="
122
+ - !ruby/object:Gem::Version
123
+ version: 4.16.0
124
+ type: :runtime
125
+ prerelease: false
126
+ version_requirements: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "!="
129
+ - !ruby/object:Gem::Version
130
+ version: 4.16.0
131
+ - !ruby/object:Gem::Dependency
132
+ name: allocation_stats
119
133
  requirement: !ruby/object:Gem::Requirement
120
134
  requirements:
121
135
  - - ">="
122
136
  - !ruby/object:Gem::Version
123
137
  version: '0'
124
- type: :runtime
138
+ type: :development
125
139
  prerelease: false
126
140
  version_requirements: !ruby/object:Gem::Requirement
127
141
  requirements:
@@ -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