a_b 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +18 -0
- data/README.markdown +4 -0
- data/Rakefile +74 -0
- data/config.ru +3 -0
- data/config/database.example.yml +6 -0
- data/config/externals.yml +6 -0
- data/config/mail.example.yml +32 -0
- data/db/migrate/001_a_b_tests.rb +14 -0
- data/db/migrate/002_a_b_variants.rb +19 -0
- data/db/migrate/003_a_b_users.rb +25 -0
- data/db/migrate/004_a_b_tokens.rb +12 -0
- data/features/example.feature +10 -0
- data/features/support/env.rb +35 -0
- data/features/support/rspec.rb +11 -0
- data/features/support/webrat.rb +3 -0
- data/gemspec.rb +41 -0
- data/lib/a_b.rb +31 -0
- data/lib/a_b/boot.rb +30 -0
- data/lib/a_b/controller/api.rb +32 -0
- data/lib/a_b/controller/application.rb +6 -0
- data/lib/a_b/controller/index.rb +16 -0
- data/lib/a_b/controller/sessions.rb +28 -0
- data/lib/a_b/controller/tests.rb +37 -0
- data/lib/a_b/controller/variants.rb +14 -0
- data/lib/a_b/helper/api.rb +8 -0
- data/lib/a_b/helper/application.rb +56 -0
- data/lib/a_b/helper/index.rb +12 -0
- data/lib/a_b/model/a_b_test.rb +65 -0
- data/lib/a_b/model/a_b_variant.rb +131 -0
- data/lib/a_b/model/token.rb +22 -0
- data/lib/a_b/model/user.rb +5 -0
- data/lib/a_b/model/user_session.rb +4 -0
- data/lib/a_b/view/index.haml +129 -0
- data/lib/a_b/view/index.sass +55 -0
- data/lib/a_b/view/layout.haml +15 -0
- data/lib/a_b/view/layout.sass +34 -0
- data/lib/a_b/view/log_in.haml +15 -0
- data/lib/a_b/view/log_in.sass +11 -0
- data/public/css/blueprint/ie.css +35 -0
- data/public/css/blueprint/print.css +29 -0
- data/public/css/blueprint/screen.css +257 -0
- data/public/js/index.js +126 -0
- data/public/js/jquery.js +19 -0
- data/public/js/visit.js +59 -0
- data/script/console +2 -0
- data/script/env.rb +30 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +16 -0
- data/vendor/authlogic/CHANGELOG.rdoc +345 -0
- data/vendor/authlogic/LICENSE +20 -0
- data/vendor/authlogic/README.rdoc +246 -0
- data/vendor/authlogic/Rakefile +42 -0
- data/vendor/authlogic/VERSION.yml +5 -0
- data/vendor/authlogic/authlogic.gemspec +217 -0
- data/vendor/authlogic/generators/session/session_generator.rb +9 -0
- data/vendor/authlogic/generators/session/templates/session.rb +2 -0
- data/vendor/authlogic/init.rb +1 -0
- data/vendor/authlogic/lib/authlogic.rb +57 -0
- data/vendor/authlogic/lib/authlogic/acts_as_authentic/base.rb +107 -0
- data/vendor/authlogic/lib/authlogic/acts_as_authentic/email.rb +110 -0
- data/vendor/authlogic/lib/authlogic/acts_as_authentic/logged_in_status.rb +60 -0
- data/vendor/authlogic/lib/authlogic/acts_as_authentic/login.rb +141 -0
- data/vendor/authlogic/lib/authlogic/acts_as_authentic/magic_columns.rb +24 -0
- data/vendor/authlogic/lib/authlogic/acts_as_authentic/password.rb +344 -0
- data/vendor/authlogic/lib/authlogic/acts_as_authentic/perishable_token.rb +105 -0
- data/vendor/authlogic/lib/authlogic/acts_as_authentic/persistence_token.rb +68 -0
- data/vendor/authlogic/lib/authlogic/acts_as_authentic/restful_authentication.rb +61 -0
- data/vendor/authlogic/lib/authlogic/acts_as_authentic/session_maintenance.rb +139 -0
- data/vendor/authlogic/lib/authlogic/acts_as_authentic/single_access_token.rb +65 -0
- data/vendor/authlogic/lib/authlogic/acts_as_authentic/validations_scope.rb +32 -0
- data/vendor/authlogic/lib/authlogic/authenticates_many/association.rb +42 -0
- data/vendor/authlogic/lib/authlogic/authenticates_many/base.rb +55 -0
- data/vendor/authlogic/lib/authlogic/controller_adapters/abstract_adapter.rb +67 -0
- data/vendor/authlogic/lib/authlogic/controller_adapters/merb_adapter.rb +30 -0
- data/vendor/authlogic/lib/authlogic/controller_adapters/rails_adapter.rb +48 -0
- data/vendor/authlogic/lib/authlogic/controller_adapters/sinatra_adapter.rb +61 -0
- data/vendor/authlogic/lib/authlogic/crypto_providers/aes256.rb +43 -0
- data/vendor/authlogic/lib/authlogic/crypto_providers/bcrypt.rb +90 -0
- data/vendor/authlogic/lib/authlogic/crypto_providers/md5.rb +34 -0
- data/vendor/authlogic/lib/authlogic/crypto_providers/sha1.rb +35 -0
- data/vendor/authlogic/lib/authlogic/crypto_providers/sha256.rb +50 -0
- data/vendor/authlogic/lib/authlogic/crypto_providers/sha512.rb +50 -0
- data/vendor/authlogic/lib/authlogic/crypto_providers/wordpress.rb +43 -0
- data/vendor/authlogic/lib/authlogic/i18n.rb +83 -0
- data/vendor/authlogic/lib/authlogic/i18n/translator.rb +15 -0
- data/vendor/authlogic/lib/authlogic/random.rb +33 -0
- data/vendor/authlogic/lib/authlogic/regex.rb +25 -0
- data/vendor/authlogic/lib/authlogic/session/activation.rb +58 -0
- data/vendor/authlogic/lib/authlogic/session/active_record_trickery.rb +61 -0
- data/vendor/authlogic/lib/authlogic/session/base.rb +37 -0
- data/vendor/authlogic/lib/authlogic/session/brute_force_protection.rb +96 -0
- data/vendor/authlogic/lib/authlogic/session/callbacks.rb +88 -0
- data/vendor/authlogic/lib/authlogic/session/cookies.rb +130 -0
- data/vendor/authlogic/lib/authlogic/session/existence.rb +93 -0
- data/vendor/authlogic/lib/authlogic/session/foundation.rb +63 -0
- data/vendor/authlogic/lib/authlogic/session/http_auth.rb +58 -0
- data/vendor/authlogic/lib/authlogic/session/id.rb +41 -0
- data/vendor/authlogic/lib/authlogic/session/klass.rb +78 -0
- data/vendor/authlogic/lib/authlogic/session/magic_columns.rb +95 -0
- data/vendor/authlogic/lib/authlogic/session/magic_states.rb +59 -0
- data/vendor/authlogic/lib/authlogic/session/params.rb +101 -0
- data/vendor/authlogic/lib/authlogic/session/password.rb +240 -0
- data/vendor/authlogic/lib/authlogic/session/perishable_token.rb +18 -0
- data/vendor/authlogic/lib/authlogic/session/persistence.rb +70 -0
- data/vendor/authlogic/lib/authlogic/session/priority_record.rb +34 -0
- data/vendor/authlogic/lib/authlogic/session/scopes.rb +101 -0
- data/vendor/authlogic/lib/authlogic/session/session.rb +62 -0
- data/vendor/authlogic/lib/authlogic/session/timeout.rb +82 -0
- data/vendor/authlogic/lib/authlogic/session/unauthorized_record.rb +50 -0
- data/vendor/authlogic/lib/authlogic/session/validation.rb +82 -0
- data/vendor/authlogic/lib/authlogic/test_case.rb +120 -0
- data/vendor/authlogic/lib/authlogic/test_case/mock_controller.rb +45 -0
- data/vendor/authlogic/lib/authlogic/test_case/mock_cookie_jar.rb +14 -0
- data/vendor/authlogic/lib/authlogic/test_case/mock_logger.rb +10 -0
- data/vendor/authlogic/lib/authlogic/test_case/mock_request.rb +19 -0
- data/vendor/authlogic/lib/authlogic/test_case/rails_request_adapter.rb +30 -0
- data/vendor/authlogic/rails/init.rb +1 -0
- data/vendor/authlogic/shoulda_macros/authlogic.rb +69 -0
- data/vendor/authlogic/test/acts_as_authentic_test/base_test.rb +18 -0
- data/vendor/authlogic/test/acts_as_authentic_test/email_test.rb +97 -0
- data/vendor/authlogic/test/acts_as_authentic_test/logged_in_status_test.rb +36 -0
- data/vendor/authlogic/test/acts_as_authentic_test/login_test.rb +109 -0
- data/vendor/authlogic/test/acts_as_authentic_test/magic_columns_test.rb +27 -0
- data/vendor/authlogic/test/acts_as_authentic_test/password_test.rb +236 -0
- data/vendor/authlogic/test/acts_as_authentic_test/perishable_token_test.rb +90 -0
- data/vendor/authlogic/test/acts_as_authentic_test/persistence_token_test.rb +55 -0
- data/vendor/authlogic/test/acts_as_authentic_test/restful_authentication_test.rb +40 -0
- data/vendor/authlogic/test/acts_as_authentic_test/session_maintenance_test.rb +84 -0
- data/vendor/authlogic/test/acts_as_authentic_test/single_access_test.rb +44 -0
- data/vendor/authlogic/test/authenticates_many_test.rb +16 -0
- data/vendor/authlogic/test/crypto_provider_test/aes256_test.rb +14 -0
- data/vendor/authlogic/test/crypto_provider_test/bcrypt_test.rb +14 -0
- data/vendor/authlogic/test/crypto_provider_test/sha1_test.rb +23 -0
- data/vendor/authlogic/test/crypto_provider_test/sha256_test.rb +14 -0
- data/vendor/authlogic/test/crypto_provider_test/sha512_test.rb +14 -0
- data/vendor/authlogic/test/fixtures/companies.yml +5 -0
- data/vendor/authlogic/test/fixtures/employees.yml +17 -0
- data/vendor/authlogic/test/fixtures/projects.yml +3 -0
- data/vendor/authlogic/test/fixtures/users.yml +24 -0
- data/vendor/authlogic/test/i18n_test.rb +33 -0
- data/vendor/authlogic/test/libs/affiliate.rb +7 -0
- data/vendor/authlogic/test/libs/company.rb +6 -0
- data/vendor/authlogic/test/libs/employee.rb +7 -0
- data/vendor/authlogic/test/libs/employee_session.rb +2 -0
- data/vendor/authlogic/test/libs/ldaper.rb +3 -0
- data/vendor/authlogic/test/libs/ordered_hash.rb +9 -0
- data/vendor/authlogic/test/libs/project.rb +3 -0
- data/vendor/authlogic/test/libs/user.rb +5 -0
- data/vendor/authlogic/test/libs/user_session.rb +6 -0
- data/vendor/authlogic/test/random_test.rb +49 -0
- data/vendor/authlogic/test/session_test/activation_test.rb +43 -0
- data/vendor/authlogic/test/session_test/active_record_trickery_test.rb +36 -0
- data/vendor/authlogic/test/session_test/brute_force_protection_test.rb +101 -0
- data/vendor/authlogic/test/session_test/callbacks_test.rb +6 -0
- data/vendor/authlogic/test/session_test/cookies_test.rb +112 -0
- data/vendor/authlogic/test/session_test/credentials_test.rb +0 -0
- data/vendor/authlogic/test/session_test/existence_test.rb +64 -0
- data/vendor/authlogic/test/session_test/http_auth_test.rb +28 -0
- data/vendor/authlogic/test/session_test/id_test.rb +17 -0
- data/vendor/authlogic/test/session_test/klass_test.rb +40 -0
- data/vendor/authlogic/test/session_test/magic_columns_test.rb +62 -0
- data/vendor/authlogic/test/session_test/magic_states_test.rb +60 -0
- data/vendor/authlogic/test/session_test/params_test.rb +53 -0
- data/vendor/authlogic/test/session_test/password_test.rb +106 -0
- data/vendor/authlogic/test/session_test/perishability_test.rb +15 -0
- data/vendor/authlogic/test/session_test/persistence_test.rb +21 -0
- data/vendor/authlogic/test/session_test/scopes_test.rb +60 -0
- data/vendor/authlogic/test/session_test/session_test.rb +59 -0
- data/vendor/authlogic/test/session_test/timeout_test.rb +52 -0
- data/vendor/authlogic/test/session_test/unauthorized_record_test.rb +13 -0
- data/vendor/authlogic/test/session_test/validation_test.rb +23 -0
- data/vendor/authlogic/test/test_helper.rb +182 -0
- metadata +325 -0
@@ -0,0 +1,28 @@
|
|
1
|
+
Application.class_eval do
|
2
|
+
|
3
|
+
get '/sessions/new' do
|
4
|
+
@css = %w(log_in)
|
5
|
+
@error = flash[:error]
|
6
|
+
haml :log_in, :layout => :layout
|
7
|
+
end
|
8
|
+
|
9
|
+
post '/sessions/create' do
|
10
|
+
@user_session = UserSession.new(params[:session])
|
11
|
+
if @user_session.save
|
12
|
+
redirect '/'
|
13
|
+
else
|
14
|
+
flash[:error] = "Log in failed."
|
15
|
+
redirect '/sessions/new'
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
get '/sessions/destroy' do
|
20
|
+
current_user_session.destroy
|
21
|
+
redirect '/sessions/new'
|
22
|
+
end
|
23
|
+
|
24
|
+
get '/css/log_in.css' do
|
25
|
+
headers 'Content-Type' => 'text/css; charset=utf-8'
|
26
|
+
sass :log_in
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
Application.class_eval do
|
2
|
+
|
3
|
+
get '/tests/:id/destroy' do
|
4
|
+
restrict
|
5
|
+
@test = ABTest.find params[:id]
|
6
|
+
if @test
|
7
|
+
@test.destroy
|
8
|
+
flash[:success] = 'Test deleted successfully.'
|
9
|
+
else
|
10
|
+
flash[:error] = 'Could not delete test.'
|
11
|
+
end
|
12
|
+
redirect '/'
|
13
|
+
end
|
14
|
+
|
15
|
+
post '/tests/:id/update' do
|
16
|
+
restrict
|
17
|
+
@test = ABTest.find params[:id]
|
18
|
+
if @test
|
19
|
+
@test.update_attributes params[:test]
|
20
|
+
flash[:success] = 'Test updated successfully.'
|
21
|
+
else
|
22
|
+
flash[:error] = 'Could not update test.'
|
23
|
+
end
|
24
|
+
redirect '/'
|
25
|
+
end
|
26
|
+
|
27
|
+
post '/tests/create' do
|
28
|
+
restrict
|
29
|
+
@test = ABTest.create params[:test]
|
30
|
+
if @test.id
|
31
|
+
flash[:success] = 'Test created successfully.'
|
32
|
+
else
|
33
|
+
flash[:error] = 'Could not create test.'
|
34
|
+
end
|
35
|
+
redirect '/'
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
Application.class_eval do
|
2
|
+
|
3
|
+
get '/variants/:id/destroy' do
|
4
|
+
restricts
|
5
|
+
@variant = ABVariant.find params[:id]
|
6
|
+
if @variant
|
7
|
+
@variant.destroy
|
8
|
+
flash[:success] = 'Variant deleted successfully.'
|
9
|
+
else
|
10
|
+
flash[:error] = 'Could not delete variant.'
|
11
|
+
end
|
12
|
+
redirect '/'
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
Application.class_eval do
|
2
|
+
helpers do
|
3
|
+
|
4
|
+
# Assets
|
5
|
+
|
6
|
+
def css(name, media='screen, projection')
|
7
|
+
options = {
|
8
|
+
:href => "/css/#{name}.css",
|
9
|
+
:media => "#{media}",
|
10
|
+
:rel => "stylesheet",
|
11
|
+
:type => "text/css"
|
12
|
+
}
|
13
|
+
haml "%link#{options.inspect}", :layout => false
|
14
|
+
end
|
15
|
+
|
16
|
+
def javascripts(&block)
|
17
|
+
(@js || []).each do |path|
|
18
|
+
block.call path
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def js(name)
|
23
|
+
options = {
|
24
|
+
:type => "text/javascript",
|
25
|
+
:src => "/js/#{name}.js"
|
26
|
+
}
|
27
|
+
haml "%script#{options.inspect}", :layout => false
|
28
|
+
end
|
29
|
+
|
30
|
+
def partial(name, options={})
|
31
|
+
haml name, options.merge(:layout => false)
|
32
|
+
end
|
33
|
+
|
34
|
+
def stylesheets(&block)
|
35
|
+
(@css || []).each do |path|
|
36
|
+
block.call path
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Authentication
|
41
|
+
|
42
|
+
def current_user_session
|
43
|
+
return @current_user_session if defined?(@current_user_session)
|
44
|
+
@current_user_session = UserSession.find
|
45
|
+
end
|
46
|
+
|
47
|
+
def current_user
|
48
|
+
return @current_user if defined?(@current_user)
|
49
|
+
@current_user = current_user_session && current_user_session.record
|
50
|
+
end
|
51
|
+
|
52
|
+
def restrict
|
53
|
+
redirect '/sessions/new' unless current_user
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
Application.class_eval do
|
2
|
+
helpers do
|
3
|
+
|
4
|
+
def table_class(test, index)
|
5
|
+
(index + 1) % 3 == 0 || @tests.last == test ? 'last' : nil
|
6
|
+
end
|
7
|
+
|
8
|
+
def table_json(test)
|
9
|
+
{ :name => test.name, :ticket_url => test.ticket_url, :variant_names => test.variant_names }.to_json
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
class ABTest < ActiveRecord::Base
|
2
|
+
|
3
|
+
set_table_name :a_b_tests
|
4
|
+
has_many :variants, :class_name => 'ABVariant', :foreign_key => 'a_b_test_id', :dependent => :delete_all
|
5
|
+
|
6
|
+
validates_uniqueness_of :name
|
7
|
+
|
8
|
+
after_save :destroy_variants
|
9
|
+
after_save :create_variants
|
10
|
+
after_save :set_control
|
11
|
+
|
12
|
+
def control
|
13
|
+
self.variants.find_by_control true
|
14
|
+
end
|
15
|
+
|
16
|
+
def sorted_variants
|
17
|
+
self.variants.find(:all, :order => 'control desc, conversions / visitors desc, visitors desc')
|
18
|
+
end
|
19
|
+
|
20
|
+
def variant_names
|
21
|
+
names = self.variants.collect &:name
|
22
|
+
names.delete self.control.name
|
23
|
+
names.unshift self.control.name
|
24
|
+
end
|
25
|
+
|
26
|
+
def variants=(names)
|
27
|
+
names = names.gsub(/[^,\w]/, '').split(',').uniq
|
28
|
+
current_names = variants.collect(&:name).uniq
|
29
|
+
@control = names.first
|
30
|
+
@new_variant_names = names - current_names
|
31
|
+
@removed_variant_names = current_names - names
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def create_variants
|
37
|
+
return unless @new_variant_names
|
38
|
+
@new_variant_names.each do |name|
|
39
|
+
self.variants.create(:name => name)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def destroy_variants
|
44
|
+
return unless @removed_variant_names
|
45
|
+
remove = self.variants.find(:all, :conditions => { :name => @removed_variant_names })
|
46
|
+
remove.each do |variant|
|
47
|
+
variant.destroy
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def set_control
|
52
|
+
if @control
|
53
|
+
if self.control
|
54
|
+
self.control.update_attribute(:control, false)
|
55
|
+
end
|
56
|
+
if name = self.variants.find_by_name(@control)
|
57
|
+
name.update_attribute(:control, true)
|
58
|
+
elsif !self.variants.empty?
|
59
|
+
self.variants.first.update_attribute(:control, true)
|
60
|
+
end
|
61
|
+
elsif !self.control && !self.variants.empty?
|
62
|
+
self.variants.first.update_attribute(:control, true)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
class ABVariant < ActiveRecord::Base
|
2
|
+
|
3
|
+
set_table_name :a_b_variants
|
4
|
+
belongs_to :test, :class_name => 'ABTest', :foreign_key => 'a_b_test_id'
|
5
|
+
|
6
|
+
after_destroy :save_test
|
7
|
+
|
8
|
+
validates_uniqueness_of :name
|
9
|
+
|
10
|
+
def confidence
|
11
|
+
if compute_confidence?
|
12
|
+
cumulative_normal_distribution(z_score(self.test.control))
|
13
|
+
else
|
14
|
+
'n/a'
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def confidence_ok?
|
19
|
+
cumulative_normal_distribution(z_score(self.test.control)) >= 0.95
|
20
|
+
end
|
21
|
+
|
22
|
+
def conversion_rate
|
23
|
+
if conversions > 0
|
24
|
+
1.0 * conversions / visitors
|
25
|
+
else
|
26
|
+
0.0
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def conversion_rate_ok?
|
31
|
+
conversion_rate > self.test.control.conversion_rate
|
32
|
+
end
|
33
|
+
|
34
|
+
def pretty_confidence
|
35
|
+
pretty confidence
|
36
|
+
end
|
37
|
+
|
38
|
+
def pretty_conversion_rate
|
39
|
+
pretty conversion_rate
|
40
|
+
end
|
41
|
+
|
42
|
+
def suggested_visitors
|
43
|
+
size = sample_size(self.test.control)
|
44
|
+
if conversion_rate == 0 || size < 100
|
45
|
+
100
|
46
|
+
else
|
47
|
+
commafy size
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def suggested_visitors_ok?
|
52
|
+
visitors > sample_size(self.test.control)
|
53
|
+
end
|
54
|
+
|
55
|
+
def suggested_visitors_with_commas
|
56
|
+
commafy suggested_visitors
|
57
|
+
end
|
58
|
+
|
59
|
+
def visitors_with_commas
|
60
|
+
commafy visitors
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def commafy(num)
|
66
|
+
num.to_s.gsub(/(\d)(?=\d{3}+(?:\.|$))(\d{3}\..*)?/,'\1,\2')
|
67
|
+
end
|
68
|
+
|
69
|
+
def cumulative_normal_distribution(z)
|
70
|
+
b1 = 0.319381530
|
71
|
+
b2 = -0.356563782
|
72
|
+
b3 = 1.781477937
|
73
|
+
b4 = -1.821255978
|
74
|
+
b5 = 1.330274429
|
75
|
+
p = 0.2316419
|
76
|
+
c = 0.39894228
|
77
|
+
|
78
|
+
if z >= 0.0
|
79
|
+
t = 1.0 / (1.0 + p * z)
|
80
|
+
(1.0 - c * Math.exp(-z * z / 2.0) * t * (t * (t * (t * (t * b5 + b4) + b3) + b2) + b1))
|
81
|
+
else
|
82
|
+
t = 1.0 / (1.0 - p * z)
|
83
|
+
(c * Math.exp(-z * z / 2.0) * t * (t * (t * (t * (t * b5 + b4) + b3) + b2) + b1))
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def compute_confidence?
|
88
|
+
self.test &&
|
89
|
+
self.test.control && self != self.test.control &&
|
90
|
+
self.visitors > 0
|
91
|
+
end
|
92
|
+
|
93
|
+
def pretty(num)
|
94
|
+
if num.respond_to?(:strip)
|
95
|
+
num
|
96
|
+
else
|
97
|
+
sprintf("%.3f", num)[1..-1]
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def sample_size(control)
|
102
|
+
# confidence level is 95% and the desired power is 80%
|
103
|
+
variance = control.conversion_rate * (1 - control.conversion_rate)
|
104
|
+
sensitivity = (self.conversion_rate - control.conversion_rate) ** 2
|
105
|
+
if sensitivity != 0
|
106
|
+
(16 * variance / sensitivity).to_i
|
107
|
+
else
|
108
|
+
0
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def save_test
|
113
|
+
self.test.save if self.test
|
114
|
+
end
|
115
|
+
|
116
|
+
def z_score(control)
|
117
|
+
cr1 = control.conversion_rate
|
118
|
+
cr2 = self.conversion_rate
|
119
|
+
|
120
|
+
v1 = control.visitors
|
121
|
+
v2 = self.visitors
|
122
|
+
|
123
|
+
if v1 == 0.0 || v2 == 0.0
|
124
|
+
0.0
|
125
|
+
else
|
126
|
+
z = cr2 - cr1
|
127
|
+
s = (cr2 * (1 - cr2)) / v2 + (cr1 * (1 - cr1)) / v1
|
128
|
+
z / Math.sqrt(s)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class Token < ActiveRecord::Base
|
2
|
+
|
3
|
+
set_table_name :a_b_tokens
|
4
|
+
@@cached_at = nil
|
5
|
+
|
6
|
+
def self.cached
|
7
|
+
if @@cached_at.nil? || @@cached_at < Time.now.utc - 60
|
8
|
+
@@cached = self.last.token
|
9
|
+
@@cached_at = Time.now.utc
|
10
|
+
end
|
11
|
+
@@cached
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.generate!
|
15
|
+
if !self.last or (self.last.created_at < Time.now.utc - 60 * 60)
|
16
|
+
token = self.create(:token => Authlogic::Random.friendly_token).token
|
17
|
+
else
|
18
|
+
token = self.last.token
|
19
|
+
end
|
20
|
+
token
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
%hr.space
|
2
|
+
%hr.space
|
3
|
+
|
4
|
+
.span-16
|
5
|
+
%h2{ :class => @tests.empty? ? 'hide' : nil }
|
6
|
+
%a{ :href => '#' } New A/B Test
|
7
|
+
.span-15.first
|
8
|
+
#new_test.box{ :class => @tests.empty? ? nil : 'hide' }
|
9
|
+
%form{ :action => '/tests/create', :method => 'post', :class => 'clearfix' }
|
10
|
+
.span-7
|
11
|
+
%label{ :for => 'test[name]' } Test name
|
12
|
+
%br/
|
13
|
+
%input{ :type => 'text', :name => 'test[name]' }
|
14
|
+
.span-7.last
|
15
|
+
%label{ :for => 'test[ticket_url]' } Ticket URL
|
16
|
+
%br/
|
17
|
+
%input{ :type => 'text', :name => 'test[ticket_url]' }
|
18
|
+
%hr.space
|
19
|
+
.span-10
|
20
|
+
%label{ :for => 'test[variants]' }
|
21
|
+
Variant names
|
22
|
+
(<a class="tooltip" title="variant_names">more info</a>)
|
23
|
+
%br/
|
24
|
+
%input{ :type => 'text', :name => 'test[variants]' }
|
25
|
+
.buttons.span-4.last
|
26
|
+
%input{ :type => 'submit', :value => 'Create', :class => 'submit' }
|
27
|
+
%input{ :type => 'submit', :value => 'Cancel', :class => 'cancel' }
|
28
|
+
|
29
|
+
#search.span-8.last
|
30
|
+
- unless @tests.empty?
|
31
|
+
%form
|
32
|
+
%input.span-5{ :type => 'text' }/
|
33
|
+
%input{ :type => 'submit', :value => 'Search' }
|
34
|
+
|
35
|
+
- if @success || @error
|
36
|
+
.span-23.first
|
37
|
+
- if @success
|
38
|
+
.success= @success
|
39
|
+
- if @error
|
40
|
+
.error= @error
|
41
|
+
|
42
|
+
%hr.space
|
43
|
+
|
44
|
+
- @tests.each_with_index do |test, index|
|
45
|
+
.table.span-8{ :class => table_class(test, index) }[test]
|
46
|
+
.data.hide
|
47
|
+
= table_json(test)
|
48
|
+
.title.clearfix
|
49
|
+
.span-5.search
|
50
|
+
%h3= test.name
|
51
|
+
.span-1.ticket
|
52
|
+
%a{ :href => test.ticket_url } Ticket
|
53
|
+
.span-1.edit
|
54
|
+
%a{ :href => '#' } Edit
|
55
|
+
.span-1.last x
|
56
|
+
.rows
|
57
|
+
.header.clearfix
|
58
|
+
.span-2 Name
|
59
|
+
.span-3.tooltip{ :title => 'visitors_suggested' } Visitors/Suggested
|
60
|
+
.span-1.tooltip{ :title => 'conf' } Conf
|
61
|
+
.span-1.last.tooltip{ :title => 'rate' } Rate
|
62
|
+
- test.sorted_variants.each do |variant|
|
63
|
+
.row.clearfix.search[variant]
|
64
|
+
.span-2
|
65
|
+
- if variant.control?
|
66
|
+
%strong= variant.name
|
67
|
+
- else
|
68
|
+
= variant.name
|
69
|
+
.span-3
|
70
|
+
%span{ :class => variant.suggested_visitors_ok? ? 'green' : nil }
|
71
|
+
= variant.visitors_with_commas
|
72
|
+
\/
|
73
|
+
= variant.suggested_visitors_with_commas
|
74
|
+
.span-1
|
75
|
+
%span{ :class => variant.confidence_ok? ? 'green' : nil }
|
76
|
+
= variant.pretty_confidence
|
77
|
+
.span-1
|
78
|
+
%span{ :class => variant.conversion_rate_ok? ? 'green' : nil }
|
79
|
+
= variant.pretty_conversion_rate
|
80
|
+
.span-1.last x
|
81
|
+
- if table_class(test, index)
|
82
|
+
%hr.space
|
83
|
+
%hr.space
|
84
|
+
|
85
|
+
%textarea.hide#tooltip_variant_names
|
86
|
+
%b Variant names
|
87
|
+
%ul
|
88
|
+
%li This field is a comma separated list of variant names.
|
89
|
+
%li The first variant is the control.
|
90
|
+
%li Variant names must be unique across all tests.
|
91
|
+
%li Variant names must only consist of letters, numbers, and underscores.
|
92
|
+
|
93
|
+
%textarea.hide#tooltip_visitors_suggested
|
94
|
+
%b Visitors
|
95
|
+
Number of exposures for this variant.
|
96
|
+
%hr.space
|
97
|
+
%b Suggested Visitors
|
98
|
+
Number of exposures necessary to trust that the amount of change between the control and the
|
99
|
+
variant is significantly different.
|
100
|
+
|
101
|
+
%textarea.hide#tooltip_conf
|
102
|
+
%b Confidence
|
103
|
+
A variant is statistically better than the control if confidence is greater than or equal to 0.95.
|
104
|
+
|
105
|
+
%textarea.hide#tooltip_rate
|
106
|
+
%b Conversion Rate
|
107
|
+
The percentage of users who converted in decimal form.
|
108
|
+
|
109
|
+
%textarea.hide#edit_template
|
110
|
+
.span-7.first.edit_container
|
111
|
+
.box.clearfix
|
112
|
+
%form{ :action => '/tests/:id/update', :method => 'post' }
|
113
|
+
%p
|
114
|
+
%label{ :for => 'test[name]' } Test name
|
115
|
+
%br/
|
116
|
+
%input{ :type => 'text', :name => 'test[name]' }
|
117
|
+
%p
|
118
|
+
%label{ :for => 'test[ticket_url]' } Ticket URL
|
119
|
+
%br/
|
120
|
+
%input{ :type => 'text', :name => 'test[ticket_url]' }
|
121
|
+
%p
|
122
|
+
%label{ :for => 'test[variants]' }
|
123
|
+
Variant names
|
124
|
+
(<a class="tooltip" title="variant_names">more info</a>)
|
125
|
+
%br/
|
126
|
+
%input{ :type => 'text', :name => 'test[variants]' }
|
127
|
+
.buttons
|
128
|
+
%input{ :type => 'submit', :value => 'Update', :class => 'submit' }
|
129
|
+
%input{ :type => 'submit', :value => 'Cancel', :class => 'cancel' }
|