Start » Blog » Analytics
27.02.2026

Tracking Audits per Claude? Wie ich meine Lösung gefunden habe

Ich prüfe Tracking-Setups. Oft - und manuell. Browser auf, DevTools auf, Netzwerk-Tab filtern, Consent-Banner klicken, schauen was passiert, Cookie-Liste durchgehen, dataLayer inspizieren, Consent Mode Parameter entziffern. Für jede einzelne Phase: Pre-Consent, Post-Accept, Post-Reject. Und dann manchmal nochmal für den typischen E-Commerce-Pfad - zumindest bis vor dem Kaufabschluss, um in den dataLayer zu starren und Inkonsistenzen zu finden. BTW: Dabei muss ich gelegentlich immer noch Erinnerungen an Universal Analytics abschütteln, wenn ich auf das vertraute, aber veraltete Format treffe. Egal. Das ist jedenfalls theoretisch gründlich, aber es skaliert nicht - und es ist fehleranfällig, weil ein Mensch beim dritten Audit am Tag Dinge übersieht, die beim ersten noch aufgefallen wären.

Daher habe ich schon länger darüber nachgedacht, genau diesen Vorgang zu automatisieren. Selbst geschrieben (wie es bisher meine Art war) ist das allerdings angesichts der vielen Variablen schwierig, komplex, zeitraubend und fehleranfällig. Deshalb bin ich es auch nie angegangen - bis in dieser Woche. Mit Claude, Node.js, Playwright und einer wachsenden Sammlung von Erkenntnissen, die man nur bekommt, wenn man es wirklich baut und verwendet. Dieser Post dokumentiert den Weg - und erzählt zugleich eine (vielleicht) typische Geschichte, wie man es endlich schafft, nicht mehr selbst der Flaschenhals zu sein. Denn: Skepsis gegenüber der Idee, eine solche Analyse einer KI zu überlassen, ist berechtigt. Warum und wie ich es dennoch getan habe, ist hier dokumentiert.

Vorbemerkung 1: Das ist keine Anleitung

Wenn Du nur hier bist, weil Du wissen willst, was ich da gebaut habe, wie es funktioniert und ob das was für Dich ist, brauchst Du nichts von dem zu lesen, was hier noch folgt. Steige einfach heimlich aus zum Repo des "Tracking Auditor" auf GitHub, ich nehme es Dir nicht übel. Das hier ist ein echt langes Brot und erzählt "nur" die Entstehungsgeschichte, nicht die Bedienung.

Vorbemerkung 2: Der alte Mann und die KI

Mein beruflicher Werdegang hat schon seit der Diplomarbeit immer etwas mit Softwareentwicklung zu tun. Von Commodore 64 Basic nach der Schule bei Karstadt, weil es noch dauerte, bis ich einen eigenen Commodore daheim hatte über Logo, Fortran, Turbo-Pascal, Clipper, VB, Delphi, C++ und erst viel später HTML, CSS und JavaScript, PHP und Python war eines immer eine Konstante: Ich schreibe Code, ich teste Code, ich sitze am Steuer. Auch in leitender Position habe ich nie den Kontakt verloren und immer eigene Projekte gehabt, in denen ich nicht nur beschrieben habe, was die Anwendung machen soll, sondern auch geschrieben habe, wie genau das funktionieren soll. UI, Logik, Datenbank, Protokolle: Alles meins.

Das mag erklären, warum ich Machine Learning im Zusammengang mit Daten immer interessant fand, bei generativer KI im Zusammenspiel mit Code aber lange nicht über die Copy & Paste Phase hinausgekommen bin. "Assisted Coding" über Vervollständigung im Editor oder gepromptete isolierte Lösungen für Funktionen im Gesamtkonstrukt eines Projekts hinaus habe ich zu früh erprobt: Ungefragtes Refactoring, zerstörter Code und unsinnige Diskussionen mit noch jungen Modellen (nicht, dass Diskussionen sich mit fortgeschrittenen Modellen mehr lohnen: Du diskutierst mit einer Buchstabenvorhersagemaschine, vergiss das nie!) haben für mich den Boden nachhaltig verbrannt. Ich habe weder Effizienzgewinn noch echte Hilfe wahrgenommen. Nutzung in der IDE daher recht lange bei Null. "Die paar Zeilen schreibe ich doch einfach selbst". Wer selbst schon mehr als 40 Jahre den Code-Griffel geschwungen hat, kann mir die späte Einsicht vielleicht nachsehen.

