growthbook 0.2.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 28649218e25654a513fd965aeed205a4208341c265ea9e06d1b2f6efe9a4b1eb
4
- data.tar.gz: 165ef4c260ec96a6c77b47acdfdd218039a7067156b0669d7b44adbf4c30382b
3
+ metadata.gz: 95b90dcba429217cc585f4ea2007e3d7aab0f3489dc93ee41d7fa72b847b7f53
4
+ data.tar.gz: 363c48f172798a02d899cfdc6ffb692ab1f85b45cf205e0d4c5dc6339c86c83a
5
5
  SHA512:
6
- metadata.gz: aa1bd7678e81a58d64a51a4f9d035c76cb9ee75ae91584b17c6ff92338e543be58935da3542f77c6e3c6d1cc5fcf030eb804a09d547fd8533511474d6c638f4a
7
- data.tar.gz: 9600cc883324faa2c7dcbbf4aa26e8469f0952057f3cf4b449c38cb59c0e6cd37eea246943f99750f3d54a1259fdebb622a14ec4b951a957cecf8bd924d107c0
6
+ metadata.gz: 6b8c950602e92cb66a644e6780827410d3c2e5eb71078adb95b469fbbe1c9b7f9925a6d3cf5917c345b9a5d04feabf21198d0349a57d5c597e7dca92f004bf43
7
+ data.tar.gz: ded0ea4cc0913bf8d93f8f61a34b622579d7346dfe02f121d011e82b11920e7703c192e8afb6331c2ac0d5245a435473e54707907e6116e46d4b4b01a5a3c394
@@ -3,6 +3,8 @@
3
3
  require 'json'
4
4
 
5
5
  module Growthbook
6
+ # internal use only
7
+ # Utils for condition evaluation
6
8
  class Conditions
7
9
  # Evaluate a targeting conditions hash against an attributes hash
8
10
  # Both attributes and conditions only have string keys (no symbols)
@@ -25,7 +27,7 @@ module Growthbook
25
27
  when Array
26
28
  return condition.map { |v| parse_condition(v) }
27
29
  when Hash
28
- return condition.map { |k, v| [k.to_s, parse_condition(v)] }.to_h
30
+ return condition.to_h { |k, v| [k.to_s, parse_condition(v)] }
29
31
  end
30
32
 
31
33
  condition
@@ -47,7 +49,7 @@ module Growthbook
47
49
  true
48
50
  end
49
51
 
50
- def self.is_operator_object(obj)
52
+ def self.operator_object?(obj)
51
53
  obj.each do |key, _value|
52
54
  return false if key[0] != '$'
53
55
  end
@@ -58,7 +60,7 @@ module Growthbook
58
60
  return 'string' if attribute_value.is_a? String
59
61
  return 'number' if attribute_value.is_a? Integer
60
62
  return 'number' if attribute_value.is_a? Float
61
- return 'boolean' if attribute_value == true || attribute_value == false
63
+ return 'boolean' if [true, false].include?(attribute_value)
62
64
  return 'array' if attribute_value.is_a? Array
63
65
  return 'null' if attribute_value.nil?
64
66
 
@@ -70,18 +72,16 @@ module Growthbook
70
72
  current = attributes
71
73
 
72
74
  parts.each do |value|
73
- if current && current.is_a?(Hash) && current.key?(value)
74
- current = current[value]
75
- else
76
- return nil
77
- end
75
+ return nil unless current.is_a?(Hash) && current&.key?(value)
76
+
77
+ current = current[value]
78
78
  end
79
79
 
80
80
  current
81
81
  end
82
82
 
83
83
  def self.eval_condition_value(condition_value, attribute_value)
84
- if condition_value.is_a?(Hash) && is_operator_object(condition_value)
84
+ if condition_value.is_a?(Hash) && operator_object?(condition_value)
85
85
  condition_value.each do |key, value|
86
86
  return false unless eval_operator_condition(key, attribute_value, value)
87
87
  end
@@ -94,7 +94,7 @@ module Growthbook
94
94
  return false unless attribute_value.is_a? Array
95
95
 
96
96
  attribute_value.each do |item|
97
- if is_operator_object(condition)
97
+ if operator_object?(condition)
98
98
  return true if eval_condition_value(condition, item)
