split 4.0.1 → 4.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +8 -3
  3. data/.rubocop.yml +2 -5
  4. data/CHANGELOG.md +38 -0
  5. data/CONTRIBUTING.md +1 -1
  6. data/Gemfile +1 -1
  7. data/README.md +11 -3
  8. data/Rakefile +4 -5
  9. data/gemfiles/5.2.gemfile +1 -3
  10. data/gemfiles/6.0.gemfile +1 -3
  11. data/gemfiles/6.1.gemfile +1 -3
  12. data/gemfiles/7.0.gemfile +1 -3
  13. data/lib/split/algorithms/block_randomization.rb +5 -6
  14. data/lib/split/algorithms/whiplash.rb +16 -18
  15. data/lib/split/algorithms.rb +14 -0
  16. data/lib/split/alternative.rb +21 -22
  17. data/lib/split/cache.rb +0 -1
  18. data/lib/split/combined_experiments_helper.rb +4 -4
  19. data/lib/split/configuration.rb +83 -84
  20. data/lib/split/dashboard/helpers.rb +6 -7
  21. data/lib/split/dashboard/pagination_helpers.rb +53 -54
  22. data/lib/split/dashboard/public/style.css +5 -2
  23. data/lib/split/dashboard/views/_experiment.erb +2 -1
  24. data/lib/split/dashboard/views/index.erb +19 -4
  25. data/lib/split/dashboard.rb +29 -23
  26. data/lib/split/encapsulated_helper.rb +4 -6
  27. data/lib/split/experiment.rb +93 -88
  28. data/lib/split/experiment_catalog.rb +6 -5
  29. data/lib/split/extensions/string.rb +1 -1
  30. data/lib/split/goals_collection.rb +8 -10
  31. data/lib/split/helper.rb +20 -20
  32. data/lib/split/metric.rb +4 -5
  33. data/lib/split/persistence/cookie_adapter.rb +44 -47
  34. data/lib/split/persistence/dual_adapter.rb +7 -8
  35. data/lib/split/persistence/redis_adapter.rb +3 -4
  36. data/lib/split/persistence/session_adapter.rb +0 -2
  37. data/lib/split/persistence.rb +4 -4
  38. data/lib/split/redis_interface.rb +7 -1
  39. data/lib/split/trial.rb +23 -24
  40. data/lib/split/user.rb +12 -13
  41. data/lib/split/version.rb +1 -1
  42. data/lib/split/zscore.rb +1 -3
  43. data/lib/split.rb +26 -25
  44. data/spec/algorithms/block_randomization_spec.rb +6 -5
  45. data/spec/algorithms/weighted_sample_spec.rb +6 -5
  46. data/spec/algorithms/whiplash_spec.rb +4 -5
  47. data/spec/alternative_spec.rb +35 -36
  48. data/spec/cache_spec.rb +15 -19
  49. data/spec/combined_experiments_helper_spec.rb +18 -17
  50. data/spec/configuration_spec.rb +32 -38
  51. data/spec/dashboard/pagination_helpers_spec.rb +69 -67
  52. data/spec/dashboard/paginator_spec.rb +10 -9
  53. data/spec/dashboard_helpers_spec.rb +19 -18
  54. data/spec/dashboard_spec.rb +79 -35
  55. data/spec/encapsulated_helper_spec.rb +12 -14
  56. data/spec/experiment_catalog_spec.rb +14 -13
  57. data/spec/experiment_spec.rb +132 -123
  58. data/spec/goals_collection_spec.rb +17 -15
  59. data/spec/helper_spec.rb +415 -382
  60. data/spec/metric_spec.rb +14 -14
  61. data/spec/persistence/cookie_adapter_spec.rb +23 -8
  62. data/spec/persistence/dual_adapter_spec.rb +71 -71
  63. data/spec/persistence/redis_adapter_spec.rb +28 -29
  64. data/spec/persistence/session_adapter_spec.rb +2 -3
  65. data/spec/persistence_spec.rb +1 -2
  66. data/spec/redis_interface_spec.rb +26 -14
  67. data/spec/spec_helper.rb +16 -13
  68. data/spec/split_spec.rb +11 -11
  69. data/spec/support/cookies_mock.rb +1 -2
  70. data/spec/trial_spec.rb +61 -60
  71. data/spec/user_spec.rb +36 -36
  72. data/split.gemspec +21 -20
  73. metadata +25 -14
  74. data/.rubocop_todo.yml +0 -226
  75. data/Appraisals +0 -19
  76. data/gemfiles/5.0.gemfile +0 -9
  77. data/gemfiles/5.1.gemfile +0 -9
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rubystats'
4
-
5
3
  module Split
