[Git][noosfero/noosfero][master] 4 commits: scheduler: defer off-request work

Weblate gitlab at gitlab.com
Wed Jul 22 09:59:47 BRT 2015


Weblate pushed to branch master at Noosfero / noosfero


Commits:
cd9c0b07 by Braulio Bhavamitra at 2015-07-21T18:54:38Z
scheduler: defer off-request work

- - - - -
4aee1457 by Braulio Bhavamitra at 2015-07-21T18:54:59Z
Add analytics plugin

- - - - -
71f53f04 by Bráulio Bhavamitra at 2015-07-21T21:57:34Z
Merge branch 'analytics-plugin' into 'master'

Analytics plugin

See merge request !632

- - - - -
6b50d1d9 by Weblate at 2015-07-22T14:59:00Z
Merge remote-tracking branch 'origin/master'

- - - - -


15 changed files:

- + config/initializers/unicorn.rb
- + lib/noosfero/scheduler/defer.rb
- + plugins/analytics/controllers/profile/analytics_plugin/time_on_page_controller.rb
- + plugins/analytics/db/migrate/20150715001149_init_analytics_plugin.rb
- + plugins/analytics/lib/analytics_plugin.rb
- + plugins/analytics/lib/analytics_plugin/base.rb
- + plugins/analytics/lib/ext/profile.rb
- + plugins/analytics/locales/en.yml
- + plugins/analytics/locales/pt.yml
- + plugins/analytics/models/analytics_plugin/page_view.rb
- + plugins/analytics/models/analytics_plugin/visit.rb
- + plugins/analytics/po/pt/analytics.po
- + plugins/analytics/public/javascripts/analytics.js
- + plugins/analytics/test/functional/content_viewer_controller_test.rb
- + plugins/analytics/views/analytics_plugin/_body_ending.html.slim


Changes:

=====================================
config/initializers/unicorn.rb
=====================================
--- /dev/null
+++ b/config/initializers/unicorn.rb
@@ -0,0 +1,8 @@
+require_dependency 'scheduler/defer'
+
+if defined? Unicorn
+  ObjectSpace.each_object Unicorn::HttpServer do |s|
+    s.extend Scheduler::Defer::Unicorn
+  end
+end
+


=====================================
lib/noosfero/scheduler/defer.rb
=====================================
--- /dev/null
+++ b/lib/noosfero/scheduler/defer.rb
@@ -0,0 +1,95 @@
+# based on https://github.com/discourse/discourse/blob/master/lib/scheduler/defer.rb
+
+module Scheduler
+  module Deferrable
+    def initialize
+      # FIXME: do some other way when not using Unicorn
+      @async = (not Rails.env.test?) and defined? Unicorn
+      @queue = Queue.new
+      @mutex = Mutex.new
+      @paused = false
+      @thread = nil
+    end
+
+    def pause
+      stop!
+      @paused = true
+    end
+
+    def resume
+      @paused = false
+    end
+
+    # for test
+    def async= val
+      @async = val
+    end
+
+    def later desc = nil, &blk
+      if @async
+        start_thread unless (@thread && @thread.alive?) || @paused
+        @queue << [blk, desc]
+      else
+        blk.call
+      end
+    end
+
+    def stop!
+      @thread.kill if @thread and @thread.alive?
+      @thread = nil
+    end
+
+    # test only
+    def stopped?
+      !(@thread and @thread.alive?)
+    end
+
+    def do_all_work
+      while !@queue.empty?
+        do_work _non_block=true
+      end
+    end
+
+    private
+
+    def start_thread
+      @mutex.synchronize do
+        return if @thread && @thread.alive?
+        @thread = Thread.new do
+          while true
+            do_work
+          end
+        end
+        @thread.priority = -2
+      end
+    end
+
+    # using non_block to match Ruby #deq
+    def do_work non_block=false
+      job, desc = @queue.deq non_block
+      begin
+        job.call
+      rescue => ex
+        ExceptionNotifier.notify_exception ex, message: "Running deferred code '#{desc}'"
+      end
+    rescue => ex
+      ExceptionNotifier.notify_exception ex, message: "Processing deferred code queue"
+    end
+  end
+
+  class Defer
+
+    module Unicorn
+      def process_client client
+        ::Scheduler::Defer.pause
+        super client
+        ::Scheduler::Defer.do_all_work
+        ::Scheduler::Defer.resume
+      end
+    end
+
+    extend Deferrable
+    initialize
+  end
+
+end


