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