Bots und Tracking Infos

Hier mal meine Infos dazu.

In den letzten Jahren gibt es einen immensen Unterschied zwischen den Statistiken, die durch den Webhoster erstellt werden und meinem Matomo Tracking. Hierbei ist ein relevanter Unterschied in den Websites, auf denen getrackt wird, zu beachten.

Die Unterschiede nehmen mit der Anzahl der Webpages pro Website zu. Die Ursache dafür ist bisher nicht eindeutig, sondern nur vermutend ausgemacht worden.

Website mit mehreren 10.000 Pages:
Webhost-Stats: 10.000 Visits/Month
Matomo-Stats: 5.000 Visits/Month

Bei diesen Visits viel auf, dass sehr viele mit Direct View und Verweilzeit von 0 seconds oder weniger als 10 seconds dabei waren. Dabei wurden die ersten 10 Sekunden ein Ping per Intervall jede Sekunde gesendet, damit die Verweilzeit deutlicher wird.

Diese Direct View + 0 seconds sind sehr wahrscheinlich Javascript Bots, die nicht erkennbar sind. Die IP verweist in der Pay GeoIP Datenbank auf Services anstatt auf Provider.

Diese Javascript Bots sind nicht erkennbar. Sie sind käuflich erwerbbar. Sie werden von vielen verschiedenen Firmen betrieben. Einige darunter mit dem Portfolio für Sicherheit im Internet. Die Websites werden von diesen auf Gefahren gecheckt und die Ergebnisse in Datenbanken gespeichert, die verkauft werden. Big Data.

Die Anzahl der Bots-Visits steigt mit der Anzahl der Webpages pro Website. Besteht eine Website aus nur einer Webpage, steht nur diese eine in den Listen der Bots und wird in einem Intervall von 1/Woche oder 1/Monat, selten auch 1/Tag besucht. Besteht eine Website aus 10.000ten Webpages, stehen all diese in den Listen der Bots und werden nicht alle bei einem, sondern bei vielen Besuchen besucht. Damit entstehen sehr viele Bots-Visits.

Nach verschiedenen Versuchen diese Direct View + 0 seconds herauszufiltern, wurde ein Javascript-Banner mit Text und Buttons eingerichtet, dass erst nach Button-Klick getrackt wird. Relevant dabei ist, dass diese Button-Hürde selbst pogrammiert wird und sich von anderen unterscheidet. Im Internet angebotene Button-Hürden, wie Consent-Banner, sind weit verbreitet, weswegen es sich lohnt, dass die Überwindung derselben in JavaScript-Bots oder das Ausblenden derselben in AdBlocker inkludiert wird.

Sofort nach Aktivierung dieser Button-Hürde vielen die Visits bei der Website mit den 10.000ten Webpages auf nur noch 10 % gegenüber vorher. Bei denen mit nur einer Webpage blieben die Direct View + 0 seconds weg. Es ist zu beobachten, dass mit dieser Hürde die Besucher viel wahrscheinlicher Menschen als Bots sind.
Es gibt zudem nochmals einen Unterschied bei der Anzahl der zu klickenden Buttons, sowie der Gestaltung dieser Hürde.

Die Visits nehmen mit der Schwere der Hürde ab. Muss nur 1 Button geklickt werden sind es 20/Tag Visits. Müssen zwei Buttons geklickt werden, sind es 15/Tag. Bei drei Buttons nur noch 10/Tag. An der Variante einer Hürde mit mehreren Buttons scheitern also vermutlich auch menschliche Besucher. Während vermutlich bei nur einem Button auch noch ein paar wenige Bots durchkommen. Sie ist also nicht optimal.

Eine optimale Lösung besteht nur mit der Verwendung einer Pay GeoIP Datenbank und dem Erkennen des ISP (Internet Service Provider), bzw. ob die IP von einem ISP kommt oder nicht. Eine andere Methode besteht in der Erkennung, ob die IP von einem Service (Cloud, etc.) anstatt einem Provider kommt. Das Erkennen der ASN (Autonomous System Number) bringt es nicht. Mit den Free Geo Datenbanken von DB-IP und Maxmind ist ISP detection oder Provider|Service nicht zu machen.

