FEATURE: Topic view stats report (#27760)

Adds a report to show the top 100 most viewed topics in a date range,
combining logged in and anonymous views. Can be filtered by category.

This is a followup to 527f02e99f
and d1191b7f5f. We are also going to
be able to see this data in a new topic map, but this admin report
helps to see an overview across the forum for a date range.
This commit is contained in:
Martin Brennan 2024-07-09 15:39:10 +10:00 committed by GitHub
parent 86e5f46175
commit e58cf24fcc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 200 additions and 19 deletions

View File

@ -0,0 +1,75 @@
# frozen_string_literal: true
module Reports::TopicViewStats
extend ActiveSupport::Concern
class_methods do
def report_topic_view_stats(report)
report.modes = [:table]
category_id, include_subcategories = report.add_category_filter
category_ids = include_subcategories ? Category.subcategory_ids(category_id) : [category_id]
sql = <<~SQL
SELECT
topic_view_stats.topic_id,
topics.title AS topic_title,
SUM(topic_view_stats.anonymous_views) AS total_anonymous_views,
SUM(topic_view_stats.logged_in_views) AS total_logged_in_views,
SUM(topic_view_stats.anonymous_views + topic_view_stats.logged_in_views) AS total_views
FROM topic_view_stats
INNER JOIN topics ON topics.id = topic_view_stats.topic_id
WHERE viewed_at BETWEEN :start_date AND :end_date
#{category_ids.any? ? "AND topics.category_id IN (:category_ids)" : ""}
GROUP BY topic_view_stats.topic_id, topics.title
ORDER BY total_views DESC
LIMIT 100
SQL
data =
DB.query(
sql,
start_date: report.start_date,
end_date: report.end_date,
category_ids: category_ids,
)
report.labels = [
{
type: :topic,
properties: {
title: :topic_title,
id: :topic_id,
},
title: I18n.t("reports.topic_view_stats.labels.topic"),
},
{
property: :total_anonymous_views,
type: :number,
title: I18n.t("reports.topic_view_stats.labels.anon_views"),
},
{
property: :total_logged_in_views,
type: :number,
title: I18n.t("reports.topic_view_stats.labels.logged_in_views"),
},
{
property: :total_views,
type: :number,
title: I18n.t("reports.topic_view_stats.labels.total_views"),
},
]
report.data =
data.map do |row|
{
topic_id: row.topic_id,
topic_title: row.topic_title,
total_anonymous_views: row.total_anonymous_views,
total_logged_in_views: row.total_logged_in_views,
total_views: row.total_views,
}
end
end
end
end

View File

@ -51,6 +51,7 @@ class Report
include Reports::TopUsersByLikesReceivedFromInferiorTrustLevel
include Reports::Topics
include Reports::TopicsWithNoResponse
include Reports::TopicViewStats
include Reports::TrendingSearch
include Reports::TrustLevelGrowth
include Reports::UserFlaggingRatio

View File

@ -1630,6 +1630,14 @@ en:
user: User
qtt_like: Likes Received
description: "Top 10 users who have had likes from a wide range of people."
topic_view_stats:
title: "Topic View Stats"
labels:
topic: Topic
logged_in_views: Logged In
anon_views: Anonymous
total_views: Total
description: "The top 100 most viewed topics in a date range, combining logged in and anonymous views. Can be filtered by category."
dashboard:
problem:

View File

@ -0,0 +1,8 @@
# frozen_string_literal: true
Fabricator(:topic_view_stat) do
topic { Fabricate(:topic) }
viewed_at { Time.zone.now }
anonymous_views { 1 }
logged_in_views { 1 }
end

View File

@ -2,9 +2,9 @@
RSpec.describe Report do
let(:user) { Fabricate(:user) }
let(:c0) { Fabricate(:category, user: user) }
let(:c1) { Fabricate(:category, parent_category: c0, user: user) } # id: 2
let(:c2) { Fabricate(:category, user: user) }
let(:category_1) { Fabricate(:category, user: user) }
let(:category_2) { Fabricate(:category, parent_category: category_1, user: user) } # id: 2
let(:category_3) { Fabricate(:category, user: user) }
shared_examples "no data" do
context "with no data" do
@ -894,7 +894,8 @@ RSpec.describe Report do
user = Fabricate(:user, refresh_auto_groups: true)
topic = Fabricate(:topic, user: user)
post0 = Fabricate(:post, topic: topic, user: user)
post1 = Fabricate(:post, topic: Fabricate(:topic, category: c1, user: user), user: user)
post1 =
Fabricate(:post, topic: Fabricate(:topic, category: category_2, user: user), user: user)
post2 = Fabricate(:post, topic: topic, user: user)
post3 = Fabricate(:post, topic: topic, user: user)
PostActionCreator.off_topic(user, post0)
@ -904,13 +905,13 @@ RSpec.describe Report do
end
context "with category filtering" do
let(:report) { Report.find("flags", filters: { category: c1.id }) }
let(:report) { Report.find("flags", filters: { category: category_2.id }) }
include_examples "category filtering"
context "with subcategories" do
let(:report) do
Report.find("flags", filters: { category: c0.id, include_subcategories: true })
Report.find("flags", filters: { category: category_1.id, include_subcategories: true })
end
include_examples "category filtering on subcategories"
@ -930,19 +931,19 @@ RSpec.describe Report do
before(:each) do
user = Fabricate(:user)
Fabricate(:topic, user: user)
Fabricate(:topic, category: c1, user: user)
Fabricate(:topic, category: category_2, user: user)
Fabricate(:topic, user: user)
Fabricate(:topic, created_at: 45.days.ago, user: user)
end
context "with category filtering" do
let(:report) { Report.find("topics", filters: { category: c1.id }) }
let(:report) { Report.find("topics", filters: { category: category_2.id }) }
include_examples "category filtering"
context "with subcategories" do
let(:report) do
Report.find("topics", filters: { category: c0.id, include_subcategories: true })
Report.find("topics", filters: { category: category_1.id, include_subcategories: true })
end
include_examples "category filtering on subcategories"
@ -1017,7 +1018,7 @@ RSpec.describe Report do
before(:each) do
user = Fabricate(:user)
topic = Fabricate(:topic, user: user)
topic_with_category_id = Fabricate(:topic, category: c1, user: user)
topic_with_category_id = Fabricate(:topic, category: category_2, user: user)
Fabricate(:post, topic: topic, user: user)
Fabricate(:post, topic: topic_with_category_id, user: user)
Fabricate(:post, topic: topic, user: user)
@ -1025,13 +1026,13 @@ RSpec.describe Report do
end
context "with category filtering" do
let(:report) { Report.find("posts", filters: { category: c1.id }) }
let(:report) { Report.find("posts", filters: { category: category_2.id }) }
include_examples "category filtering"
context "with subcategories" do
let(:report) do
Report.find("posts", filters: { category: c0.id, include_subcategories: true })
Report.find("posts", filters: { category: category_1.id, include_subcategories: true })
end
include_examples "category filtering on subcategories"
@ -1052,14 +1053,16 @@ RSpec.describe Report do
before(:each) do
user = Fabricate(:user)
Fabricate(:topic, category: c1, user: user)
Fabricate(:topic, category: category_2, user: user)
Fabricate(:post, topic: Fabricate(:topic, user: user), user: user)
Fabricate(:topic, user: user)
Fabricate(:topic, created_at: 45.days.ago, user: user)
end
context "with category filtering" do
let(:report) { Report.find("topics_with_no_response", filters: { category: c1.id }) }
let(:report) do
Report.find("topics_with_no_response", filters: { category: category_2.id })
end
include_examples "category filtering"
@ -1068,7 +1071,7 @@ RSpec.describe Report do
Report.find(
"topics_with_no_response",
filters: {
category: c0.id,
category: category_1.id,
include_subcategories: true,
},
)
@ -1089,11 +1092,11 @@ RSpec.describe Report do
include_examples "with data x/y"
before(:each) do
topic = Fabricate(:topic, category: c1)
topic = Fabricate(:topic, category: category_2)
post = Fabricate(:post, topic: topic)
PostActionCreator.like(Fabricate(:user), post)
topic = Fabricate(:topic, category: c2)
topic = Fabricate(:topic, category: category_3)
post = Fabricate(:post, topic: topic)
PostActionCreator.like(Fabricate(:user), post)
PostActionCreator.like(Fabricate(:user), post)
@ -1105,13 +1108,13 @@ RSpec.describe Report do
end
context "with category filtering" do
let(:report) { Report.find("likes", filters: { category: c1.id }) }
let(:report) { Report.find("likes", filters: { category: category_2.id }) }
include_examples "category filtering"
context "with subcategories" do
let(:report) do
Report.find("likes", filters: { category: c0.id, include_subcategories: true })
Report.find("likes", filters: { category: category_1.id, include_subcategories: true })
end
include_examples "category filtering on subcategories"
@ -1732,4 +1735,90 @@ RSpec.describe Report do
end
end
end
describe "topic_view_stats" do
let(:report) { Report.find("topic_view_stats") }
fab!(:topic_1) { Fabricate(:topic) }
fab!(:topic_2) { Fabricate(:topic) }
include_examples "no data"
context "with data" do
before do
freeze_time_safe
Fabricate(
:topic_view_stat,
topic: topic_1,
anonymous_views: 4,
logged_in_views: 2,
viewed_at: Time.zone.now - 5.days,
)
Fabricate(
:topic_view_stat,
topic: topic_1,
anonymous_views: 5,
logged_in_views: 18,
viewed_at: Time.zone.now - 3.days,
)
Fabricate(
:topic_view_stat,
topic: topic_2,
anonymous_views: 14,
logged_in_views: 21,
viewed_at: Time.zone.now - 5.days,
)
Fabricate(
:topic_view_stat,
topic: topic_2,
anonymous_views: 9,
logged_in_views: 13,
viewed_at: Time.zone.now - 1.days,
)
Fabricate(
:topic_view_stat,
topic: Fabricate(:topic),
anonymous_views: 1,
logged_in_views: 34,
viewed_at: Time.zone.now - 40.days,
)
end
it "works" do
expect(report.data.length).to eq(2)
expect(report.data[0]).to include(
topic_id: topic_2.id,
topic_title: topic_2.title,
total_anonymous_views: 23,
total_logged_in_views: 34,
total_views: 57,
)
expect(report.data[1]).to include(
topic_id: topic_1.id,
topic_title: topic_1.title,
total_anonymous_views: 9,
total_logged_in_views: 20,
total_views: 29,
)
end
context "with category filtering" do
let(:report) { Report.find("topic_view_stats", filters: { category: category_1.id }) }
before { topic_1.update!(category: category_1) }
it "filters topics to that category" do
expect(report.data.length).to eq(1)
expect(report.data[0]).to include(
topic_id: topic_1.id,
topic_title: topic_1.title,
total_anonymous_views: 9,
total_logged_in_views: 20,
total_views: 29,
)
end
end
end
end
end