Willkommen beim WP Wegerl.at 'Lesemodus' + Tastaturnavigation!
Entspanntes Lesen und spannende Artikel warten auf dich.
Entdecke unsere besten Beiträge und genieße den Lesemodus sowie die Tastaturbedienung.
WP Wegerl.at: Einzigartige Designs, optimiert für das Nutzererlebnis.
Visit Duration: Verweildauer, Tracking
smilies.4-user.de

WordPress – 'Visit Duration': Besucher-Verweildauer messen

Illustration (inspire-studio und Mooreemilyc) var. pixabay
Werbung

Der Classic-Editor: Vertraut und zuverlässig –
Ihr bewährter Begleiter.


Erleben Sie den Classic Editor neu!
(Bilder von pixelcreatures)


Viele Nutzer schätzen die vertraute Umgebung des Classic-Editors, die eine einfache und schnelle Bearbeitung ermöglicht.




Werbung

Mit dem Advanced Editor Tools auf das nächste Level –
Mehr Funktionen, mehr Möglichkeiten.


Classic Editor + Advanced Editor Tools
= Ihr Erfolgsrezept


Der Advanced Editor erweitert den Funktionsumfang des Classic-Editors und ermöglicht es, Inhalte noch effektiver zu bearbeiten.




Werbung

Einfach und intuitiv –
Der Classic-Editor für alle.


Classic Editor & Advanced Editor Tools
Erleben Sie es.


Der Classic-Editor zeichnet sich durch Stabilität und Zuverlässigkeit aus, was für professionellen Anwender von Bedeutung ist.




Werbung

Mehr schaffen in weniger Zeit –
Der Advanced Editor für kreative Köpfe.


Optimieren Sie Ihre Bearbeitung:
Advanced Editor Tools


Mit dem Advanced Editor können Designer und
Content Creatoren kreative Ideen umsetzten.





Die Verweildauer von Besuchern ist ein signifikanter Indikator, um das Nutzerverhalten auf einer Website besser zu verstehen. In diesem Beitrag wird eine einfache Methode vorgestellt, mit der die Verweildauer auf einer WordPress-Seite gemessen werden kann – ganz ohne Cookies oder eine separate Datenbanktabelle. Das Plug-in bietet eine DSGVO-konforme Lösung, die die Privatsphäre der Nutzer wahrt. Es stellt eine hervorragende Ergänzung zu den Statistiken von Statify dar, um das Nutzerverhalten effektiv zu analysieren und ein realistischen Bild zu erhalten.

Verweildauer in WordPress tracken:
Eine einfache Lösung mit "Visit Duration"

Über ein Dashboard-Widget können Administratoren die Verweildauer der Besucher in Echtzeit sehen. Die Besuchsdaten werden automatisch aktualisiert, sodass jederzeit ein aktueller Überblick über die Nutzeraktivitäten gewährleistet ist. Ideal für alle, die eine schnelle und unkomplizierte Möglichkeit suchen, das Nutzerverhalten auf ihrer Seite zu verstehen, ohne gegen die Datenschutzrichtlinien zu verstoßen.

Statify-Optimierung für rationelles Tracking:

  1. IP-Ausschlusslogik für Statify (für Entwickler geeignet)
    Dieser Beitrag erklärt, wie Admin-IP-Adressen automatisch vom Tracking ausgeschlossen werden. So kann die eigene Website in allen Browsern getestet werden, ohne in die Besucherstatistik von Statify einzufließen.
  2. Effektives Tracking mit Statify: Zeitverzögerung + Scrolltiefe
    Hier geht es um die Erweiterung der Statify-Tracking-Funktionen, einschließlich der Möglichkeit, Zeitverzögerungen und Scrolltiefe zu messen, um tiefergehende Einblicke in das Nutzerverhalten zu erhalten.

Erweiterungen für effektives Tracking:

  1. Click & Bounce Counter und Statify: im Duo für effektives Tracking
    Eine Anleitung, wie die Kombination von Statify und dem Bounce Counter Plug-in zu einem leistungsstarken Tracking-System führt.
  2. Der Beitrag hier:
    Verweildauer der Erweiterung 'Visit Duration'
    Der Artikel zeigt, wie die Verweildauer von Besucher gemessen werden kann – ohne Cookies und ohne zusätzliche Datenbanktabellen.

 'Visit Duration': Verweildauer messen

Das Plug-in 'Visit Duration' ermöglicht es Website-Betreibern, die Verweildauer der Besucher auf ihrer Seite zu messen – und das ohne den Einsatz von Cookies oder einer separaten Datenbanktabelle. Die Lösung wurde mit dem Ziel entwickelt, datenschutzkonform gemäß der DSGVO zu arbeiten und gleichzeitig wertvolle Einblicke in das Nutzerverhalten zu liefern.

Visit Duration: WP-Dashboard-Widget

Mit diesem Plug-in können Administratoren über ein Dashboard-Widget die Verweildauer der Besucher in Echtzeit einsehen. Die Daten werden dynamisch aktualisiert, indem der Aktualisieren-Button betätigt wird. Dadurch wird jederzeit ein aktueller Überblick über die Aktivitäten auf der Seite gewährleistet.

Beispielablauf einer Sitzung

  1. Sitzungseröffnung
    • Die Sitzung wird gestartet, nach dem Scrollimpuls und  3 Sekunden verstrichen sind. – Das Einbeziehen der Bewegungen von  Scrollaktivität dient dem Gedanken, dass andere Aufrufe – beispielsweise durch Bots oder automatisierte Prozesse – möglichst nicht getrackt werden sollten.
  2. Beendigung
    • Beim Weiterklicken, Schließen des Tabs oder Browsers wird die Verweildauer sofort gespeichert. Dies sollte auch im Inkognito-Modus funktionieren. Anmerkung: In Chrome funktioniert dies im Inkognito-Modus jedoch nicht ohne den Heartbeat.

Wichtige Merkmale des Plug-ins

  • DSGVO-konform: Keine Verwendung von Cookies, keine Speicherung personenbezogener Daten – das Plug-in hält sich strikt an die Datenschutzrichtlinien.
  • Echtzeit-Überwachung: Das Dashboard-Widget zeigt die Verweildauer der Besucher live an und wird bei jedem Seitenaufruf automatisch aktualisiert und ein separater Button ist auch implementiert.
  • Einfache Integration: Die Implementierung ist unkompliziert und benötigt keine speziellen technischen Kenntnisse.
  • Keine zusätzliche Datenbanktabelle erforderlich: Alle relevanten Daten werden direkt in der Optionstabelle von WordPress gespeichert, was die Serverlast minimiert.
  • WordPress-Multisite-Umgebung: Das Plug-in ist aktuell nicht für eine WordPress-Multisite-Umgebung ausgelegt. In einer Multisite-Installation müsste das Plug-in angepasst werden, um Optionen und Tracking korrekt für alle Sites zu verwalten. Das Plug-in kann auf der Hauptseite einer Multisite-Installation aktiviert und genutzt werden. Beachte jedoch, dass es nicht für die Verwendung auf einzelnen Seiten innerhalb eines Multisite-Netzwerks entwickelt wurde.
  • Dieses Plug-in bietet keine umfangreiche Statistik, wie man sie von großen Tracking-Plug-ins kennt. Es liefert punktuell Einblicke darüber, wie die Besuche auf der Website verlaufen.

Plug-in 'Visit Duration'

Das Plugin ist bereits voll funktionsfähig und für den praktischen Einsatz gut geeignet.

Plug-in: visit-duration herunterladen und im Dashboard unter Plugins > Neues Plugin hinzufügen die Option Plugin hochladen wählen. Anschließend das Plug-in aktivieren. Es sind keine weiteren Einstellungen erforderlich, und das Widget wird im Dashboard angezeigt.

Visit Duration – Einfach. Praktisch. Kompatibel.
Aus dem Herzen von WP Wegerl. Nur hier zum Download.

/wp-content/plugins/visit-duration/  
├── visit-duration.php  
└── bot-helper/  
    └── bot-functions.php

Hier ist der derselbe Code wie des Plug-ins, für des Moments

  • Hinweis: Ein erneut verbessertes JS bezüglich des Abschlusses der Sitzung ist im Test und wird bei Erfolg upgedatet!

Erstens 'visit-duration.php'

  1. visit-duration.php
<?php
/*
* Plugin Name: Visit Duration
* Description: Ermöglicht das Messen der Verweildauer von Besuchern auf einer WordPress-Seite ohne Cookies und ohne separate Datenbanktabelle. DSGVO-konform.
* (Version: 1.0.0)
* Entwicklung: 29.11.24
* Author: Team WP Wegerl
* Author URI: https://wegerl.at/visit-duration/
* Text Domain: visit-duration

Die Funktion 'is_bot_or_spider' prüft anhand des User-Agents: 
ob es sich von Bots, Spidern, Testtools und verdächtigen Anfragen basierend auf dem User-Agent und anderen Headern.
Diese Funktion nutzt auch Caching, um wiederholte Anfragen zu vermeiden und verbessert so die Performance.
*/

