split 3.3.2 → 4.0.0.pre2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  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 +61 -0
  7. data/.rspec +1 -0
  8. data/.rubocop.yml +71 -1044
  9. data/.rubocop_todo.yml +226 -0
  10. data/Appraisals +1 -1
  11. data/CHANGELOG.md +62 -0
  12. data/CODE_OF_CONDUCT.md +3 -3
  13. data/Gemfile +2 -0
  14. data/README.md +40 -18
  15. data/Rakefile +2 -0
  16. data/gemfiles/6.0.gemfile +1 -1
  17. data/lib/split/algorithms/block_randomization.rb +2 -0
  18. data/lib/split/algorithms/weighted_sample.rb +2 -1
  19. data/lib/split/algorithms/whiplash.rb +3 -2
  20. data/lib/split/alternative.rb +4 -3
  21. data/lib/split/cache.rb +28 -0
  22. data/lib/split/combined_experiments_helper.rb +2 -1
  23. data/lib/split/configuration.rb +13 -14
  24. data/lib/split/dashboard/helpers.rb +1 -0
  25. data/lib/split/dashboard/pagination_helpers.rb +3 -3
  26. data/lib/split/dashboard/paginator.rb +1 -0
  27. data/lib/split/dashboard/public/dashboard.js +10 -0
  28. data/lib/split/dashboard/public/style.css +5 -0
  29. data/lib/split/dashboard/views/_controls.erb +13 -0
  30. data/lib/split/dashboard/views/layout.erb +1 -1
  31. data/lib/split/dashboard.rb +19 -1
  32. data/lib/split/encapsulated_helper.rb +3 -2
  33. data/lib/split/engine.rb +7 -4
  34. data/lib/split/exceptions.rb +1 -0
  35. data/lib/split/experiment.rb +98 -65
  36. data/lib/split/experiment_catalog.rb +1 -3
  37. data/lib/split/extensions/string.rb +1 -0
  38. data/lib/split/goals_collection.rb +2 -0
  39. data/lib/split/helper.rb +28 -8
  40. data/lib/split/metric.rb +2 -1
  41. data/lib/split/persistence/cookie_adapter.rb +6 -1
  42. data/lib/split/persistence/dual_adapter.rb +54 -12
  43. data/lib/split/persistence/redis_adapter.rb +5 -0
  44. data/lib/split/persistence/session_adapter.rb +1 -0
  45. data/lib/split/persistence.rb +4 -2
  46. data/lib/split/redis_interface.rb +9 -28
  47. data/lib/split/trial.rb +21 -11
  48. data/lib/split/user.rb +20 -4
  49. data/lib/split/version.rb +2 -4
  50. data/lib/split/zscore.rb +1 -0
  51. data/lib/split.rb +9 -3
  52. data/spec/alternative_spec.rb +1 -1
  53. data/spec/cache_spec.rb +88 -0
  54. data/spec/configuration_spec.rb +17 -15
  55. data/spec/dashboard/pagination_helpers_spec.rb +3 -1
  56. data/spec/dashboard_helpers_spec.rb +2 -2
  57. data/spec/dashboard_spec.rb +78 -17
  58. data/spec/encapsulated_helper_spec.rb +2 -2
  59. data/spec/experiment_spec.rb +116 -12
  60. data/spec/goals_collection_spec.rb +1 -1
  61. data/spec/helper_spec.rb +186 -112
  62. data/spec/persistence/cookie_adapter_spec.rb +1 -1
  63. data/spec/persistence/dual_adapter_spec.rb +160 -68
  64. data/spec/persistence/redis_adapter_spec.rb +9 -0
  65. data/spec/redis_interface_spec.rb +0 -69
  66. data/spec/spec_helper.rb +5 -6
  67. data/spec/trial_spec.rb +45 -19
  68. data/spec/user_spec.rb +45 -3
  69. data/split.gemspec +8 -9
  70. metadata +28 -36
  71. data/.travis.yml +0 -66
  72. data/gemfiles/4.2.gemfile +0 -9
