conductor 0.8.3 → 0.9.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. data/{LICENSE → MIT-LICENSE} +1 -1
  2. data/README.rdoc +7 -0
  3. data/Rakefile +25 -45
  4. data/app/assets/javascripts/conductor/application.js +15 -0
  5. data/app/assets/javascripts/conductor/dashboard.js +2 -0
  6. data/app/assets/stylesheets/conductor/application.css +13 -0
  7. data/{generators/conductor/templates/conductor.css → app/assets/stylesheets/conductor/dashboard.css} +0 -0
  8. data/app/controllers/conductor/application_controller.rb +4 -0
  9. data/app/controllers/conductor/dashboard_controller.rb +11 -0
  10. data/app/helpers/conductor/application_helper.rb +4 -0
  11. data/{lib/conductor/rails/helpers → app/helpers/conductor}/dashboard_helper.rb +3 -1
  12. data/app/models/conductor/experiment/daily.rb +47 -0
  13. data/app/models/conductor/experiment/history.rb +9 -0
  14. data/app/models/conductor/experiment/raw.rb +20 -0
  15. data/app/models/conductor/experiment/weight.rb +11 -0
  16. data/{lib/conductor/rails/views → app/views/conductor}/dashboard/_current_weights.html.haml +0 -0
  17. data/{lib/conductor/rails/views → app/views/conductor}/dashboard/_daily_stats.html.haml +0 -0
  18. data/{lib/conductor/rails/views → app/views/conductor}/dashboard/_group_stats.html.haml +0 -0
  19. data/{lib/conductor/rails/views → app/views/conductor}/dashboard/_top_nav.html.haml +0 -0
  20. data/{lib/conductor/rails/views → app/views/conductor}/dashboard/_weight_history.html.haml +0 -0
  21. data/app/views/conductor/dashboard/index.html.haml +12 -0
  22. data/app/views/layouts/conductor/application.html.erb +14 -0
  23. data/config/routes.rb +4 -0
  24. data/db/migrate/20121010131323_create_conductor_daily_experiments.rb +16 -0
  25. data/db/migrate/20121010173406_create_conductor_experiment_weights.rb +13 -0
  26. data/db/migrate/20121010174835_create_conductor_raw_experiments.rb +13 -0
  27. data/db/migrate/20121010174950_create_conductor_weight_histories.rb +14 -0
  28. data/lib/conductor.rb +33 -57
  29. data/lib/conductor/core_ext.rb +18 -0
  30. data/lib/conductor/engine.rb +5 -0
  31. data/lib/conductor/experiment.rb +61 -60
  32. data/lib/conductor/roll_up.rb +2 -2
  33. data/lib/conductor/version.rb +3 -0
  34. data/lib/conductor/weights.rb +9 -8
  35. data/lib/tasks/conductor_tasks.rake +9 -0
  36. data/test/core_ext_test.rb +13 -0
  37. data/test/fixtures/conductor/conductor_daily_experiments.yml +17 -0
  38. data/test/functional/conductor/dashboard_controller_test.rb +11 -0
  39. data/test/integration/conductor/dashboard_test.rb +7 -0
  40. data/test/integration/navigation_test.rb +10 -0
  41. data/test/test_helper.rb +9 -23
  42. data/test/unit/conductor/conductor_daily_experiment_test.rb +9 -0
  43. data/test/unit/helpers/conductor/dashboard_helper_test.rb +6 -0
  44. metadata +240 -91
  45. data/VERSION +0 -1
  46. data/generators/conductor/conductor_generator.rb +0 -27
  47. data/generators/conductor/templates/conductor.rake +0 -6
  48. data/generators/conductor/templates/migration.rb +0 -54
  49. data/init.rb +0 -2
  50. data/lib/conductor/rails/controllers/dashboard.rb +0 -16
  51. data/lib/conductor/rails/models/daily.rb +0 -53
  52. data/lib/conductor/rails/models/history.rb +0 -14
  53. data/lib/conductor/rails/models/raw.rb +0 -27
  54. data/lib/conductor/rails/models/weight.rb +0 -18
  55. data/lib/conductor/rails/views/dashboard/index.html.haml +0 -29
  56. data/rails/init.rb +0 -7
  57. data/test/db/schema.rb +0 -43
  58. data/test/test_conductor.rb +0 -346