if ( ! defined( 'ABSPATH' ) ) {
    exit; // Exit if accessed directly
}

// Am Anfang der Datei, um die Bot-Erkennungsfunktion zu laden
if (file_exists(plugin_dir_path(__FILE__) . 'bot-helper/bot-functions.php')) {
    require_once plugin_dir_path(__FILE__) . 'bot-helper/bot-functions.php';
} else {
    error_log('bot-functions.php wurde nicht gefunden.');
}

// Prüfen, ob die Funktion is_bot_or_spider existiert, bevor sie verwendet wird
if (function_exists('is_bot_or_spider')) {
    add_action('wp_footer', 'start_visit_tracking');
} else {
    error_log('Die Funktion is_bot_or_spider ist nicht verfügbar.');
}

// Prüft, ob es sich um einen Bot handelt
$is_bot = is_bot_or_spider();

function start_visit_handler() {
    check_ajax_referer('your_nonce', 'security');

    // Die unique_id aus der POST-Anfrage holen
    $unique_id = isset($_POST['unique_id']) ? sanitize_text_field($_POST['unique_id']) : '';
    // SHA-256 Hash der unique_id erstellen
    $hashed_unique_id = hash('sha256', $unique_id);
    
    // Den Seitentitel aus der POST-Anfrage holen
    $page_title = isset($_POST['page_title']) ? sanitize_text_field($_POST['page_title']) : '';

    // Überprüfen, ob sowohl unique_id als auch page_title gesetzt sind
    if ($unique_id && $page_title) {
        global $wpdb;
        $table_name = $wpdb->prefix . 'visit_tracking';

        // Gehashten Wert in die Datenbank einfügen
        $wpdb->insert($table_name, array(
            'unique_id'   => $hashed_unique_id,  // Gehashte unique_id speichern
            'page_title'  => $page_title,
            'visit_time'  => current_time('mysql')
        ));
        
        wp_send_json_success('Besuch protokolliert');
    } else {
        wp_send_json_error('Ungültige Daten');
    }
}

// Stelle sicher, dass die Sitzung zu Beginn der Verarbeitung gestartet wird
function start_session_if_needed() {
    if (session_status() == PHP_SESSION_NONE) {
        session_start();
    }
}
add_action('init', 'start_session_if_needed');

// Besuchs-Tracking initialisieren

function start_visit_tracking() {
    // Den angemeldeten Admin und Bots ausschließen
    if (current_user_can('administrator') || is_bot_or_spider() || is_test_tool() || is_suspicious_bot() || !has_valid_referer()) {
        return; // Frühzeitig abbrechen
    }

    // Prüfen, ob die Sitzung bereits gestartet wurde
    if (session_status() == PHP_SESSION_NONE) {
        session_start();
    }

    // Sicherstellen, dass $_SESSION verfügbar ist und keine Fehler auftreten
    if (isset($_SESSION)) {
        // Besuchszeit setzen, falls noch nicht vorhanden
        if (!isset($_SESSION['visit_start_time'])) {
            $_SESSION['visit_start_time'] = time();
        }
    } else {
        // Fehlerbehandlung, falls $_SESSION nicht verfügbar ist
        error_log("Sitzung konnte nicht gestartet werden oder $_SESSION ist nicht verfügbar.");
        return; // Funktion abbrechen, falls es Probleme mit der Sitzung gibt
    }

    // Besuchsdaten abrufen und AJAX-Skript laden
    echo "<script>
// Optimiertes Tracking-Script: Scroll-Tracking, Heartbeat und Endbesuchs-Update
(function () {
    var ajaxUrl = '" . esc_url(admin_url('admin-ajax.php')) . "';
    if (!ajaxUrl) {
        console.error('AJAX-URL konnte nicht geladen werden.');
        return;
    }

    window.ajaxUrl = ajaxUrl;
    window.uniqueId = 'id-' + Math.random().toString(36).substr(2, 16);
    var isUserInteracted = false;

    // Funktion zum Starten des Besuchs
    function startVisitTracking() {
        if (!isUserInteracted) {
            isUserInteracted = true;

            console.log('Benutzer hat interagiert. Tracking beginnt in 3 Sekunden...');
            setTimeout(function () {
                console.log('3 Sekunden Verzögerung vorbei. Tracking beginnt jetzt.');
                fetch(window.ajaxUrl + '?action=start_visit', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ unique_id: window.uniqueId, page_title: document.title })
                }).catch(err => console.error('Fehler beim Start des Besuchs:', err));
            }, 3000);
        }
    }

    // Scroll-Ereignis mit Debouncing
    let scrollTimeout;
    document.addEventListener('scroll', function () {
        if (scrollTimeout) clearTimeout(scrollTimeout);
        scrollTimeout = setTimeout(startVisitTracking, 200); // Verzögerung von 200ms
    });

    // Besuch beenden, wenn die Seite verlassen wird
window.addEventListener('beforeunload', sendEndVisit);

// Zusätzlicher Fallback beim Verlassen der Seite
window.addEventListener('unload', function() {
    if (!navigator.sendBeacon) {
        // Fallback für den Fall, dass sendBeacon nicht verfügbar ist
        const xhr = new XMLHttpRequest();
        xhr.open('POST', window.ajaxUrl + '?action=update_visit_duration', false); // Synchroner Fallback
        xhr.setRequestHeader('Content-Type', 'application/json');
        xhr.send(JSON.stringify({
            unique_id: window.uniqueId,
            end_time: Date.now()
        }));
    }
});

function sendEndVisit() {
    if (window.uniqueId && window.ajaxUrl) {
        const payload = JSON.stringify({
            unique_id: window.uniqueId,
            end_time: Date.now()
        });

        // Primäre Methode: sendBeacon
        const beaconSuccess = navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', payload);
        
        if (!beaconSuccess) {
            // Fallback mit XMLHttpRequest, falls sendBeacon fehlschlägt
            const xhr = new XMLHttpRequest();
            xhr.open('POST', window.ajaxUrl + '?action=update_visit_duration', false); // Synchroner Fallback
            xhr.setRequestHeader('Content-Type', 'application/json');
            xhr.send(payload);
        }
    }
}

    // Funktion zum Senden des Heartbeats
    async function sendHeartbeat() {
        if (window.uniqueId && window.ajaxUrl) {
            try {
                await fetch(window.ajaxUrl + '?action=heartbeat', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ unique_id: window.uniqueId, timestamp: Date.now() })
                });
            } catch (err) {
                console.error('Fehler beim Heartbeat, versuche erneut:', err);
                // Optional: Hier könnte eine Retry-Logik hinzugefügt werden
            }
        }
    }

    // Heartbeat alle 10 Sekunden senden
    setInterval(sendHeartbeat, 10000);

    console.log('Tracking und Heartbeat gestartet. AJAX-URL:', ajaxUrl, 'Unique-ID:', window.uniqueId);
})();
    </script>";
}

// Besuch beenden und Verweildauer speichern
function end_visit_tracking() {
    if (isset($_SESSION['visit_start_time'])) {
        $duration = time() - $_SESSION['visit_start_time'];
        update_option('last_visitor_duration', $duration);
        unset($_SESSION['visit_start_time']);
        error_log("Visit ended. Duration: " . $duration . " seconds.");
    }
}

// Herzschlag für Beenden Browser und Tab
add_action('wp_ajax_heartbeat', 'handle_heartbeat');
add_action('wp_ajax_nopriv_heartbeat', 'handle_heartbeat');

function handle_heartbeat() {
    // Stelle sicher, dass die Daten korrekt sind
    $data = json_decode(file_get_contents('php://input'), true);

    if (isset($data['unique_id']) && isset($data['timestamp'])) {
        $unique_id = sanitize_text_field($data['unique_id']);
        $timestamp = intval($data['timestamp'] / 1000); // Zeitstempel in Sekunden

        // Hole die aktuellen Besuche
        $visits = get_option('current_visits', []);

        // Wenn der Besuch existiert, aktualisiere den Heartbeat
        if (isset($visits[$unique_id])) {
            $visits[$unique_id]['last_heartbeat'] = $timestamp; // Aktualisiere den Heartbeat-Zeitstempel
            update_option('current_visits', $visits); // Speichern der aktualisierten Besuchsdaten
        }

        // Fehlerprotokollierung für Debugging
        error_log("Heartbeat empfangen für Unique-ID: " . $unique_id . " mit Zeitstempel: " . $timestamp);
    }

    wp_die(); // Beendet die AJAX-Anfrage
}

// Prüft, ob die IP ausgeschlossen ist
if (!function_exists('is_ip_excluded')) {
    function is_ip_excluded($user_ip) {
        $excluded_ips = get_option('excluded_ips', array());
        return in_array($user_ip, $excluded_ips);
    }
}

