prefab-cloud-ruby 1.8.2 → 1.8.4

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: 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