Erst Ende des vergangenen Jahres konnte ich aber einfach nicht mehr übersehen, dass ich umdenken muss. Software, Websites oder andere Dinge mit Hilfe von Kommandozeilen-Versionen von Gemini, Codex - und in meinem Fall vor allem Claude - zu erstellen oder in Brownfield Projekten endlich die ganzen unerledigten Dinge anzugehen, ist etwas anderes, als sich beim Coding unter die Arme greifen zu lassen. Ich habe es nur erst sehr spät begriffen, dass meine Erfahrung hier kein Nachteil, sondern ein Vorteil ist. Ich weiß, wie man Anforderungen spezifiziert. Ich habe jahrelange Erfahrungen in nie niedergeschriebenen Prozessen, mit denen ich an komplexere Aufgaben gehe. Daher bin ich nie in die "Bau mir mal ein cooles Kanban Board, das alles drauf hat, was man so braucht" Sackgasse geraten, sondern habe spezifiziert, KI hat implementiert und ich immer weniger selbst verifiziert, sondern TDD von Anfang an ernst genommen. Muss man mit "seiner KI" wie mit einem Kollegen reden? Ich weiß es nicht. Aber ich habe jahrelang mit Kolleginnen und Kollegen über Anforderungen, mögliche Lösungen und Nebenwirkungen auf andere Funktionen diskutiert und mich stets iterativ dem Ziel genähert. Genauso läuft es jetzt auch, mit hohem Einsatz vor der Implementierung, flexiblem Reagieren auf unerwartete Ergebnisse und stets offen für Verbesserungen oder spontane Inspiration für neue Details oder Funktionen. Nur dass ich mit der Implementierung eben nur noch rudimentär zu tun habe und Code eher lese und fast gar nicht mehr schreibe.

Nach dieser langen Einleitung ist der folgende "Reisebericht" über die Entstehung einer Toolbox, die eigentlich sehr überschaubare Aufgaben zu erledigen hat - aber leider auf einem superkomplexen und heterogenen Spielfeld - hoffentlich nachvollziehbar. Und wenn ich damit noch jemanden über die Kante schubsen kann, den Code-Griffel aus der Hand zu nehmen: Prima.

Disclaimer: Die Idee zu diesem Post entstand spontan und während der Arbeit an dieser Toolbox: Warum nicht Claude bitten, unsere ganzen Diskussionen, Fakten zu den Audits und zu checkenden Punkten und Learnings aus dem Umwegen, die wir in diesem noch jungen Projekt (es ist zu diesem Zeitpunkt nicht mal eine Woche alt) zusammenzufassen? Das Ergebnis des Experiments findet sich im Folgenden. Konsequenterweise hat ab hier Claude 99% geschrieben und wir haben uns "nur" über den Inhalt abgestimmt. Denn so zerbröselt der Keks nunmal. Ab jetzt auch bei mir 😉

Warum Scripts statt KI-Analyse?

Bevor es losgeht, eine grundlegende Design-Entscheidung, die das gesamte Projekt prägt: Die Audit-Scripts sind deterministisch. Sie laufen immer gleich ab, schauen immer an denselben Stellen nach, produzieren immer das gleiche Report-Format. Kein Prompt, kein "schau mal ob du was Auffälliges findest", kein probabilistisches Raten. Der Grund dafür ist einfach: In einem Umfeld, in dem es jede Menge verschiedene Consent-Banner gibt, Custom Loader ihre Payloads Base64-encodieren, CSP-Violations still und heimlich Requests blockieren und der Unterschied zwischen einem harmlosen Tag-Manager-Script und einem datenschutzrelevanten Collect-Request im Prefix eines einzigen URL-Parameters stecken kann - in so einem Umfeld ist eine gepromptete KI-Analyse reines Glücksspiel. Man bekommt bei jedem Durchlauf andere Ergebnisse, übersieht mal das eine, mal das andere, und kann das Resultat nicht reproduzieren.

Was man aber sehr wohl mit KI machen kann (und sollte): die generierten Daten auswerten. Die Scripts laufen über die CLI mit Parametern oder - und hier wird es komfortabel - über Skills, die Claude Code als natürliche Schnittstelle nutzen kann. Du sagst: "audit für example.com, CMP ist Cookiebot, mit E-Commerce-Pfad" und die KI übersetzt das in den richtigen CLI-Aufruf, wartet auf den Report und liefert eine Zusammenfassung. Und danach kann man den Daten Fragen stellen.

Zum Beispiel:

  • "Feuert GA4 wirklich Pre-Consent und wie sehen die Consent Paramater aus?"
  • "Stimmen die Consent Mode Parameter auf den Folgeseiten nach Zustimmung?"
  • "haben wir consent defaults oder nur updates?"

Fragen, bei denen KI hervorragend ist, weil sie auf konkreten, deterministisch erhobenen Daten arbeitet statt auf einer vagen Browsing-Session. Best of both worlds: deterministische Erhebung, intelligente Auswertung. Klingt zu schön um wahr zu sein? Stimmt in Teilen auch - aber wenn man 90% ganz durchbekommt und der Rest erkennbar scheitert und nicht behauptet, alles zu wissen, kann das eine große Hilfe sein. Und wenn man dann mit einem "manuellem Modus" die Erhebung dennoch abschließen kann, wird ein Schuh draus.

Consent Banner: Barriere und Kernstück einer "Consent Analyse"

Das CMP-Problem

Bevor man bei einem Tracking-Audit irgendetwas Sinnvolles prüfen kann, muss man den Consent-Banner bedienen - erst Accept klicken, später im gleichen Audit auch Reject, und zwar in einem komplett frischen Browser, damit die erste Zustimmung das Ergebnis nicht verfälscht. Das klingt nach einer Kleinigkeit, entpuppt sich aber als erstaunlich widerspenstiges Problem, sobald man es automatisieren will.

