i_wonder 0.0.6 → 0.0.7
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/README.rdoc +1 -1
- data/app/controllers/i_wonder/ab_tests_controller.rb +2 -0
- data/app/models/i_wonder/ab_test.rb +37 -4
- data/app/models/i_wonder/ab_test_goal.rb +4 -3
- data/app/models/i_wonder/test_group_membership.rb +2 -2
- data/app/views/i_wonder/ab_tests/_ab_test_groups.html.erb +5 -1
- data/app/views/i_wonder/ab_tests/_form.html.erb +7 -5
- data/app/views/i_wonder/ab_tests/index.html.erb +5 -3
- data/app/views/i_wonder/ab_tests/show.html.erb +9 -5
- data/db/migrate/20111022230720_i_wonder_migrations.rb +4 -4
- data/lib/i_wonder/ab_testing/action_controller_mixins.rb +1 -2
- data/lib/i_wonder/ab_testing/loader.rb +54 -0
- data/lib/i_wonder/engine.rb +6 -1
- data/lib/i_wonder/version.rb +1 -1
- data/lib/i_wonder.rb +1 -0
- data/test/dummy/db/schema.rb +5 -5
- data/test/dummy/log/development.log +917 -0
- data/test/dummy/log/test.log +31889 -0
- data/test/functional/ab_controller_mixins_test.rb +2 -0
- data/test/test_assets/i_wonder/save_test.xml +24 -0
- data/test/test_helper.rb +3 -1
- data/test/unit/i_wonder/ab_test_test.rb +1 -1
- data/test/unit/i_wonder/loader_test.rb +89 -0
- metadata +38 -22
data/README.rdoc
CHANGED
@@ -2,7 +2,7 @@
|
|
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.
|
4
4
|
|
5
|
-
Eventually I want it to be a drop-in replacement for Google analytics.
|
5
|
+
Eventually I want it to be a drop-in replacement for Google analytics. Vanity (http://vanity.labnotes.org/) is a good alternative depending on your analytics needs.
|
6
6
|
|
7
7
|
More information can be found at https://github.com/forrest/i_wonder/wiki
|
8
8
|
|
@@ -18,6 +18,7 @@ module IWonder
|
|
18
18
|
@ab_test = AbTest.new(params[:ab_test])
|
19
19
|
|
20
20
|
if @ab_test.save
|
21
|
+
AbTesting::Loader.save_ab_tests
|
21
22
|
redirect_to @ab_test, :notice => "Successfully created ABTest"
|
22
23
|
else
|
23
24
|
render "new"
|
@@ -33,6 +34,7 @@ module IWonder
|
|
33
34
|
@ab_test = AbTest.find(params[:id])
|
34
35
|
|
35
36
|
if @ab_test.update_attributes(params[:ab_test])
|
37
|
+
AbTesting::Loader.save_ab_tests
|
36
38
|
redirect_to @ab_test, :notice => "Successfully updated ABTest"
|
37
39
|
else
|
38
40
|
render "edit"
|
@@ -1,12 +1,12 @@
|
|
1
1
|
module IWonder
|
2
2
|
class AbTest < ActiveRecord::Base
|
3
|
-
attr_accessible :name, :sym, :description, :ab_test_goals_attributes, :test_applies_to, :test_group_names
|
3
|
+
attr_accessible :name, :sym, :description, :ab_test_goals_attributes, :test_applies_to, :test_group_names, :options, :test_group_data
|
4
4
|
serialize :options, Hash
|
5
5
|
serialize :test_group_data, Hash
|
6
6
|
|
7
|
-
has_many :test_group_memberships, :dependent => :destroy
|
7
|
+
has_many :test_group_memberships, :dependent => :destroy, :foreign_key => "ab_test_sym", :primary_key => :sym
|
8
8
|
|
9
|
-
has_many :ab_test_goals, :dependent => :destroy
|
9
|
+
has_many :ab_test_goals, :dependent => :destroy, :foreign_key => "ab_test_sym", :primary_key => :sym
|
10
10
|
accepts_nested_attributes_for :ab_test_goals, :allow_destroy => true
|
11
11
|
|
12
12
|
hash_accessor :options, :test_group_names, :type => :array, :reject_blanks => true
|
@@ -28,7 +28,17 @@ module IWonder
|
|
28
28
|
errors.add(:base, "Must have atleast one goal")
|
29
29
|
end
|
30
30
|
end
|
31
|
-
|
31
|
+
|
32
|
+
after_save :save_to_file
|
33
|
+
def save_to_file
|
34
|
+
AbTesting::Loader.save_ab_test(self)
|
35
|
+
end
|
36
|
+
|
37
|
+
after_destroy :remove_file
|
38
|
+
def remove_file
|
39
|
+
AbTesting::Loader.remove_file_for(self)
|
40
|
+
end
|
41
|
+
|
32
42
|
def started?
|
33
43
|
test_group_memberships.count > 0
|
34
44
|
end
|
@@ -63,7 +73,30 @@ module IWonder
|
|
63
73
|
|
64
74
|
scoped_groups_with_goal_events.count
|
65
75
|
end
|
76
|
+
|
77
|
+
def from_xml(xml)
|
78
|
+
[super]
|
79
|
+
|
80
|
+
# make sure it's not blank. Then make sure it's a hash. Then Make sure it's symbolized.
|
81
|
+
self.options ||= {}
|
82
|
+
self.test_group_data ||= {}
|
83
|
+
self.options = {} if self.options.is_a?(String)
|
84
|
+
self.test_group_data = {} if self.test_group_data.is_a?(String)
|
85
|
+
self.options.symbolize_keys!
|
86
|
+
self.test_group_data.symbolize_keys!
|
87
|
+
|
88
|
+
self.ab_test_goals.each(&:mark_for_destruction)
|
89
|
+
|
90
|
+
hash = Hash.from_xml(xml)
|
91
|
+
hash["ab_test"]["ab_test_goals"].each{|ab_test_goal_hash|
|
92
|
+
ab_test_goal_hash["options"].symbolize_keys!
|
93
|
+
self.ab_test_goals.build(ab_test_goal_hash)
|
94
|
+
}
|
95
|
+
end
|
66
96
|
|
97
|
+
def started_at
|
98
|
+
test_group_memberships.minimum(:created_at)
|
99
|
+
end
|
67
100
|
|
68
101
|
private
|
69
102
|
|
@@ -1,8 +1,9 @@
|
|
1
1
|
module IWonder
|
2
2
|
class AbTestGoal < ActiveRecord::Base
|
3
|
-
|
3
|
+
attr_accessible :goal_type, :event_type, :page_view_controller, :page_view_action, :options
|
4
|
+
|
5
|
+
belongs_to :ab_test, :foreign_key => "ab_test_sym", :primary_key => :sym
|
4
6
|
|
5
|
-
attr_accessible :goal_type, :event_type, :page_view_controller, :page_view_action
|
6
7
|
serialize :options, Hash
|
7
8
|
hash_accessor :options, :goal_type, :default => "Event"
|
8
9
|
hash_accessor :options, :event_type
|
@@ -32,7 +33,7 @@ module IWonder
|
|
32
33
|
end
|
33
34
|
end
|
34
35
|
|
35
|
-
sc.where("i_wonder_events.created_at > ?", self.ab_test.
|
36
|
+
sc.where("i_wonder_events.created_at > ?", self.ab_test.started_at) # event had to come after goal
|
36
37
|
end
|
37
38
|
|
38
39
|
def to_s
|
@@ -1,12 +1,12 @@
|
|
1
1
|
module IWonder
|
2
2
|
class TestGroupMembership < ActiveRecord::Base
|
3
|
-
belongs_to :ab_test
|
3
|
+
belongs_to :ab_test, :foreign_key => "ab_test_sym", :primary_key => :sym
|
4
4
|
|
5
5
|
validates_presence_of :test_group_name, :on => :create, :message => "can't be blank"
|
6
6
|
validates_inclusion_of :member_type, :in => %w( account user session ), :on => :create
|
7
7
|
validates_presence_of :member_id, :on => :create, :message => "can't be blank"
|
8
8
|
|
9
|
-
validates_uniqueness_of :
|
9
|
+
validates_uniqueness_of :ab_test_sym, :on => :create, :message => "must be unique for this member", :scope => [:member_type, :member_id]
|
10
10
|
|
11
11
|
end
|
12
12
|
end
|
@@ -3,7 +3,11 @@
|
|
3
3
|
<h4>Test Groups</h4>
|
4
4
|
|
5
5
|
<ul id="ab_test_groups">
|
6
|
-
|
6
|
+
<% if f.object.test_group_names.empty? %>
|
7
|
+
<%= render :partial => "ab_test_group_row", :collection => ["", ""], :as => :test_group_name %>
|
8
|
+
<% else %>
|
9
|
+
<%= render :partial => "ab_test_group_row", :collection => f.object.test_group_names, :as => :test_group_name %>
|
10
|
+
<% end %>
|
7
11
|
</ul>
|
8
12
|
|
9
13
|
<br style="clear:both"/>
|
@@ -27,11 +27,13 @@
|
|
27
27
|
</div>
|
28
28
|
|
29
29
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
30
|
+
<% if @ab_test.new_record? %>
|
31
|
+
<div class="field">
|
32
|
+
<%= f.label :sym, "Code reference symbol" %>
|
33
|
+
<%= f.text_field :sym, :size => 10 %>
|
34
|
+
<span class="description">letters numbers and underscores. NO spaces!</span>
|
35
|
+
</div>
|
36
|
+
<% end %>
|
35
37
|
|
36
38
|
<hr/>
|
37
39
|
|
@@ -15,6 +15,8 @@
|
|
15
15
|
</ul>
|
16
16
|
<% end %>
|
17
17
|
|
18
|
-
|
19
|
-
|
20
|
-
|
18
|
+
<% unless Rails.env.production? %>
|
19
|
+
<p>
|
20
|
+
<%= button_to "Create a new ABTest", url_for(:action => :new), :method => :get%>
|
21
|
+
</p>
|
22
|
+
<% end %>
|
@@ -1,9 +1,13 @@
|
|
1
1
|
<div class="show">
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
2
|
+
<% if Rails.env.production? %>
|
3
|
+
<span class="description">No Modifying tests in production</span>
|
4
|
+
<% else %>
|
5
|
+
<p>
|
6
|
+
<%= link_to "Edit", edit_ab_test_path(@ab_test) %>
|
7
|
+
|
|
8
|
+
<%= link_to "Delete", @ab_test, :method => :delete, :confirm => "Are you sure?" %>
|
9
|
+
</p>
|
10
|
+
<% end %>
|
7
11
|
|
8
12
|
<h2>I wonder <%= @ab_test.name%>?</h2>
|
9
13
|
<p class="description">
|
@@ -73,16 +73,16 @@ class IWonderMigrations < ActiveRecord::Migration
|
|
73
73
|
add_index :i_wonder_ab_tests, :sym
|
74
74
|
|
75
75
|
create_table :i_wonder_ab_test_goals do |t|
|
76
|
-
t.
|
76
|
+
t.string :ab_test_sym
|
77
77
|
t.text :options # serialized
|
78
78
|
end
|
79
|
-
add_index :i_wonder_ab_test_goals, :
|
79
|
+
add_index :i_wonder_ab_test_goals, :ab_test_sym
|
80
80
|
|
81
81
|
|
82
82
|
|
83
83
|
|
84
84
|
create_table :i_wonder_test_group_memberships do |t|
|
85
|
-
t.
|
85
|
+
t.string :ab_test_sym
|
86
86
|
t.string :test_group_name
|
87
87
|
|
88
88
|
t.string :member_type
|
@@ -90,7 +90,7 @@ class IWonderMigrations < ActiveRecord::Migration
|
|
90
90
|
|
91
91
|
t.timestamps
|
92
92
|
end
|
93
|
-
add_index :i_wonder_test_group_memberships, :
|
93
|
+
add_index :i_wonder_test_group_memberships, :ab_test_sym
|
94
94
|
add_index :i_wonder_test_group_memberships, :test_group_name
|
95
95
|
add_index :i_wonder_test_group_memberships, [:member_type, :member_id], :name => "i_wonder_test_group_memberships_member"
|
96
96
|
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module IWonder
|
2
|
+
module AbTesting
|
3
|
+
class Loader
|
4
|
+
I_WONDER_DATA_DIR = Rails.root.join("i_wonder").to_s
|
5
|
+
AB_TEST_DATA_DIR = File.join(I_WONDER_DATA_DIR,"ab_tests").to_s
|
6
|
+
|
7
|
+
def self.save_ab_test(ab_test)
|
8
|
+
Dir.mkdir(I_WONDER_DATA_DIR) unless File.exists?(I_WONDER_DATA_DIR)
|
9
|
+
Dir.mkdir(AB_TEST_DATA_DIR) unless File.exists?(AB_TEST_DATA_DIR)
|
10
|
+
|
11
|
+
File.open(file_name(ab_test.sym), 'w') do |file|
|
12
|
+
file.write ab_test.to_xml(:except => [:id, :created_at, :updated_at], :include => :ab_test_goals)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.load_all
|
17
|
+
Dir.open(AB_TEST_DATA_DIR) do |dir|
|
18
|
+
dir.each{|file_name|
|
19
|
+
if file_name =~ /(.+).xml/
|
20
|
+
load_sym($1)
|
21
|
+
end
|
22
|
+
}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.load_sym(ab_test_sym)
|
27
|
+
if File.exists?(file_name(ab_test_sym))
|
28
|
+
File.open(file_name(ab_test_sym), 'r') do |file|
|
29
|
+
|
30
|
+
ab_test = AbTest.find_by_sym(ab_test_sym)
|
31
|
+
ab_test ||= AbTest.new
|
32
|
+
|
33
|
+
ab_test.from_xml(file.read)
|
34
|
+
|
35
|
+
ab_test.save!
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.remove_file_for(ab_test)
|
41
|
+
if File.exists?(file_name(ab_test.sym))
|
42
|
+
File.delete(file_name(ab_test.sym))
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def self.file_name(sym)
|
49
|
+
AB_TEST_DATA_DIR + "/#{sym}.xml"
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/lib/i_wonder/engine.rb
CHANGED
@@ -1,8 +1,13 @@
|
|
1
1
|
module IWonder
|
2
2
|
class Engine < Rails::Engine
|
3
3
|
# This is for the mounting the engine ===========
|
4
|
-
|
5
4
|
isolate_namespace IWonder
|
5
|
+
|
6
|
+
initializer "i_wonder.loading_tests" do |app|
|
7
|
+
ActiveRecord::Base.send :include, HashAccessor # this is to avoid load order issues from the required gem
|
8
|
+
AbTesting::Loader.load_all
|
9
|
+
end
|
10
|
+
|
6
11
|
end
|
7
12
|
end
|
8
13
|
|
data/lib/i_wonder/version.rb
CHANGED
data/lib/i_wonder.rb
CHANGED
data/test/dummy/db/schema.rb
CHANGED
@@ -35,11 +35,11 @@ ActiveRecord::Schema.define(:version => 20111023231947) do
|
|
35
35
|
add_index "delayed_jobs", ["priority", "run_at"], :name => "delayed_jobs_priority"
|
36
36
|
|
37
37
|
create_table "i_wonder_ab_test_goals", :force => true do |t|
|
38
|
-
t.
|
39
|
-
t.text
|
38
|
+
t.string "ab_test_sym"
|
39
|
+
t.text "options"
|
40
40
|
end
|
41
41
|
|
42
|
-
add_index "i_wonder_ab_test_goals", ["
|
42
|
+
add_index "i_wonder_ab_test_goals", ["ab_test_sym"], :name => "index_i_wonder_ab_test_goals_on_ab_test_sym"
|
43
43
|
|
44
44
|
create_table "i_wonder_ab_tests", :force => true do |t|
|
45
45
|
t.string "name"
|
@@ -114,7 +114,7 @@ ActiveRecord::Schema.define(:version => 20111023231947) do
|
|
114
114
|
end
|
115
115
|
|
116
116
|
create_table "i_wonder_test_group_memberships", :force => true do |t|
|
117
|
-
t.
|
117
|
+
t.string "ab_test_sym"
|
118
118
|
t.string "test_group_name"
|
119
119
|
t.string "member_type"
|
120
120
|
t.string "member_id"
|
@@ -122,7 +122,7 @@ ActiveRecord::Schema.define(:version => 20111023231947) do
|
|
122
122
|
t.datetime "updated_at"
|
123
123
|
end
|
124
124
|
|
125
|
-
add_index "i_wonder_test_group_memberships", ["
|
125
|
+
add_index "i_wonder_test_group_memberships", ["ab_test_sym"], :name => "index_i_wonder_test_group_memberships_on_ab_test_sym"
|
126
126
|
add_index "i_wonder_test_group_memberships", ["member_type", "member_id"], :name => "i_wonder_test_group_memberships_member"
|
127
127
|
add_index "i_wonder_test_group_memberships", ["test_group_name"], :name => "index_i_wonder_test_group_memberships_on_test_group_name"
|
128
128
|
|