split 3.2.0 → 4.0.5

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 (87) hide show
  1. checksums.yaml +5 -5
  2. data/.eslintrc +1 -1
  3. data/.github/FUNDING.yml +1 -0
  4. data/.github/ISSUE_TEMPLATE/bug_report.md +24 -0
  5. data/.github/dependabot.yml +7 -0
  6. data/.github/workflows/ci.yml +63 -0
  7. data/.rspec +1 -0
  8. data/.rubocop.yml +67 -1043
  9. data/CHANGELOG.md +174 -0
  10. data/CODE_OF_CONDUCT.md +3 -3
  11. data/CONTRIBUTING.md +1 -1
  12. data/Gemfile +6 -1
  13. data/README.md +79 -33
  14. data/Rakefile +6 -5
  15. data/lib/split/algorithms/block_randomization.rb +7 -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 +25 -25
  20. data/lib/split/cache.rb +27 -0
  21. data/lib/split/combined_experiments_helper.rb +6 -5
  22. data/lib/split/configuration.rb +94 -91
  23. data/lib/split/dashboard/helpers.rb +9 -9
  24. data/lib/split/dashboard/pagination_helpers.rb +86 -0
  25. data/lib/split/dashboard/paginator.rb +17 -0
  26. data/lib/split/dashboard/public/dashboard.js +10 -0
  27. data/lib/split/dashboard/public/style.css +19 -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 +24 -5
  31. data/lib/split/dashboard/views/layout.erb +1 -1
  32. data/lib/split/dashboard.rb +47 -20
  33. data/lib/split/encapsulated_helper.rb +15 -8
  34. data/lib/split/engine.rb +7 -4
  35. data/lib/split/exceptions.rb +1 -0
  36. data/lib/split/experiment.rb +160 -122
  37. data/lib/split/experiment_catalog.rb +7 -8
  38. data/lib/split/extensions/string.rb +2 -1
  39. data/lib/split/goals_collection.rb +10 -10
  40. data/lib/split/helper.rb +56 -24
  41. data/lib/split/metric.rb +6 -6
  42. data/lib/split/persistence/cookie_adapter.rb +52 -15
  43. data/lib/split/persistence/dual_adapter.rb +53 -12
  44. data/lib/split/persistence/redis_adapter.rb +8 -4
  45. data/lib/split/persistence/session_adapter.rb +1 -2
  46. data/lib/split/persistence.rb +8 -6
  47. data/lib/split/redis_interface.rb +16 -31
  48. data/lib/split/trial.rb +48 -41
  49. data/lib/split/user.rb +30 -15
  50. data/lib/split/version.rb +2 -4
  51. data/lib/split/zscore.rb +2 -3
  52. data/lib/split.rb +39 -25
  53. data/spec/algorithms/block_randomization_spec.rb +6 -5
  54. data/spec/algorithms/weighted_sample_spec.rb +6 -5
  55. data/spec/algorithms/whiplash_spec.rb +4 -5
  56. data/spec/alternative_spec.rb +35 -36
  57. data/spec/cache_spec.rb +84 -0
  58. data/spec/combined_experiments_helper_spec.rb +18 -17
  59. data/spec/configuration_spec.rb +41 -45
  60. data/spec/dashboard/pagination_helpers_spec.rb +202 -0
  61. data/spec/dashboard/paginator_spec.rb +38 -0
  62. data/spec/dashboard_helpers_spec.rb +19 -18
  63. data/spec/dashboard_spec.rb +153 -48
  64. data/spec/encapsulated_helper_spec.rb +47 -23
  65. data/spec/experiment_catalog_spec.rb +14 -13
  66. data/spec/experiment_spec.rb +224 -111
  67. data/spec/goals_collection_spec.rb +18 -16
  68. data/spec/helper_spec.rb +539 -419
  69. data/spec/metric_spec.rb +14 -14
  70. data/spec/persistence/cookie_adapter_spec.rb +105 -27
  71. data/spec/persistence/dual_adapter_spec.rb +158 -66
  72. data/spec/persistence/redis_adapter_spec.rb +35 -27
  73. data/spec/persistence/session_adapter_spec.rb +2 -3
  74. data/spec/persistence_spec.rb +1 -2
  75. data/spec/redis_interface_spec.rb +25 -82
  76. data/spec/spec_helper.rb +38 -24
  77. data/spec/split_spec.rb +18 -18
  78. data/spec/support/cookies_mock.rb +1 -2
  79. data/spec/trial_spec.rb +117 -70
  80. data/spec/user_spec.rb +69 -27
  81. data/split.gemspec +26 -22
  82. metadata +85 -37
  83. data/.travis.yml +0 -41
  84. data/Appraisals +0 -13
  85. data/gemfiles/4.2.gemfile +0 -9
  86. data/gemfiles/5.0.gemfile +0 -10
  87. data/gemfiles/5.1.gemfile +0 -10
