Enwicklung 'Visit Duration Teil 1'

Entwicklung 'Visit Duration Teil 1'

Entwicklung:

<?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: 12.12.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']) : '';

    // Die vollständige URL mit Fragment holen
    $full_url = isset($_POST['full_url']) ? sanitize_text_field($_POST['full_url']) : '';

    // Überprüfen, ob sowohl unique_id als auch page_title gesetzt sind
    if ($unique_id && $page_title) {
        // Besuchsdaten holen oder initialisieren
        $visits = get_option('current_visits', []);

        // Besuchsdaten für diesen Besuch
        $visit_data = [
            'unique_id'   => $hashed_unique_id,  // Gehashte unique_id speichern
            'page_title'  => $page_title,
            'full_url'    => $full_url,           // Speichern der vollständigen URL mit Fragment
            'visit_time'  => current_time('mysql')
        ];

        // Besuchsdaten in die Option einfügen oder aktualisieren
        $visits[$hashed_unique_id] = $visit_data;
        update_option('current_visits', $visits);

        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 () {
                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);
        }
    }
	
	(function () {
    const currentUrl = window.location.href;
    const previousUrl = sessionStorage.getItem('lastVisitedUrl');
    let uniqueId = sessionStorage.getItem('uniqueId');

    if (previousUrl === currentUrl) {
        // Gleiche Seite: Behalte die aktuelle uniqueId oder generiere eine neue, falls keine existiert
        uniqueId = uniqueId || generateUniqueId();
    } else {
        // Neue Seite oder erster Besuch: Erstelle neue uniqueId
        uniqueId = generateUniqueId();
        sessionStorage.setItem('lastVisitedUrl', currentUrl);
    }

    sessionStorage.setItem('uniqueId', uniqueId);
    window.uniqueId = uniqueId;

    function generateUniqueId() {
        return 'id-' + Math.random().toString(36).substr(2, 16);
    }

    // console.log('Aktuelle uniqueId:', uniqueId);
})();

	let debounceTimer = null; // Debounce-Timer

    // 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', function(event) {
    sendEndVisit(); // Versucht sofort, die Sitzung zu beenden
});

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', true); 
            xhr.setRequestHeader('Content-Type', 'application/json');
            xhr.onload = function() {
                if (xhr.status >= 200 && xhr.status < 300) {
                    // console.log('Daten erfolgreich gesendet');
                } else {
                    console.error('Fehler beim Fallback-Senden:', xhr.status);
                    // Speichere die Daten im localStorage für spätere Verarbeitung
                    localStorage.setItem('visitData', payload);
                }
            };
            xhr.onerror = function() {
                console.error('Netzwerkfehler beim Fallback');
                // Speichere die Daten im localStorage für spätere Verarbeitung
                localStorage.setItem('visitData', payload);
            };
            xhr.send(payload);
        }
    }
}

// Prüfen, ob noch zu sendende Daten im localStorage sind, und diese bei Gelegenheit senden
window.addEventListener('load', function() {
    const storedData = localStorage.getItem('visitData');
    if (storedData) {
        const payload = JSON.parse(storedData);
        const xhr = new XMLHttpRequest();
        xhr.open('POST', window.ajaxUrl + '?action=update_visit_duration', true);
        xhr.setRequestHeader('Content-Type', 'application/json');
        xhr.onload = function() {
            if (xhr.status >= 200 && xhr.status < 300) {
                console.log('Daten nachträglich erfolgreich gesendet');
                localStorage.removeItem('visitData'); // Entferne die zwischengespeicherten Daten
            } else {
                console.error('Fehler beim nachträglichen Senden:', xhr.status);
            }
        };
        xhr.onerror = function() {
            console.error('Netzwerkfehler beim nachträglichen Senden');
        };
        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);
            }
        }
    }

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

    // Abschlusssignal an den Server senden, wenn die Seite verlassen wird
    window.addEventListener('beforeunload', function () {
        navigator.sendBeacon(window.ajaxUrl + '?action=end_visit', JSON.stringify({
            unique_id: window.uniqueId,
            page_title: document.title
        }));
    });
	
// 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', true); // Asynchroner Fallback
        xhr.setRequestHeader('Content-Type', 'application/json');
        xhr.onload = function() {
            if (xhr.status >= 200 && xhr.status < 300) {
                console.log('Fallback-Daten erfolgreich gesendet');
            } else {
                console.error('Fehler beim Fallback-Senden:', xhr.status);
            }
        };
        xhr.onerror = function() {
            console.error('Netzwerkfehler beim Fallback');
        };
        xhr.send(JSON.stringify({
            unique_id: window.uniqueId,
            end_time: Date.now()
        }));
    }
});

})();
    </script>";
}

// End Visit Handler: Zusätzliche Logik zur Aktualisierung des Besuchsstatus und der Dauer
function end_visit_handler() {
    check_ajax_referer('your_nonce', 'security');

    // unique_id aus der Anfrage holen
    $unique_id = isset($_POST['unique_id']) ? sanitize_text_field($_POST['unique_id']) : '';
    $hashed_unique_id = hash('sha256', $unique_id);

    // Besuchsdaten holen und aktualisieren
    $visits = get_option('current_visits', []);
    $current_time = time();

    if (isset($visits[$hashed_unique_id])) {
        $visit_data = &$visits[$hashed_unique_id];
        
        // Sitzung beenden
        if (!isset($visit_data['end_time'])) {
            $visit_data['end_time'] = $current_time;
            $visit_data['duration'] = $current_time - $visit_data['start_time'];
            $visit_data['status'] = 'Beendet';
        }
    }

    // Daten speichern
    update_option('current_visits', $visits);

    wp_send_json_success('Besuch beendet');
}

add_action('wp_ajax_end_visit', 'end_visit_handler');
add_action('wp_ajax_nopriv_end_visit', 'end_visit_handler');

// 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',
        '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>Startzeit</th><th style="text-align: center; padding: 0 10px;">Verweildauer <br>(Min:Sek)</th><th>Status</th></tr>';
    echo '</thead>';
    
    echo '<tbody>';

		foreach ($visits as $visit_data) {
    // Setze den Status basierend auf der Verweildauer
    if (isset($visit_data['duration'])) {
        // Wenn die Verweildauer bereits gesetzt ist, ist die Sitzung beendet
        $status = 'Beendet';
    } else {
        // Wenn keine Dauer gesetzt ist, prüfen wir, ob das Timeout überschritten wurde
        $current_time = time();
        $timeout_limit = 30 * 60; // Timeout-Limit: 30 Minuten

        // Wenn die Sitzung das Timeout überschritten hat
        if (isset($visit_data['start_time']) && ($current_time - $visit_data['start_time']) > $timeout_limit) {
            $status = 'Offen'; // Timeout erreicht, als "Offen" markieren
        } else {
            $status = 'Aktiv'; // Noch aktiv, also "Aktiv" markieren
        }
    }
	
	if (isset($visit_data['start_time'])) {
    // Zeit eine Stunde vorverlegen (3600 Sekunden)
    $start_time = date('H:i:s', $visit_data['start_time'] + 3600); 
} else {
    $start_time = 'Unbekannt';
}

    // 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
    if ($status === 'Aktiv') {
        $row_color = 'rgba(255, 235, 59, 0.6)'; // Gelb, opac für Aktiv
    } elseif ($status === 'Offen') {
        $row_color = 'rgba(255, 235, 59, 0.2)'; // Gelb, sehr opac für Offen
    } else {
        $row_color = '#ECF3F9'; // Hellblau für beendet
    }
		  
	// Zeile in der Tabelle ausgeben
	echo "<tr style='background-color: $row_color;'>
        	<td>{$visit_data['page_title']}</td>
        	<td>{$start_time}</td>
        	<td style='text-align: center;'>{$formatted_duration}</td>
        	<td>{$status}</td>
      </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) {
                    let rowColor;

                    // Farben basierend auf dem Status setzen
                    if (visit.status === "Aktiv") {
                        rowColor = "#ffeb3b"; // Gelb für aktiv
                    } else if (visit.status === "Timeout") {
                        rowColor = 'rgba(255, 235, 59, 0.2)'; // Gelb sehr opac für timeout
                    } else {
                        rowColor = "#ECF3F9"; // Hellblau für beendet
                    }

                    var formattedDuration = visit.formatted_duration || 'Noch aktiv';

                    // Falls visit.start_time ein Unix-Timestamp ist, umwandeln
                    var formattedStartTime = visit.start_time 
                        ? formatTime(visit.start_time) 
                        : 'Unbekannt'; // Umwandlung der Startzeit in lesbares Format

                    tableBody.append(
                        '<tr style="background-color: ' + rowColor + '">' +
                        '<td>' + visit.page_title + '</td>' +
                        '<td>' + formattedStartTime + '</td>' + // Startzeit-Spalte hinzugefügt
                        '<td style="text-align: center;">' + formattedDuration + '</td>' +
                        '<td>' + visit.status + '</td>' +
                        '</tr>'
                    );
                });
            } else {
                alert('Fehler bei der Aktualisierung der Verweildauer.');
            }
        },
        error: function() {
            alert('Fehler beim Aktualisieren der Verweildauer.');
        }
    });
});

// Hilfsfunktion, um Unix-Timestamp in hh:mm:ss umzuwandeln
function formatTime(timestamp) {
    var date = new Date(timestamp * 1000); // Umwandlung von Sekunden-Timestamp zu Millisekunden
    var hours = date.getHours().toString().padStart(2, '0');
    var minutes = date.getMinutes().toString().padStart(2, '0');
    var seconds = date.getSeconds().toString().padStart(2, '0');
    return hours + ':' + minutes + ':' + seconds;
}

// "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>Startzeit</th><th style="text-align: center; padding: 0 10px;">Verweildauer <br>(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');



/* Cronjob? – ist nicht relevant
// Cron-Intervall hinzufügen
add_filter('cron_schedules', function ($schedules) {
    $schedules['every_five_minutes'] = [
        'interval' => 5 * 60, // Alle 5 Minuten
        'display'  => __('Every 5 Minutes'),
    ];
    return $schedules;
});

// Cronjob registrieren
add_action('wp', function () {
    if (!wp_next_scheduled('check_incomplete_sessions_event')) {
        wp_schedule_event(time(), 'every_five_minutes', 'check_incomplete_sessions_event');
    }
});

// Funktion für den Cronjob
add_action('check_incomplete_sessions_event', function () {
    global $wpdb;

    $table_name = $wpdb->prefix . 'visit_tracking';
    $incomplete_sessions = $wpdb->get_results("SELECT * FROM $table_name WHERE visit_time IS NULL");

    foreach ($incomplete_sessions as $session) {
        $wpdb->update(
            $table_name,
            ['visit_time' => current_time('mysql')],
            ['id' => $session->id]
        );
    }
});

// Cronjob bei Deaktivierung entfernen
register_deactivation_hook(__FILE__, function () {
    $timestamp = wp_next_scheduled('check_incomplete_sessions_event');
    if ($timestamp) {
        wp_unschedule_event($timestamp, 'check_incomplete_sessions_event');
    }
});
*/


// 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');
}

Entwicklung:

<?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: 11.12.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']) : '';

    // Die vollständige URL mit Fragment holen
    $full_url = isset($_POST['full_url']) ? sanitize_text_field($_POST['full_url']) : '';

    // Überprüfen, ob sowohl unique_id als auch page_title gesetzt sind
    if ($unique_id && $page_title) {
        // Besuchsdaten holen oder initialisieren
        $visits = get_option('current_visits', []);

        // Besuchsdaten für diesen Besuch
        $visit_data = [
            'unique_id'   => $hashed_unique_id,  // Gehashte unique_id speichern
            'page_title'  => $page_title,
            'full_url'    => $full_url,           // Speichern der vollständigen URL mit Fragment
            'visit_time'  => current_time('mysql')
        ];

        // Besuchsdaten in die Option einfügen oder aktualisieren
        $visits[$hashed_unique_id] = $visit_data;
        update_option('current_visits', $visits);

        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 () {
                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) {
            const xhr = new XMLHttpRequest();
            xhr.open('POST', window.ajaxUrl + '?action=update_visit_duration', true);
            xhr.setRequestHeader('Content-Type', 'application/json');
            xhr.onload = function() {
                if (xhr.status >= 200 && xhr.status < 300) {
                    // console.log('Fallback-Daten erfolgreich gesendet');
                } else {
                    console.error('Fehler beim Fallback-Senden:', xhr.status);
                }
            };
            xhr.onerror = function() {
                console.error('Netzwerkfehler beim Fallback');
            };
            xhr.send(JSON.stringify({
                unique_id: window.uniqueId,
                end_time: Date.now()
            }));
        }
    });

    let debounceTimer = null;

    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', true); 
                xhr.setRequestHeader('Content-Type', 'application/json');
                xhr.onload = function() {
                    if (xhr.status >= 200 && xhr.status < 300) {
                        // console.log('Daten erfolgreich gesendet');
                    } else {
                        console.error('Fehler beim Fallback-Senden:', xhr.status);
                    }
                };
                xhr.onerror = function() {
                    console.error('Netzwerkfehler beim Fallback');
                };
                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);
            }
        }
    }

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

    // Abschlusssignal an den Server senden, wenn die Seite verlassen wird
    window.addEventListener('beforeunload', function () {
        navigator.sendBeacon(window.ajaxUrl + '?action=end_visit', JSON.stringify({
            unique_id: window.uniqueId,
            page_title: document.title
        }));
    });

})();
    </script>";
}
// End Visit Handler: Zusätzliche Logik zur Aktualisierung des Besuchsstatus und der Dauer
function end_visit_handler() {
    check_ajax_referer('your_nonce', 'security');

    // unique_id aus der Anfrage holen
    $unique_id = isset($_POST['unique_id']) ? sanitize_text_field($_POST['unique_id']) : '';
    $hashed_unique_id = hash('sha256', $unique_id);

    // Besuchsdaten holen und aktualisieren
    $visits = get_option('current_visits', []);
    $current_time = time();

    if (isset($visits[$hashed_unique_id])) {
        $visit_data = &$visits[$hashed_unique_id];
        
        // Sitzung beenden
        if (!isset($visit_data['end_time'])) {
            $visit_data['end_time'] = $current_time;
            $visit_data['duration'] = $current_time - $visit_data['start_time'];
            $visit_data['status'] = 'Beendet';
        }
    }

    // Daten speichern
    update_option('current_visits', $visits);

    wp_send_json_success('Besuch beendet');
}

add_action('wp_ajax_end_visit', 'end_visit_handler');
add_action('wp_ajax_nopriv_end_visit', 'end_visit_handler');


// 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',
        '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>Startzeit</th><th style="text-align: center; padding: 0 10px;">Verweildauer <br>(Min:Sek)</th><th>Status</th></tr>';
    echo '</thead>';
    
    echo '<tbody>';

		foreach ($visits as $visit_data) {
    // Setze den Status basierend auf der Verweildauer
    if (isset($visit_data['duration'])) {
        // Wenn die Verweildauer bereits gesetzt ist, ist die Sitzung beendet
        $status = 'Beendet';
    } else {
        // Wenn keine Dauer gesetzt ist, prüfen wir, ob das Timeout überschritten wurde
        $current_time = time();
        $timeout_limit = 30 * 60; // Timeout-Limit: 30 Minuten

        // Wenn die Sitzung das Timeout überschritten hat
        if (isset($visit_data['start_time']) && ($current_time - $visit_data['start_time']) > $timeout_limit) {
            $status = 'Offen'; // Timeout erreicht, als "Offen" markieren
        } else {
            $status = 'Aktiv'; // Noch aktiv, also "Aktiv" markieren
        }
    }
	
	if (isset($visit_data['start_time'])) {
    // Zeit eine Stunde vorverlegen (3600 Sekunden)
    $start_time = date('H:i:s', $visit_data['start_time'] + 3600); 
} else {
    $start_time = 'Unbekannt';
}

    // 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
    if ($status === 'Aktiv') {
        $row_color = 'rgba(255, 235, 59, 0.6)'; // Gelb, opac für Aktiv
    } elseif ($status === 'Offen') {
        $row_color = 'rgba(255, 235, 59, 0.2)'; // Gelb, sehr opac für Offen
    } else {
        $row_color = '#ECF3F9'; // Hellblau für beendet
    }
		  
	// Zeile in der Tabelle ausgeben
	echo "<tr style='background-color: $row_color;'>
        	<td>{$visit_data['page_title']}</td>
        	<td>{$start_time}</td>
        	<td style='text-align: center;'>{$formatted_duration}</td>
        	<td>{$status}</td>
      </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) {
                    let rowColor;

                    // Farben basierend auf dem Status setzen
                    if (visit.status === "Aktiv") {
                        rowColor = "#ffeb3b"; // Gelb für aktiv
                    } else if (visit.status === "Timeout") {
                        rowColor = 'rgba(255, 235, 59, 0.2)'; // Gelb sehr opac für timeout
                    } else {
                        rowColor = "#ECF3F9"; // Hellblau für beendet
                    }

                    var formattedDuration = visit.formatted_duration || 'Noch aktiv';

                    // Falls visit.start_time ein Unix-Timestamp ist, umwandeln
                    var formattedStartTime = visit.start_time 
                        ? formatTime(visit.start_time) 
                        : 'Unbekannt'; // Umwandlung der Startzeit in lesbares Format

                    tableBody.append(
                        '<tr style="background-color: ' + rowColor + '">' +
                        '<td>' + visit.page_title + '</td>' +
                        '<td>' + formattedStartTime + '</td>' + // Startzeit-Spalte hinzugefügt
                        '<td style="text-align: center;">' + formattedDuration + '</td>' +
                        '<td>' + visit.status + '</td>' +
                        '</tr>'
                    );
                });
            } else {
                alert('Fehler bei der Aktualisierung der Verweildauer.');
            }
        },
        error: function() {
            alert('Fehler beim Aktualisieren der Verweildauer.');
        }
    });
});