@@ -1,8 +1,7 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Split
3
4
  class Configuration
4
- attr_accessor :bots
5
- attr_accessor :robot_regex
6
5
  attr_accessor :ignore_ip_addresses
7
6
  attr_accessor :ignore_filter
8
7
  attr_accessor :db_failover
@@ -12,6 +11,7 @@ module Split
12
11
  attr_accessor :enabled
13
12
  attr_accessor :persistence
14
13
  attr_accessor :persistence_cookie_length
14
+ attr_accessor :persistence_cookie_domain
15
15
  attr_accessor :algorithm
16
16
  attr_accessor :store_override
17
17
  attr_accessor :start_manually
@@ -22,14 +22,20 @@ module Split
22
22
  attr_accessor :on_experiment_reset
23
23
  attr_accessor :on_experiment_delete
24
24
  attr_accessor :on_before_experiment_reset
25
+ attr_accessor :on_experiment_winner_choose
25
26
  attr_accessor :on_before_experiment_delete
26
27
  attr_accessor :include_rails_helper
27
28
  attr_accessor :beta_probability_simulations
28
29
  attr_accessor :winning_alternative_recalculation_interval
29
30
  attr_accessor :redis
31
+ attr_accessor :dashboard_pagination_default_per_page
32
+ attr_accessor :cache
30
33
 
31
34
  attr_reader :experiments
32
35
 
36
+ attr_writer :bots
37
+ attr_writer :robot_regex
38
+
33
39
  def bots