data/VERSION DELETED
@@ -1 +0,0 @@
1
- 0.8.3
@@ -1,27 +0,0 @@
1
- # encoding: utf-8
2
-
3
- class ConductorGenerator < Rails::Generator::Base
4
-
5
- def initialize(*runtime_args)
6
- super
7
- end
8
-
9
- def manifest
10
- record do |m|
11
- m.directory File.join('lib', 'tasks')
12
- m.template 'conductor.rake', File.join('lib', 'tasks', 'conductor.rake')
13
-
14
- m.migration_template 'migration.rb', 'db/migrate', :migration_file_name => "conductor_migration"
15
-
16
- m.directory File.join('public', 'stylesheets')
17
- m.template 'conductor.css', File.join('public', 'stylesheets', 'conductor.css')
18
- end
19
- end
20
-
21
- protected
22
-
23
- def banner
24
- %{Usage: #{$0} #{spec.name}\nCopies conductor.rake to lib/tasks. Copies migration file to db/migrate. Copies conductor.css to public/stylesheets/}
25
- end
26
-
27
- end
@@ -1,6 +0,0 @@
1
- namespace :conductor do
2
- desc "Rolls-up raw data into the daily conductor model for use in weights"
3
- task :rollup do
4
- Conductor::Rollup.process
5
- end
6
- end
@@ -1,54 +0,0 @@
1
- #Creates the database tables, plus indexes, you'll need to use Conductor.
2
-
3
- class ConductorMigration < ActiveRecord::Migration
4
- def self.up
5
- create_table "conductor_daily_experiments", :force => true do |t|
6
- t.date "activity_date"
7
- t.string "group_name"
8
- t.string "alternative"
9
- t.decimal "conversion_value", :precision => 8, :scale => 2
10
- t.integer "views"
11
- t.integer "conversions"
12
- end
13
-
14
- add_index "conductor_daily_experiments", ["activity_date"], :name => "index_conductor_daily_experiments_on_activity_date"
15
- add_index "conductor_daily_experiments", ["group_name"], :name => "index_conductor_daily_experiments_on_group_name"
16
-
17
- create_table "conductor_raw_experiments", :force => true do |t|
18
- t.string "identity_id"
19
- t.string "group_name"
20
- t.string "alternative"
21
- t.decimal "conversion_value", :precision => 8, :scale => 2
22
- t.datetime "created_at"
23
- t.datetime "updated_at"
24
- t.string "goal"
25
- end
26
-
27
- create_table "conductor_weight_histories", :force => true do |t|
28
- t.string "group_name"
29
- t.string "alternative"
30
- t.decimal "weight", :precision => 8, :scale => 2
31
- t.datetime "computed_at"
32
- t.integer "launch_window"
33
- end
34
-
35
- add_index "conductor_weight_histories", ["computed_at", "group_name"], :name => "conductor_wh_date_and_group_ndx"
36
-
37
- create_table "conductor_weighted_experiments", :force => true do |t|
38
- t.string "group_name"
39
- t.string "alternative"
40
- t.decimal "weight", :precision => 8, :scale => 2
41
- t.datetime "created_at"
42
- t.datetime "updated_at"
43
- end
44
-
45
- add_index "conductor_weighted_experiments", ["group_name"], :name => "index_conductor_weighted_experiments_on_group_name"
46
- end
47
-
48
- def self.down
49
- drop_table :conductor_raw_experiments
50
- drop_table :conductor_daily_experiments
51
- drop_table :conductor_weight_histories
52
- drop_table :conductor_weighted_experiments
53
- end
54
- end
data/init.rb DELETED
@@ -1,2 +0,0 @@
1
- require File.join(File.dirname(__FILE__), 'rails', 'init')
2
-
@@ -1,16 +0,0 @@
1
- class Conductor
2
- module Controller
3
- module Dashboard
4
-
5
- ActionController::Base.view_paths.unshift File.join(File.dirname(__FILE__), "../views")
6
-
7
- def index
8
- @weights = Conductor::Experiment::Weight.all
9
- @weight_history = Conductor::Experiment::History.all
10
- @dailies = Conductor::Experiment::Daily.all
11
- render :template => 'dashboard/index'
12
- end
13
-
14
- end
15
- end
16
- end
@@ -1,53 +0,0 @@
1
- # == Schema Information
2
- #
3
- # Table name: conductor_daily_experiments
4
- #
5
- # id :integer not null, primary key
6
- # activity_date :date
7
- # group_name :string(255)
8
- # option_name :string(255)
9
- # conversion_value :decimal(8, 2)
10
- # views :integer
11
- # conversions :integer
12
- #
13
-
14
- class Conductor::Experiment::Daily < ActiveRecord::Base
15
- set_table_name "conductor_daily_experiments"
16
-
17
- named_scope :since, lambda { |a_date| { :conditions => ['activity_date >= ?',a_date] }}
18
- named_scope :for_group, lambda { |group_name| { :conditions => ['group_name = ?',group_name] }}
19
-
20
- def self.find_equalization_period_stats_for(group_name, alternatives=nil)
21
- alternative_filter = alternatives ? alternatives.inject([]) {|res,x| res << "alternative = '#{Conductor.sanitize(x)}'"}.join(' OR ') : 'true'
22
-
23
- sql = "SELECT alternative, min(activity_date) AS activity_date
24
- FROM conductor_daily_experiments
25
- WHERE group_name = '#{group_name}'
26
- AND (#{alternative_filter})
27
- GROUP BY alternative
28
- HAVING min(activity_date) > '#{Date.today - Conductor.equalization_period}'"
29
-
30
- self.find_by_sql(sql)
31
- end
32
-
33
-
34
- def self.find_post_equalization_period_stats_for(group_name, alternatives=nil)
35
- alternative_filter = alternatives ? alternatives.inject([]) {|res,x| res << "alternative = '#{Conductor.sanitize(x)}'"}.join(' OR ') : 'true'
36
-
37
- sql = "SELECT alternative, min(activity_date) AS activity_date, sum(views) AS views, sum(conversions) AS conversions, sum(conversion_value) AS conversion_value
38
- FROM conductor_daily_experiments
39
- WHERE group_name = '#{group_name}'
40
- AND (#{alternative_filter})
41
- AND activity_date >=
42
- (SELECT max(min_date) FROM
43
- (SELECT alternative, min(activity_date) AS min_date
44
- FROM conductor_daily_experiments
45
- WHERE activity_date >= '#{Date.today - Conductor.inclusion_period}'
46
- GROUP BY alternative) AS a)
47
- GROUP BY alternative
48
- HAVING min(activity_date) <= '#{Date.today - Conductor.equalization_period}'"
49
-
50
- self.find_by_sql(sql)
51
- end
52
-
53
- end
@@ -1,14 +0,0 @@
1
- # == Schema Information
2
- #
3
- # Table name: conductor_weight_histories
4
- #
5
- # id :integer not null, primary key
6
- # group_name :string(255)
7
- # option_name :string(255)
8
- # weight :decimal(8, 2)
9
- # computed_at :datetime
10
- #
11
-
12
- class Conductor::Experiment::History < ActiveRecord::Base
13
- set_table_name "conductor_weight_histories"
14
- end
@@ -1,27 +0,0 @@
1
- # == Schema Information
2
- #
3
- # Table name: conductor_raw_experiments
4
- #
5
- # id :integer not null, primary key
6
- # identity_id :string(255)
7
- # group_name :string(255)
8
- # option_name :string(255)
9
- # conversion_value :decimal(8, 2)
10
- # created_at :datetime
11
- # updated_at :datetime
12
- #
13
-
14
- class Conductor::Experiment::Raw < ActiveRecord::Base
15
- set_table_name "conductor_raw_experiments"
16
-
17
- validates_presence_of :group_name, :alternative
18
- named_scope :since, lambda { |a_date| { :conditions => ['created_at >= ?',a_date] }}
19
-
20
- def created_date
21
- self.created_at.strftime('%Y-%m-%d')
22
- end
23
-
24
- def self.purge(days_old=30)
25
- Conductor::Experiment::Raw.delete_all("created_at <= #{days_old.days.ago}")
26
- end
27
- end
@@ -1,18 +0,0 @@
1
- # == Schema Information
2
- #
3
- # Table name: conductor_weighted_experiments
4
- #
5
- # id :integer not null, primary key
6
- # group_name :string(255)
7
- # option_name :string(255)
8
- # weight :decimal(8, 2)
9
- # created_at :datetime
10
- # updated_at :datetime
11
- #
12
-
13
- class Conductor::Experiment::Weight < ActiveRecord::Base
14
- set_table_name "conductor_weighted_experiments"
15
-
16
- named_scope :for_group, lambda { |group_name| { :conditions => ['group_name = ?',group_name] }}
17
- named_scope :with_alternative, lambda { |alternative| { :conditions => ['alternative = ?',alternative] }}
18
- end
@@ -1,29 +0,0 @@
1
- !!!
2
- %html{ :xmlns => "http://www.w3.org/1999/xhtml" }
3
- %head
4
- %meta{ :content => "text/html; charset=utf-8", "http-equiv" => "Content-Type" }
5
- %title
6
- = h(yield(:title) || "Conductor Statistic as of #{Time.now}")
7
- = javascript_include_tag 'http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js'
8
- = stylesheet_link_tag 'conductor', :media => "all"
9
-
10
- :javascript
11
- $(document).ready(function() {
12
- $('.toggle_table').click(function() {
13
- $(this).parent().find('table').toggle();
14
- });
15
- });
16
-
17
- %body
18
- #conductor_dashboard
19
- .title
20
- %h1 Conductor Statistics
21
- .generated= "Generated on #{Time.now}"
22
-
23
- .top_nav= render :file => 'dashboard/_top_nav'
24
- #weights= render :file => 'dashboard/_current_weights'
25
- #group_stats= render :file => 'dashboard/_group_stats'
26
- #dailies= render :file => 'dashboard/_daily_stats'
27
- #history= render :file => 'dashboard/_weight_history'
28
-
29
-
@@ -1,7 +0,0 @@
1
- # require all files in lib directory
2
- Dir["#{File.dirname(__FILE__)}/../lib/**/*.rb"].each { |f| require f }
3
-
4
- require 'gchart'
5
-
6
- ActionView::Base.send :include, DashboardHelper
7
-
@@ -1,43 +0,0 @@
1
- ActiveRecord::Schema.define(:version => 0) do
2
- create_table "conductor_daily_experiments", :force => true do |t|
3
- t.date "activity_date"
4
- t.string "group_name"
5
- t.string "alternative"
6
- t.decimal "conversion_value", :precision => 8, :scale => 2
7
- t.integer "views"
8
- t.integer "conversions"
9
- end
10
-
11
- add_index "conductor_daily_experiments", ["activity_date"], :name => "index_conductor_daily_experiments_on_activity_date"
12
- add_index "conductor_daily_experiments", ["group_name"], :name => "index_conductor_daily_experiments_on_group_name"
13
-
14
- create_table "conductor_raw_experiments", :force => true do |t|
15
- t.string "identity_id"
16
- t.string "group_name"
17
- t.string "alternative"
18
- t.decimal "conversion_value", :precision => 8, :scale => 2
19
- t.datetime "created_at"
20
- t.datetime "updated_at"
21
- t.string "goal"
22
- end
23
-
24
- create_table "conductor_weight_histories", :force => true do |t|
25
- t.string "group_name"
26
- t.string "alternative"
27
- t.decimal "weight", :precision => 8, :scale => 2
28
- t.datetime "computed_at"
29
- t.integer "launch_window"
30
- end
31
-
32
- add_index "conductor_weight_histories", ["computed_at", "group_name"], :name => "conductor_wh_date_and_group_ndx"
33
-
34
- create_table "conductor_weighted_experiments", :force => true do |t|
35
- t.string "group_name"
36
- t.string "alternative"
37
- t.decimal "weight", :precision => 8, :scale => 2
38
- t.datetime "created_at"
39
- t.datetime "updated_at"
40
- end
41
-
42
- add_index "conductor_weighted_experiments", ["group_name"], :name => "index_conductor_weighted_experiments_on_group_name"
43
- end
@@ -1,346 +0,0 @@
1
- require File.expand_path(File.dirname(__FILE__) + '/test_helper')
2
-
3
- class TestConductor < Test::Unit::TestCase
4
- # Wipes cache, D/B prior to doing a test run.
5
- def setup
6
- Conductor.cache.clear
7
- wipe
8
- end
9
-
10
- context "conductor" do
11
- should "assign an identity if none is specified" do
12
- assert Conductor.identity != nil
13
- end
14
-
15
- should "select one of the specified options randomly" do
16
- selected = Conductor::Experiment.pick('a_group', ["a", "b", "c"]) # => value must be unique
17
- assert ["a", "b", "c"].include? selected
18
- end
19
-
20
- should "use the cache if working" do
21
- Conductor.cache.write('testing','value')
22
- x = Conductor.cache.read('testing')
23
- assert_equal x, 'value'
24
- end
25
-
26
- should "allow for the equalization_period to be configurable" do
27
- Conductor.equalization_period = 3
28
- assert_equal(3, Conductor.equalization_period)
29
- end
30
-
31
- should "raise an error if a non-numeric value, negative or 0 value is specified for the equalization_period" do
32
- assert_raise(RuntimeError, LoadError) { Conductor.equalization_period = 'junk'}
33
- assert_raise(RuntimeError, LoadError) { Conductor.equalization_period = -1.0}
34
- assert_raise(RuntimeError, LoadError) { Conductor.equalization_period = 0}
35
- assert_nothing_raised(RuntimeError, LoadError) { Conductor.equalization_period = 3}
36
- end
37
-
38
- should "raise an error if an improper attribute is specified for @attribute_for_weighting" do
39
- assert_raise(RuntimeError, LoadError) { Conductor.attribute_for_weighting = :random}
40
- end
41
-
42
- should "almost equally select each option if no weights exist" do
43
- a = 0
44
- b = 0
45
- c = 0
46
- (1..1000).each do |x|
47
- Conductor.identity = ActiveSupport::SecureRandom.hex(16)
48
- selected_lander = Conductor::Experiment.pick('a_group', ["a", "b", "c"]) # => value must be unique
49
- case selected_lander
50
- when 'a' then
51
- a += 1
52
- when 'b' then
53
- b += 1
54
- when 'c' then
55
- c += 1
56
- end
57
- end
58
-
59
- nums = [] << a << b << c
60
- nums.sort!
61
- range = nums.last - nums.first
62
-
63
- assert (nums.first * 0.20) >= range
64
- end
65
- end
66
-
67
- context "a single site visitor" do
68
- setup do
69
- Conductor.identity = ActiveSupport::SecureRandom.hex(16)
70
- end
71
-
72
- should "always select the same alternative when using the cache" do
73
- selected = Conductor::Experiment.pick('a_group', ["a", "b", "c"]) # => value must be unique
74
- different = false
75
-
76
- (1..100).each do |x|
77
- different = true if selected != Conductor::Experiment.pick('a_group', ["a", "b", "c"])
78
- end
79
-
80
- assert !different
81
- end
82
-
83
- should "select a lander and then successfully record a conversion" do
84
- selected = Conductor::Experiment.pick('a_group', ["a", "b", "c"]) # => value must be unique
85
-
86
- Conductor::Experiment.track!
87
-
88
- experiments = Conductor::Experiment::Raw.find_all_by_identity_id(Conductor.identity)
89
- assert_equal 1, experiments.count
90
- assert_equal 1, experiments.first.conversion_value
91
- end
92
-
93
- should "select a lander and then successfully record custom conversion value" do
94
- selected = Conductor::Experiment.pick('a_group', ["a", "b", "c"]) # => value must be unique
95
-
96
- Conductor::Experiment.track!({:value => 12.34})
97
-
98
- experiments = Conductor::Experiment::Raw.find_all_by_identity_id(Conductor.identity)
99
- assert_equal 1, experiments.count
100
- assert_equal 12.34, experiments.first.conversion_value
101
- end
102
-
103
- should "record three different experiments with two goals but a single conversion for all goals for the same identity" do
104
- first = Conductor::Experiment.pick('a_group', ["a", "b", "c"], {:goal => 'goal_1'}) # => value must be unique
105
- second = Conductor::Experiment.pick('b_group', ["1", "2", "3"], {:goal => 'goal_2'}) # => value must be unique
106
- third = Conductor::Experiment.pick('c_group', ["zz", "xx", "yy"], {:goal => 'goal_1'}) # => value must be unique
107
-
108
- Conductor::Experiment.track!
109
-
110
- experiments = Conductor::Experiment::Raw.find_all_by_identity_id(Conductor.identity)
111
- assert_equal 3, experiments.count
112
- assert_equal 2, experiments.count {|x| x.goal == 'goal_1'}
113
- assert_equal 3, experiments.sum_it(:conversion_value)
114
- end
115
-
116
- should "record three different experiments with two goals but only track a conversion for goal_1" do
117
- first = Conductor::Experiment.pick('a_group', ["a", "b", "c"], {:goal => 'goal_1'}) # => value must be unique
118
- second = Conductor::Experiment.pick('b_group', ["1", "2", "3"], {:goal => 'goal_2'}) # => value must be unique
119
- third = Conductor::Experiment.pick('c_group', ["zz", "xx", "yy"], {:goal => 'goal_1'}) # => value must be unique
120
-
121
- Conductor::Experiment.track!({:goal => 'goal_1'})
122
-
123
- experiments = Conductor::Experiment::Raw.find_all_by_identity_id(Conductor.identity)
124
- assert_equal 3, experiments.count
125
- assert_equal 2, experiments.count {|x| x.goal == 'goal_1'}
126
- assert_equal 2, experiments.sum_it(:conversion_value)
127
- end
128
- end
129
-
130
- context "conductor" do
131
- setup do
132
- seed_raw_data(100)
133
- Conductor::RollUp.process
134
- Conductor.identity = ActiveSupport::SecureRandom.hex(16)
135
- end
136
-
137
- should "correctly RollUp daily data" do
138
- assert Conductor::Experiment::Daily.count > 2
139
- assert Conductor::Experiment::Daily.all.detect {|x| x.conversions > 0}
140
- assert Conductor::Experiment::Daily.all.detect {|x| x.views > 0}
141
- assert Conductor::Experiment::Daily.all.detect {|x| x.conversion_value > 0}
142
- end
143
-
144
- should "correctly populate weighting table when selecting a value" do
145
- selected = Conductor::Experiment.pick('a_group', ["a", "b", "c"])
146
- assert_equal 3, Conductor::Experiment::Weight.count
147
- end
148
-
149
- should "pull weights from the cache" do
150
- Conductor::Experiment.pick('a_group', ["a", "b", "c"])
151
-
152
- (1..100).each do |x|
153
- Conductor.identity = ActiveSupport::SecureRandom.hex(16)
154
- Conductor::Experiment.pick('a_group', ["a", "b", "c"])
155
- end
156
-
157
- # => if this works the history table should have only been updated one time not 101 so there should
158
- # => be three records (one for a, b and c)
159
- assert_equal 3, Conductor::Experiment::History.count
160
- end
161
-
162
- should "pull weights from the cache and then recreate weights when the alternative list changes" do
163
- Conductor::Experiment.pick('a_group', ["a", "b", "c"])
164
-
165
- (1..100).each do |x|
166
- Conductor.identity = ActiveSupport::SecureRandom.hex(16)
167
- Conductor::Experiment.pick('a_group', ["a", "b", "c"])
168
- end
169
-
170
- Conductor.identity = ActiveSupport::SecureRandom.hex(16)
171
- Conductor::Experiment.pick('a_group', ["a", "c"])
172
-
173
- # => if this works the history table should have only been updated one time not 101 so there should
174
- # => be FIVE records (one for a, b and c and then one for a and c)
175
- assert_equal 5, Conductor::Experiment::History.count
176
- end
177
- end
178
-
179
- context "conductor" do
180
- setup do
181
- wipe
182
- seed_raw_data(100, 7)
183
- Conductor::RollUp.process
184
- end
185
-
186
- should "populate the weighting table with equal weights if all new options are launched" do
187
- # hit after rollup to populare weight table
188
- Conductor.identity = ActiveSupport::SecureRandom.hex(16)
189
- Conductor.equalization_period = 7
190
- weights = Conductor::Experiment.weights('a_group', ["a", "b", "c"])
191
-
192
- # each weight will be equal to 0.18
193
- assert_equal 7, Conductor.equalization_period
194
- assert_equal weights['a'], weights['b']
195
- assert_equal weights['c'], weights['b']
196
- end
197
- end
198
-
199
- context "conductor" do
200
- setup do
201
- seed_raw_data(100, 14);
202
-
203
- # rollup
204
- Conductor::RollUp.process
205
-
206
- # hit after rollup to populare weight table
207
- Conductor.identity = ActiveSupport::SecureRandom.hex(16)
208
- selected = Conductor::Experiment.pick('a_group', ["a", "b", "c"])
209
- end
210
-
211
- should "populate the weighting table with different weights" do
212
- # if this DOES NOT work then each weight will be equal to 0.18
213
- assert_not_equal 0.54, Conductor::Experiment::Weight.all.sum_it(:weight).to_f
214
- end
215
-
216
- should "record the new weights in the weight history table in database" do
217
- assert Conductor::Experiment::History.count > 1
218
- end
219
-
220
- should "return a weight 1.25 times higher than the highest weight for a newly launched and non-recorded alernative" do
221
- # get the highest weight
222
- max_weight = Conductor::Experiment::Weight.maximum(:weight)
223
-
224
- # pick something
225
- weights = Conductor::Experiment.weights('a_group', ["a", "b", "c", "f"]) # => value must be unique
226
-
227
- assert_equal weights['f'], (max_weight * 1.25)
228
- end
229
- end
230
-
231
- context "conductor" do
232
- should "correctly record the launch window in the weight histories table" do
233
- seed_raw_data(10, 6)
234
-
235
- # rollup
236
- Conductor::RollUp.process
237
-
238
- # hit after rollup to populare weight table
239
- Conductor.identity = ActiveSupport::SecureRandom.hex(16)
240
- selected = Conductor::Experiment.pick('a_group', ["a", "b", "c"])
241
-
242
- # make sure that launch_window values can be detected
243
- assert_not_nil Conductor::Experiment::History.find(:all, :conditions => 'launch_window > 0')
244
- end
245
- end
246
-
247
- context "conductor" do
248
- setup do
249
- seed_raw_data(500, 30)
250
-
251
- # rollup
252
- Conductor::RollUp.process
253
- end
254
-
255
- should "correctly calculate weights even if there are no conversions" do
256
- Conductor::Experiment::Daily.update_all('conversion_value = 0.00, conversions = 0')
257
- Conductor.identity = ActiveSupport::SecureRandom.hex(16)
258
-
259
- assert_nil Conductor::Experiment::Daily.all.detect {|x| x.conversions > 0 || x.conversion_value > 0}
260
- assert_equal 3, Conductor::Experiment.weights('a_group', ["a", "b", "c"]).values.sum
261
- end
262
-
263
- should "correctly calculate weights even if an alternative has no conversions" do
264
- Conductor::Experiment::Daily.update_all('conversion_value = 0.00, conversions = 0', "alternative = 'a'")
265
- Conductor.identity = ActiveSupport::SecureRandom.hex(16)
266
-
267
- assert_nil Conductor::Experiment::Daily.find_all_by_alternative('a').detect {|x| x.conversions > 0 || x.conversion_value > 0}
268
- assert_equal 0, Conductor::Experiment.weights('a_group', ["a", "b", "c"])['a']
269
- end
270
-
271
- should "allow for the number of conversions to be used for weighting instead of conversion_value" do
272
- Conductor.identity = ActiveSupport::SecureRandom.hex(16)
273
- Conductor::Experiment.pick('a_group', ["a", "b", "c"])
274
- weights_cv = Conductor::Experiment::Weight.all.map(&:weight).sort
275
-
276
- Conductor.identity = ActiveSupport::SecureRandom.hex(16)
277
- Conductor.attribute_for_weighting = :conversions
278
- Conductor::Experiment.pick('a_group', ["a", "b", "c"])
279
- weights_c = Conductor::Experiment::Weight.all.map(&:weight).sort
280
-
281
- # since one is using conversion_value and the other is using conversions, they two weight arrays should be different
282
- assert_equal :conversions, Conductor.attribute_for_weighting
283
- assert_not_equal weights_cv, weights_c
284
- end
285
- end
286
-
287
- context "conductor" do
288
- setup do
289
- seed_raw_data(500, 30)
290
-
291
- # rollup
292
- Conductor::RollUp.process
293
- end
294
-
295
- should "weight everything equally if the minimum number of conversions per group has not been hit" do
296
- Conductor::Experiment::Daily.update_all('conversion_value = 0.00, conversions = 0')
297
- Conductor::Experiment::Daily.update_all('conversion_value = 1.00, conversions = 1', '', :limit => 10)
298
-
299
- Conductor.identity = ActiveSupport::SecureRandom.hex(16)
300
- Conductor.attribute_for_weighting = :conversions
301
- Conductor.minimum_conversions_per_group = 15
302
-
303
- weights = Conductor::Experiment.weights('a_group', ["a", "b", "c"])
304
- assert_equal weights['a'], weights['b']
305
- assert_equal weights['c'], weights['b']
306
- end
307
-
308
- should "weight everything correctly if the minimum number of conversions per group HAS been hit" do
309
- Conductor::Experiment::Daily.update_all('conversion_value = 0.00, conversions = 0')
310
- Conductor::Experiment::Daily.all.each_with_index {|x,ndx|
311
- if ndx < 10
312
- x.update_attributes(:conversion_value => rand(100), :conversions => rand(20))
313
- end
314
- }
315
-
316
- Conductor.identity = ActiveSupport::SecureRandom.hex(16)
317
- Conductor.attribute_for_weighting = :conversions
318
-
319
- weights = Conductor::Experiment.weights('a_group', ["a", "b", "c"])
320
- assert_not_equal weights['a'], weights['b']
321
- assert_not_equal weights['c'], weights['b']
322
- end
323
- end
324
-
325
-
326
-
327
- private
328
-
329
- def wipe
330
- Conductor::Experiment::Daily.delete_all
331
- Conductor::Experiment::Raw.delete_all
332
- Conductor::Experiment::Weight.delete_all
333
- Conductor::Experiment::History.delete_all
334
- end
335
-
336
- def seed_raw_data(num, days_ago=14)
337
- # seed the raw data
338
- (1..num).each do |x|
339
- Conductor.identity = ActiveSupport::SecureRandom.hex(16)
340
-
341
- options = {:created_at => rand(days_ago).days.ago}
342
- options.merge!({:conversion_value => rand(100)}) if rand() < 0.20 # => convert 20% of traffic
343
- selected_lander = Conductor::Experiment.pick('a_group', ["a", "b", "c"], options) # => value must be unique
344
- end
345
- end
346
- end