// Hilfsfunktion, um Unix-Timestamp in hh:mm:ss umzuwandeln
function formatTime(timestamp) {
    var date = new Date(timestamp * 1000); // Umwandlung von Sekunden-Timestamp zu Millisekunden
    var hours = date.getHours().toString().padStart(2, '0');
    var minutes = date.getMinutes().toString().padStart(2, '0');
    var seconds = date.getSeconds().toString().padStart(2, '0');
    return hours + ':' + minutes + ':' + seconds;
}

// "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>Startzeit</th><th style="text-align: center; padding: 0 10px;">Verweildauer <br>(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');
}

Anpassungen obigen Codes: URL auch mit Fragment wie '#visit' (bspw. https://wegerl.at/visit-duration/#visit) tracken und diverse Verbesserungen durch end_visit_handler(). – Vorhergehende Datei:

<?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: 4.12.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', true); // Asynchroner Fallback
        xhr.setRequestHeader('Content-Type', 'application/json');
        xhr.onload = function() {
            if (xhr.status >= 200 && xhr.status < 300) {
                console.log('Fallback-Daten erfolgreich gesendet');
            } else {
                console.error('Fehler beim Fallback-Senden:', xhr.status);
            }
        };
        xhr.onerror = function() {
            console.error('Netzwerkfehler beim Fallback');
        };
        xhr.send(JSON.stringify({
            unique_id: window.uniqueId,
            end_time: Date.now()
        }));
    }
});

let debounceTimer = null; // Debounce-Timer

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', true); // Asynchroner Fallback
            xhr.setRequestHeader('Content-Type', 'application/json');
            xhr.onload = function() {
                if (xhr.status >= 200 && xhr.status < 300) {
                    console.log('Daten erfolgreich gesendet');
                } else {
                    console.error('Fehler beim Fallback-Senden:', xhr.status);
                }
            };
            xhr.onerror = function() {
                console.error('Netzwerkfehler beim Fallback');
            };
            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',
        '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>Startzeit</th><th style="text-align: center; padding: 0 10px;">Verweildauer <br>(Min:Sek)</th><th>Status</th></tr>';
    echo '</thead>';
    
    echo '<tbody>';

		foreach ($visits as $visit_data) {
    // Setze den Status basierend auf der Verweildauer
    if (isset($visit_data['duration'])) {
        // Wenn die Verweildauer bereits gesetzt ist, ist die Sitzung beendet
        $status = 'Beendet';
    } else {
        // Wenn keine Dauer gesetzt ist, prüfen wir, ob das Timeout überschritten wurde
        $current_time = time();
        $timeout_limit = 30 * 60; // Timeout-Limit: 30 Minuten

        // Wenn die Sitzung das Timeout überschritten hat
        if (isset($visit_data['start_time']) && ($current_time - $visit_data['start_time']) > $timeout_limit) {
            $status = 'Offen'; // Timeout erreicht, als "Offen" markieren
        } else {
            $status = 'Aktiv'; // Noch aktiv, also "Aktiv" markieren
        }
    }
	
	if (isset($visit_data['start_time'])) {
    // Zeit eine Stunde vorverlegen (3600 Sekunden)
    $start_time = date('H:i:s', $visit_data['start_time'] + 3600); 
} else {
    $start_time = 'Unbekannt';
}

    // 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
    if ($status === 'Aktiv') {
        $row_color = 'rgba(255, 235, 59, 0.6)'; // Gelb, opac für Aktiv
    } elseif ($status === 'Offen') {
        $row_color = 'rgba(255, 235, 59, 0.2)'; // Gelb, sehr opac für Offen
    } else {
        $row_color = '#ECF3F9'; // Hellblau für beendet
    }
		  
	// Zeile in der Tabelle ausgeben
	echo "<tr style='background-color: $row_color;'>
        	<td>{$visit_data['page_title']}</td>
        	<td>{$start_time}</td>
        	<td style='text-align: center;'>{$formatted_duration}</td>
        	<td>{$status}</td>
      </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) {
                    let rowColor;

                    // Farben basierend auf dem Status setzen
                    if (visit.status === "Aktiv") {
                        rowColor = "#ffeb3b"; // Gelb für aktiv
                    } else if (visit.status === "Timeout") {
                        rowColor = 'rgba(255, 235, 59, 0.2)'; // Gelb sehr opac für timeout
                    } else {
                        rowColor = "#ECF3F9"; // Hellblau für beendet
                    }

                    var formattedDuration = visit.formatted_duration || 'Noch aktiv';

                    // Falls visit.start_time ein Unix-Timestamp ist, umwandeln
                    var formattedStartTime = visit.start_time 
                        ? formatTime(visit.start_time) 
                        : 'Unbekannt'; // Umwandlung der Startzeit in lesbares Format

                    tableBody.append(
                        '<tr style="background-color: ' + rowColor + '">' +
                        '<td>' + visit.page_title + '</td>' +
                        '<td>' + formattedStartTime + '</td>' + // Startzeit-Spalte hinzugefügt
                        '<td style="text-align: center;">' + formattedDuration + '</td>' +
                        '<td>' + visit.status + '</td>' +
                        '</tr>'
                    );
                });
            } else {
                alert('Fehler bei der Aktualisierung der Verweildauer.');
            }
        },
        error: function() {
            alert('Fehler beim Aktualisieren der Verweildauer.');
        }
    });
});

// Hilfsfunktion, um Unix-Timestamp in hh:mm:ss umzuwandeln
function formatTime(timestamp) {
    var date = new Date(timestamp * 1000); // Umwandlung von Sekunden-Timestamp zu Millisekunden
    var hours = date.getHours().toString().padStart(2, '0');
    var minutes = date.getMinutes().toString().padStart(2, '0');
    var seconds = date.getSeconds().toString().padStart(2, '0');
    return hours + ':' + minutes + ':' + seconds;
}

// "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>Startzeit</th><th style="text-align: center; padding: 0 10px;">Verweildauer <br>(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');
}

Entwicklung:

Verbesserung von Beenden des Trackings und Dashboards, Neuladung bei Sitzungen mit Timeout Status "Offen" statt "Aktiv".

<?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: 3.12.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', true); // Asynchroner Fallback
        xhr.setRequestHeader('Content-Type', 'application/json');
        xhr.onload = function() {
            if (xhr.status >= 200 && xhr.status < 300) {
                console.log('Fallback-Daten erfolgreich gesendet');
            } else {
                console.error('Fehler beim Fallback-Senden:', xhr.status);
            }
        };
        xhr.onerror = function() {
            console.error('Netzwerkfehler beim Fallback');
        };
        xhr.send(JSON.stringify({
            unique_id: window.uniqueId,
            end_time: Date.now()
        }));
    }
});

let debounceTimer = null; // Debounce-Timer

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', true); // Asynchroner Fallback
            xhr.setRequestHeader('Content-Type', 'application/json');
            xhr.onload = function() {
                if (xhr.status >= 200 && xhr.status < 300) {
                    console.log('Daten erfolgreich gesendet');
                } else {
                    console.error('Fehler beim Fallback-Senden:', xhr.status);
                }
            };
            xhr.onerror = function() {
                console.error('Netzwerkfehler beim Fallback');
            };
            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 der Verweildauer
    if (isset($visit_data['duration'])) {
        // Wenn die Verweildauer bereits gesetzt ist, ist die Sitzung beendet
        $status = 'Beendet';
    } else {
        // Wenn keine Dauer gesetzt ist, prüfen wir, ob das Timeout überschritten wurde
        $current_time = time();
        $timeout_limit = 30 * 60; // Timeout-Limit: 30 Minuten

        // Wenn die Sitzung das Timeout überschritten hat
        if (isset($visit_data['start_time']) && ($current_time - $visit_data['start_time']) > $timeout_limit) {
            $status = 'Offen'; // Timeout erreicht, als "Offen" markieren
        } else {
            $status = 'Aktiv'; // Noch aktiv, also "Aktiv" markieren
        }
    }

    // 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
    if ($status === 'Aktiv') {
        $row_color = 'rgba(255, 235, 59, 0.6)'; // Gelb opac für aktiv
    } elseif ($status === 'Offen') {
        $row_color = 'rgba(255, 235, 59, 0.2)'; // Gelb sehr opac für aktiv
    } else {
        $row_color = '#ECF3F9'; // Hellblau für beendet
    }

    // Zeile in der Tabelle ausgeben
    echo "<tr style='background-color: $row_color;'>
            <td>{$visit_data['page_title']}</td>
            <td>{$formatted_duration}</td>
            <td>{$status}</td>
          </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) {
                    let rowColor;

                    // Farben basierend auf dem Status setzen
                    if (visit.status === "Aktiv") {
                        rowColor = "#ffeb3b"; // Gelb für aktiv
                    } else if (visit.status === "Timeout") {
                        rowColor = "#fff"; // Weiß für Timeout
                    } else {
                        rowColor = "#ECF3F9"; // Hellblau für beendet
                    }

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

Entwicklung:

Verbesserung von Beenden des Trackings und Dashboards Neuladung bei Sitzungen mit Timeout Status "Offen" statt "Aktiv".

<?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');
}

Entwicklung:

<?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
function update_all_visit_durations() {
    $visits = get_option('current_visits', []);
    
    $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'])) {
            // Berechne die Verweildauer für die aktiven Besuche
            $duration = time() - $visit_data['start_time']; // Verwende aktuelle Zeit
            $visit_data['duration'] = $duration;
            $visit_data['status'] = 'Aktiv'; // Status bleibt 'Aktiv', wenn noch keine Dauer
        } else {
            // Wenn die Verweildauer bereits festgelegt ist, wird der Status als 'Beendet' angezeigt
            $visit_data['status'] = 'Beendet';
        }

        // Berechne Minuten und Sekunden für jede Verweildauer
        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 (aktualisierte) Besuchsdaten hinzu
    }

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

    // Speichern der neuen Verweildauern
    update_option('current_visits', $visits); 

    // Sende die aktualisierten Daten mit der formatierten Dauer und dem Status 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');
}

Entwicklung:

<?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: 27.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 bei einem Besucher um einen Bot handelt. 
Diese Funktion nutzt 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-functions.php' ) ) {
    require_once plugin_dir_path( __FILE__ ) . 'bot-functions.php';
} else {
    // Fehlerbehandlung, falls die Datei nicht gefunden wurde
    error_log('bot-functions.php wurde nicht gefunden.');
}

// 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>
		
// 2) Sehr vereinfachtes Scenario: Tracking startet sofort nach Seitenaufruf (ohne 5 Sekunden-Verzögerung und ohne Scroll-Aktivität)
/*(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);

// Funktion zum Starten des Besuchs
function startVisitTracking() {
    console.log('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 
        })
    })
    .then(response => {
        if (!response.ok) {
            throw new Error('Server-Antwort nicht erfolgreich.');
        }
        return response.json();
    })
    .then(data => {
        console.log('Tracking erfolgreich:', data);
    })
    .catch(err => console.error('Fehler beim Start des Besuchs:', err));
}

// Start des Besuchs sofort nach Seitenaufruf
startVisitTracking();
*/		
// 1) Einfaches Scenario: Tracking mit 5 Sekunden Verzögerung nach Scroll-Aktivität
		(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 5 Sekunden...');
            setTimeout(function() {
                console.log('5 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));
            }, 5000);
        }
    }
	
	// Tracking bei Scroll-Aktivität starten
    document.addEventListener('scroll', function() {
        startVisitTracking();
    });	

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

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

            const beaconSuccess = navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', payload);
            if (!beaconSuccess) {
                const xhr = new XMLHttpRequest();
                xhr.open('POST', window.ajaxUrl + '?action=update_visit_duration', false);
                xhr.setRequestHeader('Content-Type', 'application/json');
                xhr.send(payload);
            }
        }
    }
})();
    </script>";
}

// Bot-Erkennungsfunktion laden, falls noch nicht geladen
if (!function_exists('is_bot_or_spider')) {
    if (file_exists(plugin_dir_path(__FILE__) . 'bot-functions.php')) {
        require_once plugin_dir_path(__FILE__) . 'bot-functions.php';
    } else {
        error_log('bot-functions.php wurde nicht gefunden.');
    }
}

add_action('wp_footer', 'start_visit_tracking');

// 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'])) {
        // Verarbeite die Daten, z. B. speichere den letzten Heartbeat-Zeitstempel
        // Integriere die Logik in dein bestehendes Tracking
        error_log("Heartbeat erhalten: " . print_r($data, true));
    }

    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
function update_all_visit_durations() {
    $visits = get_option('current_visits', []);
    
    $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'])) {
            // Berechne die Verweildauer für die aktiven Besuche
            $duration = time() - $visit_data['start_time']; // Verwende aktuelle Zeit
            $visit_data['duration'] = $duration;
            $visit_data['status'] = 'Aktiv'; // Status bleibt 'Aktiv', wenn noch keine Dauer
        } else {
            // Wenn die Verweildauer bereits festgelegt ist, wird der Status als 'Beendet' angezeigt
            $visit_data['status'] = 'Beendet';
        }

        // Berechne Minuten und Sekunden für jede Verweildauer
        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 (aktualisierte) Besuchsdaten hinzu
    }

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

    // Speichern der neuen Verweildauern
    update_option('current_visits', $visits); 

    // Sende die aktualisierten Daten mit der formatierten Dauer und dem Status 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 sehr funktionell
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');
}

Entwicklung:

<?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: 26.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 bei einem Besucher um einen Bot handelt. 
Diese Funktion nutzt 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-functions.php' ) ) {
    require_once plugin_dir_path( __FILE__ ) . 'bot-functions.php';
} else {
    // Fehlerbehandlung, falls die Datei nicht gefunden wurde
    error_log('bot-functions.php wurde nicht gefunden.');
}

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

/*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
    }

    if (session_status() == PHP_SESSION_NONE) {
        session_start();
    }

    if (!isset($_SESSION['visit_start_time'])) {
        $_SESSION['visit_start_time'] = time();
    }*/

    // Besuchsdaten abrufen und AJAX-Skript laden
    echo "<script>
		// Vereinfachtes Scenario: Kein 5-Minuten-Inaktivitätstimer und Tracking nur bei Scroll-Aktivität
		(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 10 Sekunden...');
            setTimeout(function() {
                console.log('10 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));
            }, 10000);
        }
    }
	
	// Tracking bei Scroll-Aktivität starten
    document.addEventListener('scroll', function() {
        startVisitTracking();
    });

    /* Tracking starten, sobald das DOM geladen ist
    document.addEventListener('DOMContentLoaded', function() {
        console.log('DOM vollständig geladen. Tracking wird in Kürze gestartet.');
        startVisitTracking();
    });*/

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

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

            const beaconSuccess = navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', payload);
            if (!beaconSuccess) {
                const xhr = new XMLHttpRequest();
                xhr.open('POST', window.ajaxUrl + '?action=update_visit_duration', false);
                xhr.setRequestHeader('Content-Type', 'application/json');
                xhr.send(payload);
            }
        }
    }
	
	// Funktion zum Senden des Heartbeats
    function sendHeartbeat() {
        if (window.uniqueId && window.ajaxUrl) {
            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:', err));
        }
    }

    // Heartbeat alle 5 Sekunden senden
    setInterval(sendHeartbeat, 5000);

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

// Bot-Erkennungsfunktion laden, falls noch nicht geladen
if (!function_exists('is_bot_or_spider')) {
    if (file_exists(plugin_dir_path(__FILE__) . 'bot-functions.php')) {
        require_once plugin_dir_path(__FILE__) . 'bot-functions.php';
    } else {
        error_log('bot-functions.php wurde nicht gefunden.');
    }
}

add_action('wp_footer', 'start_visit_tracking');

// 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'])) {
        // Verarbeite die Daten, z. B. speichere den letzten Heartbeat-Zeitstempel
        // Integriere die Logik in dein bestehendes Tracking
        error_log("Heartbeat erhalten: " . print_r($data, true));
    }

    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
function update_all_visit_durations() {
    $visits = get_option('current_visits', []);
    
    $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'])) {
            // Berechne die Verweildauer für die aktiven Besuche
            $duration = time() - $visit_data['start_time']; // Verwende aktuelle Zeit
            $visit_data['duration'] = $duration;
            $visit_data['status'] = 'Aktiv'; // Status bleibt 'Aktiv', wenn noch keine Dauer
        } else {
            // Wenn die Verweildauer bereits festgelegt ist, wird der Status als 'Beendet' angezeigt
            $visit_data['status'] = 'Beendet';
        }

        // Berechne Minuten und Sekunden für jede Verweildauer
        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 (aktualisierte) Besuchsdaten hinzu
    }

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

    // Speichern der neuen Verweildauern
    update_option('current_visits', $visits); 

    // Sende die aktualisierten Daten mit der formatierten Dauer und dem Status 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 sehr funktionell
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');
}