// AJAX-Handler zur Speicherung der Startzeit
function start_visit() {
    $data = json_decode(file_get_contents('php://input'), true);
    $unique_id = sanitize_text_field($data['unique_id']);
    $page_title = strip_tags($data['page_title']);
    $start_time = time();

    $visits = get_option('current_visits', []);

    // Neuer Eintrag für jeden Seitenaufruf speichern
    $visits[$unique_id] = [
        'unique_id' => $unique_id,
        'page_title' => $page_title,
        'start_time' => $start_time
    ];

    // Besuche auf 25 Einträge begrenzen
    $max_visits = get_option('max_visits', 25);
if (count($visits) > $max_visits) {
    array_shift($visits);
}
    update_option('current_visits', $visits);
    wp_send_json_success();
}

add_action('wp_ajax_start_visit', 'start_visit');
add_action('wp_ajax_nopriv_start_visit', 'start_visit');

// Funktion zur Aktualisierung der Verweildauer
function update_visit_duration() {
    $data = json_decode(file_get_contents('php://input'), true);
    
    // Überprüfen, ob die erforderlichen Daten vorhanden sind
    if (!isset($data['unique_id']) || !isset($data['end_time'])) {
        wp_send_json_error(['message' => 'Fehlende Parameter']);
        return;
    }

    $unique_id = sanitize_text_field($data['unique_id']);
    $end_time = intval($data['end_time'] / 1000); // Zeitstempel konvertieren

    $visits = get_option('current_visits', []);
    
    if (isset($visits[$unique_id])) {
        $duration = $end_time - $visits[$unique_id]['start_time'];
        $visits[$unique_id]['duration'] = $duration;
        update_option('current_visits', $visits);
    }

    wp_send_json_success();
}

add_action('wp_ajax_update_visit_duration', 'update_visit_duration');
add_action('wp_ajax_nopriv_update_visit_duration', 'update_visit_duration');

// Widget zur Anzeige der Verweildauer
function add_visit_duration_dashboard_widget() {
    wp_add_dashboard_widget(
        'visit_duration_widget',
        'Visit Duration: Aktuelle Verweildauer der Besucher',
        'display_visit_duration_widget'
    );
}

add_action('wp_dashboard_setup', 'add_visit_duration_dashboard_widget');

// Widget für das Dashboard mit aktualisierten Besuchsdaten und Scrollfunktion
function display_visit_duration_widget() {
    $visits = get_option('current_visits', []);

    if (empty($visits)) {
        echo '<p>Keine aktuellen Besuchsdaten verfügbar.</p>';
        return;
    }

    // Besuche nach Startzeit sortieren (neueste oben)
    usort($visits, function($a, $b) {
        return $b['start_time'] - $a['start_time']; // Sortiert absteigend nach Startzeit
    });

    // Füge CSS hinzu, um den Scrollbalken nur bei Bedarf anzuzeigen
    echo '<style>
        /* Standardmäßig versteckter Scrollbalken, der nur bei Bedarf erscheint */
        #visit-duration-container {
            height: 360px;
            overflow-y: auto; /* Scrollbalken erscheint nur bei Bedarf */
            border: 1px solid #ddd;
        }

        /* Schmaler Scrollbalken für Webkit-basierte Browser */
        #visit-duration-container::-webkit-scrollbar {
            width: 4px; /* Schmaler Scrollbalken */
        }

        #visit-duration-container::-webkit-scrollbar-thumb {
            background-color: darkgray;
            border-radius: 10px;
        }

        #visit-duration-container::-webkit-scrollbar-track {
            background: #f1f1f1;
            border-radius: 10px;
        }
    </style>';

    // Scrollbarer Container für die Tabelle
    echo '<div id="visit-duration-container">';
    echo '<table id="visit-duration-table" style="width:100%; text-align:left;">';

    // <thead> mit Sticky-Header-Styling
    echo '<thead style="position: sticky; top: 0; background-color: #fff; z-index: 1;">';
    echo '<tr><th>Seiten-Titel</th><th>Verweildauer (Min:Sek)</th><th>Status</th></tr>';
    echo '</thead>';
    
    echo '<tbody>';

    foreach ($visits as $visit_data) {
        // Setze den Status basierend auf dem Vorhandensein der Verweildauer
        $status = isset($visit_data['duration']) ? 'Beendet' : 'Aktiv';

        // Berechne Minuten und Sekunden für die Verweildauer
        if (isset($visit_data['duration'])) {
            $minutes = floor($visit_data['duration'] / 60);
            $seconds = $visit_data['duration'] % 60;
            $formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
        } else {
            $formatted_duration = 'Noch aktiv';
        }

        // Setze die Hintergrundfarbe abhängig vom Status
        $row_color = ($status === 'Aktiv') ? 'rgba(255, 235, 59, 0.7)' : '#fff';

        echo '<tr style="background-color: ' . $row_color . ';">';
        echo '<td>' . esc_html($visit_data['page_title']) . '</td>';
        echo '<td>' . esc_html($formatted_duration) . '</td>';
        echo '<td>' . esc_html($status) . '</td>';
        echo '</tr>';
    }

    echo '</tbody>';
    echo '</table>';
    echo '</div>'; // Ende des scrollbaren Containers

    echo '<button id="reset-duration-btn" class="reset-button" style="margin: 15px 15px 0;">Tabelle zurücksetzen</button>';
    echo '<button id="update-duration-btn" class="update-button">Verweildauer aktualisieren</button>';
    ?>
    <script type="text/javascript">
	
	// "Update"-Button	
	document.getElementById('update-duration-btn').addEventListener('click', function() {
    jQuery.ajax({
        url: '<?php echo admin_url('admin-ajax.php'); ?>',
        type: 'POST',
        data: {
            action: 'update_all_visit_durations',
        },
        success: function(response) {
            if (response.success) {
                // Die Tabelle aktualisieren und die Zeilen mit den korrekten Hintergrundfarben
                var tableBody = jQuery('#visit-duration-table').find('tbody');
                tableBody.empty(); // Bestehende Zeilen löschen

                // Besucher nach Startzeit absteigend sortieren
                response.data.updated_visits.sort(function(a, b) {
                    return b.start_time - a.start_time; // Sortiert absteigend nach Startzeit
                });

                // Besucher in die Tabelle einfügen
                response.data.updated_visits.forEach(function(visit) {
                    var rowColor = (visit.status === "Aktiv") ? "#ffeb3b" : "#fff";
                    var formattedDuration = visit.formatted_duration || 'Noch aktiv';
                    tableBody.append(
                        '<tr style="background-color: ' + rowColor + '">' +
                        '<td>' + visit.page_title + '</td>' +
                        '<td>' + formattedDuration + '</td>' +
                        '<td>' + visit.status + '</td>' +
                        '</tr>'
                    );
                });
            } else {
                alert('Fehler bei der Aktualisierung der Verweildauer.');
            }
        },
        error: function() {
            alert('Fehler beim Aktualisieren der Verweildauer.');
        }
    });
});

    // "Reset"-Button mit Doppel-Klick-Mechanismus
    document.getElementById('reset-duration-btn').addEventListener('click', function(event) {
        event.preventDefault();

        if (this.dataset.clickedOnce === "true") {
            jQuery.ajax({
                url: '<?php echo admin_url('admin-ajax.php'); ?>',
                type: 'POST',
                data: {
                    action: 'reset_visit_duration',
                },
                success: function(response) {
                    if (response.success) {
                        // Die Tabelle zurücksetzen und nur die Kopfzeile anzeigen
                        jQuery('#visit-duration-table').html('<thead><tr><th>Seiten-Titel</th><th>Verweildauer (Min:Sek)</th><th>Status</th></tr></thead><tbody></tbody>');
                    } else {
                        alert('Fehler beim Zurücksetzen der Tabelle.');
                    }
                },
                error: function() {
                    alert('Fehler beim Zurücksetzen der Tabelle.');
                }
            });

            this.dataset.clickedOnce = "false";
            this.innerText = "Tabelle zurücksetzen";
        } else {
            this.dataset.clickedOnce = "true";
            this.innerText = "Zum Bestätigen erneut klicken";

            setTimeout(() => {
                this.dataset.clickedOnce = "false";
                this.innerText = "Tabelle zurücksetzen";
            }, 1500);
        }
    });
    </script>
<?php
}