34
40
  @bots ||= {
35
41
  # Indexers
@@ -82,7 +88,7 @@ module Split
82
88
  'LinkedInBot' => 'LinkedIn bot',
83
89
  'LongURL' => 'URL expander service',
84
90
  'NING' => 'NING - Yet Another Twitter Swarmer',
85
- 'Pinterest' => 'Pinterest Bot',
91
+ 'Pinterestbot' => 'Pinterest Bot',
86
92
  'redditbot' => 'Reddit Bot',
87
93
  'ShortLinkTranslate' => 'Link shortener',
88
94
  'Slackbot' => 'Slackbot link expander',
@@ -171,7 +177,7 @@ module Split
171
177
  end
172
178
 
173
179
  def normalize_alternatives(alternatives)
174
- given_probability, num_with_probability = alternatives.inject([0,0]) do |a,v|
180
+ given_probability, num_with_probability = alternatives.inject([0, 0]) do |a, v|
175
181
  p, n = a
176
182
  if percent = value_for(v, :percent)
177
183
  [p + percent, n + 1]
@@ -214,27 +220,20 @@ module Split
214
220
  @on_experiment_delete = proc{|experiment|}
215
221
  @on_before_experiment_reset = proc{|experiment|}
216
222
  @on_before_experiment_delete = proc{|experiment|}
223
+ @on_experiment_winner_choose = proc{|experiment|}
217
224
  @db_failover_allow_parameter_override = false
218
225
  @allow_multiple_experiments = false
219
226
  @enabled = true
220
227
  @experiments = {}
221
228
  @persistence = Split::Persistence::SessionAdapter
222
229
  @persistence_cookie_length = 31536000 # One year from now
230
+ @persistence_cookie_domain = nil
223
231
  @algorithm = Split::Algorithms::WeightedSample
224
232
  @include_rails_helper = true
225
233
  @beta_probability_simulations = 10000
226
234
  @winning_alternative_recalculation_interval = 60 * 60 * 24 # 1 day
227
235
  @redis = ENV.fetch(ENV.fetch('REDIS_PROVIDER', 'REDIS_URL'), 'redis://localhost:6379')
228
- end
229
-
230
- def redis_url=(value)
231
- warn '[DEPRECATED] `redis_url=` is deprecated in favor of `redis=`'
232
- self.redis = value
233
- end
234
-
235
- def redis_url
236
- warn '[DEPRECATED] `redis_url` is deprecated in favor of `redis`'
237
- self.redis
236
+ @dashboard_pagination_default_per_page = 10
238
237
  end
239
238
 
240
239
  private
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Split
3
4
  module DashboardHelpers
4
5
  def h(text)
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'split/dashboard/paginator'
3
4
 
4
5
  module Split
5
6
  module DashboardPaginationHelpers
6
- DEFAULT_PER = 10
7
-
8
7
  def pagination_per
9
- @pagination_per ||= (params[:per] || DEFAULT_PER).to_i
8
+ default_per_page = Split.configuration.dashboard_pagination_default_per_page
9
+ @pagination_per ||= (params[:per] || default_per_page).to_i
10
10
  end
11
11
 
12
12
  def page_number
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Split
3
4
  class DashboardPaginator
4
5
  def initialize(collection, page_number, per)
@@ -22,3 +22,13 @@ function confirmReopen() {
22
22
  var agree = confirm("This will reopen the experiment. Are you sure?");
23
23
  return agree ? true : false;
24
24
  }
25
+
26
+ function confirmEnableCohorting(){
27
+ var agree = confirm("This will enable the cohorting of the experiment. Are you sure?");
28
+ return agree ? true : false;
29
+ }
30
+
31
+ function confirmDisableCohorting(){
32
+ var agree = confirm("This will disable the cohorting of the experiment. Note: Existing participants will continue to receive their alternative and may continue to convert. Are you sure?");
33
+ return agree ? true : false;
34
+ }
@@ -326,3 +326,8 @@ a.button.green:focus, button.green:focus, input[type="submit"].green:focus {
326
326
  display: inline-block;
327
327
  padding: 5px;
328
328
  }
329
+
330
+ .divider {
331
+ display: inline-block;
332
+ margin-left: 10px;
333
+ }
@@ -2,7 +2,20 @@
2
2
  <form action="<%= url "/reopen?experiment=#{experiment.name}" %>" method='post' onclick="return confirmReopen()">
3
3
  <input type="submit" value="Reopen Experiment">
4
4
  </form>
5
+ <% else %>
6
+ <% if experiment.cohorting_disabled? %>
7
+ <form action="<%= url "/update_cohorting?experiment=#{experiment.name}" %>" method='post' onclick="return confirmEnableCohorting()">
8
+ <input type="hidden" name="cohorting_action" value="enable">
9
+ <input type="submit" value="Enable Cohorting" class="green">
10
+ </form>
11
+ <% else %>
12
+ <form action="<%= url "/update_cohorting?experiment=#{experiment.name}" %>" method='post' onclick="return confirmDisableCohorting()">
13
+ <input type="hidden" name="cohorting_action" value="disable">
14
+ <input type="submit" value="Disable Cohorting" class="red">
15
+ </form>
16
+ <% end %>
5
17
  <% end %>
18
+ <span class="divider">|</span>
6
19
  <% if experiment.start_time %>
7
20
  <form action="<%= url "/reset?experiment=#{experiment.name}" %>" method='post' onclick="return confirmReset()">
8
21
  <input type="submit" value="Reset Data">
@@ -21,7 +21,7 @@
21
21
  </div>
22
22
 
23
23
  <div id="footer">
24
- <p>Powered by <a href="http://github.com/splitrb/split">Split</a> v<%=Split::VERSION %></p>
24
+ <p>Powered by <a href="https://github.com/splitrb/split">Split</a> v<%=Split::VERSION %></p>
25
25
  </div>
26
26
  </body>
27
27
  </html>
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'sinatra/base'
3
4
  require 'split'
4
5
  require 'bigdecimal'
@@ -33,7 +34,13 @@ module Split
33
34
  end
34
35
 
35
36
  post '/force_alternative' do
36
- Split::User.new(self)[params[:experiment]] = params[:alternative]
37
+ experiment = Split::ExperimentCatalog.find(params[:experiment])
38
+ alternative = Split::Alternative.new(params[:alternative], experiment.name)
39
+
40
+ cookies = JSON.parse(request.cookies['split_override']) rescue {}
41
+ cookies[experiment.name] = alternative.name
42
+ response.set_cookie('split_override', { value: cookies.to_json, path: '/' })
43
+
37
44
  redirect url('/')
38
45
  end
39
46
 
@@ -62,6 +69,17 @@ module Split
62
69
  redirect url('/')
63
70
  end
64
71
 
72
+ post '/update_cohorting' do
73
+ @experiment = Split::ExperimentCatalog.find(params[:experiment])
74
+ case params[:cohorting_action].downcase
75
+ when "enable"
76
+ @experiment.enable_cohorting
77
+ when "disable"
78
+ @experiment.disable_cohorting
79
+ end
80
+ redirect url('/')
81
+ end
82
+
65
83
  delete '/experiment' do
66
84
  @experiment = Split::ExperimentCatalog.find(params[:experiment])
67
85
  @experiment.delete
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "split/helper"
3
4
 
4
5
  # Split's helper exposes all kinds of methods we don't want to
@@ -28,8 +29,8 @@ module Split
28
29
  end
29
30
  end
30
31
 
31
- def ab_test(*arguments,&block)
32
- split_context_shim.ab_test(*arguments,&block)
32
+ def ab_test(*arguments, &block)
33
+ split_context_shim.ab_test(*arguments, &block)
33
34
  end
34
35
 
35
36
  private
data/lib/split/engine.rb CHANGED
@@ -1,12 +1,15 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Split
3
4
  class Engine < ::Rails::Engine
4
5
  initializer "split" do |app|
5
6
  if Split.configuration.include_rails_helper
6
- ActionController::Base.send :include, Split::Helper
7
- ActionController::Base.helper Split::Helper
8
- ActionController::Base.send :include, Split::CombinedExperimentsHelper
9
- ActionController::Base.helper Split::CombinedExperimentsHelper
7
+ ActiveSupport.on_load(:action_controller) do
8
+ ::ActionController::Base.send :include, Split::Helper
9
+ ::ActionController::Base.helper Split::Helper
10
+ ::ActionController::Base.send :include, Split::CombinedExperimentsHelper
11
+ ::ActionController::Base.helper Split::CombinedExperimentsHelper
12
+ end
10
13
  end
11
14
  end
12
15
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Split
3
4
  class InvalidPersistenceAdapterError < StandardError; end
4
5
  class ExperimentNotFound < StandardError; end
@@ -1,38 +1,34 @@
1
1
  # frozen_string_literal: true
2
+
3
+ require 'rubystats'
4
+
2
5
  module Split
3
6
  class Experiment
4
7
  attr_accessor :name
5
- attr_writer :algorithm
6
- attr_accessor :resettable
7
8
  attr_accessor :goals
8
- attr_accessor :alternatives
9
9
  attr_accessor :alternative_probabilities
10
10
  attr_accessor :metadata
11
11
 
12
+ attr_reader :alternatives
13
+ attr_reader :resettable
14
+
12
15
  DEFAULT_OPTIONS = {
13
16
  :resettable => true
14
17
  }
15
18
 
19
+ def self.find(name)
20
+ Split.cache(:experiments, name) do
21
+ return unless Split.redis.exists?(name)
22
+ Experiment.new(name).tap { |exp| exp.load_from_redis }
23
+ end
24
+ end
25
+
16
26
  def initialize(name, options = {})
17
27
  options = DEFAULT_OPTIONS.merge(options)
18
28
 
19
29
  @name = name.to_s
20
30
 
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)
31
+ extract_alternatives_from_options(options)
36
32
  end
