split 3.4.1 → 4.0.4

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.
Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/dependabot.yml +7 -0
  4. data/.github/workflows/ci.yml +76 -0
  5. data/.rubocop.yml +177 -4
  6. data/CHANGELOG.md +87 -0
  7. data/CONTRIBUTING.md +1 -1
  8. data/Gemfile +2 -1
  9. data/README.md +37 -9
  10. data/Rakefile +5 -5
  11. data/gemfiles/5.2.gemfile +1 -3
  12. data/gemfiles/6.0.gemfile +1 -3
  13. data/gemfiles/{5.0.gemfile → 6.1.gemfile} +2 -4
  14. data/gemfiles/{5.1.gemfile → 7.0.gemfile} +2 -4
  15. data/lib/split/algorithms/block_randomization.rb +6 -6
  16. data/lib/split/algorithms/weighted_sample.rb +2 -1
  17. data/lib/split/algorithms/whiplash.rb +17 -18
  18. data/lib/split/algorithms.rb +14 -0
  19. data/lib/split/alternative.rb +22 -22
  20. data/lib/split/cache.rb +27 -0
  21. data/lib/split/combined_experiments_helper.rb +5 -4
  22. data/lib/split/configuration.rb +89 -94
  23. data/lib/split/dashboard/helpers.rb +7 -7
  24. data/lib/split/dashboard/pagination_helpers.rb +54 -54
  25. data/lib/split/dashboard/paginator.rb +1 -0
  26. data/lib/split/dashboard/public/dashboard.js +10 -0
  27. data/lib/split/dashboard/public/style.css +10 -2
  28. data/lib/split/dashboard/views/_controls.erb +13 -0
  29. data/lib/split/dashboard/views/_experiment.erb +2 -1
  30. data/lib/split/dashboard/views/index.erb +19 -4
  31. data/lib/split/dashboard.rb +42 -21
  32. data/lib/split/encapsulated_helper.rb +15 -8
  33. data/lib/split/engine.rb +1 -0
  34. data/lib/split/exceptions.rb +1 -0
  35. data/lib/split/experiment.rb +151 -124
  36. data/lib/split/experiment_catalog.rb +7 -8
  37. data/lib/split/extensions/string.rb +2 -1
  38. data/lib/split/goals_collection.rb +9 -10
  39. data/lib/split/helper.rb +50 -23
  40. data/lib/split/metric.rb +6 -6
  41. data/lib/split/persistence/cookie_adapter.rb +46 -44
  42. data/lib/split/persistence/dual_adapter.rb +7 -8
  43. data/lib/split/persistence/redis_adapter.rb +8 -4
  44. data/lib/split/persistence/session_adapter.rb +1 -2
  45. data/lib/split/persistence.rb +8 -6
  46. data/lib/split/redis_interface.rb +15 -29
  47. data/lib/split/trial.rb +43 -34
  48. data/lib/split/user.rb +25 -14
  49. data/lib/split/version.rb +2 -4
  50. data/lib/split/zscore.rb +2 -3
  51. data/lib/split.rb +34 -27
  52. data/spec/algorithms/block_randomization_spec.rb +6 -5
  53. data/spec/algorithms/weighted_sample_spec.rb +6 -5
  54. data/spec/algorithms/whiplash_spec.rb +4 -5
  55. data/spec/alternative_spec.rb +35 -36
  56. data/spec/cache_spec.rb +84 -0
  57. data/spec/combined_experiments_helper_spec.rb +18 -17
  58. data/spec/configuration_spec.rb +41 -45
  59. data/spec/dashboard/pagination_helpers_spec.rb +69 -67
  60. data/spec/dashboard/paginator_spec.rb +10 -9
  61. data/spec/dashboard_helpers_spec.rb +19 -18
  62. data/spec/dashboard_spec.rb +122 -38
  63. data/spec/encapsulated_helper_spec.rb +46 -22
  64. data/spec/experiment_catalog_spec.rb +14 -13
  65. data/spec/experiment_spec.rb +198 -118
  66. data/spec/goals_collection_spec.rb +18 -16
  67. data/spec/helper_spec.rb +454 -385
  68. data/spec/metric_spec.rb +14 -14
  69. data/spec/persistence/cookie_adapter_spec.rb +26 -11
  70. data/spec/persistence/dual_adapter_spec.rb +71 -71
  71. data/spec/persistence/redis_adapter_spec.rb +35 -27
  72. data/spec/persistence/session_adapter_spec.rb +2 -3
  73. data/spec/persistence_spec.rb +1 -2
  74. data/spec/redis_interface_spec.rb +25 -82
  75. data/spec/spec_helper.rb +35 -24
  76. data/spec/split_spec.rb +11 -11
  77. data/spec/support/cookies_mock.rb +1 -2
  78. data/spec/trial_spec.rb +102 -75
  79. data/spec/user_spec.rb +60 -29
  80. data/split.gemspec +22 -21
  81. metadata +43 -40
  82. data/.rubocop_todo.yml +0 -679
  83. data/.travis.yml +0 -60
  84. data/Appraisals +0 -19
  85. data/gemfiles/4.2.gemfile +0 -9
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Split
3
4
  class Experiment
