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
|