37
33
 
38
34
  def self.finished_key(key)
@@ -40,11 +36,15 @@ module Split
40
36
  end
41
37
 
42
38
  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]
39
+ options_with_defaults = DEFAULT_OPTIONS.merge(
40
+ options.reject { |k, v| v.nil? }
41
+ )
42
+
43
+ self.alternatives = options_with_defaults[:alternatives]
44
+ self.goals = options_with_defaults[:goals]
45
+ self.resettable = options_with_defaults[:resettable]
46
+ self.algorithm = options_with_defaults[:algorithm]
47
+ self.metadata = options_with_defaults[:metadata]
48
48
  end
49
49
 
50
50
  def extract_alternatives_from_options(options)
@@ -52,7 +52,7 @@ module Split
52
52
 
53
53
  if alts.length == 1
54
54
  if alts[0].is_a? Hash
55
- alts = alts[0].map{|k,v| {k => v} }
55
+ alts = alts[0].map{|k, v| {k => v} }
56
56
  end
57
57
  end
58
58
 
@@ -81,14 +81,14 @@ module Split
81
81
 
82
82
  if new_record?
83
83
  start unless Split.configuration.start_manually
84
+ persist_experiment_configuration
84
85
  elsif experiment_configuration_has_changed?
85
86
  reset unless Split.configuration.reset_manually