Entwicklung:

<?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: 23.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 bei einem Besucher um einen Bot handelt. 
Diese Funktion nutzt 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-functions.php' ) ) {
    require_once plugin_dir_path( __FILE__ ) . 'bot-functions.php';
} else {
    // Fehlerbehandlung, falls die Datei nicht gefunden wurde
    error_log('bot-functions.php wurde nicht gefunden.');
}

// 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()) {
        return; // Frühzeitig abbrechen
    }

    if (session_status() == PHP_SESSION_NONE) {
        session_start();
    }

    if (!isset($_SESSION['visit_start_time'])) {
        $_SESSION['visit_start_time'] = time();
    }

    // Besuchsdaten abrufen und AJAX-Skript laden
    echo "<script>
		// Scenario 2) Tracking-Start bei Scroll-Aktivität und 5-Minuten-Inaktivitätstimer sofort starten
(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 isTabActive = true;
    var isUserInteracted = false;
    let inactivityTimeout;
    const inactivityDelay = 5 * 60 * 1000; // 5 Minuten Inaktivität

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

    // Funktion zum Zurücksetzen des Inaktivitätstimers
    function resetInactivityTimer() {
        clearTimeout(inactivityTimeout);
        inactivityTimeout = setTimeout(endSessionDueToInactivity, inactivityDelay);
        console.log('Inaktivitätstimer zurückgesetzt.');
    }

    // Funktion zum Beenden des Besuchs wegen Inaktivität
    function endSessionDueToInactivity() {
        if (window.uniqueId && window.ajaxUrl) {
            const payload = JSON.stringify({
                unique_id: window.uniqueId,
                end_time: Date.now()
            });

            const beaconSuccess = navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', payload);
            if (!beaconSuccess) {
                const xhr = new XMLHttpRequest();
                xhr.open('POST', window.ajaxUrl + '?action=update_visit_duration', false);
                xhr.setRequestHeader('Content-Type', 'application/json');
                xhr.send(payload);
            }

            console.log('Besuch beendet wegen Inaktivität.');
        }
    }

    // Externe Links abfangen
    document.addEventListener('click', function(event) {
        const target = event.target.closest('a');
        if (target && target.target === '_blank') {
            console.log('Externer Link in neuem Tab geöffnet. Sitzung bleibt aktiv.');
            isTabActive = true;
        }
    });

    // Inaktivitätstimer direkt nach Seitenaufruf starten
    resetInactivityTimer();

    // Auf Scroll-Aktivität setzen, um Tracking zu starten
    document.addEventListener('scroll', function() {
        startVisitTracking();
    });

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

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

            const beaconSuccess = navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', payload);
            if (!beaconSuccess) {
                const xhr = new XMLHttpRequest();
                xhr.open('POST', window.ajaxUrl + '?action=update_visit_duration', false);
                xhr.setRequestHeader('Content-Type', 'application/json');
                xhr.send(payload);
            }
        }
    }
})();
    </script>";
}

// Bot-Erkennungsfunktion laden, falls noch nicht geladen
if (!function_exists('is_bot_or_spider')) {
    if (file_exists(plugin_dir_path(__FILE__) . 'bot-functions.php')) {
        require_once plugin_dir_path(__FILE__) . 'bot-functions.php';
    } else {
        error_log('bot-functions.php wurde nicht gefunden.');
    }
}

add_action('wp_footer', 'start_visit_tracking');

// 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.");
    }
}

// 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
function update_all_visit_durations() {
    $visits = get_option('current_visits', []);
    
    $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'])) {
            // Berechne die Verweildauer für die aktiven Besuche
            $duration = time() - $visit_data['start_time']; // Verwende aktuelle Zeit
            $visit_data['duration'] = $duration;
            $visit_data['status'] = 'Aktiv'; // Status bleibt 'Aktiv', wenn noch keine Dauer
        } else {
            // Wenn die Verweildauer bereits festgelegt ist, wird der Status als 'Beendet' angezeigt
            $visit_data['status'] = 'Beendet';
        }

        // Berechne Minuten und Sekunden für jede Verweildauer
        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 (aktualisierte) Besuchsdaten hinzu
    }

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

    // Speichern der neuen Verweildauern
    update_option('current_visits', $visits); 

    // Sende die aktualisierten Daten mit der formatierten Dauer und dem Status 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 sehr funktionell
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');
}

Entwicklung:

<?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: 21.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 bei einem Besucher um einen Bot handelt. 
Diese Funktion nutzt 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-functions.php' ) ) {
    require_once plugin_dir_path( __FILE__ ) . 'bot-functions.php';
} else {
    // Fehlerbehandlung, falls die Datei nicht gefunden wurde
    error_log('bot-functions.php wurde nicht gefunden.');
}

// 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()) {
        return; // Frühzeitig abbrechen
    }

    if (session_status() == PHP_SESSION_NONE) {
        session_start();
    }

    if (!isset($_SESSION['visit_start_time'])) {
        $_SESSION['visit_start_time'] = time();
    }

    // Besuchsdaten abrufen und AJAX-Skript laden
    echo "<script>
		(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 isTabActive = true;
    var isUserInteracted = false;
    let inactivityTimeout;
    let visibilityTimeout;

    // 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));

                // Timeout-Überprüfung alle 30 Sekunden
                setInterval(function() {
                    if (isTabActive) {
                        fetch(window.ajaxUrl + '?action=check_visit_timeout', {
                            method: 'POST',
                            headers: { 'Content-Type': 'application/json' }
                        }).then(response => response.json()).then(data => {
                            if (data.timeout_reached) {
                                console.log('Besuch beendet wegen Timeout.');
                            }
                        }).catch(err => console.error('Fehler bei Timeout-Überprüfung:', err));
                    }
                }, 30000);

                resetInactivityTimer();
            }, 3000);
        }
    }

    function resetInactivityTimer() {
        clearTimeout(inactivityTimeout);
        inactivityTimeout = setTimeout(endSessionDueToInactivity, 5 * 60 * 1000);
    }

    function endSessionDueToInactivity() {
        if (window.uniqueId && window.ajaxUrl) {
            navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
                unique_id: window.uniqueId,
                end_time: Date.now()
            }));
            console.log('Besuch beendet wegen 5 Minuten Inaktivität.');
        }
    }

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

            const beaconSuccess = navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', payload);
            if (!beaconSuccess) {
                const xhr = new XMLHttpRequest();
                xhr.open('POST', window.ajaxUrl + '?action=update_visit_duration', false);
                xhr.setRequestHeader('Content-Type', 'application/json');
                xhr.send(payload);
            }
        }
    }

    // Sichtbarkeitsprüfung
    document.addEventListener('visibilitychange', function() {
        if (document.visibilityState === 'hidden') {
            console.log('Tab unsichtbar, aber Sitzung bleibt aktiv.');
            visibilityTimeout = setTimeout(sendEndVisit, 15000); // 15 Sekunden
        } else {
            console.log('Tab sichtbar.');
            clearTimeout(visibilityTimeout);
        }
    });

    // Externe Links abfangen
    document.addEventListener('click', function(event) {
        const target = event.target.closest('a');
        if (target && target.target === '_blank') {
            console.log('Externer Link in neuem Tab geöffnet. Sitzung bleibt aktiv.');
            isTabActive = true;
        }
    });

    // Benutzerinteraktionen
    document.addEventListener('mousemove', function() {
        startVisitTracking();
        resetInactivityTimer();
    }, { once: true });

    document.addEventListener('scroll', function() {
        startVisitTracking();
        resetInactivityTimer();
    }, { once: true });

    document.addEventListener('keydown', function() {
        startVisitTracking();
        resetInactivityTimer();
    }, { once: true });

    window.addEventListener('beforeunload', sendEndVisit);
})();
    </script>";
}

// Bot-Erkennungsfunktion laden, falls noch nicht geladen
if (!function_exists('is_bot_or_spider')) {
    if (file_exists(plugin_dir_path(__FILE__) . 'bot-functions.php')) {
        require_once plugin_dir_path(__FILE__) . 'bot-functions.php';
    } else {
        error_log('bot-functions.php wurde nicht gefunden.');
    }
}

add_action('wp_footer', 'start_visit_tracking');

// 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.");
    }
}

// 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
function update_all_visit_durations() {
    $visits = get_option('current_visits', []);
    
    $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'])) {
            // Berechne die Verweildauer für die aktiven Besuche
            $duration = time() - $visit_data['start_time']; // Verwende aktuelle Zeit
            $visit_data['duration'] = $duration;
            $visit_data['status'] = 'Aktiv'; // Status bleibt 'Aktiv', wenn noch keine Dauer
        } else {
            // Wenn die Verweildauer bereits festgelegt ist, wird der Status als 'Beendet' angezeigt
            $visit_data['status'] = 'Beendet';
        }

        // Berechne Minuten und Sekunden für jede Verweildauer
        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 (aktualisierte) Besuchsdaten hinzu
    }

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

    // Speichern der neuen Verweildauern
    update_option('current_visits', $visits); 

    // Sende die aktualisierten Daten mit der formatierten Dauer und dem Status 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 sehr funktionell
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');

Entwicklung:

<?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: 20.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 bei einem Besucher um einen Bot handelt. 
Diese Funktion nutzt 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-functions.php' ) ) {
    require_once plugin_dir_path( __FILE__ ) . 'bot-functions.php';
} else {
    // Fehlerbehandlung, falls die Datei nicht gefunden wurde
    error_log('bot-functions.php wurde nicht gefunden.');
}

// 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()) {
        return; // Frühzeitig abbrechen, wenn der angemeldete Benutzer ein Admin ist, sich um einen Bot oder Testtool handelt
    }

    if (session_status() == PHP_SESSION_NONE) {
        session_start();
    }

    if (!isset($_SESSION['visit_start_time'])) {
        $_SESSION['visit_start_time'] = time();
    }

    // Besuchsdaten abrufen und AJAX-Skript laden
    echo "<script>
    (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 isTabActive = true;

        // Besuch nur nach 3 Sekunden tracken
        setTimeout(function() {
            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);

        // Timeout-Überprüfung alle 30 Sekunden
        setInterval(function() {
            if (isTabActive) {
                fetch(window.ajaxUrl + '?action=check_visit_timeout', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'}
                }).then(response => response.json()).then(data => {
                    if (data.timeout_reached) {
                        console.log('Besuch beendet wegen Timeout.');
                    }
                }).catch(err => console.error('Fehler bei Timeout-Überprüfung:', err));
            }
        }, 30000);

        // Besuch beenden: Sichtbarkeit oder Schließen
        function sendEndVisit() {
            if (window.uniqueId && window.ajaxUrl) {
                const payload = JSON.stringify({
                    unique_id: window.uniqueId,
                    end_time: Date.now()
                });

                const beaconSuccess = navigator.sendBeacon(window.ajaxUrl + '?action=check_visit_timeout', payload);

                if (!beaconSuccess) {
                    const xhr = new XMLHttpRequest();
                    xhr.open('POST', window.ajaxUrl + '?action=check_visit_timeout', false); // Synchrone Anfrage
                    xhr.setRequestHeader('Content-Type', 'application/json');
                    xhr.send(payload);
                }
            }
        }

        // Sichtbarkeitswechsel überwachen
        document.addEventListener('visibilitychange', function() {
            if (document.visibilityState === 'hidden') {
                isTabActive = false;
                sendEndVisit();
            } else {
                isTabActive = true;
            }
        });

        // Besuch beim Schließen des Tabs oder Browsers beenden
        window.addEventListener('beforeunload', sendEndVisit);
    })();

// Safari spezifisch	
	document.addEventListener('DOMContentLoaded', function() {
    // Safari-Erkennung
    const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);

    var visitStartTime = localStorage.getItem('visit_start_time') || Date.now();
    var uniqueId = window.uniqueId || 'default_user';
    var ajaxUrl = window.ajaxUrl || '/wp-admin/admin-ajax.php';

    localStorage.setItem('visit_start_time', visitStartTime);

    if (isSafari) {
        console.log('Safari-Browser erkannt: Spezifische Anpassung aktiv.');

        function sendDataForSafari(startTime, endTime) {
            var data = {
                unique_id: uniqueId,
                start_time: startTime,
                end_time: endTime,
            };

            var xhr = new XMLHttpRequest();
            xhr.open('POST', ajaxUrl + '?action=update_visit_duration', true);
            xhr.setRequestHeader('Content-Type', 'application/json');
            xhr.send(JSON.stringify(data));

            xhr.onload = function() {
                if (xhr.status === 200) {
                    console.log('Safari-Spezifisch:', xhr.responseText);
                } else {
                    console.error('Safari Fehler:', xhr.statusText);
                }
            };
        }

        document.addEventListener('visibilitychange', function() {
            if (document.visibilityState === 'hidden') {
                sendDataForSafari(visitStartTime, Date.now());
            }
        });

        window.addEventListener('beforeunload', function() {
            sendDataForSafari(visitStartTime, Date.now());
        });

        setInterval(function() {
            sendDataForSafari(visitStartTime, Date.now());
        }, 30000);
    }
});	
    </script>";
}

// Bot-Erkennungsfunktion laden, falls noch nicht geladen
if (!function_exists('is_bot_or_spider')) {
    if (file_exists(plugin_dir_path(__FILE__) . 'bot-functions.php')) {
        require_once plugin_dir_path(__FILE__) . 'bot-functions.php';
    } else {
        error_log('bot-functions.php wurde nicht gefunden.');
    }
}

add_action('wp_footer', 'start_visit_tracking');

// 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.");
    }
}

// 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
function update_all_visit_durations() {
    $visits = get_option('current_visits', []);
    
    $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'])) {
            // Berechne die Verweildauer für die aktiven Besuche
            $duration = time() - $visit_data['start_time']; // Verwende aktuelle Zeit
            $visit_data['duration'] = $duration;
            $visit_data['status'] = 'Aktiv'; // Status bleibt 'Aktiv', wenn noch keine Dauer
        } else {
            // Wenn die Verweildauer bereits festgelegt ist, wird der Status als 'Beendet' angezeigt
            $visit_data['status'] = 'Beendet';
        }

        // Berechne Minuten und Sekunden für jede Verweildauer
        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 (aktualisierte) Besuchsdaten hinzu
    }

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

    // Speichern der neuen Verweildauern
    update_option('current_visits', $visits); 

    // Sende die aktualisierten Daten mit der formatierten Dauer und dem Status 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 sehr funktionell
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');

// JavaScript zur Timeout-Überprüfung und Beendigung beim Seitenverlassen oder bei Inaktivität, nur für nicht-Admins
// Option 1
// test Vorteil: Sitzungen bleiben nach automatischen Beenden bis zum Seitenverlassen im Hintergrund offen. 
// test Nachteil: Überlange Sitzungszeiten möglich, die nicht real sind.
add_action('wp_footer', function() {
    if ( !current_user_can('administrator') ) {  // Überprüft, ob der aktuelle Benutzer kein Administrator ist
        echo "<script>
            // Funktionsblock zur Überwachung der Inaktivität
            let inactivityTime = function() {
                let timeout;

                function resetTimer() {
                    clearTimeout(timeout);
                    timeout = setTimeout(endSessionDueToInactivity, 5 * 60 * 1000); // 5 Minuten Inaktivität
                }

                function endSessionDueToInactivity() {
                    if (window.uniqueId && window.ajaxUrl) {
                        navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
                            unique_id: window.uniqueId,
                            end_time: Date.now()
                        }));
                        console.log('Besuch beendet wegen Inaktivität.');
                    }
                }

                // Events für Maus- und Tastaturaktivität zur Reaktivierung des Timers
                document.onload = resetTimer;
                document.onmousemove = resetTimer;
                document.onkeypress = resetTimer;
                document.onscroll = resetTimer;
            };

            inactivityTime();

            // Besuch beim Schließen des Tabs oder Browsers beenden
            window.addEventListener('beforeunload', function() {
                if (window.uniqueId && window.ajaxUrl) {
                    navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
                        unique_id: window.uniqueId,
                        end_time: Date.now()
                    }));
                    console.log('Daten erfolgreich gesendet: Tab oder Browser geschlossen.');
                }
            });
        </script>";
    }
});