6
4
  class Experiment
7
5
  attr_accessor :name
@@ -13,7 +11,7 @@ module Split
13
11
  attr_reader :resettable
14
12
 
15
13
  DEFAULT_OPTIONS = {
16
- :resettable => true
14
+ resettable: true
17
15
  }
18
16
 
19
17
  def self.find(name)
@@ -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,7 +85,7 @@ module Split
87
85
  persist_experiment_configuration
88
86
  end
89
87
 
90
- redis.hmset(experiment_config_key, :resettable, resettable,
88
+ redis.hmset(experiment_config_key, :resettable, resettable.to_s,
91
89
  :algorithm, algorithm.to_s)
92
90
  self
93
91
  end
@@ -96,7 +94,7 @@ 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
 
@@ -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)
@@ -157,7 +155,7 @@ module Split
157
155
  end
158
156
 
159
157
  def participant_count
160
- alternatives.inject(0){|sum, a| sum + a.participant_count}
158
+ alternatives.inject(0) { |sum, a| sum + a.participant_count }
161
159
  end
162
160
 
163
161
  def control
@@ -262,8 +260,8 @@ module Split
262
260
  exp_config = redis.hgetall(experiment_config_key)
263
261
 
264
262
  options = {
265
- resettable: exp_config['resettable'],
266
- algorithm: exp_config['algorithm'],
263
+ resettable: exp_config["resettable"],
264
+ algorithm: exp_config["algorithm"],
267
265
  alternatives: load_alternatives_from_redis,
268
266
  goals: Split::GoalsCollection.new(@name).load_from_redis,
269
267
  metadata: load_metadata_from_redis
@@ -272,7 +270,16 @@ module Split
272
270
  set_alternatives_and_options(options)
273
271
  end
274
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
+
275
280
  def calc_winning_alternatives
281
+ return unless can_calculate_winning_alternatives?
282
+
276
283
  # Cache the winning alternatives so we recalculate them once per the specified interval.
277
284
  intervals_since_epoch =
278
285
  Time.now.utc.to_i / Split.configuration.winning_alternative_recalculation_interval
@@ -328,11 +335,11 @@ module Split
328
335
  winning_counts.each do |alternative, wins|
329
336
  alternative_probabilities[alternative] = wins / number_of_simulations.to_f
330
337
  end
331
- return alternative_probabilities
338
+ alternative_probabilities
332
339
  end
333
340
 
334
341
  def count_simulated_wins(winning_alternatives)
335
- # initialize a hash to keep track of winning alternative in simulations
342
+ # initialize a hash to keep track of winning alternative in simulations
336
343
  winning_counts = {}
337
344
  alternatives.each do |alternative|
338
345
  winning_counts[alternative] = 0
@@ -341,7 +348,7 @@ module Split
341
348
  winning_alternatives.each do |alternative|
342
349
  winning_counts[alternative] += 1
343
350
  end
344
- return winning_counts
351
+ winning_counts
345
352
  end
346
353
 
347
354
  def find_simulated_winner(simulated_cr_hash)
@@ -353,7 +360,7 @@ module Split
353
360
  end
354
361
  end
355
362
  winner = winning_pair[0]
356
- return winner
363
+ winner
357
364
  end
358
365
 
359
366
  def calc_simulated_conversion_rates(beta_params)
@@ -363,11 +370,11 @@ module Split
363
370
  beta_params.each do |alternative, params|
364
371
  alpha = params[0]
365
372
  beta = params[1]
366
- simulated_conversion_rate = Rubystats::BetaDistribution.new(alpha, beta).rng
373
+ simulated_conversion_rate = Split::Algorithms.beta_distribution_rng(alpha, beta)
367
374
  simulated_cr_hash[alternative] = simulated_conversion_rate
368
375
  end
369
376
 
370
- return simulated_cr_hash
377
+ simulated_cr_hash
371
378
  end
372
379
 
373
380
  def calc_beta_params(goal = nil)
@@ -381,7 +388,7 @@ module Split
381
388
 
382
389
  beta_params[alternative] = params
383
390
  end
384
- return beta_params
391
+ beta_params
385
392
  end
386
393
 
387
394
  def calc_time=(time)
@@ -394,11 +401,11 @@ module Split
394
401
 
395
402
  def jstring(goal = nil)
396
403
  js_id = if goal.nil?
397
- name
398
- else
399
- name + "-" + goal
400
- end
401
- js_id.gsub('/', '--')
404
+ name
405
+ else
406
+ name + "-" + goal
407
+ end
408
+ js_id.gsub("/", "--")
402
409
  end
403
410
 
404
411
  def cohorting_disabled?
@@ -410,95 +417,93 @@ module Split
410
417
 
411
418
  def disable_cohorting
412
419
  @cohorting_disabled = true
413
- redis.hset(experiment_config_key, :cohorting, true)
420
+ redis.hset(experiment_config_key, :cohorting, true.to_s)
414
421
  end
415
422
 
416
423
  def enable_cohorting
417
424
  @cohorting_disabled = false
418
- redis.hset(experiment_config_key, :cohorting, false)
425
+ redis.hset(experiment_config_key, :cohorting, false.to_s)
419
426
  end
420
427
 
421
428
  protected
429
+ def experiment_config_key
430
+ "experiment_configurations/#{@name}"
431
+ end
422
432
 
423
- def experiment_config_key
424
- "experiment_configurations/#{@name}"
425
- end
426
-
427
- def load_metadata_from_configuration
428
- Split.configuration.experiment_for(@name)[:metadata]
429
- end
433
+ def load_metadata_from_configuration
434
+ Split.configuration.experiment_for(@name)[:metadata]
435
+ end
430
436
 
431
- def load_metadata_from_redis
432
- meta = redis.get(metadata_key)
433
- JSON.parse(meta) unless meta.nil?
434
- end
437
+ def load_metadata_from_redis
438
+ meta = redis.get(metadata_key)
439
+ JSON.parse(meta) unless meta.nil?
440
+ end
435
441
 
436
- def load_alternatives_from_configuration
437
- alts = Split.configuration.experiment_for(@name)[:alternatives]
438
- raise ArgumentError, "Experiment configuration is missing :alternatives array" unless alts
439
- if alts.is_a?(Hash)
440
- alts.keys
441
- else
442
- alts.flatten
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
443
450
  end
444
- end
445
451
 
446
- def load_alternatives_from_redis
447
- alternatives = redis.lrange(@name, 0, -1)
448
- alternatives.map do |alt|
449
- alt = begin
450
- JSON.parse(alt)
451
- rescue
452
- alt
453
- end
454
- Split::Alternative.new(alt, @name)
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
455
462
  end
456
- end
457
463
 
458
464
  private
465
+ def redis
466
+ Split.redis
467
+ end
459
468
 
460
- def redis
461
- Split.redis
462
- end
469
+ def redis_interface
470
+ RedisInterface.new
471
+ end
463
472
 
464
- def redis_interface
465
- RedisInterface.new
466
- 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
467
477
 
468
- def persist_experiment_configuration
469
- redis_interface.add_to_set(:experiments, name)
470
- redis_interface.persist_list(name, @alternatives.map{|alt| {alt.name => alt.weight}.to_json})
471
- goals_collection.save
478
+ if @metadata
479
+ redis.set(metadata_key, @metadata.to_json)
480
+ else
481
+ delete_metadata
482
+ end
483
+ end
472
484
 
473
- if @metadata
474
- redis.set(metadata_key, @metadata.to_json)
475
- else
485
+ def remove_experiment_configuration
486
+ @alternatives.each(&:delete)
487
+ goals_collection.delete
476
488
  delete_metadata
489
+ redis.del(@name)
477
490
  end
478
- end
479
491
 
480
- def remove_experiment_configuration
481
- @alternatives.each(&:delete)
482
- goals_collection.delete
483
- delete_metadata
484
- redis.del(@name)
485
- end
486
-
487
- def experiment_configuration_has_changed?
488
- existing_experiment = Experiment.find(@name)
492
+ def experiment_configuration_has_changed?
493
+ existing_experiment = Experiment.find(@name)
489
494
 
490
- existing_experiment.alternatives.map(&:to_s) != @alternatives.map(&:to_s) ||
491
- existing_experiment.goals != @goals ||
492
- existing_experiment.metadata != @metadata
493
- end
495
+ existing_experiment.alternatives.map(&:to_s) != @alternatives.map(&:to_s) ||
496
+ existing_experiment.goals != @goals ||
497
+ existing_experiment.metadata != @metadata
498
+ end
494
499
 
495
- def goals_collection
496
- Split::GoalsCollection.new(@name, @goals)
497
- end
500
+ def goals_collection
501
+ Split::GoalsCollection.new(@name, @goals)
502
+ end
498
503
 
499
- def remove_experiment_cohorting
500
- @cohorting_disabled = false
501
- redis.hdel(experiment_config_key, :cohorting)
502
- end
504
+ def remove_experiment_cohorting
505
+ @cohorting_disabled = false
506
+ redis.hdel(experiment_config_key, :cohorting)
507
+ end
503
508
  end
504
509
  end
@@ -1,15 +1,16 @@
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)
@@ -19,14 +20,14 @@ module Split
19
20
  def self.find_or_initialize(metric_descriptor, control = nil, *alternatives)