4
5
  attr_accessor :name
@@ -10,29 +11,22 @@ module Split
10
11
  attr_reader :resettable
11
12
 
12
13
  DEFAULT_OPTIONS = {
13
- :resettable => true
14
+ resettable: true
14
15
  }
15
16
 
17
+ def self.find(name)
18
+ Split.cache(:experiments, name) do
19
+ return unless Split.redis.exists?(name)
20
+ Experiment.new(name).tap { |exp| exp.load_from_redis }
21
+ end
22
+ end
23
+
16
24
  def initialize(name, options = {})
17
25
  options = DEFAULT_OPTIONS.merge(options)
18
26
 
19
27
  @name = name.to_s
20
28
 
21
- alternatives = extract_alternatives_from_options(options)
22
-
23
- if alternatives.empty? && (exp_config = Split.configuration.experiment_for(name))
24
- options = {
25
- alternatives: load_alternatives_from_configuration,
26
- goals: Split::GoalsCollection.new(@name).load_from_configuration,
27
- metadata: load_metadata_from_configuration,
28
- resettable: exp_config[:resettable],
29
- algorithm: exp_config[:algorithm]
30
- }
31
- else
32
- options[:alternatives] = alternatives
33
- end
34
-
35
- set_alternatives_and_options(options)
29
+ extract_alternatives_from_options(options)
36
30
  end
37
31
 
38
32
  def self.finished_key(key)
@@ -40,11 +34,15 @@ module Split
40
34
  end
41
35
 
42
36
  def set_alternatives_and_options(options)
43
- self.alternatives = options[:alternatives]
44
- self.goals = options[:goals]
45
- self.resettable = options[:resettable]
46
- self.algorithm = options[:algorithm]
47
- self.metadata = options[:metadata]
37
+ options_with_defaults = DEFAULT_OPTIONS.merge(
38
+ options.reject { |k, v| v.nil? }
39
+ )
40
+
41
+ self.alternatives = options_with_defaults[:alternatives]
42
+ self.goals = options_with_defaults[:goals]
43
+ self.resettable = options_with_defaults[:resettable]
44
+ self.algorithm = options_with_defaults[:algorithm]
45
+ self.metadata = options_with_defaults[:metadata]
48
46
  end
49
47
 
50
48
  def extract_alternatives_from_options(options)
@@ -52,7 +50,7 @@ module Split
52
50
 
53
51
  if alts.length == 1
54
52
  if alts[0].is_a? Hash
55
- alts = alts[0].map{|k,v| {k => v} }
53
+ alts = alts[0].map { |k, v| { k => v } }
56
54
  end
57
55
  end
58
56
 
@@ -87,8 +85,8 @@ module Split
87
85
  persist_experiment_configuration
88
86
  end
89
87
 
90
- redis.hset(experiment_config_key, :resettable, resettable)
91
- redis.hset(experiment_config_key, :algorithm, algorithm.to_s)
88
+ redis.hmset(experiment_config_key, :resettable, resettable.to_s,
89
+ :algorithm, algorithm.to_s)
92
90
  self
93
91
  end
94
92
 
@@ -96,12 +94,12 @@ module Split
96
94
  if @alternatives.empty? && Split.configuration.experiment_for(@name).nil?
97
95
  raise ExperimentNotFound.new("Experiment #{@name} not found")
98
96
  end
99
- @alternatives.each {|a| a.validate! }
97
+ @alternatives.each { |a| a.validate! }
100
98
  goals_collection.validate!
101
99
  end
102
100
 
103
101
  def new_record?