// JavaScript zur Timeout-Überprüfung und Beendigung beim Seitenverlassen oder bei Inaktivität, nur für nicht-Admins.
// Option 2:
// test Vorteil: Sitzungen werden endgültig nach Timeout abgeschlossen, keine überlangen Zeiten. 
// test Nachteil: Nachträgliche Aktionen können nicht mehr hinzugefügt werden.
/*add_action('wp_footer', function() {
    if ( !current_user_can('administrator') ) {  // Überprüft, ob der aktuelle Benutzer kein Administrator ist
        echo "<script>
            let timeout;
            let sessionEnded = false; // Flag, um den Status der Sitzung zu verfolgen

            function resetTimer() {
                clearTimeout(timeout);
                timeout = setTimeout(endSessionDueToInactivity, 5 * 60 * 1000); // 5 Minuten Inaktivität
            }

            function endSessionDueToInactivity() {
                if (!sessionEnded) { // Sitzung nur einmal als beendet markieren
                    sessionEnded = true;
                    console.log('Besuch beendet wegen Inaktivität.');
                    sendSessionUpdate();
                }
            }

            function sendSessionUpdate() {
                if (window.uniqueId && window.ajaxUrl) {
                    navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
                        unique_id: window.uniqueId,
                        end_time: Date.now()
                    }));
                }
            }

            function handlePageNavigation() {
                // Sitzung vor dem Verlassen der Seite sauber beenden
                if (!sessionEnded) {
                    console.log('Seitenwechsel erkannt, Sitzung wird beendet.');
                    sendSessionUpdate();
                    sessionEnded = true; // Markiere Sitzung als beendet
                }
            }

            // Benutzeraktivität überwachen
            document.onmousemove = resetTimer;
            document.onkeypress = resetTimer;
            document.onscroll = resetTimer;

            // Besuch beim Schließen des Tabs oder bei internem Navigieren beenden
            window.addEventListener('beforeunload', handlePageNavigation);
            window.addEventListener('unload', handlePageNavigation);

            // Timer starten, sobald die Seite geladen wird
            resetTimer();
        </script>";
    }
});*/

Entwicklung:

<?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: (2/19.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 bei einem Besucher um einen Bot handelt. 
Diese Funktion nutzt 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-functions.php' ) ) {
    require_once plugin_dir_path( __FILE__ ) . 'bot-functions.php';
} else {
    // Fehlerbehandlung, falls die Datei nicht gefunden wurde
    error_log('bot-functions.php wurde nicht gefunden.');
}

// 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');

    $unique_id = isset($_POST['unique_id']) ? sanitize_text_field($_POST['unique_id']) : '';
    $page_title = isset($_POST['page_title']) ? sanitize_text_field($_POST['page_title']) : '';

    if ($unique_id && $page_title) {
        global $wpdb;
        $table_name = $wpdb->prefix . 'visit_tracking';
        $wpdb->insert($table_name, array(
            'unique_id'   => $unique_id,
            '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()) {
        return; // Frühzeitig abbrechen, wenn der angemeldete Benutzer ein Admin ist, sich um einen Bot oder Testtool handelt
    }

    if (session_status() == PHP_SESSION_NONE) {
        session_start();
    }

    if (!isset($_SESSION['visit_start_time'])) {
        $_SESSION['visit_start_time'] = time();
    }

    // Besuchsdaten abrufen und AJAX-Skript laden
    echo "<script>
    (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 isTabActive = true;

        // Besuch nur nach 3 Sekunden tracken
        setTimeout(function() {
            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);

        // Timeout-Überprüfung alle 30 Sekunden
        setInterval(function() {
            if (isTabActive) {
                fetch(window.ajaxUrl + '?action=check_visit_timeout', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'}
                }).then(response => response.json()).then(data => {
                    if (data.timeout_reached) {
                        console.log('Besuch beendet wegen Timeout.');
                    }
                }).catch(err => console.error('Fehler bei Timeout-Überprüfung:', err));
            }
        }, 30000);

        // Besuch beenden: Sichtbarkeit oder Schließen
        function sendEndVisit() {
            if (window.uniqueId && window.ajaxUrl) {
                const payload = JSON.stringify({
                    unique_id: window.uniqueId,
                    end_time: Date.now()
                });

                const beaconSuccess = navigator.sendBeacon(window.ajaxUrl + '?action=check_visit_timeout', payload);

                if (!beaconSuccess) {
                    const xhr = new XMLHttpRequest();
                    xhr.open('POST', window.ajaxUrl + '?action=check_visit_timeout', false); // Synchrone Anfrage
                    xhr.setRequestHeader('Content-Type', 'application/json');
                    xhr.send(payload);
                }
            }
        }

        // Sichtbarkeitswechsel überwachen
        document.addEventListener('visibilitychange', function() {
            if (document.visibilityState === 'hidden') {
                isTabActive = false;
                sendEndVisit();
            } else {
                isTabActive = true;
            }
        });

        // Besuch beim Schließen des Tabs oder Browsers beenden
        window.addEventListener('beforeunload', sendEndVisit);
    })();

// Safari spezifisch	
	document.addEventListener('DOMContentLoaded', function() {
    // Safari-Erkennung
    const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);

    var visitStartTime = localStorage.getItem('visit_start_time') || Date.now();
    var uniqueId = window.uniqueId || 'default_user';
    var ajaxUrl = window.ajaxUrl || '/wp-admin/admin-ajax.php';

    localStorage.setItem('visit_start_time', visitStartTime);

    if (isSafari) {
        console.log('Safari-Browser erkannt: Spezifische Anpassung aktiv.');

        function sendDataForSafari(startTime, endTime) {
            var data = {
                unique_id: uniqueId,
                start_time: startTime,
                end_time: endTime,
            };

            var xhr = new XMLHttpRequest();
            xhr.open('POST', ajaxUrl + '?action=update_visit_duration', true);
            xhr.setRequestHeader('Content-Type', 'application/json');
            xhr.send(JSON.stringify(data));

            xhr.onload = function() {
                if (xhr.status === 200) {
                    console.log('Safari-Spezifisch:', xhr.responseText);
                } else {
                    console.error('Safari Fehler:', xhr.statusText);
                }
            };
        }

        document.addEventListener('visibilitychange', function() {
            if (document.visibilityState === 'hidden') {
                sendDataForSafari(visitStartTime, Date.now());
            }
        });

        window.addEventListener('beforeunload', function() {
            sendDataForSafari(visitStartTime, Date.now());
        });

        setInterval(function() {
            sendDataForSafari(visitStartTime, Date.now());
        }, 30000);
    }
});	
    </script>";
}

// Bot-Erkennungsfunktion laden, falls noch nicht geladen
if (!function_exists('is_bot_or_spider')) {
    if (file_exists(plugin_dir_path(__FILE__) . 'bot-functions.php')) {
        require_once plugin_dir_path(__FILE__) . 'bot-functions.php';
    } else {
        error_log('bot-functions.php wurde nicht gefunden.');
    }
}

add_action('wp_footer', 'start_visit_tracking');

// 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.");
    }
}

// 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
function update_all_visit_durations() {
    $visits = get_option('current_visits', []);
    
    $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'])) {
            // Berechne die Verweildauer für die aktiven Besuche
            $duration = time() - $visit_data['start_time']; // Verwende aktuelle Zeit
            $visit_data['duration'] = $duration;
            $visit_data['status'] = 'Aktiv'; // Status bleibt 'Aktiv', wenn noch keine Dauer
        } else {
            // Wenn die Verweildauer bereits festgelegt ist, wird der Status als 'Beendet' angezeigt
            $visit_data['status'] = 'Beendet';
        }

        // Berechne Minuten und Sekunden für jede Verweildauer
        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 (aktualisierte) Besuchsdaten hinzu
    }

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

    // Speichern der neuen Verweildauern
    update_option('current_visits', $visits); 

    // Sende die aktualisierten Daten mit der formatierten Dauer und dem Status 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 sehr funktionell
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');

// JavaScript zur Timeout-Überprüfung und Beendigung beim Seitenverlassen oder bei Inaktivität, nur für nicht-Admins
// Option 1
// test Vorteil: Sitzungen bleiben nach automatischen Beenden bis zum Seitenverlassen im Hintergrund offen. 
// test Nachteil: Überlange Sitzungszeiten möglich, die nicht real sind.
add_action('wp_footer', function() {
    if ( !current_user_can('administrator') ) {  // Überprüft, ob der aktuelle Benutzer kein Administrator ist
        echo "<script>
            // Funktionsblock zur Überwachung der Inaktivität
            let inactivityTime = function() {
                let timeout;

                function resetTimer() {
                    clearTimeout(timeout);
                    timeout = setTimeout(endSessionDueToInactivity, 5 * 60 * 1000); // 5 Minuten Inaktivität
                }

                function endSessionDueToInactivity() {
                    if (window.uniqueId && window.ajaxUrl) {
                        navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
                            unique_id: window.uniqueId,
                            end_time: Date.now()
                        }));
                        console.log('Besuch beendet wegen Inaktivität.');
                    }
                }

                // Events für Maus- und Tastaturaktivität zur Reaktivierung des Timers
                document.onload = resetTimer;
                document.onmousemove = resetTimer;
                document.onkeypress = resetTimer;
                document.onscroll = resetTimer;
            };

            inactivityTime();

            // Besuch beim Schließen des Tabs oder Browsers beenden
            window.addEventListener('beforeunload', function() {
                if (window.uniqueId && window.ajaxUrl) {
                    navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
                        unique_id: window.uniqueId,
                        end_time: Date.now()
                    }));
                    console.log('Daten erfolgreich gesendet: Tab oder Browser geschlossen.');
                }
            });
        </script>";
    }
});

// JavaScript zur Timeout-Überprüfung und Beendigung beim Seitenverlassen oder bei Inaktivität, nur für nicht-Admins.
// Option 2:
// test Vorteil: Sitzungen werden endgültig nach Timeout abgeschlossen, keine überlangen Zeiten. 
// test Nachteil: Nachträgliche Aktionen können nicht mehr hinzugefügt werden.
/*add_action('wp_footer', function() {
    if ( !current_user_can('administrator') ) {  // Überprüft, ob der aktuelle Benutzer kein Administrator ist
        echo "<script>
            let timeout;
            let sessionEnded = false; // Flag, um den Status der Sitzung zu verfolgen

            function resetTimer() {
                clearTimeout(timeout);
                timeout = setTimeout(endSessionDueToInactivity, 5 * 60 * 1000); // 5 Minuten Inaktivität
            }

            function endSessionDueToInactivity() {
                if (!sessionEnded) { // Sitzung nur einmal als beendet markieren
                    sessionEnded = true;
                    console.log('Besuch beendet wegen Inaktivität.');
                    sendSessionUpdate();
                }
            }

            function sendSessionUpdate() {
                if (window.uniqueId && window.ajaxUrl) {
                    navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
                        unique_id: window.uniqueId,
                        end_time: Date.now()
                    }));
                }
            }

            function handlePageNavigation() {
                // Sitzung vor dem Verlassen der Seite sauber beenden
                if (!sessionEnded) {
                    console.log('Seitenwechsel erkannt, Sitzung wird beendet.');
                    sendSessionUpdate();
                    sessionEnded = true; // Markiere Sitzung als beendet
                }
            }

            // Benutzeraktivität überwachen
            document.onmousemove = resetTimer;
            document.onkeypress = resetTimer;
            document.onscroll = resetTimer;

            // Besuch beim Schließen des Tabs oder bei internem Navigieren beenden
            window.addEventListener('beforeunload', handlePageNavigation);
            window.addEventListener('unload', handlePageNavigation);

            // Timer starten, sobald die Seite geladen wird
            resetTimer();
        </script>";
    }
});*/

Entwicklung:

<?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/19.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 bei einem Besucher um einen Bot handelt. 
Diese Funktion nutzt 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-functions.php' ) ) {
    require_once plugin_dir_path( __FILE__ ) . 'bot-functions.php';
} else {
    // Fehlerbehandlung, falls die Datei nicht gefunden wurde
    error_log('bot-functions.php wurde nicht gefunden.');
}

// 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');

    $unique_id = isset($_POST['unique_id']) ? sanitize_text_field($_POST['unique_id']) : '';
    $page_title = isset($_POST['page_title']) ? sanitize_text_field($_POST['page_title']) : '';

    if ($unique_id && $page_title) {
        global $wpdb;
        $table_name = $wpdb->prefix . 'visit_tracking';
        $wpdb->insert($table_name, array(
            'unique_id'   => $unique_id,
            '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() ) {
        return; // Frühzeitig abbrechen, wenn der angemeldete Benutzer ein Admin ist oder es sich um einen Bot handelt
    }

    if (session_status() == PHP_SESSION_NONE) {
        session_start();
    }

    if (!isset($_SESSION['visit_start_time'])) {
        $_SESSION['visit_start_time'] = time();
    }

    // Besuchsdaten abrufen und AJAX-Skript laden
    echo "<script>
    (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 isTabActive = true;

        // Besuch nur nach 3 Sekunden tracken
        setTimeout(function() {
            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);

        // Timeout-Überprüfung alle 30 Sekunden
        setInterval(function() {
            if (isTabActive) {
                fetch(window.ajaxUrl + '?action=check_visit_timeout', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'}
                }).then(response => response.json()).then(data => {
                    if (data.timeout_reached) {
                        console.log('Besuch beendet wegen Timeout.');
                    }
                }).catch(err => console.error('Fehler bei Timeout-Überprüfung:', err));
            }
        }, 30000);

        // Besuch beenden: Sichtbarkeit oder Schließen
        function sendEndVisit() {
            if (window.uniqueId && window.ajaxUrl) {
                const payload = JSON.stringify({
                    unique_id: window.uniqueId,
                    end_time: Date.now()
                });

                const beaconSuccess = navigator.sendBeacon(window.ajaxUrl + '?action=check_visit_timeout', payload);

                if (!beaconSuccess) {
                    const xhr = new XMLHttpRequest();
                    xhr.open('POST', window.ajaxUrl + '?action=check_visit_timeout', false); // Synchrone Anfrage
                    xhr.setRequestHeader('Content-Type', 'application/json');
                    xhr.send(payload);
                }
            }
        }

        // Sichtbarkeitswechsel überwachen
        document.addEventListener('visibilitychange', function() {
            if (document.visibilityState === 'hidden') {
                isTabActive = false;
                sendEndVisit();
            } else {
                isTabActive = true;
            }
        });

        // Besuch beim Schließen des Tabs oder Browsers beenden
        window.addEventListener('beforeunload', sendEndVisit);
    })();

// Safari spezifisch	
	document.addEventListener('DOMContentLoaded', function() {
    // Safari-Erkennung
    const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);

    var visitStartTime = localStorage.getItem('visit_start_time') || Date.now();
    var uniqueId = window.uniqueId || 'default_user';
    var ajaxUrl = window.ajaxUrl || '/wp-admin/admin-ajax.php';

    localStorage.setItem('visit_start_time', visitStartTime);

    if (isSafari) {
        console.log('Safari-Browser erkannt: Spezifische Anpassung aktiv.');

        function sendDataForSafari(startTime, endTime) {
            var data = {
                unique_id: uniqueId,
                start_time: startTime,
                end_time: endTime,
            };

            var xhr = new XMLHttpRequest();
            xhr.open('POST', ajaxUrl + '?action=update_visit_duration', true);
            xhr.setRequestHeader('Content-Type', 'application/json');
            xhr.send(JSON.stringify(data));

            xhr.onload = function() {
                if (xhr.status === 200) {
                    console.log('Safari-Spezifisch:', xhr.responseText);
                } else {
                    console.error('Safari Fehler:', xhr.statusText);
                }
            };
        }

        document.addEventListener('visibilitychange', function() {
            if (document.visibilityState === 'hidden') {
                sendDataForSafari(visitStartTime, Date.now());
            }
        });

        window.addEventListener('beforeunload', function() {
            sendDataForSafari(visitStartTime, Date.now());
        });

        setInterval(function() {
            sendDataForSafari(visitStartTime, Date.now());
        }, 30000);
    }
});	
    </script>";
}

// Bot-Erkennungsfunktion laden, falls noch nicht geladen
if (!function_exists('is_bot_or_spider')) {
    if (file_exists(plugin_dir_path(__FILE__) . 'bot-functions.php')) {
        require_once plugin_dir_path(__FILE__) . 'bot-functions.php';
    } else {
        error_log('bot-functions.php wurde nicht gefunden.');
    }
}

add_action('wp_footer', 'start_visit_tracking');

// 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.");
    }
}

// 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 (Sek:Min)</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
function update_all_visit_durations() {
    $visits = get_option('current_visits', []);
    
    $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'])) {
            // Berechne die Verweildauer für die aktiven Besuche
            $duration = time() - $visit_data['start_time']; // Verwende aktuelle Zeit
            $visit_data['duration'] = $duration;
            $visit_data['status'] = 'Aktiv'; // Status bleibt 'Aktiv', wenn noch keine Dauer
        } else {
            // Wenn die Verweildauer bereits festgelegt ist, wird der Status als 'Beendet' angezeigt
            $visit_data['status'] = 'Beendet';
        }

        // Berechne Minuten und Sekunden für jede Verweildauer
        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 (aktualisierte) Besuchsdaten hinzu
    }

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

    // Speichern der neuen Verweildauern
    update_option('current_visits', $visits); 

    // Sende die aktualisierten Daten mit der formatierten Dauer und dem Status 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 sehr funktionell
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');