// AJAX-Handler zur Aktualisierung der Verweildauer aller Besucher inkl. 30 Minuten Timeout
function update_all_visit_durations() {
    $visits = get_option('current_visits', []);
    $timeout_limit = 30 * 60; // 30 Minuten in Sekunden
    $current_time = time();

    $updated_visits = [];

    foreach ($visits as $visit_id => $visit_data) {
        // Nur Besucher, bei denen die Verweildauer noch nicht festgelegt wurde (d.h., die noch aktiv sind)
        if (isset($visit_data['start_time']) && !isset($visit_data['duration'])) {
            $duration = $current_time - $visit_data['start_time'];

            // Prüfe, ob die Sitzung die 30-Minuten-Grenze überschritten hat
            if ($duration > $timeout_limit) {
                // Beende die Sitzung und setze die Verweildauer auf 30 Minuten
                $visit_data['duration'] = $timeout_limit;
                $visit_data['status'] = 'Timeout';
            } else {
                // Sitzung ist noch aktiv, aktualisiere die Dauer
                $visit_data['duration'] = $duration;
                $visit_data['status'] = 'Aktiv';
            }
        } else {
            // Falls die Verweildauer bereits gesetzt ist, markiere den Status als "Beendet"
            $visit_data['status'] = 'Beendet';
        }

        // Formatiere die Dauer für die Anzeige
        if (isset($visit_data['duration'])) {
            $minutes = floor($visit_data['duration'] / 60);
            $seconds = $visit_data['duration'] % 60;
            $formatted_duration = sprintf('%02d:%02d', $minutes, $seconds);
            $visit_data['formatted_duration'] = $formatted_duration;
        } else {
            $visit_data['formatted_duration'] = 'Noch aktiv';
        }

        $updated_visits[] = $visit_data; // Füge die (aktualisierten) Besuchsdaten hinzu
    }

    // Besuchsdaten nach Startzeit absteigend sortieren
    usort($updated_visits, function($a, $b) {
        return $b['start_time'] - $a['start_time']; // Sortiere absteigend nach Startzeit
    });

    // Speichere die aktualisierten Besuchsdaten in der Option
    update_option('current_visits', $visits);

    // Sende die aktualisierten Daten zurück
    wp_send_json_success(['updated_visits' => $updated_visits]);
}

add_action('wp_ajax_update_all_visit_durations', 'update_all_visit_durations');
add_action('wp_ajax_nopriv_update_all_visit_durations', 'update_all_visit_durations');

// Besuchsdaten zurücksetzen
function reset_visit_duration() {
    delete_option('current_visits');
    update_option('current_visits', []);
    wp_cache_flush();
    wp_send_json_success();
}
add_action('wp_ajax_reset_visit_duration', 'reset_visit_duration');
add_action('wp_ajax_nopriv_reset_visit_duration', 'reset_visit_duration');

// Hook for plugin deactivation
register_deactivation_hook(__FILE__, 'visit_duration_deactivate');

// Function to delete 'current_visits' option on deactivation
function visit_duration_deactivate() {
    delete_option('current_visits');
}

Zweitens 'bot-functions.php'

  1. bot-functions.php
<?php
/**
 * Erkennung von Bots, Spidern, Testtools und verdächtigen Anfragen basierend auf dem User-Agent und anderen Headern.
 *
 * Diese Funktionen überprüfen, ob der aktuelle Benutzeragent (User-Agent) mit einem bekannten Bot- oder 
 * Testtool-Identifikator übereinstimmt. Zusätzlich wird auch der Referrer und andere verdächtige Header überprüft, 
 * um Bots oder Testtools genauer zu erkennen. 
 * 
 * Alle Ergebnisse werden für 12 Stunden im Cache gespeichert, um wiederholte Anfragen effizient zu vermeiden 
 * und die Performance zu verbessern. Bei jeder Anfrage wird überprüft, ob es sich um einen Bot, ein Testtool 
 * oder eine verdächtige Anfrage handelt.
 *
 * Funktionen:
 * - `is_bot_or_spider()`: Prüft, ob der User-Agent einem bekannten Bot entspricht und verwendet Caching.
 * - `is_suspicious_bot()`: Prüft verdächtige Header (wie `X-Forwarded-For`) und ob der Referrer ungültig ist.
 * - `is_test_tool()`: Prüft, ob der User-Agent oder zusätzliche Header auf Testtools hinweisen.
 * - `has_valid_referer()`: Überprüft, ob der Referrer ein gültiger interner oder externer Ursprung ist.
 * 
 * @since 1.0.0
 * @return bool True, wenn es sich um einen Bot handelt, sonst False.
 */
if (!function_exists('is_bot_or_spider')) {
    function is_bot_or_spider() {
        // Cache-Schlüssel auf Basis der IP und des User-Agents erstellen
        $cache_key = 'is_bot_' . md5($_SERVER['REMOTE_ADDR'] . $_SERVER['HTTP_USER_AGENT']);
        $cached_result = get_transient($cache_key);
        
        if ($cached_result !== false) {
            return $cached_result;
        }

        // User-Agent in Kleinbuchstaben umwandeln
        $user_agent = strtolower($_SERVER['HTTP_USER_AGENT']);

        // Liste der bekannten Bots
        $bots = [
            'googlebot', 'bingbot', 'slurp', 'duckduckbot', 'baidu', 'yandex',
            'sogou', 'exabot', 'linkedin', 'pinterest', 'msnbot', 'crawl', 'crawler',
            'spider', 'ia_archiver', 'curl', 'fetch', 'python', 'wget', 'monitor',
            'botcheck', 'webcrawler', 'mj12bot', '360spider', 'addthis', 'adsbot',
            'adscanner', 'ahrefsbot', 'fast-webcrawler', 'scooter', 'amazonaws.com',
            'aspiegelbot', 'axios', 'bingpreview', 'blexbot', 'bloglines', 'bubing',
            'bytespider', 'ccbot', 'chatgpt', 'cliqzbot', 'crawl', 'cyotek', 'daum',
            'dispatch', 'domaincrawler', 'dotbot', 'everyonesocialbot', 'facebookexternalhit',
            'facebot', 'feedfetcher', 'femtosearchbot', 'findexa', 'flipboardproxy',
            'gaisbot', 'gigabot', 'go-http-client', 'gptbot', 'qwantbot', 'gridbot', 'heritrix',
            'ips-agent', 'james bot', 'komodiabot', 'linkdexbot', 'lycos', 'mauibot',
            'mediatoolkitbot', 'megaindex', 'metauri', 'mojeekbot', 'moreover', 'nbot',
            'node-fetch', 'obot', 'omgili', 'panscient.com', 'paperlibot', 'petalbot',
            'phantomjs', 'picsearch', 'proximic', 'pubsub', 'radian6', 'rogerbot',
            'rytebot', 'screaming frog seo spider', 'seokicks-robot', 'semrushbot', 'seznambot',
            'serendeputybot', 'sirdata', 'siteexplorer', 'sixtrix', 'smmtbot',
            'spbot', 'surveybot', 'technorati', 'telegrambot', 'thither', 'trendsmap',
            'turnitinbot', 'tweetmeme', 'twingly', 'twitterbot', 'voilabot', 'whatsapp',
            'zyborg', 'wotbox', 'xenu', 'xoviBot', 'disqus', 'yisouspider'
        ];

        // RegExp zum Prüfen auf Bot-User-Agent
        $pattern = '/(' . implode('|', array_map('preg_quote', $bots)) . ')/i';

        if (preg_match($pattern, $user_agent)) {
            // Caching das Ergebnis, dass es sich um einen Bot handelt
            set_transient($cache_key, true, 12 * HOUR_IN_SECONDS);
            return true;
        }

        // Wenn kein Bot gefunden wurde
        set_transient($cache_key, false, 12 * HOUR_IN_SECONDS);
        return false;
    }
}

function is_suspicious_bot() {
    $headers_to_check = ['HTTP_X_FORWARDED_FOR', 'HTTP_VIA', 'HTTP_PROXY_CONNECTION'];
    foreach ($headers_to_check as $header) {
        if (isset($_SERVER[$header])) {
            return true; // Verdächtige Header entdeckt, könnte ein Bot sein
        }
    }

    // Optional: Referrer-Überprüfung direkt hier
    return !has_valid_referer(); // Rückgabe von false, wenn der Referrer ungültig ist
}


function is_test_tool() {
    // User-Agent prüfen
    $user_agent = strtolower($_SERVER['HTTP_USER_AGENT'] ?? '');
    $test_tools = ['lighthouse', 'pagespeed', 'gtmetrix', 'webpagetest', 'chrome-lighthouse', 'pingdom', 'uptimerobot', 'newrelic', 'siteimprove'];
    
    // Weitere Header analysieren
    $is_test_tool = false;
    
    // 1. Prüfen, ob der User-Agent eines bekannten Tools entspricht
    foreach ($test_tools as $tool) {
        if (strpos($user_agent, $tool) !== false) {
            $is_test_tool = true;
            break;
        }
    }

    // 2. Zusätzliche Header prüfen
    $special_headers = ['x-lighthouse-request', 'chrome-proxy'];
    foreach ($special_headers as $header) {
        if (isset($_SERVER[$header])) {
            $is_test_tool = true;
            break;
        }
    }

    // Ergebnis zurückgeben
    return $is_test_tool;
}

