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.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +30 -0
  3. data/README.md +94 -0
  4. data/lib/quonfig/caching_http_connection.rb +3 -3
  5. data/lib/quonfig/client.rb +22 -27
  6. data/lib/quonfig/config_loader.rb +5 -1
  7. data/lib/quonfig/config_store.rb +10 -6
  8. data/lib/quonfig/context.rb +5 -4
  9. data/lib/quonfig/datadir.rb +4 -10
  10. data/lib/quonfig/dev_context.rb +4 -2
  11. data/lib/quonfig/duration.rb +2 -2
  12. data/lib/quonfig/encryption.rb +12 -16
  13. data/lib/quonfig/errors/invalid_environment_error.rb +1 -3
  14. data/lib/quonfig/errors/invalid_sdk_key_error.rb +6 -7
  15. data/lib/quonfig/errors/missing_env_var_error.rb +0 -3
  16. data/lib/quonfig/errors/missing_environment_error.rb +1 -1
  17. data/lib/quonfig/errors/uninitialized_error.rb +1 -1
  18. data/lib/quonfig/evaluation.rb +11 -8
  19. data/lib/quonfig/evaluator.rb +34 -37
  20. data/lib/quonfig/fixed_size_hash.rb +1 -0
  21. data/lib/quonfig/http_connection.rb +2 -4
  22. data/lib/quonfig/internal_logger.rb +63 -27
  23. data/lib/quonfig/murmer3.rb +2 -2
  24. data/lib/quonfig/options.rb +62 -75
  25. data/lib/quonfig/periodic_sync.rb +1 -1
  26. data/lib/quonfig/quonfig.rb +3 -3
  27. data/lib/quonfig/reason.rb +2 -1
  28. data/lib/quonfig/resolver.rb +8 -9
  29. data/lib/quonfig/semantic_logger_filter.rb +4 -3
  30. data/lib/quonfig/semver.rb +6 -8
  31. data/lib/quonfig/sse_config_client.rb +14 -15
  32. data/lib/quonfig/stdlib_formatter.rb +3 -3
  33. data/lib/quonfig/telemetry/context_shape_aggregator.rb +2 -3
  34. data/lib/quonfig/telemetry/example_contexts_aggregator.rb +1 -1
  35. data/lib/quonfig/telemetry/telemetry_reporter.rb +1 -0
  36. data/lib/quonfig/time_helpers.rb +2 -0
  37. data/lib/quonfig/version.rb +5 -0
  38. data/lib/quonfig.rb +2 -1
  39. data/quonfig.gemspec +29 -165
  40. metadata +24 -193
  41. data/.claude/rules/constitution.md +0 -81
  42. data/.claude/rules/git-safety.md +0 -11
  43. data/.claude/rules/issue-tracking.md +0 -13
  44. data/.claude/rules/testing-workflow.md +0 -28
  45. data/.envrc.sample +0 -3
  46. data/.github/CODEOWNERS +0 -2
  47. data/.github/pull_request_template.md +0 -8
  48. data/.github/workflows/release.yml +0 -49
  49. data/.github/workflows/ruby.yml +0 -60
  50. data/.github/workflows/test.yaml +0 -40
  51. data/.rubocop.yml +0 -13
  52. data/.tool-versions +0 -1
  53. data/CLAUDE.md +0 -29
  54. data/CODEOWNERS +0 -1
  55. data/Gemfile +0 -26
  56. data/Gemfile.lock +0 -177
  57. data/Rakefile +0 -64
  58. data/VERSION +0 -1
  59. data/dev/allocation_stats +0 -60
  60. data/dev/benchmark +0 -40
  61. data/dev/console +0 -12
  62. data/dev/script_setup.rb +0 -18
  63. data/test/fixtures/datafile.json +0 -87
  64. data/test/integration/test_context_precedence.rb +0 -112
  65. data/test/integration/test_datadir_environment.rb +0 -54
  66. data/test/integration/test_dev_overrides.rb +0 -40
  67. data/test/integration/test_enabled.rb +0 -478
  68. data/test/integration/test_enabled_with_contexts.rb +0 -64
  69. data/test/integration/test_get.rb +0 -136
  70. data/test/integration/test_get_feature_flag.rb +0 -28
  71. data/test/integration/test_get_or_raise.rb +0 -60
  72. data/test/integration/test_get_weighted_values.rb +0 -34
  73. data/test/integration/test_helpers.rb +0 -667
  74. data/test/integration/test_helpers_test.rb +0 -73
  75. data/test/integration/test_post.rb +0 -44
  76. data/test/integration/test_telemetry.rb +0 -170
  77. data/test/support/common_helpers.rb +0 -106
  78. data/test/support/mock_base_client.rb +0 -27
  79. data/test/support/mock_config_loader.rb +0 -1
  80. data/test/test_bound_client.rb +0 -109
  81. data/test/test_caching_http_connection.rb +0 -218
  82. data/test/test_client.rb +0 -255
  83. data/test/test_client_network_mode.rb +0 -136
  84. data/test/test_client_telemetry.rb +0 -175
  85. data/test/test_config_loader.rb +0 -70
  86. data/test/test_context.rb +0 -139
  87. data/test/test_context_shape.rb +0 -37
  88. data/test/test_context_shape_aggregator.rb +0 -126
  89. data/test/test_datadir.rb +0 -203
  90. data/test/test_details_getters.rb +0 -242
  91. data/test/test_dev_context.rb +0 -163
  92. data/test/test_duration.rb +0 -37
  93. data/test/test_encryption.rb +0 -16
  94. data/test/test_evaluation_summaries_aggregator.rb +0 -180
  95. data/test/test_evaluator.rb +0 -285
  96. data/test/test_example_contexts_aggregator.rb +0 -119
  97. data/test/test_exponential_backoff.rb +0 -44
  98. data/test/test_fixed_size_hash.rb +0 -119
  99. data/test/test_helper.rb +0 -17
  100. data/test/test_http_connection.rb +0 -81
  101. data/test/test_internal_logger.rb +0 -34
  102. data/test/test_options.rb +0 -198
  103. data/test/test_rate_limit_cache.rb +0 -44
  104. data/test/test_reason.rb +0 -79
  105. data/test/test_rename.rb +0 -65
  106. data/test/test_resolver.rb +0 -291
  107. data/test/test_semantic_logger_filter.rb +0 -144
  108. data/test/test_semver.rb +0 -108
  109. data/test/test_should_log.rb +0 -186
  110. data/test/test_sse_config_client.rb +0 -297
  111. data/test/test_stdlib_formatter.rb +0 -195
  112. data/test/test_telemetry_reporter.rb +0 -209
  113. data/test/test_typed_getters.rb +0 -131
  114. data/test/test_types.rb +0 -141
  115. data/test/test_weighted_value_resolver.rb +0 -84
