0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-06 06:24:48 +08:00
discourse/spec/system/detect_human_activity_spec.rb
Krzysztof Kotlarek d570c04302
DEV: Track browser session engagement for crawler detection (#41215)
Previously, crawler detection relied only on server-side heuristics
(request velocity, ASN, referrer ratios) with no signal of genuine human
interaction within a browser session.

This change gathers per-session interaction counts (mouse, keyboard,
touch, scroll, back/forward) plus engaged duration client-side and
upserts them into a new `browser_pageview_session_engagements` table via
a lightweight `/srv/se` beacon, giving `CrawlerScorer` a human-activity
signal to weigh against its bot heuristics.
2026-07-01 09:17:48 +08:00

85 lines
2.9 KiB
Ruby
Vendored

# frozen_string_literal: true
describe "Detect human activity" do
fab!(:topics) { Fabricate.times(5, :post).map(&:topic) }
let(:discovery) { PageObjects::Pages::Discovery.new }
before do
SiteSetting.dashboard_improvements = true
SiteSetting.persist_browser_pageview_events = true
end
def flush_engagement
page.driver.with_playwright_page do |pw_page|
pw_page.evaluate("window.dispatchEvent(new Event('pagehide'))")
end
end
def engagement
BrowserPageviewSessionEngagement.first
end
it "stores continuous mouse movement and time to first interaction" do
visit("/")
expect(discovery.topic_list).to have_topics(count: 5)
# `steps` interpolates the movement into many small mousemove events,
# mimicking the continuous path a real pointer traces.
page.driver.with_playwright_page { |pw_page| pw_page.mouse.move(400, 400, steps: 30) }
flush_engagement
try_until_success { expect(BrowserPageviewSessionEngagement.count).to eq(1) }
expect(engagement.mouse_move_events).to be > 0
expect(engagement.time_to_first_interaction_ms).to be_present
end
it "ignores teleporting mouse jumps" do
visit("/")
expect(discovery.topic_list).to have_topics(count: 5)
# Each move jumps straight to its destination with no intermediate events,
# the way a script driving the cursor would. A real keypress is added so a
# snapshot is actually sent (teleports alone leave the session empty).
page.driver.with_playwright_page do |pw_page|
pw_page.mouse.move(50, 50)
pw_page.mouse.move(600, 600)
pw_page.mouse.move(50, 600)
pw_page.keyboard.press("a")
end
flush_engagement
try_until_success { expect(BrowserPageviewSessionEngagement.count).to eq(1) }
expect(engagement.mouse_move_events).to eq(0)
expect(engagement.key_events).to eq(1)
end
it "accumulates engaged duration while the tab is visible and focused" do
visit("/")
expect(discovery.topic_list).to have_topics(count: 5)
# An interaction is needed so a snapshot is sent; then stay engaged for a
# beat before flushing so the duration has time to accumulate.
page.driver.with_playwright_page do |pw_page|
pw_page.keyboard.press("a")
pw_page.wait_for_timeout(1200)
end
flush_engagement
try_until_success { expect(BrowserPageviewSessionEngagement.count).to eq(1) }
expect(engagement.engaged_seconds).to be >= 1
end
it "counts back/forward navigation" do
visit("/")
expect(discovery.topic_list).to have_topics(count: 5)
# In-app navigation uses pushState (no popstate); the back button does fire
# popstate, which is what we count.
discovery.topic_list.visit_topic(topics[0])
page.go_back
flush_engagement
try_until_success { expect(BrowserPageviewSessionEngagement.count).to eq(1) }
expect(engagement.back_forward_events).to eq(1)
end
end