Denn es gibt nicht den einen Consent-Banner. Es gibt Cookiebot, OneTrust, Usercentrics (in zwei komplett verschiedenen Versionen, die praktisch nichts miteinander gemeinsam haben), Borlabs Cookie, CCM19, consentmanager.net, Axeptio, Didomi, CookieHub - und noch reichlich weitere, die mir in der freien Wildbahn begegnet sind und die ich z. B. in meinem GTM & CMP Helper (kennt um die 70 CMPs) bereits in anderem Kontext behandle. Jede davon bringt - was für ein Audit Tool leider sehr relevant ist - eigene CSS-Selektoren mit, eine eigene DOM-Struktur, eigenes Timing beim Laden... und auch heute noch haben viele Betreiber per Konfiguaration recht kreative Vorstellungen davon, wo genau der Reject-Button platziert sein sollte.

Mein erster Ansatz war eine JSON-Library mit Accept- und Reject-Selektoren pro CMP - eine Art Fingerprint-Datenbank für Consent-Banner:

{
"cookiebot": {
"detect": ["#CybotCookiebotDialog"],
"accept": "#CybotCookiebotDialogBodyLevelButtonLevelOptinAllowAll",
"reject": "#CybotCookiebotDialogBodyLevelButtonLevelOptinDeclineAll"
}
}

Man lädt die Seite, iteriert über alle bekannten CMPs, prüft ob der Detect-Selektor matcht, und weiß dann, welchen Banner man vor sich hat. Die Theorie ist elegant. Die Praxis hatte aber andere Pläne.

CMPs "scripten" und was dabei schiefgeht

Bei mittlerweile 39 Einträgen in der Library sind einige Muster aufgefallen, die man vorher nicht auf dem Schirm hatte. Das sind noch lange nicht alle, die ich aus meiner Chrome Extension kenne, aber ich baue diesen Part derzeit einfach dann aus, wenn ich realen Befarf habe. BTW: Da das Proket Open Source ist, sind Erweiterungen von Dir jederzeit willkommen (mwhr dazu unten).

Selektor-Kollisionen zum Beispiel: CMP1 und CMP2 verwenden vielleicht identische data-Attribute für ihre Buttons, sodass ein reiner Check auf einen damit gebauten Accept-Selektor nicht verrät, welche CMP man gerade vor sich hat. Die Lösung war ein detect-Array mit Selektoren, die eindeutig für eine bestimmte CMP sind - erst wenn dieser Fingerprint matcht (wenn vorhanden), kommen die Action-Selektoren zum Einsatz.

Dann gibt es das Zwei-Schritt-Reject-Problem: Viele CMPs zeigen den Reject-Button gar nicht direkt an, sondern verstecken ihn hinter einem Klick auf "Einstellungen" oder "Optionen" und erst in einem zweiten Dialog kann man ablehnen. Das ist nicht zeingend der CMP, sondern eher dem Betreiber anzulasten. Dennoch erfordert das pro CMP ggf. eine weitere eigene rejectSteps-Sequenz statt eines einzelnen Selektors - und bei CMPs, die im Shadow DOM leben (und das tun sie gern), wird auch das nochmal komplizierter, weil manche Browser-APIs dort schlicht nicht funktionieren (aber das ist ein eigenes Thema für einen eigenen Post).

Und dann das Timing: Manche CMPs brauchen erstaunlich lange zum Laden. Bei Cookie Information etwa scheitert die Auto-Erkennung auf manchen Seiten rgelmäßig, obwohl ein explizites --cmp "cookie-information" als Parameter einwandfrei funktioniert. Ein Race-Condition-Problem - der Banner ist noch nicht im DOM, wenn die Detection läuft. Warum ist das so? Ich habe nicht tiefer gegraben, aber der Check scheitert in einem solchen Fall eben gut erkennbar und Ergebnisse kann man nicht fehlinterpretieren.

Der Kreislauf: Learn, Audit, besser Learnen

Die CMP-Library war übrigens der Startpunkt des ganzen Projekts. Ich habe zuerst learn.js gebaut - ein interaktives Tool, mit dem man eine Seite im Browser öffnet, die Selektoren für Accept und Reject per Klick identifiziert und das Ergebnis in die JSON-Library schreibt. Erst danach kam der eigentliche Audit-Runner audit.js.

Was ich dabei nicht vorhergesehen habe: Der Audit-Runner hat unterwegs zeitweise statt des jetzt existierenden rein manuellen Modus einen eigenen "Lernvorgang". Ich fand die Idee gut, dass man im Vorbeigehen direkt die Bibliothek ausbauen kann. Das hat am Ende aber nur learn.js besser gemacht, nicht umgekehrt. Meint: Umwege muss man sich manchmal erlauben, aber reichtzeitig erkennen, wenn es Irrwege sind. Beim Auditen echter Seiten fielen so während des Lernens von CMPs Probleme auf, die beim reinen Einlernen mit dem anderen Script unsichtbar geblieben waren - eine CMP wird erkannt, aber der Reject-Button reagiert nicht, weil ein unsichtbares Overlay darüber liegt; ein Banner verschwindet nach Accept, taucht in einer späteren Phase beim "Reject-Durchlauf" mit frischem Browser aber nicht wieder auf. Jede dieser Erkenntnisse floss zurück ins Lern-Tool, das heute Shadow DOM erkennen kann, Detect-Selektoren automatisch vorschlägt und bei bekannten Kollisionen warnt.

