split 4.0.1 → 4.0.2
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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +6 -3
- data/.rubocop.yml +2 -5
- data/CHANGELOG.md +23 -0
- data/CONTRIBUTING.md +1 -1
- data/Gemfile +2 -1
- data/README.md +4 -2
- data/Rakefile +4 -5
- data/gemfiles/5.2.gemfile +1 -3
- data/gemfiles/6.0.gemfile +1 -3
- data/gemfiles/6.1.gemfile +1 -3
- data/gemfiles/7.0.gemfile +2 -3
- data/lib/split/algorithms/block_randomization.rb +5 -6
- data/lib/split/algorithms/whiplash.rb +16 -18
- data/lib/split/algorithms.rb +22 -0
- data/lib/split/alternative.rb +21 -22
- data/lib/split/cache.rb +0 -1
- data/lib/split/combined_experiments_helper.rb +4 -4
- data/lib/split/configuration.rb +83 -84
- data/lib/split/dashboard/helpers.rb +6 -7
- data/lib/split/dashboard/pagination_helpers.rb +53 -54
- data/lib/split/dashboard/public/style.css +5 -2
- data/lib/split/dashboard/views/index.erb +19 -4
- data/lib/split/dashboard.rb +29 -23
- data/lib/split/encapsulated_helper.rb +4 -6
- data/lib/split/experiment.rb +84 -88
- data/lib/split/experiment_catalog.rb +6 -5
- data/lib/split/extensions/string.rb +1 -1
- data/lib/split/goals_collection.rb +8 -10
- data/lib/split/helper.rb +19 -19
- data/lib/split/metric.rb +4 -5
- data/lib/split/persistence/cookie_adapter.rb +44 -47
- data/lib/split/persistence/dual_adapter.rb +7 -8
- data/lib/split/persistence/redis_adapter.rb +2 -3
- data/lib/split/persistence/session_adapter.rb +0 -2
- data/lib/split/persistence.rb +4 -4
- data/lib/split/redis_interface.rb +1 -2
- data/lib/split/trial.rb +23 -24
- data/lib/split/user.rb +12 -13
- data/lib/split/version.rb +1 -1
- data/lib/split/zscore.rb +1 -3
- data/lib/split.rb +26 -25
- data/spec/algorithms/block_randomization_spec.rb +6 -5
- data/spec/algorithms/weighted_sample_spec.rb +6 -5
- data/spec/algorithms/whiplash_spec.rb +4 -5
- data/spec/alternative_spec.rb +35 -36
- data/spec/cache_spec.rb +15 -19
- data/spec/combined_experiments_helper_spec.rb +18 -17
- data/spec/configuration_spec.rb +32 -38
- data/spec/dashboard/pagination_helpers_spec.rb +69 -67
- data/spec/dashboard/paginator_spec.rb +10 -9
- data/spec/dashboard_helpers_spec.rb +19 -18
- data/spec/dashboard_spec.rb +67 -35
- data/spec/encapsulated_helper_spec.rb +12 -14
- data/spec/experiment_catalog_spec.rb +14 -13
- data/spec/experiment_spec.rb +121 -123
- data/spec/goals_collection_spec.rb +17 -15
- data/spec/helper_spec.rb +379 -382
- data/spec/metric_spec.rb +14 -14
- data/spec/persistence/cookie_adapter_spec.rb +23 -8
- data/spec/persistence/dual_adapter_spec.rb +71 -71
- data/spec/persistence/redis_adapter_spec.rb +25 -26
- data/spec/persistence/session_adapter_spec.rb +2 -3
- data/spec/persistence_spec.rb +1 -2
- data/spec/redis_interface_spec.rb +16 -14
- data/spec/spec_helper.rb +15 -13
- data/spec/split_spec.rb +11 -11
- data/spec/support/cookies_mock.rb +1 -2
- data/spec/trial_spec.rb +61 -60
- data/spec/user_spec.rb +36 -36
- data/split.gemspec +20 -20
- metadata +7 -10
- data/.rubocop_todo.yml +0 -226
- data/Appraisals +0 -19
- data/gemfiles/5.0.gemfile +0 -9
- data/gemfiles/5.1.gemfile +0 -9
@@ -7,11 +7,11 @@ module Split
|
|
7
7
|
end
|
8
8
|
|
9
9
|
def url(*path_parts)
|
10
|
-
[ path_prefix, path_parts ].join("/").squeeze(
|
10
|
+
[ path_prefix, path_parts ].join("/").squeeze("/")
|
11
11
|
end
|
12
12
|
|
13
13
|
def path_prefix
|
14
|
-
request.env[
|
14
|
+
request.env["SCRIPT_NAME"]
|
15
15
|
end
|
16
16
|
|
17
17
|
def number_to_percentage(number, precision = 2)
|
@@ -32,15 +32,14 @@ module Split
|
|
32
32
|
z = round(z_score.to_s.to_f, 3).abs
|
33
33
|
|
34
34
|
if z >= 2.58
|
35
|
-
|
35
|
+
"99% confidence"
|
36
36
|
elsif z >= 1.96
|
37
|
-
|
37
|
+
"95% confidence"
|
38
38
|
elsif z >= 1.65
|
39
|
-
|
39
|
+
"90% confidence"
|
40
40
|
else
|
41
|
-
|
41
|
+
"Insufficient confidence"
|
42
42
|
end
|
43
|
-
|
44
43
|
end
|
45
44
|
end
|
46
45
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "split/dashboard/paginator"
|
4
4
|
|
5
5
|
module Split
|
6
6
|
module DashboardPaginationHelpers
|
@@ -30,58 +30,57 @@ module Split
|
|
30
30
|
end
|
31
31
|
|
32
32
|
private
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
end
|
33
|
+
def show_first_page_tag?
|
34
|
+
page_number > 2
|
35
|
+
end
|
36
|
+
|
37
|
+
def first_page_tag
|
38
|
+
%Q(<a href="#{url.chop}?page=1&per=#{pagination_per}">1</a>)
|
39
|
+
end
|
40
|
+
|
41
|
+
def show_first_ellipsis_tag?
|
42
|
+
page_number >= 4
|
43
|
+
end
|
44
|
+
|
45
|
+
def ellipsis_tag
|
46
|
+
"<span>...</span>"
|
47
|
+
end
|
48
|
+
|
49
|
+
def show_prev_page_tag?
|
50
|
+
page_number > 1
|
51
|
+
end
|
52
|
+
|
53
|
+
def prev_page_tag
|
54
|
+
%Q(<a href="#{url.chop}?page=#{page_number - 1}&per=#{pagination_per}">#{page_number - 1}</a>)
|
55
|
+
end
|
56
|
+
|
57
|
+
def current_page_tag
|
58
|
+
"<span><b>#{page_number}</b></span>"
|
59
|
+
end
|
60
|
+
|
61
|
+
def show_next_page_tag?(collection)
|
62
|
+
(page_number * pagination_per) < collection.count
|
63
|
+
end
|
64
|
+
|
65
|
+
def next_page_tag
|
66
|
+
%Q(<a href="#{url.chop}?page=#{page_number + 1}&per=#{pagination_per}">#{page_number + 1}</a>)
|
67
|
+
end
|
68
|
+
|
69
|
+
def show_last_ellipsis_tag?(collection)
|
70
|
+
(total_pages(collection) - page_number) >= 3
|
71
|
+
end
|
72
|
+
|
73
|
+
def total_pages(collection)
|
74
|
+
collection.count / pagination_per + ((collection.count % pagination_per).zero? ? 0 : 1)
|
75
|
+
end
|
76
|
+
|
77
|
+
def show_last_page_tag?(collection)
|
78
|
+
page_number < (total_pages(collection) - 1)
|
79
|
+
end
|
80
|
+
|
81
|
+
def last_page_tag(collection)
|
82
|
+
total = total_pages(collection)
|
83
|
+
%Q(<a href="#{url.chop}?page=#{total}&per=#{pagination_per}">#{total}</a>)
|
84
|
+
end
|
86
85
|
end
|
87
86
|
end
|
@@ -258,7 +258,7 @@ body {
|
|
258
258
|
color: #408C48;
|
259
259
|
}
|
260
260
|
|
261
|
-
a.button, button, input[type="submit"] {
|
261
|
+
.experiment a.button, .experiment button, .experiment input[type="submit"] {
|
262
262
|
padding: 4px 10px;
|
263
263
|
overflow: hidden;
|
264
264
|
background: #d8dae0;
|
@@ -312,10 +312,13 @@ a.button.green:focus, button.green:focus, input[type="submit"].green:focus {
|
|
312
312
|
background:#768E7A;
|
313
313
|
}
|
314
314
|
|
315
|
-
|
315
|
+
.dashboard-controls input, .dashboard-controls select {
|
316
316
|
padding: 10px;
|
317
317
|
}
|
318
318
|
|
319
|
+
.dashboard-controls-bottom {
|
320
|
+
margin-top: 10px;
|
321
|
+
}
|
319
322
|
|
320
323
|
.pagination {
|
321
324
|
text-align: center;
|
@@ -1,10 +1,12 @@
|
|
1
1
|
<% if @experiments.any? %>
|
2
2
|
<p class="intro">The list below contains all the registered experiments along with the number of test participants, completed and conversion rate currently in the system.</p>
|
3
3
|
|
4
|
-
<
|
5
|
-
|
6
|
-
|
7
|
-
|
4
|
+
<div class="dashboard-controls">
|
5
|
+
<input type="text" placeholder="Begin typing to filter" id="filter" />
|
6
|
+
<input type="button" id="toggle-completed" value="Hide completed" />
|
7
|
+
<input type="button" id="toggle-active" value="Hide active" />
|
8
|
+
<input type="button" id="clear-filter" value="Clear filters" />
|
9
|
+
</div>
|
8
10
|
|
9
11
|
<% paginated(@experiments).each do |experiment| %>
|
10
12
|
<% if experiment.goals.empty? %>
|
@@ -24,3 +26,16 @@
|
|
24
26
|
<p class="intro">No experiments have started yet, you need to define them in your code and introduce them to your users.</p>
|
25
27
|
<p class="intro">Check out the <a href='https://github.com/splitrb/split#readme'>Readme</a> for more help getting started.</p>
|
26
28
|
<% end %>
|
29
|
+
|
30
|
+
<div class="dashboard-controls dashboard-controls-bottom">
|
31
|
+
<form action="<%= url "/initialize_experiment" %>" method='post'>
|
32
|
+
<label>Add unregistered experiment: </label>
|
33
|
+
<select name="experiment" id="experiment-select">
|
34
|
+
<option selected disabled>experiment</option>
|
35
|
+
<% @unintialized_experiments.sort.each do |experiment_name| %>
|
36
|
+
<option value="<%= experiment_name %>"><%= experiment_name %></option>
|
37
|
+
<% end %>
|
38
|
+
</select>
|
39
|
+
<input type="submit" id="register-experiment-btn" value="register experiment"/>
|
40
|
+
</form>
|
41
|
+
<div>
|
data/lib/split/dashboard.rb
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
3
|
+
require "sinatra/base"
|
4
|
+
require "split"
|
5
|
+
require "bigdecimal"
|
6
|
+
require "split/dashboard/helpers"
|
7
|
+
require "split/dashboard/pagination_helpers"
|
8
8
|
|
9
9
|
module Split
|
10
10
|
class Dashboard < Sinatra::Base
|
@@ -18,14 +18,15 @@ module Split
|
|
18
18
|
helpers Split::DashboardHelpers
|
19
19
|
helpers Split::DashboardPaginationHelpers
|
20
20
|
|
21
|
-
get
|
21
|
+
get "/" do
|
22
22
|
# Display experiments without a winner at the top of the dashboard
|
23
23
|
@experiments = Split::ExperimentCatalog.all_active_first
|
24
|
+
@unintialized_experiments = Split.configuration.experiments.keys - @experiments.map(&:name)
|
24
25
|
|
25
26
|
@metrics = Split::Metric.all
|
26
27
|
|
27
28
|
# Display Rails Environment mode (or Rack version if not using Rails)
|
28
|
-
if Object.const_defined?(
|
29
|
+
if Object.const_defined?("Rails") && Rails.respond_to?(:env)
|
29
30
|
@current_env = Rails.env.titlecase
|
30
31
|
else
|
31
32
|
@current_env = "Rack: #{Rack.version}"
|
@@ -33,43 +34,48 @@ module Split
|
|
33
34
|
erb :index
|
34
35
|
end
|
35
36
|
|
36
|
-
post
|
37
|
+
post "/initialize_experiment" do
|
38
|
+
Split::ExperimentCatalog.find_or_create(params[:experiment]) unless params[:experiment].nil? || params[:experiment].empty?
|
39
|
+
redirect url("/")
|
40
|
+
end
|
41
|
+
|
42
|
+
post "/force_alternative" do
|
37
43
|
experiment = Split::ExperimentCatalog.find(params[:experiment])
|
38
44
|
alternative = Split::Alternative.new(params[:alternative], experiment.name)
|
39
45
|
|
40
|
-
cookies = JSON.parse(request.cookies[
|
46
|
+
cookies = JSON.parse(request.cookies["split_override"]) rescue {}
|
41
47
|
cookies[experiment.name] = alternative.name
|
42
|
-
response.set_cookie(
|
48
|
+
response.set_cookie("split_override", { value: cookies.to_json, path: "/" })
|
43
49
|
|
44
|
-
redirect url(
|
50
|
+
redirect url("/")
|
45
51
|
end
|
46
52
|
|
47
|
-
post
|
53
|
+
post "/experiment" do
|
48
54
|
@experiment = Split::ExperimentCatalog.find(params[:experiment])
|
49
55
|
@alternative = Split::Alternative.new(params[:alternative], params[:experiment])
|
50
56
|
@experiment.winner = @alternative.name
|
51
|
-
redirect url(
|
57
|
+
redirect url("/")
|
52
58
|
end
|
53
59
|
|
54
|
-
post
|
60
|
+
post "/start" do
|
55
61
|
@experiment = Split::ExperimentCatalog.find(params[:experiment])
|
56
62
|
@experiment.start
|
57
|
-
redirect url(
|
63
|
+
redirect url("/")
|
58
64
|
end
|
59
65
|
|
60
|
-
post
|
66
|
+
post "/reset" do
|
61
67
|
@experiment = Split::ExperimentCatalog.find(params[:experiment])
|
62
68
|
@experiment.reset
|
63
|
-
redirect url(
|
69
|
+
redirect url("/")
|
64
70
|
end
|
65
71
|
|
66
|
-
post
|
72
|
+
post "/reopen" do
|
67
73
|
@experiment = Split::ExperimentCatalog.find(params[:experiment])
|
68
74
|
@experiment.reset_winner
|
69
|
-
redirect url(
|
75
|
+
redirect url("/")
|
70
76
|
end
|
71
77
|
|
72
|
-
post
|
78
|
+
post "/update_cohorting" do
|
73
79
|
@experiment = Split::ExperimentCatalog.find(params[:experiment])
|
74
80
|
case params[:cohorting_action].downcase
|
75
81
|
when "enable"
|
@@ -77,13 +83,13 @@ module Split
|
|
77
83
|
when "disable"
|
78
84
|
@experiment.disable_cohorting
|
79
85
|
end
|
80
|
-
redirect url(
|
86
|
+
redirect url("/")
|
81
87
|
end
|
82
88
|
|
83
|
-
delete
|
89
|
+
delete "/experiment" do
|
84
90
|
@experiment = Split::ExperimentCatalog.find(params[:experiment])
|
85
91
|
@experiment.delete
|
86
|
-
redirect url(
|
92
|
+
redirect url("/")
|
87
93
|
end
|
88
94
|
end
|
89
95
|
end
|
@@ -15,7 +15,6 @@ require "split/helper"
|
|
15
15
|
#
|
16
16
|
module Split
|
17
17
|
module EncapsulatedHelper
|
18
|
-
|
19
18
|
class ContextShim
|
20
19
|
include Split::Helper
|
21
20
|
public :ab_test, :ab_finished
|
@@ -34,10 +33,9 @@ module Split
|
|
34
33
|
end
|
35
34
|
|
36
35
|
private
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
end
|
36
|
+
# instantiate and memoize a context shim in case of multiple ab_test* calls
|
37
|
+
def split_context_shim
|
38
|
+
@split_context_shim ||= ContextShim.new(self)
|
39
|
+
end
|
42
40
|
end
|
43
41
|
end
|
data/lib/split/experiment.rb
CHANGED
@@ -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
|
-
:
|
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 ==
|
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[
|
266
|
-
algorithm: exp_config[
|
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
|
@@ -328,11 +326,11 @@ module Split
|
|
328
326
|
winning_counts.each do |alternative, wins|
|
329
327
|
alternative_probabilities[alternative] = wins / number_of_simulations.to_f
|
330
328
|
end
|
331
|
-
|
329
|
+
alternative_probabilities
|
332
330
|
end
|
333
331
|
|
334
332
|
def count_simulated_wins(winning_alternatives)
|
335
|
-
|
333
|
+
# initialize a hash to keep track of winning alternative in simulations
|
336
334
|
winning_counts = {}
|
337
335
|
alternatives.each do |alternative|
|
338
336
|
winning_counts[alternative] = 0
|
@@ -341,7 +339,7 @@ module Split
|
|
341
339
|
winning_alternatives.each do |alternative|
|
342
340
|
winning_counts[alternative] += 1
|
343
341
|
end
|
344
|
-
|
342
|
+
winning_counts
|
345
343
|
end
|
346
344
|
|
347
345
|
def find_simulated_winner(simulated_cr_hash)
|
@@ -353,7 +351,7 @@ module Split
|
|
353
351
|
end
|
354
352
|
end
|
355
353
|
winner = winning_pair[0]
|
356
|
-
|
354
|
+
winner
|
357
355
|
end
|
358
356
|
|
359
357
|
def calc_simulated_conversion_rates(beta_params)
|
@@ -363,11 +361,11 @@ module Split
|
|
363
361
|
beta_params.each do |alternative, params|
|
364
362
|
alpha = params[0]
|
365
363
|
beta = params[1]
|
366
|
-
simulated_conversion_rate =
|
364
|
+
simulated_conversion_rate = Split::Algorithms.beta_distribution_rng(alpha, beta)
|
367
365
|
simulated_cr_hash[alternative] = simulated_conversion_rate
|
368
366
|
end
|
369
367
|
|
370
|
-
|
368
|
+
simulated_cr_hash
|
371
369
|
end
|
372
370
|
|
373
371
|
def calc_beta_params(goal = nil)
|
@@ -381,7 +379,7 @@ module Split
|
|
381
379
|
|
382
380
|
beta_params[alternative] = params
|
383
381
|
end
|
384
|
-
|
382
|
+
beta_params
|
385
383
|
end
|
386
384
|
|
387
385
|
def calc_time=(time)
|
@@ -394,11 +392,11 @@ module Split
|
|
394
392
|
|
395
393
|
def jstring(goal = nil)
|
396
394
|
js_id = if goal.nil?
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
js_id.gsub(
|
395
|
+
name
|
396
|
+
else
|
397
|
+
name + "-" + goal
|
398
|
+
end
|
399
|
+
js_id.gsub("/", "--")
|
402
400
|
end
|
403
401
|
|
404
402
|
def cohorting_disabled?
|
@@ -410,95 +408,93 @@ module Split
|
|
410
408
|
|
411
409
|
def disable_cohorting
|
412
410
|
@cohorting_disabled = true
|
413
|
-
redis.hset(experiment_config_key, :cohorting, true)
|
411
|
+
redis.hset(experiment_config_key, :cohorting, true.to_s)
|
414
412
|
end
|
415
413
|
|
416
414
|
def enable_cohorting
|
417
415
|
@cohorting_disabled = false
|
418
|
-
redis.hset(experiment_config_key, :cohorting, false)
|
416
|
+
redis.hset(experiment_config_key, :cohorting, false.to_s)
|
419
417
|
end
|
420
418
|
|
421
419
|
protected
|
420
|
+
def experiment_config_key
|
421
|
+
"experiment_configurations/#{@name}"
|
422
|
+
end
|
422
423
|
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
def load_metadata_from_configuration
|
428
|
-
Split.configuration.experiment_for(@name)[:metadata]
|
429
|
-
end
|
424
|
+
def load_metadata_from_configuration
|
425
|
+
Split.configuration.experiment_for(@name)[:metadata]
|
426
|
+
end
|
430
427
|
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
428
|
+
def load_metadata_from_redis
|
429
|
+
meta = redis.get(metadata_key)
|
430
|
+
JSON.parse(meta) unless meta.nil?
|
431
|
+
end
|
435
432
|
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
433
|
+
def load_alternatives_from_configuration
|
434
|
+
alts = Split.configuration.experiment_for(@name)[:alternatives]
|
435
|
+
raise ArgumentError, "Experiment configuration is missing :alternatives array" unless alts
|
436
|
+
if alts.is_a?(Hash)
|
437
|
+
alts.keys
|
438
|
+
else
|
439
|
+
alts.flatten
|
440
|
+
end
|
443
441
|
end
|
444
|
-
end
|
445
442
|
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
443
|
+
def load_alternatives_from_redis
|
444
|
+
alternatives = redis.lrange(@name, 0, -1)
|
445
|
+
alternatives.map do |alt|
|
446
|
+
alt = begin
|
447
|
+
JSON.parse(alt)
|
448
|
+
rescue
|
449
|
+
alt
|
450
|
+
end
|
451
|
+
Split::Alternative.new(alt, @name)
|
452
|
+
end
|
455
453
|
end
|
456
|
-
end
|
457
454
|
|
458
455
|
private
|
456
|
+
def redis
|
457
|
+
Split.redis
|
458
|
+
end
|
459
459
|
|
460
|
-
|
461
|
-
|
462
|
-
|
460
|
+
def redis_interface
|
461
|
+
RedisInterface.new
|
462
|
+
end
|
463
463
|
|
464
|
-
|
465
|
-
|
466
|
-
|
464
|
+
def persist_experiment_configuration
|
465
|
+
redis_interface.add_to_set(:experiments, name)
|
466
|
+
redis_interface.persist_list(name, @alternatives.map { |alt| { alt.name => alt.weight }.to_json })
|
467
|
+
goals_collection.save
|
467
468
|
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
469
|
+
if @metadata
|
470
|
+
redis.set(metadata_key, @metadata.to_json)
|
471
|
+
else
|
472
|
+
delete_metadata
|
473
|
+
end
|
474
|
+
end
|
472
475
|
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
+
def remove_experiment_configuration
|
477
|
+
@alternatives.each(&:delete)
|
478
|
+
goals_collection.delete
|
476
479
|
delete_metadata
|
480
|
+
redis.del(@name)
|
477
481
|
end
|
478
|
-
end
|
479
482
|
|
480
|
-
|
481
|
-
|
482
|
-
goals_collection.delete
|
483
|
-
delete_metadata
|
484
|
-
redis.del(@name)
|
485
|
-
end
|
483
|
+
def experiment_configuration_has_changed?
|
484
|
+
existing_experiment = Experiment.find(@name)
|
486
485
|
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
existing_experiment.goals != @goals ||
|
492
|
-
existing_experiment.metadata != @metadata
|
493
|
-
end
|
486
|
+
existing_experiment.alternatives.map(&:to_s) != @alternatives.map(&:to_s) ||
|
487
|
+
existing_experiment.goals != @goals ||
|
488
|
+
existing_experiment.metadata != @metadata
|
489
|
+
end
|
494
490
|
|
495
|
-
|
496
|
-
|
497
|
-
|
491
|
+
def goals_collection
|
492
|
+
Split::GoalsCollection.new(@name, @goals)
|
493
|
+
end
|
498
494
|
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
495
|
+
def remove_experiment_cohorting
|
496
|
+
@cohorting_disabled = false
|
497
|
+
redis.hdel(experiment_config_key, :cohorting)
|
498
|
+
end
|
503
499
|
end
|
504
500
|
end
|