99
99
  elsif eval_condition(item, condition)
100
100
  return true
@@ -147,10 +147,10 @@ module Growthbook
147
147
  true
148
148
  when '$exists'
149
149
  exists = !attribute_value.nil?
150
- if !condition_value
151
- !exists
152
- else
150
+ if condition_value
153
151
  exists
152
+ else
153
+ !exists
154
154
  end
155
155
  when '$type'
156
156
  condition_value == get_type(attribute_value)
@@ -1,9 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Growthbook
4
+ # Context object passed into the GrowthBook constructor.
4
5
  class Context
5
- attr_accessor :enabled, :url, :qa_mode, :listener
6
- attr_reader :attributes, :features, :impressions, :forced_variations, :forced_features
6
+ # @return [true, false] Switch to globally disable all experiments. Default true.
7
+ attr_accessor :enabled
8
+
9
+ # @return [String] The URL of the current page
10
+ attr_accessor :url
11
+
12
+ # @return [true, false, nil] If true, random assignment is disabled and only explicitly forced variations are used.
13
+ attr_accessor :qa_mode
14
+
15
+ # @return [Listener] An object that responds to some tracking methods that take experiment and result as arguments.
16
+ attr_accessor :listener
17
+
18
+ # @return [Hash] Map of user attributes that are used to assign variations
19
+ attr_reader :attributes
20
+
21
+ # @return [Hash] Feature definitions (usually pulled from an API or cache)
22
+ attr_reader :features
23
+
24
+ # @return [Hash] Force specific experiments to always assign a specific variation (used for QA)
25
+ attr_reader :forced_variations
26
+
27
+ attr_reader :impressions, :forced_features
7
28
 
8
29
  def initialize(options = {})
9
30
  @features = {}
@@ -62,9 +83,7 @@ module Growthbook
62
83
 
63
84
  def eval_feature(key)
64
85
  # Forced in the context
65
- if @forced_features.key?(key.to_s)
66
- return get_feature_result(@forced_features[key.to_s], 'override')
67
- end
86
+ return get_feature_result(@forced_features[key.to_s], 'override') if @forced_features.key?(key.to_s)
68
87
 
69
88
  # Return if we can't find the feature definition
70
89
  feature = get_feature(key)
@@ -74,24 +93,28 @@ module Growthbook
74
93
  # Targeting condition
75
94
  next if rule.condition && !condition_passes(rule.condition)
76
95
 
77
- # Rollout or forced value rule
78
- if rule.is_force?
79
- unless rule.coverage.nil?
80
- hash_value = get_attribute(rule.hash_attribute || 'id')
81
- next if hash_value.length.zero?
96
+ # If there are filters for who is included (e.g. namespaces)
97
+ next if rule.filters && filtered_out?(rule.filters)
98
+
99
+ # If this is a percentage rollout, skip if not included
100
+ if rule.force?
101
+ seed = rule.seed || key
102
+ hash_attribute = rule.hash_attribute || 'id'
103
+ included_in_rollout = included_in_rollout?(
104
+ seed: seed, hash_attribute: hash_attribute, range: rule.range,
105
+ coverage: rule.coverage, hash_version: rule.hash_version
106
+ )
107
+ next unless included_in_rollout
82
108
 
83
- n = Growthbook::Util.hash(hash_value + key)
84
- next if n > rule.coverage
85
- end
86
109
  return get_feature_result(rule.force, 'force')
87
110
  end
88
111
  # Experiment rule
89
- next unless rule.is_experiment?
112
+ next unless rule.experiment?
90
113
 
91
114
  exp = rule.to_experiment(key)
92
- result = run(exp)
115
+ result = _run(exp, key)
93
116
 
94
- next unless result.in_experiment
117
+ next unless result.in_experiment && !result.passthrough
95
118
 
96
119
  return get_feature_result(result.value, 'experiment', exp, result)
97
120
  end
@@ -101,57 +124,94 @@ module Growthbook
101
124
  end
102
125
 
103
126
  def run(exp)
127
+ _run(exp)
128
+ end
129
+
130
+ def on?(key)
131
+ eval_feature(key).on
132
+ end
133
+
134
+ def off?(key)
135
+ eval_feature(key).off
136
+ end
137
+
138
+ def feature_value(key, fallback = nil)
139
+ value = eval_feature(key).value
140
+ value.nil? ? fallback : value
141
+ end
142
+
143
+ private
144
+
145
+ def _run(exp, feature_id = '')
104
146
  key = exp.key