Und dann kam die Praxis und sagte: "Nein, so nicht." Bei vielen Audits war es schlicht schneller, den Consent manuell im Browser zu erteilen, als erst eine bislang unbekannte CMP einzulernen. Das führte zum jetzigen manuellen Modus im Audit und auch dem Compare-Tool, das z. B. einen "A/B" Test von Live vs. Stage erlaubt. Das läuft so ab: zwei Browser-Tabs offen, der Mensch klickt Banner, das Script sammelt die Daten. Die pragmatischste Form der Automatisierung ist manchmal, nur den Teil zu automatisieren, der sich lohnt, und den Rest dem Menschen zu überlassen.

Was vor dem Consent passiert (und nicht passieren sollte)

Pre-Consent: Die Stunde der Wahrheit

Die spannendste Phase eines Tracking-Audits ist Pre-Consent - also der Zustand direkt nach dem Laden der Seite in einem frischen Browser, bevor irgendein Consent erteilt wurde. Hier zeigt sich, ob ein Setup datenschutzkonform arbeitet oder ob bereits Tracker feuern, bevor der Nutzer überhaupt die Chance hatte, sich dafür oder dagegen zu entscheiden.

Was der Audit-Runner hier tut, ist im Grunde simpel: frischer Browser ohne Cache, Cookies oder localStorage starten, die Seite laden, ab dem ersten Moment alle Netzwerk-Requests mitschneiden, einen dataLayer-Snapshot nehmen und die entstandenen Cookies sowie localStorage-Einträge erfassen. Die Herausforderung liegt nicht im Sammeln, sondern im Klassifizieren: Welche dieser Requests sind überhaupt relevant?

"Google" ist nicht genug

Anfangs hatte ich eine simple Zuordnung: google-analytics.com ist Google, connect.facebook.net ist Meta, bat.bing.com ist Microsoft. Das reicht für einen groben Überblick, aber für einen echten Audit ist es viel zu ungenau, denn "Google" kann hier im Audit sechs (und der Realität noch mehr) relevante Dinge bedeuten, die aus fundamental unterschiedlich sind:

  • Google Analytics 4 - Tracking, braucht Consent
  • Google Ads - Advertising, braucht definitiv Consent
  • Floodlight - Conversion-Tracking für Display & Video 360
  • Google Tag Manager - lädt nur JavaScript, braucht (je nach Auslegung) keinen Consent
  • Google Tag (GT-Prefix) - ein Router-Container, der an andere Google-Produkte weiterleitet
  • AdSense - nochmal eine ganz eigene Kategorie

Wenn ein Pre-Consent-Report nur "Google: 3 Requests" meldet, kann das alles Mögliche heißen. Sind das harmlose Tag-Manager-Requests, die nur JavaScript laden? Oder GA4-Collect-Requests, die Nutzerdaten an Google senden? Ohne diese Unterscheidung ist der Report wertlos.

Die Lösung war eine tracking-vendors.json mit bisher überschaubaren 16 Produkten, die strikt zwischen Script-Loads (inbound - das Laden einer JavaScript-Datei) und Tracking-Endpoints (outbound - ein Datensammlungs-Request an einen Measurement-Server) unterscheidet. Beispiele:

{
"ga4": {
  "vendor": "Google",
  "product": "Google Analytics 4",
  "category": "analytics",
  "scripts": [
    { "pattern": "googletagmanager.com/gtag/js",
     "identify": { "param": "id", "match": "^G-" } }
  ],
  "endpoints": [
    { "pattern": "google-analytics.com/g/collect",
    "classify": {
    "param": "en",
    "values": { "page_view": "pageview", "purchase": "purchase" },
    "default": "event"
  } }
 ]}
}

Das identify-Feld ist dabei entscheidend: Der Host googletagmanager.com stellt sowohl ggf. GTM als auch Tags für GA4, Google Ads und Floodlight bereit - der Unterschied steckt bei den Tags ausschließlich im id-Parameter. Bei den Endpoints verrät der en-Parameter in GA4-Collect-Requests den echten Event-Namen. Zu viel Konsolidierung schadet also möglicherweise in einem Audit. Weniger Zeilen sind gut, aber nur "Event" oder "Conversion" statt page_view, add_to_cart, begin_checkout, purchase? Diese Information durch Abstrahierung zu vernichten wäre ein Fehler - der Report soll zeigen, was passiert, nicht nur dass etwas passiert.

Nicht missverstehen: Ich will hier keine disconnect.me Liste aufbauen oder komplette Muster abfangen von noch so exotischen Tools, sondern es geht um die typischen Dinge, auf denen man auf Websites so trifft.

Code Drift: Die Schattenseite bewusster Duplikation

