split 0.5.0 → 0.6.0

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.
@@ -1,20 +1,9 @@
1
1
  module Split
2
2
  class Configuration
3
- BOTS = {
4
- 'Baidu' => 'Chinese spider',
5
- 'Gigabot' => 'Gigabot spider',
6
- 'Googlebot' => 'Google spider',
7
- 'libwww-perl' => 'Perl client-server library loved by script kids',
8
- 'lwp-trivial' => 'Another Perl library loved by script kids',
9
- 'msnbot' => 'Microsoft bot',
10
- 'SiteUptime' => 'Site monitoring services',
11
- 'Slurp' => 'Yahoo spider',
12
- 'WordPress' => 'WordPress spider',
13
- 'ZIBB' => 'ZIBB spider',
14
- 'ZyBorg' => 'Zyborg? Hmmm....'
15
- }
3
+ attr_accessor :bots
16
4
  attr_accessor :robot_regex
17
5
  attr_accessor :ignore_ip_addresses
6
+ attr_accessor :ignore_filter
18
7
  attr_accessor :db_failover
19
8
  attr_accessor :db_failover_on_db_error
20
9
  attr_accessor :db_failover_allow_parameter_override
@@ -24,13 +13,64 @@ module Split
24
13
  attr_accessor :persistence
25
14
  attr_accessor :algorithm
26
15
 
16
+ def bots
17
+ @bots ||= {
18
+ # Indexers
19
+ "AdsBot-Google" => 'Google Adwords',
20
+ 'Baidu' => 'Chinese search engine',
21
+ 'Gigabot' => 'Gigabot spider',
22
+ 'Googlebot' => 'Google spider',
23
+ 'msnbot' => 'Microsoft bot',
24
+ 'bingbot' => 'Microsoft bing bot',
25
+ 'rogerbot' => 'SeoMoz spider',
26
+ 'Slurp' => 'Yahoo spider',
27
+ 'Sogou' => 'Chinese search engine',
28
+ "spider" => 'generic web spider',
29
+ 'WordPress' => 'WordPress spider',
30
+ 'ZIBB' => 'ZIBB spider',
31
+ 'YandexBot' => 'Yandex spider',
32
+ # HTTP libraries
33
+ 'Apache-HttpClient' => 'Java http library',
34
+ 'AppEngine-Google' => 'Google App Engine',
35
+ "curl" => 'curl unix CLI http client',
36
+ 'ColdFusion' => 'ColdFusion http library',
37
+ "EventMachine HttpClient" => 'Ruby http library',
38
+ "Go http package" => 'Go http library',
39
+ 'Java' => 'Generic Java http library',
40
+ 'libwww-perl' => 'Perl client-server library loved by script kids',
41
+ 'lwp-trivial' => 'Another Perl library loved by script kids',
42
+ "Python-urllib" => 'Python http library',
43
+ "PycURL" => 'Python http library',
44
+ "Test Certificate Info" => 'C http library?',
45
+ "Wget" => 'wget unix CLI http client',
46
+ # URL expanders / previewers
47
+ 'awe.sm' => 'Awe.sm URL expander',
48
+ "bitlybot" => 'bit.ly bot',
49
+ "facebookexternalhit" => 'facebook bot',
50
+ 'LongURL' => 'URL expander service',
51
+ 'Twitterbot' => 'Twitter URL expander',
52
+ 'UnwindFetch' => 'Gnip URL expander',
53
+ # Uptime monitoring
54
+ 'check_http' => 'Nagios monitor',
55
+ 'NewRelicPinger' => 'NewRelic monitor',
56
+ 'Panopta' => 'Monitoring service',
57
+ "Pingdom" => 'Pingdom monitoring',
58
+ 'SiteUptime' => 'Site monitoring services',
59
+ # ???
60
+ "DigitalPersona Fingerprint Software" => 'HP Fingerprint scanner',
61
+ "ShowyouBot" => 'Showyou iOS app spider',
62
+ 'ZyBorg' => 'Zyborg? Hmmm....',
63
+ }
64
+ end
65
+
27
66
  def disabled?
28
67
  !enabled
29
68
  end
30
69
 
31
70
  def experiment_for(name)
32
71
  if normalized_experiments
33
- normalized_experiments[name]
72
+ # TODO symbols
73
+ normalized_experiments[name.to_sym]
34
74
  end