@@ -1,38 +1,32 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Split
3
4
  class Experiment
4
5
  attr_accessor :name
5
- attr_writer :algorithm
6
- attr_accessor :resettable
7
6
  attr_accessor :goals
8
- attr_accessor :alternatives
9
7
  attr_accessor :alternative_probabilities
10
8
  attr_accessor :metadata
11
9
 
10
+ attr_reader :alternatives
11
+ attr_reader :resettable
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
 
@@ -81,14 +79,14 @@ module Split
81
79
 
82
80
  if new_record?
83
81
  start unless Split.configuration.start_manually
82
+ persist_experiment_configuration
84
83
  elsif experiment_configuration_has_changed?
85
84
  reset unless Split.configuration.reset_manually
85
+ persist_experiment_configuration
86
86
  end
87
87
 
88
- persist_experiment_configuration if new_record? || experiment_configuration_has_changed?
89
-
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,24 +133,29 @@ 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
 
146
146
  def has_winner?
147
- !winner.nil?
147
+ return @has_winner if defined? @has_winner
148
+ @has_winner = !winner.nil?
148
149
  end
149
150
 
150
151
  def winner=(winner_name)
151
152
  redis.hset(:experiment_winner, name, winner_name.to_s)
153
+ @has_winner = true
154
+ Split.configuration.on_experiment_winner_choose.call(self)
152
155
  end
153
156
 
154
157
  def participant_count
155
- alternatives.inject(0){|sum,a| sum + a.participant_count}
158
+ alternatives.inject(0) { |sum, a| sum + a.participant_count }
156
159
  end
157
160
 
158
161
  def control
@@ -161,6 +164,8 @@ module Split
161
164
 
162
165
  def reset_winner
163
166
  redis.hdel(:experiment_winner, name)
167
+ @has_winner = false
168
+ Split::Cache.clear_key(@name)
164
169
  end
165
170
 
166
171
  def start
@@ -168,13 +173,15 @@ module Split
168
173
  end
169
174
 
170
175
  def start_time
171
- t = redis.hget(:experiment_start_times, @name)
172
- if t
173
- # Check if stored time is an integer
174
- if t =~ /^[-+]?[0-9]+$/
175
- Time.at(t.to_i)
176
- else
177
- 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
178
185
  end
179
186
  end
180
187
  end
@@ -225,6 +232,7 @@ module Split
225
232
 
226
233
  def reset
227
234
  Split.configuration.on_before_experiment_reset.call(self)
235
+ Split::Cache.clear_key(@name)
228
236
  alternatives.each(&:reset)
229
237
  reset_winner
230
238
  Split.configuration.on_experiment_reset.call(self)
@@ -238,6 +246,7 @@ module Split
238
246
  end
239
247
  reset_winner
240
248
  redis.srem(:experiments, name)
249
+ remove_experiment_cohorting
241
250
  remove_experiment_configuration
242
251
  Split.configuration.on_experiment_delete.call(self)
243
252
  increment_version
@@ -251,8 +260,8 @@ module Split
251
260
  exp_config = redis.hgetall(experiment_config_key)
252
261
 