Diese Vendor-Library entstand übrigens aus Schmerz. Die gleiche Erkennungslogik existierte zuerst in zwei Scripts - audit.js für Einzelseiten-Audits und compare.js für den Vergleich zweier Setups. Bewusst dupliziert, kein Shared Module in Form von Code, weil die Scripts autark bleiben sollten. Das ging eine Weile gut, bis beim Debugging auffiel, dass die Regex für die SST-Erkennung in den beiden Scripts divergiert war: ^G- in einem, ^(G|AW|GT|DC)- im anderen. Eine Änderung war nur in einem Script gelandet - klassischer Code Drift, und ein schmerzhafter Beweis dafür, dass "bewusst dupliziert" eben auch "bewusst riskant" bedeutet.

Die tracking-vendors.json löst das elegant: Beide Scripts laden die gleiche Datei und verwenden die gleiche matchRequest()-Funktion. Ja, die Funktion selbst ist immer noch dupliziert - aber eine Funktion statt dutzender if-else-Ketten, die auf einer gemeinsamen Datenbasis arbeitet, ist ein deutlich kleineres Risiko und erlaubt den Ausbau rein durch Ergänzung im JSON File.

Consent Mode: Vertrauen ist gut, Kontrolle ist besser

gcs und gcd

Googles Consent Mode ist eine Blackbox, solange man nur in den GTM-Container schaut. Die eigentliche Wahrheit darüber, ob Consent Mode korrekt funktioniert, steckt in den Netzwerk-Requests selbst. Jeder GA4- oder Google-Ads-Request trägt zwei Parameter mit sich:

  • gcs - der Consent-Status als kompakter Code: G100 bedeutet "alles verweigert", G111 heißt "alles erlaubt", und dazwischen gibt es verschiedene Kombinationen für partielle Zustimmung
  • gcd - ein detaillierter Consent-Default-String, der anzeigt, welche Consent-Typen wie konfiguriert sind. Ich habe mich einmal ausführlich selbst damit befasst, aber die Varianten sind wirklich unübersichtlich und eas gestern stimmte, ist morgen evtl. nicht mehr vollständig.

Was der Audit-Runner damit macht, ist im Kern ein Drei-Phasen-Abgleich: Pre-Consent sollte , wenn überhaupt, G100 zeigen (alles denied, der Nutzer hat noch nicht zugestimmt), wess es denn unbedingt Advanced Consent Mode sein muss (ganz anderes Thema). Post-Accept sollte je nach Zustimmung zu Diensten / Kategorien G101, G110 oder G111 zu sehen sein. Dieser Wechsel ist der Beweis, dass der Consent-Mode-Update tatsächlich von der CMP über den dataLayer bis in den GA4-Request geschafft hat. Post-Reject wiederum sollte alles G100 bleiben. Fehlt der Update-Request nach Accept ganz, ist auch irgendwas faul. Konfigurationsfehler, die man mit bloßem Auge im GTM-Preview nicht so einfach sieht.

Tracker trotz Reject?

Die unangenehmste Erkenntnis, die ein Audit liefern kann: bekannte Tracking-Requests nach einem expliziten Reject bzw. schon vor Consent. Der Runner prüft das, so gut er kann und markiert es unmissverständlich:

WARNUNG: Folgende bekannte Tracker wurden trotz Reject gefunden:
- Meta Pixel: connect.facebook.net

In der Praxis sieht man das öfter als man denkt, und die Ursachen sind vielfältig: hardcodierte Pixel direkt im HTML (am Tag Manager vorbei), falsche Consent-Kategorien-Zuordnung im GTM, ein CMP-Reject der zwar den Banner schließt aber keinen dataLayer-Push auslöst, oder iframe-basierte Embeds, die ihren eigenen Consent-State mitbringen und sich um den der Hauptseite nicht scheren.

Custom Loader: Wenn der Hostname lügt

Das Stape-Problem

Server-Side Tagging ist der Trend der letzten Jahre, und die Grundidee ist sinnvoll: Statt Tracking-Requests direkt vom Browser an region1.irgendwasbeigooglecom oder facebook.com zu schicken, laufen sie über einen First-Party-Endpunkt auf der eigenen Domain. Aus Datenschutz-Perspektive und gegen Script Blocker sicher hilfreich (wenn korrekt implementiert), aus Audit-Perspektive allerdings ein Problem, denn die gewohnte Erkennungslogik über Hostnames funktioniert plötzlich nicht mehr.

Google Tag Gateway, eigene Gateways für Meta Pixel, Server-Side Tag Manager: Alles Komplexitätsstufen, die man sich gern sparen würde. Mit Custom Loadern bei wie Stape wird es dabei richtig kreativ. Statt des erwarteten googletagmanager.com/gtag/js?id=G-XXXXX lädt die Seite etwas in dieser Art:
https://sst.example.com/daycqquxdwmnu?622da42a=L2d0YWcvanM%2FaWQ9Ry1YWFhYWA%3D%3D
Zufälliger Pfad, zufälliger Parameter-Name, und ein Base64-encodierter Wert. Decodiert man diesen, kommt /gtag/js?id=G-XXXXX zum Vorschein - der echte Google-Tag-Aufruf, versteckt hinter einer Schicht Obfuskation. Die Collect-Requests für GA4-Daten sehen genauso aus: Base64-encodierte Google-Analytics-Payloads, verschickt an einen unscheinbaren First-Party-Endpunkt, den man ohne Kontext für einen normalen Seitenaufruf halten würde.