104
- !redis.exists(name)
102
+ ExperimentCatalog.find(name).nil?
105
103
  end
106
104
 
107
105
  def ==(obj)
@@ -109,7 +107,7 @@ module Split
109
107
  end
110
108
 
111
109
  def [](name)
112
- alternatives.find{|a| a.name == name}
110
+ alternatives.find { |a| a.name == name }
113
111
  end
114
112
 
115
113
  def algorithm
@@ -121,7 +119,7 @@ module Split
121
119
  end
122
120
 
123
121
  def resettable=(resettable)
124
- @resettable = resettable.is_a?(String) ? resettable == 'true' : resettable
122
+ @resettable = resettable.is_a?(String) ? resettable == "true" : resettable
125
123
  end
126
124
 
127
125
  def alternatives=(alts)
@@ -135,11 +133,13 @@ module Split
135
133
  end
136
134
 
137
135
  def winner
138
- experiment_winner = redis.hget(:experiment_winner, name)
139
- if experiment_winner
140
- Split::Alternative.new(experiment_winner, name)
141
- else
142
- nil
136
+ Split.cache(:experiment_winner, name) do
137
+ experiment_winner = redis.hget(:experiment_winner, name)
138
+ if experiment_winner
139
+ Split::Alternative.new(experiment_winner, name)
140
+ else
141
+ nil
142
+ end
143
143
  end
144
144
  end
145
145
 
@@ -151,10 +151,11 @@ module Split
151
151
  def winner=(winner_name)
152
152
  redis.hset(:experiment_winner, name, winner_name.to_s)
153
153
  @has_winner = true
154
+ Split.configuration.on_experiment_winner_choose.call(self)
154
155
  end
155
156
 
156
157
  def participant_count
157
- alternatives.inject(0){|sum,a| sum + a.participant_count}
158
+ alternatives.inject(0) { |sum, a| sum + a.participant_count }
158
159
  end
159
160
 
160
161
  def control
@@ -164,6 +165,7 @@ module Split
164
165
  def reset_winner
165
166
  redis.hdel(:experiment_winner, name)
166
167
  @has_winner = false
168
+ Split::Cache.clear_key(@name)
167
169
  end
168
170
 
169
171
  def start
@@ -171,13 +173,15 @@ module Split
171
173
  end
172
174
 
173
175
  def start_time
174
- t = redis.hget(:experiment_start_times, @name)
175
- if t
176
- # Check if stored time is an integer
177
- if t =~ /^[-+]?[0-9]+$/
178
- Time.at(t.to_i)
179
- else
180
- Time.parse(t)
176
+ Split.cache(:experiment_start_times, @name) do
177
+ t = redis.hget(:experiment_start_times, @name)
178
+ if t
179
+ # Check if stored time is an integer
180
+ if t =~ /^[-+]?[0-9]+$/
181
+ Time.at(t.to_i)
182
+ else
183
+ Time.parse(t)
184
+ end
181
185
  end
182
186
  end
183
187
  end
@@ -228,6 +232,7 @@ module Split
228
232
 
229
233
  def reset
230
234
  Split.configuration.on_before_experiment_reset.call(self)
235
+ Split::Cache.clear_key(@name)
231
236
  alternatives.each(&:reset)
232
237
  reset_winner
233
238
  Split.configuration.on_experiment_reset.call(self)
@@ -241,6 +246,7 @@ module Split
241
246
  end
242
247
  reset_winner
243
248
  redis.srem(:experiments, name)
249
+ remove_experiment_cohorting
244
250
  remove_experiment_configuration
245
251
  Split.configuration.on_experiment_delete.call(self)
246
252
  increment_version
@@ -254,8 +260,8 @@ module Split
254
260
  exp_config = redis.hgetall(experiment_config_key)
255
261
 
