Commit 71f53f042084b44ff8848630df439630c282e0cc

Authored by Braulio Bhavamitra
2 parents c2f481ce 4aee1457

Merge branch 'analytics-plugin' into 'master'

Analytics plugin

See merge request !632
config/initializers/unicorn.rb 0 → 100644
... ... @@ -0,0 +1,8 @@
  1 +require_dependency 'scheduler/defer'
  2 +
  3 +if defined? Unicorn
  4 + ObjectSpace.each_object Unicorn::HttpServer do |s|
  5 + s.extend Scheduler::Defer::Unicorn
  6 + end
  7 +end
  8 +
... ...
lib/noosfero/scheduler/defer.rb 0 → 100644
... ... @@ -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
... ...
plugins/analytics/lib/analytics_plugin.rb 0 → 100644
... ... @@ -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
... ...
plugins/analytics/lib/analytics_plugin/base.rb 0 → 100644
... ... @@ -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
... ...
plugins/analytics/lib/ext/profile.rb 0 → 100644
... ... @@ -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
... ...
plugins/analytics/locales/en.yml 0 → 100644
... ... @@ -0,0 +1,11 @@
  1 +
  2 +en: &en
  3 + analytics_plugin:
  4 + lib:
  5 + plugin:
  6 + name: 'Access tracking'
  7 + description: 'Register the access of selected profiles'
  8 +
  9 +en-US:
  10 + <<: *en
  11 +
... ...
plugins/analytics/locales/pt.yml 0 → 100644
... ... @@ -0,0 +1,10 @@
  1 +
  2 +pt: &pt
  3 + analytics_plugin:
  4 + lib:
  5 + plugin:
  6 + name: 'Rastreio de accesso'
  7 + description: 'Registra o acesso de perfis selecionados'
  8 +
  9 +pt-BR:
  10 + <<: *pt
... ...
plugins/analytics/models/analytics_plugin/page_view.rb 0 → 100644
... ... @@ -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 +
... ...
plugins/analytics/models/analytics_plugin/visit.rb 0 → 100644
... ... @@ -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
... ...
plugins/analytics/po/pt/analytics.po 0 → 100644
... ... @@ -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 +
... ...
plugins/analytics/public/javascripts/analytics.js 0 → 100644
... ... @@ -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 +
... ...