20
21
  # Check if array is passed to ab_test
21
22
  # e.g. ab_test('name', ['Alt 1', 'Alt 2', 'Alt 3'])
22
- if control.is_a? Array and alternatives.length.zero?
23
+ if control.is_a?(Array) && alternatives.length.zero?
23
24
  control, alternatives = control.first, control[1..-1]
24
25
  end
25
26
 
26
27
  experiment_name_with_version, goals = normalize_experiment(metric_descriptor)
27
- experiment_name = experiment_name_with_version.to_s.split(':')[0]
28
+ experiment_name = experiment_name_with_version.to_s.split(":")[0]
28
29
  Split::Experiment.new(experiment_name,
29
- :alternatives => [control].compact + alternatives, :goals => goals)
30
+ alternatives: [control].compact + alternatives, goals: goals)
30
31
  end
31
32
 
32
33
  def self.find_or_create(metric_descriptor, control = nil, *alternatives)
@@ -4,7 +4,7 @@ class String
4
4
  # Constatntize is often provided by ActiveSupport, but ActiveSupport is not a dependency of Split.
5
5
  unless method_defined?(:constantize)
6
6
  def constantize
7
- names = self.split('::')
7
+ names = self.split("::")
8
8
  names.shift if names.empty? || names.first.empty?
