split 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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