35
75
  end
36
76
 
@@ -39,37 +79,44 @@ module Split
39
79
  @metrics = {}
40
80
  if self.experiments
41
81
  self.experiments.each do |key, value|
42
- metric_name = value[:metric]
82
+ metric_name = value_for(value, :metric).to_sym rescue nil
43
83
  if metric_name
44
84
  @metrics[metric_name] ||= []
45
- @metrics[metric_name] << Split::Experiment.load_from_configuration(key)
85
+ @metrics[metric_name] << Split::Experiment.new(key)
46
86
  end
47
87
  end
48
88
  end
49
89
  @metrics
50
90
  end
51
-
91
+
52
92
  def normalized_experiments
53
93
  if @experiments.nil?
54
94
  nil
55
95
  else
56
96
  experiment_config = {}
57
- @experiments.keys.each do | name |
58
- experiment_config[name] = {}
97
+ @experiments.keys.each do |name|
98
+ experiment_config[name.to_sym] = {}
59
99
  end
60
- @experiments.each do | experiment_name, settings|
61
- experiment_config[experiment_name][:alternatives] = normalize_alternatives(settings[:alternatives]) if settings[:alternatives]
100
+
101
+ @experiments.each do |experiment_name, settings|
102
+ if alternatives = value_for(settings, :alternatives)
103
+ experiment_config[experiment_name.to_sym][:alternatives] = normalize_alternatives(alternatives)
104
+ end
105
+
106
+ if goals = value_for(settings, :goals)
107
+ experiment_config[experiment_name.to_sym][:goals] = goals
108
+ end
62
109
  end
110
+
63
111
  experiment_config
64
112
  end
65
113
  end
66
-
67
-
114
+
68
115
  def normalize_alternatives(alternatives)
69
116
  given_probability, num_with_probability = alternatives.inject([0,0]) do |a,v|
70
117
  p, n = a
71
- if v.kind_of?(Hash) && v[:percent]
72
- [p + v[:percent], n + 1]
118
+ if percent = value_for(v, :percent)
119
+ [p + percent, n + 1]
73
120
  else
74
121
  a
75
122
  end
@@ -80,14 +127,15 @@ module Split
80
127
 
81
128
  if num_with_probability.nonzero?
82
129
  alternatives = alternatives.map do |v|
83
- if v.kind_of?(Hash) && v[:name] && v[:percent]
84
- { v[:name] => v[:percent] / 100.0 }
85
- elsif v.kind_of?(Hash) && v[:name]
86
- { v[:name] => unassigned_probability }
130
+ if (name = value_for(v, :name)) && (percent = value_for(v, :percent))
131
+ { name => percent / 100.0 }
132
+ elsif name = value_for(v, :name)
133
+ { name => unassigned_probability }
87
134
  else
88
135
  { v => unassigned_probability }
89
136
  end
90
137
  end
138
+
91
139
  [alternatives.shift, alternatives]
92
140
  else
93
141
  alternatives = alternatives.dup
@@ -95,9 +143,13 @@ module Split
95
143
  end
96
144
  end
97
145
 