253
262
  options = {
254
- resettable: exp_config['resettable'],
255
- algorithm: exp_config['algorithm'],
263
+ resettable: exp_config["resettable"],
264
+ algorithm: exp_config["algorithm"],
256
265
  alternatives: load_alternatives_from_redis,
257
266
  goals: Split::GoalsCollection.new(@name).load_from_redis,
258
267
  metadata: load_metadata_from_redis
@@ -261,7 +270,16 @@ module Split
261
270
  set_alternatives_and_options(options)
262
271
  end
263
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
+
264
280
  def calc_winning_alternatives
281
+ return unless can_calculate_winning_alternatives?
282
+
265
283
  # Cache the winning alternatives so we recalculate them once per the specified interval.
266
284
  intervals_since_epoch =
267
285
  Time.now.utc.to_i / Split.configuration.winning_alternative_recalculation_interval
@@ -317,11 +335,11 @@ module Split
317
335
  winning_counts.each do |alternative, wins|
318
336
  alternative_probabilities[alternative] = wins / number_of_simulations.to_f
319
337
  end
320
- return alternative_probabilities
338
+ alternative_probabilities
321
339
  end
322
340
 
323
341
  def count_simulated_wins(winning_alternatives)
324
- # initialize a hash to keep track of winning alternative in simulations
342
+ # initialize a hash to keep track of winning alternative in simulations
325
343
  winning_counts = {}
326
344
  alternatives.each do |alternative|
327
345
  winning_counts[alternative] = 0
@@ -330,37 +348,33 @@ module Split
330
348
  winning_alternatives.each do |alternative|
331
349
  winning_counts[alternative] += 1
332
350
  end
333
- return winning_counts
351
+ winning_counts
334
352
  end
335
353
 
336
354
  def find_simulated_winner(simulated_cr_hash)
337
355
  # figure out which alternative had the highest simulated conversion rate
338
- winning_pair = ["",0.0]
356
+ winning_pair = ["", 0.0]
339
357
  simulated_cr_hash.each do |alternative, rate|
340
358
  if rate > winning_pair[1]
341
359
  winning_pair = [alternative, rate]
342
360
  end
343
361
  end
344
362
  winner = winning_pair[0]
345
- return winner
363
+ winner
346
364
  end
347
365
 
348
366
  def calc_simulated_conversion_rates(beta_params)
349
- # initialize a random variable (from which to simulate conversion rates ~beta-distributed)
350
- rand = SimpleRandom.new
351
- rand.set_seed
352
-
353
367
  simulated_cr_hash = {}
354
368
 
355
369
  # create a hash which has the conversion rate pulled from each alternative's beta distribution
356
370
  beta_params.each do |alternative, params|
357
371
  alpha = params[0]
358
372
  beta = params[1]
359
- simulated_conversion_rate = rand.beta(alpha, beta)
373
+ simulated_conversion_rate = Split::Algorithms.beta_distribution_rng(alpha, beta)
360
374
  simulated_cr_hash[alternative] = simulated_conversion_rate
361
375
  end
362
376
 
363
- return simulated_cr_hash
377
+ simulated_cr_hash
364
378
  end
365
379
 
366
380
  def calc_beta_params(goal = nil)
@@ -374,7 +388,7 @@ module Split
374
388
 
375
389
  beta_params[alternative] = params
376
390
  end
377
- return beta_params
391
+ beta_params
378
392
  end
379
393
 
380
394
  def calc_time=(time)
@@ -387,85 +401,109 @@ module Split
387
401
 
388
402
  def jstring(goal = nil)
389
403
  js_id = if goal.nil?
390
- name
391
- else
392
- name + "-" + goal
393
- end
394
- js_id.gsub('/', '--')
404
+ name
405
+ else
406
+ name + "-" + goal
407
+ end
408
+ js_id.gsub("/", "--")
395
409
  end
396
410
 
397
- protected
398
-
399
- def experiment_config_key
400
- "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
401
416
  end
402
417
 
403
- def load_metadata_from_configuration
404
- Split.configuration.experiment_for(@name)[:metadata]
418
+ def disable_cohorting
419
+ @cohorting_disabled = true
420
+ redis.hset(experiment_config_key, :cohorting, true.to_s)
405
421
  end
406
422
 
407
- def load_metadata_from_redis
408
- meta = redis.get(metadata_key)
409
- JSON.parse(meta) unless meta.nil?
423
+ def enable_cohorting
424
+ @cohorting_disabled = false
425
+ redis.hset(experiment_config_key, :cohorting, false.to_s)
410
426
  end
411
427
 
412
- def load_alternatives_from_configuration
413
- alts = Split.configuration.experiment_for(@name)[:alternatives]
414
- raise ArgumentError, "Experiment configuration is missing :alternatives array" unless alts
415
- if alts.is_a?(Hash)
416
- alts.keys
417
- else
418
- alts.flatten
428
+ protected
429
+ def experiment_config_key
430
+ "experiment_configurations/#{@name}"
419
431
  end
420
- end
421
432
 
422
- def load_alternatives_from_redis
423
- case redis.type(@name)
424
- when 'set' # convert legacy sets to lists
425
- alts = redis.smembers(@name)
426
- redis.del(@name)
427
- alts.reverse.each {|a| redis.lpush(@name, a) }
428
- redis.lrange(@name, 0, -1)
429
- else
430
- redis.lrange(@name, 0, -1)
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
431
462
  end
432
- end
433
463
 
434
464
  private
465
+ def redis
466
+ Split.redis
467
+ end
435
468
 
436
- def redis
437
- Split.redis
438
- end
469
+ def redis_interface
470
+ RedisInterface.new
471
+ end
439
472
 
440
- def redis_interface
441
- RedisInterface.new
442
- 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
443
477
 
444
- def persist_experiment_configuration
445
- redis_interface.add_to_set(:experiments, name)
446
- redis_interface.persist_list(name, @alternatives.map(&:name))
447
- goals_collection.save
448
- redis.set(metadata_key, @metadata.to_json) unless @metadata.nil?
449
- end
478
+ if @metadata
479
+ redis.set(metadata_key, @metadata.to_json)
480
+ else
481
+ delete_metadata
482
+ end
483
+ end
450
484
 
451
- def remove_experiment_configuration
452
- @alternatives.each(&:delete)
453
- goals_collection.delete
454
- delete_metadata
455
- redis.del(@name)
456
- end
485
+ def remove_experiment_configuration
486
+ @alternatives.each(&:delete)
487
+ goals_collection.delete
488
+ delete_metadata
489
+ redis.del(@name)
490
+ end
457
491
 
458
- def experiment_configuration_has_changed?
459
- existing_alternatives = load_alternatives_from_redis
460
- existing_goals = Split::GoalsCollection.new(@name).load_from_redis
461
- existing_metadata = load_metadata_from_redis
462
- existing_alternatives != @alternatives.map(&:name) ||
463
- existing_goals != @goals ||
464
- existing_metadata != @metadata
465
- end
492
+ def experiment_configuration_has_changed?
493
+ existing_experiment = Experiment.find(@name)
466
494
 
467
- def goals_collection
468
- Split::GoalsCollection.new(@name, @goals)
469
- 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
470
508
  end
471
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,7 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Split
2
4
  class GoalsCollection
3
-
4
- def initialize(experiment_name, goals=nil)
5
+ def initialize(experiment_name, goals = nil)
5
6
  @experiment_name = experiment_name
6
7
  @goals = goals
7
8
  end
@@ -13,10 +14,10 @@ module Split
13
14
  def load_from_configuration
14
15
  goals = Split.configuration.experiment_for(@experiment_name)[:goals]
15
16
 
16
- if goals.nil?
17
- goals = []
18
- else
17
+ if goals
19
18
  goals.flatten
19
+ else
20
+ []
20
21
  end
21
22
  end
22
23
 
@@ -27,7 +28,7 @@ module Split
27
28
 
28
29
  def validate!
29
30
  unless @goals.nil? || @goals.kind_of?(Array)
30
- raise ArgumentError, 'Goals must be an array'
31
+ raise ArgumentError, "Goals must be an array"
31
32
  end
32
33
  end
33
34
 
@@ -36,9 +37,8 @@ module Split
36
37
  end
37
38
 
38
39
  private
39
-
40
- def goals_key
41
- "#{@experiment_name}:goals"
42
- end
40
+ def goals_key
41
+ "#{@experiment_name}:goals"
42
+ end
43
43
  end
44
44
  end