0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-06 06:24:48 +08:00
discourse/spec/models/browser_pageview_session_engagement_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

87 lines
2.7 KiB
Ruby
Vendored

# frozen_string_literal: true
RSpec.describe BrowserPageviewSessionEngagement do
describe ".upsert_from_payload" do
let(:attributes) do
{
session_id: "sess-1",
mouse_move_events: 12,
click_events: 3,
key_events: 5,
scroll_events: 7,
touch_events: 0,
back_forward_events: 1,
engaged_seconds: 4200,
time_to_first_interaction_ms: 800,
}
end
it "inserts a row with the given metrics" do
expect { described_class.upsert_from_payload(**attributes) }.to change {
described_class.count
}.by(1)
expect(described_class.find_by(session_id: "sess-1")).to have_attributes(
mouse_move_events: 12,
click_events: 3,
key_events: 5,
scroll_events: 7,
touch_events: 0,
back_forward_events: 1,
engaged_seconds: 4200,
time_to_first_interaction_ms: 800,
)
end
it "stores a null time to first interaction when none is reported" do
described_class.upsert_from_payload(**attributes.merge(time_to_first_interaction_ms: nil))
expect(described_class.find_by(session_id: "sess-1").time_to_first_interaction_ms).to be_nil
end
it "truncates an over-long session id to the column limit" do
long_id = "a" * (described_class::MAX_SESSION_ID_LENGTH + 10)
described_class.upsert_from_payload(**attributes.merge(session_id: long_id))
expect(described_class.first.session_id).to eq("a" * described_class::MAX_SESSION_ID_LENGTH)
end
it "writes nothing when the session id is blank" do
expect {
described_class.upsert_from_payload(**attributes.merge(session_id: ""))
described_class.upsert_from_payload(**attributes.merge(session_id: nil))
}.not_to change { described_class.count }
end
it "updates the existing row for the same session instead of duplicating" do
described_class.upsert_from_payload(**attributes)
expect {
described_class.upsert_from_payload(
**attributes.merge(mouse_move_events: 40, engaged_seconds: 9000),
)
}.not_to change { described_class.count }
expect(described_class.find_by(session_id: "sess-1")).to have_attributes(
mouse_move_events: 40,
engaged_seconds: 9000,
)
end
it "keeps the largest value per column across snapshots" do
described_class.upsert_from_payload(
**attributes.merge(mouse_move_events: 40, click_events: 3),
)
described_class.upsert_from_payload(
**attributes.merge(mouse_move_events: 12, click_events: 20),
)
expect(described_class.find_by(session_id: "sess-1")).to have_attributes(
mouse_move_events: 40,
click_events: 20,
)
end
end
end