Drei Varianten, ein Problem

Das Ganze wird also verschärft, dass es nicht nur den Standard, sondern auch mehrere Arten von "Server-Side Setups" jenseits des Custom Loaders gibt:

Setup URL-Beispiel Erkennung
Custom Subdomain sst.example.com/random Anderer Host als die Site
Same Origin (Cloud Edge/LB) www.example.com/random Gleicher Host wie die Site!
Google Tag Gateway metrics.example.com/gt/... Anderer Host, erkennbare /gt/-Pfade

Die Same-Origin-Variante ist die härteste Nuss: Der SST-Host ist der Site-Host. Ein Collect-Request an www.example.com/g/collect sieht im Netzwerk-Tab aus wie irgendein beliebiger Aufruf auf der eigenen Domain. Hostname-basierte Erkennung ist in diesem Fall komplett wertlos.

Die Lösung: In die Payloads schauen

Wenn der Hostname nicht hilft, muss man tiefer graben. Der Ansatz, auf den wir gekommen sind: Alle Request-Bodies sammeln (nicht nur Third-Party), alle Query-Parameter auf Base64 prüfen, und wenn ein decodierter Wert mit /gtag/js, /g/collect oder /collect beginnt, hat man einen Stape-Transport identifiziert. Aus den decodierten Payloads lassen sich dann Measurement-IDs und Container-IDs extrahieren. Zusätzlich scannt der Runner die Response-Bodies geladener JavaScript-Dateien nach GTM-Container-Patterns. Wenn eine Datei von sst.example.com/obfuskierter-pfad.js geladen wird und im Inhalt GTM-XXXXXX steht, ist es ein Custom Loader - egal wie kreativ der Pfad verschleiert sein mag.

Stream Processing statt Daten-Hoarding

Die initiale Sorge bei diesem Ansatz war der Speicherverbrauch: Wenn wir wirklich alle Request-Bodies sammeln, explodiert dann der Speicher? Die Realität hat das schnell entkräftet: Ein typischer Seitenaufruf erzeugt vielleicht 20-30 POST-Requests mit jeweils ein paar KB, in Summe also 200-300 KB pro Phase. Trivial.

Trotzdem wäre es unsauber, alles bis zum Ende aufzuheben und dann in einer großen Batch-Analyse auszuwerten. Stattdessen läuft die Analyse als Stream Processing nach jeder Phase (Pre-Consent, Post-Accept, jeder E-Commerce-Schritt, Post-Reject) und nur die Erkenntnisse bleiben erhalten. Das schont den Speicher und liefert gleichzeitig phasen-genaue Ergebnisse: "Enhanced Conversions wurde in der Post-Accept-Phase erkannt" statt nur "Enhanced Conversions ist irgendwann aufgetaucht."

Meta Pixel und CAPI

Beim Meta Pixel gibt es drei Zustände, die ein Audit unterscheiden muss: Browser Pixel aktiv (Script von connect.facebook.net wird geladen, Requests gehen an facebook.com/tr), CAPI-Indikator erkannt (ein First-Party /events-Endpoint wird angesprochen, was auf Server-Side Events hindeutet), oder - der sauberste, aber seltenste Fall - nur CAPI ohne Browser Pixel (das _fbp-Cookie ist gesetzt, aber es wurde kein Client-Side-Script geladen). In der Praxis ist die Kombination aus Browser Pixel plus CAPI der aktuelle Best Practice, und ein Audit, der nur "Meta Pixel: aktiv" meldet, ohne den CAPI-Status zu prüfen, erzählt nur die halbe Geschichte.

Reporting: Nur was auffällt

Ein Grundprinzip, das sich über alle Iterationen hinweg als goldrichtig erwiesen hat: Findings only. Wenn keine CSP-Violations gefunden werden, steht nichts zu CSP im Report. Wenn kein Enhanced Conversions erkannt wird, gibt es keine leere "Enhanced Conversions: nicht aktiv"-Zeile, die nur Platz wegnimmt.

Das hält Reports kurz bei unauffälligen Sites und lässt die tatsächlichen Probleme bei auffälligen Sites sofort ins Auge springen. Die Zusammenfassung am Anfang des Reports zeigt auf einen Blick, was relevant ist:

Consent Mode: G100 (Pre-Consent) → G111 (Post-Accept)
Server-Side Tagging: Custom Loader auf sst.example.com (Stape-Transport)
Enhanced Conversions aktiv (hashed email)
Dynamic Remarketing: Produkt-IDs in 2 Ads-Requests
CSP blockiert 3 Tracking-Requests
Meta Pixel: Browser Pixel aktiv, kein CAPI-Endpunkt erkannt

Bei einer sauberen Site steht da nur das Wesentliche. Keine Wand aus "alles OK"-Meldungen, bei der das eine tatsächliche Problem in Zeile 47 untergeht.

CSP: Der unsichtbare Blocker

Wenn Requests nicht ankommen

