quonfig 0.0.11 → 0.0.13
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 +8 -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 +47 -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 +13 -14
- 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 +1 -1
- data/quonfig.gemspec +6 -7
- metadata +2 -16
data/lib/quonfig/evaluator.rb
CHANGED
|
@@ -51,6 +51,8 @@ module Quonfig
|
|
|
51
51
|
OP_PROP_SEMVER_GREATER_THAN = 'PROP_SEMVER_GREATER_THAN'
|
|
52
52
|
OP_IN_SEG = 'IN_SEG'
|
|
53
53
|
OP_NOT_IN_SEG = 'NOT_IN_SEG'
|
|
54
|
+
OP_IS_PRESENT = 'IS_PRESENT'
|
|
55
|
+
OP_IS_NOT_PRESENT = 'IS_NOT_PRESENT'
|
|
54
56
|
|
|
55
57
|
MAGIC_CURRENT_TIME_PROPS = %w[quonfig.current-time prefab.current-time reforge.current-time].freeze
|
|
56
58
|
|
|
@@ -99,8 +101,10 @@ module Quonfig
|
|
|
99
101
|
|
|
100
102
|
keys.each do |k|
|
|
101
103
|
return hash[k] if hash.key?(k)
|
|
104
|
+
|
|
102
105
|
sk = k.to_s
|
|
103
106
|
return hash[sk] if hash.key?(sk)
|
|
107
|
+
|
|
104
108
|
sym = k.to_sym
|
|
105
109
|
return hash[sym] if hash.key?(sym)
|
|
106
110
|
end
|
|
@@ -147,7 +151,7 @@ module Quonfig
|
|
|
147
151
|
# Faithful port of sdk-node/src/operators.ts evaluateCriterion. Matches
|
|
148
152
|
# context-exists / missing-context semantics (e.g. PROP_IS_NOT_ONE_OF is
|
|
149
153
|
# true when context is missing).
|
|
150
|
-
def evaluate_criterion(criterion, context,
|
|
154
|
+
def evaluate_criterion(criterion, context, _config)
|
|
151
155
|
property_name = hget(criterion, :propertyName) || ''
|
|
152
156
|
operator = hget(criterion, :operator)
|
|
153
157
|
match_value = hget(criterion, :valueToMatch)
|
|
@@ -156,10 +160,10 @@ module Quonfig
|
|
|
156
160
|
|
|
157
161
|
case operator
|
|
158
162
|
when OP_NOT_SET, nil
|
|
159
|
-
|
|
163
|
+
false
|
|
160
164
|
|
|
161
165
|
when OP_ALWAYS_TRUE
|
|
162
|
-
|
|
166
|
+
true
|
|
163
167
|
|
|
164
168
|
when OP_PROP_IS_ONE_OF, OP_PROP_IS_NOT_ONE_OF
|
|
165
169
|
if context_exists && match_value
|
|
@@ -170,7 +174,7 @@ module Quonfig
|
|
|
170
174
|
return match_found == (operator == OP_PROP_IS_ONE_OF)
|
|
171
175
|
end
|
|
172
176
|
end
|
|
173
|
-
|
|
177
|
+
operator == OP_PROP_IS_NOT_ONE_OF
|
|
174
178
|
|
|
175
179
|
when OP_PROP_STARTS_WITH_ONE_OF, OP_PROP_DOES_NOT_START_WITH_ONE_OF
|
|
176
180
|
if context_exists && match_value
|
|
@@ -181,7 +185,7 @@ module Quonfig
|
|
|
181
185
|
return match_found == (operator == OP_PROP_STARTS_WITH_ONE_OF)
|
|
182
186
|
end
|
|
183
187
|
end
|
|
184
|
-
|
|
188
|
+
operator == OP_PROP_DOES_NOT_START_WITH_ONE_OF
|
|
185
189
|
|
|
186
190
|
when OP_PROP_ENDS_WITH_ONE_OF, OP_PROP_DOES_NOT_END_WITH_ONE_OF
|
|
187
191
|
if context_exists && match_value
|
|
@@ -192,7 +196,7 @@ module Quonfig
|
|
|
192
196
|
return match_found == (operator == OP_PROP_ENDS_WITH_ONE_OF)
|
|
193
197
|
end
|
|
194
198
|
end
|
|
195
|
-
|
|
199
|
+
operator == OP_PROP_DOES_NOT_END_WITH_ONE_OF
|
|
196
200
|
|
|
197
201
|
when OP_PROP_CONTAINS_ONE_OF, OP_PROP_DOES_NOT_CONTAIN_ONE_OF
|
|
198
202
|
if context_exists && match_value
|
|
@@ -203,7 +207,7 @@ module Quonfig
|
|
|
203
207
|
return match_found == (operator == OP_PROP_CONTAINS_ONE_OF)
|
|
204
208
|
end
|
|
205
209
|
end
|
|
206
|
-
|
|
210
|
+
operator == OP_PROP_DOES_NOT_CONTAIN_ONE_OF
|
|
207
211
|
|
|
208
212
|
when OP_PROP_MATCHES, OP_PROP_DOES_NOT_MATCH
|
|
209
213
|
mv = hget(match_value, :value)
|
|
@@ -216,7 +220,7 @@ module Quonfig
|
|
|
216
220
|
return false
|
|
217
221
|
end
|
|
218
222
|
end
|
|
219
|
-
|
|
223
|
+
false
|
|
220
224
|
|
|
221
225
|
when OP_HIERARCHICAL_MATCH
|
|
222
226
|
if context_exists && match_value
|
|
@@ -224,7 +228,7 @@ module Quonfig
|
|
|
224
228
|
mv = to_s_nil(hget(match_value, :value))
|
|
225
229
|
return cv.start_with?(mv)
|
|
226
230
|
end
|
|
227
|
-
|
|
231
|
+
false
|
|
228
232
|
|
|
229
233
|
when OP_IN_INT_RANGE
|
|
230
234
|
if context_exists && match_value
|
|
@@ -232,7 +236,7 @@ module Quonfig
|
|
|
232
236
|
num_val = to_float(context_value)
|
|
233
237
|
return num_val >= start_v && num_val < end_v unless num_val.nil?
|
|
234
238
|
end
|
|
235
|
-
|
|
239
|
+
false
|
|
236
240
|
|
|
237
241
|
when OP_PROP_GREATER_THAN, OP_PROP_GREATER_THAN_OR_EQUAL,
|
|
238
242
|
OP_PROP_LESS_THAN, OP_PROP_LESS_THAN_OR_EQUAL
|
|
@@ -244,13 +248,13 @@ module Quonfig
|
|
|
244
248
|
return false if cmp.nil?
|
|
245
249
|
|
|
246
250
|
case operator
|
|
247
|
-
when OP_PROP_GREATER_THAN then return cmp
|
|
251
|
+
when OP_PROP_GREATER_THAN then return cmp.positive?
|
|
248
252
|
when OP_PROP_GREATER_THAN_OR_EQUAL then return cmp >= 0
|
|
249
|
-
when OP_PROP_LESS_THAN then return cmp
|
|
253
|
+
when OP_PROP_LESS_THAN then return cmp.negative?
|
|
250
254
|
when OP_PROP_LESS_THAN_OR_EQUAL then return cmp <= 0
|
|
251
255
|
end
|
|
252
256
|
end
|
|
253
|
-
|
|
257
|
+
false
|
|
254
258
|
|
|
255
259
|
when OP_PROP_BEFORE, OP_PROP_AFTER
|
|
256
260
|
if context_exists && match_value
|
|
@@ -260,7 +264,7 @@ module Quonfig
|
|
|
260
264
|
return operator == OP_PROP_BEFORE ? context_millis < match_millis : context_millis > match_millis
|
|
261
265
|
end
|
|
262
266
|
end
|
|
263
|
-
|
|
267
|
+
false
|
|
264
268
|
|
|
265
269
|
when OP_PROP_SEMVER_LESS_THAN, OP_PROP_SEMVER_EQUAL, OP_PROP_SEMVER_GREATER_THAN
|
|
266
270
|
mv = hget(match_value, :value)
|
|
@@ -270,13 +274,24 @@ module Quonfig
|
|
|
270
274
|
if sv_ctx && sv_mv
|
|
271
275
|
cmp = (sv_ctx <=> sv_mv)
|
|
272
276
|
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
|
|
277
|
+
when OP_PROP_SEMVER_LESS_THAN then return cmp.negative?
|
|
278
|
+
when OP_PROP_SEMVER_EQUAL then return cmp.zero?
|
|
279
|
+
when OP_PROP_SEMVER_GREATER_THAN then return cmp.positive?
|
|
276
280
|
end
|
|
277
281
|
end
|
|
278
282
|
end
|
|
279
|
-
|
|
283
|
+
false
|
|
284
|
+
|
|
285
|
+
when OP_IS_PRESENT, OP_IS_NOT_PRESENT
|
|
286
|
+
# Presence is type-agnostic: empty string "", 0, and false are all
|
|
287
|
+
# PRESENT. Only nil / missing key (incl. nested) is NOT present.
|
|
288
|
+
# `lookup_context` already returns context_exists=true iff the
|
|
289
|
+
# resolved value is non-nil — exactly the semantic we need. Do not
|
|
290
|
+
# use ActiveSupport's #present? / #blank? here: they treat "" and
|
|
291
|
+
# false as blank, which is the wrong behaviour for this operator.
|
|
292
|
+
return context_exists if operator == OP_IS_PRESENT
|
|
293
|
+
|
|
294
|
+
!context_exists
|
|
280
295
|
|
|
281
296
|
when OP_IN_SEG, OP_NOT_IN_SEG
|
|
282
297
|
if match_value
|
|
@@ -286,21 +301,17 @@ module Quonfig
|
|
|
286
301
|
|
|
287
302
|
return result == (operator == OP_IN_SEG)
|
|
288
303
|
end
|
|
289
|
-
|
|
304
|
+
operator == OP_NOT_IN_SEG
|
|
290
305
|
|
|
291
306
|
else
|
|
292
|
-
|
|
307
|
+
false
|
|
293
308
|
end
|
|
294
309
|
end
|
|
295
310
|
|
|
296
311
|
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
|
|
312
|
+
return [(Time.now.utc.to_f * 1000).to_i, true] if MAGIC_CURRENT_TIME_PROPS.include?(property_name)
|
|
300
313
|
|
|
301
|
-
if property_name.nil? || property_name.empty?
|
|
302
|
-
return [nil, false]
|
|
303
|
-
end
|
|
314
|
+
return [nil, false] if property_name.nil? || property_name.empty?
|
|
304
315
|
|
|
305
316
|
value = context.get(property_name)
|
|
306
317
|
[value, !value.nil?]
|
|
@@ -362,8 +373,7 @@ module Quonfig
|
|
|
362
373
|
return v.to_f if v.is_a?(Numeric)
|
|
363
374
|
return nil unless v.is_a?(String)
|
|
364
375
|
|
|
365
|
-
|
|
366
|
-
f
|
|
376
|
+
Float(v, exception: false)
|
|
367
377
|
end
|
|
368
378
|
|
|
369
379
|
def compare_numbers(a, b)
|
|
@@ -375,7 +385,7 @@ module Quonfig
|
|
|
375
385
|
end
|
|
376
386
|
|
|
377
387
|
def extract_int_range(value_hash)
|
|
378
|
-
min = -(2**53) + 1
|
|
388
|
+
min = -(2**53) + 1 # approx Number.MIN_SAFE_INTEGER
|
|
379
389
|
max = (2**53) - 1
|
|
380
390
|
raw = hget(value_hash, :value)
|
|
381
391
|
return [min, max] unless raw.is_a?(Hash)
|
|
@@ -397,10 +407,8 @@ module Quonfig
|
|
|
397
407
|
rescue ArgumentError, TypeError
|
|
398
408
|
# not a date; try integer
|
|
399
409
|
end
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
else
|
|
403
|
-
nil
|
|
410
|
+
Integer(val, exception: false)
|
|
411
|
+
|
|
404
412
|
end
|
|
405
413
|
end
|
|
406
414
|
end
|
|
@@ -437,7 +445,7 @@ module Quonfig
|
|
|
437
445
|
# of a config with no targeting rules matched, otherwise TARGETING_MATCH.
|
|
438
446
|
def wire_reason
|
|
439
447
|
return REASON_SPLIT unless @weighted_value_index.nil?
|
|
440
|
-
return REASON_STATIC if @rule_index
|
|
448
|
+
return REASON_STATIC if @rule_index.zero? && !EvalResult.send(:targeting_rules?, @config)
|
|
441
449
|
|
|
442
450
|
REASON_TARGETING_MATCH
|
|
443
451
|
end
|
|
@@ -494,13 +502,15 @@ module Quonfig
|
|
|
494
502
|
def unwrapped_value
|
|
495
503
|
raw = raw_value
|
|
496
504
|
case type
|
|
497
|
-
when 'bool'
|
|
505
|
+
when 'bool' then !!raw
|
|
498
506
|
when 'int'
|
|
499
507
|
return raw if raw.is_a?(Integer)
|
|
500
508
|
return raw.to_i if raw.is_a?(Numeric)
|
|
509
|
+
|
|
501
510
|
Integer(raw.to_s, 10)
|
|
502
511
|
when 'double'
|
|
503
512
|
return raw.to_f if raw.is_a?(Numeric)
|
|
513
|
+
|
|
504
514
|
Float(raw.to_s)
|
|
505
515
|
when 'string' then raw.to_s
|
|
506
516
|
when 'string_list' then raw.is_a?(Array) ? raw.map(&:to_s) : []
|
|
@@ -535,8 +545,8 @@ module Quonfig
|
|
|
535
545
|
(seconds * 1000).round
|
|
536
546
|
when Hash
|
|
537
547
|
secs = (raw['seconds'] || raw[:seconds] || 0).to_f
|
|
538
|
-
nanos = (raw['nanos']
|
|
539
|
-
(secs * 1000 + nanos / 1_000_000.0).round
|
|
548
|
+
nanos = (raw['nanos'] || raw[:nanos] || 0).to_f
|
|
549
|
+
((secs * 1000) + (nanos / 1_000_000.0)).round
|
|
540
550
|
else
|
|
541
551
|
raw
|
|
542
552
|
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
|
|