featurevisor 1.1.0 → 3.0.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.
@@ -11,7 +11,8 @@ module Featurevisor
11
11
  schemaVersion: "2",
12
12
  revision: "unknown",
13
13
  segments: {},
14
- features: {}
14
+ features: {},
15
+ variables: {}
15
16
  }.freeze
16
17
 
17
18
  # Initialize a new Featurevisor instance
@@ -19,26 +20,27 @@ module Featurevisor
19
20
  # @option options [Hash, String] :datafile Datafile content or JSON string
20
21
  # @option options [Hash] :context Initial context
21
22
  # @option options [String] :log_level Log level
22
- # @option options [Hash] :sticky Sticky features
23
+ # @option options [Hash] :sticky_features Sticky features
23
24
  # @option options [Array<Hash, FeaturevisorModule>] :modules Array of modules
24
25
  # @option options [Proc] :on_diagnostic Diagnostic handler
25
26
  def initialize(options = {})
26
27
  # from options
27
28
  @context = options[:context] || {}
28
- @logger = Logger.new(
29
+ @diagnostics = DiagnosticReporter.new(
29
30
  level: options[:log_level] || "info",
30
- handler: method(:handle_internal_log)
31
+ handler: method(:handle_evaluation_diagnostic)
31
32
  )
32
33
  @on_diagnostic = options[:on_diagnostic] || options[:onDiagnostic]
33
34
  @emitter = Featurevisor::Emitter.new
34
- @sticky = options[:sticky] || {}
35
+ @sticky_features = options[:sticky_features] || options[:stickyFeatures] || {}
36
+ @sticky_variables = options[:sticky_variables] || options[:stickyVariables] || {}
35
37
  @closed = false
36
38
  @module_diagnostic_subscriptions = []
37
39
 
38
40
  # datafile
39
- @datafile_reader = DatafileReader.new(
41
+ @datafile = InstanceEvaluationDataProvider.new(
40
42
  datafile: EMPTY_DATAFILE,
41
- logger: @logger
43
+ diagnostics: @diagnostics
42
44
  )
43
45
 
44
46
  @modules_manager = Featurevisor::Modules::ModulesManager.new(
@@ -62,21 +64,35 @@ module Featurevisor
62
64
  # Set the log level
63
65
  # @param level [String] Log level
64
66
  def set_log_level(level)
65
- @logger.set_level(level)
67
+ @diagnostics.set_level(level)
66
68
  end
67
69
 
68
- def handle_internal_log(level, message, details = nil)
70
+ def handle_evaluation_diagnostic(level, message, details = nil)
69
71
  details = (details || {}).dup
70
- code = details[:reason] || details["reason"] || message
72
+ code = details.delete(:code) || details.delete("code") || details[:reason] || details["reason"] || message
71
73
  code = "deprecated_feature" if message == "feature is deprecated"
72
74
  code = "deprecated_variable" if message == "variable is deprecated"
73
75
  code = "feature_not_found" if message == "feature not found"
74
76
  code = "variable_not_found" if message == "variable schema not found"
75
77
  code = "no_variations" if message == "no variations"
76
78
  code = "invalid_bucket_by" if message == "invalid bucketBy"
77
- report_diagnostic(level: level, code: code.to_s, message: message, details: details)
79
+ code = "evaluation_error" if message == "Error during evaluation"
80
+ code = "conditions_parse_error" if message == "Error parsing conditions"
81
+ original_error = details.delete(:error) || details.delete("error")
82
+ nested_details = details.delete(:details) || details.delete("details")
83
+ details.merge!(nested_details) if nested_details.is_a?(Hash)
84
+ if details.key?(:feature_key) && details.key?(:reason)
85
+ evaluation = details.dup
86
+ details = {
87
+ featureKey: evaluation[:feature_key],
88
+ variableKey: evaluation[:variable_key],
89
+ reason: evaluation[:reason],
90
+ evaluation: camelize_diagnostic_value(evaluation)
91
+ }
92
+ end
93
+ report_diagnostic(level: level, code: code.to_s, message: message, details: details, originalError: original_error)
78
94
  end
79
- private :handle_internal_log
95
+ private :handle_evaluation_diagnostic
80
96
 
81
97
  # Set the datafile
82
98
  # @param datafile [Hash, String] Datafile content or JSON string
@@ -94,17 +110,18 @@ module Featurevisor
94
110
  end
95
111
  unless parsed_datafile.is_a?(Hash) && parsed_datafile[:schemaVersion].is_a?(String) &&
96
112
  parsed_datafile[:revision].is_a?(String) && parsed_datafile[:segments].is_a?(Hash) &&
97
- parsed_datafile[:features].is_a?(Hash)
113
+ parsed_datafile[:features].is_a?(Hash) &&
114
+ (!parsed_datafile.key?(:variables) || parsed_datafile[:variables].is_a?(Hash))
98
115
  raise ArgumentError, "Invalid datafile"
99
116
  end
100
- next_datafile = replace ? parsed_datafile : merge_datafiles(@datafile_reader.get_datafile, parsed_datafile)
101
- new_datafile_reader = DatafileReader.new(
117
+ next_datafile = replace ? parsed_datafile : merge_datafiles(@datafile.get_datafile, parsed_datafile)
118
+ new_datafile = InstanceEvaluationDataProvider.new(
102
119
  datafile: next_datafile,
103
- logger: @logger
120
+ diagnostics: @diagnostics
104
121
  )
105
122
 
106
- details = Featurevisor::Events.get_params_for_datafile_set_event(@datafile_reader, new_datafile_reader, replace)
107
- @datafile_reader = new_datafile_reader
123
+ details = Featurevisor::Events.get_params_for_datafile_set_event(@datafile, new_datafile, replace)
124
+ @datafile = new_datafile
108
125
 
109
126
  report_diagnostic(
110
127
  level: "info",
@@ -126,60 +143,68 @@ module Featurevisor
126
143
  # Set sticky features
127
144
  # @param sticky [Hash] Sticky features
128
145
  # @param replace [Boolean] Whether to replace existing sticky features
129
- def set_sticky(sticky, replace = false)
130
- previous_sticky_features = @sticky || {}
146
+ def set_sticky_features(sticky, replace = false)
147
+ previous_sticky_features = @sticky_features || {}
131
148
 
132
149
  if replace
133
- @sticky = sticky
150
+ @sticky_features = sticky
134
151
  else
135
- @sticky = {
136
- **@sticky,
152
+ @sticky_features = {
153
+ **@sticky_features,
137
154
  **sticky
138
155
  }
139
156
  end
140
157
 
141
- params = Featurevisor::Events.get_params_for_sticky_set_event(previous_sticky_features, @sticky, replace)
158
+ params = Featurevisor::Events.get_params_for_sticky_features_set_event(previous_sticky_features, @sticky_features, replace)
142
159
 
143
160
  report_diagnostic(
144
161
  level: "info",
145
- code: "sticky_set",
162
+ code: "sticky_features_set",
146
163
  message: "Sticky features set",
147
164
  details: params
148
165
  )
149
- @emitter.trigger("sticky_set", params)
166
+ @emitter.trigger("sticky_features_set", params)
167
+ end
168
+
169
+ def set_sticky_variables(sticky, replace = false)
170
+ previous = @sticky_variables || {}
171
+ @sticky_variables = replace ? sticky : { **@sticky_variables, **sticky }
172
+ params = Featurevisor::Events.get_params_for_sticky_variables_set_event(previous, @sticky_variables, replace)
173
+ report_diagnostic(level: "info", code: "sticky_variables_set", message: "Sticky variables set", details: params)
174
+ @emitter.trigger("sticky_variables_set", params)
150
175
  end
151
176
 
152
177
  # Get the revision
153
178
  # @return [String] Revision string
154
179
  def get_revision
155
- @datafile_reader.get_revision
180
+ @datafile.get_revision
156
181
  end
157
182
 
158
183
  def get_schema_version
159
- @datafile_reader.get_schema_version
184
+ @datafile.get_schema_version
160
185
  end
161
186
 
162
187
  def get_segment(segment_key)
163
- @datafile_reader.get_segment(segment_key)
188
+ @datafile.get_segment(segment_key)
164
189
  end
165
190
 
166
191
  def get_feature_keys
167
- @datafile_reader.get_feature_keys
192
+ @datafile.get_feature_keys
168
193
  end
169
194
 
170
- def get_variable_keys(feature_key)
171
- @datafile_reader.get_variable_keys(feature_key)
195
+ def get_variable_keys(feature_key = nil)
196
+ @datafile.get_variable_keys(feature_key)
172
197
  end
173
198
 
174
199
  def has_variations?(feature_key)
175
- @datafile_reader.has_variations?(feature_key)
200
+ @datafile.has_variations?(feature_key)
176
201
  end
177
202
 
178
203
  # Get a feature by key
179
204
  # @param feature_key [String] Feature key
180
205
  # @return [Hash, nil] Feature data or nil if not found
181
206
  def get_feature(feature_key)
182
- @datafile_reader.get_feature(feature_key)
207
+ @datafile.get_feature(feature_key)
183
208
  end
184
209
 
185
210
  # Add a module
@@ -189,8 +214,8 @@ module Featurevisor
189
214
  @modules_manager.add(mod)
190
215
  end
191
216
 
192
- def remove_module(name_or_module)
193
- @modules_manager.remove(name_or_module)
217
+ def remove_module(name)
218
+ @modules_manager.remove(name)
194
219
  end
195
220
 
196
221
  # Subscribe to an event
@@ -198,11 +223,15 @@ module Featurevisor
198
223
  # @param callback [Proc] Callback function
199
224
  # @return [Proc] Unsubscribe function
200
225
  def on(event_name, callback)
226
+ return -> {} if @closed
227
+
201
228
  @emitter.on(event_name, callback)
202
229
  end
203
230
 
204
231
  # Close the instance
205
232
  def close
233
+ return if @closed
234
+
206
235
  @closed = true
207
236
  @modules_manager.close_all
208
237
  @module_diagnostic_subscriptions = []
@@ -257,7 +286,8 @@ module Featurevisor
257
286
  Featurevisor::ChildInstance.new(
258
287
  parent: self,
259
288
  context: get_context(context),
260
- sticky: options[:sticky]
289
+ sticky_features: options[:sticky_features] || options[:stickyFeatures],
290
+ sticky_variables: options[:sticky_variables] || options[:stickyVariables]
261
291
  )
262
292
  end
263
293
 
@@ -285,7 +315,7 @@ module Featurevisor
285
315
  evaluation = evaluate_flag(feature_key, context, options)
286
316
  evaluation[:enabled] == true
287
317
  rescue => e
288
- @logger.error("isEnabled", { feature_key: feature_key, error: e })
318
+ report_diagnostic(level: "error", code: "evaluation_error", message: "isEnabled failed", originalError: e, details: { featureKey: feature_key })
289
319
  false
290
320
  end
291
321
  end
@@ -313,7 +343,7 @@ module Featurevisor
313
343
  begin
314
344
  evaluation = evaluate_variation(feature_key, context, options)
315
345
 
316
- if evaluation[:variation_value]
346
+ if evaluation.key?(:variation_value)
317
347
  evaluation[:variation_value]
318
348
  elsif evaluation[:variation]
319
349
  evaluation[:variation][:value]
@@ -321,7 +351,7 @@ module Featurevisor
321
351
  nil
322
352
  end
323
353
  rescue => e
324
- @logger.error("getVariation", { feature_key: feature_key, error: e })
354
+ report_diagnostic(level: "error", code: "evaluation_error", message: "getVariation failed", originalError: e, details: { featureKey: feature_key })
325
355
  nil
326
356
  end
327
357
  end
@@ -332,12 +362,17 @@ module Featurevisor
332
362
  # @param context [Hash] Context
333
363
  # @param options [Hash] Override options
334
364
  # @return [Hash] Evaluation result
335
- def evaluate_variable(feature_key, variable_key, context = {}, options = {})
365
+ def evaluate_variable(feature_or_variable_key, variable_key_or_context = nil, context_or_options = {}, options = {})
366
+ if variable_key_or_context.nil? || variable_key_or_context.is_a?(Hash)
367
+ context = variable_key_or_context || {}
368
+ return evaluate_variable_without_feature(feature_or_variable_key, context, context_or_options)
369
+ end
370
+
336
371
  Featurevisor::Evaluate.evaluate_with_modules(
337
- get_evaluation_dependencies(context, options).merge(
372
+ get_evaluation_dependencies(context_or_options, options).merge(
338
373
  type: "variable",
339
- feature_key: feature_key,
340
- variable_key: variable_key
374
+ feature_key: feature_or_variable_key,
375
+ variable_key: variable_key_or_context
341
376
  )
342
377
  )
343
378
  end
@@ -348,13 +383,13 @@ module Featurevisor
348
383
  # @param context [Hash] Context
349
384
  # @param options [Hash] Override options
350
385
  # @return [Object, nil] Variable value or nil
351
- def get_variable(feature_key, variable_key, context = {}, options = {})
386
+ def get_variable(feature_or_variable_key, variable_key_or_context = nil, context_or_options = {}, options = {})
352
387
  begin
353
- evaluation = evaluate_variable(feature_key, variable_key, context, options)
388
+ evaluation = evaluate_variable(feature_or_variable_key, variable_key_or_context, context_or_options, options)
354
389
 
355
- if !evaluation[:variable_value].nil?
356
- if evaluation[:variable_schema] &&
357
- evaluation[:variable_schema][:type] == "json" &&
390
+ if evaluation.key?(:variable_value)
391
+ variable_type = evaluation.dig(:variable_schema, :type) || evaluation.dig(:variable, :type)
392
+ if variable_type == "json" &&
358
393
  evaluation[:variable_value].is_a?(String)
359
394
  JSON.parse(evaluation[:variable_value], symbolize_names: true)
360
395
  else
@@ -364,7 +399,7 @@ module Featurevisor
364
399
  nil
365
400
  end
366
401
  rescue => e
367
- @logger.error("getVariable", { feature_key: feature_key, variable_key: variable_key, error: e })
402
+ report_diagnostic(level: "error", code: "evaluation_error", message: "getVariable failed", originalError: e, details: { variableKey: variable_key_or_context || feature_or_variable_key })
368
403
  nil
369
404
  end
370
405
  end
@@ -375,8 +410,8 @@ module Featurevisor
375
410
  # @param context [Hash] Context
376
411
  # @param options [Hash] Override options
377
412
  # @return [Boolean, nil] Boolean value or nil
378
- def get_variable_boolean(feature_key, variable_key, context = {}, options = {})
379
- variable_value = get_variable(feature_key, variable_key, context, options)
413
+ def get_variable_boolean(*args)
414
+ variable_value = get_variable(*args)
380
415
  get_value_by_type(variable_value, "boolean")
381
416
  end
382
417
 
@@ -386,8 +421,8 @@ module Featurevisor
386
421
  # @param context [Hash] Context
387
422
  # @param options [Hash] Override options
388
423
  # @return [String, nil] String value or nil
389
- def get_variable_string(feature_key, variable_key, context = {}, options = {})
390
- variable_value = get_variable(feature_key, variable_key, context, options)
424
+ def get_variable_string(*args)
425
+ variable_value = get_variable(*args)
391
426
  get_value_by_type(variable_value, "string")
392
427
  end
393
428
 
@@ -397,8 +432,8 @@ module Featurevisor
397
432
  # @param context [Hash] Context
398
433
  # @param options [Hash] Override options
399
434
  # @return [Integer, nil] Integer value or nil
400
- def get_variable_integer(feature_key, variable_key, context = {}, options = {})
401
- variable_value = get_variable(feature_key, variable_key, context, options)
435
+ def get_variable_integer(*args)
436
+ variable_value = get_variable(*args)
402
437
  get_value_by_type(variable_value, "integer")
403
438
  end
404
439
 
@@ -408,8 +443,8 @@ module Featurevisor
408
443
  # @param context [Hash] Context
409
444
  # @param options [Hash] Override options
410
445
  # @return [Float, nil] Float value or nil
411
- def get_variable_double(feature_key, variable_key, context = {}, options = {})
412
- variable_value = get_variable(feature_key, variable_key, context, options)
446
+ def get_variable_double(*args)
447
+ variable_value = get_variable(*args)
413
448
  get_value_by_type(variable_value, "double")
414
449
  end
415
450
 
@@ -419,8 +454,8 @@ module Featurevisor
419
454
  # @param context [Hash] Context
420
455
  # @param options [Hash] Override options
421
456
  # @return [Array, nil] Array value or nil
422
- def get_variable_array(feature_key, variable_key, context = {}, options = {})
423
- variable_value = get_variable(feature_key, variable_key, context, options)
457
+ def get_variable_array(*args)
458
+ variable_value = get_variable(*args)
424
459
  get_value_by_type(variable_value, "array")
425
460
  end
426
461
 
@@ -430,8 +465,8 @@ module Featurevisor
430
465
  # @param context [Hash] Context
431
466
  # @param options [Hash] Override options
432
467
  # @return [Hash, nil] Object value or nil
433
- def get_variable_object(feature_key, variable_key, context = {}, options = {})
434
- variable_value = get_variable(feature_key, variable_key, context, options)
468
+ def get_variable_object(*args)
469
+ variable_value = get_variable(*args)
435
470
  get_value_by_type(variable_value, "object")
436
471
  end
437
472
 
@@ -441,8 +476,8 @@ module Featurevisor
441
476
  # @param context [Hash] Context
442
477
  # @param options [Hash] Override options
443
478
  # @return [Object, nil] JSON value or nil
444
- def get_variable_json(feature_key, variable_key, context = {}, options = {})
445
- variable_value = get_variable(feature_key, variable_key, context, options)
479
+ def get_variable_json(*args)
480
+ variable_value = get_variable(*args)
446
481
  get_value_by_type(variable_value, "json")
447
482
  end
448
483
 
@@ -451,10 +486,10 @@ module Featurevisor
451
486
  # @param feature_keys [Array<String>] Feature keys to evaluate
452
487
  # @param options [Hash] Override options
453
488
  # @return [Hash] All evaluations
454
- def get_all_evaluations(context = {}, feature_keys = [], options = {})
489
+ def get_feature_evaluations(context = {}, feature_keys = [], options = {})
455
490
  result = {}
456
491
 
457
- keys = feature_keys.size > 0 ? feature_keys : @datafile_reader.get_feature_keys
492
+ keys = feature_keys.size > 0 ? feature_keys : @datafile.get_feature_keys
458
493
 
459
494
  keys.each do |feature_key|
460
495
  # Convert symbol keys to strings for evaluation functions
@@ -466,13 +501,13 @@ module Featurevisor
466
501
  }
467
502
 
468
503
  # variation
469
- if @datafile_reader.has_variations?(feature_key_str)
504
+ if @datafile.has_variations?(feature_key_str)
470
505
  variation = get_variation(feature_key_str, context, options)
471
- evaluated_feature[:variation] = variation if variation
506
+ evaluated_feature[:variation] = variation unless variation.nil?
472
507
  end
473
508
 
474
509
  # variables
475
- variable_keys = @datafile_reader.get_variable_keys(feature_key_str)
510
+ variable_keys = @datafile.get_variable_keys(feature_key_str)
476
511
  if variable_keys.size > 0
477
512
  evaluated_feature[:variables] = {}
478
513
 
@@ -492,8 +527,100 @@ module Featurevisor
492
527
  result
493
528
  end
494
529
 
530
+ def get_variable_evaluations(context = {}, variable_keys = [], options = {})
531
+ keys = variable_keys.empty? ? @datafile.get_variable_keys : variable_keys
532
+ keys.to_h { |key| [key, get_variable(key.to_s, context, options)] }
533
+ end
534
+
495
535
  private
496
536
 
537
+ def evaluate_variable_without_feature(variable_key, context = {}, options = {})
538
+ evaluation_options = {
539
+ type: "variable",
540
+ variable_key: variable_key.to_s,
541
+ context: get_context(context)
542
+ }
543
+ evaluation_options[:default_variable_value] = options[:default_variable_value] if options.key?(:default_variable_value)
544
+ begin
545
+ evaluation_options = @modules_manager.run_before_evaluation_modules(evaluation_options)
546
+ resolved_key = evaluation_options[:variable_key]
547
+ variable = @datafile.get_global_variable(resolved_key)
548
+ sticky = options[:__featurevisor_child_sticky_variables] || @sticky_variables
549
+ evaluation = { type: "variable", variable_key: resolved_key, reason: Featurevisor::EvaluationReason::VARIABLE_NOT_FOUND }
550
+
551
+ if sticky.key?(resolved_key) || sticky.key?(resolved_key.to_sym)
552
+ sticky_value = sticky.key?(resolved_key) ? sticky[resolved_key] : sticky[resolved_key.to_sym]
553
+ evaluation.merge!(reason: Featurevisor::EvaluationReason::STICKY, variable: variable,
554
+ variable_value: sticky_value)
555
+ elsif variable
556
+ unless required_features_are_matched(variable[:requiredFeatures], evaluation_options[:context], options)
557
+ value_key = variable[:useDefaultWhenDisabled] ? :defaultValue : :disabledValue
558
+ evaluation.merge!(reason: Featurevisor::EvaluationReason::REQUIRED_FEATURES_UNMET,
559
+ variable: variable)
560
+ evaluation[:variable_value] = variable[value_key] if variable.key?(value_key)
561
+ else
562
+ (variable[:overrides] || []).each_with_index do |override, index|
563
+ next unless required_features_are_matched(override[:requiredFeatures], evaluation_options[:context], options)
564
+ conditions_match = !override[:conditions] || @datafile.all_conditions_are_matched(
565
+ @datafile.parse_conditions_if_stringified(override[:conditions]), evaluation_options[:context]
566
+ )
567
+ segments_match = !override[:segments] || @datafile.all_segments_are_matched(
568
+ @datafile.parse_segments_if_stringified(override[:segments]), evaluation_options[:context]
569
+ )
570
+ next unless conditions_match && segments_match
571
+
572
+ evaluation.merge!(reason: Featurevisor::EvaluationReason::VARIABLE_OVERRIDE_RULE,
573
+ variable: variable,
574
+ variable_override_index: index, variable_override_key: override[:key],
575
+ variable_override_path: override[:keyPath])
576
+ evaluation[:variable_value] = override[:value] if override.key?(:value)
577
+ break
578
+ end
579
+ if evaluation[:reason] == Featurevisor::EvaluationReason::VARIABLE_NOT_FOUND
580
+ evaluation.merge!(reason: Featurevisor::EvaluationReason::VARIABLE_DEFAULT,
581
+ variable: variable)
582
+ evaluation[:variable_value] = variable[:defaultValue] if variable.key?(:defaultValue)
583
+ end
584
+ end
585
+ report_diagnostic(level: "warn", code: "variable_deprecated", message: "Variable \"#{resolved_key}\" is deprecated",
586
+ details: { variableKey: resolved_key, evaluation: evaluation }) if variable[:deprecated]
587
+ end
588
+
589
+ if !evaluation.key?(:variable_value) && evaluation_options.key?(:default_variable_value)
590
+ evaluation[:variable_value] = evaluation_options[:default_variable_value]
591
+ end
592
+ evaluation = @modules_manager.run_global_after_modules(evaluation, evaluation_options)
593
+ report_diagnostic(level: "debug", code: evaluation[:reason], message: "Global variable evaluated", details: evaluation)
594
+ evaluation
595
+ rescue => e
596
+ evaluation = { type: "variable", variable_key: evaluation_options[:variable_key],
597
+ reason: Featurevisor::EvaluationReason::ERROR, error: e }
598
+ report_diagnostic(level: "error", code: "evaluation_error", message: "Global variable evaluation failed",
599
+ originalError: e, details: evaluation)
600
+ evaluation
601
+ end
602
+ end
603
+
604
+ def required_features_are_matched(requirements, context, options)
605
+ return true if requirements.nil?
606
+
607
+ items = requirements.is_a?(Array) ? requirements : [requirements]
608
+ clean_options = options.reject { |key, _| %i[default_variation_value default_variable_value].include?(key) }
609
+ items.all? do |required|
610
+ if required.is_a?(String)
611
+ key = required
612
+ expected_enabled = true
613
+ expected_variation = nil
614
+ else
615
+ key = required[:feature]
616
+ expected_enabled = required.key?(:enabled) ? required[:enabled] : true
617
+ expected_variation = required[:variation]
618
+ end
619
+ next false unless is_enabled(key, context, clean_options) == expected_enabled
620
+ expected_variation.nil? || get_variation(key, context, clean_options) == expected_variation
621
+ end
622
+ end
623
+
497
624
  # Get evaluation dependencies
498
625
  # @param context [Hash] Context
499
626
  # @param options [Hash] Override options
@@ -501,13 +628,15 @@ module Featurevisor
501
628
  def get_evaluation_dependencies(context, options = {})
502
629
  {
503
630
  context: get_context(context),
504
- logger: @logger,
631
+ diagnostics: @diagnostics,
505
632
  modules_manager: @modules_manager,
506
- datafile_reader: @datafile_reader,
507
- sticky: options[:__featurevisor_child_sticky] || @sticky,
508
- default_variation_value: options[:default_variation_value],
509
- default_variable_value: options[:default_variable_value]
510
- }
633
+ datafile: @datafile,
634
+ sticky: options[:__featurevisor_child_sticky_features] || @sticky_features,
635
+ sticky_variables: options[:__featurevisor_child_sticky_variables] || @sticky_variables,
636
+ }.tap do |dependencies|
637
+ dependencies[:default_variation_value] = options[:default_variation_value] if options.key?(:default_variation_value)
638
+ dependencies[:default_variable_value] = options[:default_variable_value] if options.key?(:default_variable_value)
639
+ end
511
640
  end
512
641
 
513
642
  # Get value by type
@@ -554,6 +683,10 @@ module Featurevisor
554
683
  features: {
555
684
  **(previous[:features] || {}),
556
685
  **(incoming[:features] || {})
686
+ },
687
+ variables: {
688
+ **(previous[:variables] || {}),
689
+ **(incoming[:variables] || {})
557
690
  }
558
691
  }.compact
559
692
  end
@@ -584,7 +717,7 @@ module Featurevisor
584
717
  diagnostic = (diagnostic || {}).dup
585
718
  diagnostic[:level] ||= "info"
586
719
  diagnostic[:module] = source_module.name if source_module && source_module.name
587
- details = (diagnostic[:details] || {}).dup
720
+ details = camelize_diagnostic_value((diagnostic[:details] || {}).dup)
588
721
  legacy_module_name = diagnostic.delete(:module_name)
589
722
  legacy_original_error = diagnostic.delete(:original_error)
590
723
  diagnostic[:moduleName] = legacy_module_name if !diagnostic.key?(:moduleName) && !legacy_module_name.nil?
@@ -609,7 +742,7 @@ module Featurevisor
609
742
  end
610
743
 
611
744
  if @on_diagnostic
612
- if should_report_diagnostic?(diagnostic[:level], @logger.level)
745
+ if should_report_diagnostic?(diagnostic[:level], @diagnostics.level)
613
746
  begin
614
747
  @on_diagnostic.call(diagnostic)
615
748
  rescue => e
@@ -617,7 +750,7 @@ module Featurevisor
617
750
  end
618
751
  end
619
752
  else
620
- Logger.new(level: @logger.level).log(
753
+ DiagnosticReporter.new(level: @diagnostics.level).log(
621
754
  diagnostic[:level],
622
755
  diagnostic[:message],
623
756
  diagnostic
@@ -638,6 +771,21 @@ module Featurevisor
638
771
 
639
772
  subscriber_index >= diagnostic_index
640
773
  end
774
+
775
+ def camelize_diagnostic_value(value)
776
+ case value
777
+ when Hash
778
+ value.each_with_object({}) do |(key, item), result|
779
+ normalized_key = key.to_s.gsub(/_([a-z])/) { Regexp.last_match(1).upcase }.to_sym
780
+ result[normalized_key] = camelize_diagnostic_value(item)
781
+ end
782
+ when Array
783
+ value.map { |item| camelize_diagnostic_value(item) }
784
+ else
785
+ value
786
+ end
787
+ end
788
+ private :camelize_diagnostic_value
641
789
  end
642
790
 
643
791
  # Create a new Featurevisor instance
@@ -13,9 +13,11 @@ module Featurevisor
13
13
  @name = options[:name]
14
14
  @setup = options[:setup]
15
15
  @before = options[:before]
16
+ @before_evaluation = options[:before_evaluation] || options[:beforeEvaluation]
16
17
  @bucket_key = options[:bucket_key]
17
18
  @bucket_value = options[:bucket_value]
18
19
  @after = options[:after]
20
+ @after_evaluation = options[:after_evaluation] || options[:afterEvaluation]
19
21
  @close = options[:close]
20
22
  end
21
23
 
@@ -29,6 +31,12 @@ module Featurevisor
29
31
  @before.call(options)
30
32
  end
31
33
 
34
+ def call_before_evaluation(options)
35
+ return options unless @before_evaluation
36
+
37
+ @before_evaluation.call(options)
38
+ end
39
+
32
40
  def call_bucket_key(options)
33
41
  return options[:bucket_key] unless @bucket_key
34
42
 
@@ -47,6 +55,12 @@ module Featurevisor
47
55
  @after.call(evaluation, options)
48
56
  end
49
57
 
58
+ def call_after_evaluation(evaluation, options)
59
+ return evaluation unless @after_evaluation
60
+
61
+ @after_evaluation.call(evaluation, options)
62
+ end
63
+
50
64
  def call_close
51
65
  @close.call if @close
52
66
  end
@@ -124,6 +138,43 @@ module Featurevisor
124
138
  end
125
139
 
126
140
  def run_before_modules(options)
141
+ result = @modules.reduce(options) do |current, mod|
142
+ mod.call_before(current)
143
+ end
144
+ @modules.reduce(result) do |current, mod|
145
+ mod.call_before_evaluation(current)
146
+ end
147
+ end
148
+
149
+ def run_before_evaluation_modules(options)
150
+ @modules.reduce(options) do |result, mod|
151
+ mod.call_before_evaluation(result)
152
+ end
153
+ end
154
+
155
+ def run_after_evaluation_modules(evaluation, options)
156
+ @modules.reduce(evaluation) do |result, mod|
157
+ mod.call_after_evaluation(result, options)
158
+ end
159
+ end
160
+
161
+ def run_after_modules(evaluation, options)
162
+ result = @modules.reduce(evaluation) do |current, mod|
163
+ mod.call_after_evaluation(current, options)
164
+ end
165
+ @modules.reduce(result) do |current, mod|
166
+ mod.call_after(current, options)
167
+ end
168
+ end
169
+
170
+ # Deprecated feature-only callbacks are intentionally excluded here.
171
+ def run_global_after_modules(evaluation, options)
172
+ @modules.reduce(evaluation) do |result, mod|
173
+ mod.call_after_evaluation(result, options)
174
+ end
175
+ end
176
+
177
+ def run_legacy_before_modules(options)
127
178
  @modules.reduce(options) do |result, mod|
128
179
  mod.call_before(result)
129
180
  end
@@ -145,12 +196,6 @@ module Featurevisor
145
196
  bucket_value
146
197
  end
147
198
 
148
- def run_after_modules(evaluation, options)
149
- @modules.reduce(evaluation) do |result, mod|
150
- mod.call_after(result, options)
151
- end
152
- end
153
-
154
199
  def close_all
155
200
  @modules.each do |mod|
156
201
  @clear_module_diagnostic_subscriptions.call(mod) if @clear_module_diagnostic_subscriptions