Broaden clustered bot filtering
This commit is contained in:
@@ -1838,10 +1838,26 @@ function currentVisitors() {
|
|||||||
ORDER BY datetime(vs.first_seen_at) ASC, vs.id ASC
|
ORDER BY datetime(vs.first_seen_at) ASC, vs.id ASC
|
||||||
LIMIT 100
|
LIMIT 100
|
||||||
`).all();
|
`).all();
|
||||||
const clusters = suspiciousVisitorClusters(rows, presence);
|
const clusters = suspiciousVisitorClusters(recentVisitorClusterRows(presence), presence);
|
||||||
return rows.map((row) => formatVisitorSession(row, presence, clusters));
|
return rows.map((row) => formatVisitorSession(row, presence, clusters));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function recentVisitorClusterRows(settings) {
|
||||||
|
if (!settings.clusterBotFilterEnabled) return [];
|
||||||
|
const lookbackSeconds = Math.max(settings.offlineSeconds, settings.clusterBotWindowSeconds * 4, 600);
|
||||||
|
return db.prepare(`
|
||||||
|
SELECT vs.*, s.site_key, vp.display_name AS visitor_display_name,
|
||||||
|
(SELECT COUNT(*) FROM conversations c WHERE c.site_id = vs.site_id AND c.visitor_token = vs.visitor_token) AS visitor_conversation_count
|
||||||
|
FROM visitor_sessions vs
|
||||||
|
JOIN sites s ON s.id = vs.site_id
|
||||||
|
LEFT JOIN visitor_profiles vp ON vp.site_id = vs.site_id AND vp.visitor_token = vs.visitor_token
|
||||||
|
WHERE datetime(COALESCE(vs.last_activity_at, vs.last_seen_at, vs.first_seen_at)) >= datetime('now', '-${lookbackSeconds} seconds')
|
||||||
|
OR datetime(vs.first_seen_at) >= datetime('now', '-${lookbackSeconds} seconds')
|
||||||
|
ORDER BY datetime(vs.first_seen_at) ASC, vs.id ASC
|
||||||
|
LIMIT 3000
|
||||||
|
`).all();
|
||||||
|
}
|
||||||
|
|
||||||
function formatVisitorSession(row, thresholds = adminPresenceSettings(), clusters = new Map()) {
|
function formatVisitorSession(row, thresholds = adminPresenceSettings(), clusters = new Map()) {
|
||||||
const history = parseJson(row.browsing_history_json, []);
|
const history = parseJson(row.browsing_history_json, []);
|
||||||
const device = parseJson(row.device_json, {});
|
const device = parseJson(row.device_json, {});
|
||||||
@@ -1855,7 +1871,7 @@ function formatVisitorSession(row, thresholds = adminPresenceSettings(), cluster
|
|||||||
sessionSeconds: sessionSecondsValue,
|
sessionSeconds: sessionSecondsValue,
|
||||||
referrer: row.referrer
|
referrer: row.referrer
|
||||||
});
|
});
|
||||||
const clusterReason = clusters.get(visitorClusterKey(row, device));
|
const clusterReason = visitorClusterReason(row, device, clusters);
|
||||||
const finalBot = clusterReason && conversationCountValue === 0 ? { isBot: true, reason: clusterReason } : bot;
|
const finalBot = clusterReason && conversationCountValue === 0 ? { isBot: true, reason: clusterReason } : bot;
|
||||||
const countryCode = row.country_code || countryFromTimezone(row.timezone) || countryFromLanguage(row.language);
|
const countryCode = row.country_code || countryFromTimezone(row.timezone) || countryFromLanguage(row.language);
|
||||||
const presence = visitorPresence(row, thresholds);
|
const presence = visitorPresence(row, thresholds);
|
||||||
@@ -1903,21 +1919,24 @@ function suspiciousVisitorClusters(rows, settings) {
|
|||||||
if (Number(row.visitor_conversation_count || 0) > 0) continue;
|
if (Number(row.visitor_conversation_count || 0) > 0) continue;
|
||||||
if (String(row.referrer || "").trim()) continue;
|
if (String(row.referrer || "").trim()) continue;
|
||||||
const device = parseJson(row.device_json, {});
|
const device = parseJson(row.device_json, {});
|
||||||
const key = visitorClusterKey(row, device);
|
|
||||||
if (!key) continue;
|
|
||||||
const firstSeen = parseTimestampMs(row.first_seen_at);
|
const firstSeen = parseTimestampMs(row.first_seen_at);
|
||||||
if (!Number.isFinite(firstSeen)) continue;
|
if (!Number.isFinite(firstSeen)) continue;
|
||||||
if (!groups.has(key)) groups.set(key, []);
|
for (const key of visitorClusterKeys(row, device)) {
|
||||||
groups.get(key).push(firstSeen);
|
if (!groups.has(key)) groups.set(key, []);
|
||||||
|
groups.get(key).push(firstSeen);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const suspicious = new Map();
|
const suspicious = new Map();
|
||||||
const windowMs = settings.clusterBotWindowSeconds * 1000;
|
const windowMs = settings.clusterBotWindowSeconds * 1000;
|
||||||
for (const [key, times] of groups.entries()) {
|
for (const [key, times] of groups.entries()) {
|
||||||
|
const requiredCount = key.startsWith("device|")
|
||||||
|
? Math.max(settings.clusterBotCount * 3, settings.clusterBotCount + 20)
|
||||||
|
: settings.clusterBotCount;
|
||||||
times.sort((a, b) => a - b);
|
times.sort((a, b) => a - b);
|
||||||
let start = 0;
|
let start = 0;
|
||||||
for (let end = 0; end < times.length; end += 1) {
|
for (let end = 0; end < times.length; end += 1) {
|
||||||
while (times[end] - times[start] > windowMs) start += 1;
|
while (times[end] - times[start] > windowMs) start += 1;
|
||||||
if (end - start + 1 >= settings.clusterBotCount) {
|
if (end - start + 1 >= requiredCount) {
|
||||||
suspicious.set(key, `cluster ${end - start + 1}/${settings.clusterBotWindowSeconds}s`);
|
suspicious.set(key, `cluster ${end - start + 1}/${settings.clusterBotWindowSeconds}s`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1926,9 +1945,16 @@ function suspiciousVisitorClusters(rows, settings) {
|
|||||||
return suspicious;
|
return suspicious;
|
||||||
}
|
}
|
||||||
|
|
||||||
function visitorClusterKey(row, device = {}) {
|
function visitorClusterReason(row, device, clusters) {
|
||||||
|
for (const key of visitorClusterKeys(row, device)) {
|
||||||
|
const reason = clusters.get(key);
|
||||||
|
if (reason) return reason;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function visitorClusterKeys(row, device = {}) {
|
||||||
const ip = String(row.ip || "").trim();
|
const ip = String(row.ip || "").trim();
|
||||||
if (!ip) return "";
|
|
||||||
const country = String(row.country_code || countryFromTimezone(row.timezone) || countryFromLanguage(row.language) || "").toUpperCase();
|
const country = String(row.country_code || countryFromTimezone(row.timezone) || countryFromLanguage(row.language) || "").toUpperCase();
|
||||||
const platform = String(device.platform || "").trim();
|
const platform = String(device.platform || "").trim();
|
||||||
const ua = String(device.userAgent || "").trim();
|
const ua = String(device.userAgent || "").trim();
|
||||||
@@ -1937,7 +1963,10 @@ function visitorClusterKey(row, device = {}) {
|
|||||||
: ua.includes("Firefox/") ? "Firefox"
|
: ua.includes("Firefox/") ? "Firefox"
|
||||||
: ua.includes("Safari/") ? "Safari"
|
: ua.includes("Safari/") ? "Safari"
|
||||||
: ua.slice(0, 80);
|
: ua.slice(0, 80);
|
||||||
return [ip, country, platform, browser].join("|");
|
const deviceKey = [country, platform, browser].join("|");
|
||||||
|
const keys = [`device|${deviceKey}`];
|
||||||
|
if (ip) keys.unshift(`ip|${ip}|${deviceKey}`);
|
||||||
|
return keys.filter((key) => key.replace(/^(ip\|[^|]*\||device\|)/, "").replace(/\|/g, "").trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
function conversationDetails(conversation) {
|
function conversationDetails(conversation) {
|
||||||
|
|||||||
Reference in New Issue
Block a user