105
147
 
106
148
  # 1. If experiment doesn't have enough variations, return immediately
107
- return get_experiment_result(exp) if exp.variations.length < 2
149
+ return get_experiment_result(exp, -1, hash_used: false, feature_id: feature_id) if exp.variations.length < 2
108
150
 
109
151
  # 2. If context is disabled, return immediately
110
- return get_experiment_result(exp) unless @enabled
152
+ return get_experiment_result(exp, -1, hash_used: false, feature_id: feature_id) unless @enabled
111
153
 
112
154
  # 3. If forced via URL querystring
113
155
  if @url
114
- qsOverride = Util.get_query_string_override(key, @url, exp.variations.length)
115
- return get_experiment_result(exp, qsOverride) unless qsOverride.nil?
156
+ qs_override = Util.get_query_string_override(key, @url, exp.variations.length)
157
+ return get_experiment_result(exp, qs_override, hash_used: false, feature_id: feature_id) unless qs_override.nil?
116
158
  end
117
159
 
118
160
  # 4. If variation is forced in the context, return the forced value
119
- return get_experiment_result(exp, @forced_variations[key.to_s]) if @forced_variations.key?(key.to_s)
161
+ if @forced_variations.key?(key.to_s)
162
+ return get_experiment_result(
163
+ exp,
164
+ @forced_variations[key.to_s],
165
+ hash_used: false,
166
+ feature_id: feature_id
167
+ )
168
+ end
120
169
 
121
170
  # 5. Exclude if not active
122
- return get_experiment_result(exp) unless exp.active
171
+ return get_experiment_result(exp, -1, hash_used: false, feature_id: feature_id) unless exp.active
123
172
 
124
173
  # 6. Get hash_attribute/value and return if empty
125
174
  hash_attribute = exp.hash_attribute || 'id'
126
- hash_value = get_attribute(hash_attribute)
127
- return get_experiment_result(exp) if hash_value.length.zero?
128
-
129
- # 7. Exclude if user not in namespace
130
- return get_experiment_result(exp) if exp.namespace && !Growthbook::Util.in_namespace(hash_value, exp.namespace)
175
+ hash_value = get_attribute(hash_attribute).to_s
176
+ return get_experiment_result(exp, -1, hash_used: false, feature_id: feature_id) if hash_value.empty?
177
+
178
+ # 7. Exclude if user is filtered out (used to be called "namespace")
179
+ if exp.filters
180
+ return get_experiment_result(exp, -1, hash_used: false, feature_id: feature_id) if filtered_out?(exp.filters)
181
+ elsif exp.namespace && !Growthbook::Util.in_namespace(hash_value, exp.namespace)
182
+ return get_experiment_result(exp, -1, hash_used: false, feature_id: feature_id)
183
+ end
131
184
 
132
185
  # 8. Exclude if condition is false
133
- return get_experiment_result(exp) if exp.condition && !condition_passes(exp.condition)
186
+ if exp.condition && !condition_passes(exp.condition)
187
+ return get_experiment_result(
188
+ exp,
189
+ -1,
190
+ hash_used: false,
191
+ feature_id: feature_id
192
+ )
193
+ end
134
194
 
