conductor 0.8.3 → 0.9.3

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.
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