0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-09 21:45:25 +08:00
discourse/app/models/browser_pageview_event.rb
Alan Guo Xiang Tan 437ab337d2
FEATURE: Add top countries and top referrers cards to the admin dashboard (#40215)
This commit adds two new cards to the redesigned admin dashboard's Site
Traffic section: top countries and top referrers, both sourced from
`browser_pageview_events`.

Key technical decisions:

1. Gate the cards on the `persist_browser_pageview_events` site setting.
The cards have no data source unless browser pageview events are being
persisted, so they are omitted from the dashboard.

2. Normalize referrers at write time. A new `normalized_referrer` column
on `browser_pageview_events` is populated by
`BrowserPageviewReferrerInspector`, which strips scheme, `www.`, port,
fragment, trailing slashes, and common tracking query params. Doing this
at insert time avoids per-row string operations at query time.

3. Count browser pageviews by country and by referrer in two new report
concerns. `Reports::TopCountriesByBrowserPageviews` groups by
`country_code` and `Reports::TopReferrersByBrowserPageviews` groups by
`normalized_referrer`. Both compute share of total browser pageviews and
rank the top 5 in SQL. The country report drops MaxMind reserved codes
(unknown, anonymous proxy, satellite). The referrer report drops
same-host referrals. Both also exclude anonymous browser pageviews
(`user_id IS NULL`) when the `login_required` site setting is enabled,
since only logged-in browser pageviews are meaningful on a closed forum.

4. Fetch each report through the existing dashboard service.
`AdminDashboardSiteTraffic#build` returns one entry per card with a `{
rows:, error: }` shape, e.g.:

   ```ruby
   {
     top_countries: {
       rows: [
         { country_code: "US", count: 142, percent: 35 },
         { country_code: "GB", count: 89, percent: 22 }
       ],
       error: nil
     },
     top_referrers: {
       rows: [
{ normalized_referrer: "news.ycombinator.com/item?id=1", count: 47,
percent: 12 },
{ normalized_referrer: "reddit.com/r/discourse", count: 31, percent: 8 }
       ],
       error: nil
     }
   }
   ```

On report failure, `rows: []` and `error: :timeout` (or another symbol).
This lets the UI render rows, error, or empty state independently.
Healthy responses are cached via `Report.find_cached`.
`SiteSetting.login_required` and `Discourse.current_hostname` flow into
`opts[:filters]` so toggling either invalidates the cache. Timeouts skip
the cache so the next request retries.

5. Use `Intl.DisplayNames` for country names instead of locale files.
`Intl.DisplayNames` is a built-in browser API that returns a localized
country name for an ISO 3166-1 alpha-2 code, avoiding ~250 translation
strings per locale.
2026-05-22 12:59:16 +08:00

54 lines
1.9 KiB
Ruby
Vendored

# frozen_string_literal: true
class BrowserPageviewEvent < ActiveRecord::Base
MAX_SESSION_ID_LENGTH = 32
MAX_URL_LENGTH = 2000
MAX_REFERRER_LENGTH = 2000
MAX_USER_AGENT_LENGTH = 1000
MAX_NORMALIZED_REFERRER_LENGTH = 2000
has_one :browser_pageview_event_score, foreign_key: :event_id, dependent: :delete
before_save :truncate_fields
private
def truncate_fields
self.url = url.slice(0, MAX_URL_LENGTH) if url.present?
self.referrer = referrer.slice(0, MAX_REFERRER_LENGTH) if referrer.present?
self.user_agent = user_agent.slice(0, MAX_USER_AGENT_LENGTH) if user_agent.present?
self.session_id = session_id.slice(0, MAX_SESSION_ID_LENGTH) if session_id.present?
if normalized_referrer.present?
self.normalized_referrer = normalized_referrer.slice(0, MAX_NORMALIZED_REFERRER_LENGTH)
end
end
end
# == Schema Information
#
# Table name: browser_pageview_events
#
# id :bigint not null, primary key
# asn :integer
# country_code :string(2)
# ip_address :inet not null
# normalized_referrer :string(2000)
# referrer :string(2000)
# score :integer
# url :string(2000) not null
# user_agent :string(1000) not null
# created_at :datetime not null
# session_id :string(32) not null
# topic_id :integer
# user_id :integer
#
# Indexes
#
# idx_bpe_created_at_country_code (created_at,country_code)
# idx_bpe_created_at_normalized_referrer (created_at,normalized_referrer)
# idx_bpe_ip_ua_created_at (ip_address,user_agent,created_at)
# idx_bpe_session_created_at (session_id,created_at)
# index_browser_pageview_events_on_created_at (created_at) USING brin
# index_browser_pageview_events_on_topic_id (topic_id)
# index_browser_pageview_events_on_user_id (user_id)
#