256
262
  options = {
257
- resettable: exp_config['resettable'],
258
- algorithm: exp_config['algorithm'],
263
+ resettable: exp_config["resettable"],
264
+ algorithm: exp_config["algorithm"],
259
265
  alternatives: load_alternatives_from_redis,
260
266
  goals: Split::GoalsCollection.new(@name).load_from_redis,
261
267
  metadata: load_metadata_from_redis
@@ -264,7 +270,16 @@ module Split
264
270
  set_alternatives_and_options(options)
265
271
  end
266
272
 
273
+ def can_calculate_winning_alternatives?
274
+ self.alternatives.all? do |alternative|
275
+ alternative.participant_count >= 0 &&
276
+ (alternative.participant_count >= alternative.completed_count)
277
+ end
278
+ end
279
+
267
280
  def calc_winning_alternatives
281
+ return unless can_calculate_winning_alternatives?
282
+
268
283
  # Cache the winning alternatives so we recalculate them once per the specified interval.
269
284
  intervals_since_epoch =
270
285
  Time.now.utc.to_i / Split.configuration.winning_alternative_recalculation_interval
@@ -320,11 +335,11 @@ module Split
320
335
  winning_counts.each do |alternative, wins|
321
336
  alternative_probabilities[alternative] = wins / number_of_simulations.to_f
322
337
  end
323
- return alternative_probabilities
338
+ alternative_probabilities
324
339
  end
325
340
 
326
341
  def count_simulated_wins(winning_alternatives)
327
- # initialize a hash to keep track of winning alternative in simulations
342
+ # initialize a hash to keep track of winning alternative in simulations
328
343
  winning_counts = {}
329
344
  alternatives.each do |alternative|
330
345
  winning_counts[alternative] = 0
@@ -333,37 +348,33 @@ module Split
333
348
  winning_alternatives.each do |alternative|
334
349
  winning_counts[alternative] += 1
335
350
  end
336
- return winning_counts
351
+ winning_counts
337
352
  end
338
353
 
339
354
  def find_simulated_winner(simulated_cr_hash)
340
355
  # figure out which alternative had the highest simulated conversion rate
341
- winning_pair = ["",0.0]
356
+ winning_pair = ["", 0.0]
342
357
  simulated_cr_hash.each do |alternative, rate|
343
358
  if rate > winning_pair[1]
344
359
  winning_pair = [alternative, rate]
345
360
  end
346
361
  end
347
362
  winner = winning_pair[0]
348
- return winner
363
+ winner
349
364
  end
350
365
 
351
366
  def calc_simulated_conversion_rates(beta_params)
352
- # initialize a random variable (from which to simulate conversion rates ~beta-distributed)
353
- rand = SimpleRandom.new
354
- rand.set_seed
355
-
356
367
  simulated_cr_hash = {}
357
368
 
358
369
  # create a hash which has the conversion rate pulled from each alternative's beta distribution
359
370
  beta_params.each do |alternative, params|
360
371
  alpha = params[0]
361
372
  beta = params[1]
362
- simulated_conversion_rate = rand.beta(alpha, beta)
373
+ simulated_conversion_rate = Split::Algorithms.beta_distribution_rng(alpha, beta)
363
374
  simulated_cr_hash[alternative] = simulated_conversion_rate
364
375
  end
365
376
 
366
- return simulated_cr_hash
377
+ simulated_cr_hash
367
378
  end
368
379
 
369
380
  def calc_beta_params(goal = nil)
@@ -377,7 +388,7 @@ module Split
377
388
 
378
389
  beta_params[alternative] = params
379
390
  end
380
- return beta_params
391
+ beta_params
381
392
  end
382
393
 
383
394
  def calc_time=(time)
@@ -390,93 +401,109 @@ module Split
390
401
 
391
402
  def jstring(goal = nil)
392
403
  js_id = if goal.nil?
393
- name
394
- else
395
- name + "-" + goal
396
- end
397
- js_id.gsub('/', '--')
404
+ name
405
+ else
406
+ name + "-" + goal
407
+ end
408
+ js_id.gsub("/", "--")
398
409
  end
399
410
 
400
- protected
401
-
402
- def experiment_config_key
403
- "experiment_configurations/#{@name}"
411
+ def cohorting_disabled?
412
+ @cohorting_disabled ||= begin
413
+ value = redis.hget(experiment_config_key, :cohorting)
414
+ value.nil? ? false : value.downcase == "true"
415
+ end
404
416
  end
405
417
 
406
- def load_metadata_from_configuration
407
- Split.configuration.experiment_for(@name)[:metadata]
418
+ def disable_cohorting
419
+ @cohorting_disabled = true
420
+ redis.hset(experiment_config_key, :cohorting, true.to_s)
408
421
  end
409
422
 
410
- def load_metadata_from_redis
411
- meta = redis.get(metadata_key)
412
- JSON.parse(meta) unless meta.nil?
423
+ def enable_cohorting
424
+ @cohorting_disabled = false
425
+ redis.hset(experiment_config_key, :cohorting, false.to_s)
413
426
  end
414
427
 
415
- def load_alternatives_from_configuration
416
- alts = Split.configuration.experiment_for(@name)[:alternatives]
417
- raise ArgumentError, "Experiment configuration is missing :alternatives array" unless alts
418
- if alts.is_a?(Hash)
419
- alts.keys
420
- else
421
- alts.flatten
428
+ protected
429
+ def experiment_config_key
430
+ "experiment_configurations/#{@name}"
422
431
  end
423
- end
424
432
 
425
- def load_alternatives_from_redis
426
- alternatives = case redis.type(@name)
427
- when 'set' # convert legacy sets to lists
428
- alts = redis.smembers(@name)
429
- redis.del(@name)
430
- alts.reverse.each {|a| redis.lpush(@name, a) }
431
- redis.lrange(@name, 0, -1)
432
- else
433
- redis.lrange(@name, 0, -1)
434
- end
435
- alternatives.map do |alt|
436
- alt = begin
437
- JSON.parse(alt)
438
- rescue
439
- alt
440
- end
441
- Split::Alternative.new(alt, @name)
433
+ def load_metadata_from_configuration
434
+ Split.configuration.experiment_for(@name)[:metadata]
435
+ end
436
+
437
+ def load_metadata_from_redis
438
+ meta = redis.get(metadata_key)
439
+ JSON.parse(meta) unless meta.nil?
440
+ end
441
+
442
+ def load_alternatives_from_configuration
443
+ alts = Split.configuration.experiment_for(@name)[:alternatives]
444
+ raise ArgumentError, "Experiment configuration is missing :alternatives array" unless alts
445
+ if alts.is_a?(Hash)
446
+ alts.keys
447
+ else
448
+ alts.flatten
449
+ end
450
+ end
451
+
452
+ def load_alternatives_from_redis
453
+ alternatives = redis.lrange(@name, 0, -1)
454
+ alternatives.map do |alt|
455
+ alt = begin
456
+ JSON.parse(alt)
457
+ rescue
458
+ alt
459
+ end
460
+ Split::Alternative.new(alt, @name)
461
+ end
442
462
  end
443
- end
444
463
 
445
464
  private
465
+ def redis
466
+ Split.redis
467
+ end
446
468
 
447
- def redis
448
- Split.redis
449
- end
469
+ def redis_interface
470
+ RedisInterface.new
471
+ end
450
472
 
451
- def redis_interface
452
- RedisInterface.new
453
- end
473
+ def persist_experiment_configuration
474
+ redis_interface.add_to_set(:experiments, name)
475
+ redis_interface.persist_list(name, @alternatives.map { |alt| { alt.name => alt.weight }.to_json })
476
+ goals_collection.save
454
477
 
455
- def persist_experiment_configuration
456
- redis_interface.add_to_set(:experiments, name)
457
- redis_interface.persist_list(name, @alternatives.map{|alt| {alt.name => alt.weight}.to_json})
458
- goals_collection.save
459
- redis.set(metadata_key, @metadata.to_json) unless @metadata.nil?
460
- end
478
+ if @metadata
479
+ redis.set(metadata_key, @metadata.to_json)
480
+ else
481
+ delete_metadata
482
+ end
483
+ end
461
484
 
462
- def remove_experiment_configuration
463
- @alternatives.each(&:delete)
464
- goals_collection.delete
465
- delete_metadata
466
- redis.del(@name)
467
- end
485
+ def remove_experiment_configuration
486
+ @alternatives.each(&:delete)
487
+ goals_collection.delete
488
+ delete_metadata
489
+ redis.del(@name)
490
+ end
468
491
 
469
- def experiment_configuration_has_changed?
470
- existing_alternatives = load_alternatives_from_redis
471
- existing_goals = Split::GoalsCollection.new(@name).load_from_redis
472
- existing_metadata = load_metadata_from_redis
473
- existing_alternatives.map(&:to_s) != @alternatives.map(&:to_s) ||
474
- existing_goals != @goals ||
475
- existing_metadata != @metadata
476
- end
492
+ def experiment_configuration_has_changed?
493
+ existing_experiment = Experiment.find(@name)
477
494
 
478
- def goals_collection
479
- Split::GoalsCollection.new(@name, @goals)
480
- end
495
+ existing_experiment.alternatives.map(&:to_s) != @alternatives.map(&:to_s) ||
496
+ existing_experiment.goals != @goals ||
497
+ existing_experiment.metadata != @metadata
498
+ end
499
+
500
+ def goals_collection
501
+ Split::GoalsCollection.new(@name, @goals)
502
+ end
503
+
504
+ def remove_experiment_cohorting
505
+ @cohorting_disabled = false
506
+ redis.hdel(experiment_config_key, :cohorting)
507
+ end
481
508
  end
482
509
  end
@@ -1,33 +1,33 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Split
3
4
  class ExperimentCatalog
4
5
  # Return all experiments
5
6
  def self.all
6
7
  # Call compact to prevent nil experiments from being returned -- seems to happen during gem upgrades
7
- Split.redis.smembers(:experiments).map {|e| find(e)}.compact
8
+ Split.redis.smembers(:experiments).map { |e| find(e) }.compact
8
9
  end
9
10
 
10
11
  # Return experiments without a winner (considered "active") first
11
12
  def self.all_active_first
12
- all.partition{|e| not e.winner}.map{|es| es.sort_by(&:name)}.flatten
13
+ all.partition { |e| not e.winner }.map { |es| es.sort_by(&:name) }.flatten
13
14
  end
14
15
 
15
16
  def self.find(name)
16
- return unless Split.redis.exists(name)
17
- Experiment.new(name).tap { |exp| exp.load_from_redis }
17
+ Experiment.find(name)
18
18
  end
19
19
 
20
20
  def self.find_or_initialize(metric_descriptor, control = nil, *alternatives)
21
21
  # Check if array is passed to ab_test
22
22
  # e.g. ab_test('name', ['Alt 1', 'Alt 2', 'Alt 3'])
23
- if control.is_a? Array and alternatives.length.zero?
23
+ if control.is_a?(Array) && alternatives.length.zero?
24
24
  control, alternatives = control.first, control[1..-1]
25
25
  end
26
26
 
27
27
  experiment_name_with_version, goals = normalize_experiment(metric_descriptor)
28
- experiment_name = experiment_name_with_version.to_s.split(':')[0]
28
+ experiment_name = experiment_name_with_version.to_s.split(":")[0]
29
29
  Split::Experiment.new(experiment_name,
30
- :alternatives => [control].compact + alternatives, :goals => goals)
30
+ alternatives: [control].compact + alternatives, goals: goals)
31
31
  end