/**
 * Überprüft, ob der Referrer-Header valide ist.
 * Ein gültiger Referrer könnte ein interner Link oder ein zugehöriger Ursprung sein.
 *
 * @return bool True, wenn der Referrer gültig ist, sonst False.
 */
 function has_valid_referer() {
    if (!isset($_SERVER['HTTP_REFERER'])) {
        return true; // Keine Referer-Überprüfung, wenn kein Referer vorhanden ist
    }  
    // Prüfe, ob ein Referer vorhanden ist
    if (isset($_SERVER['HTTP_REFERER'])) {
        $referer = strtolower($_SERVER['HTTP_REFERER']);
        $referer_host = parse_url($referer, PHP_URL_HOST);

        // Liste erlaubter externer Domains mit regulären Ausdrücken
        $allowed_external_referers = [
            '/\.google\./i',         // Alle Google-Domains (z. B. google.com, google.de)
            '/\.bing\./i',           // Alle Bing-Domains
            '/\.yahoo\./i',          // Alle Yahoo-Domains
            '/duckduckgo\.com/i',    // DuckDuckGo
            '/\.baidu\./i',          // Alle Baidu-Domains
            '/\.yandex\./i',         // Alle Yandex-Domains
            '/\.facebook\./i',       // Facebook
            '/\.twitter\./i',        // Twitter
            '/\.linkedin\./i',       // LinkedIn
            '/\.instagram\./i',      // Instagram
            '/\.pinterest\./i',      // Pinterest
            // Weitere Domains oder Netzwerke bei Bedarf hinzufügen
        ];

        // Überprüfen, ob der Referer von einer erlaubten externen Domain stammt
        foreach ($allowed_external_referers as $pattern) {
            if (preg_match($pattern, $referer_host)) {
                return true; // Externer Referer von einer Suchmaschine
            }
        }

        // Liste von gültigen internen Domains/Seiten
        $valid_internal_referers = [
            parse_url(home_url(), PHP_URL_HOST), // Hauptseite (Startseite)
            parse_url(site_url(), PHP_URL_HOST), // Basis-URL der Site
            'example.com', // Weitere interne Domains (bei Bedarf ersetzen)
        ];

        // Prüfen, ob der Referer eine gültige interne Quelle ist
        if (in_array($referer_host, $valid_internal_referers, true)) {
            return true; // Gültiger interner Referer
        }

        // Prüfen, ob der Referer zu einer spezifischen internen Seite führt
if (strpos($referer, site_url() . '/') !== false || 
    strpos($referer, site_url() . '/category/') !== false || 
    strpos($referer, site_url() . '/tag/') !== false ||
    strpos($referer, site_url() . '/post/') !== false ||
    strpos($referer, site_url() . '/page/') !== false) {
    return true; // Gültiger interner Referer (Home-Seite, Kategorie, Tag, Beitrag oder Seite)
}
		
        // Wenn keine Bedingungen erfüllt sind, ist der Referer ungültig
        return false;
    }

    // Kein Referer im Header, als ungültig betrachten
    return false;
}

Vorhergehende Bot-Datei, also überholt von obigen:

<?php
/**
 * Erkennung von Bots, Spidern und Testtools basierend auf dem User-Agent.
 *
 * Diese Funktion überprüft, ob der aktuelle Benutzeragent (User-Agent) mit einem bekannten Bot- oder Spider-Identifikator übereinstimmt.
 * Die Funktion nutzt Caching, um wiederholte Anfragen innerhalb eines bestimmten Zeitrahmens zu vermeiden.
 * Die Ergebnisse werden für 12 Stunden gespeichert, um unnötige Bot-Überprüfungen zu minimieren.
 *
 * Wenn der Benutzeragent mit einem bekannten Bot übereinstimmt, wird `true` zurückgegeben,
 * andernfalls `false`. Das Ergebnis wird auch in einem transienten Cache für 12 Stunden gespeichert,
 * um die Performance zu verbessern.
 *
 * @since 1.0.0
 * @return bool True, wenn es sich um einen Bot handelt, sonst False.
 */
if (!function_exists('is_bot_or_spider')) {
    function is_bot_or_spider() {
        // Cache-Schlüssel auf Basis der IP und des User-Agents erstellen
        $cache_key = 'is_bot_' . md5($_SERVER['REMOTE_ADDR'] . $_SERVER['HTTP_USER_AGENT']);
        $cached_result = get_transient($cache_key);
        
        if ($cached_result !== false) {
            return $cached_result;
        }

        $user_agent = strtolower($_SERVER['HTTP_USER_AGENT']);
        $bots = [
            'googlebot', 'bingbot', 'slurp', 'duckduckbot', 'baidu', 'yandex',
            'sogou', 'exabot', 'facebook', 'twitter', 'linkedin', 'pinterest',
            'msnbot', 'bot', 'crawl', 'crawler', 'spider', 'ia_archiver',
            'curl', 'fetch', 'python', 'wget', 'monitor', 'botcheck',
            'crawlerbot', 'webcrawler', 'robozilla', 'mj12bot', 
        ];

        $pattern = '/(' . implode('|', $bots) . ')/i';

        if (preg_match($pattern, $user_agent)) {
            set_transient($cache_key, true, 12 * HOUR_IN_SECONDS);
            return true;
        }

        set_transient($cache_key, false, 12 * HOUR_IN_SECONDS);
        return false;
    }
}

function is_test_tool() {
    // User-Agent prüfen
    $user_agent = strtolower($_SERVER['HTTP_USER_AGENT'] ?? '');
    $test_tools = ['lighthouse', 'pagespeed', 'gtmetrix', 'webpagetest', 'chrome-lighthouse', 'pingdom', 'uptimerobot', 'newrelic', 'siteimprove'];
    
    // Weitere Header analysieren
    $is_test_tool = false;
    
    // 1. Prüfen, ob der User-Agent eines bekannten Tools entspricht
    foreach ($test_tools as $tool) {
        if (strpos($user_agent, $tool) !== false) {
            $is_test_tool = true;
            break;
        }
    }

    // 2. Zusätzliche Header prüfen
    $special_headers = ['x-lighthouse-request', 'chrome-proxy'];
    foreach ($special_headers as $header) {
        if (isset($_SERVER[$header])) {
            $is_test_tool = true;
            break;
        }
    }

    // Ergebnis zurückgeben
    return $is_test_tool;
}

Funktionen des Plug-ins

  1. Bot- und Crawler-Erkennung
    • Bots (z. B. Suchmaschinen-Crawler), Testtools und Admin-Benutzer werden vom Tracking ausgeschlossen.
    • Der Tracking-Mechanismus wird nur bei echten Nutzern aktiviert.
  2. Verweildauer-Erfassung
    • Startzeitaufzeichnung: Sobald die Seite geladen wird, wird die Startzeit einer Sitzung erfasst.
    • Endzeit-Aufzeichnung: Beim Weiterklick, Schließen des Tabs oder Browsers wird die Sitzung abgeschlossen.
  3. Inkognito-Modus
    • Das Plug-in funktioniert auch im Inkognito-Modus und bei eingeschränkter Browserunterstützung, da keine Cookies und nur temporäre Browser-Speicher genutzt werden.
  4. Keine Cookies oder persönliche Daten
    • Keine Speicherung von IP-Adressen oder personenbezogenen Daten. Stattdessen wird eine anonymisierte, gehashte unique_id verwendet, um jeden Besucher zu identifizieren.
    • Der Hashwert wird in der Datenbank gespeichert, wodurch eine direkte Rückverfolgbarkeit des Nutzers ausgeschlossen wird.
    • Daten werden nur temporär in der Session oder lokal gespeichert ('visit_start_time').
  5. Dashboard-Widget
    • Zeigt die aktuellen Besucher und deren Verweildauer in Echtzeit an.
    • Sortierung der Sitzungen nach Startzeit (neueste zuerst).
    • nteraktive Elemente:
    • Aktualisieren: Zeigt die aktuellsten Sitzungsdaten an.
    • Zurücksetzen: Setzt die Besucher-Tabelle zurück.

Die Anzahl der angezeigten Besucher ist auf 25 begrenzt, sodass nur die letzten 25 Besucher angezeigt werden. Möchte man diese Zahl erhöhen, kann man den Wert an der Stelle "// Besuche auf 25 Einträge begrenzen" im Code anpassen.

Die Grenze von 25 Besuchern bietet eine gute Balance zwischen Übersichtlichkeit und Performance, da sie eine schnelle Anzeige der letzten Besucher ermöglicht, ohne die Benutzeroberfläche zu überlasten. Bei Bedarf kann diese Zahl erhöht werden, jedoch sollte darauf geachtet werden, dass die Performance nicht negativ beeinflusst wird, insbesondere bei größeren Besucherzahlen. Werte im Bereich von 30 bis 50 Besuchern sind möglicherweise ebenfalls geeignet, solange die Benutzeroberfläche übersichtlich bleibt und keine merklichen Verzögerungen auftreten.

