featurevisor 0.3.0 → 1.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.
- checksums.yaml +4 -4
- data/README.md +158 -83
- data/bin/cli.rb +9 -4
- data/bin/commands/assess_distribution.rb +34 -15
- data/bin/commands/benchmark.rb +65 -63
- data/bin/commands/test.rb +74 -96
- data/lib/featurevisor/child_instance.rb +22 -24
- data/lib/featurevisor/datafile_reader.rb +18 -13
- data/lib/featurevisor/emitter.rb +2 -2
- data/lib/featurevisor/evaluate.rb +16 -23
- data/lib/featurevisor/events.rb +5 -4
- data/lib/featurevisor/instance.rb +225 -39
- data/lib/featurevisor/logger.rb +0 -7
- data/lib/featurevisor/modules.rb +184 -0
- data/lib/featurevisor/version.rb +1 -1
- data/lib/featurevisor.rb +4 -1
- metadata +16 -2
- data/lib/featurevisor/hooks.rb +0 -159
|
@@ -5,7 +5,7 @@ require "json"
|
|
|
5
5
|
module Featurevisor
|
|
6
6
|
# DatafileReader class for reading and processing Featurevisor datafiles
|
|
7
7
|
class DatafileReader
|
|
8
|
-
attr_reader :schema_version, :revision, :segments, :features, :logger, :regex_cache
|
|
8
|
+
attr_reader :schema_version, :revision, :featurevisor_version, :segments, :features, :logger, :regex_cache
|
|
9
9
|
|
|
10
10
|
# Initialize a new DatafileReader
|
|
11
11
|
# @param options [Hash] Options hash containing datafile and logger
|
|
@@ -17,6 +17,7 @@ module Featurevisor
|
|
|
17
17
|
|
|
18
18
|
@schema_version = datafile[:schemaVersion]
|
|
19
19
|
@revision = datafile[:revision]
|
|
20
|
+
@featurevisor_version = datafile[:featurevisorVersion]
|
|
20
21
|
@segments = (datafile[:segments] || {}).transform_keys(&:to_sym)
|
|
21
22
|
@features = (datafile[:features] || {}).transform_keys(&:to_sym)
|
|
22
23
|
|
|
@@ -65,6 +66,16 @@ module Featurevisor
|
|
|
65
66
|
@schema_version
|
|
66
67
|
end
|
|
67
68
|
|
|
69
|
+
def get_datafile
|
|
70
|
+
{
|
|
71
|
+
schemaVersion: @schema_version,
|
|
72
|
+
revision: @revision,
|
|
73
|
+
featurevisorVersion: @featurevisor_version,
|
|
74
|
+
segments: @segments,
|
|
75
|
+
features: @features
|
|
76
|
+
}.compact
|
|
77
|
+
end
|
|
78
|
+
|
|
68
79
|
# Get a segment by key
|
|
69
80
|
# @param segment_key [String] Segment key to retrieve
|
|
70
81
|
# @return [Hash, nil] Segment data or nil if not found
|
|
@@ -172,15 +183,11 @@ module Featurevisor
|
|
|
172
183
|
end
|
|
173
184
|
|
|
174
185
|
if conditions.is_a?(Hash) && conditions[:not] && conditions[:not].is_a?(Array)
|
|
175
|
-
return conditions[:not]
|
|
176
|
-
all_conditions_are_matched({ and: conditions[:not] }, context) == false
|
|
177
|
-
end
|
|
186
|
+
return !all_conditions_are_matched({ and: conditions[:not] }, context)
|
|
178
187
|
end
|
|
179
188
|
|
|
180
189
|
if conditions.is_a?(Hash) && conditions["not"] && conditions["not"].is_a?(Array)
|
|
181
|
-
return conditions["not"]
|
|
182
|
-
all_conditions_are_matched({ "and" => conditions["not"] }, context) == false
|
|
183
|
-
end
|
|
190
|
+
return !all_conditions_are_matched({ "and" => conditions["not"] }, context)
|
|
184
191
|
end
|
|
185
192
|
|
|
186
193
|
|
|
@@ -237,15 +244,11 @@ module Featurevisor
|
|
|
237
244
|
end
|
|
238
245
|
|
|
239
246
|
if group_segments[:not] && group_segments[:not].is_a?(Array)
|
|
240
|
-
return group_segments[:not]
|
|
241
|
-
all_segments_are_matched({ and: group_segments[:not] }, context) == false
|
|
242
|
-
end
|
|
247
|
+
return !all_segments_are_matched({ and: group_segments[:not] }, context)
|
|
243
248
|
end
|
|
244
249
|
|
|
245
250
|
if group_segments["not"] && group_segments["not"].is_a?(Array)
|
|
246
|
-
return group_segments["not"]
|
|
247
|
-
all_segments_are_matched({ "and" => group_segments["not"] }, context) == false
|
|
248
|
-
end
|
|
251
|
+
return !all_segments_are_matched({ "and" => group_segments["not"] }, context)
|
|
249
252
|
end
|
|
250
253
|
|
|
251
254
|
|
|
@@ -351,4 +354,6 @@ module Featurevisor
|
|
|
351
354
|
segments
|
|
352
355
|
end
|
|
353
356
|
end
|
|
357
|
+
|
|
358
|
+
private_constant :DatafileReader
|
|
354
359
|
end
|
data/lib/featurevisor/emitter.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Featurevisor
|
|
4
4
|
# Event names for the emitter
|
|
5
|
-
EVENT_NAMES = %w[datafile_set context_set sticky_set].freeze
|
|
5
|
+
EVENT_NAMES = %w[datafile_set context_set sticky_set error].freeze
|
|
6
6
|
|
|
7
7
|
# Event emitter class for handling event subscriptions and triggers
|
|
8
8
|
class Emitter
|
|
@@ -42,7 +42,7 @@ module Featurevisor
|
|
|
42
42
|
|
|
43
43
|
return unless listeners
|
|
44
44
|
|
|
45
|
-
listeners.each do |listener|
|
|
45
|
+
listeners.dup.each do |listener|
|
|
46
46
|
begin
|
|
47
47
|
listener.call(details)
|
|
48
48
|
rescue => err
|
|
@@ -36,20 +36,17 @@ module Featurevisor
|
|
|
36
36
|
# Evaluation module for feature flag evaluation
|
|
37
37
|
module Evaluate
|
|
38
38
|
|
|
39
|
-
# Evaluate with
|
|
39
|
+
# Evaluate with modules
|
|
40
40
|
# @param options [Hash] Evaluation options
|
|
41
41
|
# @return [Hash] Evaluation result
|
|
42
|
-
def self.
|
|
42
|
+
def self.evaluate_with_modules(options)
|
|
43
43
|
begin
|
|
44
|
-
|
|
45
|
-
hooks = hooks_manager.get_all
|
|
44
|
+
modules_manager = options[:modules_manager]
|
|
46
45
|
|
|
47
|
-
# Run before
|
|
46
|
+
# Run before modules
|
|
48
47
|
result_options = options
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
result_options = hook.call_before(result_options)
|
|
52
|
-
end
|
|
48
|
+
if modules_manager
|
|
49
|
+
result_options = modules_manager.run_before_modules(result_options)
|
|
53
50
|
end
|
|
54
51
|
|
|
55
52
|
# Evaluate
|
|
@@ -69,11 +66,9 @@ module Featurevisor
|
|
|
69
66
|
evaluation[:variable_value] = options[:default_variable_value]
|
|
70
67
|
end
|
|
71
68
|
|
|
72
|
-
# Run after
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
evaluation = hook.call_after(evaluation, result_options)
|
|
76
|
-
end
|
|
69
|
+
# Run after modules
|
|
70
|
+
if modules_manager
|
|
71
|
+
evaluation = modules_manager.run_after_modules(evaluation, result_options)
|
|
77
72
|
end
|
|
78
73
|
|
|
79
74
|
evaluation
|
|
@@ -108,9 +103,7 @@ module Featurevisor
|
|
|
108
103
|
logger = options[:logger]
|
|
109
104
|
datafile_reader = options[:datafile_reader]
|
|
110
105
|
sticky = options[:sticky]
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
hooks = hooks_manager.get_all
|
|
106
|
+
modules_manager = options[:modules_manager]
|
|
114
107
|
evaluation = nil
|
|
115
108
|
|
|
116
109
|
begin
|
|
@@ -422,24 +415,24 @@ module Featurevisor
|
|
|
422
415
|
logger: logger
|
|
423
416
|
})
|
|
424
417
|
|
|
425
|
-
# Run bucket key
|
|
426
|
-
bucket_key =
|
|
418
|
+
# Run bucket key modules
|
|
419
|
+
bucket_key = modules_manager.run_bucket_key_modules({
|
|
427
420
|
feature_key: feature_key,
|
|
428
421
|
context: context,
|
|
429
422
|
bucket_by: feature[:bucketBy],
|
|
430
423
|
bucket_key: bucket_key
|
|
431
|
-
})
|
|
424
|
+
}) if modules_manager
|
|
432
425
|
|
|
433
426
|
# bucketValue
|
|
434
427
|
bucket_value = Featurevisor::Bucketer.get_bucketed_number(bucket_key)
|
|
435
428
|
|
|
436
|
-
# Run bucket value
|
|
437
|
-
bucket_value =
|
|
429
|
+
# Run bucket value modules
|
|
430
|
+
bucket_value = modules_manager.run_bucket_value_modules({
|
|
438
431
|
feature_key: feature_key,
|
|
439
432
|
bucket_key: bucket_key,
|
|
440
433
|
context: context,
|
|
441
434
|
bucket_value: bucket_value
|
|
442
|
-
})
|
|
435
|
+
}) if modules_manager
|
|
443
436
|
|
|
444
437
|
matched_traffic = nil
|
|
445
438
|
matched_allocation = nil
|
data/lib/featurevisor/events.rb
CHANGED
|
@@ -24,7 +24,7 @@ module Featurevisor
|
|
|
24
24
|
# @param previous_reader [DatafileReader] Previous datafile reader
|
|
25
25
|
# @param new_reader [DatafileReader] New datafile reader
|
|
26
26
|
# @return [Hash] Event parameters
|
|
27
|
-
def self.get_params_for_datafile_set_event(previous_reader, new_reader)
|
|
27
|
+
def self.get_params_for_datafile_set_event(previous_reader, new_reader, replace = false)
|
|
28
28
|
previous_revision = previous_reader.get_revision
|
|
29
29
|
previous_feature_keys = previous_reader.get_feature_keys
|
|
30
30
|
|
|
@@ -67,9 +67,10 @@ module Featurevisor
|
|
|
67
67
|
|
|
68
68
|
{
|
|
69
69
|
revision: new_revision,
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
features: all_affected_features
|
|
70
|
+
previousRevision: previous_revision,
|
|
71
|
+
revisionChanged: previous_revision != new_revision,
|
|
72
|
+
features: all_affected_features,
|
|
73
|
+
replaced: replace
|
|
73
74
|
}
|
|
74
75
|
end
|
|
75
76
|
end
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
+
require "securerandom"
|
|
4
5
|
|
|
5
6
|
module Featurevisor
|
|
6
7
|
# Instance class for managing feature flag evaluations
|
|
7
8
|
class Instance
|
|
8
|
-
attr_reader :context, :logger, :sticky, :datafile_reader, :hooks_manager, :emitter
|
|
9
|
-
|
|
10
9
|
# Empty datafile template
|
|
11
10
|
EMPTY_DATAFILE = {
|
|
12
11
|
schemaVersion: "2",
|
|
@@ -20,34 +19,44 @@ module Featurevisor
|
|
|
20
19
|
# @option options [Hash, String] :datafile Datafile content or JSON string
|
|
21
20
|
# @option options [Hash] :context Initial context
|
|
22
21
|
# @option options [String] :log_level Log level
|
|
23
|
-
# @option options [Logger] :logger Logger instance
|
|
24
22
|
# @option options [Hash] :sticky Sticky features
|
|
25
|
-
# @option options [Array<
|
|
23
|
+
# @option options [Array<Hash, FeaturevisorModule>] :modules Array of modules
|
|
24
|
+
# @option options [Proc] :on_diagnostic Diagnostic handler
|
|
26
25
|
def initialize(options = {})
|
|
27
26
|
# from options
|
|
28
27
|
@context = options[:context] || {}
|
|
29
|
-
@logger =
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
logger: @logger
|
|
28
|
+
@logger = Logger.new(
|
|
29
|
+
level: options[:log_level] || "info",
|
|
30
|
+
handler: method(:handle_internal_log)
|
|
33
31
|
)
|
|
32
|
+
@on_diagnostic = options[:on_diagnostic] || options[:onDiagnostic]
|
|
34
33
|
@emitter = Featurevisor::Emitter.new
|
|
35
34
|
@sticky = options[:sticky] || {}
|
|
35
|
+
@closed = false
|
|
36
|
+
@module_diagnostic_subscriptions = []
|
|
36
37
|
|
|
37
38
|
# datafile
|
|
38
|
-
@datafile_reader =
|
|
39
|
+
@datafile_reader = DatafileReader.new(
|
|
39
40
|
datafile: EMPTY_DATAFILE,
|
|
40
41
|
logger: @logger
|
|
41
42
|
)
|
|
42
43
|
|
|
44
|
+
@modules_manager = Featurevisor::Modules::ModulesManager.new(
|
|
45
|
+
modules: options[:modules] || [],
|
|
46
|
+
report_diagnostic: method(:report_diagnostic),
|
|
47
|
+
module_api_factory: method(:create_module_api),
|
|
48
|
+
clear_module_diagnostic_subscriptions: method(:clear_module_diagnostic_subscriptions)
|
|
49
|
+
)
|
|
50
|
+
|
|
43
51
|
if options[:datafile]
|
|
44
|
-
|
|
45
|
-
datafile: options[:datafile].is_a?(String) ? JSON.parse(options[:datafile], symbolize_names: true) : options[:datafile],
|
|
46
|
-
logger: @logger
|
|
47
|
-
)
|
|
52
|
+
set_datafile(options[:datafile], true)
|
|
48
53
|
end
|
|
49
54
|
|
|
50
|
-
|
|
55
|
+
report_diagnostic(
|
|
56
|
+
level: "info",
|
|
57
|
+
code: "sdk_initialized",
|
|
58
|
+
message: "SDK initialized"
|
|
59
|
+
)
|
|
51
60
|
end
|
|
52
61
|
|
|
53
62
|
# Set the log level
|
|
@@ -56,22 +65,61 @@ module Featurevisor
|
|
|
56
65
|
@logger.set_level(level)
|
|
57
66
|
end
|
|
58
67
|
|
|
68
|
+
def handle_internal_log(level, message, details = nil)
|
|
69
|
+
details = (details || {}).dup
|
|
70
|
+
code = details[:reason] || details["reason"] || message
|
|
71
|
+
code = "deprecated_feature" if message == "feature is deprecated"
|
|
72
|
+
code = "deprecated_variable" if message == "variable is deprecated"
|
|
73
|
+
code = "feature_not_found" if message == "feature not found"
|
|
74
|
+
code = "variable_not_found" if message == "variable schema not found"
|
|
75
|
+
code = "no_variations" if message == "no variations"
|
|
76
|
+
code = "invalid_bucket_by" if message == "invalid bucketBy"
|
|
77
|
+
report_diagnostic(level: level, code: code.to_s, message: message, details: details)
|
|
78
|
+
end
|
|
79
|
+
private :handle_internal_log
|
|
80
|
+
|
|
59
81
|
# Set the datafile
|
|
60
82
|
# @param datafile [Hash, String] Datafile content or JSON string
|
|
61
|
-
|
|
83
|
+
# @param replace [Boolean] Whether to replace instead of merge
|
|
84
|
+
def set_datafile(datafile, replace = false)
|
|
85
|
+
return if @closed
|
|
86
|
+
|
|
62
87
|
begin
|
|
63
|
-
|
|
64
|
-
|
|
88
|
+
parsed_datafile = if datafile.is_a?(String)
|
|
89
|
+
JSON.parse(datafile, symbolize_names: true)
|
|
90
|
+
elsif datafile.is_a?(Hash)
|
|
91
|
+
JSON.parse(JSON.generate(datafile), symbolize_names: true)
|
|
92
|
+
else
|
|
93
|
+
datafile
|
|
94
|
+
end
|
|
95
|
+
unless parsed_datafile.is_a?(Hash) && parsed_datafile[:schemaVersion].is_a?(String) &&
|
|
96
|
+
parsed_datafile[:revision].is_a?(String) && parsed_datafile[:segments].is_a?(Hash) &&
|
|
97
|
+
parsed_datafile[:features].is_a?(Hash)
|
|
98
|
+
raise ArgumentError, "Invalid datafile"
|
|
99
|
+
end
|
|
100
|
+
next_datafile = replace ? parsed_datafile : merge_datafiles(@datafile_reader.get_datafile, parsed_datafile)
|
|
101
|
+
new_datafile_reader = DatafileReader.new(
|
|
102
|
+
datafile: next_datafile,
|
|
65
103
|
logger: @logger
|
|
66
104
|
)
|
|
67
105
|
|
|
68
|
-
details = Featurevisor::Events.get_params_for_datafile_set_event(@datafile_reader, new_datafile_reader)
|
|
106
|
+
details = Featurevisor::Events.get_params_for_datafile_set_event(@datafile_reader, new_datafile_reader, replace)
|
|
69
107
|
@datafile_reader = new_datafile_reader
|
|
70
108
|
|
|
71
|
-
|
|
109
|
+
report_diagnostic(
|
|
110
|
+
level: "info",
|
|
111
|
+
code: "datafile_set",
|
|
112
|
+
message: "Datafile set",
|
|
113
|
+
details: details
|
|
114
|
+
)
|
|
72
115
|
@emitter.trigger("datafile_set", details)
|
|
73
116
|
rescue => e
|
|
74
|
-
|
|
117
|
+
report_diagnostic(
|
|
118
|
+
level: "error",
|
|
119
|
+
code: "invalid_datafile",
|
|
120
|
+
message: "Could not parse datafile",
|
|
121
|
+
original_error: e
|
|
122
|
+
)
|
|
75
123
|
end
|
|
76
124
|
end
|
|
77
125
|
|
|
@@ -92,7 +140,12 @@ module Featurevisor
|
|
|
92
140
|
|
|
93
141
|
params = Featurevisor::Events.get_params_for_sticky_set_event(previous_sticky_features, @sticky, replace)
|
|
94
142
|
|
|
95
|
-
|
|
143
|
+
report_diagnostic(
|
|
144
|
+
level: "info",
|
|
145
|
+
code: "sticky_set",
|
|
146
|
+
message: "Sticky features set",
|
|
147
|
+
details: params
|
|
148
|
+
)
|
|
96
149
|
@emitter.trigger("sticky_set", params)
|
|
97
150
|
end
|
|
98
151
|
|
|
@@ -102,6 +155,26 @@ module Featurevisor
|
|
|
102
155
|
@datafile_reader.get_revision
|
|
103
156
|
end
|
|
104
157
|
|
|
158
|
+
def get_schema_version
|
|
159
|
+
@datafile_reader.get_schema_version
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def get_segment(segment_key)
|
|
163
|
+
@datafile_reader.get_segment(segment_key)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def get_feature_keys
|
|
167
|
+
@datafile_reader.get_feature_keys
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def get_variable_keys(feature_key)
|
|
171
|
+
@datafile_reader.get_variable_keys(feature_key)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def has_variations?(feature_key)
|
|
175
|
+
@datafile_reader.has_variations?(feature_key)
|
|
176
|
+
end
|
|
177
|
+
|
|
105
178
|
# Get a feature by key
|
|
106
179
|
# @param feature_key [String] Feature key
|
|
107
180
|
# @return [Hash, nil] Feature data or nil if not found
|
|
@@ -109,11 +182,15 @@ module Featurevisor
|
|
|
109
182
|
@datafile_reader.get_feature(feature_key)
|
|
110
183
|
end
|
|
111
184
|
|
|
112
|
-
# Add a
|
|
113
|
-
# @param
|
|
114
|
-
# @return [Proc, nil] Remove function or nil if
|
|
115
|
-
def
|
|
116
|
-
@
|
|
185
|
+
# Add a module
|
|
186
|
+
# @param mod [Hash, FeaturevisorModule] Module to add
|
|
187
|
+
# @return [Proc, nil] Remove function or nil if module already exists
|
|
188
|
+
def add_module(mod)
|
|
189
|
+
@modules_manager.add(mod)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def remove_module(name_or_module)
|
|
193
|
+
@modules_manager.remove(name_or_module)
|
|
117
194
|
end
|
|
118
195
|
|
|
119
196
|
# Subscribe to an event
|
|
@@ -126,6 +203,9 @@ module Featurevisor
|
|
|
126
203
|
|
|
127
204
|
# Close the instance
|
|
128
205
|
def close
|
|
206
|
+
@closed = true
|
|
207
|
+
@modules_manager.close_all
|
|
208
|
+
@module_diagnostic_subscriptions = []
|
|
129
209
|
@emitter.clear_all
|
|
130
210
|
end
|
|
131
211
|
|
|
@@ -144,10 +224,15 @@ module Featurevisor
|
|
|
144
224
|
replaced: replace
|
|
145
225
|
})
|
|
146
226
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
227
|
+
report_diagnostic(
|
|
228
|
+
level: "debug",
|
|
229
|
+
code: "context_set",
|
|
230
|
+
message: replace ? "Context replaced" : "Context updated",
|
|
231
|
+
details: {
|
|
232
|
+
context: @context,
|
|
233
|
+
replaced: replace
|
|
234
|
+
}
|
|
235
|
+
)
|
|
151
236
|
end
|
|
152
237
|
|
|
153
238
|
# Get context
|
|
@@ -182,7 +267,7 @@ module Featurevisor
|
|
|
182
267
|
# @param options [Hash] Override options
|
|
183
268
|
# @return [Hash] Evaluation result
|
|
184
269
|
def evaluate_flag(feature_key, context = {}, options = {})
|
|
185
|
-
Featurevisor::Evaluate.
|
|
270
|
+
Featurevisor::Evaluate.evaluate_with_modules(
|
|
186
271
|
get_evaluation_dependencies(context, options).merge(
|
|
187
272
|
type: "flag",
|
|
188
273
|
feature_key: feature_key
|
|
@@ -211,7 +296,7 @@ module Featurevisor
|
|
|
211
296
|
# @param options [Hash] Override options
|
|
212
297
|
# @return [Hash] Evaluation result
|
|
213
298
|
def evaluate_variation(feature_key, context = {}, options = {})
|
|
214
|
-
Featurevisor::Evaluate.
|
|
299
|
+
Featurevisor::Evaluate.evaluate_with_modules(
|
|
215
300
|
get_evaluation_dependencies(context, options).merge(
|
|
216
301
|
type: "variation",
|
|
217
302
|
feature_key: feature_key
|
|
@@ -248,7 +333,7 @@ module Featurevisor
|
|
|
248
333
|
# @param options [Hash] Override options
|
|
249
334
|
# @return [Hash] Evaluation result
|
|
250
335
|
def evaluate_variable(feature_key, variable_key, context = {}, options = {})
|
|
251
|
-
Featurevisor::Evaluate.
|
|
336
|
+
Featurevisor::Evaluate.evaluate_with_modules(
|
|
252
337
|
get_evaluation_dependencies(context, options).merge(
|
|
253
338
|
type: "variable",
|
|
254
339
|
feature_key: feature_key,
|
|
@@ -417,9 +502,9 @@ module Featurevisor
|
|
|
417
502
|
{
|
|
418
503
|
context: get_context(context),
|
|
419
504
|
logger: @logger,
|
|
420
|
-
|
|
505
|
+
modules_manager: @modules_manager,
|
|
421
506
|
datafile_reader: @datafile_reader,
|
|
422
|
-
sticky: options[:
|
|
507
|
+
sticky: options[:__featurevisor_child_sticky] || @sticky,
|
|
423
508
|
default_variation_value: options[:default_variation_value],
|
|
424
509
|
default_variable_value: options[:default_variable_value]
|
|
425
510
|
}
|
|
@@ -436,11 +521,12 @@ module Featurevisor
|
|
|
436
521
|
when "string"
|
|
437
522
|
value.is_a?(String) ? value : nil
|
|
438
523
|
when "integer"
|
|
439
|
-
|
|
524
|
+
return value if value.is_a?(Integer)
|
|
525
|
+
value.is_a?(Float) && value.finite? && value == value.to_i ? value.to_i : nil
|
|
440
526
|
when "double"
|
|
441
|
-
value.is_a?(
|
|
527
|
+
value.is_a?(Numeric) && value.finite? ? value.to_f : nil
|
|
442
528
|
when "boolean"
|
|
443
|
-
value == true
|
|
529
|
+
value == true || value == false ? value : nil
|
|
444
530
|
when "array"
|
|
445
531
|
value.is_a?(Array) ? value : nil
|
|
446
532
|
when "object"
|
|
@@ -452,12 +538,112 @@ module Featurevisor
|
|
|
452
538
|
rescue
|
|
453
539
|
nil
|
|
454
540
|
end
|
|
541
|
+
|
|
542
|
+
def merge_datafiles(previous, incoming)
|
|
543
|
+
previous ||= EMPTY_DATAFILE
|
|
544
|
+
incoming ||= EMPTY_DATAFILE
|
|
545
|
+
|
|
546
|
+
{
|
|
547
|
+
schemaVersion: incoming[:schemaVersion],
|
|
548
|
+
revision: incoming[:revision],
|
|
549
|
+
featurevisorVersion: incoming[:featurevisorVersion],
|
|
550
|
+
segments: {
|
|
551
|
+
**(previous[:segments] || {}),
|
|
552
|
+
**(incoming[:segments] || {})
|
|
553
|
+
},
|
|
554
|
+
features: {
|
|
555
|
+
**(previous[:features] || {}),
|
|
556
|
+
**(incoming[:features] || {})
|
|
557
|
+
}
|
|
558
|
+
}.compact
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
def create_module_api(mod)
|
|
562
|
+
instance = self
|
|
563
|
+
{
|
|
564
|
+
get_revision: -> { instance.get_revision },
|
|
565
|
+
on_diagnostic: lambda do |handler, options = {}|
|
|
566
|
+
subscription = {
|
|
567
|
+
id: SecureRandom.uuid,
|
|
568
|
+
module_id: mod.id,
|
|
569
|
+
handler: handler,
|
|
570
|
+
level: options[:level] || options[:log_level] || "info"
|
|
571
|
+
}
|
|
572
|
+
@module_diagnostic_subscriptions << subscription
|
|
573
|
+
-> { @module_diagnostic_subscriptions.reject! { |item| item[:id] == subscription[:id] } }
|
|
574
|
+
end,
|
|
575
|
+
report_diagnostic: ->(diagnostic) { report_diagnostic(diagnostic, mod) }
|
|
576
|
+
}
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
def clear_module_diagnostic_subscriptions(mod)
|
|
580
|
+
@module_diagnostic_subscriptions.reject! { |item| item[:module_id] == mod.id }
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
def report_diagnostic(diagnostic, source_module = nil)
|
|
584
|
+
diagnostic = (diagnostic || {}).dup
|
|
585
|
+
diagnostic[:level] ||= "info"
|
|
586
|
+
diagnostic[:module] = source_module.name if source_module && source_module.name
|
|
587
|
+
details = (diagnostic[:details] || {}).dup
|
|
588
|
+
legacy_module_name = diagnostic.delete(:module_name)
|
|
589
|
+
legacy_original_error = diagnostic.delete(:original_error)
|
|
590
|
+
diagnostic[:moduleName] = legacy_module_name if !diagnostic.key?(:moduleName) && !legacy_module_name.nil?
|
|
591
|
+
diagnostic[:originalError] = legacy_original_error if !diagnostic.key?(:originalError) && !legacy_original_error.nil?
|
|
592
|
+
diagnostic.each do |key, value|
|
|
593
|
+
next if %i[level code message module moduleName originalError details].include?(key)
|
|
594
|
+
|
|
595
|
+
details[key] = value
|
|
596
|
+
end
|
|
597
|
+
diagnostic.select! { |key, _| %i[level code message module moduleName originalError details].include?(key) }
|
|
598
|
+
diagnostic[:details] = details
|
|
599
|
+
|
|
600
|
+
@module_diagnostic_subscriptions.dup.each do |subscription|
|
|
601
|
+
next if source_module && subscription[:module_id] == source_module.id
|
|
602
|
+
next unless should_report_diagnostic?(diagnostic[:level], subscription[:level])
|
|
603
|
+
|
|
604
|
+
begin
|
|
605
|
+
subscription[:handler].call(diagnostic)
|
|
606
|
+
rescue => e
|
|
607
|
+
Kernel.warn("[Featurevisor] Diagnostic handler failed: #{e}")
|
|
608
|
+
end
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
if @on_diagnostic
|
|
612
|
+
if should_report_diagnostic?(diagnostic[:level], @logger.level)
|
|
613
|
+
begin
|
|
614
|
+
@on_diagnostic.call(diagnostic)
|
|
615
|
+
rescue => e
|
|
616
|
+
Kernel.warn("[Featurevisor] Diagnostic handler failed: #{e}")
|
|
617
|
+
end
|
|
618
|
+
end
|
|
619
|
+
else
|
|
620
|
+
Logger.new(level: @logger.level).log(
|
|
621
|
+
diagnostic[:level],
|
|
622
|
+
diagnostic[:message],
|
|
623
|
+
diagnostic
|
|
624
|
+
)
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
if diagnostic[:level] == "error"
|
|
628
|
+
@emitter.trigger("error", diagnostic: diagnostic)
|
|
629
|
+
end
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
def should_report_diagnostic?(diagnostic_level, subscriber_level)
|
|
633
|
+
levels = Featurevisor::LOG_LEVELS
|
|
634
|
+
diagnostic_index = levels.index(diagnostic_level || "info")
|
|
635
|
+
subscriber_index = levels.index(subscriber_level || "info")
|
|
636
|
+
|
|
637
|
+
return false if diagnostic_index.nil? || subscriber_index.nil?
|
|
638
|
+
|
|
639
|
+
subscriber_index >= diagnostic_index
|
|
640
|
+
end
|
|
455
641
|
end
|
|
456
642
|
|
|
457
643
|
# Create a new Featurevisor instance
|
|
458
644
|
# @param options [Hash] Instance options
|
|
459
645
|
# @return [Instance] New instance
|
|
460
|
-
def self.
|
|
646
|
+
def self.create_featurevisor(options = {})
|
|
461
647
|
Instance.new(options)
|
|
462
648
|
end
|
|
463
649
|
end
|
data/lib/featurevisor/logger.rb
CHANGED
|
@@ -113,13 +113,6 @@ module Featurevisor
|
|
|
113
113
|
end
|
|
114
114
|
end
|
|
115
115
|
|
|
116
|
-
# Create a new logger instance
|
|
117
|
-
# @param options [Hash] Logger options
|
|
118
|
-
# @return [Logger] New logger instance
|
|
119
|
-
def self.create_logger(options = {})
|
|
120
|
-
Logger.new(options)
|
|
121
|
-
end
|
|
122
|
-
|
|
123
116
|
# Default log handler function
|
|
124
117
|
# @param level [String] Log level
|
|
125
118
|
# @param message [String] Log message
|