mountain-goat 1.0.1 → 1.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.
- data/.gitignore +2 -0
- data/README.md +5 -4
- data/generators/mg/mg_generator.rb +2 -2
- data/generators/mg/templates/update_mountain_goat_tables_v2.rb +73 -0
- data/lib/mountain-goat/controllers/mg/choices_controller.rb +81 -0
- data/lib/mountain-goat/controllers/mg/goals_controller.rb +136 -0
- data/lib/mountain-goat/controllers/mg/records_controller.rb +46 -0
- data/lib/mountain-goat/controllers/mg/tests_controller.rb +139 -0
- data/lib/mountain-goat/mg_core.rb +403 -0
- data/lib/mountain-goat/models/mg/choice.rb +50 -0
- data/lib/mountain-goat/models/mg/gi_meta.rb +10 -0
- data/lib/mountain-goat/models/mg/goal.rb +183 -0
- data/lib/mountain-goat/models/mg/goal_meta_type.rb +20 -0
- data/lib/mountain-goat/models/mg/gs_meta.rb +10 -0
- data/lib/mountain-goat/models/mg/record.rb +59 -0
- data/lib/mountain-goat/models/mg/test.rb +31 -0
- data/lib/mountain-goat/public/g-funnel.js +168 -0
- data/lib/mountain-goat/switch_choice.rb +32 -0
- data/lib/mountain-goat/version.rb +1 -1
- data/lib/mountain-goat/views/mountain_goat/layouts/.tmp_mountain_goat.html.erb.50680~ +76 -0
- data/lib/mountain-goat/views/mountain_goat/mg/choices/_choice_form.html.erb +37 -0
- data/lib/mountain-goat/views/mountain_goat/mg/choices/edit.html.erb +13 -0
- data/lib/mountain-goat/views/mountain_goat/mg/choices/index.html.erb +34 -0
- data/lib/mountain-goat/views/mountain_goat/mg/choices/new.html.erb +15 -0
- data/lib/mountain-goat/views/mountain_goat/mg/choices/show.html.erb +30 -0
- data/lib/mountain-goat/views/mountain_goat/mg/goals/.tmp__goal_form.html.erb.5027~ +0 -0
- data/lib/mountain-goat/views/mountain_goat/mg/goals/.tmp__goal_meta_type_form.html.erb.39992~ +0 -0
- data/lib/mountain-goat/views/mountain_goat/mg/goals/.tmp_edit.html.erb.55874~ +0 -0
- data/lib/mountain-goat/views/mountain_goat/mg/goals/.tmp_index.html.erb.97274~ +36 -0
- data/lib/mountain-goat/views/mountain_goat/mg/goals/_goal_form.html.erb +28 -0
- data/lib/mountain-goat/views/mountain_goat/mg/goals/_goal_meta_type_form.html.erb +34 -0
- data/lib/mountain-goat/views/mountain_goat/mg/goals/edit.html.erb +12 -0
- data/lib/mountain-goat/views/mountain_goat/mg/goals/index.html.erb +37 -0
- data/lib/mountain-goat/views/mountain_goat/mg/goals/new.html.erb +12 -0
- data/lib/mountain-goat/views/mountain_goat/mg/goals/show.html.erb +32 -0
- data/lib/mountain-goat/views/mountain_goat/mg/records/_record.html.erb +16 -0
- data/lib/mountain-goat/views/mountain_goat/mg/records/_records.html.erb +5 -0
- data/lib/mountain-goat/views/mountain_goat/mg/records/_records_form.html.erb +21 -0
- data/lib/mountain-goat/views/mountain_goat/mg/records/edit.html.erb +13 -0
- data/lib/mountain-goat/views/mountain_goat/mg/records/index.html.erb +17 -0
- data/lib/mountain-goat/views/mountain_goat/mg/records/new.html.erb +13 -0
- data/lib/mountain-goat/views/mountain_goat/mg/records/show.html.erb +14 -0
- data/lib/mountain-goat/views/mountain_goat/mg/report_items/.tmp__chart.html.erb.13419~ +18 -0
- data/lib/mountain-goat/views/mountain_goat/mg/report_items/.tmp__funnel.html.erb.60493~ +18 -0
- data/lib/mountain-goat/views/mountain_goat/mg/report_items/.tmp__report_item_form.html.erb.87420~ +10 -0
- data/lib/mountain-goat/views/mountain_goat/mg/report_items/.tmp__report_item_pivot_form.html.erb.77056~ +14 -0
- data/lib/mountain-goat/views/mountain_goat/mg/report_items/.tmp_edit.html.erb.31048~ +19 -0
- data/lib/mountain-goat/views/mountain_goat/mg/report_items/.tmp_new.html.erb.36371~ +17 -0
- data/lib/mountain-goat/views/mountain_goat/mg/report_items/_funnel.html.erb +13 -0
- data/lib/mountain-goat/views/mountain_goat/mg/reports/.tmp__report_form.html.erb.76535~ +21 -0
- data/lib/mountain-goat/views/mountain_goat/mg/reports/.tmp__report_report_items.html.erb.26030~ +5 -0
- data/lib/mountain-goat/views/mountain_goat/mg/reports/.tmp_edit.html.erb.78064~ +36 -0
- data/lib/mountain-goat/views/mountain_goat/mg/reports/.tmp_index.html.erb.74591~ +23 -0
- data/lib/mountain-goat/views/mountain_goat/mg/reports/.tmp_show.html.erb.58427~ +21 -0
- data/lib/mountain-goat/views/mountain_goat/mg/tests/_test_form.html.erb +25 -0
- data/lib/mountain-goat/views/mountain_goat/mg/tests/edit.html.erb +12 -0
- data/lib/mountain-goat/views/mountain_goat/mg/tests/index.html.erb +48 -0
- data/lib/mountain-goat/views/mountain_goat/mg/tests/new.html.erb +12 -0
- data/lib/mountain-goat/views/mountain_goat/mg/tests/show.html.erb +63 -0
- metadata +69 -6
- data/lib/mountain-goat/metric_tracking.rb +0 -401
- data/lib/mountain-goat/switch_variant.rb +0 -32
@@ -0,0 +1,403 @@
|
|
1
|
+
require File.join([File.dirname(__FILE__), 'switch_choice'])
|
2
|
+
|
3
|
+
module MgCore
|
4
|
+
|
5
|
+
#Metric Tracking routes
|
6
|
+
class << ActionController::Routing::Routes;self;end.class_eval do
|
7
|
+
define_method :clear!, lambda {}
|
8
|
+
end
|
9
|
+
|
10
|
+
Mime::Type.register "application/xhtml+xml", :xhtml
|
11
|
+
|
12
|
+
ActionController::Routing::Routes.draw do |map|
|
13
|
+
map.namespace :mg do |mg|
|
14
|
+
mg.mg '/mg', :controller => :goals, :action => :index, :path_prefix => ""
|
15
|
+
mg.login '/login', :controller => :mountain_goat, :action => :login
|
16
|
+
mg.login_create '/login/create', :controller => :mountain_goat, :action => :login_create
|
17
|
+
mg.resources :choices
|
18
|
+
mg.resources :goals, :has_many => [ :record ], :member => { :hide => :get, :unhide => :get }
|
19
|
+
mg.resources :tests, :has_many => :choices, :member => { :hide => :get, :unhide => :get }
|
20
|
+
mg.resources :records, :collection => { :new_records => :get }
|
21
|
+
mg.resources :reports, :has_many => :report_items, :member => { :show_svg => :get, :hide => :get, :unhide => :get }
|
22
|
+
mg.resources :report_items, :member => { :destroy => :get, :update => :post }, :collection => { :get_extra => :get }
|
23
|
+
mg.resources :playground, :collection => { :test => :get }
|
24
|
+
mg.new_records '/records/new', :controller => :records, :action => :new_records
|
25
|
+
mg.fresh_choices '/fresh-choices', :controller => :tests, :action => :fresh_choices
|
26
|
+
mg.connect '/public/:file', :controller => :mountain_goat, :action => :fetch
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
module Controller
|
31
|
+
|
32
|
+
#This is just for testing
|
33
|
+
def mg_rand(evaluate = false)
|
34
|
+
return "(SELECT #{@mg_i.nil? ? 1 : @mg_i.to_f})" if defined?(MOUNTAIN_GOAT_TEST) && MOUNTAIN_GOAT_TEST
|
35
|
+
evaluate ? rand.to_f : "RAND()"
|
36
|
+
end
|
37
|
+
|
38
|
+
def mg_epsilon
|
39
|
+
if @mg_epsilon.nil?
|
40
|
+
@mg_epsilon = 0.1 #default
|
41
|
+
mg_yml = nil
|
42
|
+
begin
|
43
|
+
mg_yml = YAML::load(File.open("#{RAILS_ROOT}/config/mountain-goat.yml"))
|
44
|
+
rescue
|
45
|
+
end
|
46
|
+
if mg_yml
|
47
|
+
if mg_yml.has_key?(RAILS_ENV) && mg_yml[RAILS_ENV].has_key?('epsilon')
|
48
|
+
@mg_epsilon = mg_yml[RAILS_ENV]['epsilon'].to_f
|
49
|
+
elsif mg_yml.has_key?('settings') && mg_yml['settings'].has_key?('epsilon')
|
50
|
+
@mg_epsilon = mg_yml['settings']['epsilon'].to_f
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
return @mg_epsilon
|
55
|
+
end
|
56
|
+
|
57
|
+
def mg_strategy
|
58
|
+
if @mg_strategy.nil?
|
59
|
+
@mg_strategy = 'e-greedy' #default
|
60
|
+
mg_yml = nil
|
61
|
+
begin
|
62
|
+
mg_yml = YAML::load(File.open("#{RAILS_ROOT}/config/mountain-goat.yml"))
|
63
|
+
rescue
|
64
|
+
end
|
65
|
+
if mg_yml
|
66
|
+
if mg_yml.has_key?(RAILS_ENV) && mg_yml[RAILS_ENV].has_key?('strategy')
|
67
|
+
@mg_strategy = mg_yml[RAILS_ENV]['strategy']
|
68
|
+
elsif mg_yml.has_key?('settings') && mg_yml['settings'].has_key?('strategy')
|
69
|
+
@mg_strategy = mg_yml['settings']['strategy']
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
return @mg_strategy
|
74
|
+
end
|
75
|
+
|
76
|
+
def mg_apply_strategy(test)
|
77
|
+
case mg_strategy.downcase
|
78
|
+
when 'e-greedy'
|
79
|
+
logger.warn Mg::Choice.all(:order => "CASE WHEN served = 0 THEN 1 ELSE 0 END DESC, CASE WHEN #{mg_rand(true).to_f} < #{mg_epsilon.to_f} THEN #{mg_rand} ELSE CASE WHEN served = 0 THEN -1 ELSE reward / served END END DESC, #{mg_rand} DESC", :conditions => { :mg_test_id => test.id } )
|
80
|
+
return Mg::Choice.first(:order => "CASE WHEN served = 0 THEN 1 ELSE 0 END DESC, CASE WHEN #{mg_rand(true).to_f} < #{mg_epsilon.to_f} THEN #{mg_rand} ELSE CASE WHEN served = 0 THEN -1 ELSE reward / served END END DESC, #{mg_rand} DESC", :conditions => { :mg_test_id => test.id } )
|
81
|
+
when 'e-greedy-decreasing'
|
82
|
+
return Mg::Choice.first(:order => "CASE WHEN served = 0 THEN 1 ELSE 0 END DESC,
|
83
|
+
CASE WHEN #{mg_rand(true).to_f} < #{mg_epsilon.to_f} / ( select sum(served) from mg_metric_variants where metric_id = #{ metric.id.to_i } ) THEN #{mg_rand} ELSE CASE WHEN served = 0 THEN -1 ELSE reward / served END END DESC,
|
84
|
+
#{mg_rand} DESC", :conditions => { :mg_test_id => test.id } ) # * log( ( select sum(served) from mg_metric_variants where metric_id = #{ metric.id.to_i } ) )
|
85
|
+
when 'a/b'
|
86
|
+
return Mg::Choice.first(:order => "#{mg_rand} DESC", :conditions => { :mg_test_id => test.id } )
|
87
|
+
else
|
88
|
+
raise "Invalid strategy #{mg_strategy}"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def mg_storage
|
93
|
+
if @mg_storage.nil?
|
94
|
+
@mg_storage = defined?(cookies) ? cookies : nil
|
95
|
+
|
96
|
+
mg_yml = nil
|
97
|
+
begin
|
98
|
+
mg_yml = YAML::load(File.open("#{RAILS_ROOT}/config/mountain-goat.yml"))
|
99
|
+
rescue
|
100
|
+
end
|
101
|
+
if mg_yml
|
102
|
+
if mg_yml.has_key?(RAILS_ENV) && mg_yml[RAILS_ENV].has_key?('storage')
|
103
|
+
uc = mg_yml[RAILS_ENV]['storage'].strip
|
104
|
+
@mg_storage = ( uc == "cookies" && defined?(cookies) ) ? cookies : ( uc == "session" && defined?(session) ) ? session : nil
|
105
|
+
elsif mg_yml.has_key?('settings') && mg_yml['settings'].has_key?('storage')
|
106
|
+
uc = mg_yml['settings']['storage'].strip
|
107
|
+
@mg_storage = ( uc == "cookies" && defined?(cookies) ) ? cookies : ( uc == "session" && defined?(session) ) ? session : nil
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
@mg_storage = {} if @mg_storage.nil? #'none'
|
112
|
+
return @mg_storage
|
113
|
+
end
|
114
|
+
|
115
|
+
######################
|
116
|
+
# Bandit Tracking #
|
117
|
+
######################
|
118
|
+
|
119
|
+
def bds(test_type, &block)
|
120
|
+
raise ArgumentError, "Switch choice needs block" if !block_given?
|
121
|
+
test = get_test( test_type, true )
|
122
|
+
block.call(SwitchChoice.new( logger, test, nil ) )
|
123
|
+
|
124
|
+
var = get_switch_choice( test_type )
|
125
|
+
block.call(SwitchChoice.new( logger, test, var ) )
|
126
|
+
end
|
127
|
+
|
128
|
+
def bd(test_type, default, opts = {}, opt = nil)
|
129
|
+
return get_choice(test_type, default, opts, opt)[:value]
|
130
|
+
end
|
131
|
+
|
132
|
+
def bdd(test_type, default, opts = {}, opt = nil)
|
133
|
+
return get_choice(test_type, default, opts, opt)
|
134
|
+
end
|
135
|
+
|
136
|
+
#Legacy
|
137
|
+
def sv(test_type, goal_type, &block)
|
138
|
+
bds(test_type, &block)
|
139
|
+
end
|
140
|
+
|
141
|
+
def mv(test_type, goal_type, default, opts = {}, opt = nil)
|
142
|
+
bd(test_type, default, opts, opt)
|
143
|
+
end
|
144
|
+
|
145
|
+
def mv_detailed(test_type, goal_type, default, opts = {}, opt = nil)
|
146
|
+
bdd(test_type, default, opts, opt)
|
147
|
+
end
|
148
|
+
|
149
|
+
#shorthand
|
150
|
+
def rw(goal_type, reward, options = {})
|
151
|
+
self.bandit_reward(goal_type, reward, options)
|
152
|
+
end
|
153
|
+
|
154
|
+
def rc(goal_type, options = {})
|
155
|
+
self.bandit_reward(goal_type, 1, options)
|
156
|
+
end
|
157
|
+
|
158
|
+
def record_conversion(goal_type, options = {})
|
159
|
+
self.bandit_reward(goal_type, 1, options)
|
160
|
+
end
|
161
|
+
|
162
|
+
#allows bandit_reward(goal, options)
|
163
|
+
def bandit_reward(goal_type, reward, options = {})
|
164
|
+
|
165
|
+
if reward.is_a?(Hash) #allow arguments bandit_reward(test, options)
|
166
|
+
options = reward
|
167
|
+
reward = 0
|
168
|
+
end
|
169
|
+
|
170
|
+
tests = {} #for user-defined metrics
|
171
|
+
options = options.with_indifferent_access
|
172
|
+
|
173
|
+
MountainGoat.get_meta_options.each do |k, v|
|
174
|
+
if options.include?(k) && options[k]
|
175
|
+
options.delete(k)
|
176
|
+
res = v.call(self)
|
177
|
+
options.merge!( res ) if !res.nil? && res.instance_of?(Hash)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
options.each do |k, v|
|
182
|
+
if k.to_s =~ /^test_(\w+)$/i
|
183
|
+
options.delete k
|
184
|
+
tests.merge!({ $1, v })
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
logger.warn "Recording goal #{goal_type.to_s} with options #{options.inspect}"
|
189
|
+
|
190
|
+
goal = Mg::Goal.first( :conditions => { :goal_type => goal_type.to_s } )
|
191
|
+
|
192
|
+
# Now, we just create the goal if we don't have one
|
193
|
+
goal = Mg::Goal.create!( :goal_type => goal_type.to_s, :name => goal_type.to_s, :rewards_total => reward, :rewards_given => 1 ) if goal.nil?
|
194
|
+
|
195
|
+
# First, let's tally for the goal itself
|
196
|
+
goal.tally_reward_given( reward )
|
197
|
+
|
198
|
+
# We need to see what meta information we should fill based on the goal type
|
199
|
+
Mg::Record.create!( { :mg_goal_id => goal.id, :reward => reward } ).set_meta_data(options)
|
200
|
+
|
201
|
+
# User-defined test tallies
|
202
|
+
tests.each do |test_type, choice_id|
|
203
|
+
t = Mg::Test.find_by_test_type(test_type)
|
204
|
+
if t.nil?
|
205
|
+
logger.warn "Missing user-defined test #{test_type}"
|
206
|
+
next
|
207
|
+
end
|
208
|
+
|
209
|
+
c = t.choices.first( :conditions => { :id => choice_id } ) #make sure everything matches up
|
210
|
+
|
211
|
+
if c.nil?
|
212
|
+
logger.warn "Choice #{choice_id} not in choices for #{t.title}"
|
213
|
+
next
|
214
|
+
end
|
215
|
+
|
216
|
+
logger.warn "Tallying goal #{goal.name} for #{t.title} - #{c.name} (#{c.value} - #{c.id})"
|
217
|
+
c.tally_goal(goal, reward)
|
218
|
+
end
|
219
|
+
|
220
|
+
if !mg_storage.nil?
|
221
|
+
#we just converted, let's tally each of our metrics (from cookies or session)
|
222
|
+
Mg::Test.all.each do |test|
|
223
|
+
test_sym = "test_#{test.test_type}".to_sym
|
224
|
+
choice_sym = "test_#{test.test_type}_choice".to_sym
|
225
|
+
|
226
|
+
value = mg_storage[test_sym]
|
227
|
+
choice_id = mg_storage[choice_sym]
|
228
|
+
|
229
|
+
#logger.warn "Value: #{metric_sym} - #{value}"
|
230
|
+
#logger.warn "Value: #{metric_variant_sym} - #{variant_id}"
|
231
|
+
|
232
|
+
if choice_id.blank? #the user just doesn't have this set
|
233
|
+
#This is now common-case
|
234
|
+
next
|
235
|
+
end
|
236
|
+
|
237
|
+
choice = Mg::Choice.first(:conditions => { :id => choice_id.to_i } )
|
238
|
+
|
239
|
+
if choice.nil?
|
240
|
+
logger.error "Choice #{choice_id} not in choices for #{test.title}"
|
241
|
+
next
|
242
|
+
end
|
243
|
+
|
244
|
+
if choice.value != value
|
245
|
+
logger.warn "Choice #{choice.name} values differ for test #{test.title}. '#{choice.value}' != '#{value}'!"
|
246
|
+
end
|
247
|
+
|
248
|
+
logger.warn "Tallying goal #{goal.name} for #{test.title} - #{choice.name} (#{choice.value} - #{choice.id})"
|
249
|
+
choice.tally_goal(goal, reward)
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
private
|
255
|
+
|
256
|
+
#returns a map { :value => value, :choice_id => id }
|
257
|
+
def get_choice(test_type, default, opts = {}, opt = nil)
|
258
|
+
test_sym = "test_#{test_type}#{ opt.nil? ? "" : '_' + opt.to_s }".to_sym
|
259
|
+
choice_sym = "test_#{test_type}_choice".to_sym
|
260
|
+
|
261
|
+
#first, we'll check for a cookie value
|
262
|
+
if !mg_storage.nil? && mg_storage[test_sym] && !mg_storage[test_sym].blank?
|
263
|
+
#we have the cookie
|
264
|
+
choice_id = mg_storage[choice_sym]
|
265
|
+
choice = Mg::Choice.first(:conditions => { :id => choice_id.to_i } )
|
266
|
+
if !choice.nil?
|
267
|
+
if choice.mg_test.tally_each_serve
|
268
|
+
choice.tally_serve
|
269
|
+
end
|
270
|
+
else
|
271
|
+
logger.warn "Serving test #{test_type} #{ opt.nil? ? "" : opt.to_s } without finding / tallying choice."
|
272
|
+
end
|
273
|
+
|
274
|
+
return { :value => mg_storage[test_sym], :choice_id => mg_storage[choice_sym] } #it's the best we can do
|
275
|
+
else
|
276
|
+
#we don't have the cookie, let's find a value to set
|
277
|
+
test = get_test( test_type, false )
|
278
|
+
|
279
|
+
choice = mg_apply_strategy(test)
|
280
|
+
|
281
|
+
if choice.nil?
|
282
|
+
logger.warn "Missing choices for #{test_type}"
|
283
|
+
choice = Mg::Choice.create!( { :mg_test_id => test.id, :value => default, :name => default }.merge(opts) )
|
284
|
+
end
|
285
|
+
|
286
|
+
if choice.mg_test.tally_each_serve
|
287
|
+
choice.tally_serve # denote we served this to a user
|
288
|
+
end
|
289
|
+
|
290
|
+
value = choice.read_attribute( opt.nil? ? :value : opt )
|
291
|
+
logger.debug "Serving #{choice.name} (#{value}) for #{test_sym}"
|
292
|
+
#good, we have a variant, let's store it in session
|
293
|
+
|
294
|
+
if !mg_storage.nil?
|
295
|
+
mg_storage[test_sym] = value #, :domain => WILD_DOMAIN
|
296
|
+
mg_storage[choice_sym] = choice.id #, :domain => WILD_DOMAIN
|
297
|
+
end
|
298
|
+
|
299
|
+
return { :value => value, :choice_id => choice.id }
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
def get_switch_choice(test_type)
|
304
|
+
choice_sym = "test_#{test_type}_choice".to_sym
|
305
|
+
|
306
|
+
#first, we'll check for a cookie selection
|
307
|
+
if !mg_storage.nil? && mg_storage[choice_sym] && !mg_storage[choice_sym].blank?
|
308
|
+
#we have the cookie
|
309
|
+
|
310
|
+
choice_id = mg_storage[choice_sym]
|
311
|
+
choice = Mg::Choice.first(:conditions => { :id => choice_id.to_i } )
|
312
|
+
|
313
|
+
if !choice.nil?
|
314
|
+
if choice.mg_test.tally_each_serve
|
315
|
+
choice.tally_serve
|
316
|
+
end
|
317
|
+
|
318
|
+
return choice
|
319
|
+
|
320
|
+
end
|
321
|
+
|
322
|
+
#otherwise, it's a big wtf? let's just move on
|
323
|
+
logger.warn "Missing choice for #{test_type} (switch-type), reassigning..."
|
324
|
+
end
|
325
|
+
|
326
|
+
#we don't have the cookie, let's find a value to set
|
327
|
+
test = get_test( test_type, true )
|
328
|
+
|
329
|
+
choice = mg_apply_strategy(test)
|
330
|
+
|
331
|
+
if choice.nil?
|
332
|
+
logger.warn "Missing choices for #{test_type}"
|
333
|
+
raise ArgumentError, "Missing choices for switch-type #{test_type}"
|
334
|
+
end
|
335
|
+
|
336
|
+
if choice.mg_test.tally_each_serve
|
337
|
+
choice.tally_serve # denote we served this to a user
|
338
|
+
end
|
339
|
+
|
340
|
+
logger.debug "Serving #{choice.name} (#{choice.switch_type}) for #{test.title} (switch-type)"
|
341
|
+
#good, we have a variant, let's store it in session (not the value, just the selection)
|
342
|
+
if !mg_storage.nil?
|
343
|
+
mg_storage[choice_sym] = choice.id #, :domain => WILD_DOMAIN
|
344
|
+
end
|
345
|
+
|
346
|
+
return choice
|
347
|
+
end
|
348
|
+
|
349
|
+
def get_test(test_type, is_switch = false)
|
350
|
+
|
351
|
+
test = Mg::Test.first(:conditions => { :test_type => test_type.to_s } )
|
352
|
+
|
353
|
+
if test.nil? #we don't have a metric of this type
|
354
|
+
logger.warn "Missing test type #{test_type.to_s} -- creating"
|
355
|
+
test = Mg::Test.create( :test_type => test_type.to_s, :title => test_type.to_s, :is_switch => is_switch )
|
356
|
+
end
|
357
|
+
|
358
|
+
return test
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
module View
|
363
|
+
def mv(*args, &block)
|
364
|
+
@controller.send(:mv, *args, &block)
|
365
|
+
end
|
366
|
+
|
367
|
+
def mv_detailed(*args, &block)
|
368
|
+
@controller.send(:mv_detailed, *args, &block)
|
369
|
+
end
|
370
|
+
|
371
|
+
def sv(*args, &block)
|
372
|
+
@controller.send(:sv, *args, &block)
|
373
|
+
end
|
374
|
+
|
375
|
+
def bd(*args, &block)
|
376
|
+
@controller.send(:bd, *args, &block)
|
377
|
+
end
|
378
|
+
|
379
|
+
def bdd(*args, &block)
|
380
|
+
@controller.send(:bdd, *args, &block)
|
381
|
+
end
|
382
|
+
|
383
|
+
def bds(*args, &block)
|
384
|
+
@controller.send(:bds, *args, &block)
|
385
|
+
end
|
386
|
+
end
|
387
|
+
end
|
388
|
+
|
389
|
+
class ActionController::Base
|
390
|
+
include MgCore::Controller
|
391
|
+
end
|
392
|
+
|
393
|
+
class ActionView::Base
|
394
|
+
include MgCore::View
|
395
|
+
end
|
396
|
+
|
397
|
+
class ActionMailer::Base
|
398
|
+
include MgCore::Controller
|
399
|
+
end
|
400
|
+
|
401
|
+
class ActiveRecord::Base
|
402
|
+
include MgCore::Controller
|
403
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# Mg::Choice represents a split of an a/b test
|
2
|
+
#
|
3
|
+
# Attributes
|
4
|
+
# mg_test_id:: ID of Mg::Test which this is a choice for
|
5
|
+
# name::
|
6
|
+
# value:: The value to serve when this choice comes up (assuming non-switch)
|
7
|
+
# opt1:: Optional additional data
|
8
|
+
# opt2:: Optional additional data
|
9
|
+
# served:: Number of times this choice has been served
|
10
|
+
# reward:: Total accumulated reward for this choice
|
11
|
+
# reward_count:: How many rewards factor into this total
|
12
|
+
# switch_type:: Is this a switch-type choice
|
13
|
+
# deleted_at:: Is this choice still active?
|
14
|
+
class Mg::Choice < ActiveRecord::Base
|
15
|
+
set_table_name :mg_choices
|
16
|
+
|
17
|
+
# ActiveRecord Associations
|
18
|
+
belongs_to :mg_test, :class_name => "Mg::Test"
|
19
|
+
|
20
|
+
# Validations
|
21
|
+
validates_presence_of :name
|
22
|
+
validates_presence_of :mg_test_id
|
23
|
+
|
24
|
+
# Member Functions
|
25
|
+
|
26
|
+
# Mark that we have served this choice
|
27
|
+
def tally_serve
|
28
|
+
self.transaction do
|
29
|
+
self.update_attribute(:reward, 0) if self.reward.nil? #we should merge this with the next line, but whatever
|
30
|
+
Mg::Choice.update_counters(self.id, :served => 1)
|
31
|
+
end
|
32
|
+
|
33
|
+
return self.reload
|
34
|
+
end
|
35
|
+
|
36
|
+
# Reward has a "default" or adjustable setting
|
37
|
+
def tally_goal(goal, reward)
|
38
|
+
self.transaction do
|
39
|
+
Mg::Choice.update_counters(self.id, :reward_count => 1, :reward => reward)
|
40
|
+
end
|
41
|
+
|
42
|
+
return self.reload
|
43
|
+
end
|
44
|
+
|
45
|
+
# What is the average reward given to this choice
|
46
|
+
def reward_rate
|
47
|
+
return nil if self.reward_count == 0 || self.reward_count.nil? || self.reward.nil?
|
48
|
+
return self.reward / self.reward_count.to_f
|
49
|
+
end
|
50
|
+
end
|