discourse/lib/statistics.rb
Osama Sayegh 10ae7ef44a
FEATURE: Add estimated number of global and EU visitors to the about page (#28382)
This commit implements 2 new metrics/stats in the /about page for the _estimated_ numbers of unique visitors from the EU and the rest of the world. This new feature is currently off by default, but it can be enabled by turning on the hidden `display_eu_visitor_stats` site settings via the rails console.

There are a number of assumptions that we're making here in order to estimate the number of unique visitors, specifically:

1. we're assuming that the average of page views per anonymous visitor is similar to the average number of page views that a logged-in visitor makes, and
2. we're assuming that the ratio of logged in visitors from the EU is similar to the ratio of anonymous visitors from the EU

Discourse keeps track of the number of both logged-in and anonymous page views, and also the number of unique logged-in visitors and where they're from. So with those numbers and the assumptions above, we can estimate the number of unique anonymous visitors from the EU and the rest of the world.

Internal topic: t/128480.
2024-08-21 00:03:42 +03:00

184 lines
5.3 KiB
Ruby

# frozen_string_literal: true
class Statistics
EU_COUNTRIES = %w[
AT
BE
BG
CY
CZ
DE
DK
EE
ES
FI
FR
GR
HR
HU
IE
IT
LT
LU
LV
MT
NL
PL
PT
RO
SE
SI
SK
]
def self.active_users
{
last_day: User.where("last_seen_at > ?", 1.day.ago).count,
"7_days": User.where("last_seen_at > ?", 7.days.ago).count,
"30_days": User.where("last_seen_at > ?", 30.days.ago).count,
}
end
def self.likes
{
last_day:
UserAction.where(action_type: UserAction::LIKE).where("created_at > ?", 1.day.ago).count,
"7_days":
UserAction.where(action_type: UserAction::LIKE).where("created_at > ?", 7.days.ago).count,
"30_days":
UserAction.where(action_type: UserAction::LIKE).where("created_at > ?", 30.days.ago).count,
count: UserAction.where(action_type: UserAction::LIKE).count,
}
end
def self.posts
{
last_day: Post.where("created_at > ?", 1.day.ago).count,
"7_days": Post.where("created_at > ?", 7.days.ago).count,
"30_days": Post.where("created_at > ?", 30.days.ago).count,
count: Post.count,
}
end
def self.topics
{
last_day: Topic.listable_topics.where("created_at > ?", 1.day.ago).count,
"7_days": Topic.listable_topics.where("created_at > ?", 7.days.ago).count,
"30_days": Topic.listable_topics.where("created_at > ?", 30.days.ago).count,
count: Topic.listable_topics.count,
}
end
def self.users
{
last_day: User.real.where("created_at > ?", 1.day.ago).count,
"7_days": User.real.where("created_at > ?", 7.days.ago).count,
"30_days": User.real.where("created_at > ?", 30.days.ago).count,
count: User.real.count,
}
end
def self.participating_users
{
last_day: participating_users_count(1.day.ago),
"7_days": participating_users_count(7.days.ago),
"30_days": participating_users_count(30.days.ago),
}
end
def self.visitors
periods = [[1.day.ago, :last_day], [7.days.ago, :"7_days"], [30.days.ago, :"30_days"]]
periods
.map do |(period, key)|
anon_page_views =
ApplicationRequest.request_type_count_for_period(:page_view_anon_browser, period)
logged_in_visitors = logged_in_visitors_count(period)
next key, anon_page_views if logged_in_visitors == 0
logged_in_page_views =
ApplicationRequest.request_type_count_for_period(:page_view_logged_in_browser, period)
next key, anon_page_views + logged_in_visitors if logged_in_page_views == 0
total_visitors = logged_in_visitors
avg_logged_in_page_view_per_user = logged_in_page_views.to_f / logged_in_visitors
anon_visitors = (anon_page_views / avg_logged_in_page_view_per_user).round
total_visitors += anon_visitors
[key, total_visitors]
end
.to_h
end
def self.eu_visitors
periods = [[1.day.ago, :last_day], [7.days.ago, :"7_days"], [30.days.ago, :"30_days"]]
periods
.map do |(period, key)|
logged_in_page_views =
ApplicationRequest.request_type_count_for_period(:page_view_logged_in_browser, period)
anon_page_views =
ApplicationRequest.request_type_count_for_period(:page_view_anon_browser, period)
all_logged_in_visitors = logged_in_visitors_count(period)
eu_logged_in_visitors = eu_logged_in_visitors_count(period)
next key, 0 if all_logged_in_visitors == 0 || eu_logged_in_visitors == 0
next key, eu_logged_in_visitors if logged_in_page_views == 0
avg_logged_in_page_view_per_user = logged_in_page_views / all_logged_in_visitors.to_f
eu_logged_in_visitors_ratio = eu_logged_in_visitors / all_logged_in_visitors.to_f
eu_anon_visitors =
((anon_page_views / avg_logged_in_page_view_per_user) * eu_logged_in_visitors_ratio).round
eu_visitors = eu_logged_in_visitors + eu_anon_visitors
[key, eu_visitors]
end
.to_h
end
private
def self.participating_users_count(date)
subqueries = [
"SELECT DISTINCT user_id FROM user_actions WHERE created_at > :date AND action_type IN (:action_types)",
]
if ActiveRecord::Base.connection.data_source_exists?("chat_messages")
subqueries << "SELECT DISTINCT user_id FROM chat_messages WHERE created_at > :date"
end
if ActiveRecord::Base.connection.data_source_exists?("chat_message_reactions")
subqueries << "SELECT DISTINCT user_id FROM chat_message_reactions WHERE created_at > :date"
end
sql = "SELECT COUNT(user_id) FROM (#{subqueries.join(" UNION ")}) u"
DB.query_single(sql, date: date, action_types: UserAction::USER_ACTED_TYPES).first
end
def self.logged_in_visitors_count(since)
DB.query_single(<<~SQL, since:).first
SELECT COUNT(DISTINCT user_id)
FROM user_visits
WHERE visited_at >= :since
SQL
end
def self.eu_logged_in_visitors_count(since)
results = DB.query_hash(<<~SQL, since:)
SELECT DISTINCT(user_id), ip_address
FROM user_visits uv
INNER JOIN users u
ON u.id = uv.user_id
WHERE visited_at >= :since AND ip_address IS NOT NULL
SQL
results.reduce(0) do |sum, hash|
ip_info = DiscourseIpInfo.get(hash["ip_address"].to_s)
sum + (EU_COUNTRIES.include?(ip_info[:country_code]) ? 1 : 0)
end
end
end