0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-09 21:45:25 +08:00
discourse/spec/services/admin_dashboard_site_traffic_spec.rb
Krzysztof Kotlarek 8a52bb6e51
DEV: Use beacon counters for dashboard traffic (#41894)
The redesigned admin dashboard traffic graph was always reading the
piggyback browser pageview counters, even after beacon pageview tracking
was enabled.

Use BrowserPageviewEvent.beacon_cutover_date to switch dashboard traffic
rows from piggyback counters to beacon counters. The cutover uses the
day after dashboard_improvements was enabled, based on the
site_settings.updated_at timestamp, so the first beacon day is a full
day of data.

The dashboard keeps the public series names unchanged while internally
selecting between page_view_logged_in_browser and
page_view_logged_in_browser_beacon, and between page_view_anon_browser
and page_view_anon_browser_beacon.
2026-07-23 09:24:07 +08:00

1089 lines
36 KiB
Ruby
Vendored

# frozen_string_literal: true
RSpec.describe AdminDashboardSiteTraffic do
fab!(:admin)
before do
freeze_time(Time.zone.local(2026, 5, 14, 12, 0, 0))
SiteSetting.use_legacy_pageviews = false
SiteSetting.persist_browser_pageview_events = false
end
def build_traffic(start_date: nil, end_date: nil, guardian: admin.guardian)
described_class.build(start_date: start_date, end_date: end_date, guardian: guardian)
end
def traffic_point(date, count)
{ x: date, y: count }
end
def traffic_series(id, data, req: traffic_series_req(id))
canonical_req = traffic_series_req(id)
{
req: req,
label: I18n.t("reports.site_traffic.xaxis.#{canonical_req}"),
color: Reports::SiteTraffic::SERIES_COLORS.fetch(canonical_req),
data: data,
}
end
def traffic_series_req(id)
{
logged_in: "page_view_logged_in_browser",
anonymous: "page_view_anon_browser",
embedded: "page_view_embed",
crawlers: "page_view_crawler",
}.fetch(id)
end
def traffic_series_data(response, id, req: traffic_series_req(id))
response[:pageview_series].find { |traffic_series| traffic_series[:req] == req }[:data]
end
def use_beacon_cutover_date(date)
BrowserPageviewEvent.stubs(:beacon_cutover_date).returns(date)
end
describe ".build" do
it "returns public-community KPIs and pageview series for selected dates" do
SiteSetting.embed_topics_list = true
Fabricate(:embeddable_host)
Fabricate(:logged_in_browser_application_request, date: "2026-04-28", count: 1)
Fabricate(:anonymous_browser_application_request, date: "2026-04-29", count: 2)
Fabricate(:logged_in_browser_application_request, date: "2026-05-01", count: 10)
Fabricate(:anonymous_browser_application_request, date: "2026-05-02", count: 20)
Fabricate(:embedded_application_request, date: "2026-05-02", count: 4)
Fabricate(:crawler_application_request, date: "2026-05-03", count: 3)
expect(build_traffic(start_date: "2026-05-01", end_date: "2026-05-03")).to eq(
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: [
traffic_series(
:logged_in,
[
traffic_point("2026-05-01", 10),
traffic_point("2026-05-02", 0),
traffic_point("2026-05-03", 0),
],
),
traffic_series(
:anonymous,
[
traffic_point("2026-05-01", 0),
traffic_point("2026-05-02", 20),
traffic_point("2026-05-03", 0),
],
),
traffic_series(
:embedded,
[
traffic_point("2026-05-01", 0),
traffic_point("2026-05-02", 4),
traffic_point("2026-05-03", 0),
],
),
traffic_series(
:crawlers,
[
traffic_point("2026-05-01", 0),
traffic_point("2026-05-02", 0),
traffic_point("2026-05-03", 3),
],
),
],
)
end
it "returns a negative trend when current pageviews are below the comparison period" do
Fabricate(:logged_in_browser_application_request, date: "2026-04-28", count: 20)
Fabricate(:logged_in_browser_application_request, date: "2026-05-01", count: 5)
expect(build_traffic(start_date: "2026-05-01", end_date: "2026-05-03")).to eq(
kpis: {
browser_pageviews: {
value: 5,
percent_change: -75,
comparison_period: {
start_date: "2026-04-28",
end_date: "2026-04-30",
},
},
logged_in_share: {
value: 100,
},
},
pageview_series: [
traffic_series(
:logged_in,
[
traffic_point("2026-05-01", 5),
traffic_point("2026-05-02", 0),
traffic_point("2026-05-03", 0),
],
),
traffic_series(
:anonymous,
[
traffic_point("2026-05-01", 0),
traffic_point("2026-05-02", 0),
traffic_point("2026-05-03", 0),
],
),
traffic_series(
:crawlers,
[
traffic_point("2026-05-01", 0),
traffic_point("2026-05-02", 0),
traffic_point("2026-05-03", 0),
],
),
],
)
end
it "returns daily rows for longer selected date ranges" do
Fabricate(:logged_in_browser_application_request, date: "2026-02-28", count: 99)
Fabricate(:logged_in_browser_application_request, date: "2026-03-01", count: 1)
Fabricate(:logged_in_browser_application_request, date: "2026-03-07", count: 2)
Fabricate(:logged_in_browser_application_request, date: "2026-03-08", count: 4)
Fabricate(:logged_in_browser_application_request, date: "2026-04-04", count: 8)
Fabricate(:anonymous_browser_application_request, date: "2026-03-08", count: 10)
response = build_traffic(start_date: "2026-03-01", end_date: "2026-04-04")
dates = (Date.iso8601("2026-03-01")..Date.iso8601("2026-04-04")).map(&:iso8601)
logged_in_counts = {
"2026-03-01" => 1,
"2026-03-07" => 2,
"2026-03-08" => 4,
"2026-04-04" => 8,
}
anonymous_counts = { "2026-03-08" => 10 }
expect(traffic_series_data(response, :logged_in)).to eq(
dates.map { |date| traffic_point(date, logged_in_counts.fetch(date, 0)) },
)
expect(traffic_series_data(response, :anonymous)).to eq(
dates.map { |date| traffic_point(date, anonymous_counts.fetch(date, 0)) },
)
end
it "omits trend data when the comparison period has no pageviews" do
Fabricate(:logged_in_browser_application_request, date: "2026-04-01", count: 1)
Fabricate(:logged_in_browser_application_request, date: "2026-05-01", count: 8)
expect(build_traffic(start_date: "2026-05-01", end_date: "2026-05-03")[:kpis]).to eq(
browser_pageviews: {
value: 8,
},
logged_in_share: {
value: 100,
},
)
end
it "omits trend data when current pageviews match the comparison period" do
Fabricate(:logged_in_browser_application_request, date: "2026-04-28", count: 8)
Fabricate(:logged_in_browser_application_request, date: "2026-05-01", count: 8)
expect(build_traffic(start_date: "2026-05-01", end_date: "2026-05-03")[:kpis]).to eq(
browser_pageviews: {
value: 8,
},
logged_in_share: {
value: 100,
},
)
end
it "omits trend data when the percentage change is below the display threshold" do
Fabricate(:logged_in_browser_application_request, date: "2026-04-28", count: 200_000)
Fabricate(:logged_in_browser_application_request, date: "2026-05-01", count: 200_001)
expect(build_traffic(start_date: "2026-05-01", end_date: "2026-05-03")[:kpis]).to eq(
browser_pageviews: {
value: 200_001,
},
logged_in_share: {
value: 100,
},
)
end
it "returns one decimal place for trend changes below one percent" do
Fabricate(:logged_in_browser_application_request, date: "2026-04-28", count: 10_000)
Fabricate(:logged_in_browser_application_request, date: "2026-05-01", count: 10_050)
expect(build_traffic(start_date: "2026-05-01", end_date: "2026-05-03")[:kpis]).to eq(
browser_pageviews: {
value: 10_050,
percent_change: 0.5,
comparison_period: {
start_date: "2026-04-28",
end_date: "2026-04-30",
},
},
logged_in_share: {
value: 100,
},
)
end
it "returns whole numbers for trend changes of at least one percent" do
Fabricate(:logged_in_browser_application_request, date: "2026-04-28", count: 100)
Fabricate(:logged_in_browser_application_request, date: "2026-05-01", count: 110)
expect(build_traffic(start_date: "2026-05-01", end_date: "2026-05-03")[:kpis]).to eq(
browser_pageviews: {
value: 110,
percent_change: 10,
comparison_period: {
start_date: "2026-04-28",
end_date: "2026-04-30",
},
},
logged_in_share: {
value: 100,
},
)
end
it "excludes mobile and beacon browser pageviews from totals and series" do
Fabricate(:logged_in_browser_application_request, date: "2026-05-01", count: 10)
Fabricate(:anonymous_browser_application_request, date: "2026-05-01", count: 20)
Fabricate(:logged_in_browser_mobile_application_request, date: "2026-05-01", count: 100)
Fabricate(:logged_in_browser_beacon_application_request, date: "2026-05-01", count: 200)
Fabricate(:anonymous_browser_mobile_application_request, date: "2026-05-01", count: 300)
Fabricate(:anonymous_browser_beacon_application_request, date: "2026-05-01", count: 400)
expect(build_traffic(start_date: "2026-05-01", end_date: "2026-05-01")).to eq(
kpis: {
browser_pageviews: {
value: 30,
},
logged_in_share: {
value: 33,
},
},
pageview_series: [
traffic_series(:logged_in, [traffic_point("2026-05-01", 10)]),
traffic_series(:anonymous, [traffic_point("2026-05-01", 20)]),
traffic_series(:crawlers, [traffic_point("2026-05-01", 0)]),
],
)
end
it "uses beacon browser pageviews starting the day after dashboard improvements was enabled" do
use_beacon_cutover_date(Date.new(2026, 5, 3))
Fabricate(:logged_in_browser_application_request, date: "2026-05-01", count: 10)
Fabricate(:anonymous_browser_application_request, date: "2026-05-01", count: 20)
Fabricate(:logged_in_browser_beacon_application_request, date: "2026-05-01", count: 100)
Fabricate(:anonymous_browser_beacon_application_request, date: "2026-05-01", count: 200)
Fabricate(:logged_in_browser_application_request, date: "2026-05-02", count: 11)
Fabricate(:anonymous_browser_application_request, date: "2026-05-02", count: 21)
Fabricate(:logged_in_browser_beacon_application_request, date: "2026-05-02", count: 110)
Fabricate(:anonymous_browser_beacon_application_request, date: "2026-05-02", count: 210)
Fabricate(:logged_in_browser_application_request, date: "2026-05-03", count: 12)
Fabricate(:anonymous_browser_application_request, date: "2026-05-03", count: 22)
Fabricate(:logged_in_browser_beacon_application_request, date: "2026-05-03", count: 120)
Fabricate(:anonymous_browser_beacon_application_request, date: "2026-05-03", count: 220)
expect(build_traffic(start_date: "2026-05-01", end_date: "2026-05-03")).to eq(
kpis: {
browser_pageviews: {
value: 402,
},
logged_in_share: {
value: 35,
},
},
pageview_series: [
traffic_series(
:logged_in,
[
traffic_point("2026-05-01", 10),
traffic_point("2026-05-02", 11),
traffic_point("2026-05-03", 120),
],
),
traffic_series(
:anonymous,
[
traffic_point("2026-05-01", 20),
traffic_point("2026-05-02", 21),
traffic_point("2026-05-03", 220),
],
),
traffic_series(
:crawlers,
[
traffic_point("2026-05-01", 0),
traffic_point("2026-05-02", 0),
traffic_point("2026-05-03", 0),
],
),
],
)
end
it "compares beacon pageviews against piggyback pageviews when the prior period predates the cutover" do
use_beacon_cutover_date(Date.new(2026, 5, 1))
Fabricate(:logged_in_browser_beacon_application_request, date: "2026-04-28", count: 5)
Fabricate(:logged_in_browser_application_request, date: "2026-04-29", count: 30)
Fabricate(:anonymous_browser_application_request, date: "2026-04-29", count: 70)
Fabricate(:logged_in_browser_beacon_application_request, date: "2026-04-29", count: 1)
Fabricate(:anonymous_browser_beacon_application_request, date: "2026-04-29", count: 2)
Fabricate(:logged_in_browser_beacon_application_request, date: "2026-05-02", count: 60)
Fabricate(:anonymous_browser_beacon_application_request, date: "2026-05-02", count: 140)
Fabricate(:logged_in_browser_application_request, date: "2026-05-02", count: 3)
Fabricate(:anonymous_browser_application_request, date: "2026-05-02", count: 4)
expect(build_traffic(start_date: "2026-05-01", end_date: "2026-05-03")[:kpis]).to eq(
browser_pageviews: {
value: 200,
percent_change: 100,
comparison_period: {
start_date: "2026-04-28",
end_date: "2026-04-30",
},
},
logged_in_share: {
value: 30,
},
)
end
it "uses legacy human counters when legacy pageviews are enabled" do
SiteSetting.use_legacy_pageviews = true
Fabricate(:logged_in_application_request, date: "2026-05-01", count: 11)
Fabricate(:anonymous_application_request, date: "2026-05-01", count: 22)
Fabricate(:logged_in_browser_application_request, date: "2026-05-01", count: 110)
Fabricate(:anonymous_browser_application_request, date: "2026-05-01", count: 220)
Fabricate(:crawler_application_request, date: "2026-05-01", count: 4)
expect(build_traffic(start_date: "2026-05-01", end_date: "2026-05-01")).to eq(
kpis: {
browser_pageviews: {
value: 33,
},
logged_in_share: {
value: 33,
},
},
pageview_series: [
traffic_series(:logged_in, [traffic_point("2026-05-01", 11)], req: "page_view_logged_in"),
traffic_series(:anonymous, [traffic_point("2026-05-01", 22)], req: "page_view_anon"),
traffic_series(:crawlers, [traffic_point("2026-05-01", 4)]),
],
)
end
it "only includes embedded traffic when embedding is configured" do
Fabricate(:embedded_application_request, date: "2026-05-01", count: 7)
expect(build_traffic(start_date: "2026-05-01", end_date: "2026-05-01")).to eq(
kpis: {
browser_pageviews: {
value: 0,
},
logged_in_share: {
value: 0,
},
},
pageview_series: [
traffic_series(:logged_in, [traffic_point("2026-05-01", 0)]),
traffic_series(:anonymous, [traffic_point("2026-05-01", 0)]),
traffic_series(:crawlers, [traffic_point("2026-05-01", 0)]),
],
)
SiteSetting.embed_topics_list = true
Fabricate(:embeddable_host)
expect(build_traffic(start_date: "2026-05-01", end_date: "2026-05-01")).to eq(
kpis: {
browser_pageviews: {
value: 0,
},
logged_in_share: {
value: 0,
},
},
pageview_series: [
traffic_series(:logged_in, [traffic_point("2026-05-01", 0)]),
traffic_series(:anonymous, [traffic_point("2026-05-01", 0)]),
traffic_series(:embedded, [traffic_point("2026-05-01", 7)]),
traffic_series(:crawlers, [traffic_point("2026-05-01", 0)]),
],
)
end
it "includes embedded traffic when full app embedding is enabled" do
SiteSetting.embed_full_app = true
Fabricate(:embeddable_host)
Fabricate(:embedded_application_request, date: "2026-05-01", count: 7)
expect(build_traffic(start_date: "2026-05-01", end_date: "2026-05-01")).to eq(
kpis: {
browser_pageviews: {
value: 0,
},
logged_in_share: {
value: 0,
},
},
pageview_series: [
traffic_series(:logged_in, [traffic_point("2026-05-01", 0)]),
traffic_series(:anonymous, [traffic_point("2026-05-01", 0)]),
traffic_series(:embedded, [traffic_point("2026-05-01", 7)]),
traffic_series(:crawlers, [traffic_point("2026-05-01", 0)]),
],
)
end
it "returns logged-in traffic only when login is required" do
SiteSetting.login_required = true
SiteSetting.embed_topics_list = true
Fabricate(:embeddable_host)
Fabricate(:logged_in_browser_application_request, date: "2026-05-01", count: 9)
Fabricate(:anonymous_browser_application_request, date: "2026-05-01", count: 19)
Fabricate(:crawler_application_request, date: "2026-05-01", count: 29)
Fabricate(:embedded_application_request, date: "2026-05-01", count: 5)
expect(build_traffic(start_date: "2026-05-01", end_date: "2026-05-01")).to eq(
kpis: {
browser_pageviews: {
value: 9,
},
},
pageview_series: [traffic_series(:logged_in, [traffic_point("2026-05-01", 9)])],
)
end
it "omits trend data when the comparison period predates tracked human traffic" do
Fabricate(:logged_in_browser_application_request, date: "2026-04-30", count: 2)
Fabricate(:logged_in_browser_application_request, date: "2026-05-01", count: 8)
expect(build_traffic(start_date: "2026-05-01", end_date: "2026-05-03")).to eq(
kpis: {
browser_pageviews: {
value: 8,
},
logged_in_share: {
value: 100,
},
},
pageview_series: [
traffic_series(
:logged_in,
[
traffic_point("2026-05-01", 8),
traffic_point("2026-05-02", 0),
traffic_point("2026-05-03", 0),
],
),
traffic_series(
:anonymous,
[
traffic_point("2026-05-01", 0),
traffic_point("2026-05-02", 0),
traffic_point("2026-05-03", 0),
],
),
traffic_series(
:crawlers,
[
traffic_point("2026-05-01", 0),
traffic_point("2026-05-02", 0),
traffic_point("2026-05-03", 0),
],
),
],
)
end
it "returns zero-value KPIs and series when no traffic has been recorded" do
ApplicationRequest.delete_all
expect(build_traffic(start_date: "2026-05-01", end_date: "2026-05-03")).to eq(
kpis: {
browser_pageviews: {
value: 0,
},
logged_in_share: {
value: 0,
},
},
pageview_series: [
traffic_series(
:logged_in,
[
traffic_point("2026-05-01", 0),
traffic_point("2026-05-02", 0),
traffic_point("2026-05-03", 0),
],
),
traffic_series(
:anonymous,
[
traffic_point("2026-05-01", 0),
traffic_point("2026-05-02", 0),
traffic_point("2026-05-03", 0),
],
),
traffic_series(
:crawlers,
[
traffic_point("2026-05-01", 0),
traffic_point("2026-05-02", 0),
traffic_point("2026-05-03", 0),
],
),
],
)
end
it "uses the default date range when dates are missing, malformed, or reversed" do
Fabricate(:logged_in_browser_application_request, date: "2026-04-15", count: 3)
response_summaries =
[
build_traffic(start_date: nil, end_date: nil),
build_traffic(start_date: "not-a-date", end_date: "also-not-a-date"),
build_traffic(start_date: "2026-05-10", end_date: "2026-05-01"),
].map do |response|
logged_in_series_data = traffic_series_data(response, :logged_in)
{
browser_pageviews: response.dig(:kpis, :browser_pageviews, :value),
first_point: logged_in_series_data.first,
last_point: logged_in_series_data.last,
data_points: logged_in_series_data.size,
}
end
expect(response_summaries).to eq(
[
{
browser_pageviews: 3,
first_point: traffic_point("2026-04-15", 3),
last_point: traffic_point("2026-05-14", 0),
data_points: 30,
},
{
browser_pageviews: 3,
first_point: traffic_point("2026-04-15", 3),
last_point: traffic_point("2026-05-14", 0),
data_points: 30,
},
{
browser_pageviews: 3,
first_point: traffic_point("2026-04-15", 3),
last_point: traffic_point("2026-05-14", 0),
data_points: 30,
},
],
)
end
context "for top countries and top referrers" do
before { SiteSetting.persist_browser_pageview_events = true }
def aggregate_rollups
range = { start_date: 1.year.ago.to_date, end_date: Date.current }
BrowserPageviewCountryDailyRollup.aggregate(**range)
BrowserPageviewReferrerDailyRollup.aggregate(**range)
end
it "omits top_countries and top_referrers when persist_browser_pageview_events is disabled" do
SiteSetting.persist_browser_pageview_events = false
result = build_traffic(start_date: nil, end_date: nil)
expect(result).not_to have_key(:top_countries)
expect(result).not_to have_key(:top_referrers)
end
it "returns top_countries and top_referrers with rows and no error when matching events exist" do
6.times do
Fabricate(:browser_pageview_event, country_code: "US", normalized_referrer: "google.com")
end
aggregate_rollups
result = build_traffic(start_date: nil, end_date: nil)
expect(result[:top_countries][:rows].first[:country_code]).to eq("US")
expect(result[:top_countries][:error]).to be_nil
expect(result[:top_referrers][:rows].first[:normalized_referrer]).to eq("google.com")
expect(result[:top_referrers][:error]).to be_nil
end
it "caps each card at the top 5 rows on both the fresh and cached paths" do
%w[US GB DE FR JP CA].each do |code|
Fabricate(
:browser_pageview_event,
country_code: code,
normalized_referrer: "#{code.downcase}.example.com",
)
end
aggregate_rollups
fresh = build_traffic(start_date: nil, end_date: nil)
expect(fresh[:top_countries][:rows].size).to eq(5)
expect(fresh[:top_referrers][:rows].size).to eq(5)
cached = build_traffic(start_date: nil, end_date: nil)
expect(cached[:top_countries][:rows].size).to eq(5)
expect(cached[:top_referrers][:rows].size).to eq(5)
end
it "returns empty rows when no events match the date range" do
result = build_traffic(start_date: nil, end_date: nil)
expect(result[:top_countries]).to eq(rows: [], error: nil)
expect(result[:top_referrers]).to eq(rows: [], error: nil)
end
it "returns an exception error payload when the underlying report cannot be built" do
allow(Report).to receive(:find).and_return(nil)
result = build_traffic(start_date: nil, end_date: nil)
expect(result[:top_countries]).to eq(rows: [], error: "exception")
expect(result[:top_referrers]).to eq(rows: [], error: "exception")
end
it "serves the cached payload on subsequent calls within the cache window" do
4.times do
Fabricate(:browser_pageview_event, country_code: "US", normalized_referrer: "google.com")
end
aggregate_rollups
first = build_traffic(start_date: nil, end_date: nil)
expect(first[:top_countries][:rows].first[:country_code]).to eq("US")
BrowserPageviewCountryDailyRollup.delete_all
BrowserPageviewReferrerDailyRollup.delete_all
BrowserPageviewEvent.delete_all
second = build_traffic(start_date: nil, end_date: nil)
expect(second[:top_countries][:rows].first[:country_code]).to eq("US")
expect(second[:top_countries][:rows].first.keys).to all(be_a(Symbol))
end
it "invalidates the cached payload when login_required is toggled" do
4.times do
Fabricate(:browser_pageview_event, country_code: "US", normalized_referrer: "google.com")
end
aggregate_rollups
SiteSetting.login_required = false
first = build_traffic(start_date: nil, end_date: nil)
expect(first[:top_countries][:rows].first[:country_code]).to eq("US")
SiteSetting.login_required = true
second = build_traffic(start_date: nil, end_date: nil)
expect(second[:top_countries][:rows]).to be_empty
end
it "invalidates the cached payload when current_hostname changes" do
Discourse.stubs(:current_hostname).returns("forum-a.example.com")
Fabricate(:browser_pageview_event, normalized_referrer: "forum-b.example.com/path")
aggregate_rollups
first = build_traffic(start_date: nil, end_date: nil)
expect(first[:top_referrers][:rows].first[:normalized_referrer]).to eq(
"forum-b.example.com/path",
)
Discourse.stubs(:current_hostname).returns("forum-b.example.com")
second = build_traffic(start_date: nil, end_date: nil)
expect(second[:top_referrers][:rows]).to be_empty
end
it "serves the cached error payload on subsequent calls" do
allow(Report).to receive(:find) do |type, opts|
Report
._get(type, opts)
.tap do |report|
report.error = :exception
report.data = []
end
end
first = build_traffic(start_date: nil, end_date: nil)
expect(first[:top_countries]).to eq(rows: [], error: "exception")
allow(Report).to receive(:find).and_call_original
second = build_traffic(start_date: nil, end_date: nil)
expect(second[:top_countries]).to eq(rows: [], error: "exception")
end
it "returns a timeout error payload and retries the report on a subsequent call" do
allow(Report).to receive(:find) do |type, _opts|
Report
.new(type)
.tap do |report|
report.error = :timeout
report.data = []
end
end
first = build_traffic(start_date: nil, end_date: nil)
expect(first[:top_countries]).to eq(rows: [], error: "timeout")
expect(first[:top_referrers]).to eq(rows: [], error: "timeout")
allow(Report).to receive(:find).and_call_original
Fabricate(:browser_pageview_event, country_code: "US", normalized_referrer: "google.com")
aggregate_rollups
second = build_traffic(start_date: nil, end_date: nil)
expect(second[:top_countries][:rows].first[:country_code]).to eq("US")
end
end
context "for direct traffic share" do
before { SiteSetting.persist_browser_pageview_events = true }
def aggregate_referrer_rollups
BrowserPageviewReferrerDailyRollup.aggregate(
start_date: "2026-05-01".to_date,
end_date: "2026-05-01".to_date,
)
end
it "returns the rounded share of pageviews that arrived with no referrer" do
3.times do
Fabricate(:browser_pageview_event, normalized_referrer: nil, created_at: "2026-05-01")
end
9.times do
Fabricate(
:browser_pageview_event,
normalized_referrer: "google.com",
created_at: "2026-05-01",
)
end
aggregate_referrer_rollups
expect(build_traffic(start_date: "2026-05-01", end_date: "2026-05-03")[:kpis]).to eq(
browser_pageviews: {
value: 0,
},
logged_in_share: {
value: 0,
},
direct_traffic: {
value: 25,
},
bounce_rate: {
value: nil,
},
average_session_duration_seconds: {
value: nil,
},
)
end
it "reports zero direct traffic when every tracked pageview had a referrer" do
4.times do
Fabricate(
:browser_pageview_event,
normalized_referrer: "google.com",
created_at: "2026-05-01",
)
end
aggregate_referrer_rollups
expect(build_traffic(start_date: "2026-05-01", end_date: "2026-05-03")[:kpis]).to eq(
browser_pageviews: {
value: 0,
},
logged_in_share: {
value: 0,
},
direct_traffic: {
value: 0,
},
bounce_rate: {
value: nil,
},
average_session_duration_seconds: {
value: nil,
},
)
end
it "computes the share from logged-in pageviews only when login is required" do
SiteSetting.login_required = true
member = Fabricate(:user)
Fabricate(
:browser_pageview_event,
normalized_referrer: nil,
user_id: member.id,
created_at: "2026-05-01",
)
Fabricate(
:browser_pageview_event,
normalized_referrer: nil,
user_id: nil,
created_at: "2026-05-01",
)
2.times do
Fabricate(
:browser_pageview_event,
normalized_referrer: "google.com",
user_id: member.id,
created_at: "2026-05-01",
)
end
aggregate_referrer_rollups
expect(build_traffic(start_date: "2026-05-01", end_date: "2026-05-03")[:kpis]).to eq(
browser_pageviews: {
value: 0,
},
direct_traffic: {
value: 33,
},
bounce_rate: {
value: nil,
},
average_session_duration_seconds: {
value: nil,
},
)
end
it "omits direct traffic when persist_browser_pageview_events is disabled" do
SiteSetting.persist_browser_pageview_events = false
3.times do
Fabricate(:browser_pageview_event, normalized_referrer: nil, created_at: "2026-05-01")
end
aggregate_referrer_rollups
expect(build_traffic(start_date: "2026-05-01", end_date: "2026-05-03")[:kpis]).to eq(
browser_pageviews: {
value: 0,
},
logged_in_share: {
value: 0,
},
)
end
it "omits direct traffic when no pageviews were tracked in the period" do
expect(build_traffic(start_date: "2026-05-01", end_date: "2026-05-03")[:kpis]).to eq(
browser_pageviews: {
value: 0,
},
logged_in_share: {
value: 0,
},
bounce_rate: {
value: nil,
},
average_session_duration_seconds: {
value: nil,
},
)
end
end
context "for bounce rate and average session duration" do
before { SiteSetting.persist_browser_pageview_events = true }
it "returns bounce rate and average session duration summed across the audience" do
Fabricate(
:browser_pageview_session_engagement_daily_rollup,
date: Date.new(2026, 5, 10),
logged_in: false,
sessions: 8,
bounced: 3,
engaged_seconds_total: 400,
)
Fabricate(
:browser_pageview_session_engagement_daily_rollup,
date: Date.new(2026, 5, 10),
logged_in: true,
sessions: 12,
bounced: 2,
engaged_seconds_total: 200,
)
Fabricate(
:browser_pageview_session_engagement_daily_rollup,
date: Date.new(2026, 4, 20),
logged_in: false,
sessions: 100,
bounced: 100,
engaged_seconds_total: 100_000,
)
Fabricate(
:browser_pageview_session_engagement_daily_rollup,
date: Date.new(2026, 5, 20),
logged_in: false,
sessions: 100,
bounced: 100,
engaged_seconds_total: 100_000,
)
expect(build_traffic(start_date: "2026-05-01", end_date: "2026-05-14")[:kpis]).to eq(
browser_pageviews: {
value: 0,
},
logged_in_share: {
value: 0,
},
bounce_rate: {
value: 25,
},
average_session_duration_seconds: {
value: 30,
},
)
end
it "reports zero average duration, not a placeholder, for visits with no engaged time" do
Fabricate(
:browser_pageview_session_engagement_daily_rollup,
date: Date.new(2026, 5, 10),
logged_in: false,
sessions: 8,
bounced: 8,
engaged_seconds_total: 0,
)
expect(build_traffic(start_date: "2026-05-01", end_date: "2026-05-14")[:kpis]).to include(
bounce_rate: {
value: 100,
},
average_session_duration_seconds: {
value: 0,
},
)
end
it "rounds bounce rate and average session duration to whole numbers" do
Fabricate(
:browser_pageview_session_engagement_daily_rollup,
date: Date.new(2026, 5, 10),
logged_in: false,
sessions: 7,
bounced: 3,
engaged_seconds_total: 102,
)
expect(build_traffic(start_date: "2026-05-01", end_date: "2026-05-14")[:kpis]).to include(
bounce_rate: {
value: 43,
},
average_session_duration_seconds: {
value: 15,
},
)
end
it "includes rollup rows on the exact first and last day of the period" do
Fabricate(
:browser_pageview_session_engagement_daily_rollup,
date: Date.new(2026, 5, 1),
logged_in: false,
sessions: 4,
bounced: 1,
engaged_seconds_total: 40,
)
Fabricate(
:browser_pageview_session_engagement_daily_rollup,
date: Date.new(2026, 5, 14),
logged_in: false,
sessions: 6,
bounced: 4,
engaged_seconds_total: 120,
)
expect(build_traffic(start_date: "2026-05-01", end_date: "2026-05-14")[:kpis]).to include(
bounce_rate: {
value: 50,
},
average_session_duration_seconds: {
value: 16,
},
)
end
it "returns nil values when no sessions fall in the period" do
expect(build_traffic(start_date: "2026-05-01", end_date: "2026-05-14")[:kpis]).to eq(
browser_pageviews: {
value: 0,
},
logged_in_share: {
value: 0,
},
bounce_rate: {
value: nil,
},
average_session_duration_seconds: {
value: nil,
},
)
end
it "omits the KPIs entirely when persist_browser_pageview_events is off" do
SiteSetting.persist_browser_pageview_events = false
Fabricate(
:browser_pageview_session_engagement_daily_rollup,
date: Date.new(2026, 5, 10),
logged_in: false,
sessions: 8,
bounced: 3,
engaged_seconds_total: 240,
)
expect(build_traffic(start_date: "2026-05-01", end_date: "2026-05-14")[:kpis]).to eq(
browser_pageviews: {
value: 0,
},
logged_in_share: {
value: 0,
},
)
end
end
end
end