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
data/bin/commands/benchmark.rb
CHANGED
|
@@ -27,29 +27,32 @@ module FeaturevisorCLI
|
|
|
27
27
|
exit 1
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
puts ""
|
|
31
|
-
puts "Running benchmark for feature \"#{@options.feature}\"..."
|
|
32
|
-
puts ""
|
|
33
|
-
|
|
34
|
-
# Parse context if provided
|
|
35
30
|
context = parse_context
|
|
31
|
+
targets = resolve_targets
|
|
32
|
+
(targets.empty? ? [nil] : targets).each { |target| run_for_target(context, target) }
|
|
33
|
+
end
|
|
36
34
|
|
|
37
|
-
|
|
38
|
-
datafile_build_start = Time.now
|
|
35
|
+
private
|
|
39
36
|
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
def run_for_target(context, target)
|
|
38
|
+
datafile_build_start = Time.now
|
|
39
|
+
datafile = build_datafile(@options.environment, target)
|
|
42
40
|
datafile_build_duration = Time.now - datafile_build_start
|
|
43
41
|
datafile_build_duration_ms = (datafile_build_duration * 1000).round
|
|
44
42
|
|
|
45
|
-
puts "
|
|
43
|
+
puts "\nBenchmark Featurevisor feature"
|
|
44
|
+
puts " Feature: #{@options.feature}"
|
|
45
|
+
puts " Environment: #{@options.environment}"
|
|
46
|
+
puts " Target: #{target}" if target
|
|
47
|
+
puts " Iterations: #{@options.n}"
|
|
48
|
+
puts " Build duration: #{datafile_build_duration_ms}ms"
|
|
46
49
|
|
|
47
50
|
# Calculate datafile size
|
|
48
51
|
datafile_size = datafile.to_json.bytesize
|
|
49
52
|
puts "Datafile size: #{(datafile_size / 1024.0).round(2)} kB"
|
|
50
53
|
|
|
51
54
|
# Create SDK instance with the datafile
|
|
52
|
-
instance =
|
|
55
|
+
instance = create_featurevisor(datafile)
|
|
53
56
|
puts "...SDK initialized"
|
|
54
57
|
|
|
55
58
|
puts ""
|
|
@@ -73,10 +76,26 @@ module FeaturevisorCLI
|
|
|
73
76
|
value_output = format_value(output[:value])
|
|
74
77
|
puts "Evaluated value : #{value_output}"
|
|
75
78
|
puts "Total duration : #{pretty_duration(output[:duration])}"
|
|
76
|
-
puts "
|
|
79
|
+
puts "Minimum duration: #{format_duration_ms(output[:min_duration])}"
|
|
80
|
+
puts "Average duration: #{format_duration_ms(output[:average_duration])}"
|
|
81
|
+
puts "Maximum duration: #{format_duration_ms(output[:max_duration])}"
|
|
77
82
|
end
|
|
78
83
|
|
|
79
|
-
|
|
84
|
+
def resolve_targets
|
|
85
|
+
return [] if @options.targets.empty?
|
|
86
|
+
stdout, stderr, status = Open3.capture3("npx", "featurevisor", "list", "--targets", "--json", chdir: @project_path)
|
|
87
|
+
unless status.success?
|
|
88
|
+
puts stderr
|
|
89
|
+
exit 1
|
|
90
|
+
end
|
|
91
|
+
available = JSON.parse(stdout).map { |target| target.is_a?(Hash) ? (target["key"] || target["name"]) : target }
|
|
92
|
+
unknown = @options.targets.find { |target| !available.include?(target) }
|
|
93
|
+
if unknown
|
|
94
|
+
puts "Unknown target \"#{unknown}\". Available targets: #{available.empty? ? "none" : available.join(", ")}."
|
|
95
|
+
exit 1
|
|
96
|
+
end
|
|
97
|
+
@options.targets
|
|
98
|
+
end
|
|
80
99
|
|
|
81
100
|
def parse_context
|
|
82
101
|
if @options.context
|
|
@@ -93,24 +112,15 @@ module FeaturevisorCLI
|
|
|
93
112
|
end
|
|
94
113
|
end
|
|
95
114
|
|
|
96
|
-
def build_datafile(environment)
|
|
115
|
+
def build_datafile(environment, target = nil)
|
|
97
116
|
puts "Building datafile for environment: #{environment}..."
|
|
98
117
|
|
|
99
|
-
# Build the command similar to Go implementation
|
|
100
118
|
command_parts = ["cd", @project_path, "&&", "npx", "featurevisor", "build", "--environment=#{environment}", "--json"]
|
|
101
|
-
|
|
102
|
-
# Add schema version if specified
|
|
103
|
-
if @options.schema_version && !@options.schema_version.empty?
|
|
104
|
-
command_parts = ["cd", @project_path, "&&", "npx", "featurevisor", "build", "--environment=#{environment}", "--schemaVersion=#{@options.schema_version}", "--json"]
|
|
105
|
-
end
|
|
119
|
+
command_parts << "--target=#{target}" if target
|
|
106
120
|
|
|
107
121
|
# Add inflate if specified
|
|
108
122
|
if @options.inflate && @options.inflate > 0
|
|
109
|
-
|
|
110
|
-
command_parts = ["cd", @project_path, "&&", "npx", "featurevisor", "build", "--environment=#{environment}", "--schemaVersion=#{@options.schema_version}", "--inflate=#{@options.inflate}", "--json"]
|
|
111
|
-
else
|
|
112
|
-
command_parts = ["cd", @project_path, "&&", "npx", "featurevisor", "build", "--environment=#{environment}", "--inflate=#{@options.inflate}", "--json"]
|
|
113
|
-
end
|
|
123
|
+
command_parts << "--inflate=#{@options.inflate}"
|
|
114
124
|
end
|
|
115
125
|
|
|
116
126
|
command = command_parts.join(" ")
|
|
@@ -142,9 +152,9 @@ module FeaturevisorCLI
|
|
|
142
152
|
stdout
|
|
143
153
|
end
|
|
144
154
|
|
|
145
|
-
def
|
|
155
|
+
def create_featurevisor(datafile)
|
|
146
156
|
# Create a real Featurevisor instance
|
|
147
|
-
instance = Featurevisor.
|
|
157
|
+
instance = Featurevisor.create_featurevisor(
|
|
148
158
|
log_level: get_logger_level
|
|
149
159
|
)
|
|
150
160
|
|
|
@@ -164,61 +174,53 @@ module FeaturevisorCLI
|
|
|
164
174
|
end
|
|
165
175
|
end
|
|
166
176
|
|
|
167
|
-
def
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
177
|
+
def benchmark_evaluation(n)
|
|
178
|
+
value = nil
|
|
179
|
+
total_duration_ns = 0
|
|
180
|
+
min_duration_ns = nil
|
|
181
|
+
max_duration_ns = 0
|
|
172
182
|
|
|
173
|
-
# Benchmark the evaluation
|
|
174
183
|
n.times do
|
|
175
|
-
|
|
184
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
|
|
185
|
+
value = yield
|
|
186
|
+
duration_ns = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond) - start_time
|
|
187
|
+
|
|
188
|
+
total_duration_ns += duration_ns
|
|
189
|
+
min_duration_ns = duration_ns if min_duration_ns.nil? || duration_ns < min_duration_ns
|
|
190
|
+
max_duration_ns = duration_ns if duration_ns > max_duration_ns
|
|
176
191
|
end
|
|
177
192
|
|
|
178
|
-
duration =
|
|
193
|
+
duration = total_duration_ns / 1_000_000_000.0
|
|
179
194
|
|
|
180
195
|
{
|
|
181
196
|
value: value,
|
|
182
|
-
duration: duration
|
|
197
|
+
duration: duration,
|
|
198
|
+
min_duration: (min_duration_ns || 0) / 1_000_000_000.0,
|
|
199
|
+
average_duration: duration / n,
|
|
200
|
+
max_duration: max_duration_ns / 1_000_000_000.0
|
|
183
201
|
}
|
|
184
202
|
end
|
|
185
203
|
|
|
186
|
-
def
|
|
187
|
-
|
|
204
|
+
def format_duration_ms(duration)
|
|
205
|
+
format("%.6fms", duration * 1000)
|
|
206
|
+
end
|
|
188
207
|
|
|
189
|
-
|
|
190
|
-
|
|
208
|
+
def benchmark_feature_flag(instance, feature_key, context, n)
|
|
209
|
+
benchmark_evaluation(n) do
|
|
210
|
+
instance.is_enabled(feature_key, context)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
191
213
|
|
|
192
|
-
|
|
193
|
-
n
|
|
214
|
+
def benchmark_feature_variation(instance, feature_key, context, n)
|
|
215
|
+
benchmark_evaluation(n) do
|
|
194
216
|
instance.get_variation(feature_key, context)
|
|
195
217
|
end
|
|
196
|
-
|
|
197
|
-
duration = Time.now - start_time
|
|
198
|
-
|
|
199
|
-
{
|
|
200
|
-
value: value,
|
|
201
|
-
duration: duration
|
|
202
|
-
}
|
|
203
218
|
end
|
|
204
219
|
|
|
205
220
|
def benchmark_feature_variable(instance, feature_key, variable_key, context, n)
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
# Get the actual variable value from the SDK
|
|
209
|
-
value = instance.get_variable(feature_key, variable_key, context)
|
|
210
|
-
|
|
211
|
-
# Benchmark the evaluation
|
|
212
|
-
n.times do
|
|
221
|
+
benchmark_evaluation(n) do
|
|
213
222
|
instance.get_variable(feature_key, variable_key, context)
|
|
214
223
|
end
|
|
215
|
-
|
|
216
|
-
duration = Time.now - start_time
|
|
217
|
-
|
|
218
|
-
{
|
|
219
|
-
value: value,
|
|
220
|
-
duration: duration
|
|
221
|
-
}
|
|
222
224
|
end
|
|
223
225
|
|
|
224
226
|
def format_value(value)
|
data/bin/commands/test.rb
CHANGED
|
@@ -21,16 +21,17 @@ module FeaturevisorCLI
|
|
|
21
21
|
# Get project configuration
|
|
22
22
|
config = get_config
|
|
23
23
|
environments = get_environments(config)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
schema_version = config[:schemaVersion]
|
|
24
|
+
available_targets = get_targets
|
|
25
|
+
unknown_target = @options.targets.find { |target| !available_targets.include?(target) }
|
|
26
|
+
if unknown_target
|
|
27
|
+
puts "Unknown target \"#{unknown_target}\". Available targets: #{available_targets.empty? ? "none" : available_targets.join(", ")}."
|
|
28
|
+
exit 1
|
|
30
29
|
end
|
|
30
|
+
targets = @options.targets.empty? ? available_targets : @options.targets
|
|
31
|
+
segments_by_key = get_segments
|
|
31
32
|
|
|
32
|
-
# Build datafiles for all environments
|
|
33
|
-
datafiles_by_key = build_datafiles(
|
|
33
|
+
# Build base and Target datafiles for all environments.
|
|
34
|
+
datafiles_by_key = build_datafiles(environments, targets, @options.inflate)
|
|
34
35
|
|
|
35
36
|
puts ""
|
|
36
37
|
|
|
@@ -84,6 +85,34 @@ module FeaturevisorCLI
|
|
|
84
85
|
end
|
|
85
86
|
end
|
|
86
87
|
|
|
88
|
+
def get_targets
|
|
89
|
+
puts "Getting targets..."
|
|
90
|
+
command = "(cd #{Shellwords.escape(@project_path)} && npx featurevisor list --targets --json)"
|
|
91
|
+
targets_output = execute_command(command)
|
|
92
|
+
|
|
93
|
+
begin
|
|
94
|
+
targets = JSON.parse(targets_output, symbolize_names: true)
|
|
95
|
+
|
|
96
|
+
if targets.is_a?(Array)
|
|
97
|
+
targets.filter_map do |target|
|
|
98
|
+
if target.is_a?(Hash)
|
|
99
|
+
target[:name] || target[:key]
|
|
100
|
+
else
|
|
101
|
+
target
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
elsif targets.is_a?(Hash)
|
|
105
|
+
targets.keys
|
|
106
|
+
else
|
|
107
|
+
[]
|
|
108
|
+
end
|
|
109
|
+
rescue JSON::ParserError => e
|
|
110
|
+
puts "Error: Failed to parse targets JSON: #{e.message}"
|
|
111
|
+
puts "Command output: #{targets_output}"
|
|
112
|
+
exit 1
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
87
116
|
def get_environments(config)
|
|
88
117
|
environments = config[:environments]
|
|
89
118
|
|
|
@@ -97,23 +126,15 @@ module FeaturevisorCLI
|
|
|
97
126
|
environment == false ? false : environment
|
|
98
127
|
end
|
|
99
128
|
|
|
100
|
-
def
|
|
101
|
-
if environment == false
|
|
102
|
-
"scope-#{scope_name}"
|
|
103
|
-
else
|
|
104
|
-
"#{environment}-scope-#{scope_name}"
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
def tagged_datafile_key(environment, tag)
|
|
129
|
+
def target_datafile_key(environment, target)
|
|
109
130
|
if environment == false
|
|
110
|
-
"
|
|
131
|
+
"false-target-#{target}"
|
|
111
132
|
else
|
|
112
|
-
"#{environment}-
|
|
133
|
+
"#{environment}-target-#{target}"
|
|
113
134
|
end
|
|
114
135
|
end
|
|
115
136
|
|
|
116
|
-
def build_datafiles(
|
|
137
|
+
def build_datafiles(environments, targets, inflate)
|
|
117
138
|
datafiles_by_key = {}
|
|
118
139
|
|
|
119
140
|
environments.each do |environment|
|
|
@@ -122,61 +143,35 @@ module FeaturevisorCLI
|
|
|
122
143
|
|
|
123
144
|
datafiles_by_key[base_datafile_key(environment)] = build_single_datafile(
|
|
124
145
|
environment: environment,
|
|
125
|
-
schema_version: schema_version,
|
|
126
146
|
inflate: inflate
|
|
127
147
|
)
|
|
128
148
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
schema_version: schema_version,
|
|
137
|
-
inflate: inflate,
|
|
138
|
-
scope: scope[:name]
|
|
139
|
-
)
|
|
140
|
-
end
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
if @options.with_tags && config[:tags].is_a?(Array)
|
|
144
|
-
config[:tags].each do |tag|
|
|
145
|
-
puts "Building tagged datafile for tag: #{tag}..."
|
|
146
|
-
datafiles_by_key[tagged_datafile_key(environment, tag)] = build_single_datafile(
|
|
147
|
-
environment: environment,
|
|
148
|
-
schema_version: schema_version,
|
|
149
|
-
inflate: inflate,
|
|
150
|
-
tag: tag
|
|
151
|
-
)
|
|
152
|
-
end
|
|
149
|
+
targets.each do |target|
|
|
150
|
+
puts "Building datafile for target: #{target}..."
|
|
151
|
+
datafiles_by_key[target_datafile_key(environment, target)] = build_single_datafile(
|
|
152
|
+
environment: environment,
|
|
153
|
+
inflate: inflate,
|
|
154
|
+
target: target
|
|
155
|
+
)
|
|
153
156
|
end
|
|
154
157
|
end
|
|
155
158
|
|
|
156
159
|
datafiles_by_key
|
|
157
160
|
end
|
|
158
161
|
|
|
159
|
-
def build_single_datafile(environment:,
|
|
162
|
+
def build_single_datafile(environment:, inflate:, target: nil)
|
|
160
163
|
command_parts = ["npx", "featurevisor", "build", "--json"]
|
|
161
164
|
|
|
162
165
|
if environment != false && !environment.nil?
|
|
163
166
|
command_parts << "--environment=#{Shellwords.escape(environment.to_s)}"
|
|
164
167
|
end
|
|
165
168
|
|
|
166
|
-
if schema_version && !schema_version.empty?
|
|
167
|
-
command_parts << "--schemaVersion=#{Shellwords.escape(schema_version.to_s)}"
|
|
168
|
-
end
|
|
169
|
-
|
|
170
169
|
if inflate && inflate > 0
|
|
171
170
|
command_parts << "--inflate=#{inflate}"
|
|
172
171
|
end
|
|
173
172
|
|
|
174
|
-
if
|
|
175
|
-
command_parts << "--
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
if tag
|
|
179
|
-
command_parts << "--tag=#{Shellwords.escape(tag.to_s)}"
|
|
173
|
+
if target
|
|
174
|
+
command_parts << "--target=#{Shellwords.escape(target.to_s)}"
|
|
180
175
|
end
|
|
181
176
|
|
|
182
177
|
command = "(cd #{Shellwords.escape(@project_path)} && #{command_parts.join(' ')})"
|
|
@@ -224,29 +219,15 @@ module FeaturevisorCLI
|
|
|
224
219
|
end
|
|
225
220
|
end
|
|
226
221
|
|
|
227
|
-
def get_scope_context(config, scope_name)
|
|
228
|
-
return {} unless scope_name && config[:scopes].is_a?(Array)
|
|
229
|
-
|
|
230
|
-
scope = config[:scopes].find { |s| s[:name] == scope_name }
|
|
231
|
-
return {} unless scope && scope[:context].is_a?(Hash)
|
|
232
|
-
|
|
233
|
-
parse_context(scope[:context])
|
|
234
|
-
end
|
|
235
|
-
|
|
236
222
|
def resolve_datafile_for_assertion(assertion, datafiles_by_key)
|
|
237
223
|
environment = assertion.key?(:environment) ? assertion[:environment] : false
|
|
238
224
|
environment = false if environment.nil?
|
|
239
225
|
|
|
240
|
-
|
|
241
|
-
tagged_key = assertion[:tag] ? tagged_datafile_key(environment, assertion[:tag]) : nil
|
|
226
|
+
target_key = assertion[:target] ? target_datafile_key(environment, assertion[:target]) : nil
|
|
242
227
|
base_key = base_datafile_key(environment)
|
|
243
228
|
|
|
244
|
-
if
|
|
245
|
-
return datafiles_by_key[
|
|
246
|
-
end
|
|
247
|
-
|
|
248
|
-
if tagged_key && datafiles_by_key.key?(tagged_key)
|
|
249
|
-
return datafiles_by_key[tagged_key]
|
|
229
|
+
if target_key && datafiles_by_key.key?(target_key)
|
|
230
|
+
return datafiles_by_key[target_key]
|
|
250
231
|
end
|
|
251
232
|
|
|
252
233
|
datafiles_by_key[base_key]
|
|
@@ -255,19 +236,19 @@ module FeaturevisorCLI
|
|
|
255
236
|
def create_tester_instance(datafile, level, assertion)
|
|
256
237
|
sticky = parse_sticky(assertion[:sticky])
|
|
257
238
|
|
|
258
|
-
Featurevisor.
|
|
239
|
+
Featurevisor.create_featurevisor(
|
|
259
240
|
datafile: datafile,
|
|
260
241
|
sticky: sticky,
|
|
261
242
|
log_level: level,
|
|
262
|
-
|
|
243
|
+
modules: [
|
|
263
244
|
{
|
|
264
|
-
name: "
|
|
245
|
+
name: "test-module",
|
|
265
246
|
bucket_value: ->(options) do
|
|
266
247
|
at = assertion[:at]
|
|
267
248
|
if at.is_a?(Numeric)
|
|
268
249
|
(at * 1000).to_i
|
|
269
250
|
else
|
|
270
|
-
options
|
|
251
|
+
options[:bucket_value]
|
|
271
252
|
end
|
|
272
253
|
end
|
|
273
254
|
}
|
|
@@ -284,6 +265,12 @@ module FeaturevisorCLI
|
|
|
284
265
|
tests.each do |test|
|
|
285
266
|
test_key = test[:key]
|
|
286
267
|
assertions = test[:assertions] || []
|
|
268
|
+
if test[:feature] && !@options.targets.empty?
|
|
269
|
+
assertions = assertions.select do |assertion|
|
|
270
|
+
assertion[:target].nil? || @options.targets.include?(assertion[:target])
|
|
271
|
+
end
|
|
272
|
+
next if assertions.empty?
|
|
273
|
+
end
|
|
287
274
|
results = ""
|
|
288
275
|
test_has_error = false
|
|
289
276
|
test_duration = 0.0
|
|
@@ -297,19 +284,13 @@ module FeaturevisorCLI
|
|
|
297
284
|
if datafile.nil?
|
|
298
285
|
test_result = {
|
|
299
286
|
has_error: true,
|
|
300
|
-
errors: " ✘ no datafile found for assertion
|
|
287
|
+
errors: " ✘ no datafile found for assertion target/environment combination\n",
|
|
301
288
|
duration: 0
|
|
302
289
|
}
|
|
303
290
|
end
|
|
304
291
|
|
|
305
292
|
if datafile
|
|
306
293
|
instance = create_tester_instance(datafile, level, assertion)
|
|
307
|
-
scope_context = {}
|
|
308
|
-
|
|
309
|
-
if assertion[:scope] && !@options.with_scopes
|
|
310
|
-
# If not using scoped datafiles, mimic JS behavior by merging scope context.
|
|
311
|
-
scope_context = get_scope_context(config, assertion[:scope])
|
|
312
|
-
end
|
|
313
294
|
|
|
314
295
|
# Show datafile if requested
|
|
315
296
|
if @options.show_datafile
|
|
@@ -318,7 +299,7 @@ module FeaturevisorCLI
|
|
|
318
299
|
puts ""
|
|
319
300
|
end
|
|
320
301
|
|
|
321
|
-
test_result = run_test_feature(assertion, test[:feature], instance, level
|
|
302
|
+
test_result = run_test_feature(assertion, test[:feature], instance, level)
|
|
322
303
|
end
|
|
323
304
|
elsif test[:segment]
|
|
324
305
|
segment_key = test[:segment]
|
|
@@ -366,9 +347,8 @@ module FeaturevisorCLI
|
|
|
366
347
|
end
|
|
367
348
|
end
|
|
368
349
|
|
|
369
|
-
def run_test_feature(assertion, feature_key, instance, level
|
|
350
|
+
def run_test_feature(assertion, feature_key, instance, level)
|
|
370
351
|
context = parse_context(assertion[:context])
|
|
371
|
-
context = { **scope_context, **context } if scope_context && !scope_context.empty?
|
|
372
352
|
sticky = parse_sticky(assertion[:sticky])
|
|
373
353
|
|
|
374
354
|
# Set context and sticky for this assertion
|
|
@@ -680,7 +660,7 @@ module FeaturevisorCLI
|
|
|
680
660
|
}
|
|
681
661
|
|
|
682
662
|
# Create SDK instance for segment testing
|
|
683
|
-
instance = Featurevisor.
|
|
663
|
+
instance = Featurevisor.create_featurevisor(
|
|
684
664
|
datafile: datafile,
|
|
685
665
|
log_level: level
|
|
686
666
|
)
|
|
@@ -768,16 +748,14 @@ module FeaturevisorCLI
|
|
|
768
748
|
return instance.send(method_name, feature_key, variable_key, context, override_options)
|
|
769
749
|
end
|
|
770
750
|
|
|
771
|
-
if instance.
|
|
772
|
-
parent = instance.parent
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
context
|
|
777
|
-
end
|
|
751
|
+
if instance.is_a?(Featurevisor::ChildInstance)
|
|
752
|
+
parent = instance.instance_variable_get(:@parent)
|
|
753
|
+
child_context = instance.instance_variable_get(:@context) || {}
|
|
754
|
+
child_sticky = instance.instance_variable_get(:@sticky)
|
|
755
|
+
combined_context = { **child_context, **context }
|
|
778
756
|
|
|
779
757
|
combined_options = {
|
|
780
|
-
|
|
758
|
+
__featurevisor_child_sticky: child_sticky,
|
|
781
759
|
**override_options
|
|
782
760
|
}
|
|
783
761
|
|
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
module Featurevisor
|
|
4
4
|
# Child instance class for managing child contexts and sticky features
|
|
5
5
|
class ChildInstance
|
|
6
|
-
attr_reader :parent, :context, :sticky, :emitter
|
|
7
|
-
|
|
8
6
|
# Initialize a new child instance
|
|
9
7
|
# @param options [Hash] Child instance options
|
|
10
8
|
# @option options [Instance] :parent Parent instance
|
|
@@ -94,8 +92,8 @@ module Featurevisor
|
|
|
94
92
|
**context
|
|
95
93
|
},
|
|
96
94
|
{
|
|
97
|
-
|
|
98
|
-
|
|
95
|
+
**options,
|
|
96
|
+
__featurevisor_child_sticky: @sticky
|
|
99
97
|
}
|
|
100
98
|
)
|
|
101
99
|
end
|
|
@@ -113,8 +111,8 @@ module Featurevisor
|
|
|
113
111
|
**context
|
|
114
112
|
},
|
|
115
113
|
{
|
|
116
|
-
|
|
117
|
-
|
|
114
|
+
**options,
|
|
115
|
+
__featurevisor_child_sticky: @sticky
|
|
118
116
|
}
|
|
119
117
|
)
|
|
120
118
|
end
|
|
@@ -134,8 +132,8 @@ module Featurevisor
|
|
|
134
132
|
**context
|
|
135
133
|
},
|
|
136
134
|
{
|
|
137
|
-
|
|
138
|
-
|
|
135
|
+
**options,
|
|
136
|
+
__featurevisor_child_sticky: @sticky
|
|
139
137
|
}
|
|
140
138
|
)
|
|
141
139
|
end
|
|
@@ -155,8 +153,8 @@ module Featurevisor
|
|
|
155
153
|
**context
|
|
156
154
|
},
|
|
157
155
|
{
|
|
158
|
-
|
|
159
|
-
|
|
156
|
+
**options,
|
|
157
|
+
__featurevisor_child_sticky: @sticky
|
|
160
158
|
}
|
|
161
159
|
)
|
|
162
160
|
end
|
|
@@ -176,8 +174,8 @@ module Featurevisor
|
|
|
176
174
|
**context
|
|
177
175
|
},
|
|
178
176
|
{
|
|
179
|
-
|
|
180
|
-
|
|
177
|
+
**options,
|
|
178
|
+
__featurevisor_child_sticky: @sticky
|
|
181
179
|
}
|
|
182
180
|
)
|
|
183
181
|
end
|
|
@@ -197,8 +195,8 @@ module Featurevisor
|
|
|
197
195
|
**context
|
|
198
196
|
},
|
|
199
197
|
{
|
|
200
|
-
|
|
201
|
-
|
|
198
|
+
**options,
|
|
199
|
+
__featurevisor_child_sticky: @sticky
|
|
202
200
|
}
|
|
203
201
|
)
|
|
204
202
|
end
|
|
@@ -218,8 +216,8 @@ module Featurevisor
|
|
|
218
216
|
**context
|
|
219
217
|
},
|
|
220
218
|
{
|
|
221
|
-
|
|
222
|
-
|
|
219
|
+
**options,
|
|
220
|
+
__featurevisor_child_sticky: @sticky
|
|
223
221
|
}
|
|
224
222
|
)
|
|
225
223
|
end
|
|
@@ -239,8 +237,8 @@ module Featurevisor
|
|
|
239
237
|
**context
|
|
240
238
|
},
|
|
241
239
|
{
|
|
242
|
-
|
|
243
|
-
|
|
240
|
+
**options,
|
|
241
|
+
__featurevisor_child_sticky: @sticky
|
|
244
242
|
}
|
|
245
243
|
)
|
|
246
244
|
end
|
|
@@ -260,8 +258,8 @@ module Featurevisor
|
|
|
260
258
|
**context
|
|
261
259
|
},
|
|
262
260
|
{
|
|
263
|
-
|
|
264
|
-
|
|
261
|
+
**options,
|
|
262
|
+
__featurevisor_child_sticky: @sticky
|
|
265
263
|
}
|
|
266
264
|
)
|
|
267
265
|
end
|
|
@@ -281,8 +279,8 @@ module Featurevisor
|
|
|
281
279
|
**context
|
|
282
280
|
},
|
|
283
281
|
{
|
|
284
|
-
|
|
285
|
-
|
|
282
|
+
**options,
|
|
283
|
+
__featurevisor_child_sticky: @sticky
|
|
286
284
|
}
|
|
287
285
|
)
|
|
288
286
|
end
|
|
@@ -300,8 +298,8 @@ module Featurevisor
|
|
|
300
298
|
},
|
|
301
299
|
feature_keys,
|
|
302
300
|
{
|
|
303
|
-
|
|
304
|
-
|
|
301
|
+
**options,
|
|
302
|
+
__featurevisor_child_sticky: @sticky
|
|
305
303
|
}
|
|
306
304
|
)
|
|
307
305
|
end
|