9
9
 
10
10
  constant = Object
@@ -2,8 +2,7 @@
2
2
 
3
3
  module Split
4
4
  class GoalsCollection
5
-
6
- def initialize(experiment_name, goals=nil)
5
+ def initialize(experiment_name, goals = nil)
7
6
  @experiment_name = experiment_name
8
7
  @goals = goals
9
8
  end
@@ -15,10 +14,10 @@ module Split
15
14
  def load_from_configuration
16
15
  goals = Split.configuration.experiment_for(@experiment_name)[:goals]
17
16
 
18
- if goals.nil?
19
- goals = []
20
- else
17
+ if goals
21
18
  goals.flatten
19
+ else
20
+ []
22
21
  end
23
22
  end
24
23
 
@@ -29,7 +28,7 @@ module Split
29
28
 
30
29
  def validate!
31
30
  unless @goals.nil? || @goals.kind_of?(Array)
32
- raise ArgumentError, 'Goals must be an array'
31
+ raise ArgumentError, "Goals must be an array"
33
32
  end
34
33
  end
35
34
 
@@ -38,9 +37,8 @@ module Split
38
37
  end
39
38
 
40
39
  private
41
-
42
- def goals_key
43
- "#{@experiment_name}:goals"
44
- end
40
+ def goals_key
41
+ "#{@experiment_name}:goals"
42
+ end
45
43
  end