146
+ def robot_regex
147
+ @robot_regex ||= /\b(?:#{escaped_bots.join('|')})\b|\A\W*\z/i
148
+ end
149
+
98
150
  def initialize
99
- @robot_regex = /\b(#{BOTS.keys.join('|')})\b/i
100
151
  @ignore_ip_addresses = []
152
+ @ignore_filter = proc{ |request| is_robot? || is_ignored_ip_address? }
101
153
  @db_failover = false
102
154
  @db_failover_on_db_error = proc{|error|} # e.g. use Rails logger here
103
155
  @db_failover_allow_parameter_override = false
@@ -107,5 +159,17 @@ module Split
107
159
  @persistence = Split::Persistence::SessionAdapter
108
160
  @algorithm = Split::Algorithms::WeightedSample
109
161
  end
162
+
163
+ private
164
+
165
+ def value_for(hash, key)
166
+ if hash.kind_of?(Hash)
167
+ hash[key.to_s] || hash[key.to_sym]
168
+ end
169
+ end
170
+
171
+ def escaped_bots
172
+ bots.map { |key, _| Regexp.escape(key) }
173
+ end
110
174
  end
111
175
  end
@@ -160,6 +160,10 @@ body {
160
160
  margin:30px 0;
161
161
  }
162
162
 
163
+ .experiment_with_goal {
164
+ margin: -32px 0 30px 0;
165
+ }
166
+
163
167
  .experiment .experiment-header {
164
168
  background: #f4f4f4;
165
169
  background: -webkit-gradient(linear, left top, left bottom,
@@ -177,14 +181,18 @@ body {
177
181
 
178
182
  .experiment h2 {
179
183
  color:#888;
180
- margin: 12px 0 0;
184
+ margin: 12px 0 12px 0;
181
185
  font-size: 1em;
182
186
  font-weight:bold;
183
187
  float:left;
184
188
  text-shadow:0 1px 0 rgba(255,255,255,0.8);
185
189
  }
186
190
 
187
- .experiment h2 .version{
191
+ .experiment h2 .goal {
192
+ font-style: italic;
193
+ }
194
+
195
+ .experiment h2 .version {
188
196
  font-style:italic;
189
197
  font-size:0.8em;
190
198
  color:#bbb;
@@ -1,20 +1,28 @@
1
- <div class="experiment">
1
+ <% unless goal.nil? %>
2
+ <% experiment_class = "experiment experiment_with_goal" %>
3
+ <% else %>
4
+ <% experiment_class = "experiment" %>
5
+ <% end %>
6
+ <div class="<%= experiment_class %>">
2
7
  <div class="experiment-header">
3
8
  <h2>
4
9
  Experiment: <%= experiment.name %>
5
10
  <% if experiment.version > 1 %><span class='version'>v<%= experiment.version %></span><% end %>
11
+ <% unless goal.nil? %><span class='goal'>Goal:<%= goal %></span><% end %>
6
12
  </h2>
7
13
 
8
- <div class='inline-controls'>
9
- <small><%= experiment.start_time ? experiment.start_time.strftime('%Y-%m-%d') : 'Unknown' %></small>
10
- <form action="<%= url "/reset/#{experiment.name}" %>" method='post' onclick="return confirmReset()">
11
- <input type="submit" value="Reset Data">
12
- </form>
13
- <form action="<%= url "/#{experiment.name}" %>" method='post' onclick="return confirmDelete()">
14
- <input type="hidden" name="_method" value="delete"/>
15
- <input type="submit" value="Delete" class="red">
16
- </form>
17
- </div>
14
+ <% if goal.nil? %>
15
+ <div class='inline-controls'>
16
+ <small><%= experiment.start_time ? experiment.start_time.strftime('%Y-%m-%d') : 'Unknown' %></small>
17
+ <form action="<%= url "/reset/#{experiment.name}" %>" method='post' onclick="return confirmReset()">
18
+ <input type="submit" value="Reset Data">
19
+ </form>
20
+ <form action="<%= url "/#{experiment.name}" %>" method='post' onclick="return confirmDelete()">
21
+ <input type="hidden" name="_method" value="delete"/>
22
+ <input type="submit" value="Delete" class="red">
23
+ </form>
24
+ </div>
25
+ <% end %>
18
26
  </div>
19
27
  <table>
20
28
  <tr>
@@ -27,7 +35,7 @@
27
35
  <th>Finish</th>
28
36
  </tr>
29
37
 
30
- <% total_participants = total_completed = 0 %>
38
+ <% total_participants = total_completed = total_unfinished = 0 %>
31
39
  <% experiment.alternatives.each do |alternative| %>
32
40
  <tr>
33
41
  <td>
@@ -38,23 +46,23 @@
38
46
  </td>
39
47
  <td><%= alternative.participant_count %></td>
40
48
  <td><%= alternative.unfinished_count %></td>
41
- <td><%= alternative.completed_count %></td>
49
+ <td><%= alternative.completed_count(goal) %></td>
42
50
  <td>
43
- <%= number_to_percentage(alternative.conversion_rate) %>%
44
- <% if experiment.control.conversion_rate > 0 && !alternative.control? %>
45
- <% if alternative.conversion_rate > experiment.control.conversion_rate %>
51
+ <%= number_to_percentage(alternative.conversion_rate(goal)) %>%
52
+ <% if experiment.control.conversion_rate(goal) > 0 && !alternative.control? %>
53
+ <% if alternative.conversion_rate(goal) > experiment.control.conversion_rate(goal) %>
46
54
  <span class='better'>
47
- +<%= number_to_percentage((alternative.conversion_rate/experiment.control.conversion_rate)-1) %>%
55
+ +<%= number_to_percentage((alternative.conversion_rate(goal)/experiment.control.conversion_rate(goal))-1) %>%
48
56
  </span>
49
- <% elsif alternative.conversion_rate < experiment.control.conversion_rate %>
57
+ <% elsif alternative.conversion_rate(goal) < experiment.control.conversion_rate(goal) %>
50
58
  <span class='worse'>
51
- <%= number_to_percentage((alternative.conversion_rate/experiment.control.conversion_rate)-1) %>%
59
+ <%= number_to_percentage((alternative.conversion_rate(goal)/experiment.control.conversion_rate(goal))-1) %>%
52
60
  </span>
53
61
  <% end %>
54
62
  <% end %>
55
63
  </td>
56
64
  <td>
57
- <span title='z-score: <%= round(alternative.z_score, 3) %>'><%= confidence_level(alternative.z_score) %></span>
65
+ <span title='z-score: <%= round(alternative.z_score(goal), 3) %>'><%= confidence_level(alternative.z_score(goal)) %></span>
58
66
  </td>
59
67
  <td>
60
68
  <% if experiment.winner %>
@@ -73,13 +81,14 @@
73
81
  </tr>
74
82
 
75
83
  <% total_participants += alternative.participant_count %>
76
- <% total_completed += alternative.completed_count %>
84
+ <% total_unfinished += alternative.unfinished_count %>
85
+ <% total_completed += alternative.completed_count(goal) %>
77
86
  <% end %>
78
87
 
79
88
  <tr class="totals">
80
89
  <td>Totals</td>
81
90
  <td><%= total_participants %></td>
82
- <td><%= total_participants - total_completed %></td>
91
+ <td><%= total_unfinished %></td>
83
92
  <td><%= total_completed %></td>
84
93
  <td>N/A</td>
85
94
  <td>N/A</td>
@@ -0,0 +1,14 @@
1
+ <div class="experiment">
2
+ <div class="experiment-header">
3
+ <div class='inline-controls'>
4
+ <small><%= experiment.start_time ? experiment.start_time.strftime('%Y-%m-%d') : 'Unknown' %></small>
5
+ <form action="<%= url "/reset/#{experiment.name}" %>" method='post' onclick="return confirmReset()">
6
+ <input type="submit" value="Reset Data">
7
+ </form>
8
+ <form action="<%= url "/#{experiment.name}" %>" method='post' onclick="return confirmDelete()">
9
+ <input type="hidden" name="_method" value="delete"/>
10
+ <input type="submit" value="Delete" class="red">
11
+ </form>
12
+ </div>
13
+ </div>
14
+ </div>
@@ -2,7 +2,14 @@
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
4
  <% @experiments.each do |experiment| %>
5
- <%= erb :_experiment, :locals => {:experiment => experiment} %>
5
+ <% if experiment.goals.empty? %>
6
+ <%= erb :_experiment, :locals => {:goal => nil, :experiment => experiment} %>
7
+ <% else %>
8
+ <%= erb :_experiment_with_goal_header, :locals => {:experiment => experiment} %>
9
+ <% experiment.goals.each do |g| %>
10
+ <%= erb :_experiment, :locals => {:goal => g, :experiment => experiment} %>
11
+ <% end %>
12
+ <% end %>
6
13
  <% end %>
7
14
  <% else %>
8
15
  <p class="intro">No experiments have started yet, you need to define them in your code and introduce them to your users.</p>
@@ -3,40 +3,134 @@ module Split
3
3
  attr_accessor :name
4
4
  attr_writer :algorithm
5
5
  attr_accessor :resettable
6
+ attr_accessor :goals
7
+ attr_accessor :alternatives
6
8
 
7
- def initialize(name, options = {})
9
+ def initialize(name, options = {})
8
10
  options = {
9
- :resettable => true,
10
- }.merge(options)
11
-
12
- @name = name.to_s
13
- @alternatives = options[:alternatives] if !options[:alternatives].nil?
14
-
15
- if !options[:algorithm].nil?
16
- @algorithm = options[:algorithm].is_a?(String) ? options[:algorithm].constantize : options[:algorithm]
11
+ :resettable => true,
12
+ }.merge(options)
13
+
14
+ @name = name.to_s
15
+
16
+ alts = options[:alternatives] || []
17
+
18
+ if alts.length == 1
19
+ if alts[0].is_a? Hash
20
+ alts = alts[0].map{|k,v| {k => v} }
21
+ end
17
22
  end
18
-
19
- if !options[:resettable].nil?
20
- @resettable = options[:resettable].is_a?(String) ? options[:resettable] == 'true' : options[:resettable]
23
+
24
+ if alts.empty?
25
+ exp_config = Split.configuration.experiment_for(name)
26
+ if exp_config
27
+ alts = load_alternatives_from_configuration
28
+ options[:goals] = load_goals_from_configuration
29
+ options[:resettable] = exp_config[:resettable]
30
+ options[:algorithm] = exp_config[:algorithm]
31
+ end
21
32
  end
22
-
23
- if !options[:alternative_names].nil?
24
- @alternatives = options[:alternative_names].map do |alternative|
25
- Split::Alternative.new(alternative, name)
26
- end
33
+
34
+ self.alternatives = alts
35
+ self.goals = options[:goals]
36
+ self.algorithm = options[:algorithm]
37
+ self.resettable = options[:resettable]
38
+ end
39
+
40
+ def self.all
41
+ Split.redis.smembers(:experiments).map {|e| find(e)}
42
+ end
43
+
44
+ def self.find(name)
45
+ if Split.redis.exists(name)
46
+ obj = self.new name
47
+ obj.load_from_redis
48
+ else
49
+ obj = nil
27
50
  end
28
-
29
-
51
+ obj
30
52
  end
31
-
32
- def algorithm
33
- @algorithm ||= Split.configuration.algorithm
53
+
54
+ def self.find_or_create(label, *alternatives)
55
+ experiment_name_with_version, goals = normalize_experiment(label)
56
+ name = experiment_name_with_version.to_s.split(':')[0]
57
+
58
+ exp = self.new name, :alternatives => alternatives, :goals => goals
59
+ exp.save
60
+ exp
61
+ end
62
+
63
+ def save
64
+ validate!
65
+
66
+ if new_record?
67
+ Split.redis.sadd(:experiments, name)
68
+ Split.redis.hset(:experiment_start_times, @name, Time.now)
69
+ @alternatives.reverse.each {|a| Split.redis.lpush(name, a.name)}
70
+ @goals.reverse.each {|a| Split.redis.lpush(goals_key, a)} unless @goals.nil?
71
+ else
72
+
73
+ existing_alternatives = load_alternatives_from_redis
74
+ existing_goals = load_goals_from_redis
75
+ unless existing_alternatives == @alternatives.map(&:name) && existing_goals == @goals
76
+ reset
77
+ @alternatives.each(&:delete)
78
+ delete_goals
79
+ Split.redis.del(@name)
80
+ @alternatives.reverse.each {|a| Split.redis.lpush(name, a.name)}
81
+ @goals.reverse.each {|a| Split.redis.lpush(goals_key, a)} unless @goals.nil?
82
+ end
83
+ end
84
+
85
+ Split.redis.hset(experiment_config_key, :resettable, resettable)
86
+ Split.redis.hset(experiment_config_key, :algorithm, algorithm.to_s)
87
+ self
88
+ end
89
+
90
+ def validate!
91
+ if @alternatives.empty? && Split.configuration.experiment_for(@name).nil?
92
+ raise ExperimentNotFound.new("Experiment #{@name} not found")
93
+ end
94
+ @alternatives.each {|a| a.validate! }
95
+ unless @goals.nil? || goals.kind_of?(Array)
96
+ raise ArgumentError, 'Goals must be an array'
97
+ end
98
+ end
99
+
100
+ def new_record?
101
+ !Split.redis.exists(name)
34
102
  end
35
103
 
36
104
  def ==(obj)
37
105
  self.name == obj.name
38
106
  end
39
107
 
108
+ def [](name)
109
+ alternatives.find{|a| a.name == name}
110
+ end
111
+
112
+ def algorithm
113
+ @algorithm ||= Split.configuration.algorithm
114
+ end
115
+
116
+ def algorithm=(algorithm)
117
+ @algorithm = algorithm.is_a?(String) ? algorithm.constantize : algorithm
118
+ end
119
+
120
+ def resettable=(resettable)
121
+ @resettable = resettable.is_a?(String) ? resettable == 'true' : resettable
122
+ end
123
+
124
+ def alternatives=(alts)
125
+ @alternatives = alts.map do |alternative|
126
+ if alternative.kind_of?(Split::Alternative)
127
+ alternative
128
+ else
129
+ Split::Alternative.new(alternative, @name)
130
+ end
131
+ end
132
+ end
133
+
40
134
  def winner
41
135
  if w = Split.redis.hget(:experiment_winner, name)
42
136
  Split::Alternative.new(w, name)
@@ -44,7 +138,11 @@ module Split
44
138
  nil
45
139
  end
46
140
  end
47
-
141
+
142
+ def winner=(winner_name)
143
+ Split.redis.hset(:experiment_winner, name, winner_name.to_s)
144
+ end
145
+
48
146
  def participant_count
49
147
  alternatives.inject(0){|sum,a| sum + a.participant_count}
50
148
  end
@@ -57,26 +155,10 @@ module Split
57
155
  Split.redis.hdel(:experiment_winner, name)
58
156
  end
59
157
 
60
- def winner=(winner_name)
61
- Split.redis.hset(:experiment_winner, name, winner_name.to_s)
62
- end
63
-
64
158
  def start_time
65
159
  t = Split.redis.hget(:experiment_start_times, @name)
66
160
  Time.parse(t) if t
67
161
  end
68
-
69
- def [](name)
70
- alternatives.find{|a| a.name == name}
71
- end
72
-
73
- def alternatives
74
- @alternatives.dup
75
- end
76
-
77
- def alternative_names
78
- @alternatives.map(&:name)
79
- end
80
162
 
81
163
  def next_alternative
82
164
  winner || random_alternative
@@ -106,6 +188,10 @@ module Split
106
188
  end
107
189
  end
108
190
 
191
+ def goals_key
192
+ "#{name}:goals"
193
+ end
194
+
109
195
  def finished_key
110
196
  "#{key}:finished"
111
197
  end
@@ -125,139 +211,73 @@ module Split
125
211
  reset_winner
126
212
  Split.redis.srem(:experiments, name)
127
213
  Split.redis.del(name)
214
+ delete_goals
128
215
  increment_version
129
216
  end
130
217
 
131
- def new_record?
132
- !Split.redis.exists(name)
218
+ def delete_goals
219
+ Split.redis.del(goals_key)
133
220
  end
134
221
 
135
- def save
136
- if new_record?
137
- Split.redis.sadd(:experiments, name)
138
- Split.redis.hset(:experiment_start_times, @name, Time.now)
139
- @alternatives.reverse.each {|a| Split.redis.lpush(name, a.name) }
140
- else
141
- Split.redis.del(name)
142
- @alternatives.reverse.each {|a| Split.redis.lpush(name, a.name) }
143
- end
144
- config_key = Split::Experiment.experiment_config_key(name)
145
- Split.redis.hset(config_key, :resettable, resettable)
146
- Split.redis.hset(config_key, :algorithm, algorithm.to_s)
147
- self
222
+ def load_from_redis
223
+ exp_config = Split.redis.hgetall(experiment_config_key)
224
+ self.resettable = exp_config['resettable']
225
+ self.algorithm = exp_config['algorithm']
226
+ self.alternatives = load_alternatives_from_redis
227
+ self.goals = load_goals_from_redis
148
228
  end
149
229
 
150
- def self.load_alternatives_for(name)
151
- if Split.configuration.experiment_for(name)
152
- load_alternatives_from_configuration_for(name)
230
+ protected
231
+
232
+ def self.normalize_experiment(label)
233
+ if Hash === label
234
+ experiment_name = label.keys.first
235
+ goals = label.values.first
153
236
  else
154
- load_alternatives_from_redis_for(name)
237
+ experiment_name = label
238
+ goals = []
155
239
  end
240
+ return experiment_name, goals
156
241
  end
157
242
 
158
- def self.load_alternatives_from_configuration_for(name)
159
- alts = Split.configuration.experiment_for(name)[:alternatives]
160
- raise ArgumentError, "Experiment configuration is missing :alternatives array" if alts.nil?
161
- if alts.is_a?(Hash)
162
- alts.keys
163
- else
164
- alts.flatten
165
- end
243
+ def experiment_config_key
244
+ "experiment_configurations/#{@name}"
166
245
  end
167
246
 
168
- def self.load_alternatives_from_redis_for(name)
169
- case Split.redis.type(name)
170
- when 'set' # convert legacy sets to lists
171
- alts = Split.redis.smembers(name)
172
- Split.redis.del(name)
173
- alts.reverse.each {|a| Split.redis.lpush(name, a) }
174
- Split.redis.lrange(name, 0, -1)
247
+ def load_goals_from_configuration
248
+ goals = Split.configuration.experiment_for(@name)[:goals]
249
+ if goals.nil?
250
+ goals = []
175
251
  else
176
- Split.redis.lrange(name, 0, -1)
252
+ goals.flatten
177
253
  end
178
254
  end
179
255
 
180
- def self.load_from_configuration(name)
181
- exp_config = Split.configuration.experiment_for(name) || {}
182
- self.new(name, :alternative_names => load_alternatives_for(name),
183
- :resettable => exp_config[:resettable],
184
- :algorithm => exp_config[:algorithm])
256
+ def load_goals_from_redis
257
+ Split.redis.lrange(goals_key, 0, -1)
185
258
  end
186
259
 
187
- def self.load_from_redis(name)
188
- exp_config = Split.redis.hgetall(experiment_config_key(name))
189
- self.new(name, :alternative_names => load_alternatives_for(name),
190
- :resettable => exp_config['resettable'],
191
- :algorithm => exp_config['algorithm'])
192
- end
193
-
194
- def self.experiment_config_key(name)
195
- "experiment_configurations/#{name}"
196
- end
197
-
198
- def self.all
199
- Array(all_experiment_names_from_redis + all_experiment_names_from_configuration).map {|e| find(e)}
200
- end
201
-
202
- def self.all_experiment_names_from_redis
203
- Split.redis.smembers(:experiments)
204
- end
205
-
206
- def self.all_experiment_names_from_configuration
207
- Split.configuration.experiments ? Split.configuration.experiments.keys : []
208
- end
209
-
210
-
211
- def self.find(name)
212
- if Split.configuration.experiment_for(name)
213
- obj = load_from_configuration(name)
214
- elsif Split.redis.exists(name)
215
- obj = load_from_redis(name)
260
+ def load_alternatives_from_configuration
261
+ alts = Split.configuration.experiment_for(@name)[:alternatives]
262
+ raise ArgumentError, "Experiment configuration is missing :alternatives array" unless alts
263
+ if alts.is_a?(Hash)
264
+ alts.keys
216
265
  else
217
- obj = nil
266
+ alts.flatten
218
267
  end
219
- obj
220
268
  end
221
269
 
222
- def self.find_or_create(key, *alternatives)
223
- name = key.to_s.split(':')[0]
224
-
225
- if alternatives.length == 1
226
- if alternatives[0].is_a? Hash
227
- alternatives = alternatives[0].map{|k,v| {k => v} }
228
- end
229
- end
230
-
231
- alts = initialize_alternatives(alternatives, name)
232
-
233
- if Split.redis.exists(name)
234
- existing_alternatives = load_alternatives_for(name)
235
- if existing_alternatives == alts.map(&:name)
236
- experiment = self.new(name, :alternative_names => alternatives)
237
- else
238
- exp = self.new(name, :alternative_names => existing_alternatives)
239
- exp.reset
240
- exp.alternatives.each(&:delete)
241
- experiment = self.new(name, :alternative_names =>alternatives)
242
- experiment.save
243
- end
270
+ def load_alternatives_from_redis
271
+ case Split.redis.type(@name)
272
+ when 'set' # convert legacy sets to lists
273
+ alts = Split.redis.smembers(@name)
274
+ Split.redis.del(@name)
275
+ alts.reverse.each {|a| Split.redis.lpush(@name, a) }
276
+ Split.redis.lrange(@name, 0, -1)
244
277
  else
245
- experiment = self.new(name, :alternative_names => alternatives)
246
- experiment.save
278
+ Split.redis.lrange(@name, 0, -1)
247
279
  end
248
- return experiment
249
-
250
280
  end
251
281
 
252
- def self.initialize_alternatives(alternatives, name)
253
-
254
- unless alternatives.all? { |a| Split::Alternative.valid?(a) }
255
- raise ArgumentError, 'Alternatives must be strings'
256
- end
257
-
258
- alternatives.map do |alternative|
259
- Split::Alternative.new(alternative, name)
260
- end
261
- end
262
282
  end
263
283
  end