Beenden des Trackings – Kunststücke

Das Beeden des Trackings ist teils eine Herausforderung, insbesodere das Beenden des Trackings beim Schließen des Browsers zs. Tabs. Weiter sollten anklicke externen Links welche in neuem Tab öffnen das Tracking der Ausgangsseite nicht beenden und weitere so Kleinigkeiten ergaben sich der Entwicklung mehr.  

Normalerweise würde man erwarten, dass beim Schließen eines Browsers oder Tabs das Tracking automatisch beendet wird – das ist schließlich der Moment, in dem der Nutzer die Seite verlässt. Aber in der Praxis ist das nicht ganz so einfach, besonders wenn es darum geht, Daten zuverlässig zu senden.

In vielen modernen Browsern gibt es Einschränkungen, wie Ereignisse wie beforeunload verarbeitet werden. Das bedeutet, dass beim Schließen eines Tabs oder Browsers nicht immer sichergestellt ist, dass eine Anfrage, wie etwa zum Beenden des Besuchs, tatsächlich gesendet wird. Dies liegt daran, dass der Browser die Seite möglicherweise sofort schließt, ohne der Anfrage ausreichend Zeit zu geben.

Warum funktioniert das nicht immer?

  1. Abgebrochene Requests: Wenn ein Browser-Tab geschlossen wird, wird oft auch die ausstehende Anfrage abgebrochen. Standardmethoden wie fetch() reichen dann nicht aus, um die Anfrage zuverlässig zu senden.
  2. Browser-Beschränkungen: Moderne Browser haben Mechanismen eingebaut, die verhindern, dass eine Webseite beim Verlassen des Tabs unnötige Prozesse blockiert oder verzögert. Das bedeutet, dass viele Funktionen (wie das Senden von Daten zum Server) in diesem Moment nicht immer zuverlässig durchgeführt werden.

Die Lösung: navigator.sendBeacon() Eine stabilere Lösung in solchen Fällen ist die Verwendung von navigator.sendBeacon(). Diese Methode ist speziell dafür konzipiert, Daten auch dann zu senden, wenn die Seite oder der Tab geschlossen wird. Der Browser garantiert, dass die Anfrage auch dann gesendet wird, wenn der Nutzer die Seite schließt oder einen anderen Tab öffnet – und das ohne Verzögerungen oder Blockierungen.

Im Vergleich zu normalen Methoden wie fetch() funktioniert sendBeacon() zuverlässiger und stellt sicher, dass wir beim Verlassen der Seite keine wichtigen Tracking-Daten verlieren. Damit sollte das Besuchstracking sauber abgeschlossen werden, selbst wenn der Nutzer die Seite abrupt verlässt.

AJAX-Handler zur Aktualisierung und das feste Timeout

Es kann vorkommen, dass eine Sitzung nicht abgeschlossen und entsprechend markiert wird. Das Folgende bezieht sich im Code des Abschnitts "// AJAX-Handler zur Aktualisierung der Verweildauer aller Besucher inkl. 30 Minuten Timeout". Das dient als Fallback auf Serverebene, um den falschen Eindruck einer langen Sitzung zu vermeiden, welchen aus technischer Hinsicht das Beenden nicht funktioniert hat.

  • Timeout: Ein Besuch wird zum Beispiel nach 30 Minuten  automatisch als 'Timeout' markiert.
  • Timeout: Falls die Seite danach verlassen wird, wird die Sitzungszeit richtiggestellt. Der Timeout bei "Min:Sek" wird reaktiviert, ohne dass die Sitzung als 'Aktiv' zurückgesetzt wird, und die tatsächliche Sitzungszeit wird mit 'Beendet' angezeigt.
Beispiel: Timeout im Dashboard-Widget

Beachte im Dashboard-Widget: dass beim Betätigen von "Verweildauer aktualisieren" die durch Timeout beendeten Sitzungen als "30:00 / Timeout" angezeigt werden:

Bei einer "Neuladung des Dashboards" wird dasselbe jedoch als 'Noch aktiv / Aktiv' angezeigt:
Dies ist technisch so vorgesehen, damit die Sitzungszeit bei Bedarf erneut aktualisiert werden kann und die tatsächliche Verweildauer korrekt angezeigt wird.

Mit tatsächlichen verlassen der Seite, wird die Sitzung endgültig als 'Beendet' gespeichert, somit folgt nach Neuladung und "Verweildauer aktualisieren" dasselbe Ergebnis:

  • Timeout-Fallback: Wenn die Sitzung aus technischen Gründen nicht beendet wird, bleibt in Neuladung des Dashboards der Status 'Noch aktiv / Aktiv' erhalten und mit "Verweildauer aktualisieren" der Status ’30:00 /  Timeout' erhalten, bis die Tabelle zurückgesetzt wird oder der Besucher durch die Begrenzung aus der Liste entfernt wird.

Die Zeitdauer des Timeouts kann individuell eingestellt werden.

Der feste Timeout ist verlässlicher als andere Techniken …

Timeout- und Inaktivitäts-Handling, das müsste separat implementiert werden.

    • Timeout nach 5 Minuten Inaktivität.
    • Fortsetzung der Sitzungszeit bei Weiterklick durch Reaktivierung des Timeout bei "Beendet", also ohne sich als Aktiv zu resetten.

Das Timeout- und Inaktivitäts-Handling würde so konzipiert, um nicht abgeschlossene Sitzungen zeitlich zu begrenzen. Dabei sollte sichergestellt werden, dass die Zeit nicht überschritten wird. Dieses Ziel wurde jedoch nicht vollständig erreicht, und würde das Ideal darstellen. Doch ist die Zeit begrenzt, welche bei Stillstand der Browser in den Ruhezustand wechslet und selbst ein Herzschlag wird das kaum am laufen halten. Schon ab so 10 Minuten kann das der Fall sein und die Reaktivierung findet nicht mehr statt. Das eingentliche Problem des Fallweisen nicht "Beenden" wurde somit nicht gelöst. Bei Interesse findet sich Enwicklung 'Visit Duration: Scenarien.

Technische Highlights

Das Hauptproblem bestand darin, dass Besuche in bestimmten Szenarien, insbesondere bei der Nutzung von VPN-Diensten, nicht korrekt abgeschlossen wurden. Dies führte zu ungenauen Ergebnissen in der Besuchsdauer. In der Annahme, dass dies vor allem von automatisierten Aufrufen abhängt, wurden in der bot-functions.php wesentliche Verbesserungen und Erweiterungen vorgenommen.

Virtuelle private Netzwerke (VPNs)

Virtuelle private Netzwerke (VPNs) werden zunehmend von Nutzern eingesetzt, um ihre Online-Aktivitäten zu schützen und ihre Privatsphäre zu wahren. Ein VPN verschlüsselt den Internetverkehr und maskiert die IP-Adresse, wodurch es schwieriger wird, die Online-Aktivitäten nachzuverfolgen. Viele Menschen nutzen VPNs auch, um geographische Beschränkungen zu umgehen und auf Inhalte zuzugreifen, die in ihrem Land möglicherweise blockiert sind. Auch in Zeiten von wachsender Überwachung und Datensammlung im Internet setzen immer mehr Nutzer auf VPNs, um ihre digitale Identität zu schützen.

Ein entscheidender Schritt war die Implementierung eines zusätzlichen Fallbacks, siehe im  JS-Code unter dem Abschnitt "// Zusätzlicher Fallback beim Verlassen der Seite". Dieser berücksichtigt spezifische Herausforderungen, die durch VPN-Dienste entstehen, beispielsweise das Blockieren bestimmter Skripte oder Funktionen, die normalerweise das Tracking abschließen. Mit dieser Optimierung konnte die Genauigkeit der Ergebnisse deutlich gesteigert werden.

Zusätzlich wurde ein Mechanismus integriert, der sicherstellt, dass Besuche nach einer Inaktivitätsdauer durch Timeout automatisch als Beendet markiert werden (siehe AJAX-Handler und der feste Timeout ↑). Dies erfolgt serverseitig und bietet eine Absicherung für den Fall, dass der Tracking-Abschluss – beispielsweise durch ungewöhnliches Browserverhalten oder technische Störungen – nicht ordnungsgemäß ausgeführt wurde.

Fazit:
Mit diesen Optimierungen kombiniert unser Plug-in nun zwei leistungsstarke Mechanismen:

  1. Präzise Abschlüsse auch bei komplexen Nutzungsbedingungen wie VPN.
  2. Eine zuverlässige serverseitige Sicherung für eine konsistente Datenerfassung.