46
44
  end
data/lib/split/helper.rb CHANGED
@@ -12,9 +12,9 @@ module Split
12
12
  alternative = if Split.configuration.enabled && !exclude_visitor?
13
13
  experiment.save
14
14
  raise(Split::InvalidExperimentsFormatError) unless (Split.configuration.experiments || {}).fetch(experiment.name.to_sym, {})[:combined_experiments].nil?
15
- trial = Trial.new(:user => ab_user, :experiment => experiment,
16
- :override => override_alternative(experiment.name), :exclude => exclude_visitor?,
17
- :disabled => split_generically_disabled?)
15
+ trial = Trial.new(user: ab_user, experiment: experiment,
16
+ override: override_alternative(experiment.name), exclude: exclude_visitor?,
17
+ disabled: split_generically_disabled?)
18
18
  alt = trial.choose!(self)
19
19
  alt ? alt.name : nil
20
20
  else
@@ -44,21 +44,21 @@ module Split
44
44
  ab_user.delete(experiment.key)
45
45
  end
46
46
 
47
- def finish_experiment(experiment, options = {:reset => true})
47
+ def finish_experiment(experiment, options = { reset: true })
48
48
  return false if active_experiments[experiment.name].nil?
49
49
  return true if experiment.has_winner?
50
50
  should_reset = experiment.resettable? && options[:reset]
51
51
  if ab_user[experiment.finished_key] && !should_reset
52
- return true
52
+ true
53
53
  else
54
54
  alternative_name = ab_user[experiment.key]
55
55
  trial = Trial.new(
56
- :user => ab_user,
57
- :experiment => experiment,
58
- :alternative => alternative_name,
59
- :goals => options[:goals],
60
- )
61
-
56
+ user: ab_user,
57
+ experiment: experiment,
58
+ alternative: alternative_name,
59
+ goals: options[:goals],
60
+ )
61
+
62
62
  trial.complete!(self)
63
63
 
64
64
  if should_reset
@@ -69,7 +69,7 @@ module Split
69
69
  end
70
70
  end
71
71
 
72
- def ab_finished(metric_descriptor, options = {:reset => true})
72
+ def ab_finished(metric_descriptor, options = { reset: true })
73
73
  return if exclude_visitor? || Split.configuration.disabled?
74
74
  metric_descriptor, goals = normalize_metric(metric_descriptor)
75
75
  experiments = Metric.possible_experiments(metric_descriptor)
@@ -77,7 +77,7 @@ module Split
77
77
  if experiments.any?
78
78
  experiments.each do |experiment|
79
79
  next if override_present?(experiment.key)
80
- finish_experiment(experiment, options.merge(:goals => goals))
80
+ finish_experiment(experiment, options.merge(goals: goals))
81
81
  end
82
82
  end
83
83
  rescue => e
@@ -86,7 +86,7 @@ module Split
86
86
  end
87
87
 