// JavaScript zur Timeout-Überprüfung und Beendigung beim Seitenverlassen oder bei Inaktivität, nur für nicht-Admins
// Option 1
// test Vorteil: Sitzungen bleiben nach automatischen Beenden bis zum Seitenverlassen im Hintergrund offen. 
// test Nachteil: Überlange Sitzungszeiten möglich, die nicht real sind.
add_action('wp_footer', function() {
    if ( !current_user_can('administrator') ) {  // Überprüft, ob der aktuelle Benutzer kein Administrator ist
        echo "<script>
            // Funktionsblock zur Überwachung der Inaktivität
            let inactivityTime = function() {
                let timeout;

                function resetTimer() {
                    clearTimeout(timeout);
                    timeout = setTimeout(endSessionDueToInactivity, 5 * 60 * 1000); // 5 Minuten Inaktivität
                }

                function endSessionDueToInactivity() {
                    if (window.uniqueId && window.ajaxUrl) {
                        navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
                            unique_id: window.uniqueId,
                            end_time: Date.now()
                        }));
                        console.log('Besuch beendet wegen Inaktivität.');
                    }
                }

                // Events für Maus- und Tastaturaktivität zur Reaktivierung des Timers
                document.onload = resetTimer;
                document.onmousemove = resetTimer;
                document.onkeypress = resetTimer;
                document.onscroll = resetTimer;
            };

            inactivityTime();

            // Besuch beim Schließen des Tabs oder Browsers beenden
            window.addEventListener('beforeunload', function() {
                if (window.uniqueId && window.ajaxUrl) {
                    navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
                        unique_id: window.uniqueId,
                        end_time: Date.now()
                    }));
                    console.log('Daten erfolgreich gesendet: Tab oder Browser geschlossen.');
                }
            });
        </script>";
    }
});

// JavaScript zur Timeout-Überprüfung und Beendigung beim Seitenverlassen oder bei Inaktivität, nur für nicht-Admins.
// Option 2:
// test Vorteil: Sitzungen werden endgültig nach Timeout abgeschlossen, keine überlangen Zeiten. 
// test Nachteil: Nachträgliche Aktionen können nicht mehr hinzugefügt werden.
/*add_action('wp_footer', function() {
    if ( !current_user_can('administrator') ) {  // Überprüft, ob der aktuelle Benutzer kein Administrator ist
        echo "<script>
            let timeout;
            let sessionEnded = false; // Flag, um den Status der Sitzung zu verfolgen

            function resetTimer() {
                clearTimeout(timeout);
                timeout = setTimeout(endSessionDueToInactivity, 5 * 60 * 1000); // 5 Minuten Inaktivität
            }

            function endSessionDueToInactivity() {
                if (!sessionEnded) { // Sitzung nur einmal als beendet markieren
                    sessionEnded = true;
                    console.log('Besuch beendet wegen Inaktivität.');
                    sendSessionUpdate();
                }
            }

            function sendSessionUpdate() {
                if (window.uniqueId && window.ajaxUrl) {
                    navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
                        unique_id: window.uniqueId,
                        end_time: Date.now()
                    }));
                }
            }

            function handlePageNavigation() {
                // Sitzung vor dem Verlassen der Seite sauber beenden
                if (!sessionEnded) {
                    console.log('Seitenwechsel erkannt, Sitzung wird beendet.');
                    sendSessionUpdate();
                    sessionEnded = true; // Markiere Sitzung als beendet
                }
            }

            // Benutzeraktivität überwachen
            document.onmousemove = resetTimer;
            document.onkeypress = resetTimer;
            document.onscroll = resetTimer;

            // Besuch beim Schließen des Tabs oder bei internem Navigieren beenden
            window.addEventListener('beforeunload', handlePageNavigation);
            window.addEventListener('unload', handlePageNavigation);

            // Timer starten, sobald die Seite geladen wird
            resetTimer();
        </script>";
    }
});*/

Entwicklung:

<?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: (18.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 bei einem Besucher um einen Bot handelt. 
Diese Funktion nutzt 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-functions.php' ) ) {
    require_once plugin_dir_path( __FILE__ ) . 'bot-functions.php';
} else {
    // Fehlerbehandlung, falls die Datei nicht gefunden wurde
    error_log('bot-functions.php wurde nicht gefunden.');
}

// 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');

    $unique_id = isset($_POST['unique_id']) ? sanitize_text_field($_POST['unique_id']) : '';
    $page_title = isset($_POST['page_title']) ? sanitize_text_field($_POST['page_title']) : '';

    if ($unique_id && $page_title) {
        global $wpdb;
        $table_name = $wpdb->prefix . 'visit_tracking';
        $wpdb->insert($table_name, array(
            'unique_id'   => $unique_id,
            '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() ) {
        return; // Frühzeitig abbrechen, wenn der angemeldete Benutzer ein Admin ist oder es sich um einen Bot handelt
    }

    if (session_status() == PHP_SESSION_NONE) {
        session_start();
    }

    if (!isset($_SESSION['visit_start_time'])) {
        $_SESSION['visit_start_time'] = time();
    }

    // Besuchsdaten abrufen und AJAX-Skript laden
    echo "<script>
        window.ajaxUrl = '" . admin_url('admin-ajax.php') . "';
        window.addEventListener('load', function() {
            window.uniqueId = 'id-' + Math.random().toString(36).substr(2, 16);
            
            // Besuch nur nach 3 Sekunden tracken
            setTimeout(function() {
                fetch(window.ajaxUrl + '?action=start_visit', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({ unique_id: window.uniqueId, page_title: document.title })
                });
            }, 3000);

            // Timeout-Überprüfung alle 30 Sekunden
            setInterval(function() {
                fetch(window.ajaxUrl + '?action=check_visit_timeout', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'}
                }).then(response => response.json()).then(data => {
                    if (data.timeout_reached) {
                        console.log('Besuch beendet wegen Timeout.');
                    }
                });
            }, 30000);

            // Besuch beenden beim Schließen des Tabs oder Browsers
            window.addEventListener('beforeunload', function() {
                if (window.uniqueId && window.ajaxUrl) {
                    navigator.sendBeacon(window.ajaxUrl + '?action=check_visit_timeout', JSON.stringify({
                        unique_id: window.uniqueId,
                        end_time: Date.now()
                    }));
                }
            });
        });
	
// Safari spezifisch	
	document.addEventListener('DOMContentLoaded', function() {
    // Safari-Erkennung
    const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);

    var visitStartTime = localStorage.getItem('visit_start_time') || Date.now();
    var uniqueId = window.uniqueId || 'default_user';
    var ajaxUrl = window.ajaxUrl || '/wp-admin/admin-ajax.php';

    localStorage.setItem('visit_start_time', visitStartTime);

    if (isSafari) {
        console.log('Safari-Browser erkannt: Spezifische Anpassung aktiv.');

        function sendDataForSafari(startTime, endTime) {
            var data = {
                unique_id: uniqueId,
                start_time: startTime,
                end_time: endTime,
            };

            var xhr = new XMLHttpRequest();
            xhr.open('POST', ajaxUrl + '?action=update_visit_duration', true);
            xhr.setRequestHeader('Content-Type', 'application/json');
            xhr.send(JSON.stringify(data));

            xhr.onload = function() {
                if (xhr.status === 200) {
                    console.log('Safari-Spezifisch:', xhr.responseText);
                } else {
                    console.error('Safari Fehler:', xhr.statusText);
                }
            };
        }

        document.addEventListener('visibilitychange', function() {
            if (document.visibilityState === 'hidden') {
                sendDataForSafari(visitStartTime, Date.now());
            }
        });

        window.addEventListener('beforeunload', function() {
            sendDataForSafari(visitStartTime, Date.now());
        });

        setInterval(function() {
            sendDataForSafari(visitStartTime, Date.now());
        }, 30000);
    }
});					
    </script>";
}

// Bot-Erkennungsfunktion laden, falls noch nicht geladen
if (!function_exists('is_bot_or_spider')) {
    if (file_exists(plugin_dir_path(__FILE__) . 'bot-functions.php')) {
        require_once plugin_dir_path(__FILE__) . 'bot-functions.php';
    } else {
        error_log('bot-functions.php wurde nicht gefunden.');
    }
}

add_action('wp_footer', 'start_visit_tracking');

// 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.");
    }
}

// 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 (Sek:Min)</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
function update_all_visit_durations() {
    $visits = get_option('current_visits', []);
    
    $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'])) {
            // Berechne die Verweildauer für die aktiven Besuche
            $duration = time() - $visit_data['start_time']; // Verwende aktuelle Zeit
            $visit_data['duration'] = $duration;
            $visit_data['status'] = 'Aktiv'; // Status bleibt 'Aktiv', wenn noch keine Dauer
        } else {
            // Wenn die Verweildauer bereits festgelegt ist, wird der Status als 'Beendet' angezeigt
            $visit_data['status'] = 'Beendet';
        }

        // Berechne Minuten und Sekunden für jede Verweildauer
        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 (aktualisierte) Besuchsdaten hinzu
    }

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

    // Speichern der neuen Verweildauern
    update_option('current_visits', $visits); 

    // Sende die aktualisierten Daten mit der formatierten Dauer und dem Status 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 sehr funktionell
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');

// JavaScript zur Timeout-Überprüfung und Beendigung beim Seitenverlassen oder bei Inaktivität, nur für nicht-Admins
// Option 1
// test Vorteil: Sitzungen bleiben nach automatischen Beenden bis zum Seitenverlassen im Hintergrund offen. 
// test Nachteil: Überlange Sitzungszeiten möglich, die nicht real sind.
add_action('wp_footer', function() {
    if ( !current_user_can('administrator') ) {  // Überprüft, ob der aktuelle Benutzer kein Administrator ist
        echo "<script>
            // Funktionsblock zur Überwachung der Inaktivität
            let inactivityTime = function() {
                let timeout;

                function resetTimer() {
                    clearTimeout(timeout);
                    timeout = setTimeout(endSessionDueToInactivity, 5 * 60 * 1000); // 5 Minuten Inaktivität
                }

                function endSessionDueToInactivity() {
                    if (window.uniqueId && window.ajaxUrl) {
                        navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
                            unique_id: window.uniqueId,
                            end_time: Date.now()
                        }));
                        console.log('Besuch beendet wegen Inaktivität.');
                    }
                }

                // Events für Maus- und Tastaturaktivität zur Reaktivierung des Timers
                document.onload = resetTimer;
                document.onmousemove = resetTimer;
                document.onkeypress = resetTimer;
                document.onscroll = resetTimer;
            };

            inactivityTime();

            // Besuch beim Schließen des Tabs oder Browsers beenden
            window.addEventListener('beforeunload', function() {
                if (window.uniqueId && window.ajaxUrl) {
                    navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
                        unique_id: window.uniqueId,
                        end_time: Date.now()
                    }));
                    console.log('Daten erfolgreich gesendet: Tab oder Browser geschlossen.');
                }
            });
        </script>";
    }
});

// JavaScript zur Timeout-Überprüfung und Beendigung beim Seitenverlassen oder bei Inaktivität, nur für nicht-Admins.
// Option 2:
// test Vorteil: Sitzungen werden endgültig nach Timeout abgeschlossen, keine überlangen Zeiten. 
// test Nachteil: Nachträgliche Aktionen können nicht mehr hinzugefügt werden.
/*add_action('wp_footer', function() {
    if ( !current_user_can('administrator') ) {  // Überprüft, ob der aktuelle Benutzer kein Administrator ist
        echo "<script>
            let timeout;
            let sessionEnded = false; // Flag, um den Status der Sitzung zu verfolgen

            function resetTimer() {
                clearTimeout(timeout);
                timeout = setTimeout(endSessionDueToInactivity, 5 * 60 * 1000); // 5 Minuten Inaktivität
            }

            function endSessionDueToInactivity() {
                if (!sessionEnded) { // Sitzung nur einmal als beendet markieren
                    sessionEnded = true;
                    console.log('Besuch beendet wegen Inaktivität.');
                    sendSessionUpdate();
                }
            }

            function sendSessionUpdate() {
                if (window.uniqueId && window.ajaxUrl) {
                    navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
                        unique_id: window.uniqueId,
                        end_time: Date.now()
                    }));
                }
            }

            function handlePageNavigation() {
                // Sitzung vor dem Verlassen der Seite sauber beenden
                if (!sessionEnded) {
                    console.log('Seitenwechsel erkannt, Sitzung wird beendet.');
                    sendSessionUpdate();
                    sessionEnded = true; // Markiere Sitzung als beendet
                }
            }

            // Benutzeraktivität überwachen
            document.onmousemove = resetTimer;
            document.onkeypress = resetTimer;
            document.onscroll = resetTimer;

            // Besuch beim Schließen des Tabs oder bei internem Navigieren beenden
            window.addEventListener('beforeunload', handlePageNavigation);
            window.addEventListener('unload', handlePageNavigation);

            // Timer starten, sobald die Seite geladen wird
            resetTimer();
        </script>";
    }
});*/

Entwicklung:

<?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 vom 15.11.24)
* Author: Team WP Wegerl
* Author URI: https://wegerl.at
* Text Domain: visit-duration

Die Funktion 'is_bot_or_spider' prüft anhand des User-Agents, ob es sich bei einem Besucher um einen Bot handelt. 
Diese Funktion nutzt 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-functions.php' ) ) {
    require_once plugin_dir_path( __FILE__ ) . 'bot-functions.php';
} else {
    // Fehlerbehandlung, falls die Datei nicht gefunden wurde
    error_log('bot-functions.php wurde nicht gefunden.');
}

// Beispiel, wie die Funktion verwendet wird:
if ( is_bot_or_spider() ) {
    // Bot erkannt, keine Zählung vornehmen
}

// 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() ) {
        return; // Frühzeitig abbrechen, wenn der angemeldete Benutzer ein Admin ist oder es sich um einen Bot handelt
    }

    if (session_status() == PHP_SESSION_NONE) {
        session_start();
    }

    if (!isset($_SESSION['visit_start_time'])) {
        $_SESSION['visit_start_time'] = time();
    }

    // Besuchsdaten abrufen und AJAX-Skript laden
    echo "<script>
        window.ajaxUrl = '" . admin_url('admin-ajax.php') . "';
        window.addEventListener('load', function() {
            window.uniqueId = 'id-' + Math.random().toString(36).substr(2, 16);
            fetch(window.ajaxUrl + '?action=start_visit', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({ unique_id: window.uniqueId, page_title: document.title })
            });

            // Timeout-Überprüfung alle 30 Sekunden
            setInterval(function() {
                fetch(window.ajaxUrl + '?action=check_visit_timeout', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'}
                }).then(response => response.json()).then(data => {
                    if (data.timeout_reached) {
                        console.log('Besuch beendet wegen Timeout.');
                    }
                });
            }, 30000);

            // Besuch beenden beim Schließen des Tabs oder Browsers
            window.addEventListener('beforeunload', function() {
                if (window.uniqueId && window.ajaxUrl) {
                    navigator.sendBeacon(window.ajaxUrl + '?action=check_visit_timeout', JSON.stringify({
                        unique_id: window.uniqueId,
                        end_time: Date.now()
                    }));
                }
            });
        });
    </script>";
}

// Bot-Erkennungsfunktion laden, falls noch nicht geladen
if (!function_exists('is_bot_or_spider')) {
 if (file_exists(plugin_dir_path(__FILE__) . 'bot-functions.php')) {
     require_once plugin_dir_path(__FILE__) . 'bot-functions.php';
 } else {
     error_log('bot-functions.php wurde nicht gefunden.');
 }
}

add_action('wp_footer', 'start_visit_tracking');

// 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.");
    }
}

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

// Funktion zur Erkennung von Bots und Spidern
if (is_bot_or_spider()) {
    // Bot erkannt
}

// 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 (Sek:Min)</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
function update_all_visit_durations() {
    $visits = get_option('current_visits', []);
    
    $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'])) {
            // Berechne die Verweildauer für die aktiven Besuche
            $duration = time() - $visit_data['start_time']; // Verwende aktuelle Zeit
            $visit_data['duration'] = $duration;
            $visit_data['status'] = 'Aktiv'; // Status bleibt 'Aktiv', wenn noch keine Dauer
        } else {
            // Wenn die Verweildauer bereits festgelegt ist, wird der Status als 'Beendet' angezeigt
            $visit_data['status'] = 'Beendet';
        }

        // Berechne Minuten und Sekunden für jede Verweildauer
        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 (aktualisierte) Besuchsdaten hinzu
    }

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

    // Speichern der neuen Verweildauern
    update_option('current_visits', $visits); 

    // Sende die aktualisierten Daten mit der formatierten Dauer und dem Status 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 sehr funktionell
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');

// JavaScript zur Timeout-Überprüfung und Beendigung beim Seitenverlassen oder bei Inaktivität, nur für nicht-Admins
add_action('wp_footer', function() {
    if ( !current_user_can('administrator') ) {  // Überprüft, ob der aktuelle Benutzer kein Administrator ist
        echo "<script>
            // Funktionsblock zur Überwachung der Inaktivität
            let inactivityTime = function() {
                let timeout;

                function resetTimer() {
                    clearTimeout(timeout);
                    timeout = setTimeout(endSessionDueToInactivity, 10 * 60 * 1000); // 10 Minuten Inaktivität
                }

                function endSessionDueToInactivity() {
                    if (window.uniqueId && window.ajaxUrl) {
                        navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
                            unique_id: window.uniqueId,
                            end_time: Date.now()
                        }));
                        console.log('Besuch beendet wegen Inaktivität.');
                    }
                }

                // Events für Maus- und Tastaturaktivität zur Reaktivierung des Timers
                document.onload = resetTimer;
                document.onmousemove = resetTimer;
                document.onkeypress = resetTimer;
                document.onscroll = resetTimer;
            };

            inactivityTime();

            // Besuch beim Schließen des Tabs oder Browsers beenden
            window.addEventListener('beforeunload', function() {
                if (window.uniqueId && window.ajaxUrl) {
                    navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
                        unique_id: window.uniqueId,
                        end_time: Date.now()
                    }));
                    console.log('Daten erfolgreich gesendet: Tab oder Browser geschlossen.');
                }
            });
        </script>";
    }
});