Mehr Infos zu den Versuchen diese Bots nicht zu tracken gibt es in einem Issue des Matomo Plugins Tracking Spam Prevention: https://github.com/matomo-org/plugin-TrackingSpamPrevention/issues/112

Es gibt eine viel einfachere Lösung, um “richtigen” von “falschem” Traffic unterscheiden zu können, wenngleich es dafür Programmierkenntnisse braucht. Das vermutlich markanteste Identifizierungsmerkmal von falschem Traffic sind die Request Header, die bei Spam Bots in 99,99% aller Fälle unzureichend sind und genau diese unzureichenden Request Header lassen sich herausfiltern, sodass nur bei validen Headern der Matomo JS Schnipsel ausgeführt wird. Funktioniert bei mir seit inzwischen 10 Jahren, sodass ich “natürliche” von “unnatürlichen” Requests getrennt voneinander tracken kann.

Zu unnatürlichen Requests zähle ich reguläre Bots für die ich die Matomo PHP API verwende. Ich kenne deswegen nicht nur das Verhalten von validen Nutzern, sondern auch welche Bots und sonstiges Zeugs so auf meinen Seiten treiben, wenngleich ich nur den erwünschten Bots Zugriff gewähre. Alle anderen werden per CloudFlare WAF rausgefiltert.

Schön und gut Serpent_Diver. Dass etwas mit den Angaben dieser JavaScript-Bots nicht stimmt, ist mir auch schon aufgefallen. Dazu zählt die Angabe der Bildschirmauflösung bei Mobile Devices. Diese passt nicht zu den angegebenen Devices.

Wie erkennst du diese JS-Bots? Welche Merkmale der Request Header verwendest du zur Erkennung? Ist dein Script ein Geheimnis?

Tracking und Traffic sind zwei unterschiedliche Angelegenheiten. Hier geht es nur um Tracking.

Wenngleich ich dir nur ungern widerspreche, aber wenn kein Traffic, dann auch kein Tracking. Demzufolge spielt es keine Rolle, ob es um Traffic oder Tracking geht.

Richtig oder Richtig? :wink:

Wie bereits erwähnt oder angedeutet, spielt es bei der Identifizierung von egal welchem Request keine Rolle, ob JS verwendet wird oder nicht. Zumal jeglicher Versuch es trotzdem zu tun in hohem Maße fehlertolerant ist. Die besagten Request Header sind absolut kein Geheimnis. Wir reden hier ausschließlich über standardisierte HTTP Header. Vor dem Hintergrund, dass es schon seit unzähligen Jahren (fast) nur mehr noch 3 Browser Hersteller gibt, ergeben sich daraus nur 3 (eigentlich nur 2) typische Header Muster für den Request. Das macht selbst für denjenigen, der zumindest die Browser Dev Console öffnen kann, spielend einfach zumindest die Request Header einzusehen.

Wenn Du nicht nur neugierig bist, sondern es reproduzieren willst, was ich hier anpreise, dann nutze z.B. PHP getallheaders(), um die Request Header vorübergehend zu loggen und wann immmer dir ein Request spanisch vorkommt, dann untersuche über die IP Adresse die jeweiligen Request Header. Dann wirst du sehr schnell herausfinden, was daran auffällig ist. Das wäre das mindeste, was Du tun müsstest damit wir diese Diskussion fortführen können, weil es noch eine weitere Eskalationsstufe braucht, um auf die besagten 99,99% Erfolgsrate zu kommen.

Es geht hier nicht um Traffic blockieren, sondern um Tracking blockieren.

Es sind zuviele Requests um diese jeweils zu untersuchen. Deswegen auch die Intention mit der Möglichkeit von IP-Datenbanken.

Mein bisheriges Filtern:

  • DNT (PHP)
  • erkennbare Bot Header und empty Header
  • Button click

getallheaders() sind mir erstmal zuviel Informationen. Für den Anfang erstmal nur time - REMOTE_ADDR - HTTP_USER_AGENT speichern und mal sehen.

Du brauchst aber alle Request Header. Andernfalls brauchst Du erst gar nicht weitermachen.

kein Streß till Death.

