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.
Files changed (44) hide show
  1. data/History.txt +4 -0
  2. data/Manifest.txt +12 -6
  3. data/Rakefile +5 -0
  4. data/TODO +1 -6
  5. data/example/example.rb +1 -1
  6. data/features/app_tracking.feature +1 -2
  7. data/features/basic_tracking.feature +19 -38
  8. data/features/psycho/dashboard.feature +3 -11
  9. data/features/psycho/experiments.feature +28 -0
  10. data/features/psycho/visitor_tracking.feature +4 -4
  11. data/features/step_definitions/experiments.rb +8 -0
  12. data/features/step_definitions/psycho.rb +14 -0
  13. data/features/step_definitions/tracking.rb +56 -24
  14. data/features/support/env.rb +0 -1
  15. data/features/support/helpers.rb +6 -0
  16. data/lib/metry.rb +12 -5
  17. data/lib/metry/cohort.rb +19 -0
  18. data/lib/metry/event.rb +26 -0
  19. data/lib/metry/experiment.rb +36 -13
  20. data/lib/metry/goal.rb +18 -0
  21. data/lib/metry/psycho.rb +24 -93
  22. data/lib/metry/psycho/dashboard.erb +5 -5
  23. data/lib/metry/psycho/edit_goal.erb +1 -1
  24. data/lib/metry/psycho/experiment.erb +25 -0
  25. data/lib/metry/psycho/new_goal.erb +1 -1
  26. data/lib/metry/psycho/visitor.erb +4 -1
  27. data/lib/metry/psycho/visitors.erb +6 -0
  28. data/lib/metry/rack/tracking.rb +19 -18
  29. data/lib/metry/visitor.rb +30 -0
  30. data/radiant/example/features/metry.feature +21 -43
  31. data/radiant/example/features/step_definitions/tracking.rb +56 -24
  32. data/radiant/example/features/step_definitions/web.rb +0 -7
  33. data/radiant/example/features/support/env.rb +7 -3
  34. data/radiant/example/features/support/helpers.rb +6 -0
  35. data/radiant/extension/lib/metry_tags.rb +3 -1
  36. data/test/shared.rb +3 -1
  37. data/test/test_experiment.rb +104 -0
  38. metadata +45 -9
  39. data/features/psycho/goals.feature +0 -53
  40. data/features/step_definitions/goals.rb +0 -3
  41. data/lib/metry/psycho/goal.erb +0 -9
  42. data/lib/metry/storage.rb +0 -142
  43. data/radiant/example/features/step_definitions/experiments.rb +0 -12
  44. data/test/test_storage.rb +0 -25
@@ -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
@@ -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
@@ -1,23 +1,46 @@
1
1
  module Metry
2
2
  class Experiment
3
+ include MongoMapper::Document
3
4
  METHODS = {
4
- "rand" => proc{|list, visitor| list.sort_by{rand}.first},
5
- "mod_visitor" => proc{|list, visitor| list[(visitor['_id'].to_i-1)%list.size]}, # mod_visitor will only work with predictable_keys = true
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
- def initialize(name, event, visitor)
9
- @key = "experiment:#{name}"
10
- @event = event
11
- @visitor = visitor
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=nil)
15
- unless choice = @visitor[@key]
16
- choice = METHODS[method || "rand"][options.keys, @visitor]
17
- @visitor[@key] = choice
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
- @event[@key] = choice
20
- options[choice]
43
+ cohort.name
21
44
  end
22
45
  end
23
46
  end
@@ -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
@@ -14,7 +14,7 @@ module Metry
14
14
 
15
15
  before do
16
16
  if unescape(@request.path_info) =~ /^#{path}/
17
- @env["metry.event"]["ignore"] = "true"
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
- @goals = Goal.all
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
- goal = Goal.create!(params[:name], params[:path])
40
- redirect url("/goals/#{goal.id}")
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
- @goal = Goal.find(params[:id])
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("/goals/#{goal.id}")
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>Goals</h1>
2
- <ul id="goals">
3
- <% @goals.each do |goal| %>
4
- <li class="goal"><a href="<%= url "/goals/#{goal.id}" %>"><%= goal.name %></a>: <%= goal.visits %> visits</li>
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>
@@ -0,0 +1,6 @@
1
+ <h1>Visitors</h1>
2
+ <ul>
3
+ <% @visitors.each do |v| %>
4
+ <li><a href="<%= url "/visitors/#{v.id}" %>">Visitor <%= v.id %></a> (<%= v.events.size %> events)</li>
5
+ <% end %>
6
+ </ul>
@@ -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["ignore"] == "true"
24
- event["status"] = status.to_s
25
- event["visitor"] = visitor['_id'].to_s
26
- @storage.add_event(event)
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
- { "event" => "pageview",
34
- "path" => request.fullpath,
35
- "time" => Time.now.to_f,
36
- "ip" => request.ip,
37
- "host" => request.host,
38
- "method" => request.request_method,
39
- "referrer" => request.env["HTTP_REFERER"],
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
- @storage.visitor(request.cookies[COOKIE]) || @storage.new_visitor
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
- @storage.save_visitor(visitor)
49
+ visitor.save
49
50
  response.set_cookie(COOKIE,
50
- :value => visitor['_id'].to_s,
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