FEATURE: track user visits on mobile and display on admin dashboard in a new Mobile section

This commit is contained in:
Neil Lalonde 2015-07-07 12:31:07 -04:00
parent 0330e17ffa
commit 782dd13e78
14 changed files with 86 additions and 31 deletions

View File

@ -13,7 +13,7 @@ export default Discourse.Route.extend({
c.set('versionCheck', Discourse.VersionCheck.create(d.version_check)); c.set('versionCheck', Discourse.VersionCheck.create(d.version_check));
} }
['global_reports', 'page_view_reports', 'private_message_reports', 'http_reports', 'user_reports'].forEach(name => { ['global_reports', 'page_view_reports', 'private_message_reports', 'http_reports', 'user_reports', 'mobile_reports'].forEach(name => {
c.set(name, d[name].map(r => Discourse.Report.create(r))); c.set(name, d[name].map(r => Discourse.Report.create(r)));
}); });

View File

@ -108,6 +108,28 @@
</table> </table>
</div> </div>
<div class="dashboard-stats">
<table class="table table-condensed table-hover">
<thead>
<tr>
<th class="title" title="{{i18n 'admin.dashboard.mobile_title'}}">{{i18n 'admin.dashboard.mobile_title'}}</th>
<th>{{i18n 'admin.dashboard.reports.today'}}</th>
<th>{{i18n 'admin.dashboard.reports.yesterday'}}</th>
<th>{{i18n 'admin.dashboard.reports.last_7_days'}}</th>
<th>{{i18n 'admin.dashboard.reports.last_30_days'}}</th>
<th>{{i18n 'admin.dashboard.reports.all'}}</th>
</tr>
</thead>
<tbody>
{{#unless loading}}
{{#each r in mobile_reports}}
{{admin-report-counts report=r}}
{{/each}}
{{/unless}}
</tbody>
</table>
</div>
<div class="dashboard-stats"> <div class="dashboard-stats">
<table class="table table-condensed table-hover"> <table class="table table-condensed table-hover">
<thead> <thead>

View File

@ -370,7 +370,8 @@ class TopicsController < ApplicationController
current_user, current_user,
params[:topic_id].to_i, params[:topic_id].to_i,
params[:topic_time].to_i, params[:topic_time].to_i,
(params[:timings] || []).map{|post_number, t| [post_number.to_i, t.to_i]} (params[:timings] || []).map{|post_number, t| [post_number.to_i, t.to_i]},
{mobile: view_context.mobile_view?}
) )
render nothing: true render nothing: true
end end

View File

@ -15,7 +15,7 @@ class AdminDashboardData
'emails', 'emails',
] ]
PAGE_VIEW_REPORTS ||= ['page_view_total_reqs'] + ApplicationRequest.req_types.keys.select { |r| r =~ /^page_view_/ }.map { |r| r + "_reqs" } PAGE_VIEW_REPORTS ||= ['page_view_total_reqs'] + ApplicationRequest.req_types.keys.select { |r| r =~ /^page_view_/ && r !~ /mobile/ }.map { |r| r + "_reqs" }
PRIVATE_MESSAGE_REPORTS ||= [ PRIVATE_MESSAGE_REPORTS ||= [
'user_to_user_private_messages', 'user_to_user_private_messages',
@ -29,7 +29,7 @@ class AdminDashboardData
USER_REPORTS ||= ['users_by_trust_level'] USER_REPORTS ||= ['users_by_trust_level']
# TODO: MOBILE_REPORTS MOBILE_REPORTS ||= ['mobile_visits'] + ApplicationRequest.req_types.keys.select {|r| r =~ /mobile/}.map { |r| r + "_reqs" }
def problems def problems
[ rails_env_check, [ rails_env_check,
@ -81,6 +81,7 @@ class AdminDashboardData
private_message_reports: AdminDashboardData.reports(PRIVATE_MESSAGE_REPORTS), private_message_reports: AdminDashboardData.reports(PRIVATE_MESSAGE_REPORTS),
http_reports: AdminDashboardData.reports(HTTP_REPORTS), http_reports: AdminDashboardData.reports(HTTP_REPORTS),
user_reports: AdminDashboardData.reports(USER_REPORTS), user_reports: AdminDashboardData.reports(USER_REPORTS),
mobile_reports: AdminDashboardData.reports(MOBILE_REPORTS),
admins: User.admins.count, admins: User.admins.count,
moderators: User.moderators.count, moderators: User.moderators.count,
suspended: User.suspended.count, suspended: User.suspended.count,

View File

@ -70,7 +70,7 @@ class PostTiming < ActiveRecord::Base
MAX_READ_TIME_PER_BATCH = 60*1000.0 MAX_READ_TIME_PER_BATCH = 60*1000.0
def self.process_timings(current_user, topic_id, topic_time, timings) def self.process_timings(current_user, topic_id, topic_time, timings, opts={})
current_user.user_stat.update_time_read! current_user.user_stat.update_time_read!
max_time_per_post = ((Time.now - current_user.created_at) * 1000.0) max_time_per_post = ((Time.now - current_user.created_at) * 1000.0)
@ -129,7 +129,8 @@ SQL
end end
topic_time = max_time_per_post if topic_time > max_time_per_post topic_time = max_time_per_post if topic_time > max_time_per_post
TopicUser.update_last_read(current_user, topic_id, highest_seen, topic_time)
TopicUser.update_last_read(current_user, topic_id, highest_seen, topic_time, opts)
if total_changed > 0 if total_changed > 0
current_user.reload current_user.reload

View File

@ -88,6 +88,12 @@ class Report
add_counts report, UserVisit, 'visited_at' add_counts report, UserVisit, 'visited_at'
end end
def self.report_mobile_visits(report)
basic_report_about report, UserVisit, :mobile_by_day, report.start_date, report.end_date
report.total = UserVisit.where(mobile: true).count
report.prev30Days = UserVisit.where(mobile: true).where("visited_at >= ? and visited_at < ?", report.start_date - 30.days, report.start_date).count
end
def self.report_signups(report) def self.report_signups(report)
report_about report, User.real, :count_by_signup_date report_about report, User.real, :count_by_signup_date
end end

View File

@ -137,7 +137,7 @@ class TopicUser < ActiveRecord::Base
# Update the last read and the last seen post count, but only if it doesn't exist. # Update the last read and the last seen post count, but only if it doesn't exist.
# This would be a lot easier if psql supported some kind of upsert # This would be a lot easier if psql supported some kind of upsert
def update_last_read(user, topic_id, post_number, msecs) def update_last_read(user, topic_id, post_number, msecs, opts={})
return if post_number.blank? return if post_number.blank?
msecs = 0 if msecs.to_i < 0 msecs = 0 if msecs.to_i < 0
@ -192,7 +192,7 @@ class TopicUser < ActiveRecord::Base
if before_last_read < post_number if before_last_read < post_number
# The user read at least one new post # The user read at least one new post
TopicTrackingState.publish_read(topic_id, post_number, user.id, after) TopicTrackingState.publish_read(topic_id, post_number, user.id, after)
user.update_posts_read!(post_number - before_last_read) user.update_posts_read!(post_number - before_last_read, mobile: opts[:mobile])
end end
if before != after if before != after
@ -207,7 +207,8 @@ class TopicUser < ActiveRecord::Base
args[:new_status] = notification_levels[:tracking] args[:new_status] = notification_levels[:tracking]
end end
TopicTrackingState.publish_read(topic_id, post_number, user.id, args[:new_status]) TopicTrackingState.publish_read(topic_id, post_number, user.id, args[:new_status])
user.update_posts_read!(post_number)
user.update_posts_read!(post_number, mobile: opts[:mobile])
exec_sql("INSERT INTO topic_users (user_id, topic_id, last_read_post_number, highest_seen_post_number, last_visited_at, first_visited_at, notification_level) exec_sql("INSERT INTO topic_users (user_id, topic_id, last_read_post_number, highest_seen_post_number, last_visited_at, first_visited_at, notification_level)
SELECT :user_id, :topic_id, :post_number, ft.highest_post_number, :now, :now, :new_status SELECT :user_id, :topic_id, :post_number, ft.highest_post_number, :now, :now, :new_status

View File

@ -368,6 +368,11 @@ class User < ActiveRecord::Base
last_seen_at.present? last_seen_at.present?
end end
def create_visit_record!(date, opts={})
user_stat.update_column(:days_visited, user_stat.days_visited + 1)
user_visits.create!(visited_at: date, posts_read: opts[:posts_read] || 0, mobile: opts[:mobile] || false)
end
def visit_record_for(date) def visit_record_for(date)
user_visits.find_by(visited_at: date) user_visits.find_by(visited_at: date)
end end
@ -376,14 +381,18 @@ class User < ActiveRecord::Base
create_visit_record!(date) unless visit_record_for(date) create_visit_record!(date) unless visit_record_for(date)
end end
def update_posts_read!(num_posts, now=Time.zone.now, _retry=false) def update_posts_read!(num_posts, opts={})
now = opts[:at] || Time.zone.now
_retry = opts[:retry] || false
if user_visit = visit_record_for(now.to_date) if user_visit = visit_record_for(now.to_date)
user_visit.posts_read += num_posts user_visit.posts_read += num_posts
user_visit.mobile = true if opts[:mobile]
user_visit.save user_visit.save
user_visit user_visit
else else
begin begin
create_visit_record!(now.to_date, num_posts) create_visit_record!(now.to_date, posts_read: num_posts, mobile: opts.fetch(:mobile, false))
rescue ActiveRecord::RecordNotUnique rescue ActiveRecord::RecordNotUnique
if !_retry if !_retry
update_posts_read!(num_posts, now, _retry=true) update_posts_read!(num_posts, now, _retry=true)
@ -844,11 +853,6 @@ class User < ActiveRecord::Base
email_tokens.create(email: email) email_tokens.create(email: email)
end end
def create_visit_record!(date, posts_read=0)
user_stat.update_column(:days_visited, user_stat.days_visited + 1)
user_visits.create!(visited_at: date, posts_read: posts_read)
end
def ensure_password_is_hashed def ensure_password_is_hashed
if @raw_password if @raw_password
self.salt = SecureRandom.hex(16) self.salt = SecureRandom.hex(16)

View File

@ -1,8 +1,16 @@
class UserVisit < ActiveRecord::Base class UserVisit < ActiveRecord::Base
# A count of visits in the last month by day def self.counts_by_day_query(start_date, end_date)
where('visited_at >= ? and visited_at <= ?', start_date.to_date, end_date.to_date).group(:visited_at).order(:visited_at)
end
# A count of visits in a date range by day
def self.by_day(start_date, end_date) def self.by_day(start_date, end_date)
where('visited_at >= ? and visited_at <= ?', start_date.to_date, end_date.to_date).group(:visited_at).order(:visited_at).count counts_by_day_query(start_date, end_date).count
end
def self.mobile_by_day(start_date, end_date)
counts_by_day_query(start_date, end_date).where(mobile: true).count
end end
def self.ensure_consistency! def self.ensure_consistency!
@ -27,6 +35,7 @@ end
# user_id :integer not null # user_id :integer not null
# visited_at :date not null # visited_at :date not null
# posts_read :integer default(0) # posts_read :integer default(0)
# mobile :boolean default(FALSE)
# #
# Indexes # Indexes
# #

View File

@ -1689,6 +1689,7 @@ en:
suspended: 'Suspended:' suspended: 'Suspended:'
private_messages_short: "Msgs" private_messages_short: "Msgs"
private_messages_title: "Messages" private_messages_title: "Messages"
mobile_title: "Mobile"
space_free: "{{size}} free" space_free: "{{size}} free"
uploads: "uploads" uploads: "uploads"
backups: "backups" backups: "backups"

View File

@ -653,11 +653,11 @@ en:
xaxis: "Day" xaxis: "Day"
yaxis: "Total API Requests" yaxis: "Total API Requests"
page_view_logged_in_mobile_reqs: page_view_logged_in_mobile_reqs:
title: "Mobile Logged In" title: "Logged In API Requests"
xaxis: "Day" xaxis: "Day"
yaxis: "Mobile Logged In API Requests" yaxis: "Mobile Logged In API Requests"
page_view_anon_mobile_reqs: page_view_anon_mobile_reqs:
title: "Mobile Anon" title: "Anon API Requests"
xaxis: "Day" xaxis: "Day"
yaxis: "Mobile Anon API Requests" yaxis: "Mobile Anon API Requests"
http_background_reqs: http_background_reqs:
@ -692,6 +692,10 @@ en:
title: "Topics with no response" title: "Topics with no response"
xaxis: "Day" xaxis: "Day"
yaxis: "Total" yaxis: "Total"
mobile_visits:
title: "User Visits"
xaxis: "Day"
yaxis: "Number of visits"
dashboard: dashboard:
rails_env_warning: "Your server is running in %{env} mode." rails_env_warning: "Your server is running in %{env} mode."

View File

@ -0,0 +1,5 @@
class AddMobileToUserVisits < ActiveRecord::Migration
def change
add_column :user_visits, :mobile, :boolean, default: false
end
end

View File

@ -67,10 +67,10 @@ describe TrustLevel3Requirements do
describe "days_visited" do describe "days_visited" do
it "counts visits when posts were read no further back than 100 days ago" do it "counts visits when posts were read no further back than 100 days ago" do
user.save user.save
user.update_posts_read!(1, 2.days.ago) user.update_posts_read!(1, at: 2.days.ago)
user.update_posts_read!(1, 3.days.ago) user.update_posts_read!(1, at: 3.days.ago)
user.update_posts_read!(0, 4.days.ago) user.update_posts_read!(0, at: 4.days.ago)
user.update_posts_read!(3, 101.days.ago) user.update_posts_read!(3, at: 101.days.ago)
expect(tl3_requirements.days_visited).to eq(2) expect(tl3_requirements.days_visited).to eq(2)
end end
end end
@ -106,10 +106,10 @@ describe TrustLevel3Requirements do
describe "posts_read" do describe "posts_read" do
it "counts posts read within the last 100 days" do it "counts posts read within the last 100 days" do
user.save user.save
user.update_posts_read!(3, 2.days.ago) user.update_posts_read!(3, at: 2.days.ago)
user.update_posts_read!(1, 3.days.ago) user.update_posts_read!(1, at: 3.days.ago)
user.update_posts_read!(0, 4.days.ago) user.update_posts_read!(0, at: 4.days.ago)
user.update_posts_read!(5, 101.days.ago) user.update_posts_read!(5, at: 101.days.ago)
expect(tl3_requirements.posts_read).to eq(4) expect(tl3_requirements.posts_read).to eq(4)
end end
end end
@ -127,8 +127,8 @@ describe TrustLevel3Requirements do
describe "posts_read_all_time" do describe "posts_read_all_time" do
it "counts posts read at any time" do it "counts posts read at any time" do
user.save user.save
user.update_posts_read!(3, 2.days.ago) user.update_posts_read!(3, at: 2.days.ago)
user.update_posts_read!(1, 101.days.ago) user.update_posts_read!(1, at: 101.days.ago)
expect(tl3_requirements.posts_read_all_time).to eq(4) expect(tl3_requirements.posts_read_all_time).to eq(4)
end end
end end

View File

@ -921,7 +921,7 @@ describe User do
it "with no existing UserVisit record, creates a new UserVisit record and increments the posts_read count" do it "with no existing UserVisit record, creates a new UserVisit record and increments the posts_read count" do
expect { expect {
user_visit = user.update_posts_read!(3, 5.days.ago) user_visit = user.update_posts_read!(3, at: 5.days.ago)
expect(user_visit.posts_read).to eq(3) expect(user_visit.posts_read).to eq(3)
}.to change { UserVisit.count }.by(1) }.to change { UserVisit.count }.by(1)
end end