Entwicklung.

<?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.
* Entwicklungs-Datum: 15.11.24 / 1
* Author: Team WP Wegerl
* Author URI: https://wegerl.at
* Text Domain: visit-duration

Die Funktion 'is_bot_or_spider' prüft anhand des User-Agents, ob es sich bei einem Besucher um einen Bot handelt. 
Diese Funktion nutzt 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-functions.php' ) ) {
    require_once plugin_dir_path( __FILE__ ) . 'bot-functions.php';
} else {
    // Fehlerbehandlung, falls die Datei nicht gefunden wurde
    error_log('bot-functions.php wurde nicht gefunden.');
}

// Beispiel, wie die Funktion verwendet wird:
if ( is_bot_or_spider() ) {
    // Bot erkannt, keine Zählung vornehmen
}

// 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() ) {
        return; // Frühzeitig abbrechen, wenn der angemeldete Benutzer ein Admin ist oder es sich um einen Bot handelt
    }

    if (session_status() == PHP_SESSION_NONE) {
        session_start();
    }

    if (!isset($_SESSION['visit_start_time'])) {
        $_SESSION['visit_start_time'] = time();
    }

    // Besuchsdaten abrufen und AJAX-Skript laden
    echo "<script>
        window.ajaxUrl = '" . admin_url('admin-ajax.php') . "';
        window.addEventListener('load', function() {
            window.uniqueId = 'id-' + Math.random().toString(36).substr(2, 16);
            fetch(window.ajaxUrl + '?action=start_visit', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({ unique_id: window.uniqueId, page_title: document.title })
            });

            // Timeout-Überprüfung alle 30 Sekunden
            setInterval(function() {
                fetch(window.ajaxUrl + '?action=check_visit_timeout', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'}
                }).then(response => response.json()).then(data => {
                    if (data.timeout_reached) {
                        console.log('Besuch beendet wegen Timeout.');
                    }
                });
            }, 30000);

            // Besuch beenden beim Schließen des Tabs oder Browsers
            window.addEventListener('beforeunload', function() {
                if (window.uniqueId && window.ajaxUrl) {
                    navigator.sendBeacon(window.ajaxUrl + '?action=check_visit_timeout', JSON.stringify({
                        unique_id: window.uniqueId,
                        end_time: Date.now()
                    }));
                }
            });
        });
    </script>";
}

// Bot-Erkennungsfunktion laden, falls noch nicht geladen
if (!function_exists('is_bot_or_spider')) {
 if (file_exists(plugin_dir_path(__FILE__) . 'bot-functions.php')) {
     require_once plugin_dir_path(__FILE__) . 'bot-functions.php';
 } else {
     error_log('bot-functions.php wurde nicht gefunden.');
 }
}

add_action('wp_footer', 'start_visit_tracking');

// 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.");
    }
}

// Ajax-Handler für Timeout-Überprüfung
function check_visit_timeout() {
    if (!isset($_SESSION['visit_start_time'])) {
        wp_send_json(['timeout_reached' => false]);
        return;
    }

    $max_duration = 90 * 60; // 90 Minuten
    $current_duration = time() - $_SESSION['visit_start_time'];
    
    // Fehlerprotokollierung hinzufügen
    error_log("Current visit duration: " . $current_duration . " seconds.");

    if ($current_duration > $max_duration) {
        end_visit_tracking(); // Besuch beenden bei Timeout
        wp_send_json(['timeout_reached' => true]);
    } else {
        wp_send_json(['timeout_reached' => false]);
    }
}

add_action('wp_ajax_check_visit_timeout', 'check_visit_timeout');
add_action('wp_ajax_nopriv_check_visit_timeout', 'check_visit_timeout');

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

// Funktion zur Erkennung von Bots und Spidern
if (is_bot_or_spider()) {
    // Bot erkannt
}

// 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 (Sek:Min)</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
function update_all_visit_durations() {
    $visits = get_option('current_visits', []);
    
    $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'])) {
            // Berechne die Verweildauer für die aktiven Besuche
            $duration = time() - $visit_data['start_time']; // Verwende aktuelle Zeit
            $visit_data['duration'] = $duration;
            $visit_data['status'] = 'Aktiv'; // Status bleibt 'Aktiv', wenn noch keine Dauer
        } else {
            // Wenn die Verweildauer bereits festgelegt ist, wird der Status als 'Beendet' angezeigt
            $visit_data['status'] = 'Beendet';
        }

        // Berechne Minuten und Sekunden für jede Verweildauer
        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 (aktualisierte) Besuchsdaten hinzu
    }

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

    // Speichern der neuen Verweildauern
    update_option('current_visits', $visits); 

    // Sende die aktualisierten Daten mit der formatierten Dauer und dem Status 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 sehr funktionell
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');

// JavaScript zur Timeout-Überprüfung und Beendigung beim Seitenverlassen oder bei Inaktivität, nur für nicht-Admins
add_action('wp_footer', function() {
    if ( !current_user_can('administrator') ) {  // Überprüft, ob der aktuelle Benutzer kein Administrator ist
        echo "<script>
            // Funktionsblock zur Überwachung der Inaktivität
            let inactivityTime = function() {
                let timeout;

                function resetTimer() {
                    clearTimeout(timeout);
                    timeout = setTimeout(endSessionDueToInactivity, 10 * 60 * 1000); // 5 Minuten Inaktivität
                }

                function endSessionDueToInactivity() {
                    if (window.uniqueId && window.ajaxUrl) {
                        navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
                            unique_id: window.uniqueId,
                            end_time: Date.now()
                        }));
                        console.log('Besuch beendet wegen Inaktivität.');
                    }
                }

                // Events für Maus- und Tastaturaktivität zur Reaktivierung des Timers
                document.onload = resetTimer;
                document.onmousemove = resetTimer;
                document.onkeypress = resetTimer;
                document.onscroll = resetTimer;
            };

            inactivityTime();

            // Intervallüberprüfung alle 30 Sekunden
            setInterval(function() {
                fetch(window.ajaxUrl + '?action=check_visit_timeout', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'}
                }).then(response => response.json()).then(data => {
                    if (data.timeout_reached) {
                        console.log('Besuch beendet wegen Timeout.');
                        // alert('Der Besuch wurde aufgrund des Timeouts beendet.');
                    }
                });
            }, 30000); // Intervall von 30 Sekunden

            // Besuch beim Schließen des Tabs oder Browsers beenden
            window.addEventListener('beforeunload', function() {
                if (window.uniqueId && window.ajaxUrl) {
                    navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
                        unique_id: window.uniqueId,
                        end_time: Date.now()
                    }));
                    console.log('Daten erfolgreich gesendet: Tab oder Browser geschlossen.');
                }
            });
        </script>";
    }
});

Entwicklung;

<?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.
* Entwicklungs-Datum: 13.11.24
* Author: Team WP Wegerl
* Author URI: https://wegerl.at
* Text Domain: visit-duration

Die Funktion 'is_bot_or_spider' prüft anhand des User-Agents, ob es sich bei einem Besucher um einen Bot handelt. 
Diese Funktion nutzt 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-functions.php' ) ) {
    require_once plugin_dir_path( __FILE__ ) . 'bot-functions.php';
} else {
    // Fehlerbehandlung, falls die Datei nicht gefunden wurde
    error_log('bot-functions.php wurde nicht gefunden.');
}

// Beispiel, wie die Funktion verwendet wird:
if ( is_bot_or_spider() ) {
    // Bot erkannt, keine Zählung vornehmen
}

// 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() ) {
        return; // Frühzeitig abbrechen, wenn der angemeldete Benutzer ein Admin ist oder es sich um einen Bot handelt
    }

    if (session_status() == PHP_SESSION_NONE) {
        session_start();
    }

    if (!isset($_SESSION['visit_start_time'])) {
        $_SESSION['visit_start_time'] = time();
    }

    // Besuchsdaten abrufen und AJAX-Skript laden
    echo "<script>
        window.ajaxUrl = '" . admin_url('admin-ajax.php') . "';
        window.addEventListener('load', function() {
            window.uniqueId = 'id-' + Math.random().toString(36).substr(2, 16);
            fetch(window.ajaxUrl + '?action=start_visit', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({ unique_id: window.uniqueId, page_title: document.title })
            });

            // Timeout-Überprüfung alle 30 Sekunden
            setInterval(function() {
                fetch(window.ajaxUrl + '?action=check_visit_timeout', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'}
                }).then(response => response.json()).then(data => {
                    if (data.timeout_reached) {
                        console.log('Besuch beendet wegen Timeout.');
                    }
                });
            }, 30000);

            // Besuch beenden beim Schließen des Tabs oder Browsers
            window.addEventListener('beforeunload', function() {
                if (window.uniqueId && window.ajaxUrl) {
                    navigator.sendBeacon(window.ajaxUrl + '?action=check_visit_timeout', JSON.stringify({
                        unique_id: window.uniqueId,
                        end_time: Date.now()
                    }));
                }
            });
        });
    </script>";
}

// Bot-Erkennungsfunktion laden, falls noch nicht geladen
if (!function_exists('is_bot_or_spider')) {
 if (file_exists(plugin_dir_path(__FILE__) . 'bot-functions.php')) {
     require_once plugin_dir_path(__FILE__) . 'bot-functions.php';
 } else {
     error_log('bot-functions.php wurde nicht gefunden.');
 }
}

add_action('wp_footer', 'start_visit_tracking');

// 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.");
    }
}

// Ajax-Handler für Timeout-Überprüfung
function check_visit_timeout() {
    if (!isset($_SESSION['visit_start_time'])) {
        wp_send_json(['timeout_reached' => false]);
        return;
    }

    $max_duration = 90 * 60; // 90 Minuten
    $current_duration = time() - $_SESSION['visit_start_time'];
    
    // Fehlerprotokollierung hinzufügen
    error_log("Current visit duration: " . $current_duration . " seconds.");

    if ($current_duration > $max_duration) {
        end_visit_tracking(); // Besuch beenden bei Timeout
        wp_send_json(['timeout_reached' => true]);
    } else {
        wp_send_json(['timeout_reached' => false]);
    }
}

add_action('wp_ajax_check_visit_timeout', 'check_visit_timeout');
add_action('wp_ajax_nopriv_check_visit_timeout', 'check_visit_timeout');

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

// Funktion zur Erkennung von Bots und Spidern
if (is_bot_or_spider()) {
    // Bot erkannt
}

// 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 (Sekunden)</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
function update_all_visit_durations() {
    $visits = get_option('current_visits', []);
    
    $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'])) {
            // Berechne die Verweildauer für die aktiven Besuche
            $duration = time() - $visit_data['start_time']; // Verwende aktuelle Zeit
            $visit_data['duration'] = $duration;
            $visit_data['status'] = 'Aktiv'; // Status bleibt 'Aktiv', wenn noch keine Dauer
        } else {
            // Wenn die Verweildauer bereits festgelegt ist, wird der Status als 'Beendet' angezeigt
            $visit_data['status'] = 'Beendet';
        }

        // Berechne Minuten und Sekunden für jede Verweildauer
        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 (aktualisierte) Besuchsdaten hinzu
    }

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

    // Speichern der neuen Verweildauern
    update_option('current_visits', $visits); 

    // Sende die aktualisierten Daten mit der formatierten Dauer und dem Status 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 sehr funktionell
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');

// JavaScript zur Timeout-Überprüfung und Beendigung beim Seitenverlassen, nur für nicht-Admins
add_action('wp_footer', function() {
    if ( !current_user_can('administrator') ) {  // Überprüft, ob der aktuelle Benutzer kein Administrator ist
        echo "<script>
            // Timeout-Überprüfung alle 30 Sekunden
            setInterval(function() {
                fetch(window.ajaxUrl + '?action=check_visit_timeout', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'}
                }).then(response => response.json()).then(data => {
                    if (data.timeout_reached) {
                        console.log('Besuch beendet wegen Timeout.');
                        // alert('Der Besuch wurde aufgrund des Timeouts beendet.');
                    }
                });
            }, 30000); // Intervall von 30 Sekunden

            // Besuch beim Schließen des Tabs beenden
            window.addEventListener('beforeunload', function() {
                if (window.uniqueId && window.ajaxUrl) {
                    navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
                        unique_id: window.uniqueId,
                        end_time: Date.now()
                    }), function(response) {
                        console.log('Daten erfolgreich gesendet:', response);
                    });
                }
            });
        </script>";
    }
});

Entwicklung:

<?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: Entwicklung 7
* 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 bei einem Besucher um einen Bot handelt. 
Diese Funktion nutzt 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-functions.php' ) ) {
    require_once plugin_dir_path( __FILE__ ) . 'bot-functions.php';
} else {
    // Fehlerbehandlung, falls die Datei nicht gefunden wurde
    error_log('bot-functions.php wurde nicht gefunden.');
}

// Beispiel, wie die Funktion verwendet wird:
if ( is_bot_or_spider() ) {
    // Bot erkannt, keine Zählung vornehmen
}

// Besuchs-Tracking initialisieren
function start_visit_tracking() {
    // Den angemeldeten Admin und Bots ausschließen
    if ( current_user_can( 'administrator' ) || is_bot_or_spider() ) {
        return; // Frühzeitig abbrechen, wenn der angemeldete Benutzer ein Admin ist oder es sich um einen Bot handelt
    }

    if (session_status() == PHP_SESSION_NONE) {
        session_start();
    }

    if (!isset($_SESSION['visit_start_time'])) {
        $_SESSION['visit_start_time'] = time();
    }

    // Besuchsdaten abrufen und AJAX-Skript laden
    echo "<script>
        window.ajaxUrl = '" . admin_url('admin-ajax.php') . "';
        window.addEventListener('load', function() {
            window.uniqueId = 'id-' + Math.random().toString(36).substr(2, 16);
            fetch(window.ajaxUrl + '?action=start_visit', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({ unique_id: window.uniqueId, page_title: document.title })
            });

            // Timeout-Überprüfung alle 30 Sekunden
            setInterval(function() {
                fetch(window.ajaxUrl + '?action=check_visit_timeout', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'}
                }).then(response => response.json()).then(data => {
                    if (data.timeout_reached) {
                        console.log('Besuch beendet wegen Timeout.');
                    }
                });
            }, 30000);

            // Besuch beenden beim Schließen des Tabs oder Browsers
            window.addEventListener('beforeunload', function() {
                if (window.uniqueId && window.ajaxUrl) {
                    navigator.sendBeacon(window.ajaxUrl + '?action=check_visit_timeout', JSON.stringify({
                        unique_id: window.uniqueId,
                        end_time: Date.now()
                    }));
                }
            });
        });
    </script>";
}

// Bot-Erkennungsfunktion laden, falls noch nicht geladen
if (!function_exists('is_bot_or_spider')) {
 if (file_exists(plugin_dir_path(__FILE__) . 'bot-functions.php')) {
     require_once plugin_dir_path(__FILE__) . 'bot-functions.php';
 } else {
     error_log('bot-functions.php wurde nicht gefunden.');
 }
}

add_action('wp_footer', 'start_visit_tracking');

// 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.");
    }
}

// Ajax-Handler für Timeout-Überprüfung
function check_visit_timeout() {
    if (!isset($_SESSION['visit_start_time'])) {
        wp_send_json(['timeout_reached' => false]);
        return;
    }

    $max_duration = 90 * 60; // 90 Minuten
    $current_duration = time() - $_SESSION['visit_start_time'];

    if ($current_duration > $max_duration) {
        end_visit_tracking(); // Besuch beenden bei Timeout
        wp_send_json(['timeout_reached' => true]);
    } else {
        wp_send_json(['timeout_reached' => false]);
    }
}

add_action('wp_ajax_check_visit_timeout', 'check_visit_timeout');
add_action('wp_ajax_nopriv_check_visit_timeout', 'check_visit_timeout');

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

// Funktion zur Erkennung von Bots und Spidern
if (is_bot_or_spider()) {
    // Bot erkannt
}

// 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 (Sekunden)</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
function update_all_visit_durations() {
    $visits = get_option('current_visits', []);
    
    $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'])) {
            // Berechne die Verweildauer für die aktiven Besuche
            $duration = time() - $visit_data['start_time']; // Verwende aktuelle Zeit
            $visit_data['duration'] = $duration;
            $visit_data['status'] = 'Aktiv'; // Status bleibt 'Aktiv', wenn noch keine Dauer
        } else {
            // Wenn die Verweildauer bereits festgelegt ist, wird der Status als 'Beendet' angezeigt
            $visit_data['status'] = 'Beendet';
        }

        // Berechne Minuten und Sekunden für jede Verweildauer
        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 (aktualisierte) Besuchsdaten hinzu
    }

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

    // Speichern der neuen Verweildauern
    update_option('current_visits', $visits); 

    // Sende die aktualisierten Daten mit der formatierten Dauer und dem Status 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 sehr funktionell
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');