=====================================
plugins/analytics/controllers/profile/analytics_plugin/time_on_page_controller.rb
=====================================
--- /dev/null
+++ b/plugins/analytics/controllers/profile/analytics_plugin/time_on_page_controller.rb
@@ -0,0 +1,30 @@
+class AnalyticsPlugin::TimeOnPageController < ProfileController
+
+  before_filter :skip_page_view
+
+  def page_load
+    # to avoid concurrency problems with the original deferred request, also defer this
+    Scheduler::Defer.later do
+      page_view = profile.page_views.where(request_id: params[:id]).first
+      page_view.request = request
+      page_view.page_load!
+    end
+
+    render nothing: true
+  end
+
+  def report
+    page_view = profile.page_views.where(request_id: params[:id]).first
+    page_view.request = request
+    page_view.increase_time_on_page!
+
+    render nothing: true
+  end
+
+  protected
+
+  def skip_page_view
+    @analytics_skip_page_view = true
+  end
+
+end


=====================================
plugins/analytics/db/migrate/20150715001149_init_analytics_plugin.rb
=====================================
--- /dev/null
+++ b/plugins/analytics/db/migrate/20150715001149_init_analytics_plugin.rb
@@ -0,0 +1,47 @@
+class InitAnalyticsPlugin < ActiveRecord::Migration
+
+  def up
+    create_table :analytics_plugin_visits do |t|
+      t.integer :profile_id
+    end
+
+    create_table :analytics_plugin_page_views do |t|
+      t.string :type
+      t.integer :visit_id
+      t.integer :track_id
+      t.integer :referer_page_view_id
+      t.string :request_id
+
+      t.integer :user_id
+      t.integer :session_id
+      t.integer :profile_id
+
+      t.text :url
+      t.text :referer_url
+
+      t.text :user_agent
+      t.string :remote_ip
+
+      t.datetime :request_started_at
+      t.datetime :request_finished_at
+      t.datetime :page_loaded_at
+      t.integer :time_on_page, default: 0
+
+      t.text :data, default: {}.to_yaml
+    end
+    add_index :analytics_plugin_page_views, :request_id
+    add_index :analytics_plugin_page_views, :referer_page_view_id
+
+    add_index :analytics_plugin_page_views, :user_id
+    add_index :analytics_plugin_page_views, :session_id
+    add_index :analytics_plugin_page_views, :profile_id
+    add_index :analytics_plugin_page_views, :url
+    add_index :analytics_plugin_page_views, [:user_id, :session_id, :profile_id, :url], name: :analytics_plugin_referer_find
+  end
+
+  def down
+    drop_table :analytics_plugin_visits
+    drop_table :analytics_plugin_page_views
+  end
+
+end


=====================================
plugins/analytics/lib/analytics_plugin.rb
=====================================
--- /dev/null
+++ b/plugins/analytics/lib/analytics_plugin.rb
@@ -0,0 +1,15 @@
+module AnalyticsPlugin
+
+  TimeOnPageUpdateInterval = 2.minutes * 1000
+
+  extend Noosfero::Plugin::ParentMethods
+
+  def self.plugin_name
+    I18n.t'analytics_plugin.lib.plugin.name'
+  end
+
+  def self.plugin_description
+    I18n.t'analytics_plugin.lib.plugin.description'
+  end
+
+end