@@ -16,13 +16,15 @@ module Quonfig
16
16
  end
17
17
 
18
18
  def reason
19
- @reason ||= @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
- Quonfig::Reason::UNKNOWN
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
@@ -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, config)
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
- return false
161
+ false
160
162
 
161
163
  when OP_ALWAYS_TRUE
162
- return true
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
- return operator == OP_PROP_IS_NOT_ONE_OF
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
- return operator == OP_PROP_DOES_NOT_START_WITH_ONE_OF
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
- return operator == OP_PROP_DOES_NOT_END_WITH_ONE_OF
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
- return operator == OP_PROP_DOES_NOT_CONTAIN_ONE_OF
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
- return false
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
- return false
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
- return false
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 > 0
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 < 0
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
- return false
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
- return false
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 < 0
274
- when OP_PROP_SEMVER_EQUAL then return cmp == 0
275
- when OP_PROP_SEMVER_GREATER_THAN then return cmp > 0
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
- return false
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
- return operator == OP_NOT_IN_SEG
291
+ operator == OP_NOT_IN_SEG
290
292
 
291
293
  else
292
- return false
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
- f = Float(v, exception: false)
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 # approx Number.MIN_SAFE_INTEGER
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
- n = Integer(val, exception: false)
401
- n
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 == 0 && !EvalResult.send(:targeting_rules?, @config)
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' then !!raw
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'] || raw[:nanos] || 0).to_f
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
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Quonfig
3
4
  class FixedSizeHash < Hash
4
5
  def initialize(max_size)
@@ -18,9 +18,7 @@ module Quonfig
18
18
  @sdk_key = sdk_key
19
19
  end
20
20
 
21
- def uri
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
- 'Basic ' + Base64.strict_encode64("1:#{@sdk_key}")
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
- def initialize(klass)
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 defined?(SemanticLogger)
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
- when Logger::DEBUG then :debug
55
- when Logger::INFO then :info
56
- when Logger::WARN then :warn
57
- when Logger::ERROR then :error
58
- when Logger::FATAL then :fatal
59
- else :warn
60
- end
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
- @logger.level = case new_level
73
- when :trace, :debug then Logger::DEBUG
74
- when :info then Logger::INFO
75
- when :warn then Logger::WARN
76
- when :error then Logger::ERROR
77
- when :fatal then Logger::FATAL
78
- else Logger::WARN
79
- end
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(log, message, progname, &block)
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
- when :trace, :debug then Logger::DEBUG
136
- when :info then Logger::INFO
137
- when :warn then Logger::WARN
138
- when :error then Logger::ERROR
139
- when :fatal then Logger::FATAL
140
- else Logger::WARN
141
- end
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 |severity, datetime, progname, msg|
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['QUONFIG_LOG_CLIENT_BOOTSTRAP_LOG_LEVEL']
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
@@ -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
- for k1 in numbers
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?
@@ -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
- attr_reader :environment
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['QUONFIG_DOMAIN']
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
- private def init(
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['QUONFIG_BACKEND_SDK_KEY'],
91
- environment: ENV['QUONFIG_ENVIRONMENT'],
92
- datadir: ENV['QUONFIG_DIR'],
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
- .map { |url| remove_trailing_slash(url) }
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
@@ -5,7 +5,7 @@ module Quonfig
5
5
  LOG = Quonfig::InternalLogger.new(self)
6
6
 
7
7
  def sync
8
- return if @data.size.zero?
8
+ return if @data.empty?
9
9
 
10
10
  LOG.debug "Syncing #{@data.size} items"
11
11
 
@@ -51,8 +51,8 @@ module Quonfig
51
51
  end
52
52
 
53
53
  def self.ensure_initialized(key = nil)
54
- if !defined?(@singleton) || @singleton.nil?
55
- raise Quonfig::Errors::UninitializedError.new(key)
56
- end
54
+ return unless !defined?(@singleton) || @singleton.nil?
55
+
56
+ raise Quonfig::Errors::UninitializedError, key
57
57
  end
58
58
  end
@@ -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 && weighted_value_index.positive?
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