Weitere technische Details:
  1. Initialisierung des Besuchs-Trackings:
    • Die Funktion start_visit_tracking() wird durch den wp_footer-Hook in den Footer jeder Seite eingebunden.
    • Sie erfasst die IP-Adresse des Besuchers ($user_ip) und prüft, ob dieser Besucher ein Bot oder Admin ist. Falls ja, wird das Tracking abgebrochen.
    • Wenn keine Sitzung aktiv ist, wird eine neue Sitzung gestartet (session_start()), und es wird überprüft, ob bereits eine Startzeit für den Besuch gesetzt wurde. Wenn nicht, wird die aktuelle Zeit als Startzeit gespeichert.
    • Ein JavaScript-Skript wird im Footer eingebunden, das beim Laden der Seite eine eindeutige ID für den Besuch erzeugt und die Startzeit des Besuchs über AJAX an den Server sendet.
  2. Besuch beenden und Verweildauer speichern:
    • Die Funktion end_visit_tracking() wird aufgerufen, wenn der Besuch abgeschlossen ist. Sie berechnet die Verweildauer und speichert diese als Option in WordPress (last_visitor_duration). Die Startzeit des Besuchs wird dann aus der Sitzung entfernt.
    • Dies ermöglicht die Speicherung der Verweildauer für spätere Auswertungen.
  3. Timeout-Überprüfung im Backend (AJAX-Handler):
    • Die Funktion check_visit_timeout() wird durch AJAX-Anfragen aufgerufen, um zu prüfen, ob das Besuchs-Timeout überschritten wurde. Wenn ja, wird der Besuch als beendet markiert und die Besuchsdaten im Backend aktualisiert.
    • Wenn das Timeout erreicht ist, wird der Status des Besuchs auf "Beendet" gesetzt und die Verweildauer wird berechnet und gespeichert.
  4. Ausschluss von IPs und Bots:
    • Zwei Funktionen (is_ip_excluded() und is_bot_or_spider()) werden verwendet, um Besucher anhand ihrer IP-Adresse oder durch eine Bot-Detektion aus dem Tracking auszuschließen.
    • Diese Funktionen prüfen, ob die IP-Adresse in einer Liste von ausgeschlossenen IPs enthalten ist oder ob der User-Agent des Besuchers auf bekannte Bots hinweist.
  5. Verarbeitung der Besuchsdaten:
    • Mit der Funktion start_visit() werden Besuchsdaten beim Seitenaufruf gespeichert. Eine eindeutige ID für den Besuch sowie der Titel der Seite werden zusammen mit der Startzeit in einer Option (current_visits) abgelegt.
    • Diese Funktion stellt sicher, dass nur eine begrenzte Anzahl von 25 Besuchsdaten gleichzeitig gespeichert wird. Wird diese Zahl überschritten, wird der älteste Besuch gelöscht, um Platz für neue Daten zu schaffen.
    • Der Wert für die maximale Anzahl der gespeicherten Besucher ist auf 25 festgelegt. Dieser Wert kann angepasst werden, indem man den Wert an der Stelle // Besuche auf 25 Einträge begrenzen im Code ändert.
  6. Update der Verweildauer:
    • Die Funktion update_visit_duration() aktualisiert die Verweildauer eines Besuchs, indem sie den Endzeitpunkt des Besuchs erfasst und die Differenz zur Startzeit berechnet.
    • Falls keine Verweildauer vorhanden ist, wird der Besuch als "Aktiv" markiert.
  7. Anzeige der Besuchsdaten im Dashboard:
    • Über das WordPress-Dashboard wird ein Widget mit der Funktion add_visit_duration_dashboard_widget() erstellt, das die Besuchsdaten anzeigt.
    • Im Widget werden die Besucher nach ihrer Startzeit sortiert, und es wird die Verweildauer in Minuten und Sekunden sowie der Status des Besuchs (aktiv oder beendet) angezeigt.
    • Ein scrollbarer Bereich zeigt die Besuchsdaten, und der Status wird farblich hervorgehoben: Aktive Besuche werden gelb markiert.
  8. JavaScript für die Interaktivität:
    • Der Update-Button aktualisiert die Besuchsdaten, indem alle aktiven Besuchsdaten überprüft und in die Tabelle eingefügt werden.
    • Der Reset-Button ermöglicht das Zurücksetzen der Besuchsdaten im Widget, indem alle Besuchsdaten gelöscht werden.
  9. Speicherung und Anzeige von Besuchsdaten:
    • Wenn ein Besucher den Browser schließt (oder die Seite verlässt), wird die Verweildauer durch die beforeunload-Ereignisbehandlung erfasst und an den Server gesendet.
  10. Verwaltung der Besuchsdaten im Backend:
    • Die Funktion update_all_visit_durations() aktualisiert die Verweildauer aller aktiven Besucher. Dies erfolgt regelmäßig oder auf Anfrage, um sicherzustellen, dass die Daten im Dashboard immer aktuell sind.
  11. Zurücksetzen der Besuchsdaten:
    • Die Funktion reset_visit_duration() löscht alle gespeicherten Besuchsdaten, sowohl im Frontend als auch im Backend.
'wp_options' oder eigene Datenbanktabelle: Welche Lösung passt besser?

Für den aktuellen Anwendungsfall des Plug-ins, bei dem:

  1. Daten begrenzt sind (max. 50 Trackings).
  2. Ältere Daten überschrieben werden (keine wachsende Datenmenge).
  3. Daten nicht langfristig gespeichert oder für umfangreiche Statistiken verwendet werden.

… ist die Speicherung in der wp_options-Tabelle effizienter als eine separate Datenbanktabelle.

Warum wp_options für das Plug-in besser ist:

  1. Einfache Verwaltung: Die wp_options-Tabelle ist bereits integraler Bestandteil von WordPress. Somit ist sich um die Erstellung, Pflege oder Optimierung einer separaten Tabelle nicht zu kümmern.
  2. Performance passt: Da es sich um eine begrenzte Menge an Daten handelt, wird die Performance durch die Nutzung der wp_options-Tabelle nicht beeinträchtigt.
  3. Reduzierter Aufwand: Die Logik für die Datenbankschicht, wie z. B. Schema-Definitionen, Updates und spezielle Queries entfällt.
  4. Automatische Kompatibilität: Backups, Exports und Migrationen sind nahtlos, weil die Daten mit anderen WordPress-Optionen zusammenhängen.

Warum eine Datenbank-Tabelle sinnvoll wäre:

Die Relevanz einer eigenen Tabelle würde erst entstehen, wenn:

  • Langfristige Daten gespeichert werden (z. B. Besuchshistorien oder komplexe Analysen).
  • Mehr Daten pro Eintrag zu verwalten sind (z. B. detaillierte Metadaten pro Besuch).
  • Spezielle Abfragen durchzuführen sind, wie z. B. Durchschnittswerte oder Filter nach Datum.
Fazit für das aktuelle Plug-in

Da die Datenmenge gering ist, sich laufend ändert und keine langfristige Speicherung nötig ist, ist die Nutzung der wp_options-Tabelle sogar besser geeignet. Bei dem aktuellen Ziel ist die Struktur schlank.


Für eine bessere Strukturierung wäre es sinnvoll, die Dateien zu separieren, in etwa:

/wp-content/plugins/visit-duration/  
├── visit-duration.php       # Haupt-Plugin-Datei (Loader und zentrale Logik)  
├── includes/                # Enthält PHP-Funktionen und Hilfsdateien  
│   ├── bot-functions.php    # Funktionen für Bot-Handling  
│   ├── ajax-handlers.php    # Funktionen für AJAX-Requests  
│   ├── helpers.php          # Hilfsfunktionen (z. B. IP-Validierung)  
├── assets/                  # Statische Ressourcen wie JS und CSS  
│   ├── script.js            # JavaScript-Datei (Frontend)  
│   ├── style.css            # CSS-Datei  

Kommentar aus dem Alltag: In der aktuellen Bearbeitung funktionierte zwar der erste Abschnitt des Scripts in der visit-duration.js, aber der zweite Teil wollte nicht wie gewünscht laufen. Eh klar, weil ja danach neues php ist … Daher verbleibt das Plug-in vorerst in der Datei visit-duration.php sowie der bot-functions.php.

Es ist nicht immer einfach mit ChatGPT, da es selbst so grundlegende Dinge wie die vorgegebene Struktur eigenständig veränderte. Selbst nach fünf Anläufen und einem sorgfältigen Beginn des Chats kommt es nicht immer zum gewünschten Erfolg – aber großes "Danke sehr" an ChatGPT! – "Helferlein, du bist schon ein tolles Kerlchen und immer freundlich" 🙂

DSGVO-Konformität

  • Keine Speicherung von personenbezogenen Daten.
  • Verzicht auf Cookies und IP-Adressen.
  • Alle Daten werden ausschließlich für die aktuelle Sitzung verarbeitet und nicht langfristig gespeichert.

Verweildauer im Blick –
für eine bessere Analyse, ohne Daten zu sammeln!

Hinweise zur Zähler-Funktionalität

Bei der Implementierung von 'Visit Duration, gibt es grundsätzliche Punkte zu beachten, um eine zuverlässige Funktionalität sicherzustellen. Im Folgenden werden die Aspekte behandelt, die zu berücksichtigen sind, um eine fehlerfreie und stabile Zählerfunktion zu gewährleisten.