Also … 1500 Visits, ergibt 165 verschiedene User Agents. Bei den User Agents ist ein Muster zu erkennen. Bei den meisten Visits ist der User Agent allerdings leer - diese machen 15 % aus. Also müsste bei diesen gecheckt werden, ob bei diesen weitere Header Informationen vorhanden sind. Aufwendig … Oder einfach leere User Agents blocken? Moment mal … remember remember … leere User Agents werden (bei mir) als Headless Browser gehandelt und blockiert.

Du scheinst meine Ansage, dass Du den Fokus auf die Request Header legen musst, weiterhin zu ignorieren. Der UA gehört zwar auch zu den Request Headern, aber Du verfällst einem fatalen Irrtum, wenn Du den UA als alleiniges Merkmal verwendest. Zumal es doch hinreichend bekannt sein sollte, dass man den UA spielend einfach faken kann. Das gilt zwar auch für alle anderen Request Header, ist besonders bei den SpamBots aber eher weniger bis gar nicht verbreitet, weil klassische Filter wie z.B. eine WAF Request Header als Filtermerkmal zumeist nicht oder nur sehr geringfügig unterstützen.

Wenn Du das Thema wirklich ernstnehmen willst und daraus keine sinlose Diskussion wird, bitte ich Dich ein weiteres mal Dich an den Request Headern zu orientieren. (exklusive UA).

Eins nach dem anderen. Kommt Zeit, kommt Rat.

Die Visits mit leerem User Agent (headless Browser) werden prozentual immer mehr. Sind jetzt bei 27 %. Auffallend bei diesen sind zB 70 Requests in Folge in 60 Sekunden, bei Websites mit 1 Webpage + Imprint-Pages.

Auffallend auch IPs mit Doppel- bis Dreifach-Requests in der selben Sekunde.

Mach doch hier mit: https://github.com/matomo-org/device-detector/issues/7979

Sorry, wenn ich mich so direkt ausdrücke, aber Du verschwendest sinnlos Zeit damit, wenn Du dich an Dingen orientierst, die alles andere als zweckdienlich sind. Den Matomo Device Detector kannst du für den konkreten Fall in die Tonne treten.

Ja, sorry ist notwendig, weil klingt echt seltsam. So Geheimniskrämerei. Und am Ende sei man selbst schuld, weil zu doof, und du die einzige Person die es checkt.

getallheaders()
Auch da gibt es Muster. Aber auch da wie beim User Agent false positive bei der Erkennung der Muster, weil es immer Ausnahmen gibt.

false positive:

net-htp.de Cable/DSL

X-Acl-Runtime - 0.000
X-Lima-Vid - 1234567890
X-Lima-Uid - 1234567890
X-Upstream - nestis
X-Document-Root - /path/path/path/path
X-Forwarded-For - 0.0.0.0
X-Forwarded-Proto - https
X-Lima-Id - abcdefghijklmnopqrstuvwxyz
Accept-Encoding - gzip, deflate, br
Accept-Language - de-DE,de;q=0.9
User-Agent - WhatsApp/2.34.56.7
Range - bytes=0-123456
Accept - */*
Host - www.___.de
1&1 Versatel

X-Acl-Runtime - 0.000
X-Lima-Vid - 1234567890
X-Lima-Uid - 1234567890
X-Upstream - nestis
X-Document-Root - /path/path/path/path
X-Forwarded-For - 0.0.0.0
X-Forwarded-Proto - https
X-Lima-Id - abcdefghijklmnopqrstuvwxyz
Accept-Encoding - gzip, deflate, br
Accept-Language - de-DE,de;q=0.9
Accept - */*
User-Agent - NetworkingExtension/123.4.5.67.8 CFNetwork/1234.567.89 Darwin/12.3.4
Host - www.___.de

Mal angenommen das sind keine Bots, sondern Humans, weil Visit von einem ISP (Provider). Dann wird es bei diesen mittels Mustererkennung ein false positiv geben.

Die allgemein vorhandenen Muster:
Array Key fehlt:

Sec-Fetch-Dest
Sec-Fetch-Mode
Sec-Fetch-Site

Array Key Placeholder:

