Commit 71f53f042084b44ff8848630df439630c282e0cc
Exists in
master
and in
29 other branches
Merge branch 'analytics-plugin' into 'master'
Analytics plugin See merge request !632
Showing
15 changed files
with
489 additions
and
0 deletions
Show diff stats
... | ... | @@ -0,0 +1,95 @@ |
1 | +# based on https://github.com/discourse/discourse/blob/master/lib/scheduler/defer.rb | |
2 | + | |
3 | +module Scheduler | |
4 | + module Deferrable | |
5 | + def initialize | |
6 | + # FIXME: do some other way when not using Unicorn | |
7 | + @async = (not Rails.env.test?) and defined? Unicorn | |
8 | + @queue = Queue.new | |
9 | + @mutex = Mutex.new | |
10 | + @paused = false | |
11 | + @thread = nil | |
12 | + end | |
13 | + | |
14 | + def pause | |
15 | + stop! | |
16 | + @paused = true | |
17 | + end | |
18 | + | |
19 | + def resume | |
20 | + @paused = false | |
21 | + end | |
22 | + | |
23 | + # for test | |
24 | + def async= val | |
25 | + @async = val | |
26 | + end | |
27 | + | |
28 | + def later desc = nil, &blk | |
29 | + if @async | |
30 | + start_thread unless (@thread && @thread.alive?) || @paused | |
31 | + @queue << [blk, desc] | |
32 | + else | |
33 | + blk.call | |
34 | + end | |
35 | + end | |
36 | + | |
37 | + def stop! | |
38 | + @thread.kill if @thread and @thread.alive? | |
39 | + @thread = nil | |
40 | + end | |
41 | + | |
42 | + # test only | |
43 | + def stopped? | |
44 | + !(@thread and @thread.alive?) | |
45 | + end | |
46 | + | |
47 | + def do_all_work | |
48 | + while !@queue.empty? | |
49 | + do_work _non_block=true | |
50 | + end | |
51 | + end | |
52 | + | |
53 | + private | |
54 | + | |
55 | + def start_thread | |
56 | + @mutex.synchronize do | |
57 | + return if @thread && @thread.alive? | |
58 | + @thread = Thread.new do | |
59 | + while true | |
60 | + do_work | |
61 | + end | |
62 | + end | |
63 | + @thread.priority = -2 | |
64 | + end | |
65 | + end | |
66 | + | |
67 | + # using non_block to match Ruby #deq | |
68 | + def do_work non_block=false | |
69 | + job, desc = @queue.deq non_block | |
70 | + begin | |
71 | + job.call | |
72 | + rescue => ex | |
73 | + ExceptionNotifier.notify_exception ex, message: "Running deferred code '#{desc}'" | |
74 | + end | |
75 | + rescue => ex | |
76 | + ExceptionNotifier.notify_exception ex, message: "Processing deferred code queue" | |
77 | + end | |
78 | + end | |
79 | + | |
80 | + class Defer | |
81 | + | |
82 | + module Unicorn | |
83 | + def process_client client | |
84 | + ::Scheduler::Defer.pause | |
85 | + super client | |
86 | + ::Scheduler::Defer.do_all_work | |
87 | + ::Scheduler::Defer.resume | |
88 | + end | |
89 | + end | |
90 | + | |
91 | + extend Deferrable | |
92 | + initialize | |
93 | + end | |
94 | + | |
95 | +end | ... | ... |
plugins/analytics/controllers/profile/analytics_plugin/time_on_page_controller.rb
0 → 100644
... | ... | @@ -0,0 +1,30 @@ |
1 | +class AnalyticsPlugin::TimeOnPageController < ProfileController | |
2 | + | |
3 | + before_filter :skip_page_view | |
4 | + | |
5 | + def page_load | |
6 | + # to avoid concurrency problems with the original deferred request, also defer this | |
7 | + Scheduler::Defer.later do | |
8 | + page_view = profile.page_views.where(request_id: params[:id]).first | |
9 | + page_view.request = request | |
10 | + page_view.page_load! | |
11 | + end | |
12 | + | |
13 | + render nothing: true | |
14 | + end | |
15 | + | |
16 | + def report | |
17 | + page_view = profile.page_views.where(request_id: params[:id]).first | |
18 | + page_view.request = request | |
19 | + page_view.increase_time_on_page! | |
20 | + | |
21 | + render nothing: true | |
22 | + end | |
23 | + | |
24 | + protected | |
25 | + | |
26 | + def skip_page_view | |
27 | + @analytics_skip_page_view = true | |
28 | + end | |
29 | + | |
30 | +end | ... | ... |
plugins/analytics/db/migrate/20150715001149_init_analytics_plugin.rb
0 → 100644
... | ... | @@ -0,0 +1,47 @@ |
1 | +class InitAnalyticsPlugin < ActiveRecord::Migration | |
2 | + | |
3 | + def up | |
4 | + create_table :analytics_plugin_visits do |t| | |
5 | + t.integer :profile_id | |
6 | + end | |
7 | + | |
8 | + create_table :analytics_plugin_page_views do |t| | |
9 | + t.string :type | |
10 | + t.integer :visit_id | |
11 | + t.integer :track_id | |
12 | + t.integer :referer_page_view_id | |
13 | + t.string :request_id | |
14 | + | |
15 | + t.integer :user_id | |
16 | + t.integer :session_id | |
17 | + t.integer :profile_id | |
18 | + | |
19 | + t.text :url | |
20 | + t.text :referer_url | |
21 | + | |
22 | + t.text :user_agent | |
23 | + t.string :remote_ip | |
24 | + | |
25 | + t.datetime :request_started_at | |
26 | + t.datetime :request_finished_at | |
27 | + t.datetime :page_loaded_at | |
28 | + t.integer :time_on_page, default: 0 | |
29 | + | |
30 | + t.text :data, default: {}.to_yaml | |
31 | + end | |
32 | + add_index :analytics_plugin_page_views, :request_id | |
33 | + add_index :analytics_plugin_page_views, :referer_page_view_id | |
34 | + | |
35 | + add_index :analytics_plugin_page_views, :user_id | |
36 | + add_index :analytics_plugin_page_views, :session_id | |
37 | + add_index :analytics_plugin_page_views, :profile_id | |
38 | + add_index :analytics_plugin_page_views, :url | |
39 | + add_index :analytics_plugin_page_views, [:user_id, :session_id, :profile_id, :url], name: :analytics_plugin_referer_find | |
40 | + end | |
41 | + | |
42 | + def down | |
43 | + drop_table :analytics_plugin_visits | |
44 | + drop_table :analytics_plugin_page_views | |
45 | + end | |
46 | + | |
47 | +end | ... | ... |
... | ... | @@ -0,0 +1,15 @@ |
1 | +module AnalyticsPlugin | |
2 | + | |
3 | + TimeOnPageUpdateInterval = 2.minutes * 1000 | |
4 | + | |
5 | + extend Noosfero::Plugin::ParentMethods | |
6 | + | |
7 | + def self.plugin_name | |
8 | + I18n.t'analytics_plugin.lib.plugin.name' | |
9 | + end | |
10 | + | |
11 | + def self.plugin_description | |
12 | + I18n.t'analytics_plugin.lib.plugin.description' | |
13 | + end | |
14 | + | |
15 | +end | ... | ... |
... | ... | @@ -0,0 +1,43 @@ |
1 | + | |
2 | +class AnalyticsPlugin::Base < Noosfero::Plugin | |
3 | + | |
4 | + def body_ending | |
5 | + return unless profile and profile.analytics_enabled? | |
6 | + lambda do | |
7 | + render 'analytics_plugin/body_ending' | |
8 | + end | |
9 | + end | |
10 | + | |
11 | + def js_files | |
12 | + ['analytics'].map{ |j| "javascripts/#{j}" } | |
13 | + end | |
14 | + | |
15 | + def application_controller_filters | |
16 | + [{ | |
17 | + type: 'around_filter', options: {}, block: -> &block do | |
18 | + request_started_at = Time.now | |
19 | + block.call | |
20 | + request_finished_at = Time.now | |
21 | + | |
22 | + return if @analytics_skip_page_view | |
23 | + return unless profile and profile.analytics_enabled? | |
24 | + | |
25 | + Scheduler::Defer.later 'analytics: register page view' do | |
26 | + page_view = profile.page_views.build request: request, profile_id: profile, | |
27 | + request_started_at: request_started_at, request_finished_at: request_finished_at | |
28 | + | |
29 | + unless profile.analytics_anonymous? | |
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] | |
33 | + page_view.user = user | |
34 | + page_view.session_id = session_id | |
35 | + end | |
36 | + | |
37 | + page_view.save! | |
38 | + end | |
39 | + end, | |
40 | + }] | |
41 | + end | |
42 | + | |
43 | +end | ... | ... |
... | ... | @@ -0,0 +1,30 @@ |
1 | +require_dependency 'profile' | |
2 | +require_dependency 'community' | |
3 | + | |
4 | +([Profile] + Profile.descendants).each do |subclass| | |
5 | +subclass.class_eval do | |
6 | + | |
7 | + has_many :visits, foreign_key: :profile_id, class_name: 'AnalyticsPlugin::Visit' | |
8 | + has_many :page_views, foreign_key: :profile_id, class_name: 'AnalyticsPlugin::PageView' | |
9 | + | |
10 | +end | |
11 | +end | |
12 | + | |
13 | +class Profile | |
14 | + | |
15 | + def analytics_settings attrs = {} | |
16 | + @analytics_settings ||= Noosfero::Plugin::Settings.new self, AnalyticsPlugin, attrs | |
17 | + attrs.each{ |a, v| @analytics_settings.send "#{a}=", v } | |
18 | + @analytics_settings | |
19 | + end | |
20 | + alias_method :analytics_settings=, :analytics_settings | |
21 | + | |
22 | + def analytics_enabled? | |
23 | + self.analytics_settings.enabled | |
24 | + end | |
25 | + | |
26 | + def analytics_anonymous? | |
27 | + self.analytics_settings.anonymous | |
28 | + end | |
29 | + | |
30 | +end | ... | ... |
... | ... | @@ -0,0 +1,67 @@ |
1 | +class AnalyticsPlugin::PageView < ActiveRecord::Base | |
2 | + | |
3 | + serialize :data | |
4 | + | |
5 | + attr_accessible *self.column_names | |
6 | + attr_accessible :user, :profile | |
7 | + | |
8 | + attr_accessor :request | |
9 | + attr_accessible :request | |
10 | + | |
11 | + acts_as_having_settings field: :options | |
12 | + | |
13 | + belongs_to :visit, class_name: 'AnalyticsPlugin::Visit' | |
14 | + belongs_to :referer_page_view, class_name: 'AnalyticsPlugin::PageView' | |
15 | + | |
16 | + belongs_to :user, class_name: 'Person' | |
17 | + belongs_to :session, primary_key: :session_id, foreign_key: :session_id, class_name: 'Session' | |
18 | + belongs_to :profile | |
19 | + | |
20 | + validates_presence_of :visit | |
21 | + validates_presence_of :request, on: :create | |
22 | + validates_presence_of :url | |
23 | + | |
24 | + before_validation :extract_request_data, on: :create | |
25 | + before_validation :fill_referer_page_view, on: :create | |
26 | + before_validation :fill_visit, on: :create | |
27 | + | |
28 | + def request_duration | |
29 | + self.request_finished_at - self.request_started_at | |
30 | + end | |
31 | + | |
32 | + def page_load! | |
33 | + self.page_loaded_at = Time.now | |
34 | + self.update_column :page_loaded_at, self.page_loaded_at | |
35 | + end | |
36 | + | |
37 | + def increase_time_on_page! | |
38 | + now = Time.now | |
39 | + initial_time = self.page_loaded_at || self.request_finished_at | |
40 | + return unless now > initial_time | |
41 | + | |
42 | + self.time_on_page = now - initial_time | |
43 | + self.update_column :time_on_page, self.time_on_page | |
44 | + end | |
45 | + | |
46 | + protected | |
47 | + | |
48 | + def extract_request_data | |
49 | + self.url = self.request.url.sub /\/+$/, '' | |
50 | + self.referer_url = self.request.referer | |
51 | + self.user_agent = self.request.headers['User-Agent'] | |
52 | + self.request_id = self.request.env['action_dispatch.request_id'] | |
53 | + self.remote_ip = self.request.remote_ip | |
54 | + end | |
55 | + | |
56 | + def fill_referer_page_view | |
57 | + self.referer_page_view = AnalyticsPlugin::PageView.order('request_started_at DESC'). | |
58 | + where(url: self.referer_url, session_id: self.session_id, user_id: self.user_id, profile_id: self.profile_id).first if self.referer_url.present? | |
59 | + end | |
60 | + | |
61 | + def fill_visit | |
62 | + self.visit = self.referer_page_view.visit if self.referer_page_view | |
63 | + self.visit ||= AnalyticsPlugin::Visit.new profile: profile | |
64 | + end | |
65 | + | |
66 | +end | |
67 | + | ... | ... |
... | ... | @@ -0,0 +1,11 @@ |
1 | +class AnalyticsPlugin::Visit < ActiveRecord::Base | |
2 | + | |
3 | + attr_accessible *self.column_names | |
4 | + attr_accessible :profile | |
5 | + | |
6 | + default_scope -> { includes :page_views } | |
7 | + | |
8 | + belongs_to :profile | |
9 | + has_many :page_views, class_name: 'AnalyticsPlugin::PageView', dependent: :destroy | |
10 | + | |
11 | +end | ... | ... |
... | ... | @@ -0,0 +1,29 @@ |
1 | +# translation of analytic.po to portuguese | |
2 | +# Krishnamurti Lelis Lima Vieira Nunes <krishna@colivre.coop.br>, 2007. | |
3 | +# noosfero - Brazilian Portuguese translation | |
4 | +# Copyright (C) 2007, | |
5 | +# Forum Brasileiro de Economia Solidaria <http://www.fbes.org.br/> | |
6 | +# Copyright (C) 2007, | |
7 | +# Ynternet.org Foundation <http://www.ynternet.org/> | |
8 | +# This file is distributed under the same license as noosfero itself. | |
9 | +# Joenio Costa <joenio@colivre.coop.br>, 2008. | |
10 | +# | |
11 | +# | |
12 | +msgid "" | |
13 | +msgstr "" | |
14 | +"Project-Id-Version: 1.0-690-gcb6e853\n" | |
15 | +"POT-Creation-Date: 2015-03-05 12:10-0300\n" | |
16 | +"PO-Revision-Date: 2015-07-21 09:23-0300\n" | |
17 | +"Last-Translator: Michal Čihař <michal@cihar.com>\n" | |
18 | +"Language-Team: Portuguese <https://hosted.weblate.org/projects/noosfero" | |
19 | +"/plugin-solr/pt/>\n" | |
20 | +"Language: pt\n" | |
21 | +"MIME-Version: 1.0\n" | |
22 | +"Content-Type: text/plain; charset=UTF-8\n" | |
23 | +"Content-Transfer-Encoding: 8bit\n" | |
24 | +"Plural-Forms: nplurals=2; plural=n != 1;\n" | |
25 | +"X-Generator: Weblate 2.3-dev\n" | |
26 | + | |
27 | +msgid "Select the set of communities and users to track" | |
28 | +msgstr "Seleciona o conjunto de comunidades e usuários para rastrear" | |
29 | + | ... | ... |
... | ... | @@ -0,0 +1,39 @@ |
1 | +analytics = { | |
2 | + requestId: '', | |
3 | + | |
4 | + timeOnPage: { | |
5 | + updateInterval: 0, | |
6 | + baseUrl: '', | |
7 | + | |
8 | + report: function() { | |
9 | + $.ajax(analytics.timeOnPage.baseUrl+'/report', { | |
10 | + type: 'POST', data: {id: analytics.requestId}, | |
11 | + success: function(data) { | |
12 | + | |
13 | + analytics.timeOnPage.poll() | |
14 | + }, | |
15 | + }) | |
16 | + }, | |
17 | + | |
18 | + poll: function() { | |
19 | + if (analytics.timeOnPage.updateInterval) | |
20 | + setTimeout(analytics.timeOnPage.report, analytics.timeOnPage.updateInterval) | |
21 | + }, | |
22 | + }, | |
23 | + | |
24 | + init: function() { | |
25 | + analytics.timeOnPage.poll() | |
26 | + }, | |
27 | + | |
28 | + pageLoad: function() { | |
29 | + $.ajax(analytics.timeOnPage.baseUrl+'/page_load', { | |
30 | + type: 'POST', data: {id: analytics.requestId}, | |
31 | + success: function(data) { | |
32 | + }, | |
33 | + }); | |
34 | + } | |
35 | + | |
36 | +}; | |
37 | + | |
38 | +$(document).ready(analytics.pageLoad) | |
39 | + | ... | ... |
plugins/analytics/test/functional/content_viewer_controller_test.rb
0 → 100644
... | ... | @@ -0,0 +1,48 @@ |
1 | +require 'test_helper' | |
2 | +require 'content_viewer_controller' | |
3 | + | |
4 | +class ContentViewerControllerTest < ActionController::TestCase | |
5 | + | |
6 | + def setup | |
7 | + @controller = ContentViewerController.new | |
8 | + @request = ActionController::TestRequest.new | |
9 | + @response = ActionController::TestResponse.new | |
10 | + | |
11 | + @environment = Environment.default | |
12 | + @environment.enabled_plugins += ['AnalyticsPlugin'] | |
13 | + @environment.save! | |
14 | + | |
15 | + @user = create_user('testinguser').person | |
16 | + login_as @user.identifier | |
17 | + | |
18 | + @community = build Community, identifier: 'testcomm', name: 'test' | |
19 | + @community.analytics_settings.enabled = true | |
20 | + @community.analytics_settings.anonymous = false | |
21 | + @community.save! | |
22 | + @community.add_member @user | |
23 | + end | |
24 | + | |
25 | + should 'register page view correctly' do | |
26 | + @request.env['HTTP_REFERER'] = 'http://google.com' | |
27 | + first_url = 'http://test.host' | |
28 | + get :view_page, profile: @community.identifier, page: [] | |
29 | + assert_equal 1, @community.page_views.count | |
30 | + assert_equal 1, @community.visits.count | |
31 | + | |
32 | + first_page_view = @community.page_views.order(:id).first | |
33 | + assert_equal @request.referer, first_page_view.referer_url | |
34 | + | |
35 | + @request.env['HTTP_REFERER'] = first_url | |
36 | + get :view_page, profile: @community.identifier, page: @community.articles.last.path.split('/') | |
37 | + assert_equal 2, @community.page_views.count | |
38 | + assert_equal 1, @community.visits.count | |
39 | + | |
40 | + second_page_view = @community.page_views.order(:id).last | |
41 | + assert_equal first_page_view, second_page_view.referer_page_view | |
42 | + | |
43 | + assert_equal @user, second_page_view.user | |
44 | + | |
45 | + assert second_page_view.request_duration > 0 and second_page_view.request_duration < 1 | |
46 | + end | |
47 | + | |
48 | +end | ... | ... |
plugins/analytics/views/analytics_plugin/_body_ending.html.slim
0 → 100644
... | ... | @@ -0,0 +1,6 @@ |
1 | +javascript: | |
2 | + analytics.timeOnPage.baseUrl = #{url_for(controller: 'analytics_plugin/time_on_page').to_json} | |
3 | + analytics.timeOnPage.updateInterval = #{AnalyticsPlugin::TimeOnPageUpdateInterval.to_json} | |
4 | + analytics.requestId = #{request.env['action_dispatch.request_id'].to_json} | |
5 | + analytics.init() | |
6 | + | ... | ... |