Spaces:
Running
Running
File size: 1,894 Bytes
45dc401 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | <?php
declare(strict_types=1);
namespace App\Domain\Analytics;
/**
* UA-based bot heuristic. Returns a score 0–100.
* 0 = confident human.
* 100 = definite bot.
* Callers should treat >= 80 as "bot" and discard or flag the event.
* The score is stored on analytics_events.bot_score so the threshold
* can be adjusted in queries without re-processing events.
*/
final class AnalyticsBotDetector
{
/** Known crawler / monitoring user-agent substrings (lowercase). */
private const CRAWLER_PATTERNS = [
'bot', 'crawler', 'spider', 'scraper', 'slurp', 'facebookexternalhit',
'googlebot', 'bingbot', 'yandex', 'baiduspider', 'duckduckbot', 'applebot',
'twitterbot', 'linkedinbot', 'whatsapp', 'discordbot', 'telegrambot',
'headlesschrome', 'phantomjs', 'selenium', 'puppeteer', 'playwright',
'wget', 'curl', 'python-requests', 'go-http-client', 'java/', 'okhttp',
'axios/', 'node-fetch', 'libwww-perl', 'httrack', 'archive.org',
'pingdom', 'uptime', 'statuspage', 'newrelic', 'datadog', 'nagios',
'prerender', 'rendertron', 'netlify', 'vercel-monitoring',
];
public static function score(string $ua): int
{
if (trim($ua) === '') {
return 100;
}
$lower = mb_strtolower($ua);
foreach (self::CRAWLER_PATTERNS as $pattern) {
if (str_contains($lower, $pattern)) {
return 100;
}
}
// Suspiciously short or simplistic UAs
if (mb_strlen($ua) < 20) {
return 90;
}
// No recognisable browser token — likely a non-browser HTTP client
$hasBrowserToken = (bool) preg_match(
'/(Mozilla|Chrome|Safari|Firefox|Edge|Opera|Trident)/i',
$ua,
);
if (!$hasBrowserToken) {
return 70;
}
return 0;
}
}
|