=====================================
plugins/analytics/lib/analytics_plugin/base.rb
=====================================
--- /dev/null
+++ b/plugins/analytics/lib/analytics_plugin/base.rb
@@ -0,0 +1,43 @@
+
+class AnalyticsPlugin::Base < Noosfero::Plugin
+
+  def body_ending
+    return unless profile and profile.analytics_enabled?
+    lambda do
+      render 'analytics_plugin/body_ending'
+    end
+  end
+
+  def js_files
+    ['analytics'].map{ |j| "javascripts/#{j}" }
+  end
+
+  def application_controller_filters
+    [{
+      type: 'around_filter', options: {}, block: -> &block do
+        request_started_at = Time.now
+        block.call
+        request_finished_at = Time.now
+
+        return if @analytics_skip_page_view
+        return unless profile and profile.analytics_enabled?
+
+        Scheduler::Defer.later 'analytics: register page view' do
+          page_view = profile.page_views.build request: request, profile_id: profile,
+            request_started_at: request_started_at, request_finished_at: request_finished_at
+
+          unless profile.analytics_anonymous?
+            # FIXME: use session.id in Rails 4
+            session_id = Marshal.load(Base64.decode64 request['_session_id'])['session_id'] rescue nil
+            #session_id = request.session_options[:id]
+            page_view.user = user
+            page_view.session_id = session_id
+          end
+
+          page_view.save!
+        end
+      end,
+    }]
+  end
+
+end


=====================================
plugins/analytics/lib/ext/profile.rb
=====================================
--- /dev/null
+++ b/plugins/analytics/lib/ext/profile.rb
@@ -0,0 +1,30 @@
+require_dependency 'profile'
+require_dependency 'community'
+
+([Profile] + Profile.descendants).each do |subclass|
+subclass.class_eval do
+
+  has_many :visits, foreign_key: :profile_id, class_name: 'AnalyticsPlugin::Visit'
+  has_many :page_views, foreign_key: :profile_id, class_name: 'AnalyticsPlugin::PageView'
+
+end
+end
+
+class Profile
+
+  def analytics_settings attrs = {}
+    @analytics_settings ||= Noosfero::Plugin::Settings.new self, AnalyticsPlugin, attrs
+    attrs.each{ |a, v| @analytics_settings.send "#{a}=", v }
+    @analytics_settings
+  end
+  alias_method :analytics_settings=, :analytics_settings
+
+  def analytics_enabled?
+    self.analytics_settings.enabled
+  end
+
+  def analytics_anonymous?
+    self.analytics_settings.anonymous
+  end
+
+end


=====================================
plugins/analytics/locales/en.yml
=====================================
--- /dev/null
+++ b/plugins/analytics/locales/en.yml
@@ -0,0 +1,11 @@
+
+en: &en
+  analytics_plugin:
+    lib:
+      plugin:
+        name: 'Access tracking'
+        description: 'Register the access of selected profiles'
+
+en-US:
+  <<: *en
+


=====================================
plugins/analytics/locales/pt.yml
=====================================
--- /dev/null
+++ b/plugins/analytics/locales/pt.yml
@@ -0,0 +1,10 @@
+
+pt: &pt
+  analytics_plugin:
+    lib:
+      plugin:
+        name: 'Rastreio de accesso'
+        description: 'Registra o acesso de perfis selecionados'
+
+pt-BR:
+  <<: *pt