Content Security Policies sind aus Sicherheitsperspektive eine sehr gute Sache. Aus Tracking-Perspektive können sie allerdings zum stillen Killer werden: Ein Tracking-Pixel ist korrekt im GTM konfiguriert, der Consent ist erteilt, im GTM-Preview sieht alles oberflächlich betrachtet sauber aus - aber die CSP blockiert den Request, bevor er den Browser verlässt. Im GTM erscheint kein Fehler. In Google Analytics fehlen einfach Daten. Ohne einen Blick in die Browser-Konsole (oder eben einen automatisierten Check) fällt das nicht auf - und bei Sites, die ihre CSP nachträglich verschärft haben, ohne das Tracking-Team darüber zu informieren, sieht man das leider regelmäßig.

Violations statt Header-Parsing

Mein erster Ansatz war, den CSP-Header zu parsen und gegen die bekannten Tracking-Domains abzugleichen. Theoretisch korrekt, praktisch unnötig kompliziert - CSP-Regeln können Wildcards, Nonces und Hash-Werte enthalten, und ein korrekter Parser, der all diese Fälle abdeckt, ist ein eigenes Projekt.

Die bessere Lösung: securitypolicyviolation-Events direkt im Browser abfangen. Diese Events feuern ausschließlich dann, wenn tatsächlich etwas blockiert wird. Kein theoretischer Abgleich, sondern echte, beobachtete Blockaden.Wenn keine Violations auftreten, steht nichts im Report (Findings only). Treten welche auf, wird es konkret:

Blockierte Ressource Direktive
analytics.tiktok.com/i18n/pixel/events.js (TikTok) script-src-elem
snap.licdn.com/li.lms-analytics/insight.min.js (LinkedIn) script-src-elem

Der Playwright-Fallstrick

Ein Detail, das beim Testen beinahe zu falschen Ergebnissen geführt hätte: Playwright's page.on('request') Event feuert auch für Requests, die anschließend von der CSP blockiert werden. Der Browser versucht den Request, Playwright registriert den Versuch, aber der Request kommt nie am Ziel an. Ohne den zusätzlichen CSP-Violation-Check würde der Report behaupten, das TikTok Pixel sei aktiv, obwohl es von der CSP effektiv blockiert wird. Das ist genau die Art von Subtilität, die man bei manuellen Audits ebenfalls leicht übersieht, weil der Netzwerk-Tab den Request zwar rot markiert, aber man erst beim genauen Hinsehen merkt, dass "blocked by CSP" und "fehlgeschlagen" verschiedene Dinge sind.

HAR-Dateien: Pures Gold bei der Fehleranalyse

Warum ein Compare-Tool?

Es gibt eine Frage, die in der Praxis ständig auftaucht: "Wir haben unser Tracking umgebaut - funktioniert noch alles?" Dafür braucht man keinen vollständigen Audit, sondern eher einen Diff. Zwei Seiten öffnen (oder dieselbe Seite mit zwei verschiedenen Setups auf getrennten URLs), auf beiden Consent erteilen, Requests vergleichen: Was ist gleich, was fehlt, was ist neu? Das Compare-Tool macht genau das - zwei isolierte Browser-Kontexte mit eigenen Cookie-Jars, paralleles Netzwerk-Monitoring, und am Ende ein Diff-Report plus zwei HAR-Dateien.

HAR als Nachschlagewerk

Ein Markdown-Report kann nur die Zusammenfassung zeigen, aber manchmal muss man in den einzelnen Request schauen: Welche Parameter hat dieser spezifische GA4-Collect-Request genau? Stimmen die Event-Parameter? Hat der Consent-Mode-Update die richtigen Werte? Im HAR steckt alles: Request-URL, Headers, Timing, Response-Body. Man kann es direkt in Chrome DevTools importieren (Network-Tab > Import HAR), in Charles Proxy laden oder maschinell auswerten.

Ein konkretes Beispiel: Bei einem Projekt wurde ein sGTM-Setup mit Custom Loader getestet. Der Compare-Report zeigte "gleiche Vendoren auf beiden Seiten" - oberflächlich betrachtet alles in Ordnung. Erst im HAR fiel auf, dass die Measurement-IDs zwischen Standard-Setup und Custom-Loader-Setup unterschiedlich waren. Ein Konfigurationsfehler, der im zusammengefassten Report nicht sichtbar war, weil "GA4 ist da" auf beiden Seiten stimmte, aber eben mit verschiedenen Properties. Das Gute an der Sache: Ist das HAR File im Report-Ordner, kann auch der KI Assistent Deiner Wahl sich das Ganze ansehen und analysieren. Format bekannt, Frage klar, das klappt meistens recht gut.

Der Weg von Klein nach Komplex

Rückblickend lässt sich die Entwicklung in Phasen einteilen, die vermutlich jeder kennt, der ein "kleines Script" geschrieben hat, das dann ein Eigenleben entwickelt.

"Wir brauchen nur die Requests." Version 1 konnte genau eine Sache: Seite laden, Netzwerk-Requests mitschneiden, nach bekannten Tracking-Domains filtern. Ein if-else für Google, eins für Meta, eins für TikTok. Reichte für die Grundfrage "Feuern Pre-Consent Tracker?", aber nicht für die Folgefragen: Welche genau? Was tun sie? Funktioniert der Consent Mode?

