split 3.3.2 → 4.0.0.pre2

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 (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