=====================================
plugins/analytics/models/analytics_plugin/page_view.rb
=====================================
--- /dev/null
+++ b/plugins/analytics/models/analytics_plugin/page_view.rb
@@ -0,0 +1,67 @@
+class AnalyticsPlugin::PageView < ActiveRecord::Base
+
+  serialize :data
+
+  attr_accessible *self.column_names
+  attr_accessible :user, :profile
+
+  attr_accessor :request
+  attr_accessible :request
+
+  acts_as_having_settings field: :options
+
+  belongs_to :visit, class_name: 'AnalyticsPlugin::Visit'
+  belongs_to :referer_page_view, class_name: 'AnalyticsPlugin::PageView'
+
+  belongs_to :user, class_name: 'Person'
+  belongs_to :session, primary_key: :session_id, foreign_key: :session_id, class_name: 'Session'
+  belongs_to :profile
+
+  validates_presence_of :visit
+  validates_presence_of :request, on: :create
+  validates_presence_of :url
+
+  before_validation :extract_request_data, on: :create
+  before_validation :fill_referer_page_view, on: :create
+  before_validation :fill_visit, on: :create
+
+  def request_duration
+    self.request_finished_at - self.request_started_at
+  end
+
+  def page_load!
+    self.page_loaded_at = Time.now
+    self.update_column :page_loaded_at, self.page_loaded_at
+  end
+
+  def increase_time_on_page!
+    now = Time.now
+    initial_time = self.page_loaded_at || self.request_finished_at
+    return unless now > initial_time
+
+    self.time_on_page = now - initial_time
+    self.update_column :time_on_page, self.time_on_page
+  end
+
+  protected
+
+  def extract_request_data
+    self.url = self.request.url.sub /\/+$/, ''
+    self.referer_url = self.request.referer
+    self.user_agent = self.request.headers['User-Agent']
+    self.request_id = self.request.env['action_dispatch.request_id']
+    self.remote_ip = self.request.remote_ip
+  end
+
+  def fill_referer_page_view
+    self.referer_page_view = AnalyticsPlugin::PageView.order('request_started_at DESC').
+      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?
+  end
+
+  def fill_visit
+    self.visit = self.referer_page_view.visit if self.referer_page_view
+    self.visit ||= AnalyticsPlugin::Visit.new profile: profile
+  end
+
+end
+


=====================================
plugins/analytics/models/analytics_plugin/visit.rb
=====================================
--- /dev/null
+++ b/plugins/analytics/models/analytics_plugin/visit.rb
@@ -0,0 +1,11 @@
+class AnalyticsPlugin::Visit < ActiveRecord::Base
+
+  attr_accessible *self.column_names
+  attr_accessible :profile
+
+  default_scope -> { includes :page_views }
+
+  belongs_to :profile
+  has_many :page_views, class_name: 'AnalyticsPlugin::PageView', dependent: :destroy
+
+end


=====================================
plugins/analytics/po/pt/analytics.po
=====================================
--- /dev/null
+++ b/plugins/analytics/po/pt/analytics.po
@@ -0,0 +1,29 @@
+# translation of analytic.po to portuguese
+# Krishnamurti Lelis Lima Vieira Nunes <krishna at colivre.coop.br>, 2007.
+# noosfero - Brazilian Portuguese translation
+# Copyright (C) 2007,
+# Forum Brasileiro de Economia Solidaria <http://www.fbes.org.br/>
+# Copyright (C) 2007,
+# Ynternet.org Foundation <http://www.ynternet.org/>
+# This file is distributed under the same license as noosfero itself.
+# Joenio Costa <joenio at colivre.coop.br>, 2008.
+#
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: 1.0-690-gcb6e853\n"
+"POT-Creation-Date: 2015-03-05 12:10-0300\n"
+"PO-Revision-Date: 2015-07-21 09:23-0300\n"
+"Last-Translator: Michal Čihař <michal at cihar.com>\n"
+"Language-Team: Portuguese <https://hosted.weblate.org/projects/noosfero"
+"/plugin-solr/pt/>\n"
+"Language: pt\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 2.3-dev\n"
+
+msgid "Select the set of communities and users to track"
+msgstr "Seleciona o conjunto de comunidades e usuários para rastrear"
+


