mirror of
https://github.com/discourse/discourse.git
synced 2026-08-06 06:24:48 +08:00
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.
87 lines
2.7 KiB
Ruby
Vendored
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
|