135
- # 9. Calculate bucket ranges and choose one
136
- ranges = Growthbook::Util.get_bucket_ranges(
195
+ # 9. Get bucket ranges and choose variation
196
+ ranges = exp.ranges || Growthbook::Util.get_bucket_ranges(
137
197
  exp.variations.length,
138
198
  exp.coverage,
139
199
  exp.weights
140
200
  )
141
- n = Growthbook::Util.hash(hash_value + key)
201
+ n = Growthbook::Util.hash(seed: exp.seed || key, value: hash_value, version: exp.hash_version || 1)
142
202
  assigned = Growthbook::Util.choose_variation(n, ranges)
143
203
 
144
204
  # 10. Return if not in experiment
145
- return get_experiment_result(exp) if assigned.negative?
205
+ return get_experiment_result(exp, -1, hash_used: false, feature_id: feature_id) if assigned.negative?
146
206
 
147
207
  # 11. Experiment has a forced variation
148
- return get_experiment_result(exp, exp.force) unless exp.force.nil?
208
+ return get_experiment_result(exp, exp.force, hash_used: false, feature_id: feature_id) unless exp.force.nil?
149
209
 
150
210
  # 12. Exclude if in QA mode
151
- return get_experiment_result(exp) if @qa_mode
211
+ return get_experiment_result(exp, -1, hash_used: false, feature_id: feature_id) if @qa_mode
152
212
 
153
213
  # 13. Build the result object
154
- result = get_experiment_result(exp, assigned, true)
214
+ result = get_experiment_result(exp, assigned, hash_used: true, feature_id: feature_id, bucket: n)
155
215
 
156
216
  # 14. Fire tracking callback
157
217
  track_experiment(exp, result)
@@ -160,21 +220,6 @@ module Growthbook
160
220
  result
161
221
  end
162
222
 
163
- def on?(key)
164
- eval_feature(key).on
165
- end
166
-
167
- def off?(key)
168
- eval_feature(key).off
169
- end
170
-
171
- def feature_value(key, fallback = nil)
172
- value = eval_feature(key).value
173
- value.nil? ? fallback : value
174
- end
175
-
176
- private
177
-
178
223
  def stringify_keys(hash)
179
224
  new_hash = {}
180
225
  hash.each do |key, value|
@@ -187,7 +232,7 @@ module Growthbook
187
232
  Growthbook::Conditions.eval_condition(@attributes, condition)
188
233
  end
189
234
 
190
- def get_experiment_result(experiment, variation_index = -1, hash_used = false)
235
+ def get_experiment_result(experiment, variation_index = -1, hash_used: false, feature_id: '', bucket: nil)
191
236
  in_experiment = true
192
237
  if variation_index.negative? || variation_index >= experiment.variations.length
193
238
  variation_index = 0
@@ -196,9 +241,17 @@ module Growthbook
196
241
 
197
242
  hash_attribute = experiment.hash_attribute || 'id'
198
243
  hash_value = get_attribute(hash_attribute)
244
+ meta = experiment.meta ? experiment.meta[variation_index] : {}
199
245
 
200
- Growthbook::InlineExperimentResult.new(hash_used, in_experiment, variation_index,
201
- experiment.variations[variation_index], hash_attribute, hash_value)
246
+ result = Growthbook::InlineExperimentResult.new(
247
+ { key: meta['key'] || variation_index, in_experiment: in_experiment, variation_id: variation_index,
248
+ value: experiment.variations[variation_index], hash_used: hash_used, hash_attribute: hash_attribute,
249
+ hash_value: hash_value, feature_id: feature_id, bucket: bucket, name: meta['name'] }
250
+ )
251
+
252
+ result.passthrough = true if meta['passthrough']
253
+
254
+ result
202
255
  end
203
256
 
204
257
  def get_feature_result(value, source, experiment = nil, experiment_result = nil)
@@ -220,10 +273,37 @@ module Growthbook
220
273
  end
221
274
 
222
275
  def track_experiment(experiment, result)
223
- if @listener && @listener.respond_to?(:on_experiment_viewed)
224
- @listener.on_experiment_viewed(experiment, result)
225
- end
276
+ @listener.on_experiment_viewed(experiment, result) if @listener.respond_to?(:on_experiment_viewed)
226
277
  @impressions[experiment.key] = result
227
278
  end
279
+
280
+ def included_in_rollout?(seed:, hash_attribute:, hash_version:, range:, coverage:)
281
+ return true if range.nil? && coverage.nil?
282
+
283
+ hash_value = get_attribute(hash_attribute)
284
+
285
+ return false if hash_value.empty?
286
+
287
+ n = Growthbook::Util.hash(seed: seed, value: hash_value, version: hash_version || 1)
288
+
289
+ return Growthbook::Util.in_range?(n, range) if range
290
+ return n <= coverage if coverage
291
+
292
+ true
293
+ end
294
+
295
+ def filtered_out?(filters)
296
+ filters.any? do |filter|
297
+ hash_value = get_attribute(filter['attribute'] || 'id')
298
+
299
+ if hash_value.empty?
300
+ false
301
+ else
302
+ n = Growthbook::Util.hash(seed: filter['seed'], value: hash_value, version: filter['hashVersion'] || 2)
303
+
304
+ filter['ranges'].none? { |range| Growthbook::Util.in_range?(n, range) }
305
+ end
306
+ end
307
+ end
228
308
  end
229
309
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Growthbook
4
+ # The feature with a generic value type.
4
5
  class Feature
5
6
  # @return [Any , nil]
6
7
  attr_reader :default_value
@@ -9,9 +10,9 @@ module Growthbook
9
10
  attr_reader :rules
10
11
 
11
12
  def initialize(feature)
12
- @default_value = getOption(feature, :defaultValue)
13
+ @default_value = get_option(feature, :defaultValue)
13
14
 
14
- rules = getOption(feature, :rules)
15
+ rules = get_option(feature, :rules)
15
16
 
16
17
  @rules = []
17
18
  rules&.each do |rule|
@@ -31,7 +32,7 @@ module Growthbook
31
32
 
32
33
  private
33
34
 
34
- def getOption(hash, key)
35
+ def get_option(hash, key)
35
36
  return hash[key.to_sym] if hash.key?(key.to_sym)
36
37
  return hash[key.to_s] if hash.key?(key.to_s)
37
38
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Growthbook
4
+ # Result of a feature evaluation
4
5
  class FeatureResult
5
6
  # The assigned value of the feature
6
7
  # @return [Any, nil]
@@ -19,11 +20,11 @@ module Growthbook
19
20
  attr_reader :source
20
21
 
21
22
  # The experiment used to decide the feature value
22
- # @return [Growthbook.Experiment, nil]
23
+ # @return [Growthbook::InlineExperiment, nil]
23
24
  attr_reader :experiment
24
25
 
25
26
  # The result of the experiment
26
- # @return [Growthbook.ExperimentResult, nil]
27
+ # @return [Growthbook::InlineExperimentResult, nil]
27
28
  attr_reader :experiment_result
28
29
 
29
30
  def initialize(
@@ -1,37 +1,82 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Growthbook
4
+ # Internal class that overrides the default value of a Feature based on a set of requirements.
4
5
  class FeatureRule
5
- # @return [Hash , nil]
6
+ # @return [Hash , nil] Optional targeting condition
6
7
  attr_reader :condition
7
- # @return [Float , nil]
8
+
9
+ # @return [Float , nil] What percent of users should be included in the experiment (between 0 and 1, inclusive)
8
10
  attr_reader :coverage
9
- # @return [T , nil]
11
+
12
+ # @return [T , nil] Immediately force a specific value (ignore every other option besides condition and coverage)
10
13
  attr_reader :force
11
- # @return [T[] , nil]
14
+
15
+ # @return [T[] , nil] Run an experiment (A/B test) and randomly choose between these variations
12
16
  attr_reader :variations
13
- # @return [String , nil]
17
+
18
+ # @return [String , nil] The globally unique tracking key for the experiment (default to the feature key)
14
19
  attr_reader :key
15
- # @return [Float[] , nil]
20
+
21
+ # @return [Float[] , nil] How to weight traffic between variations. Must add to 1.
16
22
  attr_reader :weights
17
- # @return [Array , nil]
23
+
24
+ # @return [String , nil] Adds the experiment to a namespace
18
25
  attr_reader :namespace
19
- # @return [String , nil]
26
+
27
+ # @return [String , nil] What user attribute should be used to assign variations (defaults to id)
20
28
  attr_reader :hash_attribute
21
29
 
30
+ # @return [Integer , nil] The hash version to use (default to 1)
31
+ attr_reader :hash_version
32
+
33
+ # @return [BucketRange , nil] A more precise version of coverage
34
+ attr_reader :range
35
+
36
+ # @return [BucketRanges[] , nil] Ranges for experiment variations
37
+ attr_reader :ranges
38
+
39
+ # @return [VariationMeta[] , nil] Meta info about the experiment variations
40
+ attr_reader :meta
41
+
42
+ # @return [Filter[] , nil] Array of filters to apply to the rule
43
+ attr_reader :filters
44
+
45
+ # @return [String , nil] Seed to use for hashing
46
+ attr_reader :seed
47
+
48
+ # @return [String , nil] Human-readable name for the experiment
49
+ attr_reader :name
50
+
51
+ # @return [String , nil] The phase id of the experiment
52
+ attr_reader :phase
53
+
54
+ # @return [TrackData[] , nil] Array of tracking calls to fire
55
+ attr_reader :tracks
56
+
22
57
  def initialize(rule)
23
- @coverage = getOption(rule, :coverage)
24
- @force = getOption(rule, :force)
25
- @variations = getOption(rule, :variations)
26
- @key = getOption(rule, :key)
27
- @weights = getOption(rule, :weights)
28
- @namespace = getOption(rule, :namespace)
29
- @hash_attribute = getOption(rule, :hash_attribute) || getOption(rule, :hashAttribute)
30
-
31
- cond = getOption(rule, :condition)
58
+ @coverage = get_option(rule, :coverage)
59
+ @force = get_option(rule, :force)
60
+ @variations = get_option(rule, :variations)
61
+ @key = get_option(rule, :key)
62
+ @weights = get_option(rule, :weights)
63
+ @namespace = get_option(rule, :namespace)
64
+ @hash_attribute = get_option(rule, :hash_attribute) || get_option(rule, :hashAttribute)
65
+ @hash_version = get_option(rule, :hash_version) || get_option(rule, :hashVersion)
66
+ @range = get_option(rule, :range)
67
+ @ranges = get_option(rule, :ranges)
68
+ @meta = get_option(rule, :meta)
69
+ @filters = get_option(rule, :filters)
70
+ @seed = get_option(rule, :seed)
71
+ @name = get_option(rule, :name)
72
+ @phase = get_option(rule, :phase)
73
+ @tracks = get_option(rule, :tracks)
74
+
75
+ cond = get_option(rule, :condition)
32
76
  @condition = Growthbook::Conditions.parse_condition(cond) unless cond.nil?
33
77
  end
34
78
 
79
+ # @return [Growthbook::InlineExperiment, nil]
35
80
  def to_experiment(feature_key)
36
81
  return nil unless @variations
37
82
 
@@ -41,34 +86,49 @@ module Growthbook
41
86
  coverage: @coverage,
42
87
  weights: @weights,
43
88
  hash_attribute: @hash_attribute,
44
- namespace: @namespace
89
+ hash_version: @hash_version,
90
+ namespace: @namespace,
91
+ meta: @meta,
92
+ ranges: @ranges,
93
+ filters: @filters,
94
+ name: @name,
95
+ phase: @phase,
96
+ seed: @seed
45
97
  )
46
98
  end
47
99
 
48
- def is_experiment?
100
+ def experiment?
49
101
  !!@variations
50
102
  end
51
103
 
52
- def is_force?
53
- !is_experiment? && !@force.nil?
104
+ def force?
105
+ !experiment? && !@force.nil?
54
106
  end
55
107
 
56
108
  def to_json(*_args)
57
- res = {}
58
- res['condition'] = @condition unless @condition.nil?
59
- res['coverage'] = @coverage unless @coverage.nil?
60
- res['force'] = @force unless @force.nil?
61
- res['variations'] = @variations unless @variations.nil?
62
- res['key'] = @key unless @key.nil?
63
- res['weights'] = @weights unless @weights.nil?
64
- res['namespace'] = @namespace unless @namespace.nil?
65
- res['hashAttribute'] = @hash_attribute unless @hash_attribute.nil?
66
- res
109
+ {
110
+ 'condition' => @condition,
111
+ 'coverage' => @coverage,
112
+ 'force' => @force,
113
+ 'variations' => @variations,
114
+ 'key' => @key,
115
+ 'weights' => @weights,
116
+ 'namespace' => @namespace,
117
+ 'hashAttribute' => @hash_attribute,
118
+ 'range' => @range,
119
+ 'ranges' => @ranges,
120
+ 'meta' => @meta,
121
+ 'filters' => @filters,
122
+ 'seed' => @seed,
123
+ 'name' => @name,
124
+ 'phase' => @phase,
125
+ 'tracks' => @tracks
126
+ }.compact
67
127
  end
68
128
 
69
129
  private
70
130
 
71
- def getOption(hash, key)
131
+ def get_option(hash, key)
72
132
  return hash[key.to_sym] if hash.key?(key.to_sym)
73
133
  return hash[key.to_s] if hash.key?(key.to_s)
74
134