88
88
  def ab_record_extra_info(metric_descriptor, key, value = 1)
89
- return if exclude_visitor? || Split.configuration.disabled?
89
+ return if exclude_visitor? || Split.configuration.disabled? || value.nil?
90
90
  metric_descriptor, _ = normalize_metric(metric_descriptor)
91
91
  experiments = Metric.possible_experiments(metric_descriptor)
92
92
 
@@ -95,7 +95,7 @@ module Split
95
95
  alternative_name = ab_user[experiment.key]
96
96
 
97
97
  if alternative_name
98
- alternative = experiment.alternatives.find{|alt| alt.name == alternative_name}
98
+ alternative = experiment.alternatives.find { |alt| alt.name == alternative_name }
99
99
  alternative.record_extra_info(key, value) if alternative
100
100
  end
101
101
  end
@@ -105,7 +105,7 @@ module Split
105
105
  Split.configuration.db_failover_on_db_error.call(e)
106
106
  end
107
107
 
108
- def ab_active_experiments()
108
+ def ab_active_experiments
109
109
  ab_user.active_experiments
110
110
  rescue => e
111
111
  raise unless Split.configuration.db_failover
@@ -127,14 +127,14 @@ module Split
127
127
  def override_alternative_by_cookies(experiment_name)
128
128
  return unless defined?(request)
129
129
 
130
- if request.cookies && request.cookies.key?('split_override')
131
- experiments = JSON.parse(request.cookies['split_override']) rescue {}
130
+ if request.cookies && request.cookies.key?("split_override")
131
+ experiments = JSON.parse(request.cookies["split_override"]) rescue {}
132
132
  experiments[experiment_name]
133
133
  end
134
134
  end
135
135
 
136
136
  def split_generically_disabled?
137
- defined?(params) && params['SPLIT_DISABLE']
137
+ defined?(params) && params["SPLIT_DISABLE"]
138
138
  end
139
139
 
140
140
  def ab_user
@@ -150,7 +150,7 @@ module Split
150
150
  end
151
151
 
152
152
  def is_preview?
153
- defined?(request) && defined?(request.headers) && request.headers['x-purpose'] == 'preview'
153
+ defined?(request) && defined?(request.headers) && request.headers["x-purpose"] == "preview"
154
154
  end
155
155
 
156
156
  def is_ignored_ip_address?
data/lib/split/metric.rb CHANGED
@@ -16,13 +16,13 @@ module Split
16
16
  def self.load_from_redis(name)
17
17
  metric = Split.redis.hget(:metrics, name)
18
18
  if metric
19
- experiment_names = metric.split(',')
19
+ experiment_names = metric.split(",")
20
20
 
21
21
  experiments = experiment_names.collect do |experiment_name|
22
22
  Split::ExperimentCatalog.find(experiment_name)
23
23
  end
24
24
 
25
- Split::Metric.new(:name => name, :experiments => experiments)
25
+ Split::Metric.new(name: name, experiments: experiments)
26
26
  else
27
27
  nil
28
28
  end
@@ -31,7 +31,7 @@ module Split
31
31
  def self.load_from_configuration(name)
32
32
  metrics = Split.configuration.metrics
33
33
  if metrics && metrics[name]
34
- Split::Metric.new(:experiments => metrics[name], :name => name)
34
+ Split::Metric.new(experiments: metrics[name], name: name)
35
35
  else
36
36
  nil
37
37
  end
@@ -77,7 +77,7 @@ module Split
77
77
  end
78
78
 
79
79
  def save
80
- Split.redis.hset(:metrics, name, experiments.map(&:name).join(','))
80
+ Split.redis.hset(:metrics, name, experiments.map(&:name).join(","))
81
81
  end
82
82
 
83
83
  def complete!
@@ -97,6 +97,5 @@ module Split
97
97
  return metric_name, goals
98
98
  end
99
99
  private_class_method :normalize_metric
100
-
101
100
  end
102
101
  end