Adjust dashboard analytics to be more reliable (#1115)

* Update analytics

* Remove analytics file

* Remove unused files

* Add delete button back in on dashboard
This commit is contained in:
Ben Halpern 2018-11-13 20:37:29 -05:00 committed by GitHub
parent 652ec00f73
commit c5807205b5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 148 additions and 248 deletions

View file

@ -32,7 +32,6 @@ function callInitalizers(){
initNotifications();
initializeSplitTestTracking();
initializeStylesheetAppend();
initializeAnalytics();
initializeCommentDropdown();
initializeSettings();
initializeFooterMod();

View file

@ -1,32 +0,0 @@
function initializeAnalytics() {
if (getCurrentPage("dashboards-show")) {
var els = document.querySelectorAll('[data-analytics-pageviews]');
if (els.length === 0) { return; }
var ids = [];
for(var i = 0; i < els.length; i++) {
ids.push(els[i].dataset.articleId)
}
// .map(function() {return this.dataset.reactableId});
window.fetch('/analytics?article_ids='+ids.join(","), {headers: {Accept: 'application/json'}, credentials: 'same-origin'})
.then(function(response) {
response.json().then(function(json) {
var total = 0;
for (var k in json){
total = total + Number(json[k]);
var numString = parseInt(json[k]) < 100 ? "< 100 " : numberWithCommas(json[k]) ;
var totalString = parseInt(total) < 100 ? "< 100 " : numberWithCommas(total) ;
document.getElementById("pageviews-"+k).innerHTML = numString + " Views";
document.getElementById("pageviews-"+k).classList.add("loaded");
document.getElementById("dashboard-analytics").innerHTML = totalString+ " Total Views";
}
document.getElementById("dashboard-analytics-header").classList.add("loaded");
});
}).catch(function(err) {
console.log(err);
});
}
}
function numberWithCommas(x) {
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}

View file

@ -6,18 +6,21 @@
max-width:96%;
margin:auto;
h1{
font-size:30px;
font-size:22px;
font-family: $helvetica-condensed;
font-stretch:condensed;
padding-bottom: 20px;
a{
padding: 0px 20px;
border-radius: 3px;
padding: 6px 25px;
border-radius: 100px;
margin-right: 18px;
display: inline-block;
color: $black;
&:hover{
background: lighten($light-medium-gray, 3%);
}
&.active{
background: $black;
background: $dark-gray;
color:white;
}
}
@ -70,39 +73,49 @@
.dashboard-analytics-header-wrapper{
text-align: center;
margin-bottom: 28px;
background: $dark-gray;
box-shadow: $bold-shadow;
padding: 15px 0px;
}
.dashboard-analytics-header{
color:$black;
color: white;
margin:5px auto;
font-size:calc(0.77em + 0.22vw);
padding: 20px 0px;
font-size:calc(1.1em + 0.1vw);
padding: 18px 0px 7px;
padding-left: 10px;
text-align: left;
font-family: $monospace;
border-radius: 200px;
border: 1px solid $light-medium-gray;
box-shadow: $bold-shadow;
// border-radius: 200px;
border: 1px solid $light-gray;
// box-shadow: $bold-shadow;
display: inline-block;
width: 90%;
text-align: center;
margin: 1.1%;
@media screen and ( min-width: 800px ) {
width: 43%;
margin: 1.2%;
}
span{
opacity:0.3;
font-size: 1.8em;
font-weight: bold;
margin-left: 8px;
display: inline-block;
}
img{
width: calc(0.9em + 1.2vw);
height: calc(0.9em + 1.2vw);
margin-right: 0.4em;
margin-left: 1.1em;
vertical-align: calc(-0.6vw - 2px);
width: calc(1.2em + 0.2vw);
height: calc(1.2em + 0.2vw);
margin-left: 0.4em;
vertical-align: calc(-0.2vw - 0.3em);
background: $sky-blue;
border-radius: 100px;
border: 2px solid darken($light-gray, 7%);
}
&.loaded{
span{
opacity:1;
}
.dashboard-analytics-sub-indicator{
color: darken($light-gray, 7%);
padding: 10px 0px;
font-weight: bold;
font-size:0.66em;
}
}
@ -152,7 +165,7 @@
color:rgb(67, 78, 87);
}
.dashboard-actions{
padding: 8px 0px;
padding: 5px 0px;
}
.pill{
background:$bold-blue;
@ -173,22 +186,16 @@
background:$red;
}
&.black{
background: $black;
background: $dark-gray;
}
}
.dashboard-pageviews-indicator{
display:inline-block;
font-family: $monospace;
color: $black;
color: $dark-gray;
background: white;
text-align:center;
opacity:0.4;
height: 23px;
line-height: 22px;
padding: 2px 8px;
&.loaded{
opacity:1;
}
}
form{
padding: 10px 0px;
@ -241,4 +248,4 @@
min-height: 350px;
}
}
}
}