87
+ persist_experiment_configuration
86
88
  end
87
89
 
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)
90
+ redis.hmset(experiment_config_key, :resettable, resettable,
91
+ :algorithm, algorithm.to_s)
92
92
  self
93
93
  end
94
94
 
@@ -101,7 +101,7 @@ module Split
101
101
  end
102
102
 
103
103
  def new_record?
104
- !redis.exists(name)
104
+ ExperimentCatalog.find(name).nil?
105
105
  end
106
106
 
107
107
  def ==(obj)
@@ -135,24 +135,29 @@ module Split
135
135
  end
136
136
 
137
137
  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
138
+ Split.cache(:experiment_winner, name) do
139
+ experiment_winner = redis.hget(:experiment_winner, name)
140
+ if experiment_winner
141
+ Split::Alternative.new(experiment_winner, name)
142
+ else
143
+ nil
144
+ end
143
145
  end
144
146
  end
145
147
 
146
148
  def has_winner?
147
- !winner.nil?
149
+ return @has_winner if defined? @has_winner
150
+ @has_winner = !winner.nil?
148
151
  end
149
152
 
150
153
  def winner=(winner_name)
151
154
  redis.hset(:experiment_winner, name, winner_name.to_s)
155
+ @has_winner = true
156
+ Split.configuration.on_experiment_winner_choose.call(self)
152
157
  end
153
158
 
154
159
  def participant_count
155
- alternatives.inject(0){|sum,a| sum + a.participant_count}
160
+ alternatives.inject(0){|sum, a| sum + a.participant_count}
156
161
  end
157
162
 
158
163
  def control
@@ -161,6 +166,8 @@ module Split
161
166
 
162
167
  def reset_winner
163
168
  redis.hdel(:experiment_winner, name)
169
+ @has_winner = false
170
+ Split::Cache.clear_key(@name)
164
171
  end
165
172
 
166
173
  def start
@@ -168,13 +175,15 @@ module Split
168
175
  end
169
176
 
170
177
  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)
178
+ Split.cache(:experiment_start_times, @name) do
179
+ t = redis.hget(:experiment_start_times, @name)
180
+ if t
181
+ # Check if stored time is an integer
182
+ if t =~ /^[-+]?[0-9]+$/
183
+ Time.at(t.to_i)
184
+ else
185
+ Time.parse(t)
186
+ end
178
187
  end
179
188
  end
180
189
  end
@@ -225,6 +234,7 @@ module Split
225
234
 
226
235
  def reset
227
236
  Split.configuration.on_before_experiment_reset.call(self)
237
+ Split::Cache.clear_key(@name)
228
238
  alternatives.each(&:reset)
229
239
  reset_winner
230
240
  Split.configuration.on_experiment_reset.call(self)
@@ -238,6 +248,7 @@ module Split
238
248
  end
239
249
  reset_winner
240
250
  redis.srem(:experiments, name)
251
+ remove_experiment_cohorting
241
252
  remove_experiment_configuration
242
253
  Split.configuration.on_experiment_delete.call(self)
243
254
  increment_version
@@ -335,7 +346,7 @@ module Split
335
346
 
336
347
  def find_simulated_winner(simulated_cr_hash)
