quonfig 0.0.10 → 0.0.12
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/CHANGELOG.md +30 -0
- data/README.md +94 -0
- data/lib/quonfig/caching_http_connection.rb +3 -3
- data/lib/quonfig/client.rb +22 -27
- data/lib/quonfig/config_loader.rb +5 -1
- data/lib/quonfig/config_store.rb +10 -6
- data/lib/quonfig/context.rb +5 -4
- data/lib/quonfig/datadir.rb +4 -10
- data/lib/quonfig/dev_context.rb +4 -2
- data/lib/quonfig/duration.rb +2 -2
- data/lib/quonfig/encryption.rb +12 -16
- data/lib/quonfig/errors/invalid_environment_error.rb +1 -3
- data/lib/quonfig/errors/invalid_sdk_key_error.rb +6 -7
- data/lib/quonfig/errors/missing_env_var_error.rb +0 -3
- data/lib/quonfig/errors/missing_environment_error.rb +1 -1
- data/lib/quonfig/errors/uninitialized_error.rb +1 -1
- data/lib/quonfig/evaluation.rb +11 -8
- data/lib/quonfig/evaluator.rb +34 -37
- data/lib/quonfig/fixed_size_hash.rb +1 -0
- data/lib/quonfig/http_connection.rb +2 -4
- data/lib/quonfig/internal_logger.rb +63 -27
- data/lib/quonfig/murmer3.rb +2 -2
- data/lib/quonfig/options.rb +62 -75
- data/lib/quonfig/periodic_sync.rb +1 -1
- data/lib/quonfig/quonfig.rb +3 -3
- data/lib/quonfig/reason.rb +2 -1
- data/lib/quonfig/resolver.rb +8 -9
- data/lib/quonfig/semantic_logger_filter.rb +4 -3
- data/lib/quonfig/semver.rb +6 -8
- data/lib/quonfig/sse_config_client.rb +14 -15
- data/lib/quonfig/stdlib_formatter.rb +3 -3
- data/lib/quonfig/telemetry/context_shape_aggregator.rb +2 -3
- data/lib/quonfig/telemetry/example_contexts_aggregator.rb +1 -1
- data/lib/quonfig/telemetry/telemetry_reporter.rb +1 -0
- data/lib/quonfig/time_helpers.rb +2 -0
- data/lib/quonfig/version.rb +5 -0
- data/lib/quonfig.rb +2 -1
- data/quonfig.gemspec +29 -165
- metadata +24 -193
- data/.claude/rules/constitution.md +0 -81
- data/.claude/rules/git-safety.md +0 -11
- data/.claude/rules/issue-tracking.md +0 -13
- data/.claude/rules/testing-workflow.md +0 -28
- data/.envrc.sample +0 -3
- data/.github/CODEOWNERS +0 -2
- data/.github/pull_request_template.md +0 -8
- data/.github/workflows/release.yml +0 -49
- data/.github/workflows/ruby.yml +0 -60
- data/.github/workflows/test.yaml +0 -40
- data/.rubocop.yml +0 -13
- data/.tool-versions +0 -1
- data/CLAUDE.md +0 -29
- data/CODEOWNERS +0 -1
- data/Gemfile +0 -26
- data/Gemfile.lock +0 -177
- data/Rakefile +0 -64
- data/VERSION +0 -1
- data/dev/allocation_stats +0 -60
- data/dev/benchmark +0 -40
- data/dev/console +0 -12
- data/dev/script_setup.rb +0 -18
- data/test/fixtures/datafile.json +0 -87
- data/test/integration/test_context_precedence.rb +0 -112
- data/test/integration/test_datadir_environment.rb +0 -54
- data/test/integration/test_dev_overrides.rb +0 -40
- data/test/integration/test_enabled.rb +0 -478
- data/test/integration/test_enabled_with_contexts.rb +0 -64
- data/test/integration/test_get.rb +0 -136
- data/test/integration/test_get_feature_flag.rb +0 -28
- data/test/integration/test_get_or_raise.rb +0 -60
- data/test/integration/test_get_weighted_values.rb +0 -34
- data/test/integration/test_helpers.rb +0 -667
- data/test/integration/test_helpers_test.rb +0 -73
- data/test/integration/test_post.rb +0 -44
- data/test/integration/test_telemetry.rb +0 -170
- data/test/support/common_helpers.rb +0 -106
- data/test/support/mock_base_client.rb +0 -27
- data/test/support/mock_config_loader.rb +0 -1
- data/test/test_bound_client.rb +0 -109
- data/test/test_caching_http_connection.rb +0 -218
- data/test/test_client.rb +0 -255
- data/test/test_client_network_mode.rb +0 -136
- data/test/test_client_telemetry.rb +0 -175
- data/test/test_config_loader.rb +0 -70
- data/test/test_context.rb +0 -139
- data/test/test_context_shape.rb +0 -37
- data/test/test_context_shape_aggregator.rb +0 -126
- data/test/test_datadir.rb +0 -203
- data/test/test_details_getters.rb +0 -242
- data/test/test_dev_context.rb +0 -163
- data/test/test_duration.rb +0 -37
- data/test/test_encryption.rb +0 -16
- data/test/test_evaluation_summaries_aggregator.rb +0 -180
- data/test/test_evaluator.rb +0 -285
- data/test/test_example_contexts_aggregator.rb +0 -119
- data/test/test_exponential_backoff.rb +0 -44
- data/test/test_fixed_size_hash.rb +0 -119
- data/test/test_helper.rb +0 -17
- data/test/test_http_connection.rb +0 -81
- data/test/test_internal_logger.rb +0 -34
- data/test/test_options.rb +0 -198
- data/test/test_rate_limit_cache.rb +0 -44
- data/test/test_reason.rb +0 -79
- data/test/test_rename.rb +0 -65
- data/test/test_resolver.rb +0 -291
- data/test/test_semantic_logger_filter.rb +0 -144
- data/test/test_semver.rb +0 -108
- data/test/test_should_log.rb +0 -186
- data/test/test_sse_config_client.rb +0 -297
- data/test/test_stdlib_formatter.rb +0 -195
- data/test/test_telemetry_reporter.rb +0 -209
- data/test/test_typed_getters.rb +0 -131
- data/test/test_types.rb +0 -141
- data/test/test_weighted_value_resolver.rb +0 -84
data/lib/quonfig/evaluation.rb
CHANGED
|
@@ -16,13 +16,15 @@ module Quonfig
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def reason
|
|
19
|
-
@reason ||= @conditional_value
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
@reason ||= if @conditional_value
|
|
20
|
+
Quonfig::Reason.compute(
|
|
21
|
+
config: @config,
|
|
22
|
+
conditional_value: @conditional_value,
|
|
23
|
+
weighted_value_index: deepest_value.weighted_value_index
|
|
24
|
+
)
|
|
25
|
+
else
|
|
26
|
+
Quonfig::Reason::UNKNOWN
|
|
27
|
+
end
|
|
26
28
|
end
|
|
27
29
|
|
|
28
30
|
def unwrapped_value
|
|
@@ -54,7 +56,8 @@ module Quonfig
|
|
|
54
56
|
selected_value: deepest_value.reportable_wrapped_value,
|
|
55
57
|
weighted_value_index: deepest_value.weighted_value_index,
|
|
56
58
|
selected_index: nil # TODO
|
|
57
|
-
}
|
|
59
|
+
}
|
|
60
|
+
)
|
|
58
61
|
end
|
|
59
62
|
|
|
60
63
|
def deepest_value
|
data/lib/quonfig/evaluator.rb
CHANGED
|
@@ -99,8 +99,10 @@ module Quonfig
|
|
|
99
99
|
|
|
100
100
|
keys.each do |k|
|
|
101
101
|
return hash[k] if hash.key?(k)
|
|
102
|
+
|
|
102
103
|
sk = k.to_s
|
|
103
104
|
return hash[sk] if hash.key?(sk)
|
|
105
|
+
|
|
104
106
|
sym = k.to_sym
|
|
105
107
|
return hash[sym] if hash.key?(sym)
|
|
106
108
|
end
|
|
@@ -147,7 +149,7 @@ module Quonfig
|
|
|
147
149
|
# Faithful port of sdk-node/src/operators.ts evaluateCriterion. Matches
|
|
148
150
|
# context-exists / missing-context semantics (e.g. PROP_IS_NOT_ONE_OF is
|
|
149
151
|
# true when context is missing).
|
|
150
|
-
def evaluate_criterion(criterion, context,
|
|
152
|
+
def evaluate_criterion(criterion, context, _config)
|
|
151
153
|
property_name = hget(criterion, :propertyName) || ''
|
|
152
154
|
operator = hget(criterion, :operator)
|
|
153
155
|
match_value = hget(criterion, :valueToMatch)
|
|
@@ -156,10 +158,10 @@ module Quonfig
|
|
|
156
158
|
|
|
157
159
|
case operator
|
|
158
160
|
when OP_NOT_SET, nil
|
|
159
|
-
|
|
161
|
+
false
|
|
160
162
|
|
|
161
163
|
when OP_ALWAYS_TRUE
|
|
162
|
-
|
|
164
|
+
true
|
|
163
165
|
|
|
164
166
|
when OP_PROP_IS_ONE_OF, OP_PROP_IS_NOT_ONE_OF
|
|
165
167
|
if context_exists && match_value
|
|
@@ -170,7 +172,7 @@ module Quonfig
|
|
|
170
172
|
return match_found == (operator == OP_PROP_IS_ONE_OF)
|
|
171
173
|
end
|
|
172
174
|
end
|
|
173
|
-
|
|
175
|
+
operator == OP_PROP_IS_NOT_ONE_OF
|
|
174
176
|
|
|
175
177
|
when OP_PROP_STARTS_WITH_ONE_OF, OP_PROP_DOES_NOT_START_WITH_ONE_OF
|
|
176
178
|
if context_exists && match_value
|
|
@@ -181,7 +183,7 @@ module Quonfig
|
|
|
181
183
|
return match_found == (operator == OP_PROP_STARTS_WITH_ONE_OF)
|
|
182
184
|
end
|
|
183
185
|
end
|
|
184
|
-
|
|
186
|
+
operator == OP_PROP_DOES_NOT_START_WITH_ONE_OF
|
|
185
187
|
|
|
186
188
|
when OP_PROP_ENDS_WITH_ONE_OF, OP_PROP_DOES_NOT_END_WITH_ONE_OF
|
|
187
189
|
if context_exists && match_value
|
|
@@ -192,7 +194,7 @@ module Quonfig
|
|
|
192
194
|
return match_found == (operator == OP_PROP_ENDS_WITH_ONE_OF)
|
|
193
195
|
end
|
|
194
196
|
end
|
|
195
|
-
|
|
197
|
+
operator == OP_PROP_DOES_NOT_END_WITH_ONE_OF
|
|
196
198
|
|
|
197
199
|
when OP_PROP_CONTAINS_ONE_OF, OP_PROP_DOES_NOT_CONTAIN_ONE_OF
|
|
198
200
|
if context_exists && match_value
|
|
@@ -203,7 +205,7 @@ module Quonfig
|
|
|
203
205
|
return match_found == (operator == OP_PROP_CONTAINS_ONE_OF)
|
|
204
206
|
end
|
|
205
207
|
end
|
|
206
|
-
|
|
208
|
+
operator == OP_PROP_DOES_NOT_CONTAIN_ONE_OF
|
|
207
209
|
|
|
208
210
|
when OP_PROP_MATCHES, OP_PROP_DOES_NOT_MATCH
|
|
209
211
|
mv = hget(match_value, :value)
|
|
@@ -216,7 +218,7 @@ module Quonfig
|
|
|
216
218
|
return false
|
|
217
219
|
end
|
|
218
220
|
end
|
|
219
|
-
|
|
221
|
+
false
|
|
220
222
|
|
|
221
223
|
when OP_HIERARCHICAL_MATCH
|
|
222
224
|
if context_exists && match_value
|
|
@@ -224,7 +226,7 @@ module Quonfig
|
|
|
224
226
|
mv = to_s_nil(hget(match_value, :value))
|
|
225
227
|
return cv.start_with?(mv)
|
|
226
228
|
end
|
|
227
|
-
|
|
229
|
+
false
|
|
228
230
|
|
|
229
231
|
when OP_IN_INT_RANGE
|
|
230
232
|
if context_exists && match_value
|
|
@@ -232,7 +234,7 @@ module Quonfig
|
|
|
232
234
|
num_val = to_float(context_value)
|
|
233
235
|
return num_val >= start_v && num_val < end_v unless num_val.nil?
|
|
234
236
|
end
|
|
235
|
-
|
|
237
|
+
false
|
|
236
238
|
|
|
237
239
|
when OP_PROP_GREATER_THAN, OP_PROP_GREATER_THAN_OR_EQUAL,
|
|
238
240
|
OP_PROP_LESS_THAN, OP_PROP_LESS_THAN_OR_EQUAL
|
|
@@ -244,13 +246,13 @@ module Quonfig
|
|
|
244
246
|
return false if cmp.nil?
|
|
245
247
|
|
|
246
248
|
case operator
|
|
247
|
-
when OP_PROP_GREATER_THAN then return cmp
|
|
249
|
+
when OP_PROP_GREATER_THAN then return cmp.positive?
|
|
248
250
|
when OP_PROP_GREATER_THAN_OR_EQUAL then return cmp >= 0
|
|
249
|
-
when OP_PROP_LESS_THAN then return cmp
|
|
251
|
+
when OP_PROP_LESS_THAN then return cmp.negative?
|
|
250
252
|
when OP_PROP_LESS_THAN_OR_EQUAL then return cmp <= 0
|
|
251
253
|
end
|
|
252
254
|
end
|
|
253
|
-
|
|
255
|
+
false
|
|
254
256
|
|
|
255
257
|
when OP_PROP_BEFORE, OP_PROP_AFTER
|
|
256
258
|
if context_exists && match_value
|
|
@@ -260,7 +262,7 @@ module Quonfig
|
|
|
260
262
|
return operator == OP_PROP_BEFORE ? context_millis < match_millis : context_millis > match_millis
|
|
261
263
|
end
|
|
262
264
|
end
|
|
263
|
-
|
|
265
|
+
false
|
|
264
266
|
|
|
265
267
|
when OP_PROP_SEMVER_LESS_THAN, OP_PROP_SEMVER_EQUAL, OP_PROP_SEMVER_GREATER_THAN
|
|
266
268
|
mv = hget(match_value, :value)
|
|
@@ -270,13 +272,13 @@ module Quonfig
|
|
|
270
272
|
if sv_ctx && sv_mv
|
|
271
273
|
cmp = (sv_ctx <=> sv_mv)
|
|
272
274
|
case operator
|
|
273
|
-
when OP_PROP_SEMVER_LESS_THAN then return cmp
|
|
274
|
-
when OP_PROP_SEMVER_EQUAL then return cmp
|
|
275
|
-
when OP_PROP_SEMVER_GREATER_THAN then return cmp
|
|
275
|
+
when OP_PROP_SEMVER_LESS_THAN then return cmp.negative?
|
|
276
|
+
when OP_PROP_SEMVER_EQUAL then return cmp.zero?
|
|
277
|
+
when OP_PROP_SEMVER_GREATER_THAN then return cmp.positive?
|
|
276
278
|
end
|
|
277
279
|
end
|
|
278
280
|
end
|
|
279
|
-
|
|
281
|
+
false
|
|
280
282
|
|
|
281
283
|
when OP_IN_SEG, OP_NOT_IN_SEG
|
|
282
284
|
if match_value
|
|
@@ -286,21 +288,17 @@ module Quonfig
|
|
|
286
288
|
|
|
287
289
|
return result == (operator == OP_IN_SEG)
|
|
288
290
|
end
|
|
289
|
-
|
|
291
|
+
operator == OP_NOT_IN_SEG
|
|
290
292
|
|
|
291
293
|
else
|
|
292
|
-
|
|
294
|
+
false
|
|
293
295
|
end
|
|
294
296
|
end
|
|
295
297
|
|
|
296
298
|
def lookup_context(context, property_name)
|
|
297
|
-
if MAGIC_CURRENT_TIME_PROPS.include?(property_name)
|
|
298
|
-
return [(Time.now.utc.to_f * 1000).to_i, true]
|
|
299
|
-
end
|
|
299
|
+
return [(Time.now.utc.to_f * 1000).to_i, true] if MAGIC_CURRENT_TIME_PROPS.include?(property_name)
|
|
300
300
|
|
|
301
|
-
if property_name.nil? || property_name.empty?
|
|
302
|
-
return [nil, false]
|
|
303
|
-
end
|
|
301
|
+
return [nil, false] if property_name.nil? || property_name.empty?
|
|
304
302
|
|
|
305
303
|
value = context.get(property_name)
|
|
306
304
|
[value, !value.nil?]
|
|
@@ -362,8 +360,7 @@ module Quonfig
|
|
|
362
360
|
return v.to_f if v.is_a?(Numeric)
|
|
363
361
|
return nil unless v.is_a?(String)
|
|
364
362
|
|
|
365
|
-
|
|
366
|
-
f
|
|
363
|
+
Float(v, exception: false)
|
|
367
364
|
end
|
|
368
365
|
|
|
369
366
|
def compare_numbers(a, b)
|
|
@@ -375,7 +372,7 @@ module Quonfig
|
|
|
375
372
|
end
|
|
376
373
|
|
|
377
374
|
def extract_int_range(value_hash)
|
|
378
|
-
min = -(2**53) + 1
|
|
375
|
+
min = -(2**53) + 1 # approx Number.MIN_SAFE_INTEGER
|
|
379
376
|
max = (2**53) - 1
|
|
380
377
|
raw = hget(value_hash, :value)
|
|
381
378
|
return [min, max] unless raw.is_a?(Hash)
|
|
@@ -397,10 +394,8 @@ module Quonfig
|
|
|
397
394
|
rescue ArgumentError, TypeError
|
|
398
395
|
# not a date; try integer
|
|
399
396
|
end
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
else
|
|
403
|
-
nil
|
|
397
|
+
Integer(val, exception: false)
|
|
398
|
+
|
|
404
399
|
end
|
|
405
400
|
end
|
|
406
401
|
end
|
|
@@ -437,7 +432,7 @@ module Quonfig
|
|
|
437
432
|
# of a config with no targeting rules matched, otherwise TARGETING_MATCH.
|
|
438
433
|
def wire_reason
|
|
439
434
|
return REASON_SPLIT unless @weighted_value_index.nil?
|
|
440
|
-
return REASON_STATIC if @rule_index
|
|
435
|
+
return REASON_STATIC if @rule_index.zero? && !EvalResult.send(:targeting_rules?, @config)
|
|
441
436
|
|
|
442
437
|
REASON_TARGETING_MATCH
|
|
443
438
|
end
|
|
@@ -494,13 +489,15 @@ module Quonfig
|
|
|
494
489
|
def unwrapped_value
|
|
495
490
|
raw = raw_value
|
|
496
491
|
case type
|
|
497
|
-
when 'bool'
|
|
492
|
+
when 'bool' then !!raw
|
|
498
493
|
when 'int'
|
|
499
494
|
return raw if raw.is_a?(Integer)
|
|
500
495
|
return raw.to_i if raw.is_a?(Numeric)
|
|
496
|
+
|
|
501
497
|
Integer(raw.to_s, 10)
|
|
502
498
|
when 'double'
|
|
503
499
|
return raw.to_f if raw.is_a?(Numeric)
|
|
500
|
+
|
|
504
501
|
Float(raw.to_s)
|
|
505
502
|
when 'string' then raw.to_s
|
|
506
503
|
when 'string_list' then raw.is_a?(Array) ? raw.map(&:to_s) : []
|
|
@@ -535,8 +532,8 @@ module Quonfig
|
|
|
535
532
|
(seconds * 1000).round
|
|
536
533
|
when Hash
|
|
537
534
|
secs = (raw['seconds'] || raw[:seconds] || 0).to_f
|
|
538
|
-
nanos = (raw['nanos']
|
|
539
|
-
(secs * 1000 + nanos / 1_000_000.0).round
|
|
535
|
+
nanos = (raw['nanos'] || raw[:nanos] || 0).to_f
|
|
536
|
+
((secs * 1000) + (nanos / 1_000_000.0)).round
|
|
540
537
|
else
|
|
541
538
|
raw
|
|
542
539
|
end
|
|
@@ -18,9 +18,7 @@ module Quonfig
|
|
|
18
18
|
@sdk_key = sdk_key
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
@uri
|
|
23
|
-
end
|
|
21
|
+
attr_reader :uri
|
|
24
22
|
|
|
25
23
|
def get(path, headers = {})
|
|
26
24
|
connection(headers).get(path)
|
|
@@ -40,7 +38,7 @@ module Quonfig
|
|
|
40
38
|
private
|
|
41
39
|
|
|
42
40
|
def auth_header
|
|
43
|
-
|
|
41
|
+
"Basic #{Base64.strict_encode64("1:#{@sdk_key}")}"
|
|
44
42
|
end
|
|
45
43
|
end
|
|
46
44
|
end
|
|
@@ -4,11 +4,24 @@ module Quonfig
|
|
|
4
4
|
# Internal logger for the Quonfig SDK
|
|
5
5
|
# Uses SemanticLogger if available, falls back to stdlib Logger
|
|
6
6
|
class InternalLogger
|
|
7
|
-
|
|
7
|
+
# Optional, host-app-supplied logger. When set (typically via
|
|
8
|
+
# Quonfig::Client.new(logger:)), all InternalLogger instances route
|
|
9
|
+
# writes to it instead of their default backend. Must duck-type as a
|
|
10
|
+
# stdlib Logger (responds to debug/info/warn/error). Missing levels
|
|
11
|
+
# are silently dropped.
|
|
12
|
+
class << self
|
|
13
|
+
attr_accessor :user_logger
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def initialize(klass, logger: nil)
|
|
8
17
|
@klass = klass
|
|
9
18
|
@level_sym = nil # Track the symbol level for consistency
|
|
19
|
+
@injected_logger = logger
|
|
10
20
|
|
|
11
|
-
if
|
|
21
|
+
if @injected_logger
|
|
22
|
+
@logger = @injected_logger
|
|
23
|
+
@using_semantic = false
|
|
24
|
+
elsif defined?(SemanticLogger)
|
|
12
25
|
@logger = create_semantic_logger
|
|
13
26
|
@using_semantic = true
|
|
14
27
|
else
|
|
@@ -51,13 +64,13 @@ module Quonfig
|
|
|
51
64
|
else
|
|
52
65
|
# Return the symbol level we tracked, or map from Logger constant
|
|
53
66
|
@level_sym || case @logger.level
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
67
|
+
when Logger::DEBUG then :debug
|
|
68
|
+
when Logger::INFO then :info
|
|
69
|
+
when Logger::WARN then :warn
|
|
70
|
+
when Logger::ERROR then :error
|
|
71
|
+
when Logger::FATAL then :fatal
|
|
72
|
+
else :warn
|
|
73
|
+
end
|
|
61
74
|
end
|
|
62
75
|
end
|
|
63
76
|
|
|
@@ -69,14 +82,16 @@ module Quonfig
|
|
|
69
82
|
@level_sym = new_level
|
|
70
83
|
|
|
71
84
|
# Map symbol to Logger constant
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
85
|
+
next_level = case new_level
|
|
86
|
+
when :trace, :debug then Logger::DEBUG
|
|
87
|
+
when :info then Logger::INFO
|
|
88
|
+
when :warn then Logger::WARN
|
|
89
|
+
when :error then Logger::ERROR
|
|
90
|
+
when :fatal then Logger::FATAL
|
|
91
|
+
else Logger::WARN
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
@logger.level = next_level if @logger.respond_to?(:level=)
|
|
80
95
|
end
|
|
81
96
|
end
|
|
82
97
|
|
|
@@ -99,9 +114,10 @@ module Quonfig
|
|
|
99
114
|
class << logger
|
|
100
115
|
def log(log, message = nil, progname = nil, &block)
|
|
101
116
|
return if recurse_check[local_log_id]
|
|
117
|
+
|
|
102
118
|
recurse_check[local_log_id] = true
|
|
103
119
|
begin
|
|
104
|
-
super
|
|
120
|
+
super
|
|
105
121
|
ensure
|
|
106
122
|
recurse_check[local_log_id] = false
|
|
107
123
|
end
|
|
@@ -132,19 +148,19 @@ module Quonfig
|
|
|
132
148
|
@level_sym = env_log_level || default_level_sym
|
|
133
149
|
|
|
134
150
|
logger.level = case @level_sym
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
151
|
+
when :trace, :debug then Logger::DEBUG
|
|
152
|
+
when :info then Logger::INFO
|
|
153
|
+
when :warn then Logger::WARN
|
|
154
|
+
when :error then Logger::ERROR
|
|
155
|
+
when :fatal then Logger::FATAL
|
|
156
|
+
else Logger::WARN
|
|
157
|
+
end
|
|
142
158
|
logger.progname = @klass.to_s
|
|
143
159
|
|
|
144
160
|
# Use a custom formatter that mimics SemanticLogger format
|
|
145
161
|
# SemanticLogger format: "ClassName -- Message"
|
|
146
162
|
# This helps tests that expect SemanticLogger-style output
|
|
147
|
-
logger.formatter = proc do |
|
|
163
|
+
logger.formatter = proc do |_severity, _datetime, progname, msg|
|
|
148
164
|
"#{progname} -- #{msg}\n"
|
|
149
165
|
end
|
|
150
166
|
|
|
@@ -152,20 +168,40 @@ module Quonfig
|
|
|
152
168
|
end
|
|
153
169
|
|
|
154
170
|
def env_log_level
|
|
155
|
-
level_str = ENV
|
|
171
|
+
level_str = ENV.fetch('QUONFIG_LOG_CLIENT_BOOTSTRAP_LOG_LEVEL', nil)
|
|
156
172
|
level_str&.downcase&.to_sym
|
|
157
173
|
end
|
|
158
174
|
|
|
159
175
|
def log_message(level, message, &block)
|
|
176
|
+
override = Quonfig::InternalLogger.user_logger
|
|
177
|
+
if override
|
|
178
|
+
write_to_user_logger(override, level, message, &block)
|
|
179
|
+
return
|
|
180
|
+
end
|
|
181
|
+
|
|
160
182
|
if @using_semantic
|
|
161
183
|
@logger.send(level, message, &block)
|
|
162
184
|
else
|
|
163
185
|
# stdlib Logger doesn't have trace
|
|
164
186
|
level = :debug if level == :trace
|
|
187
|
+
return unless @logger.respond_to?(level)
|
|
188
|
+
|
|
165
189
|
@logger.send(level, message || block&.call)
|
|
166
190
|
end
|
|
167
191
|
end
|
|
168
192
|
|
|
193
|
+
# Route a message to a host-app-supplied logger that duck-types as a
|
|
194
|
+
# stdlib Logger. Missing levels degrade gracefully (trace -> debug;
|
|
195
|
+
# otherwise a no-op). The class name is prepended to keep parity with
|
|
196
|
+
# the SemanticLogger / stdlib formatter output.
|
|
197
|
+
def write_to_user_logger(target, level, message, &block)
|
|
198
|
+
level = :debug if level == :trace && !target.respond_to?(:trace)
|
|
199
|
+
return unless target.respond_to?(level)
|
|
200
|
+
|
|
201
|
+
msg = message || block&.call
|
|
202
|
+
target.public_send(level, "#{@klass} -- #{msg}")
|
|
203
|
+
end
|
|
204
|
+
|
|
169
205
|
def instances
|
|
170
206
|
@@instances ||= []
|
|
171
207
|
end
|
data/lib/quonfig/murmer3.rb
CHANGED
|
@@ -30,10 +30,10 @@ class Murmur3
|
|
|
30
30
|
numbers = str.unpack('V*C*')
|
|
31
31
|
tailn = str.length % 4
|
|
32
32
|
tail = numbers.slice!(numbers.size - tailn, tailn)
|
|
33
|
-
|
|
33
|
+
numbers.each do |k1|
|
|
34
34
|
h1 ^= murmur3_32__mmix(k1)
|
|
35
35
|
h1 = murmur3_32_rotl(h1, 13)
|
|
36
|
-
h1 = (h1 * 5 + 0xe6546b64) & MASK32
|
|
36
|
+
h1 = ((h1 * 5) + 0xe6546b64) & MASK32
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
unless tail.empty?
|
data/lib/quonfig/options.rb
CHANGED
|
@@ -5,23 +5,8 @@ require 'uri'
|
|
|
5
5
|
module Quonfig
|
|
6
6
|
# Options passed to Quonfig::Client at construction time.
|
|
7
7
|
class Options
|
|
8
|
-
attr_reader :sdk_key
|
|
9
|
-
|
|
10
|
-
attr_reader :api_urls
|
|
11
|
-
attr_reader :sse_api_urls
|
|
12
|
-
attr_reader :telemetry_destination
|
|
13
|
-
attr_reader :config_api_urls
|
|
14
|
-
attr_reader :on_no_default
|
|
15
|
-
attr_reader :initialization_timeout_sec
|
|
16
|
-
attr_reader :on_init_failure
|
|
17
|
-
attr_reader :collect_sync_interval
|
|
18
|
-
attr_reader :datadir
|
|
19
|
-
attr_reader :enable_sse
|
|
20
|
-
attr_reader :enable_polling
|
|
21
|
-
attr_reader :poll_interval
|
|
22
|
-
attr_reader :global_context
|
|
23
|
-
attr_reader :logger_key
|
|
24
|
-
attr_reader :enable_quonfig_user_context
|
|
8
|
+
attr_reader :sdk_key, :environment, :api_urls, :sse_api_urls, :telemetry_destination, :config_api_urls,
|
|
9
|
+
:on_no_default, :initialization_timeout_sec, :on_init_failure, :collect_sync_interval, :datadir, :enable_sse, :enable_polling, :poll_interval, :global_context, :logger_key, :logger, :enable_quonfig_user_context
|
|
25
10
|
attr_accessor :is_fork
|
|
26
11
|
|
|
27
12
|
module ON_INITIALIZATION_FAILURE
|
|
@@ -46,13 +31,13 @@ module Quonfig
|
|
|
46
31
|
# and no explicit api_urls are provided). Mirrors derive_api_urls(DEFAULT_DOMAIN).
|
|
47
32
|
DEFAULT_API_URLS = [
|
|
48
33
|
'https://primary.quonfig.com',
|
|
49
|
-
'https://secondary.quonfig.com'
|
|
34
|
+
'https://secondary.quonfig.com'
|
|
50
35
|
].freeze
|
|
51
36
|
|
|
52
37
|
# Resolve the active domain. Reads QUONFIG_DOMAIN; falls back to
|
|
53
38
|
# DEFAULT_DOMAIN. Mirrors `cli/src/util/domain-urls.ts#getDomain`.
|
|
54
39
|
def self.domain
|
|
55
|
-
env = ENV
|
|
40
|
+
env = ENV.fetch('QUONFIG_DOMAIN', nil)
|
|
56
41
|
env && !env.empty? ? env : DEFAULT_DOMAIN
|
|
57
42
|
end
|
|
58
43
|
|
|
@@ -62,7 +47,7 @@ module Quonfig
|
|
|
62
47
|
def self.derive_api_urls(domain)
|
|
63
48
|
[
|
|
64
49
|
"https://primary.#{domain}",
|
|
65
|
-
"https://secondary.#{domain}"
|
|
50
|
+
"https://secondary.#{domain}"
|
|
66
51
|
]
|
|
67
52
|
end
|
|
68
53
|
|
|
@@ -84,12 +69,62 @@ module Quonfig
|
|
|
84
69
|
uri.to_s
|
|
85
70
|
end
|
|
86
71
|
|
|
87
|
-
|
|
72
|
+
def initialize(options = {})
|
|
73
|
+
init(**options)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# In datadir mode the SDK evaluates config from a local workspace and does
|
|
77
|
+
# not connect to the delivery service.
|
|
78
|
+
def local_only?
|
|
79
|
+
!@datadir.nil?
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def datadir?
|
|
83
|
+
!@datadir.nil?
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def collect_max_paths
|
|
87
|
+
return 0 unless telemetry_allowed?(true)
|
|
88
|
+
|
|
89
|
+
@collect_max_paths
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def collect_max_shapes
|
|
93
|
+
return 0 unless telemetry_allowed?(@collect_shapes)
|
|
94
|
+
|
|
95
|
+
@collect_max_shapes
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def collect_max_example_contexts
|
|
99
|
+
return 0 unless telemetry_allowed?(@collect_example_contexts)
|
|
100
|
+
|
|
101
|
+
@collect_max_example_contexts
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def collect_max_evaluation_summaries
|
|
105
|
+
return 0 unless telemetry_allowed?(@collect_evaluation_summaries)
|
|
106
|
+
|
|
107
|
+
@collect_max_evaluation_summaries
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def sdk_key_id
|
|
111
|
+
@sdk_key&.split('-')&.first
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def for_fork
|
|
115
|
+
clone = self.clone
|
|
116
|
+
clone.is_fork = true
|
|
117
|
+
clone
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
def init(
|
|
88
123
|
api_urls: nil,
|
|
89
124
|
telemetry_url: nil,
|
|
90
|
-
sdk_key: ENV
|
|
91
|
-
environment: ENV
|
|
92
|
-
datadir: ENV
|
|
125
|
+
sdk_key: ENV.fetch('QUONFIG_BACKEND_SDK_KEY', nil),
|
|
126
|
+
environment: ENV.fetch('QUONFIG_ENVIRONMENT', nil),
|
|
127
|
+
datadir: ENV.fetch('QUONFIG_DIR', nil),
|
|
93
128
|
enable_sse: true,
|
|
94
129
|
enable_polling: true,
|
|
95
130
|
poll_interval: 60,
|
|
@@ -105,6 +140,7 @@ module Quonfig
|
|
|
105
140
|
allow_telemetry_in_local_mode: false,
|
|
106
141
|
global_context: {},
|
|
107
142
|
logger_key: nil,
|
|
143
|
+
logger: nil,
|
|
108
144
|
enable_quonfig_user_context: false
|
|
109
145
|
)
|
|
110
146
|
@sdk_key = sdk_key
|
|
@@ -125,6 +161,7 @@ module Quonfig
|
|
|
125
161
|
@is_fork = false
|
|
126
162
|
@global_context = global_context
|
|
127
163
|
@logger_key = logger_key
|
|
164
|
+
@logger = logger
|
|
128
165
|
@enable_quonfig_user_context = enable_quonfig_user_context
|
|
129
166
|
|
|
130
167
|
# defaults that may be overridden by context_upload_mode
|
|
@@ -140,7 +177,7 @@ module Quonfig
|
|
|
140
177
|
domain = Quonfig::Options.domain
|
|
141
178
|
|
|
142
179
|
@api_urls = Array(api_urls || Quonfig::Options.derive_api_urls(domain))
|
|
143
|
-
|
|
180
|
+
.map { |url| remove_trailing_slash(url) }
|
|
144
181
|
|
|
145
182
|
@sse_api_urls = @api_urls.map { |url| Quonfig::Options.derive_stream_url(url) }
|
|
146
183
|
@config_api_urls = @api_urls
|
|
@@ -163,56 +200,6 @@ module Quonfig
|
|
|
163
200
|
end
|
|
164
201
|
end
|
|
165
202
|
|
|
166
|
-
def initialize(options = {})
|
|
167
|
-
init(**options)
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
# In datadir mode the SDK evaluates config from a local workspace and does
|
|
171
|
-
# not connect to the delivery service.
|
|
172
|
-
def local_only?
|
|
173
|
-
!@datadir.nil?
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
def datadir?
|
|
177
|
-
!@datadir.nil?
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
def collect_max_paths
|
|
181
|
-
return 0 unless telemetry_allowed?(true)
|
|
182
|
-
|
|
183
|
-
@collect_max_paths
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
def collect_max_shapes
|
|
187
|
-
return 0 unless telemetry_allowed?(@collect_shapes)
|
|
188
|
-
|
|
189
|
-
@collect_max_shapes
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
def collect_max_example_contexts
|
|
193
|
-
return 0 unless telemetry_allowed?(@collect_example_contexts)
|
|
194
|
-
|
|
195
|
-
@collect_max_example_contexts
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
def collect_max_evaluation_summaries
|
|
199
|
-
return 0 unless telemetry_allowed?(@collect_evaluation_summaries)
|
|
200
|
-
|
|
201
|
-
@collect_max_evaluation_summaries
|
|
202
|
-
end
|
|
203
|
-
|
|
204
|
-
def sdk_key_id
|
|
205
|
-
@sdk_key&.split('-')&.first
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
def for_fork
|
|
209
|
-
clone = self.clone
|
|
210
|
-
clone.is_fork = true
|
|
211
|
-
clone
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
private
|
|
215
|
-
|
|
216
203
|
def telemetry_allowed?(option)
|
|
217
204
|
option && (!local_only? || @allow_telemetry_in_local_mode)
|
|
218
205
|
end
|
data/lib/quonfig/quonfig.rb
CHANGED
|
@@ -51,8 +51,8 @@ module Quonfig
|
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
def self.ensure_initialized(key = nil)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
return unless !defined?(@singleton) || @singleton.nil?
|
|
55
|
+
|
|
56
|
+
raise Quonfig::Errors::UninitializedError, key
|
|
57
57
|
end
|
|
58
58
|
end
|
data/lib/quonfig/reason.rb
CHANGED
|
@@ -20,9 +20,10 @@ module Quonfig
|
|
|
20
20
|
module_function
|
|
21
21
|
|
|
22
22
|
def compute(config:, conditional_value:, weighted_value_index: nil)
|
|
23
|
-
return SPLIT if weighted_value_index
|
|
23
|
+
return SPLIT if weighted_value_index&.positive?
|
|
24
24
|
return RULE_MATCH if targeting_rules?(config)
|
|
25
25
|
return RULE_MATCH if non_always_true_criteria?(conditional_value)
|
|
26
|
+
|
|
26
27
|
DEFAULT
|
|
27
28
|
end
|
|
28
29
|
|