conductor 0.8.3 → 0.9.3
Sign up to get free protection for your applications and to get access to all the features.
- data/{LICENSE → MIT-LICENSE} +1 -1
- data/README.rdoc +7 -0
- data/Rakefile +25 -45
- data/app/assets/javascripts/conductor/application.js +15 -0
- data/app/assets/javascripts/conductor/dashboard.js +2 -0
- data/app/assets/stylesheets/conductor/application.css +13 -0
- data/{generators/conductor/templates/conductor.css → app/assets/stylesheets/conductor/dashboard.css} +0 -0
- data/app/controllers/conductor/application_controller.rb +4 -0
- data/app/controllers/conductor/dashboard_controller.rb +11 -0
- data/app/helpers/conductor/application_helper.rb +4 -0
- data/{lib/conductor/rails/helpers → app/helpers/conductor}/dashboard_helper.rb +3 -1
- data/app/models/conductor/experiment/daily.rb +47 -0
- data/app/models/conductor/experiment/history.rb +9 -0
- data/app/models/conductor/experiment/raw.rb +20 -0
- data/app/models/conductor/experiment/weight.rb +11 -0
- data/{lib/conductor/rails/views → app/views/conductor}/dashboard/_current_weights.html.haml +0 -0
- data/{lib/conductor/rails/views → app/views/conductor}/dashboard/_daily_stats.html.haml +0 -0
- data/{lib/conductor/rails/views → app/views/conductor}/dashboard/_group_stats.html.haml +0 -0
- data/{lib/conductor/rails/views → app/views/conductor}/dashboard/_top_nav.html.haml +0 -0
- data/{lib/conductor/rails/views → app/views/conductor}/dashboard/_weight_history.html.haml +0 -0
- data/app/views/conductor/dashboard/index.html.haml +12 -0
- data/app/views/layouts/conductor/application.html.erb +14 -0
- data/config/routes.rb +4 -0
- data/db/migrate/20121010131323_create_conductor_daily_experiments.rb +16 -0
- data/db/migrate/20121010173406_create_conductor_experiment_weights.rb +13 -0
- data/db/migrate/20121010174835_create_conductor_raw_experiments.rb +13 -0
- data/db/migrate/20121010174950_create_conductor_weight_histories.rb +14 -0
- data/lib/conductor.rb +33 -57
- data/lib/conductor/core_ext.rb +18 -0
- data/lib/conductor/engine.rb +5 -0
- data/lib/conductor/experiment.rb +61 -60
- data/lib/conductor/roll_up.rb +2 -2
- data/lib/conductor/version.rb +3 -0
- data/lib/conductor/weights.rb +9 -8
- data/lib/tasks/conductor_tasks.rake +9 -0
- data/test/core_ext_test.rb +13 -0
- data/test/fixtures/conductor/conductor_daily_experiments.yml +17 -0
- data/test/functional/conductor/dashboard_controller_test.rb +11 -0
- data/test/integration/conductor/dashboard_test.rb +7 -0
- data/test/integration/navigation_test.rb +10 -0
- data/test/test_helper.rb +9 -23
- data/test/unit/conductor/conductor_daily_experiment_test.rb +9 -0
- data/test/unit/helpers/conductor/dashboard_helper_test.rb +6 -0
- metadata +240 -91
- data/VERSION +0 -1
- data/generators/conductor/conductor_generator.rb +0 -27
- data/generators/conductor/templates/conductor.rake +0 -6
- data/generators/conductor/templates/migration.rb +0 -54
- data/init.rb +0 -2
- data/lib/conductor/rails/controllers/dashboard.rb +0 -16
- data/lib/conductor/rails/models/daily.rb +0 -53
- data/lib/conductor/rails/models/history.rb +0 -14
- data/lib/conductor/rails/models/raw.rb +0 -27
- data/lib/conductor/rails/models/weight.rb +0 -18
- data/lib/conductor/rails/views/dashboard/index.html.haml +0 -29
- data/rails/init.rb +0 -7
- data/test/db/schema.rb +0 -43
- 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,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,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
|
-
|
data/rails/init.rb
DELETED
data/test/db/schema.rb
DELETED
@@ -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
|
data/test/test_conductor.rb
DELETED
@@ -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
|