| Server IP : 202.61.199.114 / Your IP : 216.73.217.139 Web Server : nginx/1.22.1 System : Linux de.arni-solutions.de 6.1.0-49-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.174-1 (2026-05-26) x86_64 User : web20 ( 1018) PHP Version : 8.4.23 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : ON Directory : /var/www/clients/client2/web20/web/wp-content/plugins/arni-crm-tracking/ |
Upload File : |
# ArNi CRM Tracking
DSGVO-bewusstes, **cookieloses First-Party-Tracking** für WordPress – serverseitig **und** per JavaScript – ganz **ohne externe Dienste** (kein Google Analytics, kein Matomo, kein CDN, keine externen Fonts/Libraries). Alle Daten liegen in deiner eigenen Datenbank, sind im WP-Admin sichtbar und über eine **eigene, API-Key-geschützte REST-API** für ein CRM abrufbar.
- **Version:** 1.0.0
- **Autor:** ArNi Solutions · <https://www.arni-solutions.de>
- **PHP:** ≥ 7.3 · **WordPress:** ≥ 5.6
- **Plugin-Ordner:** `wp-content/plugins/arni-crm-tracking/`
---
## Inhalt
1. [Funktionsüberblick](#funktionsüberblick)
2. [Installation & Konfiguration](#installation--konfiguration)
3. [Datenmodell](#datenmodell)
4. [Tracking (Server + JavaScript)](#tracking-server--javascript)
5. [PHP-Helper](#php-helper)
6. [**API-Kommunikation CRM ↔ Plugin**](#api-kommunikation-crm--plugin)
7. [Datenschutz](#datenschutz)
8. [Cron / Aufbewahrung](#cron--aufbewahrung)
9. [Deinstallation](#deinstallation)
---
## Funktionsüberblick
- **Serverseitiges Tracking** bei jedem Frontend-Aufruf (funktioniert auch ohne JavaScript).
- **JavaScript-Events** via `navigator.sendBeacon` (Fallback `fetch`).
- **Conversion-Tracking** server- *und* clientseitig.
- **Admin-Dashboard** mit Kennzahlen, Top-Listen, Geräte-Verteilung, Conversions, CSV-Export.
- **REST-API** für CRM-Anbindung (API-Key).
- IP-Anonymisierung (immer aktiv), cookieloser Tages-Session-Hash, automatische Löschung alter Daten.
---
## Installation & Konfiguration
1. Ordner `arni-crm-tracking/` unter `wp-content/plugins/` ablegen.
2. Im WP-Admin unter **Plugins** aktivieren → legt Tabellen, Salt, API-Key und einen täglichen Cleanup-Cron an.
3. **WP-Admin → ArNi Tracking → Einstellungen** anpassen.
### Einstellungen
| Option | Beschreibung | Default |
|---|---|---|
| Tracking aktiv | Serverseitiges Tracking an/aus | an |
| JavaScript-Tracking aktiv | Frontend-JS einbinden | an |
| Bots ignorieren | Bot-Traffic nicht speichern | an |
| Eingeloggte Admins ignorieren | `manage_options`-Nutzer nicht tracken | an |
| Do-Not-Track respektieren | DNT-Header beachten | an |
| REST-API aktiv | CRM-Endpunkte erreichbar | an |
| Speicherdauer (Tage) | Auto-Löschung; `0` = unbegrenzt | 365 |
| Consent-Modus | `cookieless` oder `consent` vorbereitet | cookieless |
| Daten beim Deinstallieren löschen | Tabellen bei Uninstall droppen | aus |
| IP-Anonymisierung | **immer aktiv, nicht abschaltbar** (IPv4 /24, IPv6 /64) | fix |
Der **API-Key** wird unter *Einstellungen* angezeigt und kann dort neu generiert werden (der alte wird sofort ungültig).
---
## Datenmodell
Drei Tabellen (Präfix `wp_`):
- **`wp_arni_tracking_visits`** – ein Datensatz je Session-Hash (cookielos, täglich rotierend): `session_hash`, `first_seen`, `last_seen`, `landing_page`, `referrer`, `referrer_host`, `utm_*`, `ip_anon`, `user_agent_hash`, `device_type`, `browser`, `os`, `is_bot`, `pageviews`, `total_duration`, `created_at`, `updated_at`.
- **`wp_arni_tracking_events`** – Einzel-Events: `visit_id`, `session_hash`, `event_type`, `page_url`, `page_title`, `event_value`, `event_data` (JSON), `duration_seconds`, `scroll_depth`, `created_at`.
- **`wp_arni_tracking_conversions`** – Conversions: `visit_id`, `session_hash`, `conversion_type`, `reference_id`, `value`, `currency`, `meta` (JSON), `created_at`.
Alle Zeitstempel werden in **UTC** gespeichert.
---
## Tracking (Server + JavaScript)
### Serverseitig
Bei jedem Frontend-GET wird am Request-Ende (`shutdown`, daher kein TTFB-Einfluss) ein `pageview`-Event + Visit-Upsert geschrieben. Funktioniert **auch ohne JavaScript**.
### JavaScript-Events (an `POST /wp-json/arni-tracking/v1/event`)
| `event_type` | Auslöser |
|---|---|
| `pageview_js` | Seitenaufruf mit aktivem JS |
| `page_leave` | Verlassen der Seite (inkl. Verweildauer + max. Scrolltiefe) |
| `scroll_depth` | 25 / 50 / 75 / 100 % |
| `track_click` | Klick auf Element mit `data-track` |
| `button_click` | Klick auf `<button>` |
| `outbound_click` | Klick auf externen Link |
| `file_download` | Klick auf Datei-Link (PDF, ZIP, …) oder `download`-Attribut |
| `form_start` / `form_submit` | Formular-Start / Absenden |
| `conversion` | Conversion (siehe unten) |
**`data-track`-Beispiel:**
```html
<button data-track="cta_hero" data-position="header">Jetzt buchen</button>
```
Outbound-Links, Downloads, Buttons und Formulare werden **automatisch** erfasst – kein Attribut nötig.
---
## PHP-Helper
```php
// Serverseitige Conversion (immer möglich, unabhängig von JavaScript):
arni_track_conversion( 'booking_completed', 129.00, 'ABC123', array( 'room' => 'suite' ), 'EUR' );
arni_track_conversion( 'contact_form_sent' );
// Beliebiges serverseitiges Event:
arni_track_event( 'lead_generated', array( 'event_value' => 'kontaktformular' ) );
```
JavaScript-Pendant:
```js
window.ArNiTracking.trackConversion('booking_completed', { value: 129.00, reference_id: 'ABC123', currency: 'EUR' });
window.ArNiTracking.trackEvent('video_play', { event_value: 'intro' });
```
---
## API-Kommunikation CRM ↔ Plugin
Es gibt **zwei Richtungen**:
1. **Website → Plugin (öffentlicher Ingest):** Das Frontend-JS sendet Events/Conversions an `POST …/event`. Dieser Endpunkt ist **nicht** für das CRM gedacht.
2. **CRM → Plugin (Daten abrufen):** Das CRM **pollt** die `GET`-Endpunkte und authentifiziert sich per **API-Key im HTTP-Header**. Das ist der Hauptweg der CRM-Anbindung.
> Das Plugin pusht **nicht** von sich aus zum CRM – das CRM holt sich die Daten in seinem eigenen Intervall (Pull-Prinzip). Conversions entstehen auf der Website (JS oder PHP-Helper).
### Basis-URL
```
https://DEINE-DOMAIN/wp-json/arni-tracking/v1
```
Namespace: `arni-tracking/v1`.
### Authentifizierung (CRM-Endpunkte)
Jeder `GET`-Aufruf muss den API-Key als Header mitsenden:
```
X-ArNi-Tracking-Key: <API_KEY>
```
| Situation | HTTP-Status | Body (`code`) |
|---|---|---|
| Kein/falscher Key | `401` | `arni_forbidden` |
| REST-API in den Einstellungen deaktiviert | `403` | `arni_rest_disabled` |
| OK | `200` | siehe Endpunkt |
Der Key wird serverseitig per `hash_equals()` (zeitkonstant) geprüft. Den Key findest/erneuerst du unter **ArNi Tracking → Einstellungen**. Niemals im Frontend ausgeben.
### Gemeinsame Query-Parameter (GET-Endpunkte)
| Parameter | Typ | Beschreibung |
|---|---|---|
| `period` | string | `today`, `yesterday`, `24h`, `7d` (Default), `30d`, `90d`, `all` |
| `from` / `to` | `YYYY-MM-DD` | Eigener Zeitraum (überschreibt `period`); in **Site-Zeitzone** interpretiert |
| `limit` | int | Zeilenlimit (max. **1000**, Default 50) |
| `page` | int | Seite für Paginierung (bei `conversions`/`events`) |
| `event_type` | string | Filter (nur `/events`) |
| `conversion_type` | string | Filter (nur `/conversions`) |
| `domain` | string | optional (Single-Site: i. d. R. nicht nötig) |
Antworten enthalten immer ein `range`-Objekt mit den effektiv genutzten **UTC**-Grenzen.
### Endpunkt-Übersicht
| Methode | Pfad | Zweck | Auth |
|---|---|---|---|
| `POST` | `/event` | Event-/Conversion-Ingest (Website-JS) | – (Same-Origin) |
| `GET` | `/stats` | Kennzahlen-Überblick | API-Key |
| `GET` | `/pages` | Top-Seiten | API-Key |
| `GET` | `/referrers` | Top-Referrer (Hosts) | API-Key |
| `GET` | `/campaigns` | Top-Kampagnen (UTM) | API-Key |
| `GET` | `/conversions` | Conversions (Summen, nach Typ, Liste) | API-Key |
| `GET` | `/events` | Einzel-Events (gefiltert) | API-Key |
| `GET` | `/sessions` | Vollständige Session-Journeys (Pfad, Events, Conversions) | API-Key |
---
### `GET /stats`
```bash
curl -s "https://DOMAIN/wp-json/arni-tracking/v1/stats?period=30d" \
-H "X-ArNi-Tracking-Key: $KEY"
```
**Antwort:**
```json
{
"range": { "from": "2026-05-13 22:00:00", "to": "2026-06-12 21:59:59" },
"visitors": 1840,
"pageviews": 5231,
"avg_duration": 78.4,
"conversions": 42,
"conversion_value": 5418.00,
"conversion_rate": 2.28,
"devices": [ { "label": "desktop", "hits": "1100" }, { "label": "mobile", "hits": "640" } ],
"form_abandonments": 17
}
```
### `GET /pages`
```json
{ "range": {…}, "items": [ { "label": "https://…/preise/", "title": "Preise", "hits": "812" } ] }
```
### `GET /referrers`
```json
{ "range": {…}, "items": [ { "label": "google.com", "hits": "430" } ] }
```
### `GET /campaigns`
```json
{ "range": {…}, "items": [ { "label": "sommer2026", "source": "newsletter", "medium": "email", "hits": "120" } ] }
```
### `GET /conversions`
```bash
curl -s "https://DOMAIN/wp-json/arni-tracking/v1/conversions?period=30d&conversion_type=booking_completed&limit=100&page=1" \
-H "X-ArNi-Tracking-Key: $KEY"
```
```json
{
"range": {…},
"totals": { "count": 42, "value": 5418.0 },
"by_type": [ { "label": "booking_completed", "hits": "42", "total": "5418.00" } ],
"items": [
{
"id": "128", "visit_id": "934",
"session_hash": "d7360344a1522bc0…",
"conversion_type": "booking_completed",
"reference_id": "ABC123",
"value": "129.00", "currency": "EUR",
"meta": "{\"room\":\"suite\"}",
"created_at": "2026-06-12 09:14:55"
}
]
}
```
### `GET /events`
```bash
curl -s "https://DOMAIN/wp-json/arni-tracking/v1/events?from=2026-06-01&to=2026-06-12&event_type=outbound_click&limit=200&page=1" \
-H "X-ArNi-Tracking-Key: $KEY"
```
```json
{
"range": {…},
"items": [
{
"id": "9931", "visit_id": "934", "session_hash": "d7360344…",
"event_type": "outbound_click", "page_url": "https://…/blog/",
"page_title": "Blog", "event_value": "https://partner.example/",
"event_data": "{\"host\":\"partner.example\"}",
"duration_seconds": "0", "scroll_depth": "0",
"created_at": "2026-06-12 10:02:11"
}
]
}
```
> Hinweis: Zahlenfelder kommen als String aus MySQL zurück – im CRM ggf. casten. Paginierung über `page` + `limit`; ist `items` kürzer als `limit`, gibt es keine weitere Seite.
### `GET /sessions`
Liefert pro Besuch (Session) die **komplette Customer-Journey**: Stammdaten, Seitenpfad mit Verweildauer, Interaktions-Events und Conversions. Ideal, um im CRM eine Besucher-/Lead-Timeline aufzubauen.
```bash
curl -s "https://DOMAIN/wp-json/arni-tracking/v1/sessions?from=2026-06-01&to=2026-06-12&limit=50&page=1" \
-H "X-ArNi-Tracking-Key: $KEY"
```
```json
{
"range": { "from": "2026-05-31 22:00:00", "to": "2026-06-12 21:59:59" },
"items": [
{
"visit_id": 934,
"session_hash": "d736…",
"started_at": "2026-06-12 10:01:55",
"last_seen": "2026-06-12 10:14:55",
"duration_seconds": 780,
"country": "DE",
"device": "desktop",
"user_agent": "Chrome · Windows · desktop",
"referrer": "google.com",
"utm_source": "newsletter", "utm_medium": "email", "utm_campaign": "sommer2026",
"entry_page": "https://…/",
"exit_page": "https://…/preise/",
"pageviews": 6,
"is_bounce": false,
"pages": [ { "url": "https://…/", "title": "Start", "ts": "2026-06-12 10:01:55", "dwell": 42 } ],
"events": [ { "type": "outbound_click", "value": "https://partner…", "ts": "2026-06-12 10:02:25" } ],
"conversions": [ { "conversion_type": "booking_completed", "value": 129.0, "reference_id": "ABC123", "currency": "EUR" } ]
}
]
}
```
**Feld-Herkunft & Hinweise:**
- `country` – ISO-3166-alpha-2, **ohne externen Dienst** ermittelt (nur das Land, nie die IP). Quellen in Reihenfolge:
1. **Reverse-Proxy-/CDN-Header:** Cloudflare (`CF-IPCountry`), AWS CloudFront (`CloudFront-Viewer-Country`), Google App Engine (`X-AppEngine-Country`), generische `X-GeoIP-Country-Code` / `X-Geo-Country` / `X-Country-Code`.
2. **Webserver-GeoIP-Modul:** `$_SERVER['GEOIP_COUNTRY_CODE']` (nginx `ngx_http_geoip`, Apache `mod_geoip`) bzw. `MM_COUNTRY_CODE` (MaxMind).
3. **Lokale PHP-GeoIP-Erweiterung** `geoip_country_code_by_name()`, falls auf dem Server installiert (lokale DB, kein Netzwerkdienst).
4. **Filter `arni_crm_tracking_country`** für eigene Auflösung (z. B. eigener MaxMind-Reader). Beispiel:
```php
add_filter( 'arni_crm_tracking_country', function ( $cc ) {
if ( $cc ) { return $cc; }
// eigene Logik, z. B. MaxMind-DB-Reader auf Basis von $_SERVER['REMOTE_ADDR']
return $cc;
} );
```
Leer (`""`), wenn keine Quelle ein Land liefert. `XX`/`T1` (unbekannt/Tor) werden als leer gewertet.
- `user_agent` – standardmäßig eine **zusammengesetzte** Angabe `"Browser · OS · Gerät"`. Den **rohen** User-Agent gibt es nur, wenn unter *Einstellungen → „Vollständigen User-Agent speichern"* aktiviert (Default **aus**, Privacy-by-Design). Der UA wird unabhängig davon immer **gehasht** gespeichert.
- `referrer` – externer Referrer-**Host** (interne Navigation wird nicht als Referrer gewertet).
- `pages[].dwell` – Verweildauer in Sekunden, berechnet aus dem Abstand aufeinanderfolgender Pageviews; die letzte Seite hat `dwell: 0`.
- `exit_page` – letzte aufgerufene Seite; `is_bounce` – `true` bei nur einem Pageview.
- `events` enthält Interaktionen (Klicks, Scroll, Formular, Verlassen …) **ohne** Pageviews; Pageviews stehen in `pages`.
- **Limit:** wegen der verschachtelten Daten max. **200** Sessions pro Aufruf (Default 25); verschachtelte `pages`/`events` sind je Session auf 300 Einträge begrenzt. Paginierung über `page`.
- Performance: pro Aufruf nur **3 Datenbank-Queries** (Visits, Events, Conversions) – Gruppierung erfolgt in PHP.
---
### `POST /event` (Website-Ingest, kein CRM)
Wird vom Frontend-JS genutzt. **Öffentlich**, aber mit leichtem **Same-Origin-Schutz** (Header `Origin`/`Referer` muss zur Site-Domain passen, sofern gesetzt). Kein API-Key. Die Session wird **serverseitig** aus anonymisierter IP + User-Agent + Tages-Salt berechnet – der Client sendet **keinen** Session-Identifier.
**Einzel-Event:**
```json
{ "event_type": "scroll_depth", "scroll_depth": 75,
"page_url": "https://DOMAIN/", "page_title": "Start" }
```
**Conversion:**
```json
{ "event_type": "conversion", "conversion_type": "newsletter_signup",
"value": 0, "reference_id": "NL-9", "currency": "EUR", "meta": { "src": "footer" } }
```
**Batch (max. 25):**
```json
{ "events": [ { "event_type": "page_leave", "duration_seconds": 64, "scroll_depth": 90 } ] }
```
**Antwort:** `{ "ok": true, "saved": 1 }` · bei Same-Origin-Verstoß `403`, bei deaktiviertem/DNT-Tracking `202 { "ok": false, "reason": "…" }`.
---
### CRM-Integrationsbeispiele
**Polling (Bash/cron im CRM):**
```bash
KEY="arni_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
BASE="https://DOMAIN/wp-json/arni-tracking/v1"
curl -s "$BASE/conversions?from=2026-06-01&to=2026-06-12&limit=1000" \
-H "X-ArNi-Tracking-Key: $KEY" | jq '.items[]'
```
**PHP (CRM-seitig):**
```php
$res = wp_remote_get( 'https://DOMAIN/wp-json/arni-tracking/v1/stats?period=7d', array(
'headers' => array( 'X-ArNi-Tracking-Key' => ARNI_KEY ),
'timeout' => 15,
) );
$data = json_decode( wp_remote_retrieve_body( $res ), true );
```
**Python (CRM-seitig):**
```python
import requests
r = requests.get(
"https://DOMAIN/wp-json/arni-tracking/v1/conversions",
params={"period": "30d", "limit": 1000, "page": 1},
headers={"X-ArNi-Tracking-Key": KEY}, timeout=15,
)
for c in r.json()["items"]:
print(c["conversion_type"], c["reference_id"], c["value"], c["created_at"])
```
**Empfohlener Pull-Workflow fürs CRM:**
1. Periodisch (z. B. alle 15 Min) `/conversions` mit `from = letzter_abruf` ziehen.
2. Über `reference_id` mit eigenen Datensätzen abgleichen (Deduplizierung).
3. `created_at` (UTC) als Hochwasser-Marke für den nächsten Abruf speichern.
4. Bei großen Mengen über `page` + `limit` (max. 1000) blättern.
---
## Datenschutz
- Keine externen Dienste, kein CDN, keine externen Fonts/Libraries.
- **Niemals vollständige IPs** (IPv4 /24, IPv6 /64). Gehashter User-Agent (mit Salt).
- Cookieloser, **täglich rotierender** Session-Hash → keine Langzeit-/Cross-Site-Profile.
- Keine Cookies im Standardmodus. Opt-out per Link: `https://DOMAIN/?arni_optout=1`.
- DNT wird (optional) respektiert. Fertiger **Datenschutz-Textbaustein** unter *ArNi Tracking → Datenschutz*.
---
## Cron / Aufbewahrung
- Täglicher WP-Cron-Hook `arni_crm_tracking_cleanup` löscht Daten älter als die Speicherdauer.
- Ist `DISABLE_WP_CRON` gesetzt, muss `wp-cron.php` per System-Cron laufen, z. B.:
```cron
*/5 * * * * web32 /usr/bin/php8.4 -d max_execution_time=300 /pfad/zu/web/wp-cron.php >/dev/null 2>&1
```
---
## Deinstallation
`uninstall.php` entfernt den Cron immer; **Tabellen & Optionen** werden nur gelöscht, wenn unter *Einstellungen* „Daten beim Deinstallieren löschen" aktiviert ist. Andernfalls bleiben die Daten erhalten.