i_wonder 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +9 -97
- data/app/assets/javascripts/i_wonder/ab_tests.js +2 -0
- data/app/assets/javascripts/i_wonder/application.js +15 -0
- data/app/assets/stylesheets/i_wonder/ab_tests.css.scss +22 -0
- data/app/assets/stylesheets/i_wonder/application.css.scss +1 -0
- data/app/controllers/i_wonder/ab_tests_controller.rb +48 -0
- data/app/controllers/i_wonder/application_controller.rb +4 -4
- data/app/controllers/i_wonder/metrics_controller.rb +33 -34
- data/app/helpers/i_wonder/ab_tests_helper.rb +4 -0
- data/app/models/i_wonder/ab_test.rb +101 -0
- data/app/models/i_wonder/ab_test_goal.rb +49 -0
- data/app/models/i_wonder/event.rb +5 -2
- data/app/models/i_wonder/test_group_membership.rb +12 -0
- data/app/views/i_wonder/ab_tests/_ab_test_goal_row.html.erb +23 -0
- data/app/views/i_wonder/ab_tests/_ab_test_goals.html.erb +13 -0
- data/app/views/i_wonder/ab_tests/_ab_test_group_row.html.erb +5 -0
- data/app/views/i_wonder/ab_tests/_ab_test_groups.html.erb +9 -0
- data/app/views/i_wonder/ab_tests/_form.html.erb +50 -0
- data/app/views/i_wonder/ab_tests/_show_not_started.html.erb +18 -0
- data/app/views/i_wonder/ab_tests/_show_report.html.erb +26 -0
- data/app/views/i_wonder/ab_tests/edit.html.erb +7 -0
- data/app/views/i_wonder/ab_tests/index.html.erb +20 -0
- data/app/views/i_wonder/ab_tests/new.html.erb +7 -0
- data/app/views/i_wonder/ab_tests/show.html.erb +20 -0
- data/app/views/i_wonder/reports/_form.html.erb +9 -5
- data/app/views/layouts/i_wonder.html.erb +1 -0
- data/config/routes.rb +1 -0
- data/db/migrate/20111022230720_i_wonder_migrations.rb +35 -26
- data/lib/i_wonder/ab_testing/action_controller_mixins.rb +20 -0
- data/lib/i_wonder/engine.rb +2 -0
- data/lib/i_wonder/logging/{mixins/action_controller_mixins.rb → action_controller_mixins.rb} +3 -3
- data/lib/i_wonder/logging/{mixins/active_record_mixins.rb → active_record_mixins.rb} +0 -0
- data/lib/i_wonder/version.rb +1 -1
- data/lib/i_wonder.rb +7 -3
- data/test/dummy/app/controllers/application_controller.rb +7 -7
- data/test/dummy/db/schema.rb +33 -0
- data/test/dummy/log/development.log +16012 -0
- data/test/dummy/log/test.log +30206 -0
- data/test/dummy/tmp/cache/assets/C7B/1F0/sprockets%2F69ce38b6f0a103527048612c29753c1f +0 -0
- data/test/dummy/tmp/cache/assets/C81/600/sprockets%2F30b4f9798169b0d0157b48c09ea23472 +0 -0
- data/test/dummy/tmp/cache/assets/CB1/1C0/sprockets%2F0677230b357317e47dffaff9013e0377 +0 -0
- data/test/dummy/tmp/cache/assets/CE6/720/sprockets%2F9718b522ff20c58a59b179b7759ca20b +0 -0
- data/test/dummy/tmp/cache/assets/D20/120/sprockets%2F73fcb7f2f97642793feb2c3d871c1482 +0 -0
- data/test/dummy/tmp/cache/assets/D2F/9D0/sprockets%2F50ce229bd4200bed47618724bcab6c73 +0 -0
- data/test/dummy/tmp/cache/assets/D37/100/sprockets%2F4e900e2ccfdda5075214028c89e25f0e +0 -0
- data/test/dummy/tmp/cache/assets/D79/600/sprockets%2Fc2a1e7e47cead78810554cc9c0de9388 +0 -0
- data/test/dummy/tmp/cache/assets/D84/210/sprockets%2Fabd0103ccec2b428ac62c94e4c40b384 +0 -0
- data/test/dummy/tmp/cache/assets/D8E/250/sprockets%2Fefe57e932e1d26c3b6af42a5311f0a1c +0 -0
- data/test/dummy/tmp/cache/assets/DA7/BA0/sprockets%2F463a9dd811f2418d96fa72cfe05fb8cd +0 -0
- data/test/dummy/tmp/cache/assets/DAD/040/sprockets%2Fc59fd78ac2642a5f3c1b6ef899d50cf3 +0 -0
- data/test/dummy/tmp/cache/assets/DD1/B10/sprockets%2F85b9bce1c5bc3fb86a83d8c8c34e05e0 +0 -0
- data/test/dummy/tmp/cache/assets/DDF/BA0/sprockets%2F187ad995e2ff3588c339fdbecceba119 +0 -0
- data/test/dummy/tmp/cache/assets/DF9/5C0/sprockets%2Fd711a81a91fcc3cd1a30798dd94fdcbd +0 -0
- data/test/dummy/tmp/cache/assets/E04/890/sprockets%2F2f5173deea6c795b8fdde723bb4b63af +0 -0
- data/test/dummy/tmp/pids/server.pid +1 -0
- data/test/functional/ab_controller_mixins_test.rb +80 -0
- data/test/functional/{test_controller_test.rb → logging_controller_mixins_test.rb} +4 -3
- data/test/integration/i_wonder/dashboard_controller_test.rb +10 -3
- data/test/integration/middleware_test.rb +4 -0
- data/test/unit/helpers/i_wonder/ab_tests_helper_test.rb +6 -0
- data/test/unit/i_wonder/ab_test_test.rb +37 -0
- data/test/unit/i_wonder/event_test.rb +5 -3
- data/test/unit/i_wonder/test_group_membership_test.rb +9 -0
- metadata +65 -24
data/README.rdoc
CHANGED
@@ -2,114 +2,26 @@
|
|
2
2
|
|
3
3
|
Flexible Analytics, and Testing for every corner of your rails application. I read "The Lean Startup" and got inspired. There were a few good solutions, but none of them quite did what I was looking for. I highly recommend you check out Vanity (http://vanity.labnotes.org/) as it is a drop-in solution for many rails apps.
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
- Events get tracked by session, as well as an optional user and/or account. This allows the tracking of a users history.
|
8
|
-
- Tests can be performed against a session (non-user), logged in user, or an entire account. For account based apps, having different versions for different users can be bad.
|
9
|
-
- Data is collected periodically. Lots of useful scenerios for this. (info get's deleted, overwritten, etc)
|
10
|
-
- Different reports (and some metrics) can be setup and changed without having to modify your code.
|
11
|
-
- Different reports can be generated without coding knowledge.
|
12
|
-
- Each hit and visitor comes with a referrer and IP code. Because that events will be transferred to the user when they signup, you can see which campaigns are resulting in actual signups and paid conversions.
|
13
|
-
- Because the session events get added to the user when they log in, you avoid false positives on marketing analytics from visitors returning on different devices. If your application has a mobile interface, this basically avoids all your users being logged as visitors twice.
|
14
|
-
|
15
|
-
More big plans!
|
16
|
-
|
17
|
-
- Long term campaign tracking interface (the events already get logged, but this will make it easier to see results)
|
18
|
-
- E-mail open tracking (for application generated e-mails like the 'Welcome E-mail')
|
19
|
-
- Feature request buttons. - details coming soon
|
20
|
-
|
21
|
-
== Requirements
|
22
|
-
|
23
|
-
* Due to performance optimization IWonder must be used with an SQL style DB currently.
|
24
|
-
* Rails 3.1 or higher
|
25
|
-
* Ability to run hourly rake tasks (rake cron)
|
26
|
-
* Ability to run a background worker process (delayed_job)
|
5
|
+
More information can be found at https://github.com/forrest/i_wonder/wiki
|
27
6
|
|
28
7
|
== Installation
|
29
8
|
|
30
|
-
|
9
|
+
You can install IWonder with almost no change to your code in 10-15 minutes. Check it out at http://github.com/forrest/i_wonder/wiki/Installation
|
31
10
|
|
32
|
-
|
33
|
-
|
34
|
-
gem "i_wonder"
|
11
|
+
== Usage
|
35
12
|
|
36
|
-
|
13
|
+
Usage instructions can be found at https://github.com/forrest/i_wonder/wiki/Usage
|
37
14
|
|
38
|
-
bundle install
|
39
15
|
|
40
|
-
|
41
|
-
|
42
|
-
Generate the migration files and migrate your database:
|
43
|
-
|
44
|
-
rake i_wonder:install:migrations
|
45
|
-
rake db:migrate
|
46
|
-
|
47
|
-
|
48
|
-
=== Create the dashboard controller
|
49
|
-
|
50
|
-
Add the engine to your routes. This is where the dashboard will be hosted
|
51
|
-
|
52
|
-
Rails.application.routes.draw do
|
53
|
-
...
|
54
|
-
mount IWonder::Engine => "/super_analytics_engine"
|
55
|
-
end
|
56
|
-
|
57
|
-
=== Setting up the hourly cron task
|
58
|
-
|
59
|
-
This gem hooks into the "<code>rake cron</code>" request. It can't take snapshots of your metrics any faster than this cron is run. We recommend every hour.
|
60
|
-
This will be run automatically if you are hosted on Heroku and have the 'hourly cron' add-on enabled.
|
61
|
-
|
62
|
-
|
63
|
-
=== Install Dependecies
|
64
|
-
|
65
|
-
This Gem requires the Delayed::Job gem. Installation and configuration instructions can be found at http://github.com/tobi/delayed_job.
|
66
|
-
|
67
|
-
We also require the highcharts javascript file in your applications asset path. We don't include it because of licencing reasons. It's free for testing and development. You can find it at http://www.highcharts.com/
|
68
|
-
|
69
|
-
|
70
|
-
=== Security
|
71
|
-
|
72
|
-
IWonder's controllers will run any <code>:before_filters</code> you have set in your application controller. You can easily check for a password or admin status there.
|
73
|
-
|
74
|
-
=== Options & Configurations
|
75
|
-
|
76
|
-
You can choose to navigate directly to the dashboard. If you want to include a link on your admin page to the i_wonder dashboard, your can access the url with <code>i_wonder.root_url</code> such as:
|
77
|
-
|
78
|
-
<%= link_to "Go to analytics >", i_wonder.root_url %>
|
79
|
-
|
80
|
-
|
81
|
-
If you want to configure this gem, add this to an initializer.
|
82
|
-
|
83
|
-
IWonder.configure do |c|
|
84
|
-
c.controllers_to_ignore = [] # These are all the controllers which don't log any events (i_wonder is also ignored)
|
85
|
-
c.only_log_hits_on_200 = true # By default hits and new visitors won't be logged on anything but a 200 or 304 (not modified). Your custom events will still be recorded.
|
86
|
-
c.back_to_app_link = "/" # this is the link hitting the home button in IWonder will take you to.
|
87
|
-
c.app_name = "My App"
|
88
|
-
end
|
89
|
-
|
90
|
-
|
91
|
-
== Usage
|
92
|
-
|
93
|
-
=== Event tracking
|
94
|
-
|
95
|
-
==== Setting session, user, and account details
|
16
|
+
== Contributing & Reporting Issues
|
96
17
|
|
97
|
-
|
18
|
+
Report a bug at http://github.com/forrest/i_wonder/issues
|
98
19
|
|
99
|
-
|
20
|
+
Request a feature at http://github.com/forrest/i_wonder/issues
|
100
21
|
|
101
|
-
|
102
|
-
|
103
|
-
i_wonder_for_account_id(account_id)
|
104
|
-
|
105
|
-
:hits and :new_visitors will be logged automatically. You can store any additional by calling <code>report!(event_sym, options = {})</code> in the controller.
|
106
|
-
You can also log an event from a model with the same method <code>report!(event_sym, options = {})</code>, but you will need to provide the <code>:user_id</code>
|
107
|
-
or <code>:account_id</code> in the options, if you want them recorded.
|
108
|
-
|
109
|
-
|
110
|
-
== Contributing & Reporting Issues
|
22
|
+
Contribute code by picking something off the feature request list at http://github.com/forrest/i_wonder/issues
|
111
23
|
|
112
|
-
|
24
|
+
Contribute time by helping with some documentation at http://github.com/forrest/i_wonder/wiki
|
113
25
|
|
114
26
|
|
115
27
|
== Licensing
|
@@ -8,3 +8,18 @@
|
|
8
8
|
//= require jquery-ui
|
9
9
|
//= require jquery_ujs
|
10
10
|
//= require_tree .
|
11
|
+
|
12
|
+
|
13
|
+
|
14
|
+
function nested_delete(raw_this){
|
15
|
+
var row = $(raw_this).closest('.deletable_row');
|
16
|
+
row.hide();
|
17
|
+
row.find(".hidden_destroy").val("true");
|
18
|
+
}
|
19
|
+
|
20
|
+
//this is called when a new row is being inserted from a partial.
|
21
|
+
//It will replace the "new_row_id" with the current timestamp.
|
22
|
+
function new_row_inserter(html){
|
23
|
+
var timestamp = new Date().getTime();
|
24
|
+
return html.replace(/new_row_id/g, timestamp)
|
25
|
+
}
|
@@ -0,0 +1,22 @@
|
|
1
|
+
@import "_shared.css.scss";
|
2
|
+
|
3
|
+
.main_form.ab_test { min-width:400px;
|
4
|
+
|
5
|
+
#new_goal, #new_group {margin:10px 0; float:right;}
|
6
|
+
|
7
|
+
#ab_test_goals, #ab_test_groups { list-style:decimal; padding:0 0 0 25px; margin:0;
|
8
|
+
li { padding:0px; clear:both;
|
9
|
+
.options {display:inline-block; margin-left:10px;}
|
10
|
+
.delete {float:right;}
|
11
|
+
}
|
12
|
+
}
|
13
|
+
}
|
14
|
+
|
15
|
+
table#ab_test_summary { background-color:$light_blue_box_background; padding:0px; margin:0px; border-style:solid; border-width:1px 1px 0 0; border-color:$light_blue_box_border;
|
16
|
+
thead {
|
17
|
+
td {background-color:#ddd;}
|
18
|
+
}
|
19
|
+
tr {
|
20
|
+
td {padding:5px; border-style:solid; border-width:0 0 1px 1px; border-color:$light_blue_box_border; text-align:center;}
|
21
|
+
}
|
22
|
+
}
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module IWonder
|
2
|
+
class AbTestsController < ApplicationController
|
3
|
+
layout "i_wonder"
|
4
|
+
|
5
|
+
def index
|
6
|
+
@ab_tests = AbTest.all
|
7
|
+
end
|
8
|
+
|
9
|
+
def show
|
10
|
+
@ab_test = AbTest.find(params[:id])
|
11
|
+
end
|
12
|
+
|
13
|
+
def new
|
14
|
+
@ab_test = AbTest.new(params[:ab_test])
|
15
|
+
end
|
16
|
+
|
17
|
+
def create
|
18
|
+
@ab_test = AbTest.new(params[:ab_test])
|
19
|
+
|
20
|
+
if @ab_test.save
|
21
|
+
redirect_to @ab_test, :notice => "Successfully created ABTest"
|
22
|
+
else
|
23
|
+
render "new"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def edit
|
28
|
+
@ab_test = AbTest.find(params[:id])
|
29
|
+
render "edit"
|
30
|
+
end
|
31
|
+
|
32
|
+
def update
|
33
|
+
@ab_test = AbTest.find(params[:id])
|
34
|
+
|
35
|
+
if @ab_test.update_attributes(params[:ab_test])
|
36
|
+
redirect_to @ab_test, :notice => "Successfully updated ABTest"
|
37
|
+
else
|
38
|
+
render "edit"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def destroy
|
43
|
+
@ab_test = AbTest.find(params[:id])
|
44
|
+
@ab_test.destroy
|
45
|
+
redirect_to ab_tests_path, :notice => "ABTest has been destroyed"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
module IWonder
|
2
|
-
|
3
|
-
|
4
|
-
end
|
1
|
+
# module IWonder
|
2
|
+
# class ApplicationController < ActionController::Base
|
3
|
+
# end
|
4
|
+
# end
|
@@ -1,49 +1,48 @@
|
|
1
1
|
module IWonder
|
2
2
|
class MetricsController < ApplicationController
|
3
3
|
layout "i_wonder"
|
4
|
-
|
5
|
-
def index
|
6
|
-
@metrics = Metric.all
|
7
|
-
end
|
8
4
|
|
9
|
-
|
10
|
-
|
11
|
-
|
5
|
+
def index
|
6
|
+
@metrics = Metric.all
|
7
|
+
end
|
12
8
|
|
13
|
-
|
14
|
-
|
15
|
-
|
9
|
+
def show
|
10
|
+
@metric = Metric.find(params[:id])
|
11
|
+
end
|
16
12
|
|
17
|
-
|
18
|
-
|
13
|
+
def new
|
14
|
+
@metric = Metric.new(params[:metric])
|
15
|
+
end
|
19
16
|
|
20
|
-
|
21
|
-
|
22
|
-
else
|
23
|
-
render "new"
|
24
|
-
end
|
25
|
-
end
|
17
|
+
def create
|
18
|
+
@metric = Metric.new(params[:metric])
|
26
19
|
|
27
|
-
|
28
|
-
@metric
|
29
|
-
|
20
|
+
if @metric.save
|
21
|
+
redirect_to @metric, :notice => "Successfully created metric"
|
22
|
+
else
|
23
|
+
render "new"
|
30
24
|
end
|
25
|
+
end
|
31
26
|
|
32
|
-
|
33
|
-
|
27
|
+
def edit
|
28
|
+
@metric = Metric.find(params[:id])
|
29
|
+
render "edit"
|
30
|
+
end
|
34
31
|
|
35
|
-
|
36
|
-
|
37
|
-
else
|
38
|
-
render "edit"
|
39
|
-
end
|
40
|
-
end
|
32
|
+
def update
|
33
|
+
@metric = Metric.find(params[:id])
|
41
34
|
|
42
|
-
|
43
|
-
@metric
|
44
|
-
|
45
|
-
|
35
|
+
if @metric.update_attributes(params[:metric])
|
36
|
+
redirect_to @metric, :notice => "Successfully updated metric"
|
37
|
+
else
|
38
|
+
render "edit"
|
46
39
|
end
|
47
|
-
|
40
|
+
end
|
41
|
+
|
42
|
+
def destroy
|
43
|
+
@metric = Metric.find(params[:id])
|
44
|
+
@metric.destroy
|
45
|
+
redirect_to metrics_path, :notice => "Metric has been destroyed"
|
46
|
+
end
|
48
47
|
end
|
49
48
|
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module IWonder
|
2
|
+
class AbTest < ActiveRecord::Base
|
3
|
+
attr_accessible :name, :sym, :description, :ab_test_goals_attributes, :test_applies_to, :test_group_names
|
4
|
+
serialize :options, Hash
|
5
|
+
serialize :test_group_data, Hash
|
6
|
+
|
7
|
+
has_many :test_group_memberships, :dependent => :destroy
|
8
|
+
|
9
|
+
has_many :ab_test_goals, :dependent => :destroy
|
10
|
+
accepts_nested_attributes_for :ab_test_goals, :allow_destroy => true
|
11
|
+
|
12
|
+
hash_accessor :options, :test_group_names, :type => :array, :reject_blanks => true
|
13
|
+
|
14
|
+
validates_presence_of :name, :on => :create, :message => "can't be blank"
|
15
|
+
validates_uniqueness_of :name, :on => :create, :message => "must be unique"
|
16
|
+
validates_presence_of :sym, :on => :create, :message => "can't be blank"
|
17
|
+
validates_format_of :sym, :with => /^[\w\d\_]+$/, :on => :create, :message => "can only contain letters, numbers and underscores"
|
18
|
+
validates_uniqueness_of :sym, :on => :create, :message => "must be unique"
|
19
|
+
validates_inclusion_of :test_applies_to, :in => %w( session user account ), :on => :create, :message => "extension %s is not included in the list"
|
20
|
+
|
21
|
+
validate :has_two_groups_and_a_goal
|
22
|
+
def has_two_groups_and_a_goal
|
23
|
+
unless test_group_names.length >= 2
|
24
|
+
errors.add(:base, "Must have atleast two test groups")
|
25
|
+
end
|
26
|
+
|
27
|
+
if ab_test_goals.reject(&:marked_for_destruction?).length == 0
|
28
|
+
errors.add(:base, "Must have atleast one goal")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def started?
|
33
|
+
test_group_memberships.count > 0
|
34
|
+
end
|
35
|
+
|
36
|
+
def which_test_group?(current_controller)
|
37
|
+
if(current_group = get_current_group(current_controller))
|
38
|
+
return current_group.test_group_name
|
39
|
+
else
|
40
|
+
test_group = add_to_test_group(randomly_chosen_test_group, current_controller)
|
41
|
+
return test_group.test_group_name
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def assigned_count(test_group_name)
|
46
|
+
self.test_group_memberships.where(:test_group_name => test_group_name).count
|
47
|
+
end
|
48
|
+
|
49
|
+
def get_results_for(test_group_name, ab_test_goal)
|
50
|
+
scoped_test_group_memberships = self.test_group_memberships.where(:test_group_name => test_group_name).scoped
|
51
|
+
|
52
|
+
if test_applies_to =~ /account/i
|
53
|
+
event_membership_key = "account_id"
|
54
|
+
elsif test_applies_to =~ /user/i
|
55
|
+
event_membership_key = "user_id"
|
56
|
+
else
|
57
|
+
event_membership_key = "session_id"
|
58
|
+
end
|
59
|
+
|
60
|
+
scoped_groups_with_events = scoped_test_group_memberships.joins("LEFT JOIN i_wonder_events ON i_wonder_events.#{event_membership_key} = i_wonder_test_group_memberships.member_id")
|
61
|
+
|
62
|
+
scoped_groups_with_goal_events = ab_test_goal.add_goal_to_query(scoped_groups_with_events)
|
63
|
+
|
64
|
+
scoped_groups_with_goal_events.count
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def get_current_group(current_controller)
|
71
|
+
if test_applies_to =~ /account/i
|
72
|
+
env = current_controller.request.env
|
73
|
+
test_group_memberships.where(:member_type => "account", :member_id => env[ENV_KEY]["account_id"].to_s).first
|
74
|
+
elsif test_applies_to =~ /user/i
|
75
|
+
env = current_controller.request.env
|
76
|
+
test_group_memberships.where(:member_type => "user", :member_id => env[ENV_KEY]["user_id"].to_s).first
|
77
|
+
else
|
78
|
+
session_id = current_controller.send("cookies")[COOKIE_KEY+Logging::SESSION_KEY_NAME]
|
79
|
+
test_group_memberships.where(:member_type => "session", :member_id => session_id.to_s).first
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def add_to_test_group(test_group_name, current_controller)
|
84
|
+
if test_applies_to =~ /account/i
|
85
|
+
env = current_controller.request.env
|
86
|
+
test_group_memberships.create!(:member_type => "account", :member_id => env[ENV_KEY]["account_id"].to_s, :test_group_name => test_group_name)
|
87
|
+
elsif test_applies_to =~ /user/i
|
88
|
+
env = current_controller.request.env
|
89
|
+
test_group_memberships.create!(:member_type => "user", :member_id => env[ENV_KEY]["user_id"].to_s, :test_group_name => test_group_name)
|
90
|
+
else
|
91
|
+
session_id = current_controller.send("cookies")[COOKIE_KEY+Logging::SESSION_KEY_NAME]
|
92
|
+
test_group_memberships.create!(:member_type => "session", :member_id => session_id.to_s, :test_group_name => test_group_name)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def randomly_chosen_test_group
|
97
|
+
test_group_names[rand(test_group_names.length)]
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module IWonder
|
2
|
+
class AbTestGoal < ActiveRecord::Base
|
3
|
+
belongs_to :ab_test
|
4
|
+
|
5
|
+
attr_accessible :goal_type, :event_type, :page_view_controller, :page_view_action
|
6
|
+
serialize :options, Hash
|
7
|
+
hash_accessor :options, :goal_type, :default => "Event"
|
8
|
+
hash_accessor :options, :event_type
|
9
|
+
hash_accessor :options, :page_view_controller
|
10
|
+
hash_accessor :options, :page_view_action
|
11
|
+
|
12
|
+
validates_inclusion_of :goal_type, :in => ["Event", "Page View"]
|
13
|
+
validates_presence_of :event_type, :if => :tracks_event?
|
14
|
+
validates_presence_of :page_view_controller, :if => :tracks_page_view?
|
15
|
+
|
16
|
+
def tracks_event?
|
17
|
+
goal_type == "Event"
|
18
|
+
end
|
19
|
+
|
20
|
+
def tracks_page_view?
|
21
|
+
goal_type == "Page View"
|
22
|
+
end
|
23
|
+
|
24
|
+
def add_goal_to_query(scoped_statement)
|
25
|
+
if tracks_event?
|
26
|
+
sc = scoped_statement.where("i_wonder_events.event_type = ?", event_type)
|
27
|
+
else
|
28
|
+
sc = scoped_statement.where("i_wonder_events.event_type = ? AND controller = ?", "hit", page_view_controller)
|
29
|
+
|
30
|
+
if page_view_action.present?
|
31
|
+
sc = sc.where("i_wonder_events.action = ?", page_view_action)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
sc.where("i_wonder_events.created_at > ?", self.ab_test.created_at) # event had to come after goal
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_s
|
39
|
+
if tracks_event?
|
40
|
+
"#{event_type} occurred"
|
41
|
+
elsif page_view_action.present?
|
42
|
+
"#{page_view_controller}##{page_view_action} hit"
|
43
|
+
else
|
44
|
+
"any #{page_view_controller} hit"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
@@ -15,12 +15,15 @@ module IWonder
|
|
15
15
|
# for all events on the current session, attach the user and session from that first event
|
16
16
|
update_all({:user_id => user_id, :session_id => original_session_id}, ["session_id = ? AND user_id IS NULL", session_id])
|
17
17
|
|
18
|
-
#
|
18
|
+
# Change the new_visitor to a return_visit
|
19
19
|
if new_visitor_event and original_session_id != session_id
|
20
|
-
|
20
|
+
update_all({:event_type => "return_visit"}, ["user_id = ? AND event_type = ? AND id <> ?", user_id, "new_visitor", new_visitor_event.id])
|
21
21
|
end
|
22
22
|
|
23
23
|
|
24
|
+
# TODO: merge any tests on the session id over to the original session_id
|
25
|
+
# remove any duplicates (going with the original)
|
26
|
+
|
24
27
|
return original_session_id
|
25
28
|
end
|
26
29
|
# handle_asynchronously :merge_session_to_user
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module IWonder
|
2
|
+
class TestGroupMembership < ActiveRecord::Base
|
3
|
+
belongs_to :ab_test
|
4
|
+
|
5
|
+
validates_presence_of :test_group_name, :on => :create, :message => "can't be blank"
|
6
|
+
validates_inclusion_of :member_type, :in => %w( account user session ), :on => :create
|
7
|
+
validates_presence_of :member_id, :on => :create, :message => "can't be blank"
|
8
|
+
|
9
|
+
validates_uniqueness_of :ab_test_id, :on => :create, :message => "must be unique for this member", :scope => [:member_type, :member_id]
|
10
|
+
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
<li class="ab_test_goal deletable_row">
|
2
|
+
<%= f.hidden_field :id%>
|
3
|
+
|
4
|
+
<%= f.select :goal_type, ["Event", "Page View"], {}, :onchange => "var row = $(this).closest('li'); row.find('.options').hide(); if($(this).val()=='Event'){ row.find('#event_options').show(); }else{ row.find('#page_view_options').show(); }" %>
|
5
|
+
|
6
|
+
<div id="page_view_options" class='options' <% unless f.object.tracks_page_view? %> style="display:none" <% end %> >
|
7
|
+
<%= f.text_field :page_view_controller, :placeholder => "controller", :size => 20 %>
|
8
|
+
|
9
|
+
<%= f.text_field :page_view_action, :placeholder => "action (optional)", :size => 20 %>
|
10
|
+
</div>
|
11
|
+
|
12
|
+
<div id="event_options" class='options' <% unless f.object.tracks_event? %> style="display:none" <% end %> >
|
13
|
+
<%= f.text_field :event_type, :placeholder => "event name" %>
|
14
|
+
</div>
|
15
|
+
|
16
|
+
<% if f.object.new_record? %>
|
17
|
+
<%= link_to_function "", "$(this).closest('li').remove();", :class => "ui-icon ui-icon-circle-close delete" %>
|
18
|
+
<% else%>
|
19
|
+
<%= f.hidden_field :_destroy, :class => "hidden_destroy" %>
|
20
|
+
<%= link_to_function "", "nested_delete(this);", :class => "ui-icon ui-icon-circle-close delete" %>
|
21
|
+
<% end %>
|
22
|
+
|
23
|
+
</li>
|
@@ -0,0 +1,13 @@
|
|
1
|
+
<%= f.fields_for :ab_test_goals, IWonder::AbTestGoal.new, :child_index => "new_row_id" do |f2|%>
|
2
|
+
<%= link_to_function "+ Add Goal", "$('#ab_test_goals').append(new_row_inserter(\"#{j render(:partial => "ab_test_goal_row", :locals => {:f => f2})}\"));", :id => "new_goal" %>
|
3
|
+
<% end %>
|
4
|
+
|
5
|
+
<h4>Goals</h4>
|
6
|
+
|
7
|
+
<ul id="ab_test_goals">
|
8
|
+
<%= f.fields_for :ab_test_goals do |f2|%>
|
9
|
+
<%= render :partial => "ab_test_goal_row", :locals => {:f => f2} %>
|
10
|
+
<% end %>
|
11
|
+
</ul>
|
12
|
+
|
13
|
+
<br style="clear:both"/>
|
@@ -0,0 +1,5 @@
|
|
1
|
+
<li class="ab_test_group deletable_row">
|
2
|
+
<%= text_field_tag "ab_test[test_group_names][]", (defined?(test_group_name) ? test_group_name : ""), :placeholder => "Name..." %>
|
3
|
+
|
4
|
+
<%= link_to_function "", "$(this).closest('li').remove();", :class => "ui-icon ui-icon-circle-close delete" %>
|
5
|
+
</li>
|
@@ -0,0 +1,9 @@
|
|
1
|
+
<%= link_to_function "+ Add Group", "$('#ab_test_groups').append(\"#{j render(:partial => "ab_test_group_row")}\");", :id => "new_goal" %>
|
2
|
+
|
3
|
+
<h4>Test Groups</h4>
|
4
|
+
|
5
|
+
<ul id="ab_test_groups">
|
6
|
+
<%= render :partial => "ab_test_group_row", :collection => f.object.test_group_names, :as => :test_group_name %>
|
7
|
+
</ul>
|
8
|
+
|
9
|
+
<br style="clear:both"/>
|
@@ -0,0 +1,50 @@
|
|
1
|
+
<%= form_for @ab_test, :html => {:class => "main_form ab_test"} do |f| %>
|
2
|
+
<% if @ab_test.errors.any? %>
|
3
|
+
<div id="error_explanation">
|
4
|
+
<h2><%= pluralize(@ab_test.errors.count, "error") %> prohibited this test from being saved:</h2>
|
5
|
+
|
6
|
+
<ul>
|
7
|
+
<% @ab_test.errors.full_messages.each do |msg| %>
|
8
|
+
<li><%= msg %></li>
|
9
|
+
<% end %>
|
10
|
+
</ul>
|
11
|
+
</div>
|
12
|
+
<% end %>
|
13
|
+
|
14
|
+
<div class="field">
|
15
|
+
<b>I wonder</b> <%= f.text_field :name, :placeholder => "test name...", :size => 50 %><b>?</b>
|
16
|
+
</div>
|
17
|
+
|
18
|
+
<div class="field">
|
19
|
+
<%= f.label :description %>
|
20
|
+
<%= f.text_area :description, :rows => 3, :cols => 50%>
|
21
|
+
</div>
|
22
|
+
|
23
|
+
<div class="field">
|
24
|
+
<%= f.label :test_applies_to %>
|
25
|
+
<%= f.select :test_applies_to, [["Visitor", "session"], ["User", "user"], ["Account", "account"]] %><br/>
|
26
|
+
<span class="description">If User or Account is selected but not available, the first option will be returned.</span>
|
27
|
+
</div>
|
28
|
+
|
29
|
+
|
30
|
+
<div class="field">
|
31
|
+
<%= f.label :sym, "Code reference symbol" %>
|
32
|
+
<%= f.text_field :sym, :size => 10 %>
|
33
|
+
<span class="description">letters numbers and underscores. NO spaces!</span>
|
34
|
+
</div>
|
35
|
+
|
36
|
+
<hr/>
|
37
|
+
|
38
|
+
<%= render :partial => "ab_test_groups", :locals => {:f => f} %>
|
39
|
+
|
40
|
+
<hr/>
|
41
|
+
|
42
|
+
<%= render :partial => "ab_test_goals", :locals => {:f => f} %>
|
43
|
+
|
44
|
+
<hr/>
|
45
|
+
|
46
|
+
<div class="field">
|
47
|
+
<%= f.submit %>
|
48
|
+
</div>
|
49
|
+
|
50
|
+
<% end %>
|
@@ -0,0 +1,18 @@
|
|
1
|
+
<p>
|
2
|
+
This test hasn't started yet. Use the following code in a controller or view to begin:
|
3
|
+
</p>
|
4
|
+
|
5
|
+
<br/>
|
6
|
+
<p class="description">You can use the group name directly:</p>
|
7
|
+
<pre><%= which_test_group?("<%= @ab_test.sym %>") %></pre>
|
8
|
+
|
9
|
+
<p class="description">or run through different scenarios:</p>
|
10
|
+
|
11
|
+
<pre>
|
12
|
+
case which_test_group?("<%= @ab_test.sym %>")
|
13
|
+
<% for option_name in @ab_test.test_group_names -%>
|
14
|
+
when "<%= option_name%>"
|
15
|
+
...
|
16
|
+
<% end -%>
|
17
|
+
end
|
18
|
+
</pre>
|
@@ -0,0 +1,26 @@
|
|
1
|
+
<table id="ab_test_summary" cellpadding=0 cellspacing=0>
|
2
|
+
<thead>
|
3
|
+
<tr>
|
4
|
+
<td></td>
|
5
|
+
<td>Assigned</td>
|
6
|
+
|
7
|
+
<% for ab_test_goal in @ab_test.ab_test_goals do %>
|
8
|
+
<td><%= ab_test_goal %></td>
|
9
|
+
<% end %>
|
10
|
+
|
11
|
+
</tr>
|
12
|
+
</thead>
|
13
|
+
|
14
|
+
<% for group_name in @ab_test.test_group_names do %>
|
15
|
+
<tr>
|
16
|
+
<td><%= group_name%></td>
|
17
|
+
<td><%= @ab_test.test_group_memberships.where(:test_group_name => group_name).count %></td>
|
18
|
+
|
19
|
+
<% for ab_test_goal in @ab_test.ab_test_goals do %>
|
20
|
+
<td><%= @ab_test.get_results_for(group_name, ab_test_goal) %></td>
|
21
|
+
<% end %>
|
22
|
+
|
23
|
+
|
24
|
+
</tr>
|
25
|
+
<% end %>
|
26
|
+
</table>
|