// JavaScript zur Timeout-Überprüfung und Beendigung beim Seitenverlassen
add_action('wp_footer', function() {
    echo "<script>
        // Timeout-Überprüfung alle 30 Sekunden
        setInterval(function() {
            fetch(window.ajaxUrl + '?action=check_visit_timeout', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'}
            }).then(response => response.json()).then(data => {
                if (data.timeout_reached) {
                    console.log('Besuch beendet wegen Timeout.');
                    // alert('Der Besuch wurde aufgrund des Timeouts beendet.');
                }
            });
        }, 30000); // Intervall von 30 Sekunden

        // Besuch beim Schließen des Tabs beenden
        window.addEventListener('beforeunload', function() {
            if (window.uniqueId && window.ajaxUrl) {
                navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
                    unique_id: window.uniqueId,
                    end_time: Date.now()
                }));
            }
        });
    </script>";
});

Entwicklung:

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

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

// Besuchs-Tracking initialisieren
function start_visit_tracking() {
    $user_ip = $_SERVER['REMOTE_ADDR'];
    
    // Admin- und Bot-Ausschluss mithilfe der IP- und Bot-Check-Funktionen
    if (function_exists('is_ip_excluded') && is_ip_excluded($user_ip) || is_bot_or_spider()) {
        return; // Frühzeitig abbrechen
    }

    if (session_status() == PHP_SESSION_NONE) {
        session_start();
    }

    // Wenn keine Startzeit gesetzt ist, wird sie auf den aktuellen Zeitpunkt gesetzt
    if (!isset($_SESSION['visit_start_time'])) {
        $_SESSION['visit_start_time'] = time();
    }

    // Besuchsdaten abrufen und AJAX-Skript laden
    echo "<script>
        window.ajaxUrl = '" . admin_url('admin-ajax.php') . "';
        
        window.addEventListener('load', function() {
            window.uniqueId = 'id-' + Math.random().toString(36).substr(2, 16);

            fetch(window.ajaxUrl + '?action=start_visit', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({ unique_id: window.uniqueId, page_title: document.title })
            });

            // Timeout-Überprüfung alle 30 Sekunden
            setInterval(function() {
                fetch(window.ajaxUrl + '?action=check_visit_timeout', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'}
                }).then(response => response.json()).then(data => {
                    if (data.timeout_reached) {
                        console.log('Besuch beendet wegen Timeout.');
                        // Hier kannst du zusätzliche Aktionen durchführen, z.B. Seite neu laden oder den Status im Frontend ändern.
                        // alert('Der Besuch wurde aufgrund des Timeouts beendet.');
                    }
                });
            }, 30000); // Alle 30 Sekunden prüfen

            // Besuch beim Schließen des Tabs oder Browsers beenden mit sendBeacon
            window.addEventListener('beforeunload', function() {
                if (window.uniqueId && window.ajaxUrl) {
                    // sendBeacon anstelle von fetch verwenden, um die Anfrage zuverlässig abzuschicken
                    navigator.sendBeacon(window.ajaxUrl + '?action=check_visit_timeout', JSON.stringify({
                        unique_id: window.uniqueId,
                        end_time: Date.now() // Besuch beenden
                    }));
                }
            });
        });
    </script>";
}

add_action('wp_footer', 'start_visit_tracking');

// 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.");
    }
}

// Ajax-Handler für Timeout-Überprüfung
function check_visit_timeout() {
    if (isset($_SESSION['visit_start_time'])) {
        $max_duration = 90 * 60; // 90 Minuten in Sekunden
        $current_duration = time() - $_SESSION['visit_start_time'];

        if ($current_duration > $max_duration) {
            end_visit_tracking(); // Besuch beenden bei Timeout

            // Besuchsdaten im Backend aktualisieren
            $visits = get_option('current_visits', []);
            foreach ($visits as $unique_id => $visit_data) {
                if (!isset($visit_data['duration'])) {
                    // Besuch als beendet markieren
                    $visits[$unique_id]['status'] = 'Beendet';
                    $visits[$unique_id]['duration'] = $current_duration;
                    $visits[$unique_id]['formatted_duration'] = sprintf('%02d:%02d', floor($current_duration / 60), $current_duration % 60);
                }
            }

            update_option('current_visits', $visits); // Besuche aktualisieren
            wp_send_json(['timeout_reached' => true]);
        } else {
            wp_send_json(['timeout_reached' => false]);
        }
    } else {
        wp_send_json(['timeout_reached' => false]);
    }
}

add_action('wp_ajax_check_visit_timeout', 'check_visit_timeout');
add_action('wp_ajax_nopriv_check_visit_timeout', 'check_visit_timeout');


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

// Funktion zur Erkennung von Bots und Spidern
if (!function_exists('is_bot_or_spider')) {
    function is_bot_or_spider() {
        $cached_result = get_transient('is_bot_' . $_SERVER['REMOTE_ADDR']);
        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'
        ];

        // Erstellen des Regex-Musters basierend auf der Liste der Bot-Namen
        $pattern = '/(' . implode('|', $bots) . ')/i';

        // Prüfen, ob der User-Agent mit dem Muster übereinstimmt
        if (preg_match($pattern, $user_agent)) {
            set_transient('is_bot_' . $_SERVER['REMOTE_ADDR'], true, 12 * HOUR_IN_SECONDS);
            return true;
        }

        set_transient('is_bot_' . $_SERVER['REMOTE_ADDR'], false, 12 * HOUR_IN_SECONDS);
        return false;
    }
}

// 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',
        '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 (Sekunden)</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
function update_all_visit_durations() {
    $visits = get_option('current_visits', []);
    
    $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'])) {
            // Berechne die Verweildauer für die aktiven Besuche
            $duration = time() - $visit_data['start_time']; // Verwende aktuelle Zeit
            $visit_data['duration'] = $duration;
            $visit_data['status'] = 'Aktiv'; // Status bleibt 'Aktiv', wenn noch keine Dauer
        } else {
            // Wenn die Verweildauer bereits festgelegt ist, wird der Status als 'Beendet' angezeigt
            $visit_data['status'] = 'Beendet';
        }

        // Berechne Minuten und Sekunden für jede Verweildauer
        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 (aktualisierte) Besuchsdaten hinzu
    }

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

    // Speichern der neuen Verweildauern
    update_option('current_visits', $visits); 

    // Sende die aktualisierten Daten mit der formatierten Dauer und dem Status 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 sehr funktionell
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');

// JavaScript zur Timeout-Überprüfung und Beendigung beim Seitenverlassen
add_action('wp_footer', function() {
    echo "<script>
        // Timeout-Überprüfung alle 30 Sekunden
        setInterval(function() {
            fetch(window.ajaxUrl + '?action=check_visit_timeout', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'}
            }).then(response => response.json()).then(data => {
                if (data.timeout_reached) {
                    console.log('Besuch beendet wegen Timeout.');
                    alert('Der Besuch wurde aufgrund des Timeouts beendet.');
                }
            });
        }, 30000); // Intervall von 30 Sekunden

        // Besuch beim Schließen des Tabs beenden
        window.addEventListener('beforeunload', function() {
            if (window.uniqueId && window.ajaxUrl) {
                navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
                    unique_id: window.uniqueId,
                    end_time: Date.now()
                }));
            }
        });
    </script>";
});

Entwicklung :

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

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

// Besuchs-Tracking initialisieren
function start_visit_tracking() {
    $user_ip = $_SERVER['REMOTE_ADDR'];
    
    // Admin- und Bot-Ausschluss mithilfe der IP- und Bot-Check-Funktionen
    if (function_exists('is_ip_excluded') && is_ip_excluded($user_ip) || is_bot_or_spider()) {
        return; // Frühzeitig abbrechen
    }

    if (session_status() == PHP_SESSION_NONE) {
        session_start();
    }

    // Wenn keine Startzeit gesetzt ist, wird sie auf den aktuellen Zeitpunkt gesetzt
    if (!isset($_SESSION['visit_start_time'])) {
        $_SESSION['visit_start_time'] = time();
    }

    // Besuchsdaten abrufen und AJAX-Skript laden
    echo "<script>
        window.ajaxUrl = '" . admin_url('admin-ajax.php') . "';
        
        window.addEventListener('load', function() {
            window.uniqueId = 'id-' + Math.random().toString(36).substr(2, 16);

            fetch(window.ajaxUrl + '?action=start_visit', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({ unique_id: window.uniqueId, page_title: document.title })
            });

            // Timeout-Überprüfung alle 30 Sekunden
            setInterval(function() {
                fetch(window.ajaxUrl + '?action=check_visit_timeout', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'}
}).then(response => response.json()).then(data => {
    if (data.timeout_reached) {
        console.log('Besuch beendet wegen Timeout.');
        // Hier kannst du zusätzliche Aktionen durchführen, z.B. Seite neu laden oder den Status im Frontend ändern.
        // alert('Der Besuch wurde aufgrund des Timeouts beendet.');
    }
});

            }, 30000); // Alle 30 Sekunden prüfen
        });
    </script>";
}

add_action('wp_footer', 'start_visit_tracking');

// 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.");
    }
}

// Ajax-Handler für Timeout-Überprüfung
function check_visit_timeout() {
    if (isset($_SESSION['visit_start_time'])) {
        $max_duration = 90 * 60; // 90 Minuten in Sekunden
        $current_duration = time() - $_SESSION['visit_start_time'];

        if ($current_duration > $max_duration) {
            end_visit_tracking(); // Besuch beenden bei Timeout

            // Besuchsdaten im Backend aktualisieren
            $visits = get_option('current_visits', []);
            foreach ($visits as $unique_id => $visit_data) {
                if (!isset($visit_data['duration'])) {
                    // Besuch als beendet markieren
                    $visits[$unique_id]['status'] = 'Beendet';
                    $visits[$unique_id]['duration'] = $current_duration;
                    $visits[$unique_id]['formatted_duration'] = sprintf('%02d:%02d', floor($current_duration / 60), $current_duration % 60);
                }
            }

            update_option('current_visits', $visits); // Besuche aktualisieren
            wp_send_json(['timeout_reached' => true]);
        } else {
            wp_send_json(['timeout_reached' => false]);
        }
    } else {
        wp_send_json(['timeout_reached' => false]);
    }
}

add_action('wp_ajax_check_visit_timeout', 'check_visit_timeout');
add_action('wp_ajax_nopriv_check_visit_timeout', 'check_visit_timeout');


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

// Funktion zur Erkennung von Bots und Spidern
if (!function_exists('is_bot_or_spider')) {
    function is_bot_or_spider() {
        $cached_result = get_transient('is_bot_' . $_SERVER['REMOTE_ADDR']);
        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', 'spider', 'ia_archiver'
        ];

        foreach ($bots as $bot) {
            if (strpos($user_agent, $bot) !== false) {
                set_transient('is_bot_' . $_SERVER['REMOTE_ADDR'], true, 12 * HOUR_IN_SECONDS);
                return true;
            }
        }

        set_transient('is_bot_' . $_SERVER['REMOTE_ADDR'], false, 12 * HOUR_IN_SECONDS);
        return false;
    }
}

// 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',
        '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 (Sekunden)</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
function update_all_visit_durations() {
    $visits = get_option('current_visits', []);
    
    $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'])) {
            // Berechne die Verweildauer für die aktiven Besuche
            $duration = time() - $visit_data['start_time']; // Verwende aktuelle Zeit
            $visit_data['duration'] = $duration;
            $visit_data['status'] = 'Aktiv'; // Status bleibt 'Aktiv', wenn noch keine Dauer
        } else {
            // Wenn die Verweildauer bereits festgelegt ist, wird der Status als 'Beendet' angezeigt
            $visit_data['status'] = 'Beendet';
        }

        // Berechne Minuten und Sekunden für jede Verweildauer
        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 (aktualisierte) Besuchsdaten hinzu
    }

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

    // Speichern der neuen Verweildauern
    update_option('current_visits', $visits); 

    // Sende die aktualisierten Daten mit der formatierten Dauer und dem Status 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');

// JavaScript zur Erfassung der Verweildauer beim Seitenverlassen
add_action('wp_footer', function() {
    echo "<script>
        window.addEventListener('beforeunload', function() {
            if (window.uniqueId && window.ajaxUrl) {
                navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
                    unique_id: window.uniqueId,
                    end_time: Date.now()
                }));
            }
        });
    </script>";
});

Entwicklung :

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

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

// Besuchs-Tracking initialisieren
function start_visit_tracking() {
    $user_ip = $_SERVER['REMOTE_ADDR'];
    
    // Admin- und Bot-Ausschluss mithilfe der IP- und Bot-Check-Funktionen
    if (function_exists('is_ip_excluded') && is_ip_excluded($user_ip) || is_bot_or_spider()) {
        return; // Frühzeitig abbrechen
    }

    if (session_status() == PHP_SESSION_NONE) {
        session_start();
    }

    // Wenn keine Startzeit gesetzt, dann setzen wir sie auf den aktuellen Zeitpunkt
    if (!isset($_SESSION['visit_start_time'])) {
        $_SESSION['visit_start_time'] = time();
    }

    // 90 Minuten Timeout: Überprüfen, ob die Zeit überschritten wurde
    $max_duration = 90 * 60; // 90 Minuten in Sekunden
    if (isset($_SESSION['visit_start_time']) && (time() - $_SESSION['visit_start_time']) > $max_duration) {
        // Timeout erreicht, Verweildauer als beendet setzen
        end_visit_tracking();  // Diese Funktion behandelt das Beenden der Verweildauer
    }

    // Besuchsdaten abrufen und AJAX-Skript laden
    $visits = get_option('current_visits', []);
    echo "<script>
        window.ajaxUrl = '" . admin_url('admin-ajax.php') . "';
        
        window.addEventListener('load', function() {
            window.uniqueId = 'id-' + Math.random().toString(36).substr(2, 16);

            fetch(window.ajaxUrl + '?action=start_visit', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({ unique_id: window.uniqueId, page_title: document.title })
            });
        });
    </script>";
}

add_action('wp_footer', 'start_visit_tracking');

function end_visit_tracking() {
    // Berechnung der Verweildauer
    if (isset($_SESSION['visit_start_time'])) {
        $duration = time() - $_SESSION['visit_start_time']; // Verweildauer in Sekunden

        // Verweildauer speichern, z.B. in einer Option oder einer Sitzung
        // Zum Beispiel: update_option('visit_duration_' . session_id(), $duration);
        
        // Oder hier eine einfache Option, um die Verweildauer zu speichern
        update_option('last_visitor_duration', $duration);  // Option für den letzten Besuch

        // Verweildauer zurücksetzen
        unset($_SESSION['visit_start_time']);
    }

    // Optional: Sende eine Nachricht oder führe eine Aktion nach dem Timeout aus
    // Zum Beispiel: Benachrichtigung in der Admin-Oberfläche
    // wp_mail('admin@deinedomain.com', 'Besucher hat die Seite 90 Minuten lang offen gelassen', 'Die Verweildauer wurde überschritten.');
}

// Optionale Funktion zum Abfragen der letzten Verweildauer
function get_last_visitor_duration() {
    return get_option('last_visitor_duration', 0); // Standardwert ist 0, falls keine Verweildauer gesetzt
}

add_action('wp_footer', 'end_visit_tracking');


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

// Funktion zur Erkennung von Bots und Spidern
if (!function_exists('is_bot_or_spider')) {
    function is_bot_or_spider() {
        $cached_result = get_transient('is_bot_' . $_SERVER['REMOTE_ADDR']);
        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', 'spider', 'ia_archiver'
        ];

        foreach ($bots as $bot) {
            if (strpos($user_agent, $bot) !== false) {
                set_transient('is_bot_' . $_SERVER['REMOTE_ADDR'], true, 12 * HOUR_IN_SECONDS);
                return true;
            }
        }

        set_transient('is_bot_' . $_SERVER['REMOTE_ADDR'], false, 12 * HOUR_IN_SECONDS);
        return false;
    }
}

// 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',
        '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;
    }

    // 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

                    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 (Sekunden)</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
function update_all_visit_durations() {
    $visits = get_option('current_visits', []);
    
    $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'])) {
            // Berechne die Verweildauer für die aktiven Besuche
            $duration = time() - $visit_data['start_time']; // Verwende aktuelle Zeit
            $visit_data['duration'] = $duration;
            $visit_data['status'] = 'Aktiv'; // Status bleibt 'Aktiv', wenn noch keine Dauer
        } else {
            // Wenn die Verweildauer bereits festgelegt ist, wird der Status als 'Beendet' angezeigt
            $visit_data['status'] = 'Beendet';
        }

        // Berechne Minuten und Sekunden für jede Verweildauer
        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 (aktualisierte) Besuchsdaten hinzu
    }

    update_option('current_visits', $visits); // Speichern der neuen Verweildauern

    wp_send_json_success(['updated_visits' => $updated_visits]); // Sende die aktualisierten Daten mit der formatierten Dauer und dem Status zurück
}


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');

