splitclient-rb 7.0.0.pre.rc3-java → 7.0.1-java
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/pull_request_template.md +10 -0
- data/.gitignore +2 -0
- data/.travis.yml +9 -0
- data/CHANGES.txt +18 -11
- data/CONTRIBUTORS-GUIDE.md +49 -0
- data/README.md +75 -41
- data/lib/splitclient-rb.rb +4 -4
- data/lib/splitclient-rb/cache/adapters/memory_adapters/map_adapter.rb +4 -0
- data/lib/splitclient-rb/cache/adapters/memory_adapters/queue_adapter.rb +4 -0
- data/lib/splitclient-rb/cache/repositories/impressions/memory_repository.rb +4 -0
- data/lib/splitclient-rb/cache/repositories/impressions_repository.rb +2 -2
- data/lib/splitclient-rb/cache/repositories/metrics/memory_repository.rb +1 -1
- data/lib/splitclient-rb/cache/repositories/metrics_repository.rb +2 -2
- data/lib/splitclient-rb/cache/senders/impressions_sender.rb +0 -5
- data/lib/splitclient-rb/cache/senders/localhost_repo_cleaner.rb +49 -0
- data/lib/splitclient-rb/cache/stores/localhost_split_builder.rb +94 -0
- data/lib/splitclient-rb/cache/stores/localhost_split_store.rb +112 -0
- data/lib/splitclient-rb/cache/stores/segment_store.rb +1 -7
- data/lib/splitclient-rb/cache/stores/split_store.rb +1 -7
- data/lib/splitclient-rb/cache/stores/store_utils.rb +13 -0
- data/lib/splitclient-rb/clients/split_client.rb +16 -13
- data/lib/splitclient-rb/engine/api/client.rb +6 -11
- data/lib/splitclient-rb/engine/api/events.rb +3 -5
- data/lib/splitclient-rb/engine/api/impressions.rb +1 -1
- data/lib/splitclient-rb/engine/parser/split_adapter.rb +18 -1
- data/lib/splitclient-rb/managers/split_manager.rb +10 -9
- data/lib/splitclient-rb/split_config.rb +79 -17
- data/lib/splitclient-rb/split_factory.rb +11 -4
- data/lib/splitclient-rb/split_factory_builder.rb +1 -21
- data/lib/splitclient-rb/version.rb +1 -1
- data/sonar-scanner.sh +42 -0
- metadata +11 -10
- data/Detailed-README.md +0 -576
- data/NEWS +0 -144
- data/lib/splitclient-rb/clients/localhost_split_client.rb +0 -184
- data/lib/splitclient-rb/localhost_split_factory.rb +0 -14
- data/lib/splitclient-rb/localhost_utils.rb +0 -59
- data/lib/splitclient-rb/managers/localhost_split_manager.rb +0 -60
data/NEWS
DELETED
@@ -1,144 +0,0 @@
|
|
1
|
-
6.4.0 (Jul 05, 2019)
|
2
|
-
- Added properties to track method.
|
3
|
-
|
4
|
-
6.3.0 (Apr 30, 2019)
|
5
|
-
|
6
|
-
- Added Dynamic Configurations support through two new methods that mimick the regular ones, changing the type of what is returned.
|
7
|
-
- get_treatment_with_config: Same as get_treatment but returning the treatment with it's config.
|
8
|
-
- get_treatments_with_config: Same as get_treatments, but instead of a map of string it returns a map of treatments with config.
|
9
|
-
- Added configs to SplitViews returned by the manager module.
|
10
|
-
- Updated localhost mode. Now besides supporting the old text files with `.split` extension (to be deprecated soon), we support YAML (.yaml/.yml) files where you can
|
11
|
-
define configurations for your treatments and also whitelisted keys. Read more in our docs!
|
12
|
-
|
13
|
-
6.2.0
|
14
|
-
Ensure SDK flushes information to Split servers on client destroy
|
15
|
-
Fix for compatibility issue between Faraday < 0.13 and net-http-persistent 3
|
16
|
-
Change default features refresh rate interval to 5 seconds
|
17
|
-
|
18
|
-
6.1.0
|
19
|
-
|
20
|
-
Review input validation for client API methods: get_treatment, get_treatments, track, manager. Add input validation to block_until_ready, client startup, and destroy
|
21
|
-
Add additional logging to the SDK Client in order to help debug matcher related issues
|
22
|
-
Fix for issue causing redis_url parameter to be ignored
|
23
|
-
|
24
|
-
6.0.1
|
25
|
-
|
26
|
-
Fix an issue in events and impressions API calls log messages introduced in 6.0.0
|
27
|
-
|
28
|
-
6.0.0
|
29
|
-
|
30
|
-
BREAKING CHANGE: Remove producer mode, make memory adapter mandatory in standalone mode, and Redis adapter mandatory in consumer mode.
|
31
|
-
BREAKING CHANGE: Reduce the total number of Redis operations of the SDK by changing the impressions storage format and adding a memory cache for splits and segments. It requires to have Split Synchronizer > v2.0.0 running if you are using Redis.
|
32
|
-
SDK is now compatible with net-http-persistent 3.0.
|
33
|
-
|
34
|
-
5.1.2
|
35
|
-
|
36
|
-
Add input validation for client API methods: get_treatment, get_treatments, track, manager
|
37
|
-
|
38
|
-
5.1.1
|
39
|
-
|
40
|
-
Reduces the number of calls to Redis when calling #client.get_treatments using such cache adapter.
|
41
|
-
|
42
|
-
5.1.0
|
43
|
-
|
44
|
-
Prevent unhandled exceptions from raising when API get calls fail on Segments and Treatments.
|
45
|
-
|
46
|
-
5.0.3
|
47
|
-
|
48
|
-
Creates a new configuration parameter (impressions_bulk_size) to manage the max number of impressions sent to the Split backend on each post.
|
49
|
-
Changes impressions_queue_size so that it manages only the queue size in the memory adapter.
|
50
|
-
A backwards compatibility fail safe is included for users of previous SDK versions.
|
51
|
-
|
52
|
-
5.0.2
|
53
|
-
|
54
|
-
Turn Impression Listener into an optional SDK feature.
|
55
|
-
Prevents the impression thread from being started if a listener is not in place
|
56
|
-
|
57
|
-
5.0.1
|
58
|
-
|
59
|
-
Adding stop! method to the factory.
|
60
|
-
With this method the user will be able to stop the threads that queries the Split Service. This will be used in the before_fork configuration in Puma and Unicorn to stop the threads in the master process.
|
61
|
-
|
62
|
-
5.0.0
|
63
|
-
|
64
|
-
This is a breaking change in how users buckets are allocated.
|
65
|
-
Ruby SDK was wrongly picking up the right buckets and defauled to
|
66
|
-
use a "legacy hashing" instead of our moder murmur3 algo which is
|
67
|
-
what other SDKs use. Please reach out to support@split.io for help
|
68
|
-
on how to upgrade to this release.
|
69
|
-
|
70
|
-
4.5.0
|
71
|
-
|
72
|
-
Add JRuby support
|
73
|
-
|
74
|
-
4.4.0
|
75
|
-
|
76
|
-
Add the ability to send event data to split service (.track() api)
|
77
|
-
|
78
|
-
4.3.0
|
79
|
-
|
80
|
-
Add support for impression listener, there is an ability to use user-defined class to capture all impressions
|
81
|
-
Now you can apply attribute matcher to the traffic type
|
82
|
-
|
83
|
-
4.2.0
|
84
|
-
|
85
|
-
Introduce new matchers: boolean and regexp (string).
|
86
|
-
Remove unneeded dependencies
|
87
|
-
Introduce dependency matcher, which allows treatment of one split to depend on the evaluation of another.
|
88
|
-
|
89
|
-
4.1.0
|
90
|
-
|
91
|
-
Introduce set matchers: part of set, contains all and contains any, starts with, ends with and contains.
|
92
|
-
|
93
|
-
4.0.0
|
94
|
-
|
95
|
-
Add support for murmur3 hashing algorithm
|
96
|
-
Optimize gem memory usage
|
97
|
-
|
98
|
-
3.3.0
|
99
|
-
|
100
|
-
Add support for traffic allocation
|
101
|
-
|
102
|
-
3.1.0
|
103
|
-
|
104
|
-
Now supporting Redis as a cache adapter
|
105
|
-
Factory is build now as SplitIoClient::SplitFactoryBuilder.build('<API_KEY>')
|
106
|
-
|
107
|
-
3.0.2
|
108
|
-
|
109
|
-
now support also client.get_treatment( { :matching_key = 'bb' , "bucketing_key = ''}, ....)
|
110
|
-
|
111
|
-
2.0.1
|
112
|
-
|
113
|
-
No news for this release
|
114
|
-
|
115
|
-
2.0.0
|
116
|
-
|
117
|
-
Instantiation of the split client is now through a factory:
|
118
|
-
|
119
|
-
factory = SplitIoClient::SplitFactory.new("rbk5je8be5qflpa047m17fe4ra", options)
|
120
|
-
client = factory.client
|
121
|
-
manager = factory.manager
|
122
|
-
|
123
|
-
1.0.4
|
124
|
-
|
125
|
-
Added AND combiner for conditions support. Added events_uri config param which is the url where the metrics post are send to.
|
126
|
-
|
127
|
-
1.0.3
|
128
|
-
|
129
|
-
This version of the gem fixes two minor bugs related to the config option for the refresh rates, as well as an inconsistency for the split definitions that are not created from the web app, but directly from the api instead.
|
130
|
-
|
131
|
-
1.0.2
|
132
|
-
|
133
|
-
Support for all the new matchers including: number comparison with >=, <=, =, between; date comparison with is on or before, is on or after, is on, between; attribute value in a defined set of values.
|
134
|
-
No other major updates for this release.
|
135
|
-
|
136
|
-
1.0.1
|
137
|
-
|
138
|
-
isTreatment was removed from the API
|
139
|
-
local environment file was removed to match the rest of the SDKs. "split".
|
140
|
-
No other major updates for this release.
|
141
|
-
|
142
|
-
1.0.0
|
143
|
-
|
144
|
-
No news for this release.
|
@@ -1,184 +0,0 @@
|
|
1
|
-
module SplitIoClient
|
2
|
-
class LocalhostSplitClient
|
3
|
-
include SplitIoClient::LocalhostUtils
|
4
|
-
|
5
|
-
#
|
6
|
-
# variables to if the sdk is being used in localhost mode and store the list of features
|
7
|
-
attr_reader :localhost_mode
|
8
|
-
attr_reader :localhost_mode_features
|
9
|
-
|
10
|
-
#
|
11
|
-
# Creates a new split client instance that reads from the given splits file
|
12
|
-
#
|
13
|
-
# @param splits_file [File] file that contains some splits
|
14
|
-
#
|
15
|
-
# @return [LocalhostSplitIoClient] split.io localhost client instance
|
16
|
-
def initialize(splits_file, config, reload_rate = nil)
|
17
|
-
@localhost_mode = true
|
18
|
-
@localhost_mode_features = []
|
19
|
-
load_localhost_mode_features(splits_file, reload_rate)
|
20
|
-
@config = config
|
21
|
-
end
|
22
|
-
|
23
|
-
#
|
24
|
-
# method that returns the sdk gem version
|
25
|
-
#
|
26
|
-
# @return [string] version value for this sdk
|
27
|
-
def self.sdk_version
|
28
|
-
'ruby-'+SplitIoClient::VERSION
|
29
|
-
end
|
30
|
-
|
31
|
-
#
|
32
|
-
# obtains the treatments and configs for a given set of features
|
33
|
-
#
|
34
|
-
# @param key [string] evaluation key, only used with yaml split files
|
35
|
-
# @param split_names [array] name of the features being validated
|
36
|
-
# @param attributes [hash] kept for consistency with actual SDK client. Omitted in calls
|
37
|
-
#
|
38
|
-
# @return [hash] map of treatments (split_name, treatment)
|
39
|
-
def get_treatments_with_config(key, split_names, attributes = nil)
|
40
|
-
get_localhost_treatments(key, split_names, attributes, 'get_treatments_with_config')
|
41
|
-
end
|
42
|
-
|
43
|
-
#
|
44
|
-
# obtains the treatments for a given set of features
|
45
|
-
#
|
46
|
-
# @param key [string] evaluation key, only used with yaml split files
|
47
|
-
# @param split_names [array] name of the features being validated
|
48
|
-
# @param attributes [hash] kept for consistency with actual SDK client. Omitted in calls
|
49
|
-
#
|
50
|
-
# @return [hash] map of treatments (split_name, treatment_name)
|
51
|
-
def get_treatments(key, split_names, attributes = nil)
|
52
|
-
treatments = get_localhost_treatments(key, split_names, attributes)
|
53
|
-
return treatments if treatments.nil?
|
54
|
-
keys = treatments.keys
|
55
|
-
treats = treatments.map { |_,t| t[:treatment] }
|
56
|
-
Hash[keys.zip(treats)]
|
57
|
-
end
|
58
|
-
|
59
|
-
#
|
60
|
-
# obtains the treatment for a given feature
|
61
|
-
#
|
62
|
-
# @param key [string] evaluation key, only used with yaml split files
|
63
|
-
# @param split_name [string] name of the feature that is being validated
|
64
|
-
# @param attributes [hash] kept for consistency with actual SDK client. Omitted in calls
|
65
|
-
#
|
66
|
-
# @return [string] corresponding treatment
|
67
|
-
def get_treatment(key, split_name, attributes = nil)
|
68
|
-
get_localhost_treatment(key, split_name, attributes)[:treatment]
|
69
|
-
end
|
70
|
-
|
71
|
-
#
|
72
|
-
# obtains the treatment and config for a given feature
|
73
|
-
#
|
74
|
-
# @param key [string] evaluation key, only used with yaml split files
|
75
|
-
# @param split_name [string] name of the feature that is being validated
|
76
|
-
# @param attributes [hash] kept for consistency with actual SDK client. Omitted in calls
|
77
|
-
#
|
78
|
-
# @return [hash] corresponding treatment and config
|
79
|
-
def get_treatment_with_config(key, split_name, attributes = nil)
|
80
|
-
get_localhost_treatment(key, split_name, attributes, 'get_treatment_with_config')
|
81
|
-
end
|
82
|
-
|
83
|
-
def track
|
84
|
-
end
|
85
|
-
|
86
|
-
private
|
87
|
-
|
88
|
-
#
|
89
|
-
# method to check if the sdk is running in localhost mode based on api key
|
90
|
-
#
|
91
|
-
# @return [boolean] True if is in localhost mode, false otherwise
|
92
|
-
def is_localhost_mode?
|
93
|
-
true
|
94
|
-
end
|
95
|
-
|
96
|
-
# @param key [string] evaluation key, only used with yaml split files
|
97
|
-
# @param split_name [string] name of the feature that is being validated
|
98
|
-
#
|
99
|
-
# @return [Hash] corresponding treatment and config, control otherwise
|
100
|
-
def get_localhost_treatment(key, split_name, attributes, calling_method = 'get_treatment')
|
101
|
-
control_treatment = { label: Engine::Models::Label::EXCEPTION, treatment: SplitIoClient::Engine::Models::Treatment::CONTROL, config: nil }
|
102
|
-
parsed_control_treatment = parsed_treatment(control_treatment)
|
103
|
-
|
104
|
-
bucketing_key, matching_key = keys_from_key(key)
|
105
|
-
return parsed_control_treatment unless @config.split_validator.valid_get_treatment_parameters(calling_method, key, split_name, matching_key, bucketing_key, attributes)
|
106
|
-
|
107
|
-
sanitized_split_name = split_name.to_s.strip
|
108
|
-
|
109
|
-
if split_name.to_s != sanitized_split_name
|
110
|
-
@config.logger.warn("get_treatment: split_name #{split_name} has extra whitespace, trimming")
|
111
|
-
split_name = sanitized_split_name
|
112
|
-
end
|
113
|
-
|
114
|
-
treatment = @localhost_mode_features.select { |h| h[:feature] == split_name && has_key(h[:keys], key) }.last
|
115
|
-
|
116
|
-
if treatment.nil?
|
117
|
-
treatment = @localhost_mode_features.select { |h| h[:feature] == split_name && h[:keys] == nil }.last
|
118
|
-
end
|
119
|
-
|
120
|
-
if treatment && treatment[:treatment]
|
121
|
-
{
|
122
|
-
treatment: treatment[:treatment],
|
123
|
-
config: treatment[:config]
|
124
|
-
}
|
125
|
-
else
|
126
|
-
parsed_control_treatment
|
127
|
-
end
|
128
|
-
end
|
129
|
-
|
130
|
-
def get_localhost_treatments(key, split_names, attributes = nil, calling_method = 'get_treatments')
|
131
|
-
return nil unless @config.split_validator.valid_get_treatments_parameters(calling_method, split_names)
|
132
|
-
|
133
|
-
sanitized_split_names = sanitize_split_names(calling_method, split_names)
|
134
|
-
|
135
|
-
if sanitized_split_names.empty?
|
136
|
-
@config.logger.error("#{calling_method}: split_names must be a non-empty Array")
|
137
|
-
return {}
|
138
|
-
end
|
139
|
-
|
140
|
-
split_names.each_with_object({}) do |split_name, memo|
|
141
|
-
memo.merge!(split_name => get_treatment_with_config(key, split_name, attributes))
|
142
|
-
end
|
143
|
-
end
|
144
|
-
|
145
|
-
def sanitize_split_names(calling_method, split_names)
|
146
|
-
split_names.compact.uniq.select do |split_name|
|
147
|
-
if (split_name.is_a?(String) || split_name.is_a?(Symbol)) && !split_name.empty?
|
148
|
-
true
|
149
|
-
elsif split_name.is_a?(String) && split_name.empty?
|
150
|
-
@config.logger.warn("#{calling_method}: you passed an empty split_name, split_name must be a non-empty String or a Symbol")
|
151
|
-
false
|
152
|
-
else
|
153
|
-
@config.logger.warn("#{calling_method}: you passed an invalid split_name, split_name must be a non-empty String or a Symbol")
|
154
|
-
false
|
155
|
-
end
|
156
|
-
end
|
157
|
-
end
|
158
|
-
|
159
|
-
def parsed_treatment(treatment_data)
|
160
|
-
{
|
161
|
-
treatment: treatment_data[:treatment],
|
162
|
-
config: treatment_data[:config]
|
163
|
-
}
|
164
|
-
end
|
165
|
-
|
166
|
-
def has_key(keys, key)
|
167
|
-
case keys
|
168
|
-
when Array then keys.include? key
|
169
|
-
when String then keys == key
|
170
|
-
else
|
171
|
-
false
|
172
|
-
end
|
173
|
-
end
|
174
|
-
|
175
|
-
def keys_from_key(key)
|
176
|
-
case key
|
177
|
-
when Hash
|
178
|
-
key.values_at(:bucketing_key, :matching_key).map { |k| k.nil? ? nil : k }
|
179
|
-
else
|
180
|
-
[nil, key].map { |k| k.nil? ? nil : k }
|
181
|
-
end
|
182
|
-
end
|
183
|
-
end
|
184
|
-
end
|
@@ -1,14 +0,0 @@
|
|
1
|
-
module SplitIoClient
|
2
|
-
class LocalhostSplitFactory
|
3
|
-
attr_reader :client, :manager
|
4
|
-
|
5
|
-
def initialize(splits_file, config, reload_rate = nil, logger = nil)
|
6
|
-
@splits_file = splits_file
|
7
|
-
@reload_rate = reload_rate
|
8
|
-
@config = config
|
9
|
-
|
10
|
-
@client = LocalhostSplitClient.new(@splits_file, @config, @reload_rate)
|
11
|
-
@manager = LocalhostSplitManager.new(@splits_file, @reload_rate)
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|
@@ -1,59 +0,0 @@
|
|
1
|
-
module SplitIoClient
|
2
|
-
module LocalhostUtils
|
3
|
-
|
4
|
-
require 'yaml'
|
5
|
-
#
|
6
|
-
# method to set localhost mode features by reading the given .splits
|
7
|
-
#
|
8
|
-
# @param splits_file [File] the .split file that contains the splits
|
9
|
-
# @param reload_rate [Integer] the number of seconds to reload splits_file
|
10
|
-
# @return nil
|
11
|
-
def load_localhost_mode_features(splits_file, reload_rate = nil)
|
12
|
-
return @localhost_mode_features unless File.exists?(splits_file)
|
13
|
-
|
14
|
-
store_features(splits_file)
|
15
|
-
|
16
|
-
return unless reload_rate
|
17
|
-
|
18
|
-
Thread.new do
|
19
|
-
loop do
|
20
|
-
@localhost_mode_features = []
|
21
|
-
store_features(splits_file)
|
22
|
-
|
23
|
-
sleep(SplitIoClient::Utilities.randomize_interval(reload_rate))
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
def store_features(splits_file)
|
29
|
-
yaml_extensions = [".yml", ".yaml"]
|
30
|
-
if yaml_extensions.include? File.extname(splits_file)
|
31
|
-
store_yaml_features(splits_file)
|
32
|
-
else
|
33
|
-
store_plain_text_features(splits_file)
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
|
-
private
|
38
|
-
|
39
|
-
def store_plain_text_features(splits_file)
|
40
|
-
File.open(splits_file).each do |line|
|
41
|
-
feature, treatment = line.strip.split(' ')
|
42
|
-
|
43
|
-
next if line.start_with?('#') || line.strip.empty?
|
44
|
-
|
45
|
-
@localhost_mode_features << { feature: feature, treatment: treatment, key: nil, config: nil }
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
def store_yaml_features(splits_file)
|
50
|
-
YAML.load(File.read(splits_file)).each do |feature|
|
51
|
-
feat_symbolized_keys = feature[feature.keys.first].inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo}
|
52
|
-
|
53
|
-
feat_symbolized_keys[:config] = feat_symbolized_keys[:config].to_json
|
54
|
-
|
55
|
-
@localhost_mode_features << { feature: feature.keys.first }.merge(feat_symbolized_keys)
|
56
|
-
end
|
57
|
-
end
|
58
|
-
end
|
59
|
-
end
|
@@ -1,60 +0,0 @@
|
|
1
|
-
module SplitIoClient
|
2
|
-
class LocalhostSplitManager
|
3
|
-
include SplitIoClient::LocalhostUtils
|
4
|
-
|
5
|
-
#
|
6
|
-
# Creates a new split manager instance that holds the splits from a given file
|
7
|
-
#
|
8
|
-
# @param splits_file [File] the .split file that contains the splits
|
9
|
-
# @param reload_rate [Integer] the number of seconds to reload splits_file
|
10
|
-
#
|
11
|
-
# @return [LocalhostSplitIoManager] split.io localhost manager instance
|
12
|
-
def initialize(splits_file, reload_rate = nil)
|
13
|
-
@localhost_mode = true
|
14
|
-
@localhost_mode_features = []
|
15
|
-
|
16
|
-
load_localhost_mode_features(splits_file, reload_rate)
|
17
|
-
end
|
18
|
-
|
19
|
-
#
|
20
|
-
# method to get a split view
|
21
|
-
#
|
22
|
-
# @returns a split view
|
23
|
-
def split(split_name)
|
24
|
-
features = @localhost_mode_features.find_all { |feat| feat[:feature] == split_name }
|
25
|
-
|
26
|
-
return nil if features.nil?
|
27
|
-
|
28
|
-
treatments = features.map { |feat| feat[:treatment] }
|
29
|
-
|
30
|
-
configs = Hash[ features.map { |feat| [ feat[:treatment].to_sym, feat[:config] ] } ]
|
31
|
-
|
32
|
-
{
|
33
|
-
change_number: nil,
|
34
|
-
killed: false,
|
35
|
-
name: split_name,
|
36
|
-
traffic_type: nil,
|
37
|
-
treatments: treatments,
|
38
|
-
configs: configs
|
39
|
-
}
|
40
|
-
end
|
41
|
-
|
42
|
-
#
|
43
|
-
# method to get the split list from the client
|
44
|
-
#
|
45
|
-
# @returns Array of split view
|
46
|
-
def splits
|
47
|
-
split_names.map do |split_name|
|
48
|
-
split(split_name)
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
#
|
53
|
-
# method to get the list of just split names. Ideal for ietrating and calling client.get_treatment
|
54
|
-
#
|
55
|
-
# @returns [object] array of split names (String)
|
56
|
-
def split_names
|
57
|
-
@localhost_mode_features.map{ |feat| feat[:feature]}.uniq
|
58
|
-
end
|
59
|
-
end
|
60
|
-
end
|