metry 2.0.5 → 2.1.0
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/History.txt +4 -0
- data/Manifest.txt +12 -6
- data/Rakefile +5 -0
- data/TODO +1 -6
- data/example/example.rb +1 -1
- data/features/app_tracking.feature +1 -2
- data/features/basic_tracking.feature +19 -38
- data/features/psycho/dashboard.feature +3 -11
- data/features/psycho/experiments.feature +28 -0
- data/features/psycho/visitor_tracking.feature +4 -4
- data/features/step_definitions/experiments.rb +8 -0
- data/features/step_definitions/psycho.rb +14 -0
- data/features/step_definitions/tracking.rb +56 -24
- data/features/support/env.rb +0 -1
- data/features/support/helpers.rb +6 -0
- data/lib/metry.rb +12 -5
- data/lib/metry/cohort.rb +19 -0
- data/lib/metry/event.rb +26 -0
- data/lib/metry/experiment.rb +36 -13
- data/lib/metry/goal.rb +18 -0
- data/lib/metry/psycho.rb +24 -93
- data/lib/metry/psycho/dashboard.erb +5 -5
- data/lib/metry/psycho/edit_goal.erb +1 -1
- data/lib/metry/psycho/experiment.erb +25 -0
- data/lib/metry/psycho/new_goal.erb +1 -1
- data/lib/metry/psycho/visitor.erb +4 -1
- data/lib/metry/psycho/visitors.erb +6 -0
- data/lib/metry/rack/tracking.rb +19 -18
- data/lib/metry/visitor.rb +30 -0
- data/radiant/example/features/metry.feature +21 -43
- data/radiant/example/features/step_definitions/tracking.rb +56 -24
- data/radiant/example/features/step_definitions/web.rb +0 -7
- data/radiant/example/features/support/env.rb +7 -3
- data/radiant/example/features/support/helpers.rb +6 -0
- data/radiant/extension/lib/metry_tags.rb +3 -1
- data/test/shared.rb +3 -1
- data/test/test_experiment.rb +104 -0
- metadata +45 -9
- data/features/psycho/goals.feature +0 -53
- data/features/step_definitions/goals.rb +0 -3
- data/lib/metry/psycho/goal.erb +0 -9
- data/lib/metry/storage.rb +0 -142
- data/radiant/example/features/step_definitions/experiments.rb +0 -12
- data/test/test_storage.rb +0 -25
data/lib/metry/cohort.rb
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Metry
|
|
2
|
+
class Cohort
|
|
3
|
+
include MongoMapper::Document
|
|
4
|
+
|
|
5
|
+
key :name, String
|
|
6
|
+
key :visitor_ids, Array
|
|
7
|
+
|
|
8
|
+
belongs_to :experiment
|
|
9
|
+
|
|
10
|
+
def visitors
|
|
11
|
+
[(visitor_ids.empty? ? nil : Visitor.find(visitor_ids))].flatten
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def reached_goal(goal)
|
|
15
|
+
goal_visitor_ids = goal.visitors.collect{|e| e.id}
|
|
16
|
+
(goal_visitor_ids & visitor_ids).size.to_f/visitor_ids.size.to_f
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
data/lib/metry/event.rb
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Metry
|
|
2
|
+
class Event
|
|
3
|
+
include MongoMapper::Document
|
|
4
|
+
|
|
5
|
+
key :event, String
|
|
6
|
+
key :path, String
|
|
7
|
+
key :created_at, Time
|
|
8
|
+
key :ip, String
|
|
9
|
+
key :host, String
|
|
10
|
+
key :method, String
|
|
11
|
+
key :referrer, String
|
|
12
|
+
key :user_agent, String
|
|
13
|
+
key :status, String
|
|
14
|
+
key :extra, Hash
|
|
15
|
+
|
|
16
|
+
belongs_to :visitor, :class_name => "Metry::Visitor"
|
|
17
|
+
|
|
18
|
+
def ignore?
|
|
19
|
+
@ignore
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def ignore!
|
|
23
|
+
@ignore = true
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
data/lib/metry/experiment.rb
CHANGED
|
@@ -1,23 +1,46 @@
|
|
|
1
1
|
module Metry
|
|
2
2
|
class Experiment
|
|
3
|
+
include MongoMapper::Document
|
|
3
4
|
METHODS = {
|
|
4
|
-
"rand" => proc{|list,
|
|
5
|
-
"
|
|
5
|
+
"rand" => proc{|list, experiment| list.sort_by{rand}.first},
|
|
6
|
+
"sequential" => proc do |list, experiment|
|
|
7
|
+
r = list[experiment.counter%list.size]
|
|
8
|
+
experiment.counter += 1
|
|
9
|
+
experiment.save
|
|
10
|
+
r
|
|
11
|
+
end
|
|
6
12
|
}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
|
|
14
|
+
key :name, String
|
|
15
|
+
key :counter, Integer, :default => 0
|
|
16
|
+
|
|
17
|
+
many :cohorts, :class_name => 'Metry::Cohort', :foreign_key => 'experiment_id'
|
|
18
|
+
many :goals, :class_name => "Metry::Goal", :foreign_key => "experiment_id"
|
|
19
|
+
|
|
20
|
+
def self.find_or_create_by_name(name)
|
|
21
|
+
find(:first, :conditions => {:name => name}) || create(:name => name)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def lookup_cohort(name)
|
|
25
|
+
cohort = cohorts.find(:first, :conditions => {:name => name})
|
|
26
|
+
unless cohort
|
|
27
|
+
cohort = Cohort.create(:name => name)
|
|
28
|
+
cohorts << cohort
|
|
29
|
+
end
|
|
30
|
+
cohort
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def cohort_for(visitor)
|
|
34
|
+
cohorts.detect{|e| e.visitor_ids.include?(visitor.id)}
|
|
12
35
|
end
|
|
13
36
|
|
|
14
|
-
def choose(options, method
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
37
|
+
def choose(options, method, event, visitor)
|
|
38
|
+
cohort = cohort_for(visitor)
|
|
39
|
+
unless cohort
|
|
40
|
+
cohort = lookup_cohort(METHODS[method||'rand'][options, self])
|
|
41
|
+
visitor.in_cohort(cohort)
|
|
18
42
|
end
|
|
19
|
-
|
|
20
|
-
options[choice]
|
|
43
|
+
cohort.name
|
|
21
44
|
end
|
|
22
45
|
end
|
|
23
46
|
end
|
data/lib/metry/goal.rb
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Metry
|
|
2
|
+
class Goal
|
|
3
|
+
include MongoMapper::Document
|
|
4
|
+
|
|
5
|
+
key :name, String
|
|
6
|
+
key :path, String
|
|
7
|
+
|
|
8
|
+
belongs_to :experiment
|
|
9
|
+
|
|
10
|
+
def path_regexp
|
|
11
|
+
Regexp.new("^#{path}$")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def visitors
|
|
15
|
+
Event.find(:all, :conditions => {:path => path_regexp}).inject({}){|a,e| a[e.visitor_id] = e.visitor; a}.values
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
data/lib/metry/psycho.rb
CHANGED
|
@@ -14,7 +14,7 @@ module Metry
|
|
|
14
14
|
|
|
15
15
|
before do
|
|
16
16
|
if unescape(@request.path_info) =~ /^#{path}/
|
|
17
|
-
@env["metry.event"]
|
|
17
|
+
@env["metry.event"].ignore!
|
|
18
18
|
if !auth.call(env)
|
|
19
19
|
reply = on_deny.call(env)
|
|
20
20
|
status reply.first
|
|
@@ -25,41 +25,50 @@ module Metry
|
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
get "#{path}/?" do
|
|
28
|
-
@visitors = Visitor.all
|
|
29
|
-
@
|
|
28
|
+
@visitors = Visitor.find(:all, :limit => 10)
|
|
29
|
+
@experiments = Experiment.find(:all)
|
|
30
30
|
erb :dashboard
|
|
31
31
|
end
|
|
32
|
+
|
|
33
|
+
get "#{path}/visitors" do
|
|
34
|
+
@visitors = Visitor.find(:all)
|
|
35
|
+
end
|
|
32
36
|
|
|
33
37
|
get "#{path}/visitors/:id" do
|
|
34
38
|
@visitor = Visitor.find(params["id"])
|
|
35
39
|
erb :visitor
|
|
36
40
|
end
|
|
37
41
|
|
|
38
|
-
post "#{path}/goals" do
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
post "#{path}/experiments/:experiment_id/goals" do
|
|
43
|
+
experiment = Experiment.find(params[:experiment_id])
|
|
44
|
+
experiment.goals << Goal.create(params)
|
|
45
|
+
redirect url("/experiments/#{experiment.id}")
|
|
41
46
|
end
|
|
42
47
|
|
|
43
|
-
get "#{path}/goals/new" do
|
|
48
|
+
get "#{path}/experiments/:experiment_id/goals/new" do
|
|
49
|
+
@experiment = Experiment.find(params[:experiment_id])
|
|
44
50
|
erb :new_goal
|
|
45
51
|
end
|
|
46
52
|
|
|
47
|
-
get "#{path}/goals/:id" do
|
|
48
|
-
@
|
|
49
|
-
erb :goal
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
get "#{path}/goals/:id/edit" do
|
|
53
|
+
get "#{path}/experiments/:experiment_id/goals/:id/edit" do
|
|
54
|
+
@experiment = Experiment.find(params[:experiment_id])
|
|
53
55
|
@goal = Goal.find(params[:id])
|
|
54
56
|
erb :edit_goal
|
|
55
57
|
end
|
|
56
58
|
|
|
57
|
-
post "#{path}/goals/:id" do
|
|
59
|
+
post "#{path}/experiments/:experiment_id/goals/:id" do
|
|
58
60
|
goal = Goal.find(params[:id])
|
|
59
61
|
goal.name = params[:name]
|
|
60
62
|
goal.path = params[:path]
|
|
61
63
|
goal.save
|
|
62
|
-
redirect url("/
|
|
64
|
+
redirect url("/experiments/#{params[:experiment_id]}")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
get "#{path}/experiments/:id" do
|
|
68
|
+
@experiment = Experiment.find(params[:id])
|
|
69
|
+
@goals = @experiment.goals
|
|
70
|
+
@cohorts = @experiment.cohorts
|
|
71
|
+
erb :experiment
|
|
63
72
|
end
|
|
64
73
|
|
|
65
74
|
helpers do
|
|
@@ -72,83 +81,5 @@ module Metry
|
|
|
72
81
|
end
|
|
73
82
|
end
|
|
74
83
|
end
|
|
75
|
-
|
|
76
|
-
class Goal
|
|
77
|
-
def self.find(id)
|
|
78
|
-
new(Metry.current.goal(id))
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def self.create!(name, path)
|
|
82
|
-
new(Metry.current.add_goal('name' => name, 'path' => path))
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def self.all
|
|
86
|
-
Metry.current.goals.collect{|e| new(e)}
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
attr_accessor :path, :id, :name
|
|
90
|
-
|
|
91
|
-
def initialize(hash)
|
|
92
|
-
@id = hash['_id'].to_s
|
|
93
|
-
@name = hash['name']
|
|
94
|
-
@path = hash['path']
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def save
|
|
98
|
-
Metry.current.save_goal({'_id' => id, 'name' => name, 'path' => path})
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def path_regexp
|
|
102
|
-
Regexp.new("^#{path}$")
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def visits
|
|
106
|
-
events.size
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def events
|
|
110
|
-
Metry.current.all_events({'path' => path_regexp})
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def visitors
|
|
114
|
-
events.collect{|e| e['visitor']}.uniq.collect{|e| Visitor.find(e)}
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
class Visitor
|
|
119
|
-
def self.all
|
|
120
|
-
Metry.current.visitors.collect{|e| new(e)}.select{|e| e.has_events?}
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
def self.find(id)
|
|
124
|
-
new(Metry.current.visitor(id))
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
def initialize(visitor)
|
|
128
|
-
@visitor = visitor
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
def id
|
|
132
|
-
@visitor['_id'].to_s
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
def events
|
|
136
|
-
Metry.current.events_for(id).collect{|e| Event.new(e)}
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
def has_events?
|
|
140
|
-
!events.empty?
|
|
141
|
-
end
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
class Event
|
|
145
|
-
def initialize(event)
|
|
146
|
-
@event = event
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
def path
|
|
150
|
-
@event["path"]
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
84
|
end
|
|
154
85
|
end
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
<h1>
|
|
2
|
-
<ul id="
|
|
3
|
-
<% @
|
|
4
|
-
<li class="
|
|
1
|
+
<h1>Experiments</h1>
|
|
2
|
+
<ul id="experiments">
|
|
3
|
+
<% @experiments.each do |experiment| %>
|
|
4
|
+
<li class="experiment"><a href="<%= url "/experiments/#{experiment.id}" %>"><%= experiment.name %></a></li>
|
|
5
5
|
<% end %>
|
|
6
6
|
</ul>
|
|
7
|
-
<p><a href="<%= url "/goals/new" %>">New Goal</a></p>
|
|
8
7
|
|
|
9
8
|
<h1>Recent Visitors</h1>
|
|
10
9
|
<ul>
|
|
@@ -12,3 +11,4 @@
|
|
|
12
11
|
<li><a href="<%= url "/visitors/#{v.id}" %>">Visitor <%= v.id %></a> (<%= v.events.size %> events)</li>
|
|
13
12
|
<% end %>
|
|
14
13
|
</ul>
|
|
14
|
+
<p><a href="<%= url "/visitors" %>">More</a></p>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<h1>Edit Goal</h1>
|
|
2
2
|
|
|
3
|
-
<form action="<%= url "/goals/#{@goal.id}" %>" method="post">
|
|
3
|
+
<form action="<%= url "/experiments/#{@experiment.id}/goals/#{@goal.id}" %>" method="post">
|
|
4
4
|
<fieldset>
|
|
5
5
|
<p><label for="name">Name</label> <input type="text" name="name" value="<%= @goal.name %>"/></p>
|
|
6
6
|
<p><label for="path">Path</label> <input type="text" name="path" value="<%= @goal.path %>"/></p>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<h1>Experiment <%= @experiment.name %></h1>
|
|
2
|
+
|
|
3
|
+
<% @goals.each do |goal| %>
|
|
4
|
+
<p>Name: <%= goal.name %></p>
|
|
5
|
+
<p>Path: <%=h goal.path %></p>
|
|
6
|
+
<p><a href="<%= url "/experiments/#{@experiment.id}/goals/#{goal.id}/edit" %>">Edit</a></p>
|
|
7
|
+
<% end %>
|
|
8
|
+
<p><a href="<%= url "/experiments/#{@experiment.id}/goals/new" %>">New Goal</a></p>
|
|
9
|
+
|
|
10
|
+
<table>
|
|
11
|
+
<tr>
|
|
12
|
+
<th>Goal</th>
|
|
13
|
+
<% @cohorts.each do |cohort| %>
|
|
14
|
+
<th><%= cohort.name %></th>
|
|
15
|
+
<% end %>
|
|
16
|
+
</tr>
|
|
17
|
+
<% @goals.each do |goal| %>
|
|
18
|
+
<tr>
|
|
19
|
+
<td><%= goal.name %></td>
|
|
20
|
+
<% @cohorts.each do |cohort| %>
|
|
21
|
+
<td><%= (cohort.reached_goal(goal)*100).to_i %>%</td>
|
|
22
|
+
<% end %>
|
|
23
|
+
</tr>
|
|
24
|
+
<% end %>
|
|
25
|
+
</table>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<h1>New Goal</h1>
|
|
2
2
|
|
|
3
|
-
<form action="<%= url "/goals" %>" method="post">
|
|
3
|
+
<form action="<%= url "/experiments/#{@experiment.id}/goals" %>" method="post">
|
|
4
4
|
<fieldset>
|
|
5
5
|
<p><label for="name">Name</label> <input type="text" name="name" /></p>
|
|
6
6
|
<p><label for="path">Path</label> <input type="text" name="path" /></p>
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
<h1>Visitor <%= @visitor.id %></h1>
|
|
2
2
|
|
|
3
|
+
<p>Last IP: <%= @visitor.last_ip %></p>
|
|
4
|
+
<p>User agent: <%= @visitor.user_agent %></p>
|
|
5
|
+
|
|
3
6
|
<ol>
|
|
4
7
|
<% @visitor.events.each do |event| %>
|
|
5
|
-
<li><%= event.path %></li>
|
|
8
|
+
<li><%= event.path %> from <%= event.referrer %></li>
|
|
6
9
|
<% end %>
|
|
7
10
|
</ol>
|
data/lib/metry/rack/tracking.rb
CHANGED
|
@@ -5,14 +5,12 @@ module Metry
|
|
|
5
5
|
|
|
6
6
|
def initialize(app)
|
|
7
7
|
@app = app
|
|
8
|
-
@storage = Metry.current
|
|
9
8
|
end
|
|
10
9
|
|
|
11
10
|
def call(env)
|
|
12
11
|
request = ::Rack::Request.new(env)
|
|
13
|
-
visitor = find_or_create_visitor(request)
|
|
14
|
-
env["metry.visitor"] = visitor
|
|
15
12
|
env["metry.event"] = event = build_event(request)
|
|
13
|
+
env["metry.visitor"] = visitor = find_or_create_visitor(request)
|
|
16
14
|
|
|
17
15
|
status, headers, body = @app.call(env)
|
|
18
16
|
|
|
@@ -20,34 +18,37 @@ module Metry
|
|
|
20
18
|
|
|
21
19
|
save_visitor(response, visitor)
|
|
22
20
|
|
|
23
|
-
unless event
|
|
24
|
-
event
|
|
25
|
-
event
|
|
26
|
-
|
|
21
|
+
unless event.ignore?
|
|
22
|
+
event.visitor = visitor
|
|
23
|
+
event.status = status.to_s
|
|
24
|
+
event.save
|
|
27
25
|
end
|
|
28
26
|
|
|
29
27
|
response.to_a
|
|
30
28
|
end
|
|
31
29
|
|
|
32
30
|
def build_event(request)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"user_agent" => request.env["HTTP_USER_AGENT"] }
|
|
31
|
+
Event.new("event" => "pageview",
|
|
32
|
+
:path => request.fullpath,
|
|
33
|
+
:ip => request.ip,
|
|
34
|
+
:host => request.host,
|
|
35
|
+
:method => request.request_method,
|
|
36
|
+
:referrer => request.env["HTTP_REFERER"],
|
|
37
|
+
:user_agent => request.env["HTTP_USER_AGENT"])
|
|
41
38
|
end
|
|
42
39
|
|
|
43
40
|
def find_or_create_visitor(request)
|
|
44
|
-
|
|
41
|
+
visitor = nil
|
|
42
|
+
if id = request.cookies[COOKIE]
|
|
43
|
+
visitor = Visitor.find(id)
|
|
44
|
+
end
|
|
45
|
+
(visitor || Visitor.create)
|
|
45
46
|
end
|
|
46
47
|
|
|
47
48
|
def save_visitor(response, visitor)
|
|
48
|
-
|
|
49
|
+
visitor.save
|
|
49
50
|
response.set_cookie(COOKIE,
|
|
50
|
-
:value => visitor
|
|
51
|
+
:value => visitor.id.to_s,
|
|
51
52
|
:expires => (Time.now+(60*60*24*365*20)),
|
|
52
53
|
:path => '/')
|
|
53
54
|
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module Metry
|
|
2
|
+
class Visitor
|
|
3
|
+
include MongoMapper::Document
|
|
4
|
+
|
|
5
|
+
key :created_at, Time
|
|
6
|
+
key :updated_at, Time
|
|
7
|
+
key :cohort_ids, Array
|
|
8
|
+
|
|
9
|
+
many :events, :class_name => "Metry::Event", :foreign_key => "visitor_id"
|
|
10
|
+
|
|
11
|
+
def last_ip
|
|
12
|
+
(events.last ? events.last.ip : nil)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def user_agent
|
|
16
|
+
(events.last ? events.last.user_agent : nil)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def in_cohort(cohort)
|
|
20
|
+
cohort_ids << cohort.id
|
|
21
|
+
cohort.visitor_ids << self.id
|
|
22
|
+
save
|
|
23
|
+
cohort.save
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def cohorts
|
|
27
|
+
[(cohort_ids.empty? ? nil : Cohort.find(cohort_ids))].flatten
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|