sdk-reforge 1.9.0
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 +7 -0
- data/.envrc.sample +3 -0
- data/.github/CODEOWNERS +2 -0
- data/.github/pull_request_template.md +8 -0
- data/.github/workflows/ruby.yml +48 -0
- data/.gitmodules +3 -0
- data/.rubocop.yml +13 -0
- data/.tool-versions +1 -0
- data/CHANGELOG.md +257 -0
- data/CODEOWNERS +1 -0
- data/Gemfile +29 -0
- data/Gemfile.lock +182 -0
- data/LICENSE.txt +20 -0
- data/README.md +105 -0
- data/Rakefile +63 -0
- data/VERSION +1 -0
- data/compile_protos.sh +20 -0
- data/dev/allocation_stats +60 -0
- data/dev/benchmark +40 -0
- data/dev/console +12 -0
- data/dev/script_setup.rb +18 -0
- data/lib/prefab_pb.rb +77 -0
- data/lib/reforge/caching_http_connection.rb +95 -0
- data/lib/reforge/client.rb +133 -0
- data/lib/reforge/config_client.rb +275 -0
- data/lib/reforge/config_client_presenter.rb +18 -0
- data/lib/reforge/config_loader.rb +67 -0
- data/lib/reforge/config_resolver.rb +84 -0
- data/lib/reforge/config_value_unwrapper.rb +123 -0
- data/lib/reforge/config_value_wrapper.rb +18 -0
- data/lib/reforge/context.rb +241 -0
- data/lib/reforge/context_shape.rb +20 -0
- data/lib/reforge/context_shape_aggregator.rb +70 -0
- data/lib/reforge/criteria_evaluator.rb +345 -0
- data/lib/reforge/duration.rb +58 -0
- data/lib/reforge/encryption.rb +65 -0
- data/lib/reforge/error.rb +6 -0
- data/lib/reforge/errors/env_var_parse_error.rb +11 -0
- data/lib/reforge/errors/initialization_timeout_error.rb +12 -0
- data/lib/reforge/errors/invalid_sdk_key_error.rb +19 -0
- data/lib/reforge/errors/missing_default_error.rb +13 -0
- data/lib/reforge/errors/missing_env_var_error.rb +11 -0
- data/lib/reforge/errors/uninitialized_error.rb +13 -0
- data/lib/reforge/evaluation.rb +53 -0
- data/lib/reforge/evaluation_summary_aggregator.rb +86 -0
- data/lib/reforge/example_contexts_aggregator.rb +77 -0
- data/lib/reforge/exponential_backoff.rb +21 -0
- data/lib/reforge/feature_flag_client.rb +43 -0
- data/lib/reforge/fixed_size_hash.rb +14 -0
- data/lib/reforge/http_connection.rb +45 -0
- data/lib/reforge/internal_logger.rb +43 -0
- data/lib/reforge/javascript_stub.rb +99 -0
- data/lib/reforge/local_config_parser.rb +151 -0
- data/lib/reforge/murmer3.rb +50 -0
- data/lib/reforge/options.rb +191 -0
- data/lib/reforge/periodic_sync.rb +74 -0
- data/lib/reforge/prefab.rb +120 -0
- data/lib/reforge/rate_limit_cache.rb +41 -0
- data/lib/reforge/resolved_config_presenter.rb +86 -0
- data/lib/reforge/semver.rb +132 -0
- data/lib/reforge/sse_config_client.rb +112 -0
- data/lib/reforge/time_helpers.rb +7 -0
- data/lib/reforge/weighted_value_resolver.rb +42 -0
- data/lib/reforge/yaml_config_parser.rb +34 -0
- data/lib/reforge-sdk.rb +57 -0
- data/test/fixtures/datafile.json +87 -0
- data/test/integration_test.rb +171 -0
- data/test/integration_test_helpers.rb +114 -0
- data/test/support/common_helpers.rb +201 -0
- data/test/support/mock_base_client.rb +41 -0
- data/test/support/mock_config_client.rb +19 -0
- data/test/support/mock_config_loader.rb +1 -0
- data/test/test_caching_http_connection.rb +218 -0
- data/test/test_client.rb +351 -0
- data/test/test_config_client.rb +84 -0
- data/test/test_config_loader.rb +82 -0
- data/test/test_config_resolver.rb +502 -0
- data/test/test_config_value_unwrapper.rb +270 -0
- data/test/test_config_value_wrapper.rb +42 -0
- data/test/test_context.rb +271 -0
- data/test/test_context_shape.rb +50 -0
- data/test/test_context_shape_aggregator.rb +150 -0
- data/test/test_criteria_evaluator.rb +1180 -0
- data/test/test_duration.rb +37 -0
- data/test/test_encryption.rb +16 -0
- data/test/test_evaluation_summary_aggregator.rb +162 -0
- data/test/test_example_contexts_aggregator.rb +233 -0
- data/test/test_exponential_backoff.rb +18 -0
- data/test/test_feature_flag_client.rb +16 -0
- data/test/test_fixed_size_hash.rb +119 -0
- data/test/test_helper.rb +17 -0
- data/test/test_integration.rb +75 -0
- data/test/test_internal_logger.rb +25 -0
- data/test/test_javascript_stub.rb +176 -0
- data/test/test_local_config_parser.rb +147 -0
- data/test/test_logger_initialization.rb +12 -0
- data/test/test_options.rb +93 -0
- data/test/test_prefab.rb +16 -0
- data/test/test_rate_limit_cache.rb +44 -0
- data/test/test_semver.rb +108 -0
- data/test/test_sse_config_client.rb +211 -0
- data/test/test_weighted_value_resolver.rb +71 -0
- metadata +345 -0
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Reforge
|
4
|
+
class JavaScriptStub
|
5
|
+
LOG = Reforge::InternalLogger.new(self)
|
6
|
+
CAMELS = {}
|
7
|
+
|
8
|
+
def initialize(client = nil)
|
9
|
+
@client = client || Prefab.instance
|
10
|
+
end
|
11
|
+
|
12
|
+
def bootstrap(context)
|
13
|
+
configs, warnings = data(context, :bootstrap)
|
14
|
+
<<~JS
|
15
|
+
window._prefabBootstrap = {
|
16
|
+
evaluations: #{JSON.dump(configs)},
|
17
|
+
context: #{JSON.dump(context)}
|
18
|
+
}
|
19
|
+
#{log_warnings(warnings)}
|
20
|
+
JS
|
21
|
+
end
|
22
|
+
|
23
|
+
def generate_stub(context, callback = nil)
|
24
|
+
configs, warnings = data(context, :stub)
|
25
|
+
<<~JS
|
26
|
+
window.prefab = window.prefab || {};
|
27
|
+
window.prefab.config = #{JSON.dump(configs)};
|
28
|
+
window.prefab.get = function(key) {
|
29
|
+
var value = window.prefab.config[key];
|
30
|
+
#{callback && " #{callback}(key, value);"}
|
31
|
+
return value;
|
32
|
+
};
|
33
|
+
window.prefab.isEnabled = function(key) {
|
34
|
+
var value = window.prefab.config[key] === true;
|
35
|
+
#{callback && " #{callback}(key, value);"}
|
36
|
+
return value;
|
37
|
+
};
|
38
|
+
#{log_warnings(warnings)}
|
39
|
+
JS
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def underlying_value(value)
|
45
|
+
v = Reforge::ConfigValueUnwrapper.new(value, @client.resolver).unwrap(raw_json: true)
|
46
|
+
case v
|
47
|
+
when Google::Protobuf::RepeatedField
|
48
|
+
v.to_a
|
49
|
+
when Reforge::Duration
|
50
|
+
v.as_json
|
51
|
+
else
|
52
|
+
v
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def log_warnings(warnings)
|
57
|
+
return '' if warnings.empty?
|
58
|
+
|
59
|
+
<<~JS
|
60
|
+
console.warn('The following keys could not be resolved:', #{JSON.dump(warnings)});
|
61
|
+
JS
|
62
|
+
end
|
63
|
+
|
64
|
+
def data(context, mode)
|
65
|
+
permitted = {}
|
66
|
+
warnings = []
|
67
|
+
resolver_keys = @client.resolver.keys
|
68
|
+
|
69
|
+
resolver_keys.each do |key|
|
70
|
+
begin
|
71
|
+
config = @client.resolver.raw(key)
|
72
|
+
|
73
|
+
if config.config_type == :FEATURE_FLAG || config.send_to_client_sdk || config.config_type == :LOG_LEVEL
|
74
|
+
value = @client.resolver.get(key, context).value
|
75
|
+
if mode == :bootstrap
|
76
|
+
permitted[key] = { value: { to_camel_case(value.type) => underlying_value(value) } }
|
77
|
+
else
|
78
|
+
permitted[key] = underlying_value(value)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
rescue StandardError => e
|
82
|
+
LOG.warn("Could not resolve key #{key}: #{e}")
|
83
|
+
|
84
|
+
warnings << key
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
[permitted, warnings]
|
89
|
+
end
|
90
|
+
|
91
|
+
def to_camel_case(str)
|
92
|
+
CAMELS[str] ||= begin
|
93
|
+
str.to_s.split('_').map.with_index { |word, index|
|
94
|
+
index == 0 ? word : word.capitalize
|
95
|
+
}.join
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Reforge
|
4
|
+
class LocalConfigParser
|
5
|
+
class << self
|
6
|
+
def parse(key, value, config, file)
|
7
|
+
if value.instance_of?(Hash)
|
8
|
+
if value['feature_flag']
|
9
|
+
config[key] = feature_flag_config(file, key, value)
|
10
|
+
elsif value['type'] == 'provided'
|
11
|
+
config[key] = provided_config(file, key, value)
|
12
|
+
elsif value['decrypt_with'] || value['confidential']
|
13
|
+
config[key] = complex_string(file, key, value)
|
14
|
+
else
|
15
|
+
value.each do |nest_key, nest_value|
|
16
|
+
nested_key = "#{key}.#{nest_key}"
|
17
|
+
nested_key = key if nest_key == '_'
|
18
|
+
parse(nested_key, nest_value, config, file)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
else
|
22
|
+
config[key] = {
|
23
|
+
source: file,
|
24
|
+
match: 'default',
|
25
|
+
config: PrefabProto::Config.new(
|
26
|
+
config_type: :CONFIG,
|
27
|
+
key: key,
|
28
|
+
rows: [
|
29
|
+
PrefabProto::ConfigRow.new(values: [
|
30
|
+
PrefabProto::ConditionalValue.new(value: value_from(key, value))
|
31
|
+
])
|
32
|
+
]
|
33
|
+
)
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
config
|
38
|
+
end
|
39
|
+
|
40
|
+
def value_from(key, raw)
|
41
|
+
case raw
|
42
|
+
when String
|
43
|
+
if key.to_s.start_with? 'log-level'
|
44
|
+
prefab_log_level_resolve = PrefabProto::LogLevel.resolve(raw.upcase.to_sym) || PrefabProto::LogLevel::NOT_SET_LOG_LEVEL
|
45
|
+
{ log_level: prefab_log_level_resolve }
|
46
|
+
else
|
47
|
+
{ string: raw }
|
48
|
+
end
|
49
|
+
when Integer
|
50
|
+
{ int: raw }
|
51
|
+
when TrueClass, FalseClass
|
52
|
+
{ bool: raw }
|
53
|
+
when Float
|
54
|
+
{ double: raw }
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def feature_flag_config(file, key, value)
|
59
|
+
criterion = (parse_criterion(value['criterion']) if value['criterion'])
|
60
|
+
|
61
|
+
variant = PrefabProto::ConfigValue.new(value_from(key, value['value']))
|
62
|
+
|
63
|
+
row = PrefabProto::ConfigRow.new(
|
64
|
+
values: [
|
65
|
+
PrefabProto::ConditionalValue.new(
|
66
|
+
criteria: [criterion].compact,
|
67
|
+
value: variant
|
68
|
+
)
|
69
|
+
]
|
70
|
+
)
|
71
|
+
|
72
|
+
raise Reforge::Error, "Feature flag config `#{key}` #{file} must have a `value`" unless value.key?('value')
|
73
|
+
|
74
|
+
{
|
75
|
+
source: file,
|
76
|
+
match: key,
|
77
|
+
config: PrefabProto::Config.new(
|
78
|
+
config_type: :FEATURE_FLAG,
|
79
|
+
key: key,
|
80
|
+
allowable_values: [variant],
|
81
|
+
rows: [row]
|
82
|
+
)
|
83
|
+
}
|
84
|
+
end
|
85
|
+
|
86
|
+
def provided_config(file, key, value_hash)
|
87
|
+
value = PrefabProto::ConfigValue.new(provided: PrefabProto::Provided.new(
|
88
|
+
source: :ENV_VAR,
|
89
|
+
lookup: value_hash["lookup"],
|
90
|
+
),
|
91
|
+
confidential: value_hash["confidential"],
|
92
|
+
)
|
93
|
+
|
94
|
+
row = PrefabProto::ConfigRow.new(
|
95
|
+
values: [
|
96
|
+
PrefabProto::ConditionalValue.new(
|
97
|
+
value: value
|
98
|
+
)
|
99
|
+
]
|
100
|
+
)
|
101
|
+
|
102
|
+
{
|
103
|
+
source: file,
|
104
|
+
match: value.provided.lookup,
|
105
|
+
config: PrefabProto::Config.new(
|
106
|
+
config_type: :CONFIG,
|
107
|
+
key: key,
|
108
|
+
rows: [row]
|
109
|
+
)
|
110
|
+
}
|
111
|
+
end
|
112
|
+
|
113
|
+
def complex_string(file, key, value_hash)
|
114
|
+
value = PrefabProto::ConfigValue.new(
|
115
|
+
string: value_hash["value"],
|
116
|
+
confidential: value_hash["confidential"],
|
117
|
+
decrypt_with: value_hash["decrypt_with"],
|
118
|
+
)
|
119
|
+
|
120
|
+
row = PrefabProto::ConfigRow.new(
|
121
|
+
values: [
|
122
|
+
PrefabProto::ConditionalValue.new(
|
123
|
+
value: value
|
124
|
+
)
|
125
|
+
]
|
126
|
+
)
|
127
|
+
|
128
|
+
{
|
129
|
+
source: file,
|
130
|
+
config: PrefabProto::Config.new(
|
131
|
+
config_type: :CONFIG,
|
132
|
+
key: key,
|
133
|
+
rows: [row]
|
134
|
+
)
|
135
|
+
}
|
136
|
+
end
|
137
|
+
|
138
|
+
def parse_criterion(criterion)
|
139
|
+
PrefabProto::Criterion.new(operator: criterion['operator'],
|
140
|
+
property_name: criterion['property'],
|
141
|
+
value_to_match: parse_value_to_match(criterion['values']))
|
142
|
+
end
|
143
|
+
|
144
|
+
def parse_value_to_match(values)
|
145
|
+
raise "Can't handle #{values}" unless values.instance_of?(Array)
|
146
|
+
|
147
|
+
PrefabProto::ConfigValue.new(string_list: PrefabProto::StringList.new(values: values))
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Murmur3
|
4
|
+
## MurmurHash3 was written by Austin Appleby, and is placed in the public
|
5
|
+
## domain. The author hereby disclaims copyright to this source code.
|
6
|
+
|
7
|
+
MASK32 = 0xffffffff
|
8
|
+
|
9
|
+
def self.murmur3_32_rotl(x, r)
|
10
|
+
((x << r) | (x >> (32 - r))) & MASK32
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.murmur3_32_fmix(h)
|
14
|
+
h &= MASK32
|
15
|
+
h ^= h >> 16
|
16
|
+
h = (h * 0x85ebca6b) & MASK32
|
17
|
+
h ^= h >> 13
|
18
|
+
h = (h * 0xc2b2ae35) & MASK32
|
19
|
+
h ^ (h >> 16)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.murmur3_32__mmix(k1)
|
23
|
+
k1 = (k1 * 0xcc9e2d51) & MASK32
|
24
|
+
k1 = murmur3_32_rotl(k1, 15)
|
25
|
+
(k1 * 0x1b873593) & MASK32
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.murmur3_32(str, seed = 0)
|
29
|
+
h1 = seed
|
30
|
+
numbers = str.unpack('V*C*')
|
31
|
+
tailn = str.length % 4
|
32
|
+
tail = numbers.slice!(numbers.size - tailn, tailn)
|
33
|
+
for k1 in numbers
|
34
|
+
h1 ^= murmur3_32__mmix(k1)
|
35
|
+
h1 = murmur3_32_rotl(h1, 13)
|
36
|
+
h1 = (h1 * 5 + 0xe6546b64) & MASK32
|
37
|
+
end
|
38
|
+
|
39
|
+
unless tail.empty?
|
40
|
+
k1 = 0
|
41
|
+
tail.reverse_each do |c1|
|
42
|
+
k1 = (k1 << 8) | c1
|
43
|
+
end
|
44
|
+
h1 ^= murmur3_32__mmix(k1)
|
45
|
+
end
|
46
|
+
|
47
|
+
h1 ^= str.length
|
48
|
+
murmur3_32_fmix(h1)
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,191 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Reforge
|
4
|
+
# This class contains all the options that can be passed to the Prefab client.
|
5
|
+
class Options
|
6
|
+
attr_reader :sdk_key
|
7
|
+
attr_reader :namespace
|
8
|
+
attr_reader :sources
|
9
|
+
attr_reader :sse_sources
|
10
|
+
attr_reader :telemetry_destination
|
11
|
+
attr_reader :config_sources
|
12
|
+
attr_reader :on_no_default
|
13
|
+
attr_reader :initialization_timeout_sec
|
14
|
+
attr_reader :on_init_failure
|
15
|
+
attr_reader :collect_sync_interval
|
16
|
+
attr_reader :use_local_cache
|
17
|
+
attr_reader :datafile
|
18
|
+
attr_reader :global_context
|
19
|
+
attr_accessor :is_fork
|
20
|
+
attr_reader :symbolize_json_names
|
21
|
+
|
22
|
+
module ON_INITIALIZATION_FAILURE
|
23
|
+
RAISE = :raise
|
24
|
+
RETURN = :return
|
25
|
+
end
|
26
|
+
|
27
|
+
module ON_NO_DEFAULT
|
28
|
+
RAISE = :raise
|
29
|
+
RETURN_NIL = :return_nil
|
30
|
+
end
|
31
|
+
|
32
|
+
module DATASOURCES
|
33
|
+
ALL = :all
|
34
|
+
LOCAL_ONLY = :local_only
|
35
|
+
end
|
36
|
+
|
37
|
+
DEFAULT_MAX_PATHS = 1_000
|
38
|
+
DEFAULT_MAX_KEYS = 100_000
|
39
|
+
DEFAULT_MAX_EXAMPLE_CONTEXTS = 100_000
|
40
|
+
DEFAULT_MAX_EVAL_SUMMARIES = 100_000
|
41
|
+
|
42
|
+
DEFAULT_SOURCES = [
|
43
|
+
"https://primary.reforge.com",
|
44
|
+
"https://secondary.reforge.com",
|
45
|
+
].freeze
|
46
|
+
|
47
|
+
private def init(
|
48
|
+
sources: nil,
|
49
|
+
sdk_key: ENV['SDK_API_KEY'] || ENV['PREFAB_API_KEY'],
|
50
|
+
namespace: '',
|
51
|
+
reforge_api_url: nil,
|
52
|
+
on_no_default: ON_NO_DEFAULT::RAISE, # options :raise, :warn_and_return_nil,
|
53
|
+
initialization_timeout_sec: 10, # how long to wait before on_init_failure
|
54
|
+
on_init_failure: ON_INITIALIZATION_FAILURE::RAISE,
|
55
|
+
prefab_datasources: (ENV['REFORGE_DATASOURCES'] || ENV['PREFAB_DATASOURCES']) == 'LOCAL_ONLY' ? DATASOURCES::LOCAL_ONLY : DATASOURCES::ALL,
|
56
|
+
collect_logger_counts: true,
|
57
|
+
collect_max_paths: DEFAULT_MAX_PATHS,
|
58
|
+
collect_sync_interval: nil,
|
59
|
+
context_upload_mode: :periodic_example, # :periodic_example, :shape_only, :none
|
60
|
+
context_max_size: DEFAULT_MAX_EVAL_SUMMARIES,
|
61
|
+
collect_evaluation_summaries: true,
|
62
|
+
collect_max_evaluation_summaries: DEFAULT_MAX_EVAL_SUMMARIES,
|
63
|
+
allow_telemetry_in_local_mode: false,
|
64
|
+
datafile: ENV['PREFAB_DATAFILE'],
|
65
|
+
x_datafile: nil, # DEPRECATED in favor of `datafile`
|
66
|
+
x_use_local_cache: false,
|
67
|
+
symbolize_json_names: false,
|
68
|
+
global_context: {}
|
69
|
+
)
|
70
|
+
@sdk_key = sdk_key
|
71
|
+
@namespace = namespace
|
72
|
+
@on_no_default = on_no_default
|
73
|
+
@initialization_timeout_sec = initialization_timeout_sec
|
74
|
+
@on_init_failure = on_init_failure
|
75
|
+
@prefab_datasources = prefab_datasources
|
76
|
+
|
77
|
+
@datafile = datafile || x_datafile
|
78
|
+
|
79
|
+
if !x_datafile.nil?
|
80
|
+
warn '[DEPRECATION] x_datafile is deprecated. Please provide `datafile` instead'
|
81
|
+
end
|
82
|
+
|
83
|
+
@collect_logger_counts = collect_logger_counts
|
84
|
+
@collect_max_paths = collect_max_paths
|
85
|
+
@collect_sync_interval = collect_sync_interval
|
86
|
+
@collect_evaluation_summaries = collect_evaluation_summaries
|
87
|
+
@collect_max_evaluation_summaries = collect_max_evaluation_summaries
|
88
|
+
@allow_telemetry_in_local_mode = allow_telemetry_in_local_mode
|
89
|
+
@use_local_cache = x_use_local_cache
|
90
|
+
@is_fork = false
|
91
|
+
@global_context = global_context
|
92
|
+
@symbolize_json_names = symbolize_json_names
|
93
|
+
|
94
|
+
# defaults that may be overridden by context_upload_mode
|
95
|
+
@collect_shapes = false
|
96
|
+
@collect_max_shapes = 0
|
97
|
+
@collect_example_contexts = false
|
98
|
+
@collect_max_example_contexts = 0
|
99
|
+
|
100
|
+
if ENV['PREFAB_API_URL_OVERRIDE'] && ENV['PREFAB_API_URL_OVERRIDE'].length > 0
|
101
|
+
sources = ENV['PREFAB_API_URL_OVERRIDE']
|
102
|
+
end
|
103
|
+
|
104
|
+
@sources = Array(sources || DEFAULT_SOURCES).map {|source| remove_trailing_slash(source) }
|
105
|
+
|
106
|
+
@sse_sources = @sources
|
107
|
+
@config_sources = @sources
|
108
|
+
|
109
|
+
@telemetry_destination = @sources.select do |source|
|
110
|
+
source.start_with?('https://') && (source.include?("primary") || source.include?("secondary") || source.include?("belt") || source.include?("suspenders"))
|
111
|
+
end.map do |source|
|
112
|
+
source.sub(/(primary|secondary)\./, 'telemetry.').sub(/(belt|suspenders)\./, 'telemetry.')
|
113
|
+
end[0]
|
114
|
+
|
115
|
+
if reforge_api_url
|
116
|
+
warn '[DEPRECATION] reforge_api_url is deprecated. Please provide `sources` if you need to override the default sources'
|
117
|
+
end
|
118
|
+
|
119
|
+
case context_upload_mode
|
120
|
+
when :none
|
121
|
+
# do nothing
|
122
|
+
when :periodic_example
|
123
|
+
@collect_example_contexts = true
|
124
|
+
@collect_max_example_contexts = context_max_size
|
125
|
+
@collect_shapes = true
|
126
|
+
@collect_max_shapes = context_max_size
|
127
|
+
when :shape_only
|
128
|
+
@collect_shapes = true
|
129
|
+
@collect_max_shapes = context_max_size
|
130
|
+
else
|
131
|
+
raise "Unknown context_upload_mode #{context_upload_mode}. Please provide :periodic_example, :shape_only, or :none."
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def initialize(options = {})
|
136
|
+
init(**options)
|
137
|
+
end
|
138
|
+
|
139
|
+
def local_only?
|
140
|
+
@prefab_datasources == DATASOURCES::LOCAL_ONLY
|
141
|
+
end
|
142
|
+
|
143
|
+
def datafile?
|
144
|
+
!@datafile.nil?
|
145
|
+
end
|
146
|
+
|
147
|
+
def collect_max_paths
|
148
|
+
return 0 unless telemetry_allowed?(@collect_logger_counts)
|
149
|
+
|
150
|
+
@collect_max_paths
|
151
|
+
end
|
152
|
+
|
153
|
+
def collect_max_shapes
|
154
|
+
return 0 unless telemetry_allowed?(@collect_shapes)
|
155
|
+
|
156
|
+
@collect_max_shapes
|
157
|
+
end
|
158
|
+
|
159
|
+
def collect_max_example_contexts
|
160
|
+
return 0 unless telemetry_allowed?(@collect_example_contexts)
|
161
|
+
|
162
|
+
@collect_max_example_contexts
|
163
|
+
end
|
164
|
+
|
165
|
+
def collect_max_evaluation_summaries
|
166
|
+
return 0 unless telemetry_allowed?(@collect_evaluation_summaries)
|
167
|
+
|
168
|
+
@collect_max_evaluation_summaries
|
169
|
+
end
|
170
|
+
|
171
|
+
def sdk_key_id
|
172
|
+
@sdk_key&.split("-")&.first
|
173
|
+
end
|
174
|
+
|
175
|
+
def for_fork
|
176
|
+
clone = self.clone
|
177
|
+
clone.is_fork = true
|
178
|
+
clone
|
179
|
+
end
|
180
|
+
|
181
|
+
private
|
182
|
+
|
183
|
+
def telemetry_allowed?(option)
|
184
|
+
option && (!local_only? || @allow_telemetry_in_local_mode)
|
185
|
+
end
|
186
|
+
|
187
|
+
def remove_trailing_slash(url)
|
188
|
+
url.end_with?('/') ? url[0..-2] : url
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Reforge
|
4
|
+
module PeriodicSync
|
5
|
+
LOG = Reforge::InternalLogger.new(self)
|
6
|
+
|
7
|
+
def sync
|
8
|
+
return if @data.size.zero?
|
9
|
+
|
10
|
+
LOG.debug "Syncing #{@data.size} items"
|
11
|
+
|
12
|
+
start_at_was = @start_at
|
13
|
+
@start_at = Reforge::TimeHelpers.now_in_ms
|
14
|
+
|
15
|
+
flush(prepare_data, start_at_was)
|
16
|
+
end
|
17
|
+
|
18
|
+
def prepare_data
|
19
|
+
to_ship = @data.dup
|
20
|
+
@data.clear
|
21
|
+
|
22
|
+
on_prepare_data
|
23
|
+
|
24
|
+
to_ship
|
25
|
+
end
|
26
|
+
|
27
|
+
def on_prepare_data
|
28
|
+
# noop -- override as you wish
|
29
|
+
end
|
30
|
+
|
31
|
+
def post(url, data)
|
32
|
+
@client.post(url, data)
|
33
|
+
end
|
34
|
+
|
35
|
+
def instance_hash
|
36
|
+
@client.instance_hash
|
37
|
+
end
|
38
|
+
|
39
|
+
def start_periodic_sync(sync_interval)
|
40
|
+
@start_at = Reforge::TimeHelpers.now_in_ms
|
41
|
+
|
42
|
+
@sync_interval = calculate_sync_interval(sync_interval)
|
43
|
+
|
44
|
+
Thread.new do
|
45
|
+
LOG.debug "Initialized #{@name} instance_hash=#{@client.instance_hash}"
|
46
|
+
|
47
|
+
loop do
|
48
|
+
sleep @sync_interval.call
|
49
|
+
sync
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def pool
|
55
|
+
@pool ||= Concurrent::ThreadPoolExecutor.new(
|
56
|
+
fallback_policy: :discard,
|
57
|
+
max_queue: 5,
|
58
|
+
max_threads: 4,
|
59
|
+
min_threads: 1,
|
60
|
+
name: @name
|
61
|
+
)
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def calculate_sync_interval(sync_interval)
|
67
|
+
if sync_interval.is_a?(Numeric)
|
68
|
+
proc { sync_interval }
|
69
|
+
else
|
70
|
+
sync_interval || ExponentialBackoff.new(initial_delay: 8, max_delay: 60 * 5)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Reforge
|
4
|
+
LOG = Reforge::InternalLogger.new(self)
|
5
|
+
@@lock = Concurrent::ReadWriteLock.new
|
6
|
+
@config_has_loaded = false
|
7
|
+
|
8
|
+
def self.init(options = Reforge::Options.new)
|
9
|
+
unless @singleton.nil?
|
10
|
+
LOG.warn 'Prefab already initialized.'
|
11
|
+
return @singleton
|
12
|
+
end
|
13
|
+
|
14
|
+
@@lock.with_write_lock {
|
15
|
+
@singleton = Reforge::Client.new(options)
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.fork
|
20
|
+
ensure_initialized
|
21
|
+
@@lock.with_write_lock {
|
22
|
+
@singleton = @singleton.fork
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.set_rails_loggers
|
27
|
+
ensure_initialized
|
28
|
+
@singleton.set_rails_loggers
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.get(key, default = NO_DEFAULT_PROVIDED, jit_context = NO_DEFAULT_PROVIDED)
|
32
|
+
ensure_initialized key
|
33
|
+
@singleton.get(key, default, jit_context)
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.enabled?(feature_name, jit_context = NO_DEFAULT_PROVIDED)
|
37
|
+
ensure_initialized feature_name
|
38
|
+
@singleton.enabled?(feature_name, jit_context)
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.with_context(properties, &block)
|
42
|
+
ensure_initialized
|
43
|
+
@singleton.with_context(properties, &block)
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.instance
|
47
|
+
ensure_initialized
|
48
|
+
@singleton
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.log_filter
|
52
|
+
InternalLogger.using_reforge_log_filter!
|
53
|
+
return Proc.new do |log|
|
54
|
+
bootstrap_log_level(log)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.finish_init!
|
59
|
+
@config_has_loaded = true
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.bootstrap_log_level(log)
|
63
|
+
level = ENV['REFORGE_LOG_CLIENT_BOOTSTRAP_LOG_LEVEL'] ? ENV['REFORGE_LOG_CLIENT_BOOTSTRAP_LOG_LEVEL'].downcase.to_sym : :warn
|
64
|
+
SemanticLogger::Levels.index(level) <= SemanticLogger::Levels.index(log.level)
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.defined?(key)
|
68
|
+
ensure_initialized key
|
69
|
+
@singleton.defined?(key)
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.is_ff?(key)
|
73
|
+
ensure_initialized key
|
74
|
+
@singleton.is_ff?(key)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Generate the JavaScript snippet to bootstrap the client SDK. This will
|
78
|
+
# include the configuration values that are permitted to be sent to the
|
79
|
+
# client SDK.
|
80
|
+
#
|
81
|
+
# If the context provided to the client SDK is not the same as the context
|
82
|
+
# used to generate the configuration values, the client SDK will still
|
83
|
+
# generate a fetch to get the correct values for the context.
|
84
|
+
#
|
85
|
+
# Any keys that could not be resolved will be logged as a warning to the
|
86
|
+
# console.
|
87
|
+
def self.bootstrap_javascript(context)
|
88
|
+
ensure_initialized
|
89
|
+
Reforge::JavaScriptStub.new(@singleton).bootstrap(context)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Generate the JavaScript snippet to *replace* the client SDK. Use this to
|
93
|
+
# get `prefab.get` and `prefab.isEnabled` functions on the window object.
|
94
|
+
#
|
95
|
+
# Only use this if you are not using the client SDK and do not need
|
96
|
+
# client-side context.
|
97
|
+
#
|
98
|
+
# Any keys that could not be resolved will be logged as a warning to the
|
99
|
+
# console.
|
100
|
+
#
|
101
|
+
# You can pass an optional callback function to be called with the key and
|
102
|
+
# value of each configuration value. This can be useful for logging,
|
103
|
+
# tracking experiment exposure, etc.
|
104
|
+
#
|
105
|
+
# e.g.
|
106
|
+
# - `Prefab.generate_javascript_stub(context, "reportExperimentExposure")`
|
107
|
+
# - `Prefab.generate_javascript_stub(context, "(key,value)=>{console.log({eval: 'eval', key,value})}")`
|
108
|
+
def self.generate_javascript_stub(context, callback = nil)
|
109
|
+
ensure_initialized
|
110
|
+
Reforge::JavaScriptStub.new(@singleton).generate_stub(context, callback)
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
def self.ensure_initialized(key = nil)
|
116
|
+
if not defined? @singleton or @singleton.nil?
|
117
|
+
raise Reforge::Errors::UninitializedError.new(key)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|