Accept => */*

Array Value empty:

Accept-Language =>

Array Key fehlt:

Accept
Accept-Language

Aus diesen kann eine komplex verschachtelte if-Regel mit || und/oder && erstellt werden, aber wie auch immer diese gestalten wird, es wird false positive geben.

Es geht nicht um Geheimniskrämerei, sondern um Transparenz und Nachvollziehbarkeit. Was hilft es als “Entwicklungshelfer” die Lösung vorzukauen, wenn nicht nur du, sondern auch jeder andere Leser nicht versteht, worum es geht.

Sind wir uns da einig?

Schon klar. Ist halt deine Art. Kein Problem.

Na dann teile mal mit wie viele false positive deine Lösung (geschätzt) hat? Und teile mal mit, was du zu den von mir erwähnten zwei Beispielen denkst.

Es ist nicht meine Art, sondern die einzige Art, um der breiten Masse eine Lösung zu vermitteln. Wärst Du der einzige, würde ich Geld von Dir für eine vorgekaute Lösung verlangen.

Es gibt kein false positive, sondern lediglich darum die einzigartigen Merkmale, im Sinne von bestimmten Headern als Identifier zu verwenden und das sind in erster Linie die Sec-Fetch-* header. Alle Browser bis auf den Apple Safari Browser senden diese Header, aber klassiche Spam Bots und auch alle anderen Bots verwenden diese Header nicht. Das grenzt das Ganze schon mal dramatisch ein. Es braucht aber noch eine Möglichkeit, um den Safari Browser zu selektieren, bzw. zu identifizieren. Deswegen meine vorherige Rede von Eskalationsstufen. Verinnerliche dir das zunächst.

Fortsetzung erfolgt aus Zeitgründen später.

Es sei an dieser Stelle angemerkt, dass diese hier beschriebene Lösung für die native Verwendung des Matomo Javascript Schnipsel ausgelegt ist. Wer also z.B. das Matomo Plugin für WordPress verwendet, kann sich die Zeit sparen weiter zu lesen.

Das gleiche gilt, wenn ein HTTP Page Cache verwendet wird, also z.B. LiteSpeed, WP Rocket und andere WordPress Cache Plugins, sowie Varnish.

Tja, das ist überheblich. Es gibt immer false positiv. Es kommt nur drauf an wie viele und ob diese toleriert werden können. Nichts ist perfekt.

Das mit den Array-Keys Sec-Fetch-*, da ist was dran, aber …
Es gibt auch jede Menge Apple Safari Browser mit Sec-Fetch-*, sogar die Mehrzahl.
Habe jetzt mal eben 100 User Agents mit Safari und ohne Sec-Fetch-* gecheckt. Das sind meistens Bots von Clouds etc., doch auch hier sind Visits von ISP dabei:

**ISP: Vodafone Germany**
X-Acl-Runtime - 0.000
X-Lima-Vid - 1234567
X-Lima-Uid - 123456
X-Upstream - nestis
X-Document-Root - /path/path
X-Forwarded-For - 98.76.543.21
X-Forwarded-Proto - https
X-Lima-Id - abcdefghijklmnop
Accept-Encoding - gzip, deflate, br
Accept-Language - en-GB,en;q=0.9
Accept - */*
User-Agent - Mozilla/5.0 (iPhone; CPU iPhone OS 18_0_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1 Ddg/18.0
Host - www.---.de
**ISP: 1&1 Versatel**
X-Acl-Runtime - 0.000
X-Lima-Vid - 1234567
X-Lima-Uid - 123456
X-Upstream - nestis
X-Document-Root - /path/path
X-Forwarded-For - 98.76.543.21
X-Forwarded-Proto - https
X-Lima-Id - abcdefghijklmnop
Accept-Encoding - gzip, deflate, br
Accept-Language - de-DE,de;q=0.9
Accept - */*
User-Agent - Mozilla/5.0 (iPhone; CPU iPhone OS 18_1_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.1 Mobile/15E148 Safari/604.1 Ddg/18.1
Host - www.---.de

Diese sind mittels getallheaders() von den anderen, die von Clouds etc. kommen, nicht zu unterscheiden. Sind diese beiden bei dir auch Bots? Es gibt also immer auch false positiv, oder zumindest uneindeutige Treffer.

EDIT: die hier zuerst weiter oben geposteten beiden Beispiele sind anhand der User Agents als Bots identifizierbar.

Neben false positive gibt es auch positive false:

**iCloud Private Relay**
X-Acl-Runtime - 0.001
X-Lima-Vid - 1234567
X-Lima-Uid - 123456
X-Upstream - nestis
X-Document-Root - /path/path
X-Forwarded-For - 98.76.543.21
X-Forwarded-Proto - https
X-Lima-Id - abcdefghijklmnop
Accept-Encoding - gzip, deflate, br
Priority - u=0, i
Accept-Language - de-DE,de;q=0.9
Sec-Fetch-Mode - navigate
Sec-Fetch-Site - cross-site
Referer - https://duckduckgo.com/
Accept - text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
User-Agent - Mozilla/5.0 (iPhone; CPU iPhone OS 18_2_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.2 Mobile/15E148 Safari/604.1
Sec-Fetch-Dest - document
Host - www.---.de

Diese Header liefert nichts zum Erkennen und kommt von einer Cloud, ist also sehr wahrscheinlich ein Bot. Oder die Geo IP Datenbanken haben hier einen Fehler.

@melbao

Lust auf eine neue Runde oder hast schon aufgegeben? :wink:

Na war ja meine Frage, wie du die zuletzt geposteten getallheaders() richtig als Human oder Bot erkennen willst?

Hey @Serpent_Driver , soll ich mal genauso fragen wie du?

Hast du schon aufgegeben?

Was wäre denn deiner Ansicht nach eine nächste Runde?

Wieso sollte ich aufgegeben haben? Mein vorletzter Post war gestern vor 1 Woche und da ich am Wochenende was besseres vorhabe, habe ich dich am darauffolgenden Montag gefragt, ob Du Lust auf eine neue Runde hast. Deine Antwort ließ aber auf sich warten…

Aber egal, um die Diskussion nicht noch weiter in die Länge zu ziehen, werde ich voraussichtlich am Montag die ganze Lösung präsentieren.

Wie angekündigt, kommt hier nun die finale Lösung. Da ich diese nicht exklusiv für Dich bereitstelle, sondern für jeden anderen, der daran Interesse hat, aber nicht Programmierer ist, will ich zunächst beschreiben worauf die Logik dieser Lösung beruht.

Es gilt vorab anzumerken, dass ich diese Lösung zusammen mit Matomo aka Piwik seit inzwischen 10 Jahren erfolgreich und mit 99,99%iger Erfolgsrate anwende. Warum nicht 100%? Weil 100% unrealistisch sind, zumal 100% bedeuten würde, dass man eine falsche Erkennung dafür in Kauf nehmen müsste und das will ja keiner. Falsche Erkennung würde bedeuten, dass ein wenn auch nur kleiner Anteil nicht getracked werden würde.

Die Lösung beruht nicht darauf eine Whitelist definieren zu müssen, um die vorher erwähnte falsche Erkennung zu verhinden. Die ganze Lösung ist eine einzige Whitelist, heißt, mit dem nachfolgenden Code wird definiert, was ein “natürlicher” Nutzer ist, was das Definieren einer Blacklist obsolete macht.

Die besagte Lösung ist zwar nicht gänzlich wartungsarm, aber mindestens so wartungsarm, dass man nicht öfter als alle 6 oder 9 Monate darauf achten sollte wie oft die gängigen Browser aktualisiert wurden.

Was es mit dem zuletzt genannten auf sich hat, erklärt sich wie folgt:

Das Lösungsprinzip basiert zum Einen auf dem User-Agent und zum anderen auf den Sec-Fetch- * Request Headern. Beim User-Agent geht es aber nicht um den gesammten UA String, sondern um typische Namen, also Chrome, Firefox usw. Außerdem spielt die Versionsnummer der gebräuchlichen Browser eine wichtige Rolle. Heißt folglich, wenn diese eindeutigen Namen und die Versionsnummer fehlen, hat man bereits damit ein wichtiges Identifikationsmerkmal, aber das reicht noch nicht.

Seit bereits mehreren Jahren verwenden alle gängigen Browser die Sec-Fetch-* Header mit 4 Header an der Zahl, sodass alle 4 Sec-Fetch-* Header für eine eindeutige Identifikation gesendet werden müssen. Der Safari Browser sendet diese Header erst seit der Version 16.4. Da dadurch für alle vorherigen Safari Versionen eine Lücke entsteht, gilt es diese Lücke gesondert zu behandeln, bzw. zu schließen und auch dabei spielt die Versionsnummer eine Rolle.

Wenn es nun um die Versionsnummern geht, dann habe ich für mich definiert, dass bei Chrome und Firefox basierten Browsern mit Versionen <= 120 dies kein natürlicher Nutzer sein kann. Gleichermaßen gilt dies auch für den Safari Browser, wenn Version <13 ist. Dass ich genau bei diesen Versionsnummern die Grenze setze, ist keine Schätzung. Bevor ich diese Lösung angewendet habe, habe ich zunächst mal über fast 1 Jahr maximal mögliches Logging betrieben, um ein falsches Tracking auszuschließen. Außerdem verwende die Versionsnummern als Filter für die CloudFlare WAF, aber nicht ausschließlich damit. Eine nahezu tägliche Kontrolle, was CloudFlare blockt ergänzt, bzw. bestätigt, dass diese Lösung wie erwartet funktioniert.

So, genug beschrieben. Hier kommt der Code:

function lcSafari() {
    $lcUserAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';

    if (stripos($lcUserAgent, 'Safari') !== false &&
            preg_match("/\b(iphone|ipad|macintosh|mac os x)\b/i", $lcUserAgent) &&
            !preg_match("/\b(chrome|firefox|edge|opera|applebot)\b/i", $lcUserAgent)) {

        // Safari-Version extrahieren
        if (preg_match("/Version\/(\d+)/i", $lcUserAgent, $matches)) {
            $safariVersion = (int) $matches[1];

            if ($safariVersion > 12) {
                return true;
            }
        }
    }

    return false;
}

function lcSecHeaders() {
    $requested_headers = getallheaders();
    if ((strtolower(isset($requested_headers['sec-fetch-dest'])) && strtolower(isset($requested_headers['sec-fetch-mode'])) && strtolower(isset($requested_headers['sec-fetch-site'])) && strtolower(isset($requested_headers['sec-fetch-user'])))) {
        return true;
    }
    return false;
}

function lcModernBrowser() {
    $lcUserAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';

    if (preg_match("/(chrome|firefox|edge)[\/\s]?(\d+)/i", $lcUserAgent, $matches)) {
        $browser = strtolower($matches[1]);
        $version = (int) $matches[2];

        if ($version > 120) {
            return true;
        }
    }

    return false;
}

function lcModernAppleDevice() {
    $lcUserAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';

    if (preg_match("/\b(chrome|firefox|edge|applebot)\b/i", $lcUserAgent)) {
        return false;
    }

    if (preg_match("/\b(iphone|ipad|macintosh|mac os x)[\/\s]?(\d+)/i", $lcUserAgent, $matches)) {
        $version = (int) $matches[2];

        if ($version > 12) {
            return true;
        }
    }

    return false;
}

function lcIsHumanUser() {
    $isSafari = lcSafari();
    $isModernBrowser = lcModernBrowser();
    $hasSecHeaders = lcSecHeaders();
    $isModernApple = lcModernAppleDevice();

    // Debugging
    #error_log("[DEBUG] lcIsHumanUser - lcSafari: " . json_encode($isSafari));
    #error_log("[DEBUG] lcIsHumanUser - lcModernBrowser: " . json_encode($isModernBrowser));
    #error_log("[DEBUG] lcIsHumanUser - lcSecHeaders: " . json_encode($hasSecHeaders));
    #error_log("[DEBUG] lcIsHumanUser - lcModernAppleDevice: " . json_encode($isModernApple));
    // Echte Nutzerbedingungen
    if ($isSafari || ($isModernBrowser && $hasSecHeaders) || $isModernApple) {
        return true;
    }

    // Falls die Browser-Version > 120 ist, aber keine Sec-Fetch-Header vorhanden sind → verdächtig!
    if ($isModernBrowser && !$hasSecHeaders) {
        #error_log("[DEBUG] Fake-User erkannt: Moderner Browser aber keine Sec-Fetch Header");
        return false;
    }

    // Falls die Browser-Version ≤ 120 ist und Sec-Fetch-Header existieren → unwahrscheinlich, aber möglich
    if (!$isModernBrowser && $hasSecHeaders) {
        #error_log("[DEBUG] Ungewöhnlicher Fall: Alter Browser sendet Sec-Fetch Header");
        return false;
    }

    return false;
}

if (lcIsHumanUser()) {
	// Matomo Javascript Schnipsel
}