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;
    }
}