Enwicklung 'Visit Duration'

 

In Reihe von unten nach oben zum Plug-in.

Aktuelle Version ist im Beitrag: 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: (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 sollen, empfiehlt es sich, im Ausschlussverfahren festzustellen, auf welche am ehesten verzichtet werden kann, und dann das auszuwählen, was am wichtigsten erscheint.


Hinweis zur 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.

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

So in 8-Tage mag sein 138 Klick's

 

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

Danke sehr!

Zur Optimierung unserer Website nutzen wir Tools wie 'Statify' und 'Visit Duration', die anonymisierte Daten wie die Verweildauer erfassen.

Pop-up  

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

Lesen Sie die Inhalte nicht bequem?

Erhöhen Sie die Schriftgröße über die der Headerzeile oder gleich hier:

Möchten Sie das Licht an- oder ausschalten?

Sie können auch die Tastatur verwenden:
Fokus mit Tab, Auswahl mit Space und Schließen mit Esc.


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.


Erfolgreichen Besuch
wünscht Ihnen! – WP Wegerl.at
WP Wegerl.at
Leistungsmetriken im Blick
× -