0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-14 13:58:53 +08:00
discourse/spec/system/page_objects/components/admin_dashboard_site_traffic.rb
Alan Guo Xiang Tan 539eff3f93
FEATURE: Show bounce rate and average session duration on the dashboard (#40936)
This PR adds two KPI tiles to the Traffic section — bounce rate and
average session duration — measured per browsing session as real engaged
time: the seconds a tab is both visible and focused.

It builds on the per-session engagement core already captures
(`browser_pageview_session_engagements`), so the only new table is its
daily summary `browser_pageview_session_engagement_daily_rollups` —
additive, no existing schema changes.

Key technical changes:

1. Reuse core's capture. Engaged time is already recorded per session by
core's `human-activity-tracker` → `/srv/se` beacon →
`browser_pageview_session_engagements`; this PR adds no second tracker,
endpoint, table, or site setting.

2. Key engagement capture on `persist_browser_pageview_events` alone.
`Middleware::RequestTracker#is_engagement_tracking_request?` and the
`discourse-engagement-tracking-enabled` meta tag no longer also require
the `dashboard_improvements` upcoming change, so engagement is captured
wherever pageview events are persisted rather than only where the
redesigned dashboard is enabled.

3. Pre-compute in a scheduled job. The existing
`Jobs::MaintainBrowserPageviewRollups` job, which already maintains the
country and referrer rollups on the same cadence and setting gate, now
also rolls sessions into
`browser_pageview_session_engagement_daily_rollups`, one row per `(date,
logged_in)`, so the dashboard reads this small summary instead of
scanning raw events. A session is bounced when it has a single pageview
and under ten engaged seconds (no engagement counts as zero). Sessions
that started within the last ten minutes are held out of aggregation
until a later run, so a live session is not counted as a zero-engagement
bounce before the client's first engagement flush (three minutes in) has
arrived. Engagement rolls up only from the first engagement row's date
forward, because pageview rows predate this feature and have no
engagement data — counting them would make all of history look like
instant bounces.

4. Show the tiles only when there's data. They render when
`persist_browser_pageview_events` is on, alongside the existing Direct
traffic KPI, with a neutral placeholder until the first visits are
recorded; duration is formatted as `Xm Ys`.
2026-07-03 10:49:32 +08:00

184 lines
5.3 KiB
Ruby
Vendored

# frozen_string_literal: true
module PageObjects
module Components
class AdminDashboardSiteTraffic < PageObjects::Components::Base
def has_headline?(text)
has_css?(".db-section__subintro h3", text: text)
end
def has_trend?(text)
has_css?(".db-section__subintro h3", text: text)
end
def has_up_trend?(text)
has_trend?(text)
end
def has_down_trend?(text)
has_trend?(text)
end
def has_no_trend?
has_no_css?(".db-section__subintro h3", text: "")
end
def has_metric?(label, value)
has_css?(".db-section__metric-number", exact_text: value) &&
has_css?(".db-section__metric-label", exact_text: label)
end
def has_no_metric?(label)
has_no_css?(".db-section__metric", text: label)
end
def has_chart?
has_css?(".db-section__traffic-chart canvas")
end
def has_see_details_link?
has_css?("a.db-traffic__see-details")
end
def click_see_details
find("a.db-traffic__see-details").click
end
def hover_comparison_tooltip
find("[data-trigger][data-identifier='site-traffic-comparison-tooltip']").hover
self
end
def has_comparison_tooltip?(text)
Tooltips.new("site-traffic-comparison-tooltip").present?(text: text)
end
def has_no_comparison_tooltip?
has_no_css?("[data-trigger][data-identifier='site-traffic-comparison-tooltip']")
end
def hover_logged_in_share_tooltip
find("[data-trigger][data-identifier='site-traffic-logged-in-share-tooltip']").hover
self
end
def has_logged_in_share_tooltip?(text)
Tooltips.new("site-traffic-logged-in-share-tooltip").present?(text: text)
end
def hover_direct_traffic_tooltip
find("[data-trigger][data-identifier='site-traffic-direct-traffic-tooltip']").hover
self
end
def has_direct_traffic_tooltip?(text)
Tooltips.new("site-traffic-direct-traffic-tooltip").present?(text: text)
end
def has_no_top_countries_card?
has_no_top_card?("Top countries")
end
def has_no_top_referrers_card?
has_no_top_card?("Top referrers")
end
def has_top_country_rows?(rows)
within_top_card("Top countries") do
next false unless has_css?(".db-traffic__list-row", count: rows.size)
rows.each_with_index.all? do |row, index|
nth =
".db-traffic__list-row:nth-child(#{index + 1})[data-test-country-code='#{row[:country]}']"
has_css?(nth) && has_css?("#{nth} .db-traffic__percent", text: "#{row[:percent]}%")
end
end
end
def has_top_referrer_rows?(rows)
within_top_card("Top referrers") do
next false unless has_css?(".db-traffic__list-row", count: rows.size)
rows.each_with_index.all? do |row, index|
nth = ".db-traffic__list-row:nth-child(#{index + 1})"
next false unless has_css?("#{nth} a.db-traffic__link", text: row[:referrer])
next true unless row.key?(:percent)
has_css?("#{nth} .db-traffic__percent", text: "#{row[:percent]}%")
end
end
end
def has_top_countries_empty_state?
has_empty_state_in?("Top countries", "No country data for this period.")
end
def has_top_referrers_empty_state?
has_empty_state_in?("Top referrers", "No referrer data for this period.")
end
def click_top_referrers_drilldown
within_top_card("Top referrers") { find("h3.db-section__row-block-title a").click }
end
def click_top_countries_drilldown
within_top_card("Top countries") { find("h3.db-section__row-block-title a").click }
end
def has_top_referrers_drilldown?
within_top_card("Top referrers") { has_css?("h3.db-section__row-block-title a") }
end
def has_top_countries_drilldown?
within_top_card("Top countries") { has_css?("h3.db-section__row-block-title a") }
end
def has_bounce_rate?(value)
has_kpi_value?("bounce_rate", value)
end
def has_no_bounce_rate?
has_no_kpi?("bounce_rate")
end
def has_average_session_duration?(value)
has_kpi_value?("average_session_duration", value)
end
def has_no_average_session_duration?
has_no_kpi?("average_session_duration")
end
def hover_bounce_rate_tooltip
find("[data-test-kpi='bounce_rate'] [data-trigger]").hover
self
end
def has_session_metric_tooltip?(text)
Tooltips.new("site-traffic-bounce-rate-tooltip").present?(text: text)
end
private
def has_kpi_value?(key, value)
has_css?("[data-test-kpi='#{key}'] .db-section__metric-number", exact_text: value)
end
def has_no_kpi?(key)
has_no_css?("[data-test-kpi='#{key}']")
end
def has_no_top_card?(title)
has_no_css?(".db-section__row-block", text: title)
end
def within_top_card(title, &block)
within(".db-section__row-block", text: title, &block)
end
def has_empty_state_in?(title, message)
within_top_card(title) { has_css?(".db-traffic__list-empty", text: message) }
end
end
end
end