Spaces:
Running
Running
| 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; | |
| } | |
| } | |