32
32
 
33
33
  def self.find_or_create(metric_descriptor, control = nil, *alternatives)
@@ -46,6 +46,5 @@ module Split
46
46
  return experiment_name, goals
47
47
  end
48
48
  private_class_method :normalize_experiment
49
-
50
49
  end
51
50
  end
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  class String
3
4
  # Constatntize is often provided by ActiveSupport, but ActiveSupport is not a dependency of Split.
4
5
  unless method_defined?(:constantize)
5
6
  def constantize
6
- names = self.split('::')
7
+ names = self.split("::")
7
8
  names.shift if names.empty? || names.first.empty?
8
9
 
9
10
  constant = Object
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Split
3
4
  class GoalsCollection
4
-
5
- def initialize(experiment_name, goals=nil)
5
+ def initialize(experiment_name, goals = nil)
6
6
  @experiment_name = experiment_name
7
7
  @goals = goals
8
8
  end
@@ -14,10 +14,10 @@ module Split
14
14
  def load_from_configuration
15
15
  goals = Split.configuration.experiment_for(@experiment_name)[:goals]
16
16
 
17
- if goals.nil?
18
- goals = []
19
- else
17
+ if goals
20
18
  goals.flatten
19
+ else
20
+ []
21
21
  end
22
22
  end
23
23
 
@@ -28,7 +28,7 @@ module Split
28
28
 
29
29
  def validate!
30
30
  unless @goals.nil? || @goals.kind_of?(Array)
31
- raise ArgumentError, 'Goals must be an array'
31
+ raise ArgumentError, "Goals must be an array"
32
32
  end
33
33
  end
34
34
 
@@ -37,9 +37,8 @@ module Split
37
37
  end
38
38
 
39
39
  private
40
-
41
- def goals_key
42
- "#{@experiment_name}:goals"
43
- end
40
+ def goals_key
41
+ "#{@experiment_name}:goals"
42
+ end
44
43
  end
45
44
  end