prefab-cloud-ruby 1.8.2 → 1.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +1 -1
- data/CHANGELOG.md +8 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +4 -0
- data/Rakefile +8 -6
- data/VERSION +1 -1
- data/dev/allocation_stats +3 -3
- data/dev/console +2 -17
- data/dev/script_setup.rb +18 -0
- data/lib/prefab/config_client.rb +2 -2
- data/lib/prefab/config_resolver.rb +4 -0
- data/lib/prefab/criteria_evaluator.rb +4 -2
- data/lib/prefab/duration.rb +4 -0
- data/lib/prefab/javascript_stub.rb +85 -0
- data/lib/prefab/prefab.rb +36 -0
- data/lib/prefab/sse_config_client.rb +38 -18
- data/lib/prefab-cloud-ruby.rb +1 -0
- data/prefab-cloud-ruby.gemspec +8 -3
- data/test/test_javascript_stub.rb +141 -0
- data/test/test_sse_config_client.rb +156 -11
- metadata +19 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ec8c47fb2fe1945e2d189198fbb2ecb8b2eee9d77537ac6569196659931badf4
|
4
|
+
data.tar.gz: 58afaf18e4541429db3e4204f7bc2c19340028d894b84a75d7195aac39fd6b37
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7ed0b65787d13a25a301a75481cff40db9b7dc1b036aea75c5e241b0bdddfd26577317b013830c7533ad0da51b7d087c3e59df5d8c38c07382509cfbc8d9eeb1
|
7
|
+
data.tar.gz: c57bb8881f6a61b252013c118240bc64b8ffaa248caafd234747f1897dd3d67aab6b02b72a3c1e8b164feb689590587475fe5d605f13c1ac4b5c0b426e45f137
|
data/.github/workflows/ruby.yml
CHANGED
@@ -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
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.
|
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('
|
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('
|
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('
|
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
|
-
|
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"
|
data/dev/script_setup.rb
ADDED
@@ -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)
|
data/lib/prefab/config_client.rb
CHANGED
@@ -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,
|
51
|
+
load_configs(configs, source)
|
52
52
|
end
|
53
53
|
end
|
54
54
|
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
|
-
|
25
|
-
LOG.trace
|
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
|
|
data/lib/prefab/duration.rb
CHANGED
@@ -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
|
-
|
6
|
-
|
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,
|
11
|
-
@
|
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 @
|
23
|
-
|
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
|
51
|
+
sleep @options.sleep_delay_for_new_connection_check
|
34
52
|
|
35
53
|
if @client.closed?
|
36
|
-
closed_count +=
|
54
|
+
closed_count += @options.sleep_delay_for_new_connection_check
|
37
55
|
|
38
|
-
if closed_count >
|
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
|
-
|
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:
|
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
|
-
|
81
|
+
@logger.error "SSE Streaming Error: #{error.inspect} for url #{url}"
|
62
82
|
|
63
|
-
if error.is_a?(
|
64
|
-
|
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}:#{@
|
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 >= @
|
105
|
+
if @source_index >= @prefab_options.sse_sources.size
|
86
106
|
@source_index = 0
|
87
107
|
end
|
88
108
|
|
89
|
-
return @
|
109
|
+
return @prefab_options.sse_sources[@source_index].sub(/(belt|suspenders)\./, 'stream.')
|
90
110
|
end
|
91
111
|
end
|
92
112
|
end
|
data/lib/prefab-cloud-ruby.rb
CHANGED
data/prefab-cloud-ruby.gemspec
CHANGED
@@ -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.
|
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.
|
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-
|
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
|
-
|
9
|
+
'https://belt.staging-prefab.cloud/'
|
7
10
|
]
|
8
11
|
|
9
|
-
options = Prefab::Options.new(sources: sources, api_key: ENV
|
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
|
-
|
35
|
-
|
38
|
+
'https://does.not.exist.staging-prefab.cloud/',
|
39
|
+
'https://api.staging-prefab.cloud/'
|
36
40
|
]
|
37
41
|
|
38
|
-
|
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
|
-
|
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? }
|
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
|
-
|
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.
|
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-
|
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
|