Startseite und die statische Seite  konsistent trackbar

Im Zusammenhang mit dem Tracking kann der folgende Code integriert werden, damit der Titel der Startseite und der statischen Seite beim Laden der Seite dynamisch geändert wird. Dadurch wird sichergestellt, dass beim Tracking diese Seiten mit einem spezifischen Titel erfasst werden.

Die JavaScript-Anweisungen document.title = 'Startseite …' und document.title = 'Statische Seite …' sollten entsprechend angepasst werden, um die gewünschten Titel für diese Seiten zu setzen. So erhält die Startseite sowie die statische Seite einen eindeutigen Titel, was die Nachverfolgbarkeit und Auswertung im Tracking-System erleichtert.

functions.php

/* --- Titel für Startseite und Beitragsseite für den Tracker definieren --- */

function set_custom_title_for_pages() {
    // JavaScript-Code je nach Seite dynamisch einfügen
    if (is_front_page()) {
        echo "<script>
            // Setze den Titel für die Startseite
            document.title = 'Startseite von WP Wegerl'; // Beispiel-Titel für die Startseite
        </script>";
    } elseif (is_home()) {
        echo "<script>
            // Setze den Titel für die Beitragsseite
            document.title = 'Beitrags-Blogseite'; // Beispiel-Titel für die Blogseite
        </script>";
    }
}
add_action('wp_footer', 'set_custom_title_for_pages');

/* - Ende Titel für Startseite und Beitragsseite - */
  • Der Titel der Startseite und der statischen Seite wird durch den Code dynamisch gesetzt, was es ermöglicht, beide Seiten korrekt zu tracken.
  • Dies trägt dazu bei, den Titel der Seiten bei der Erfassung der Besuchsdaten festzulegen und eine präzise Verweildauer zu messen.

Dieser kleine Code sorgt also dafür, dass die Startseite im Kontext des Besuchs-Trackings klar identifizierbar ist.

Weiterer Nutzen:

  • SEO: Der benutzerdefinierte Titel hilft, die Startseite eindeutig zu kennzeichnen, was für Suchmaschinen von Vorteil sein kann, da der Titel dann gezielt für diese Seite optimiert werden kann.
  • Benutzerfreundlichkeit: Falls der Titel der Startseite von Belang für die Identifikation der Seite ist (zum Beispiel „Startseite von [Website-Name]“), sorgt dieser Code dafür, dass dies auch im Browser-Tab korrekt angezeigt wird.

Aktualisierung des Dashboards nach Inaktivität

Wenn man nach längerer Zeit zum Dashboard zurückkehrt, kann es sein, dass die Aktualisierung nicht sofort funktioniert und ein Neuladen der Seite erforderlich ist. Dies ist technisch bedingt, da das Dashboard in der Regel auf AJAX-basierte Datenabfragen angewiesen ist, die eine gewisse Lebensdauer haben. Nach einer längeren Inaktivität oder einem Timeout der Sitzung können diese Abfragen nicht mehr die aktuellen Daten liefern. Ein Neuladen der Seite sorgt dafür, dass die Verbindung zu den Serverressourcen und den neuesten Daten neu hergestellt wird.

Beobachtungen im Dashboard-Widget 'Visit Duration'

Titel erscheint in andere Sprache

Des Trackings der Website erscheint ein Titel دکمه برگشت صفحه قبل! – مطمئناً کاملاً عملی است, in einer anderes Sprache, was bedeutet das?

Der Titel "دکمه برگشت صفحه قبل! – مطمئناً کاملاً عملی است" ist auf Persisch (Farsi)

Wenn die Website automatisch übersetzt wird (zum Beispiel durch ein Plug-in wie Google Translate oder eine andere Übersetzungsfunktion), könnte der Titel der Seite in die Sprache des Besuchers (in diesem Fall Persisch) übersetzt worden sein. Wenn jemand aus Persien oder einer anderen persischsprachigen Region auf die Seite zugreift, könnte dies zu der unerwarteten persischen Zeichenfolge führen.

Hier ist, was wahrscheinlich passiert ist:

  1. Automatische Übersetzung: Die Seite hat möglicherweise eine automatische Übersetzung aktiviert, die den Titel oder den Inhalt der Seite basierend auf der Region oder Sprache des Besuchers übersetzt.
  2. Sprachspezifische Inhalte: Wenn die automatische Übersetzung aktiviert ist, könnte der Titel einer Seite, die ursprünglich auf Deutsch oder Englisch war, fälschlicherweise ins Persische übersetzt werden. In diesem Fall wird der Titel "Zurück-Button der vorherigen Seite" zu "دکمه برگشت صفحه قبل" übersetzt.
  3. Tracking: Dein Tracking-Plugin erfasst diesen Titel und speichert ihn als den "Seiten-Titel". Da die Seite in einer anderen Sprache angezeigt wird, erscheint auch der Titel in dieser Sprache im Tracking.
Schneller Seitenwechsel

Bei schnellem Wechsel zwischen zwei Seiten, beispielsweise durch die Vor- und Zurück-Navigation des Browsers, kann es vorkommen, dass die Anzeige "-1:-1" oder ähnliche zeigt.

Datenbanktabelle 'wp_options'

Hinweis: Die Aufrufe befinden sich in der Datenbank unter ‚Options‘ (wp_options).

Siehe auch hier In der Datenbank …

Dem Reiter SQL, der SELECT-Befehl:

SELECT option_value FROM wp_options WHERE option_name = 'current_visits';

… ruft den gesamten Inhalt von option_value für current_visits ab. Das bedeutet:

  • Alle gespeicherten Besuchsdaten: Wenn das Plug-in neue Besuchsdaten hinzufügt oder aktualisiert, werden sie alle unter dem option_name "current_visits" in der option_value-Spalte abgelegt.
  • Datenformat: Da WordPress hier Arrays oder Objekte als serialisierte Zeichenketten speichert, wird alles in einem einzigen option_value-Eintrag zusammengefasst.

Der gesamte Datensatz für "current_visits" wird in einem einzigen Datenbankeintrag gespeichert. Die get_option('current_visits')-Funktion holt diesen Eintrag und wandelt ihn in das ursprüngliche Array um, sodass das Plug-in direkt auf die strukturierten Daten zugreifen kann.

Falls das Plug-in nicht mehr verwendet wird, kann folgender Code in die Datei eingefügt werden, um die Option automatisch beim Deaktivieren des Plug-ins zu entfernen. – die sicherste Methode.

Folgendes ist dem aktuellen Plug-in schon implementiert:

// Hook for plugin deactivation
register_deactivation_hook(__FILE__, 'visit_duration_deactivate');

// Function to delete 'current_visits' option on deactivation
function visit_duration_deactivate() {
    delete_option('current_visits');
}

Tracking im Sprintmodus: Warum der Code nicht mitkommt

Der Code ist nicht dafür konzipiert, Sitzungen mit schnellen, aufeinanderfolgenden Klicks, wie sie in Testszenarien auftreten können, vollständig zu erfassen – selbst wenn diese außerhalb der festgelegten 3-Sekunden-Schwelle liegen (Besuchstracking startet erst nach 3 Sekunden).

Von Rennmäusen und Webseiten:
Klicks wie Käsefallen – Tracking,
das sich im Mauseloch versteckt!

Dabei ist zu berücksichtigen, dass Testszenarien oft anders mit dem Tracking-Skript interagieren als ein typischer Nutzer. Besonders bei raschen Klickfolgen von hin und her kann es vorkommen, dass der Aufruf zum Beenden einer Seite nicht rechtzeitig verarbeitet wird, bevor die nächste Anfrage gestellt wird. Solche Fälle sind im gewöhnlichen Nutzerverhalten selten, können jedoch in Testsituationen auftreten.

Zusammenfassend:  Kommen schnelle Klickfolgen ins Spiel, bleibt der Beendigungsaufruf manchmal "hängen". Das bedeutet, die Zeit des vorherigen Seitenbesuchs wird nicht korrekt abgeschlossen, da der nächste Seitenaufruf die Verarbeitung unterbricht.


Die Entwicklung des Plug-ins nahm etwa drei Wochen in Anspruch, wobei ein Arbeitstag oft weit über die üblichen acht Stunden hinausging. Doch für uns war das kein Problem – unser Engagement und unsere Leidenschaft für dieses Projekt machten jede Stunde lohnenswert. 🙂

wp wegerl.at

Der Beitrag wurde mit fachlicher Unterstützung erstellt.


Aktualisiert im Jahr 2024 November

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Ich stimme zu, dass meine Daten auf dieser Website gespeichert und verarbeitet werden. Hier sind die Informationen zu den Datenschutzerklärungen.

  • Bitte beachte, dass vor der Publikation eines Kommentars eine manuelle Freigabe erforderlich ist.