// JavaScript zur Erfassung der Verweildauer beim Seitenverlassen
add_action('wp_footer', function() {
    echo "<script>
        window.addEventListener('beforeunload', function() {
            if (window.uniqueId && window.ajaxUrl) {
                navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
                    unique_id: window.uniqueId,
                    end_time: Date.now()
                }));
            }
        });
    </script>";
});

Entwicklung:

<?php
/*
Plugin Name: Visit Duration Tracker
Description: Ermöglicht das Messen der Verweildauer von Besuchern auf einer WordPress-Seite ohne Cookies und ohne separate Datenbanktabelle. DSGVO-konform.
Version: Entwicklung 3
Author: Team WP Wegerl.at
*/
// Besuchs-Tracking initialisieren
function start_visit_tracking() {
    $user_ip = $_SERVER['REMOTE_ADDR'];
    
    // Admin- und Bot-Ausschluss mithilfe der IP- und Bot-Check-Funktionen
    if (function_exists('is_ip_excluded') && is_ip_excluded($user_ip) || is_bot_or_spider()) {
        return; // Frühzeitig abbrechen
    }

    if (session_status() == PHP_SESSION_NONE) {
        session_start();
    }

    // Besuchsdaten abrufen und AJAX-Skript laden
    $visits = get_option('current_visits', []);
    echo "<script>
        window.ajaxUrl = '" . admin_url('admin-ajax.php') . "';
        
        window.addEventListener('load', function() {
            window.uniqueId = 'id-' + Math.random().toString(36).substr(2, 16);

            fetch(window.ajaxUrl + '?action=start_visit', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({ unique_id: window.uniqueId, page_title: document.title })
            });
        });
    </script>";
}

add_action('wp_footer', 'start_visit_tracking');

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

// Funktion zur Erkennung von Bots und Spidern
if (!function_exists('is_bot_or_spider')) {
    function is_bot_or_spider() {
        $cached_result = get_transient('is_bot_' . $_SERVER['REMOTE_ADDR']);
        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', 'spider', 'ia_archiver'
        ];

        foreach ($bots as $bot) {
            if (strpos($user_agent, $bot) !== false) {
                set_transient('is_bot_' . $_SERVER['REMOTE_ADDR'], true, 12 * HOUR_IN_SECONDS);
                return true;
            }
        }

        set_transient('is_bot_' . $_SERVER['REMOTE_ADDR'], false, 12 * HOUR_IN_SECONDS);
        return false;
    }
}

// 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 12 Einträge begrenzen
    if (count($visits) > 12) {
        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);
    $unique_id = sanitize_text_field($data['unique_id']);
    $end_time = intval($data['end_time'] / 1000);

    $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',
        '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
function display_visit_duration_widget() {
    $visits = get_option('current_visits', []);

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

    echo '<table id="visit-duration-table" style="width:100%; text-align:left;">';
    echo '<tr><th>Seiten-Titel</th><th>Verweildauer (Min:Sek)</th><th>Status</th></tr>';

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

        echo '<tr>';
        echo '<td>' . esc_html($visit_data['page_title']) . '</td>';
        echo '<td>' . esc_html($formatted_duration) . '</td>'; // Formatiert als "Min:Sek"
        echo '<td>' . esc_html($status) . '</td>';
        echo '</tr>';
    }

    echo '</table>';
    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) {
                    // Erstelle die neue Tabelle ohne Unique ID
                    jQuery('#visit-duration-table').html('<tr><th>Seiten-Titel</th><th>Verweildauer (Min:Sek)</th><th>Status</th></tr>');
                    response.data.updated_visits.forEach(function(visit) {
                        jQuery('#visit-duration-table').append(
                            '<tr>' +
                            '<td>' + visit.page_title + '</td>' +
                            '<td>' + visit.formatted_duration + '</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('<tr><th>Seiten-Titel</th><th>Verweildauer (Sekunden)</th><th>Status</th></tr>');
                    } 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
// Funktion zur Aktualisierung der Verweildauer aller Besuche
// AJAX-Handler zur Aktualisierung der Verweildauer aller Besucher
function update_all_visit_durations() {
    $visits = get_option('current_visits', []);
    
    $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'])) {
            // Berechne die Verweildauer für die aktiven Besuche
            $duration = time() - $visit_data['start_time']; // Verwende aktuelle Zeit
            $visit_data['duration'] = $duration;
            $visit_data['status'] = 'Aktiv'; // Status bleibt 'Aktiv', wenn noch keine Dauer
        } else {
            // Wenn die Verweildauer bereits festgelegt ist, wird der Status als 'Beendet' angezeigt
            $visit_data['status'] = 'Beendet';
        }

        // Berechne Minuten und Sekunden für jede Verweildauer
        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 (aktualisierte) Besuchsdaten hinzu
    }

    update_option('current_visits', $visits); // Speichern der neuen Verweildauern

    wp_send_json_success(['updated_visits' => $updated_visits]); // Sende die aktualisierten Daten mit der formatierten Dauer und dem Status zurück
}


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');

// JavaScript zur Erfassung der Verweildauer beim Seitenverlassen
add_action('wp_footer', function() {
    echo "<script>
        window.addEventListener('beforeunload', function() {
            if (window.uniqueId && window.ajaxUrl) {
                navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
                    unique_id: window.uniqueId,
                    end_time: Date.now()
                }));
            }
        });
    </script>";
});

Entwicklung:

<?php
/*
Plugin Name: Visit Duration Tracker
Description: Ermöglicht das Messen der Verweildauer von Besuchern auf einer WordPress-Seite ohne Cookies und ohne separate Datenbanktabelle. DSGVO-konform.
Version: Entwicklung 2
Author: Team WP Wegerl.at
Author URI: https://wegerl.at
Text Domain: visit-duration-tracker
*/
// Funktion zur Initialisierung und Startzeit eines Besuchs
function start_visit_tracking() {
    if (session_status() == PHP_SESSION_NONE) {
        session_start();
    }

    // Besuchsdaten aus der Optionstabelle abrufen
    $visits = get_option('current_visits', []);

    // Übergabe von AJAX-URL und Anweisungen an den Browser
    echo "<script>
        window.ajaxUrl = '" . admin_url('admin-ajax.php') . "';
        
        // Bei jedem Neuladen der Seite wird eine neue ID generiert
        window.addEventListener('load', function() {
            window.uniqueId = 'id-' + Math.random().toString(36).substr(2, 16); // Zufällige ID erstellen

            // Senden der Startzeit und ID an den Server
            fetch(window.ajaxUrl + '?action=start_visit', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({ unique_id: window.uniqueId, page_title: document.title })
            });
        });
    </script>";
}

add_action('wp_footer', 'start_visit_tracking');

// 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 12 Einträge begrenzen
    if (count($visits) > 12) {
        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);
    $unique_id = sanitize_text_field($data['unique_id']);
    $end_time = intval($data['end_time'] / 1000);

    $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',
        '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
function display_visit_duration_widget() {
    $visits = get_option('current_visits', []);

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

    echo '<table id="visit-duration-table" style="width:100%; text-align:left;">';
    echo '<tr><th>Seiten-Titel</th><th>Verweildauer (Min:Sek)</th><th>Status</th></tr>';

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

        echo '<tr>';
        echo '<td>' . esc_html($visit_data['page_title']) . '</td>';
        echo '<td>' . esc_html($formatted_duration) . '</td>'; // Formatiert als "Min:Sek"
        echo '<td>' . esc_html($status) . '</td>';
        echo '</tr>';
    }

    echo '</table>';
    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) {
                    // Erstelle die neue Tabelle ohne Unique ID
                    jQuery('#visit-duration-table').html('<tr><th>Seiten-Titel</th><th>Verweildauer (Min:Sek)</th><th>Status</th></tr>');
                    response.data.updated_visits.forEach(function(visit) {
                        jQuery('#visit-duration-table').append(
                            '<tr>' +
                            '<td>' + visit.page_title + '</td>' +
                            '<td>' + visit.formatted_duration + '</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('<tr><th>Seiten-Titel</th><th>Verweildauer (Sekunden)</th><th>Status</th></tr>');
                    } 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
function update_all_visit_durations() {
    $visits = get_option('current_visits', []);
    
    $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'])) {
            // Berechne die Verweildauer für die aktiven Besuche
            $duration = time() - $visit_data['start_time']; // Verwende aktuelle Zeit
            $visit_data['duration'] = $duration;
            $visit_data['status'] = 'Aktiv'; // Status bleibt 'Aktiv', wenn noch keine Dauer
        } else {
            // Wenn die Verweildauer bereits festgelegt ist, wird der Status als 'Beendet' angezeigt
            $visit_data['status'] = 'Beendet';
        }

        // Berechne Minuten und Sekunden für jede Verweildauer
        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 (aktualisierte) Besuchsdaten hinzu
    }

    update_option('current_visits', $visits); // Speichern der neuen Verweildauern

    wp_send_json_success(['updated_visits' => $updated_visits]); // Sende die aktualisierten Daten mit der formatierten Dauer und dem Status zurück
}


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');

// JavaScript zur Erfassung der Verweildauer beim Seitenverlassen
add_action('wp_footer', function() {
    echo "<script>
        window.addEventListener('beforeunload', function() {
            if (window.uniqueId && window.ajaxUrl) {
                navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
                    unique_id: window.uniqueId,
                    end_time: Date.now()
                }));
            }
        });
    </script>";
});

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

Entwicklung:

<?php
/*
Plugin Name: Visit Duration Tracker
Description:
Version: Entwicklung 1
Author: Team WP Wegerl.at
*/
// Funktion zur Initialisierung und Startzeit eines Besuchs
function start_visit_tracking() {
    if (session_status() == PHP_SESSION_NONE) {
        session_start();
    }

    // Besuchsdaten aus der Optionstabelle abrufen
    $visits = get_option('current_visits', []);

    // Übergabe von AJAX-URL und Anweisungen an den Browser
    echo "<script>
        window.ajaxUrl = '" . admin_url('admin-ajax.php') . "';
        
        // Bei jedem Neuladen der Seite wird eine neue ID generiert
        window.addEventListener('load', function() {
            window.uniqueId = 'id-' + Math.random().toString(36).substr(2, 16); // Zufällige ID erstellen

            // Senden der Startzeit und ID an den Server
            fetch(window.ajaxUrl + '?action=start_visit', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({ unique_id: window.uniqueId, page_title: document.title })
            });
        });
    </script>";
}

add_action('wp_footer', 'start_visit_tracking');

// 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 12 Einträge begrenzen
    if (count($visits) > 12) {
        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);
    $unique_id = sanitize_text_field($data['unique_id']);
    $end_time = intval($data['end_time'] / 1000);

    $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',
        '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
function display_visit_duration_widget() {
    $visits = get_option('current_visits', []);

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

    echo '<table id="visit-duration-table" style="width:100%; text-align:left;">';
    echo '<tr><th>Unique ID</th><th>Seiten-Titel</th><th>Verweildauer (Sekunden)</th><th>Status</th></tr>';

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

        echo '<tr>';
        echo '<td>' . esc_html($visit_data['unique_id']) . '</td>';
        echo '<td>' . esc_html($visit_data['page_title']) . '</td>';
        echo '<td>' . esc_html($duration) . '</td>';
        echo '<td>' . esc_html($status) . '</td>';
        echo '</tr>';
    }

    echo '</table>';
	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) {
                    // Erstelle die neue Tabelle mit formatierten Verweildauern und Status
                    jQuery('#visit-duration-table').html('<tr><th>Unique ID</th><th>Seiten-Titel</th><th>Verweildauer (Minuten:Sekunden)</th><th>Status</th></tr>');
                    response.data.updated_visits.forEach(function(visit) {
                        // Zeige die formatierten Zeiten (Minuten:Sekunden) und den Status an
                        jQuery('#visit-duration-table').append(
                            '<tr>' +
                            '<td>' + visit.unique_id + '</td>' +
                            '<td>' + visit.page_title + '</td>' +
                            '<td>' + visit.formatted_duration + '</td>' +
                            '<td>' + visit.status + '</td>' + // Status hinzugefügt
                            '</tr>'
                        );
                    });
                } else {
                    alert('Fehler bei der Aktualisierung der Verweildauer.');
                }
            },
            error: function() {
                alert('Fehler beim Aktualisieren der Verweildauer.');
            }
        });
    });


    // "Reset"-Button
    document.getElementById('reset-duration-btn').addEventListener('click', function() {
        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('<tr><th>Unique ID</th><th>Seiten-Titel</th><th>Verweildauer (Sekunden)</th><th>Status</th></tr>');
                } else {
                    alert('Fehler beim Zurücksetzen der Tabelle.');
                }
            },
            error: function() {
                alert('Fehler beim Zurücksetzen der Tabelle.');
            }
        });
    });
</script>

<?php
}

// AJAX-Handler zur Aktualisierung der Verweildauer aller Besucher
// Funktion zur Aktualisierung der Verweildauer aller Besuche
// AJAX-Handler zur Aktualisierung der Verweildauer aller Besucher
function update_all_visit_durations() {
    $visits = get_option('current_visits', []);
    
    $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'])) {
            // Berechne die Verweildauer für die aktiven Besuche
            $duration = time() - $visit_data['start_time']; // Verwende aktuelle Zeit
            $visit_data['duration'] = $duration;
            $visit_data['status'] = 'Aktiv'; // Status bleibt 'Aktiv', wenn noch keine Dauer
        } else {
            // Wenn die Verweildauer bereits festgelegt ist, wird der Status als 'Beendet' angezeigt
            $visit_data['status'] = 'Beendet';
        }

        // Berechne Minuten und Sekunden für jede Verweildauer
        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 (aktualisierte) Besuchsdaten hinzu
    }

    update_option('current_visits', $visits); // Speichern der neuen Verweildauern

    wp_send_json_success(['updated_visits' => $updated_visits]); // Sende die aktualisierten Daten mit der formatierten Dauer und dem Status zurück
}


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');

// JavaScript zur Erfassung der Verweildauer beim Seitenverlassen
add_action('wp_footer', function() {
    echo "<script>
        window.addEventListener('beforeunload', function() {
            if (window.uniqueId && window.ajaxUrl) {
                navigator.sendBeacon(window.ajaxUrl + '?action=update_visit_duration', JSON.stringify({
                    unique_id: window.uniqueId,
                    end_time: Date.now()
                }));
            }
        });
    </script>";
});

Schaffe mit WordPress und Advanced Editor schöne Websites. Hier ist für dich, euch eine leicht lesbare und freundliche Anleitung.

Katze
Die Website verwendet funktionelle Cookies. Sie verwendet keine Cookies von Drittanbietern.

Aber hallo! – zur Begrüßung eine Rundfrage?

🧡 … das so zum Zeit entschleunigen.

Die Erstellung von Website-Inhalten erfordert oft kreative Ideen. Es wäre interessant zu erfahren, wo Ideen für die Gestaltung der Website-Inhalte gefunden werden und wie diese gestaltet werden. Bitte mitteilen, wie typischerweise Inspiration für die Website-Inhalte gefunden wird.

Sie können die Tastaturnavigation nutzen: Tab zum Fokussieren, Leertaste zum Auswählen und Esc zum Schließen der Rundfrage.

Was sind die besten Quellen zur
Erstellung von Website-Inhalten?
Bis zu drei Antworten sind möglich!



Start der Umfrage im September 2024

ERGEBNISSE

Wird geladen ... Wird geladen ...

Falls alle Optionen ausgewählt werden sollten, sollte man im Ausschussverfahren herausfinden, welches die Hauptsächlichen sind.


Im Inhalt die Tastaturnavigation:

Sobald Sie die Website scrollen, können Sie die Enter-Taste drücken, um den Inhalt direkt zu fokussieren, ohne den Tabindex zu durchlaufen. Das zweite Enter aktiviert das Element.

  • Tabulator (Tab): Navigiert durch interaktive Elemente.
  • Shift + Tab: Gehe zurück zum vorherigen Element.
  • Enter: Erste Betätigung fokussiert, zweite aktiviert.
  • Pfeiltasten: Scrollen in Texten oder Menüs.
  • Leertaste: Aktiviert Header-Buttons und scrollt im Content.
  • Shift + Leertaste: Scrollt nach oben.
  • 7: Aktiviert die Suche; Esc zum Schließen.
  • Esc: Bricht Fokussierung und Dialoge ab.

Echte Besucher statt nur Klicks, siehe:
effektives Tracking mit Statify! (neuer Tab)

So in 8-Tage mag sein 80 Besucher

Zur Optimierung unserer Website nutzen wir Tools wie 'Statify' sowie 'Visit Duration', das anonymisierte Daten zur Verweildauer, erfasst.

Danke sehr!

 🎶  Während des Besuchs kann es neben informativen Ergebnissen auch zu Klangeffekten kommen, um bestimmte Elemente hervorzuheben. – Viel Spaß beim Erkunden!


Erfolgreichen Besuch
wünscht Ihnen! – WP Wegerl.at

Pop-up  

Das zur Umfrage ist von WP-Polls und
das Pop-up ist von Boxzilla.

WP Wegerl.at
Leistungsmetriken im Blick
× -