From 825aa9be916e2d0355b2de32d7d97d548a542d0c Mon Sep 17 00:00:00 2001 From: Ben Halpern Date: Thu, 7 Mar 2019 11:53:33 -0800 Subject: [PATCH] Add page_view model (#1985) * Add page_view model * Uncomment DJ routes * Add time on site max and don't update article page views every time --- .../initializeBaseTracking.js.erb | 52 ++++++++ app/controllers/page_views_controller.rb | 40 ++++++ app/models/article.rb | 1 + app/models/page_view.rb | 4 + app/models/user.rb | 1 + config/routes.rb | 1 + .../20190305221008_create_page_views.rb | 15 +++ db/schema.rb | 16 ++- spec/models/page_view_spec.rb | 5 + spec/requests/page_views_spec.rb | 116 ++++++++++++++++++ 10 files changed, 250 insertions(+), 1 deletion(-) create mode 100644 app/controllers/page_views_controller.rb create mode 100644 app/models/page_view.rb create mode 100644 db/migrate/20190305221008_create_page_views.rb create mode 100644 spec/models/page_view_spec.rb create mode 100644 spec/requests/page_views_spec.rb diff --git a/app/assets/javascripts/initializers/initializeBaseTracking.js.erb b/app/assets/javascripts/initializers/initializeBaseTracking.js.erb index 154a472ea..1883349de 100644 --- a/app/assets/javascripts/initializers/initializeBaseTracking.js.erb +++ b/app/assets/javascripts/initializers/initializeBaseTracking.js.erb @@ -116,6 +116,34 @@ function trackCustomImpressions() { successLinks[i].addEventListener('click', function() { trackHtmlVariantSuccess(dataBody, csrfToken) }); } } + + // page view + if (ArticleElement && tokenMeta && !isBot) { + var randomNumber = Math.floor(Math.random() * 10); // 1 in 10; Only track 1 in 10 impressions + if (!checkUserLoggedIn() && randomNumber != 1) { + return; + } + var dataBody = { + article_id: ArticleElement.dataset.articleId, + referrer: document.referrer, + user_agent: navigator.userAgent, + }; + var csrfToken = tokenMeta.getAttribute('content'); + trackPageView(dataBody, csrfToken); + var timeOnSiteCounter = 0; + var timeOnSiteInterval = setInterval(function(){ + timeOnSiteCounter++ + var ArticleElement = document.getElementById('article-body'); + if (ArticleElement && checkUserLoggedIn()) { + trackFifteenSecondsOnPage(ArticleElement.dataset.articleId, csrfToken); + } else { + clearInterval(timeOnSiteInterval); + } + if ( timeOnSiteCounter > 118 ) { + clearInterval(timeOnSiteInterval); + } + }, 15000) + } }, 1500) } @@ -144,4 +172,28 @@ function trackHtmlVariantSuccess(dataBody, csrfToken) { body: JSON.stringify(dataBody), credentials: 'same-origin', }) +} + +function trackPageView(dataBody, csrfToken) { + window.fetch('/page_views', { + method: 'POST', + headers: { + 'X-CSRF-Token': csrfToken, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(dataBody), + credentials: 'same-origin', + }) +} + +function trackFifteenSecondsOnPage(articleId, csrfToken) { + window.fetch('/page_views/' + articleId, { + method: 'PATCH', + headers: { + 'X-CSRF-Token': csrfToken, + 'Content-Type': 'application/json', + }, + credentials: 'same-origin', + }) + } \ No newline at end of file diff --git a/app/controllers/page_views_controller.rb b/app/controllers/page_views_controller.rb new file mode 100644 index 000000000..e63b021b3 --- /dev/null +++ b/app/controllers/page_views_controller.rb @@ -0,0 +1,40 @@ +class PageViewsController < ApplicationController + # No policy needed. All views are for all users + def create + if user_signed_in? + PageView.create(user_id: current_user.id, + article_id: page_view_params[:article_id], + referrer: page_view_params[:referrer], + user_agent: page_view_params[:user_agent]) + else + PageView.create(counts_for_number_of_views: 10, + article_id: page_view_params[:article_id], + referrer: page_view_params[:referrer], + user_agent: page_view_params[:user_agent]) + end + update_article_page_views + head :ok + end + + def update + if user_signed_in? + page_view = PageView.where(article_id: params[:id], user_id: current_user.id).last + page_view.update_column(:time_tracked_in_seconds, page_view.time_tracked_in_seconds + 15) + end + head :ok + end + + private + + def update_article_page_views + return if Rails.env.production? && rand(5) != 1 # We don't need to update the article page views every time. + + article = Article.find(page_view_params[:article_id]) + new_page_views_count = article.page_views.sum(:counts_for_number_of_views) + article.update_column(:page_views_count, new_page_views_count) if new_page_views_count > article.page_views_count + end + + def page_view_params + params.require(:page_view).permit(%i[article_id referrer user_agent]) + end +end diff --git a/app/models/article.rb b/app/models/article.rb index a8ce4c4db..045406cbd 100644 --- a/app/models/article.rb +++ b/app/models/article.rb @@ -19,6 +19,7 @@ class Article < ApplicationRecord has_many :buffer_updates has_many :notifications, as: :notifiable has_many :rating_votes + has_many :page_views validates :slug, presence: { if: :published? }, format: /\A[0-9a-z-]*\z/, uniqueness: { scope: :user_id } diff --git a/app/models/page_view.rb b/app/models/page_view.rb new file mode 100644 index 000000000..96b6621c5 --- /dev/null +++ b/app/models/page_view.rb @@ -0,0 +1,4 @@ +class PageView < ApplicationRecord + belongs_to :user, optional: true + belongs_to :article +end diff --git a/app/models/user.rb b/app/models/user.rb index b48e65e9b..9edf3fc54 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -37,6 +37,7 @@ class User < ApplicationRecord has_many :feedback_messages has_many :rating_votes has_many :html_variants, dependent: :destroy + has_many :page_views has_many :mentor_relationships_as_mentee, class_name: "MentorRelationship", foreign_key: "mentee_id" has_many :mentor_relationships_as_mentor, diff --git a/config/routes.rb b/config/routes.rb index a1563e695..a170d20df 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -131,6 +131,7 @@ Rails.application.routes.draw do resources :push_notification_subscriptions, only: [:create] resources :tag_adjustments, only: [:create] resources :rating_votes, only: [:create] + resources :page_views, only: [:create, :update] get "/notifications/:filter" => "notifications#index" get "/notifications/:filter/:org_id" => "notifications#index" diff --git a/db/migrate/20190305221008_create_page_views.rb b/db/migrate/20190305221008_create_page_views.rb new file mode 100644 index 000000000..eb5472a13 --- /dev/null +++ b/db/migrate/20190305221008_create_page_views.rb @@ -0,0 +1,15 @@ +class CreatePageViews < ActiveRecord::Migration[5.1] + def change + create_table :page_views do |t| + t.bigint :user_id + t.bigint :article_id + t.integer :counts_for_number_of_views, default: 1 + t.integer :time_tracked_in_seconds, default: 15 + t.string :referrer + t.string :user_agent + t.timestamps + end + add_index :page_views, :user_id + add_index :page_views, :article_id + end +end diff --git a/db/schema.rb b/db/schema.rb index 38cca9b8b..296671d1b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20190227163803) do +ActiveRecord::Schema.define(version: 20190305221008) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -519,6 +519,19 @@ ActiveRecord::Schema.define(version: 20190227163803) do t.index ["slug"], name: "index_organizations_on_slug", unique: true end + create_table "page_views", force: :cascade do |t| + t.bigint "article_id" + t.integer "counts_for_number_of_views", default: 1 + t.datetime "created_at", null: false + t.string "referrer" + t.integer "time_tracked_in_seconds", default: 15 + t.datetime "updated_at", null: false + t.string "user_agent" + t.bigint "user_id" + t.index ["article_id"], name: "index_page_views_on_article_id" + t.index ["user_id"], name: "index_page_views_on_user_id" + end + create_table "podcast_episodes", id: :serial, force: :cascade do |t| t.text "body" t.integer "comments_count", default: 0, null: false @@ -776,6 +789,7 @@ ActiveRecord::Schema.define(version: 20190227163803) do t.datetime "github_repos_updated_at", default: "2017-01-01 05:00:00" t.string "github_username" t.string "gitlab_url" + t.string "inbox_type", default: "private" t.jsonb "language_settings", default: {}, null: false t.datetime "last_article_at", default: "2017-01-01 05:00:00" t.datetime "last_comment_at", default: "2017-01-01 05:00:00" diff --git a/spec/models/page_view_spec.rb b/spec/models/page_view_spec.rb new file mode 100644 index 000000000..2af255599 --- /dev/null +++ b/spec/models/page_view_spec.rb @@ -0,0 +1,5 @@ +require "rails_helper" + +RSpec.describe PageView, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/requests/page_views_spec.rb b/spec/requests/page_views_spec.rb new file mode 100644 index 000000000..a816c4ae3 --- /dev/null +++ b/spec/requests/page_views_spec.rb @@ -0,0 +1,116 @@ +require "rails_helper" + +RSpec.describe "PageViews", type: :request do + let(:user) { create(:user, :trusted) } + let(:article) { create(:article) } + + describe "POST /page_views" do + context "when user signed in" do + before do + sign_in user + end + + it "creates a new page view" do + post "/page_views", params: { + page_view: { + article_id: article.id + } + } + expect(article.reload.page_views.size).to eq(1) + expect(article.reload.page_views_count).to eq(1) + expect(user.reload.page_views.size).to eq(1) + expect(PageView.last.counts_for_number_of_views).to eq(1) + end + + it "sends referrer" do + post "/page_views", params: { + page_view: { + article_id: article.id, + referrer: "test" + } + } + expect(PageView.last.referrer).to eq("test") + end + + it "sends user agent" do + post "/page_views", params: { + page_view: { + article_id: article.id, + user_agent: "test" + } + } + expect(PageView.last.user_agent).to eq("test") + end + end + + context "when user not signed in" do + it "creates a new page view" do + post "/page_views", params: { + page_view: { + article_id: article.id + } + } + expect(article.reload.page_views.size).to eq(1) + expect(article.reload.page_views_count).to eq(10) + expect(user.reload.page_views.size).to eq(0) + expect(PageView.last.counts_for_number_of_views).to eq(10) + end + + it "creates stores aggregate page views" do + post "/page_views", params: { page_view: { article_id: article.id } } + post "/page_views", params: { page_view: { article_id: article.id } } + expect(article.reload.page_views_count).to eq(20) + end + + it "sends referrer" do + post "/page_views", params: { + page_view: { + article_id: article.id, + referrer: "test" + } + } + expect(PageView.last.referrer).to eq("test") + end + + it "sends user agent" do + post "/page_views", params: { + page_view: { + article_id: article.id, + user_agent: "test" + } + } + expect(PageView.last.user_agent).to eq("test") + end + end + end + + describe "PUT /page_views/:id" do + context "when user is signed in" do + before do + sign_in user + end + + it "updates a new page view time on page by 15" do + post "/page_views", params: { + page_view: { + article_id: article.id + } + } + put "/page_views/" + article.id.to_s + expect(PageView.last.time_tracked_in_seconds).to eq(30) + end + end + + context "when user is not signed in" do + it "updates a new page view time on page by 15" do + post "/page_views", params: { + page_view: { + article_id: article.id + } + } + put "/page_views/" + article.id.to_s + expect(PageView.last.time_tracked_in_seconds).to eq(15) + end + end + end +end