"Wir brauchen Phasen." Der Audit wurde aufgeteilt in Pre-Consent, Post-Accept, Post-Reject und den E-Commerce-Pfad, jeweils mit eigenem Netzwerk-Snapshot und dataLayer-Diff. Plötzlich konnte man sehen: "GA4 ist Pre-Consent nicht da, aber Post-Accept schon - korrekt."

"Wir müssen in die Payloads schauen." Requests zählen reicht nicht, man muss wissen, was drin steckt. Base64-Decoding für Stape-Requests, Event-Namen aus GA4-Collect-Requests, Enhanced-Conversion-Flags - die Reports wurden substanziell besser, der Code allerdings auch substanziell komplexer.

"Wir müssen die Vendor-Erkennung zentralisieren." Nach dem dritten Fall von Code Drift zwischen audit.js und compare.js war klar: Die Tracking-Erkennung gehört nicht in if-else-Ketten, sondern in eine deklarative Vendor-Library. tracking-vendors.json war geboren - 16 Produkte, mit Scripts, Endpoints, Domains und Klassifikationsregeln.

"Manchmal braucht der Mensch die Kontrolle." Die vollautomatische CMP-Erkennung funktioniert in 80% der Fälle. Für die restlichen 20% gibt es den manuellen Modus. Der Mensch klickt, das Script sammelt. Pragmatismus schlägt Perfektion.

Was ich gelernt habe

  1. Hostname-basierte Erkennung reicht nicht. Custom Loader, Same-Origin-Setups und kreative Obfuskation machen URL-Pattern-Matching allein unzuverlässig. Wer es ernst meint, muss in Payloads schauen.
  2. Consent Mode muss man mehrstufig verifizieren. Der Plan steht im dataLayer, die Realität in den Parametern der Requests. G100 oder gar kein Request vor Consent, G111 nach Accept - das ist sauber nachweisbar.
  3. CSP blockiert still. Ohne aktive Violation-Erkennung sieht ein Tracking-Setup korrekt aus, obwohl Requests nie ankommen. Besonders tückisch bei nachträglich verschärften CSPs, von denen das Tracking-Team nichts weiß.
  4. HAR-Dateien sind der beste Freund. Ein Summary-Report zeigt die Richtung, für die Ursachenforschung braucht man die Rohdaten. HAR-Export bei jedem Compare-Run hat sich schon jetzt mehrfach im meiner Praxis bewährt.
  5. Deklarativ schlägt imperativ. Eine JSON-Library mit Vendor-Fingerprints ist wartbarer als hundert if-else-Blöcke - und verhindert Code Drift zwischen Scripts, die auf denselben Daten operieren.
  6. Die 80/20-Regel gilt auch hier. Den Banner-Klick zu automatisieren lohnt sich bei bekannten CMPs. Bei unbekannten ist der manuelle Modus schneller als das Einlernen. Und das ist in Ordnung.
  7. Muster nicht zu eng definieren. px.ads.linkedin.com als Pattern klingt präzise - bis man px4.ads.linkedin.com in freier Wildbahn trifft. Lieber etwas breiter als False Negatives, was übrigens auch in CSPs oft die Lösung wäre, aber die IT mag das aus bekannten Gründen nicht so gern 😉
  8. Automatisierung macht gründlicher, nicht zwingend schneller. Der eigentliche Gewinn ist Konsistenz: Der Runner übersieht nicht das Meta Pixel im Reject-Durchlauf, weil es schon der dritte Audit am Tag ist und die Konzentration nachlässt. Sich mit den Ergebnissen des Reports zu befassen, ist aber immer noch nötig. Zentral und komprimiert statt viele Klicks auf der Website und Kontrolle in Network-, Application- und Console-Tab im Browser ist mir aber immer noch viel lieber.

Na, ausprobieren? Oder gar mitmachen?

Wenn Du jetzt Bock bekommen hast, Das Audit- oder Compare Tool selbst auszuprobieren oder in Deinen Alltag einzubauen (ob Console oder Skill), findest Du alles dazu im Repo des "Tracking Auditor" auf GitHub. Feedback, Issues und PRs willkommen! Vor allem bei Nutzung mit realen Websites mag die eine oder andere Ergänzung von CMPs oder Tracker-Mustern anfallen, die dem Repo gut tun könnten. Ich würde mich freuen, denn nur für meinen eigenen Gebrauch hätte ich nur einen Bruchteil der Zeit investieren müssen, um die gleichen Ergebnisse zu erzielen. Und wenn was nicht funktioniert, bitte an die Einleitung denken: Dieses Umfeld ist sehr komplex und voller Fallstricke, also erwarte a) keine 100% für jeden Anwendungsfall und b) Überraschungen, trotz aller Vorhersehbarkeit der Ergebnisformate. Mein Claude-Fu ist noch nicht sehr stark... und was sich Leute so alles in Tracking-Setups einfallen lassen... ihr wisst schon 🙂

War der Beitrag hilfreich?

Dann freue ich mich, wenn er mit anderen geteilt wird!

Einen Tee ausgeben ;)