prefab-cloud-ruby 1.8.2 → 1.8.3

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: bc28dd551eb4ba62f97902ae49cfa8f474c7a2883a8dce5ea43605d661420407
4
- data.tar.gz: f2bba00e271a6947d0394453f21964d4d849f22004747a434efa58f3a97d6f09
3
+ metadata.gz: ddab427174fc8ee9580364b7f55becb58806d98b94968ee3d903ce67e9b01268
4
+ data.tar.gz: 181cdf62f18a7c9b09846746597273230f6b13100b302ef4531693f0a3d07085
5
5
  SHA512:
6
- metadata.gz: d9b2e0a3ce0aaa1812677c06744272f2ac52a92687420c10942f271261be1b9c7a4de64efc8e98ed2537df1597c00a2a1f359408c19114d64b3ba5838aeab917
7
- data.tar.gz: 22892c02a6570108400edc76af207e233125e4ec85df3d895db2be90c86e9dba7840a9ea4f8e46c2868e9ba6b1c642115677f47cb90db928be40e354a74d1e13
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,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.8.3 - 2024-09-16
4
+
5
+ - Add JavaScript stub & bootstrapping (#200)
6
+
3
7
  ## 1.8.2 - 2024-09-03
4
8
 
5
9
  - 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.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.2 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.2"
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",
@@ -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,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.2
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
@@ -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