featurevisor 0.1.1

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.
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Featurevisor
4
+ # Events module for generating event parameters
5
+ module Events
6
+ # Get parameters for sticky set event
7
+ # @param previous_sticky [Hash] Previous sticky features
8
+ # @param new_sticky [Hash] New sticky features
9
+ # @param replace [Boolean] Whether features were replaced
10
+ # @return [Hash] Event parameters
11
+ def self.get_params_for_sticky_set_event(previous_sticky = {}, new_sticky = {}, replace = false)
12
+ keys_before = previous_sticky.keys
13
+ keys_after = new_sticky.keys
14
+
15
+ all_keys = (keys_before + keys_after).uniq
16
+
17
+ {
18
+ features: all_keys,
19
+ replaced: replace
20
+ }
21
+ end
22
+
23
+ # Get parameters for datafile set event
24
+ # @param previous_reader [DatafileReader] Previous datafile reader
25
+ # @param new_reader [DatafileReader] New datafile reader
26
+ # @return [Hash] Event parameters
27
+ def self.get_params_for_datafile_set_event(previous_reader, new_reader)
28
+ previous_revision = previous_reader.get_revision
29
+ previous_feature_keys = previous_reader.get_feature_keys
30
+
31
+ new_revision = new_reader.get_revision
32
+ new_feature_keys = new_reader.get_feature_keys
33
+
34
+ # results
35
+ removed_features = []
36
+ changed_features = []
37
+ added_features = []
38
+
39
+ # checking against existing datafile
40
+ previous_feature_keys.each do |previous_feature_key|
41
+ if !new_feature_keys.include?(previous_feature_key)
42
+ # feature was removed in new datafile
43
+ removed_features << previous_feature_key
44
+ next
45
+ end
46
+
47
+ # feature exists in both datafiles, check if it was changed
48
+ previous_feature = previous_reader.get_feature(previous_feature_key)
49
+ new_feature = new_reader.get_feature(previous_feature_key)
50
+
51
+ if previous_feature && new_feature && previous_feature[:hash] != new_feature[:hash]
52
+ # feature was changed in new datafile
53
+ changed_features << previous_feature_key
54
+ end
55
+ end
56
+
57
+ # checking against new datafile
58
+ new_feature_keys.each do |new_feature_key|
59
+ if !previous_feature_keys.include?(new_feature_key)
60
+ # feature was added in new datafile
61
+ added_features << new_feature_key
62
+ end
63
+ end
64
+
65
+ # combine all affected feature keys
66
+ all_affected_features = (removed_features + changed_features + added_features).uniq
67
+
68
+ {
69
+ revision: new_revision,
70
+ previous_revision: previous_revision,
71
+ revision_changed: previous_revision != new_revision,
72
+ features: all_affected_features
73
+ }
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Featurevisor
4
+ # Hooks module for extending evaluation behavior
5
+ module Hooks
6
+ # Hook interface for extending evaluation behavior
7
+ class Hook
8
+ attr_reader :name
9
+
10
+ # Initialize a new hook
11
+ # @param options [Hash] Hook options
12
+ # @option options [String] :name Hook name
13
+ # @option options [Proc, nil] :before Before evaluation hook
14
+ # @option options [Proc, nil] :bucket_key Bucket key configuration hook
15
+ # @option options [Proc, nil] :bucket_value Bucket value configuration hook
16
+ # @option options [Proc, nil] :after After evaluation hook
17
+ def initialize(options)
18
+ @name = options[:name]
19
+ @before = options[:before]
20
+ @bucket_key = options[:bucket_key]
21
+ @bucket_value = options[:bucket_value]
22
+ @after = options[:after]
23
+ end
24
+
25
+ # Call the before hook if defined
26
+ # @param options [Hash] Evaluation options
27
+ # @return [Hash] Modified evaluation options
28
+ def call_before(options)
29
+ return options unless @before
30
+
31
+ @before.call(options)
32
+ end
33
+
34
+ # Call the bucket key hook if defined
35
+ # @param options [Hash] Bucket key options
36
+ # @return [String] Modified bucket key
37
+ def call_bucket_key(options)
38
+ return options[:bucket_key] unless @bucket_key
39
+
40
+ @bucket_key.call(options)
41
+ end
42
+
43
+ # Call the bucket value hook if defined
44
+ # @param options [Hash] Bucket value options
45
+ # @return [Integer] Modified bucket value
46
+ def call_bucket_value(options)
47
+ return options[:bucket_value] unless @bucket_value
48
+
49
+ @bucket_value.call(options)
50
+ end
51
+
52
+ # Call the after hook if defined
53
+ # @param evaluation [Hash] Evaluation result
54
+ # @param options [Hash] Evaluation options
55
+ # @return [Hash] Modified evaluation result
56
+ def call_after(evaluation, options)
57
+ return evaluation unless @after
58
+
59
+ @after.call(evaluation, options)
60
+ end
61
+ end
62
+
63
+ # HooksManager class for managing hooks
64
+ class HooksManager
65
+ attr_reader :hooks, :logger
66
+
67
+ # Initialize a new HooksManager
68
+ # @param options [Hash] Options hash containing hooks and logger
69
+ # @option options [Array<Hook>] :hooks Array of hooks
70
+ # @option options [Logger] :logger Logger instance
71
+ def initialize(options)
72
+ @logger = options[:logger]
73
+ @hooks = []
74
+
75
+ if options[:hooks]
76
+ options[:hooks].each do |hook|
77
+ add(hook)
78
+ end
79
+ end
80
+ end
81
+
82
+ # Add a hook to the manager
83
+ # @param hook [Hook] Hook to add
84
+ # @return [Proc, nil] Remove function or nil if hook already exists
85
+ def add(hook)
86
+ if @hooks.any? { |existing_hook| existing_hook.name == hook.name }
87
+ @logger.error("Hook with name \"#{hook.name}\" already exists.", {
88
+ name: hook.name,
89
+ hook: hook
90
+ })
91
+
92
+ return nil
93
+ end
94
+
95
+ @hooks << hook
96
+
97
+ # Return a remove function
98
+ -> { remove(hook.name) }
99
+ end
100
+
101
+ # Remove a hook by name
102
+ # @param name [String] Hook name to remove
103
+ def remove(name)
104
+ @hooks = @hooks.reject { |hook| hook.name == name }
105
+ end
106
+
107
+ # Get all hooks
108
+ # @return [Array<Hook>] Array of all hooks
109
+ def get_all
110
+ @hooks
111
+ end
112
+
113
+ # Run before hooks
114
+ # @param options [Hash] Evaluation options
115
+ # @return [Hash] Modified evaluation options
116
+ def run_before_hooks(options)
117
+ result = options
118
+ @hooks.each do |hook|
119
+ result = hook.call_before(result)
120
+ end
121
+ result
122
+ end
123
+
124
+ # Run bucket key hooks
125
+ # @param options [Hash] Bucket key options
126
+ # @return [String] Modified bucket key
127
+ def run_bucket_key_hooks(options)
128
+ bucket_key = options[:bucket_key]
129
+ @hooks.each do |hook|
130
+ bucket_key = hook.call_bucket_key(options.merge(bucket_key: bucket_key))
131
+ end
132
+ bucket_key
133
+ end
134
+
135
+ # Run bucket value hooks
136
+ # @param options [Hash] Bucket value options
137
+ # @return [Integer] Modified bucket value
138
+ def run_bucket_value_hooks(options)
139
+ bucket_value = options[:bucket_value]
140
+ @hooks.each do |hook|
141
+ bucket_value = hook.call_bucket_value(options.merge(bucket_value: bucket_value))
142
+ end
143
+ bucket_value
144
+ end
145
+
146
+ # Run after hooks
147
+ # @param evaluation [Hash] Evaluation result
148
+ # @param options [Hash] Evaluation options
149
+ # @return [Hash] Modified evaluation result
150
+ def run_after_hooks(evaluation, options)
151
+ result = evaluation
152
+ @hooks.each do |hook|
153
+ result = hook.call_after(result, options)
154
+ end
155
+ result
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,463 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Featurevisor
6
+ # Instance class for managing feature flag evaluations
7
+ class Instance
8
+ attr_reader :context, :logger, :sticky, :datafile_reader, :hooks_manager, :emitter
9
+
10
+ # Empty datafile template
11
+ EMPTY_DATAFILE = {
12
+ schemaVersion: "2",
13
+ revision: "unknown",
14
+ segments: {},
15
+ features: {}
16
+ }.freeze
17
+
18
+ # Initialize a new Featurevisor instance
19
+ # @param options [Hash] Instance options
20
+ # @option options [Hash, String] :datafile Datafile content or JSON string
21
+ # @option options [Hash] :context Initial context
22
+ # @option options [String] :log_level Log level
23
+ # @option options [Logger] :logger Logger instance
24
+ # @option options [Hash] :sticky Sticky features
25
+ # @option options [Array<Hook>] :hooks Array of hooks
26
+ def initialize(options = {})
27
+ # from options
28
+ @context = options[:context] || {}
29
+ @logger = options[:logger] || Featurevisor.create_logger(level: options[:log_level] || "info")
30
+ @hooks_manager = Featurevisor::Hooks::HooksManager.new(
31
+ hooks: (options[:hooks] || []).map { |hook_data| Featurevisor::Hooks::Hook.new(hook_data) },
32
+ logger: @logger
33
+ )
34
+ @emitter = Featurevisor::Emitter.new
35
+ @sticky = options[:sticky] || {}
36
+
37
+ # datafile
38
+ @datafile_reader = Featurevisor::DatafileReader.new(
39
+ datafile: EMPTY_DATAFILE,
40
+ logger: @logger
41
+ )
42
+
43
+ if options[:datafile]
44
+ @datafile_reader = Featurevisor::DatafileReader.new(
45
+ datafile: options[:datafile].is_a?(String) ? JSON.parse(options[:datafile]) : options[:datafile],
46
+ logger: @logger
47
+ )
48
+ end
49
+
50
+ @logger.info("Featurevisor SDK initialized")
51
+ end
52
+
53
+ # Set the log level
54
+ # @param level [String] Log level
55
+ def set_log_level(level)
56
+ @logger.set_level(level)
57
+ end
58
+
59
+ # Set the datafile
60
+ # @param datafile [Hash, String] Datafile content or JSON string
61
+ def set_datafile(datafile)
62
+ begin
63
+ new_datafile_reader = Featurevisor::DatafileReader.new(
64
+ datafile: datafile.is_a?(String) ? JSON.parse(datafile) : datafile,
65
+ logger: @logger
66
+ )
67
+
68
+ details = Featurevisor::Events.get_params_for_datafile_set_event(@datafile_reader, new_datafile_reader)
69
+ @datafile_reader = new_datafile_reader
70
+
71
+ @logger.info("datafile set", details)
72
+ @emitter.trigger("datafile_set", details)
73
+ rescue => e
74
+ @logger.error("could not parse datafile", { error: e })
75
+ end
76
+ end
77
+
78
+ # Set sticky features
79
+ # @param sticky [Hash] Sticky features
80
+ # @param replace [Boolean] Whether to replace existing sticky features
81
+ def set_sticky(sticky, replace = false)
82
+ previous_sticky_features = @sticky || {}
83
+
84
+ if replace
85
+ @sticky = sticky
86
+ else
87
+ @sticky = {
88
+ **@sticky,
89
+ **sticky
90
+ }
91
+ end
92
+
93
+ params = Featurevisor::Events.get_params_for_sticky_set_event(previous_sticky_features, @sticky, replace)
94
+
95
+ @logger.info("sticky features set", params)
96
+ @emitter.trigger("sticky_set", params)
97
+ end
98
+
99
+ # Get the revision
100
+ # @return [String] Revision string
101
+ def get_revision
102
+ @datafile_reader.get_revision
103
+ end
104
+
105
+ # Get a feature by key
106
+ # @param feature_key [String] Feature key
107
+ # @return [Hash, nil] Feature data or nil if not found
108
+ def get_feature(feature_key)
109
+ @datafile_reader.get_feature(feature_key)
110
+ end
111
+
112
+ # Add a hook
113
+ # @param hook [Hook] Hook to add
114
+ # @return [Proc, nil] Remove function or nil if hook already exists
115
+ def add_hook(hook)
116
+ @hooks_manager.add(hook)
117
+ end
118
+
119
+ # Subscribe to an event
120
+ # @param event_name [String] Event name
121
+ # @param callback [Proc] Callback function
122
+ # @return [Proc] Unsubscribe function
123
+ def on(event_name, callback)
124
+ @emitter.on(event_name, callback)
125
+ end
126
+
127
+ # Close the instance
128
+ def close
129
+ @emitter.clear_all
130
+ end
131
+
132
+ # Set context
133
+ # @param context [Hash] Context to set
134
+ # @param replace [Boolean] Whether to replace existing context
135
+ def set_context(context, replace = false)
136
+ if replace
137
+ @context = context
138
+ else
139
+ @context = { **@context, **context }
140
+ end
141
+
142
+ @emitter.trigger("context_set", {
143
+ context: @context,
144
+ replaced: replace
145
+ })
146
+
147
+ @logger.debug(replace ? "context replaced" : "context updated", {
148
+ context: @context,
149
+ replaced: replace
150
+ })
151
+ end
152
+
153
+ # Get context
154
+ # @param context [Hash, nil] Additional context to merge
155
+ # @return [Hash] Merged context
156
+ def get_context(context = nil)
157
+ if context
158
+ {
159
+ **@context,
160
+ **context
161
+ }
162
+ else
163
+ @context
164
+ end
165
+ end
166
+
167
+ # Spawn a child instance
168
+ # @param context [Hash] Child context
169
+ # @param options [Hash] Override options
170
+ # @return [ChildInstance] Child instance
171
+ def spawn(context = {}, options = {})
172
+ Featurevisor::ChildInstance.new(
173
+ parent: self,
174
+ context: get_context(context),
175
+ sticky: options[:sticky]
176
+ )
177
+ end
178
+
179
+ # Evaluate a flag
180
+ # @param feature_key [String] Feature key
181
+ # @param context [Hash] Context
182
+ # @param options [Hash] Override options
183
+ # @return [Hash] Evaluation result
184
+ def evaluate_flag(feature_key, context = {}, options = {})
185
+ Featurevisor::Evaluate.evaluate_with_hooks(
186
+ get_evaluation_dependencies(context, options).merge(
187
+ type: "flag",
188
+ feature_key: feature_key
189
+ )
190
+ )
191
+ end
192
+
193
+ # Check if a feature is enabled
194
+ # @param feature_key [String] Feature key
195
+ # @param context [Hash] Context
196
+ # @param options [Hash] Override options
197
+ # @return [Boolean] True if feature is enabled
198
+ def is_enabled(feature_key, context = {}, options = {})
199
+ begin
200
+ evaluation = evaluate_flag(feature_key, context, options)
201
+ evaluation[:enabled] == true
202
+ rescue => e
203
+ @logger.error("isEnabled", { feature_key: feature_key, error: e })
204
+ false
205
+ end
206
+ end
207
+
208
+ # Evaluate a variation
209
+ # @param feature_key [String] Feature key
210
+ # @param context [Hash] Context
211
+ # @param options [Hash] Override options
212
+ # @return [Hash] Evaluation result
213
+ def evaluate_variation(feature_key, context = {}, options = {})
214
+ Featurevisor::Evaluate.evaluate_with_hooks(
215
+ get_evaluation_dependencies(context, options).merge(
216
+ type: "variation",
217
+ feature_key: feature_key
218
+ )
219
+ )
220
+ end
221
+
222
+ # Get variation value
223
+ # @param feature_key [String] Feature key
224
+ # @param context [Hash] Context
225
+ # @param options [Hash] Override options
226
+ # @return [String, nil] Variation value or nil
227
+ def get_variation(feature_key, context = {}, options = {})
228
+ begin
229
+ evaluation = evaluate_variation(feature_key, context, options)
230
+
231
+ if evaluation[:variation_value]
232
+ evaluation[:variation_value]
233
+ elsif evaluation[:variation]
234
+ evaluation[:variation][:value]
235
+ else
236
+ nil
237
+ end
238
+ rescue => e
239
+ @logger.error("getVariation", { feature_key: feature_key, error: e })
240
+ nil
241
+ end
242
+ end
243
+
244
+ # Evaluate a variable
245
+ # @param feature_key [String] Feature key
246
+ # @param variable_key [String] Variable key
247
+ # @param context [Hash] Context
248
+ # @param options [Hash] Override options
249
+ # @return [Hash] Evaluation result
250
+ def evaluate_variable(feature_key, variable_key, context = {}, options = {})
251
+ Featurevisor::Evaluate.evaluate_with_hooks(
252
+ get_evaluation_dependencies(context, options).merge(
253
+ type: "variable",
254
+ feature_key: feature_key,
255
+ variable_key: variable_key
256
+ )
257
+ )
258
+ end
259
+
260
+ # Get variable value
261
+ # @param feature_key [String] Feature key
262
+ # @param variable_key [String] Variable key
263
+ # @param context [Hash] Context
264
+ # @param options [Hash] Override options
265
+ # @return [Object, nil] Variable value or nil
266
+ def get_variable(feature_key, variable_key, context = {}, options = {})
267
+ begin
268
+ evaluation = evaluate_variable(feature_key, variable_key, context, options)
269
+
270
+ if !evaluation[:variable_value].nil?
271
+ if evaluation[:variable_schema] &&
272
+ evaluation[:variable_schema][:type] == "json" &&
273
+ evaluation[:variable_value].is_a?(String)
274
+ JSON.parse(evaluation[:variable_value], symbolize_names: true)
275
+ else
276
+ evaluation[:variable_value]
277
+ end
278
+ else
279
+ nil
280
+ end
281
+ rescue => e
282
+ @logger.error("getVariable", { feature_key: feature_key, variable_key: variable_key, error: e })
283
+ nil
284
+ end
285
+ end
286
+
287
+ # Get variable as boolean
288
+ # @param feature_key [String] Feature key
289
+ # @param variable_key [String] Variable key
290
+ # @param context [Hash] Context
291
+ # @param options [Hash] Override options
292
+ # @return [Boolean, nil] Boolean value or nil
293
+ def get_variable_boolean(feature_key, variable_key, context = {}, options = {})
294
+ variable_value = get_variable(feature_key, variable_key, context, options)
295
+ get_value_by_type(variable_value, "boolean")
296
+ end
297
+
298
+ # Get variable as string
299
+ # @param feature_key [String] Feature key
300
+ # @param variable_key [String] Variable key
301
+ # @param context [Hash] Context
302
+ # @param options [Hash] Override options
303
+ # @return [String, nil] String value or nil
304
+ def get_variable_string(feature_key, variable_key, context = {}, options = {})
305
+ variable_value = get_variable(feature_key, variable_key, context, options)
306
+ get_value_by_type(variable_value, "string")
307
+ end
308
+
309
+ # Get variable as integer
310
+ # @param feature_key [String] Feature key
311
+ # @param variable_key [String] Variable key
312
+ # @param context [Hash] Context
313
+ # @param options [Hash] Override options
314
+ # @return [Integer, nil] Integer value or nil
315
+ def get_variable_integer(feature_key, variable_key, context = {}, options = {})
316
+ variable_value = get_variable(feature_key, variable_key, context, options)
317
+ get_value_by_type(variable_value, "integer")
318
+ end
319
+
320
+ # Get variable as double
321
+ # @param feature_key [String] Feature key
322
+ # @param variable_key [String] Variable key
323
+ # @param context [Hash] Context
324
+ # @param options [Hash] Override options
325
+ # @return [Float, nil] Float value or nil
326
+ def get_variable_double(feature_key, variable_key, context = {}, options = {})
327
+ variable_value = get_variable(feature_key, variable_key, context, options)
328
+ get_value_by_type(variable_value, "double")
329
+ end
330
+
331
+ # Get variable as array
332
+ # @param feature_key [String] Feature key
333
+ # @param variable_key [String] Variable key
334
+ # @param context [Hash] Context
335
+ # @param options [Hash] Override options
336
+ # @return [Array, nil] Array value or nil
337
+ def get_variable_array(feature_key, variable_key, context = {}, options = {})
338
+ variable_value = get_variable(feature_key, variable_key, context, options)
339
+ get_value_by_type(variable_value, "array")
340
+ end
341
+
342
+ # Get variable as object
343
+ # @param feature_key [String] Feature key
344
+ # @param variable_key [String] Variable key
345
+ # @param context [Hash] Context
346
+ # @param options [Hash] Override options
347
+ # @return [Hash, nil] Object value or nil
348
+ def get_variable_object(feature_key, variable_key, context = {}, options = {})
349
+ variable_value = get_variable(feature_key, variable_key, context, options)
350
+ get_value_by_type(variable_value, "object")
351
+ end
352
+
353
+ # Get variable as JSON
354
+ # @param feature_key [String] Feature key
355
+ # @param variable_key [String] Variable key
356
+ # @param context [Hash] Context
357
+ # @param options [Hash] Override options
358
+ # @return [Object, nil] JSON value or nil
359
+ def get_variable_json(feature_key, variable_key, context = {}, options = {})
360
+ variable_value = get_variable(feature_key, variable_key, context, options)
361
+ get_value_by_type(variable_value, "json")
362
+ end
363
+
364
+ # Get all evaluations
365
+ # @param context [Hash] Context
366
+ # @param feature_keys [Array<String>] Feature keys to evaluate
367
+ # @param options [Hash] Override options
368
+ # @return [Hash] All evaluations
369
+ def get_all_evaluations(context = {}, feature_keys = [], options = {})
370
+ result = {}
371
+
372
+ keys = feature_keys.size > 0 ? feature_keys : @datafile_reader.get_feature_keys
373
+
374
+ keys.each do |feature_key|
375
+ # Convert symbol keys to strings for evaluation functions
376
+ feature_key_str = feature_key.to_s
377
+
378
+ # isEnabled
379
+ evaluated_feature = {
380
+ enabled: is_enabled(feature_key_str, context, options)
381
+ }
382
+
383
+ # variation
384
+ if @datafile_reader.has_variations?(feature_key_str)
385
+ variation = get_variation(feature_key_str, context, options)
386
+ evaluated_feature[:variation] = variation if variation
387
+ end
388
+
389
+ # variables
390
+ variable_keys = @datafile_reader.get_variable_keys(feature_key_str)
391
+ if variable_keys.size > 0
392
+ evaluated_feature[:variables] = {}
393
+
394
+ variable_keys.each do |variable_key|
395
+ evaluated_feature[:variables][variable_key] = get_variable(
396
+ feature_key_str,
397
+ variable_key,
398
+ context,
399
+ options
400
+ )
401
+ end
402
+ end
403
+
404
+ result[feature_key] = evaluated_feature
405
+ end
406
+
407
+ result
408
+ end
409
+
410
+ private
411
+
412
+ # Get evaluation dependencies
413
+ # @param context [Hash] Context
414
+ # @param options [Hash] Override options
415
+ # @return [Hash] Evaluation dependencies
416
+ def get_evaluation_dependencies(context, options = {})
417
+ {
418
+ context: get_context(context),
419
+ logger: @logger,
420
+ hooks_manager: @hooks_manager,
421
+ datafile_reader: @datafile_reader,
422
+ sticky: options[:sticky] ? { **(@sticky || {}), **options[:sticky] } : @sticky,
423
+ default_variation_value: options[:default_variation_value],
424
+ default_variable_value: options[:default_variable_value]
425
+ }
426
+ end
427
+
428
+ # Get value by type
429
+ # @param value [Object] Value to convert
430
+ # @param type [String] Target type
431
+ # @return [Object] Converted value
432
+ def get_value_by_type(value, type)
433
+ return nil if value.nil?
434
+
435
+ case type
436
+ when "string"
437
+ value.is_a?(String) ? value : nil
438
+ when "integer"
439
+ value.is_a?(String) ? Integer(value, 10) : (value.is_a?(Integer) ? value : nil)
440
+ when "double"
441
+ value.is_a?(String) ? Float(value) : (value.is_a?(Numeric) ? value.to_f : nil)
442
+ when "boolean"
443
+ value == true
444
+ when "array"
445
+ value.is_a?(Array) ? value : nil
446
+ when "object"
447
+ value.is_a?(Hash) ? value : nil
448
+ # @NOTE: `json` is not handled here intentionally
449
+ else
450
+ value
451
+ end
452
+ rescue
453
+ nil
454
+ end
455
+ end
456
+
457
+ # Create a new Featurevisor instance
458
+ # @param options [Hash] Instance options
459
+ # @return [Instance] New instance
460
+ def self.create_instance(options = {})
461
+ Instance.new(options)
462
+ end
463
+ end