Commit 492c3d63597701e331a22aad279e182c569f03a4
1 parent
15eb68fe
Exists in
master
and in
21 other branches
analytics: measure time on page
Showing
12 changed files
with
127 additions
and
14 deletions
Show diff stats
plugins/analytics/controllers/myprofile/analytics_plugin/stats_controller.rb
0 → 100644
... | ... | @@ -0,0 +1,21 @@ |
1 | +class AnalyticsPlugin::StatsController < MyProfileController | |
2 | + | |
3 | + no_design_blocks | |
4 | + | |
5 | + before_filter :skip_page_view | |
6 | + | |
7 | + def index | |
8 | + end | |
9 | + | |
10 | + protected | |
11 | + | |
12 | + def default_url_options | |
13 | + # avoid rails' use_relative_controller! | |
14 | + {use_route: '/'} | |
15 | + end | |
16 | + | |
17 | + def skip_page_view | |
18 | + @analytics_skip_page_view = true | |
19 | + end | |
20 | + | |
21 | +end | ... | ... |
plugins/analytics/lib/analytics_plugin.rb
plugins/analytics/lib/analytics_plugin/base.rb
... | ... | @@ -28,8 +28,7 @@ class AnalyticsPlugin::Base < Noosfero::Plugin |
28 | 28 | |
29 | 29 | unless profile.analytics_anonymous? |
30 | 30 | # FIXME: use session.id in Rails 4 |
31 | - session_id = Marshal.load(Base64.decode64 request['_session_id'])['session_id'] rescue nil | |
32 | - #session_id = request.session_options[:id] | |
31 | + session_id = request.session_options[:id] | |
33 | 32 | page_view.user = user |
34 | 33 | page_view.session_id = session_id |
35 | 34 | end |
... | ... | @@ -40,4 +39,12 @@ class AnalyticsPlugin::Base < Noosfero::Plugin |
40 | 39 | }] |
41 | 40 | end |
42 | 41 | |
42 | + def control_panel_buttons | |
43 | + { | |
44 | + title: I18n.t('analytics_plugin.lib.plugin.panel_button'), | |
45 | + icon: 'analytics-access', | |
46 | + url: {controller: 'analytics_plugin/stats', action: :index} | |
47 | + } | |
48 | + end | |
49 | + | |
43 | 50 | end | ... | ... |
plugins/analytics/lib/ext/profile.rb
... | ... | @@ -13,7 +13,7 @@ end |
13 | 13 | class Profile |
14 | 14 | |
15 | 15 | def analytics_settings attrs = {} |
16 | - @analytics_settings ||= Noosfero::Plugin::Settings.new self, AnalyticsPlugin, attrs | |
16 | + @analytics_settings ||= Noosfero::Plugin::Settings.new self, ::AnalyticsPlugin, attrs | |
17 | 17 | attrs.each{ |a, v| @analytics_settings.send "#{a}=", v } |
18 | 18 | @analytics_settings |
19 | 19 | end | ... | ... |
plugins/analytics/locales/en.yml
... | ... | @@ -5,6 +5,13 @@ en: &en |
5 | 5 | plugin: |
6 | 6 | name: 'Access tracking' |
7 | 7 | description: 'Register the access of selected profiles' |
8 | + panel_button: 'Access tracking' | |
9 | + | |
10 | + views: | |
11 | + stats: | |
12 | + user: 'User' | |
13 | + initial_time: 'Time' | |
14 | + pages: 'Pages' | |
8 | 15 | |
9 | 16 | en-US: |
10 | 17 | <<: *en | ... | ... |
plugins/analytics/locales/pt.yml
... | ... | @@ -5,6 +5,13 @@ pt: &pt |
5 | 5 | plugin: |
6 | 6 | name: 'Rastreio de accesso' |
7 | 7 | description: 'Registra o acesso de perfis selecionados' |
8 | + panel_button: 'Rastreio de accesso' | |
9 | + | |
10 | + views: | |
11 | + stats: | |
12 | + user: 'Usuário' | |
13 | + initial_time: 'Horário' | |
14 | + pages: 'Páginas' | |
8 | 15 | |
9 | 16 | pt-BR: |
10 | 17 | <<: *pt | ... | ... |
plugins/analytics/models/analytics_plugin/page_view.rb
... | ... | @@ -25,10 +25,24 @@ class AnalyticsPlugin::PageView < ActiveRecord::Base |
25 | 25 | before_validation :fill_referer_page_view, on: :create |
26 | 26 | before_validation :fill_visit, on: :create |
27 | 27 | |
28 | + scope :latest, -> { order 'request_started_at DESC' } | |
29 | + | |
28 | 30 | def request_duration |
29 | 31 | self.request_finished_at - self.request_started_at |
30 | 32 | end |
31 | 33 | |
34 | + def initial_time | |
35 | + self.page_loaded_at || self.request_finished_at | |
36 | + end | |
37 | + | |
38 | + def user_last_time_seen | |
39 | + self.initial_time + self.time_on_page | |
40 | + end | |
41 | + | |
42 | + def user_on_page? | |
43 | + Time.now < self.user_last_time_seen + AnalyticsPlugin::TimeOnPageUpdateInterval | |
44 | + end | |
45 | + | |
32 | 46 | def page_load! |
33 | 47 | self.page_loaded_at = Time.now |
34 | 48 | self.update_column :page_loaded_at, self.page_loaded_at |
... | ... | @@ -36,10 +50,9 @@ class AnalyticsPlugin::PageView < ActiveRecord::Base |
36 | 50 | |
37 | 51 | def increase_time_on_page! |
38 | 52 | now = Time.now |
39 | - initial_time = self.page_loaded_at || self.request_finished_at | |
40 | - return unless now > initial_time | |
53 | + return unless now > self.initial_time | |
41 | 54 | |
42 | - self.time_on_page = now - initial_time | |
55 | + self.time_on_page = now - self.initial_time | |
43 | 56 | self.update_column :time_on_page, self.time_on_page |
44 | 57 | end |
45 | 58 | |
... | ... | @@ -59,7 +72,7 @@ class AnalyticsPlugin::PageView < ActiveRecord::Base |
59 | 72 | end |
60 | 73 | |
61 | 74 | def fill_visit |
62 | - self.visit = self.referer_page_view.visit if self.referer_page_view | |
75 | + self.visit = self.referer_page_view.visit if self.referer_page_view and self.referer_page_view.user_on_page? | |
63 | 76 | self.visit ||= AnalyticsPlugin::Visit.new profile: profile |
64 | 77 | end |
65 | 78 | ... | ... |
plugins/analytics/models/analytics_plugin/visit.rb
... | ... | @@ -3,9 +3,17 @@ class AnalyticsPlugin::Visit < ActiveRecord::Base |
3 | 3 | attr_accessible *self.column_names |
4 | 4 | attr_accessible :profile |
5 | 5 | |
6 | - default_scope -> { includes :page_views } | |
7 | - | |
8 | 6 | belongs_to :profile |
9 | 7 | has_many :page_views, class_name: 'AnalyticsPlugin::PageView', dependent: :destroy |
10 | 8 | |
9 | + default_scope -> { joins(:page_views).includes :page_views } | |
10 | + | |
11 | + scope :latest, -> { order 'analytics_plugin_page_views.request_started_at DESC' } | |
12 | + | |
13 | + def first_page_view | |
14 | + self.page_views.first | |
15 | + end | |
16 | + | |
17 | + delegate :user, :initial_time, to: :first_page_view | |
18 | + | |
11 | 19 | end | ... | ... |
plugins/analytics/test/functional/content_viewer_controller_test.rb
... | ... | @@ -31,6 +31,8 @@ class ContentViewerControllerTest < ActionController::TestCase |
31 | 31 | |
32 | 32 | first_page_view = @community.page_views.order(:id).first |
33 | 33 | assert_equal @request.referer, first_page_view.referer_url |
34 | + assert_equal @user, first_page_view.user | |
35 | + assert first_page_view.request_duration > 0 and first_page_view.request_duration < 1 | |
34 | 36 | |
35 | 37 | @request.env['HTTP_REFERER'] = first_url |
36 | 38 | get :view_page, profile: @community.identifier, page: @community.articles.last.path.split('/') |
... | ... | @@ -40,9 +42,13 @@ class ContentViewerControllerTest < ActionController::TestCase |
40 | 42 | second_page_view = @community.page_views.order(:id).last |
41 | 43 | assert_equal first_page_view, second_page_view.referer_page_view |
42 | 44 | |
43 | - assert_equal @user, second_page_view.user | |
44 | - | |
45 | - assert second_page_view.request_duration > 0 and second_page_view.request_duration < 1 | |
45 | + # another visit, the referer is set but should be ignored because | |
46 | + # the user didn't report to be on the page until now | |
47 | + @request.env['HTTP_REFERER'] = first_url | |
48 | + future = Time.now + 2*AnalyticsPlugin::TimeOnPageUpdateInterval | |
49 | + Time.stubs(:now).returns(future) | |
50 | + get :view_page, profile: @community.identifier, page: @community.articles.last.path.split('/') | |
51 | + assert_equal 2, @community.visits.count | |
46 | 52 | end |
47 | 53 | |
48 | 54 | end | ... | ... |
plugins/analytics/views/analytics_plugin/_body_ending.html.slim
1 | 1 | javascript: |
2 | 2 | analytics.timeOnPage.baseUrl = #{url_for(controller: 'analytics_plugin/time_on_page').to_json} |
3 | - analytics.timeOnPage.updateInterval = #{AnalyticsPlugin::TimeOnPageUpdateInterval.to_json} | |
3 | + analytics.timeOnPage.updateInterval = #{AnalyticsPlugin::TimeOnPageUpdateIntervalMs.to_json} | |
4 | 4 | analytics.requestId = #{request.env['action_dispatch.request_id'].to_json} |
5 | 5 | analytics.init() |
6 | 6 | ... | ... |
plugins/analytics/views/analytics_plugin/stats/_table.html.slim
0 → 100644
... | ... | @@ -0,0 +1,38 @@ |
1 | + | |
2 | +table#analytics-stats.table data-toggle='table' data-striped='true' data-sortable='true' data-icons-prefix='fa' | |
3 | + thead | |
4 | + - unless profile.analytics_anonymous? | |
5 | + th= t'analytics_plugin.views.stats.user' | |
6 | + th= t'analytics_plugin.views.stats.initial_time' | |
7 | + th= t'analytics_plugin.views.stats.pages' | |
8 | + | |
9 | + tbody | |
10 | + - profile.visits.each do |visit| | |
11 | + tr | |
12 | + td= link_to visit.user.name, visit.user.url | |
13 | + td | |
14 | + div data-toggle="tooltip" data-title='#{l visit.initial_time}' | |
15 | + = time_ago_in_words(visit.initial_time) | |
16 | + |  | |
17 | + = _'ago' | |
18 | + td | |
19 | + - visit.page_views.each do |page_view| | |
20 | + = link_to page_view.url, page_view.url | |
21 | + | | |
22 | + = "(#{distance_of_time_in_words page_view.time_on_page})" | |
23 | + | -> | |
24 | + | |
25 | +javascript: | |
26 | + $('#analytics-stats').bootstrapTable({ | |
27 | + striped: true, | |
28 | + columns: [ | |
29 | + {sortable: true}, | |
30 | + {sortable: true}, | |
31 | + {sortable: true}, | |
32 | + ], | |
33 | + }) | |
34 | + | |
35 | + $(document).ready(function() { | |
36 | + $('[data-toggle="tooltip"]').tooltip() | |
37 | + }) | |
38 | + | ... | ... |
plugins/analytics/views/analytics_plugin/stats/index.html.slim
0 → 100644
... | ... | @@ -0,0 +1,5 @@ |
1 | +- content_for :head | |
2 | + = javascript_include_tag 'https://cdnjs.cloudflare.com/ajax/libs/bootstrap-table/1.8.1/bootstrap-table-all.min.js' | |
3 | + = stylesheet_link_tag 'https://cdnjs.cloudflare.com/ajax/libs/bootstrap-table/1.8.1/bootstrap-table.css' | |
4 | + | |
5 | += render 'table' | ... | ... |