337
348
  # figure out which alternative had the highest simulated conversion rate
338
- winning_pair = ["",0.0]
349
+ winning_pair = ["", 0.0]
339
350
  simulated_cr_hash.each do |alternative, rate|
340
351
  if rate > winning_pair[1]
341
352
  winning_pair = [alternative, rate]
@@ -346,17 +357,13 @@ module Split
346
357
  end
347
358
 
348
359
  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
360
  simulated_cr_hash = {}
354
361
 
355
362
  # create a hash which has the conversion rate pulled from each alternative's beta distribution
356
363
  beta_params.each do |alternative, params|
357
364
  alpha = params[0]
358
365
  beta = params[1]
359
- simulated_conversion_rate = rand.beta(alpha, beta)
366
+ simulated_conversion_rate = Rubystats::BetaDistribution.new(alpha, beta).rng
360
367
  simulated_cr_hash[alternative] = simulated_conversion_rate
361
368
  end
362
369
 
@@ -394,6 +401,23 @@ module Split
394
401
  js_id.gsub('/', '--')
395
402
  end
396
403
 
404
+ def cohorting_disabled?
405
+ @cohorting_disabled ||= begin
406
+ value = redis.hget(experiment_config_key, :cohorting)
407
+ value.nil? ? false : value.downcase == "true"
408
+ end
409
+ end
410
+
411
+ def disable_cohorting
412
+ @cohorting_disabled = true
413
+ redis.hset(experiment_config_key, :cohorting, true)
414
+ end
415
+
416
+ def enable_cohorting
417
+ @cohorting_disabled = false
418
+ redis.hset(experiment_config_key, :cohorting, false)
419
+ end
420
+
397
421
  protected
398
422
 
399
423
  def experiment_config_key
@@ -420,14 +444,14 @@ module Split
420
444
  end
421
445
 
422
446
  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)
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)
431
455
  end
432
456
  end
433
457
 
@@ -443,9 +467,14 @@ module Split
443
467
 
444
468
  def persist_experiment_configuration
445
469
  redis_interface.add_to_set(:experiments, name)
446
- redis_interface.persist_list(name, @alternatives.map(&:name))
470
+ redis_interface.persist_list(name, @alternatives.map{|alt| {alt.name => alt.weight}.to_json})
447
471
  goals_collection.save
448
- redis.set(metadata_key, @metadata.to_json) unless @metadata.nil?
472
+
473
+ if @metadata
474
+ redis.set(metadata_key, @metadata.to_json)
475
+ else
476
+ delete_metadata
477
+ end
449
478
  end
450
479
 
451
480
  def remove_experiment_configuration
@@ -456,16 +485,20 @@ module Split
456
485
  end
457
486
 
458
487
  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
488
+ existing_experiment = Experiment.find(@name)
489
+
490
+ existing_experiment.alternatives.map(&:to_s) != @alternatives.map(&:to_s) ||
491
+ existing_experiment.goals != @goals ||
492
+ existing_experiment.metadata != @metadata
465
493
  end
466
494
 
467
495
  def goals_collection
468
496
  Split::GoalsCollection.new(@name, @goals)
469
497
  end
498
+
499
+ def remove_experiment_cohorting
500
+ @cohorting_disabled = false
501
+ redis.hdel(experiment_config_key, :cohorting)
502
+ end
470
503
  end
471
504
  end
@@ -13,8 +13,7 @@ module Split
13
13
  end
14
14
 
15
15
  def self.find(name)
16
- return unless Split.redis.exists(name)
17
- Experiment.new(name).tap { |exp| exp.load_from_redis }
16
+ Experiment.find(name)
18
17
  end
19
18
 
20
19
  def self.find_or_initialize(metric_descriptor, control = nil, *alternatives)
@@ -46,6 +45,5 @@ module Split
46
45
  return experiment_name, goals
47
46
  end
48
47
  private_class_method :normalize_experiment
49
-
50
48
  end
51
49
  end
@@ -1,4 +1,5 @@
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)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Split
2
4
  class GoalsCollection
3
5