View file

@ -72,8 +72,6 @@ class GoogleAnalytics
grr = GetReportsRequest.new(report_requests: report_requests, quota_user: @user_id.to_s)
response = @client.batch_get_reports(grr)
response.reports.map do |report|
p report.data
p report.data
(report.data.maximums || report.data.totals)[0].values[0]
end
end
@ -84,4 +82,4 @@ class GoogleAnalytics
scope: [AUTH_ANALYTICS_READONLY],
)
end
end
end

View file

@ -1,58 +0,0 @@
class AnalyticsController < ApplicationController
caches_action :index,
cache_path: Proc.new { "#{request.params}___#{current_user.id}" },
expires_in: 12.minutes
after_action :verify_authorized
def index
@article_ids = analytics_params.split(",")
articles_to_check = Article.where(id: @article_ids)
authorize articles_to_check, :analytics_index?
qualified_articles = get_articles_that_qualify(articles_to_check)
fetch_and_update_page_views_and_reaction_counts(qualified_articles)
render_finalized_json
end
def get_articles_that_qualify(articles_to_check)
qualified_articles = []
articles_to_check.each do |article|
if should_fetch(article)
qualified_articles << article
end
end
qualified_articles
end
def fetch_and_update_page_views_and_reaction_counts(qualified_articles)
qualified_articles.each_slice(25).to_a.each do |chunk|
pageviews = GoogleAnalytics.new(chunk.pluck(:id), current_user.id).get_pageviews
page_views_obj = pageviews.to_h
chunk.each do |article|
next if article.page_views_count > page_views_obj[article.id].to_i
article.update_columns(page_views_count: page_views_obj[article.id].to_i,
previous_positive_reactions_count: article.positive_reactions_count)
end
end
end
def render_finalized_json
finalized_object = {}
Article.where(id: @article_ids).
pluck(:id, :page_views_count).map { |a| finalized_object[a[0]] = a[1].to_s }
render json: finalized_object.to_json
end
def should_fetch(article)
new_reactions = (article.positive_reactions_count > article.previous_positive_reactions_count)
random = (rand(80) == 1)
new_reactions || random
end
private
def analytics_params
params.require(:article_ids)
end
end

View file