=====================================
plugins/analytics/public/javascripts/analytics.js
=====================================
--- /dev/null
+++ b/plugins/analytics/public/javascripts/analytics.js
@@ -0,0 +1,39 @@
+analytics = {
+  requestId: '',
+
+  timeOnPage: {
+    updateInterval: 0,
+    baseUrl: '',
+
+    report: function() {
+      $.ajax(analytics.timeOnPage.baseUrl+'/report', {
+        type: 'POST', data: {id: analytics.requestId},
+        success: function(data) {
+
+          analytics.timeOnPage.poll()
+        },
+      })
+    },
+
+    poll: function() {
+      if (analytics.timeOnPage.updateInterval)
+        setTimeout(analytics.timeOnPage.report, analytics.timeOnPage.updateInterval)
+    },
+  },
+
+  init: function() {
+    analytics.timeOnPage.poll()
+  },
+
+  pageLoad: function() {
+    $.ajax(analytics.timeOnPage.baseUrl+'/page_load', {
+      type: 'POST', data: {id: analytics.requestId},
+      success: function(data) {
+      },
+    });
+  }
+
+};
+
+$(document).ready(analytics.pageLoad)
+


=====================================
plugins/analytics/test/functional/content_viewer_controller_test.rb
=====================================
--- /dev/null
+++ b/plugins/analytics/test/functional/content_viewer_controller_test.rb
@@ -0,0 +1,48 @@
+require 'test_helper'
+require 'content_viewer_controller'
+
+class ContentViewerControllerTest < ActionController::TestCase
+
+  def setup
+    @controller = ContentViewerController.new
+    @request    = ActionController::TestRequest.new
+    @response   = ActionController::TestResponse.new
+
+    @environment = Environment.default
+    @environment.enabled_plugins += ['AnalyticsPlugin']
+    @environment.save!
+
+    @user = create_user('testinguser').person
+    login_as @user.identifier
+
+    @community = build Community, identifier: 'testcomm', name: 'test'
+    @community.analytics_settings.enabled = true
+    @community.analytics_settings.anonymous = false
+    @community.save!
+    @community.add_member @user
+  end
+
+  should 'register page view correctly' do
+    @request.env['HTTP_REFERER'] = 'http://google.com'
+    first_url = 'http://test.host'
+    get :view_page, profile: @community.identifier, page: []
+    assert_equal 1, @community.page_views.count
+    assert_equal 1, @community.visits.count
+
+    first_page_view = @community.page_views.order(:id).first
+    assert_equal @request.referer, first_page_view.referer_url
+
+    @request.env['HTTP_REFERER'] = first_url
+    get :view_page, profile: @community.identifier, page: @community.articles.last.path.split('/')
+    assert_equal 2, @community.page_views.count
+    assert_equal 1, @community.visits.count
+
+    second_page_view = @community.page_views.order(:id).last
+    assert_equal first_page_view, second_page_view.referer_page_view
+
+    assert_equal @user, second_page_view.user
+
+    assert second_page_view.request_duration > 0 and second_page_view.request_duration < 1
+  end
+
+end


=====================================
plugins/analytics/views/analytics_plugin/_body_ending.html.slim
=====================================
--- /dev/null
+++ b/plugins/analytics/views/analytics_plugin/_body_ending.html.slim
@@ -0,0 +1,6 @@
+javascript:
+  analytics.timeOnPage.baseUrl = #{url_for(controller: 'analytics_plugin/time_on_page').to_json}
+  analytics.timeOnPage.updateInterval = #{AnalyticsPlugin::TimeOnPageUpdateInterval.to_json}
+  analytics.requestId = #{request.env['action_dispatch.request_id'].to_json}
+  analytics.init()
+



View it on GitLab: https://gitlab.com/noosfero/noosfero/compare/0c90a215e85ebd3c37749589100c69fbade23c46...6b50d1d94c792d2bdbf588cd34da9b8afb787b22
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://listas.softwarelivre.org/pipermail/noosfero-dev/attachments/20150722/8becf5e5/attachment-0001.html>


More information about the Noosfero-dev mailing list