discourse/app/models/concerns/reports/site_traffic.rb
Alan Guo Xiang Tan 87e695fc53
FEATURE: Add site traffic dashboard section (#40023)
Makes the redesigned dashboard Site traffic section real for the core
traffic summary.

- In scope: live pageview headline, comparison trend, logged-in share
KPI, and stacked traffic chart.
- Out of scope: top referrers, top countries, and narrative traffic
insights. Those remain placeholder UI for a follow-up.
- Reuses the existing report stacked chart for both the dashboard
section and `/admin/reports/site_traffic`.
- Extends the existing site traffic report data contract instead of
introducing a dashboard-only chart shape.
- Keeps traffic aggregation, KPI calculation, and trend eligibility on
the backend.
- Supports dashboard preset and custom date ranges with inclusive date
windows.
- Ships traffic data in this section payload shape:

```json
{
  "id": "traffic",
  "data": {
    "kpis": {
      "browser_pageviews": {
        "value": 30,
        "percent_change": 900,
        "comparison_period": {
          "start_date": "2026-04-28",
          "end_date": "2026-04-30"
        }
      },
      "logged_in_share": {
        "value": 33
      }
    },
    "pageview_series": [
      {
        "req": "page_view_logged_in_browser",
        "label": "Logged in",
        "color": "#4B3CE0",
        "data": [
          { "x": "2026-05-01", "y": 10 }
        ]
      }
    ]
  }
}
```
2026-05-19 14:08:21 +08:00

100 lines
4 KiB
Ruby
Vendored

# frozen_string_literal: true
module Reports::SiteTraffic
extend ActiveSupport::Concern
SERIES_COLORS = {
"page_view_logged_in_browser" => "#4B3CE0",
"page_view_anon_browser" => "#9C8DEC",
"page_view_crawler" => "#D5CDF7",
"page_view_embed" => "#E6E1F8",
"page_view_other" => "#E84A5F",
}.freeze
class_methods do
def report_site_traffic(report)
report.modes = [Report::MODES[:stacked_chart]]
first_browser_pageview_date =
DB.query_single(
<<~SQL,
SELECT date FROM application_requests
WHERE req_type = :page_view_logged_in_browser OR req_type = :page_view_anon_browser ORDER BY date LIMIT 1
SQL
page_view_logged_in_browser: ApplicationRequest.req_types[:page_view_logged_in_browser],
page_view_anon_browser: ApplicationRequest.req_types[:page_view_anon_browser],
).first
data =
DB.query(
<<~SQL,
SELECT
date,
SUM(CASE WHEN req_type = :page_view_logged_in_browser THEN count ELSE 0 END) AS page_view_logged_in_browser,
SUM(CASE WHEN req_type = :page_view_anon_browser THEN count ELSE 0 END) AS page_view_anon_browser,
SUM(CASE WHEN req_type = :page_view_crawler THEN count ELSE 0 END) AS page_view_crawler,
SUM(CASE WHEN req_type = :page_view_embed THEN count ELSE 0 END) AS page_view_embed,
SUM(
CASE WHEN req_type = :page_view_anon THEN count
WHEN req_type = :page_view_logged_in THEN count
WHEN req_type = :page_view_anon_browser THEN -count
WHEN req_type = :page_view_logged_in_browser THEN -count
ELSE 0
END
) AS page_view_other
FROM application_requests
WHERE date >= :start_date AND date <= :end_date AND date >= :first_browser_pageview_date
GROUP BY date
ORDER BY date ASC
SQL
start_date: report.start_date,
end_date: report.end_date,
page_view_anon: ApplicationRequest.req_types[:page_view_anon],
page_view_crawler: ApplicationRequest.req_types[:page_view_crawler],
page_view_logged_in: ApplicationRequest.req_types[:page_view_logged_in],
page_view_anon_browser: ApplicationRequest.req_types[:page_view_anon_browser],
page_view_logged_in_browser: ApplicationRequest.req_types[:page_view_logged_in_browser],
page_view_embed: ApplicationRequest.req_types[:page_view_embed],
first_browser_pageview_date: first_browser_pageview_date,
)
report.data = [
{
req: "page_view_logged_in_browser",
label: I18n.t("reports.site_traffic.xaxis.page_view_logged_in_browser"),
color: SERIES_COLORS.fetch("page_view_logged_in_browser"),
data: data.map { |row| { x: row.date, y: row.page_view_logged_in_browser } },
},
{
req: "page_view_anon_browser",
label: I18n.t("reports.site_traffic.xaxis.page_view_anon_browser"),
color: SERIES_COLORS.fetch("page_view_anon_browser"),
data: data.map { |row| { x: row.date, y: row.page_view_anon_browser } },
},
{
req: "page_view_crawler",
label: I18n.t("reports.site_traffic.xaxis.page_view_crawler"),
color: SERIES_COLORS.fetch("page_view_crawler"),
data: data.map { |row| { x: row.date, y: row.page_view_crawler } },
},
]
if EmbeddableHost.exists?
report.data << {
req: "page_view_embed",
label: I18n.t("reports.site_traffic.xaxis.page_view_embed"),
color: SERIES_COLORS.fetch("page_view_embed"),
data: data.map { |row| { x: row.date, y: row.page_view_embed } },
}
end
report.data << {
req: "page_view_other",
label: I18n.t("reports.site_traffic.xaxis.page_view_other"),
color: SERIES_COLORS.fetch("page_view_other"),
data: data.map { |row| { x: row.date, y: row.page_view_other } },
}
end
end
end