@ -17,6 +17,8 @@ class AsyncInfoController < ApplicationController
remember_me(current_user)
end
@user = current_user.decorate
# Updates article analytics periodically:
ArticleAnalyticsFetcher.new.delay.update_analytics(@user.id) if rand(20) == 1
respond_to do |format|
format.json do
render json: {
@ -60,4 +62,4 @@ class AsyncInfoController < ApplicationController
#{current_user&.articles_count}__
#{cookies[:remember_user_token]}"
end
end
end

View file

@ -16,10 +16,15 @@ class DashboardsController < ApplicationController
elsif params[:which] == "user_followers"
@follows = Follow.where(followable_id: @user.id, followable_type: "User").
includes(:follower).order("created_at DESC").limit(80)
elsif params[:which] == "organization_user_followers"
@follows = Follow.where(followable_id: @user.organization_id, followable_type: "Organization").
includes(:follower).order("created_at DESC").limit(80)
elsif @user&.organization && @user&.org_admin && params[:which] == "organization"
@articles = @user.organization.articles.order("created_at DESC").decorate
elsif @user
@articles = @user.articles.order("created_at DESC").decorate
end
# Updates analytics in background if appropriate:
ArticleAnalyticsFetcher.new.delay.update_analytics(current_user.id) if @articles
end
end
end

View file

@ -0,0 +1,34 @@
class ArticleAnalyticsFetcher
def update_analytics(user_id)
articles_to_check = Article.where(user_id: user_id, published: true)
qualified_articles = get_articles_that_qualify(articles_to_check)
return if qualified_articles.none?
fetch_and_update_page_views_and_reaction_counts(qualified_articles, user_id)
end
def fetch_and_update_page_views_and_reaction_counts(qualified_articles, user_id)
qualified_articles.each_slice(15).to_a.each do |chunk|
pageviews = GoogleAnalytics.new(chunk.pluck(:id), user_id).get_pageviews
page_views_obj = pageviews.to_h
chunk.each do |article|
article.update_columns(previous_positive_reactions_count: article.positive_reactions_count)
next if article.page_views_count > page_views_obj[article.id].to_i
article.update_columns(page_views_count: page_views_obj[article.id].to_i)
end
end
end
def get_articles_that_qualify(articles_to_check)
qualified_articles = []
articles_to_check.each do |article|
if should_fetch(article)
qualified_articles << article
end
end
qualified_articles
end
def should_fetch(article)
article.positive_reactions_count > article.previous_positive_reactions_count
end
end

View file

@ -0,0 +1,18 @@
<% num_views = @articles.sum(&:page_views_count) %>
<div class="dashboard-analytics-header-wrapper">
<div class="dashboard-analytics-header">
<span><%= number_with_delimiter(@articles.sum(&:positive_reactions_count), delimeter: ",") %></span>
<div class="dashboard-analytics-sub-indicator">
<img src="<%= asset_path "emoji/emoji-one-heart.png" %>" /><img src="<%= asset_path "emoji/emoji-one-unicorn.png" %>" /><img src="<%= asset_path "emoji/emoji-one-bookmark.png" %>" />
Total Post Reactions
</div>
</div>
<div class="dashboard-analytics-header" id="dashboard-analytics-header">
<span><%= num_views > 500 ? number_with_delimiter(num_views, delimeter: ",") : "under 500" %></span>
<div class="dashboard-analytics-sub-indicator">
<img id="dashboard-analytics-image" src="<%= asset_path "emoji/apple-eyes.png" %>" />
Total Post Views
</div>
</div>
</div>

View file

@ -0,0 +1,22 @@
<div class="single-article">
<a href="<%= article.current_state_path %>"><h2><%= article.title %></h2></a>
<div class="dashboard-actions">
<% if !article.published %>
<span class="pill cta yellow">DRAFT</span>
<% end %>
<a href="<%= article.path %>/edit" class="pill cta green">EDIT</a>
<a href="<%= article.path %>/delete_confirm" data-no-instant class="cta pill black">DELETE</a>
<% if article.published && current_user.can_view_analytics? %>
<span id="pageviews-<%= article.id %>" class="cta pill dashboard-pageviews-indicator" data-analytics-pageviews data-article-id="<%= article.id %>">
<%= article.page_views_count > 100 ? article.page_views_count : "under 100" %> views // <%= article.positive_reactions_count %> reactions
</span>
<% end %>
</div>
<% if org_admin %>
<%= form_for(article) do |f| %>
<input type="hidden" name="destination" value="/dashboard" />
AUTHOR: <%= f.select(:user_id, options_for_select(@user.organization.users.map {|x| [x.name, x.id] }, article.user_id )) %>
<%= f.submit "SUBMIT AUTHOR CHANGE", class:"cta pill black" %>
<% end %>
<% end %>
</div>

View file

@ -6,7 +6,7 @@
<span>POSTS</span>
<span>(<%= @user.articles_count %>)</span>
</a>
<a class="action <%= 'active' if params[:which] == "user_followers" %>" href="/dashboard/user_followers">
<a class="action <%= 'active' if params[:which] == "user_followers" || params[:which] == "organization_user_followers" %>" href="/dashboard/user_followers">
<span>FOLLOWERS</span>
<span>(<%= @user.followers_count %>)</span>
</a>
@ -15,66 +15,43 @@
<span>(<%= @user.following_users_count %>)</span>
</a>
</div>
<% if @user.org_admin && @user.organization %>
<% if @user.org_admin && @user.organization && (params[:which].blank? || params[:which] == "organization") %>
<h1>
<a href="/dashboard" class=<%= "active" if params[:which].blank? %>>MY POSTS</a>
<a href="/dashboard/organization" class=<%= "active" if params[:which] == "organization" %>><%= @user.organization.name.upcase %> POSTS</a>
<a href="/dashboard/organization" class=<%= "active" if params[:which] == "organization" %>><%= @user.organization.name.upcase %> POSTS (<%= @user.organization.articles.size %>)</a>
</h1>
<% elsif @user.org_admin && @user.organization && (params[:which] == "organization_user_followers" || params[:which] == "user_followers")%>
<h1>
<a href="/dashboard/user_followers" class=<%= "active" if params[:which] == "user_followers" %>>MY FOLLOWERS</a>
<a href="/dashboard/organization_user_followers" class=<%= "active" if params[:which] == "organization_user_followers" %>><%= @user.organization.name.upcase %> FOLLOWERS (<%= @user.organization.followers_count %>)</a>
</h1>
<% end %>
<% if @user.org_admin && @user.organization && params[:which] == "organization" %>
<% if current_user.can_view_analytics? %>
<div class="dashboard-analytics-header-wrapper">
<div class="dashboard-analytics-header loaded">
<img src="<%= asset_path "emoji/emoji-one-heart.png" %>" /><span><%= number_with_delimiter(@articles.sum(&:positive_reactions_count), delimeter: ",") %> Total Post Reactions</span>
</div>
<div class="dashboard-analytics-header" id="dashboard-analytics-header">
<img id="dashboard-analytics-image" src="<%= asset_path "emoji/apple-eyes.png" %>" /><span id="dashboard-analytics">Loading Views Data...</span>
</div>
</div>
<%= render "analytics" %>
<% end %>
<% @articles.each do |article| %>
<div class="single-article">
<a href="<%= article.current_state_path %>"><h2><%= article.title %></h2></a>
<div>
<% if !article.published %>
<span class="pill cta yellow">DRAFT</span>
<% end %>
<a href="<%= article.path %>/edit" class="pill cta green">EDIT</a>
<% if article.published && current_user.can_view_analytics? %>
<span id="pageviews-<%= article.id %>" class="cta pill dashboard-pageviews-indicator" data-analytics-pageviews data-article-id="<%= article.id %>">Loading View Data...</span>
<% end %>
</div>
<%= form_for(article) do |f| %>
<input type="hidden" name="destination" value="/dashboard" />
AUTHOR: <%= f.select(:user_id, options_for_select(@user.organization.users.map {|x| [x.name, x.id] }, article.user_id )) %>
<%= f.submit "SUBMIT AUTHOR CHANGE", class:"cta pill black" %>
<% end %>
</div>
<%= render "dashboard_article", article: article, org_admin: true %>
<% end %>
</div>
<% elsif params[:which] == "user_followers" || params[:which] == "following_users" %>
<% elsif params[:which] == "user_followers" || params[:which] == "following_users" || params[:which] == "organization_user_followers" %>
<% @follows.each do |follow| %>
<% user = params[:which] == "user_followers" ? follow.follower : follow.followable %>
<div class="single-article">
<a href="<%= user.path %>" class="block-link">
<h2>
<img alt="<%= user.username %> profile image" src="<%= ProfileImage.new(user).get(60)%>" />
<%= user.name %>
<span class="dashboard-username">@<%= user.username %></a>
</h2>
</a>
</div>
<% user = params[:which].include?("followers") ? follow.follower : follow.followable %>
<% if user %>
<div class="single-article">
<a href="<%= user.path %>" class="block-link">
<h2>
<img alt="<%= user.username %> profile image" src="<%= ProfileImage.new(user).get(60)%>" />
<%= user.name %>
<span class="dashboard-username">@<%= user.username %></a>
</h2>
</a>
</div>
<% end %>
<% end %>
<% elsif @articles.any? %>
<% if current_user.can_view_analytics? %>
<div class="dashboard-analytics-header-wrapper">
<div class="dashboard-analytics-header loaded">
<img src="<%= asset_path "emoji/emoji-one-heart.png" %>" /><span><%= number_with_delimiter(@articles.sum(&:positive_reactions_count), delimeter: ",") %> Total Post Reactions</span>
</div>
<div class="dashboard-analytics-header" id="dashboard-analytics-header">
<img id="dashboard-analytics-image" src="<%= asset_path "emoji/apple-eyes.png" %>" /><span id="dashboard-analytics">Loading Views Data...</span>
</div>
</div>
<%= render "analytics" %>
<% end %>
<% if current_user.has_role?(:video_permission) %>
<a class="video-upload-cta" href="/videos/new" data-no-instant>
@ -82,25 +59,7 @@
</a>
<% end %>
<% @articles.each do |article| %>
<div class="single-article">
<a href="<%= article.current_state_path %>">
<h2>
<%= article.title %>
</h2>
</a>
<div class="dashboard-actions">
<% if article.published %>
<a href="<%= article.path %>" class="cta pill">PUBLISHED</a>
<% else %>
<a href="<%= article.path %>?preview=<%= article.password %>" class="cta pill yellow">DRAFT</a>
<% end %>
<a href="<%= article.path %>/edit" class="cta pill green">EDIT</a>
<a href="<%= article.path %>/delete_confirm" data-no-instant class="cta pill black">DELETE</a>
<% if article.published && current_user.can_view_analytics? %>
<span id="pageviews-<%= article.id %>" class="cta pill dashboard-pageviews-indicator" data-analytics-pageviews data-article-id="<%= article.id %>">Loading View Data...</span>
<% end %>
</div>
</div>
<%= render "dashboard_article", article: article, org_admin: false %>
<% end %>
<% else %>
<div class="no-articles">

View file

@ -107,7 +107,6 @@ Rails.application.routes.draw do
resources :blocks
resources :notifications, only: [:index]
resources :tags, only: [:index]
resources :analytics, only: [:index]
resources :stripe_subscriptions, only: %i[create update destroy]
resources :stripe_active_cards, only: %i[create update destroy]
resources :live_articles, only: [:index]
@ -237,7 +236,7 @@ Rails.application.routes.draw do
get "/dashboard" => "dashboards#show"
get "/dashboard/:which" => "dashboards#show",
constraints: {
which: /organization|user_followers|following_users|reading/
which: /organization|organization_user_followers|user_followers|following_users|reading/
}
get "/dashboard/:username" => "dashboards#show"
@ -299,4 +298,4 @@ Rails.application.routes.draw do
root "stories#index"
end
# rubocop:enable Metrics/BlockLength
# rubocop:enable Metrics/BlockLength

View file

@ -1,53 +0,0 @@
require "rails_helper"
vcr_option = {
cassette_name: "google_api_request_spec"
}
RSpec.describe "Analytics", type: :request, vcr: vcr_option do
describe "GET /analytics" do
context "when signed in as an authorized user" do
let(:user) { create(:user, :analytics) }
let(:article1) { create(:article, user_id: user.id) }
let(:article2) { create(:article, user_id: user.id) }
let(:ga_double) { instance_double(GoogleAnalytics) }
before do
allow(GoogleAnalytics).to receive(:new).and_return(ga_double)
allow(ga_double).to receive(:create_service_account_credential).and_return({})
allow(ga_double).to receive(:get_pageviews) do
{ article1.id.to_s => "0", article2.id.to_s => "0" }
end
login_as user
end
it "raise ParameterMissing if no proper params is given" do
expect { get "/analytics" }.to raise_error ActionController::ParameterMissing
end
it "returns pageviews" do
get "/analytics?article_ids=#{article1.id},#{article2.id}"
expect(JSON.parse(response.body)).to eq(article1.id.to_s => "0", article2.id.to_s => "0")
end
it "returns pageviews for super_admins" do
user.remove_role :analytics_beta_tester
user.add_role :super_admin
get "/analytics?article_ids=#{article1.id},#{article2.id}"
expect(JSON.parse(response.body)).to eq(article1.id.to_s => "0", article2.id.to_s => "0")
end
it "updates article view counts" do
Reaction.create!(
user_id: user.id,
reactable_id: article1.id,
reactable_type: "Article",
category: "readinglist",
)
expect(article1.reload.previous_positive_reactions_count).not_to eq(article1.positive_reactions_count)
get "/analytics?article_ids=#{article1.id},#{article2.id}"
expect(article1.reload.previous_positive_reactions_count).to eq(article1.positive_reactions_count)
end
end
end
end