Nowoczesny system detekcji online/idle/offline w PHP i JavaScript — kompletny przewodnik
Monitoring aktywności użytkownika w aplikacji webowej to dziś standard: komunikatory, CRM-y, intranety, systemy wsparcia, platformy e-learningowe — wszystkie potrzebują aktualnej informacji, czy użytkownik jest online, idle (bezczynny) czy offline.
Wiele implementacji jest jednak błędnych lub nieefektywnych: za rzadkie pingowanie, brak detekcji realnej aktywności, niepoprawne trackowanie kart, opóźnienia sięgające minut. W tym artykule pokazuję kompletny i praktyczny sposób wdrożenia takiego systemu, który:
- nie przeciąża serwera,
- wykrywa realną aktywność,
- działa natychmiast po wejściu na stronę,
- radzi sobie z wieloma otwartymi kartami,
- pozwala łatwo określić status usera w PHP.
Przedstawię również najczęstsze błędy i pułapki, które w takich systemach pojawiają się najczęściej — wraz z gotowymi rozwiązaniami.
1. Założenia i architektura rozwiązania
System opiera się na trzech fundamentach:
1) Detekcja aktywności użytkownika w JavaScript
Monitorujemy:
- ruch myszki,
- klawiaturę,
- kliknięcia,
- scroll,
- wejście na kartę (
visibilitychange), - aktywność mobilną.
Każde takie zdarzenie aktualizuje timestamp.
2) Heartbeat / ping wysyłany tylko wtedy, gdy użytkownik jest aktywny
Nie wysyłamy ciągłego „zegarowego” pingu co X sekund. Wysyłamy go tylko, jeśli użytkownik wykazuje aktywność.
3) PHP ustala status użytkownika na podstawie timestampu
Na backendzie przechowujemy last_activity i obliczamy status:
- ONLINE: ostatnia aktywność < 2 min
- IDLE: 2–5 min
- OFFLINE: > 5 min
Daje to pełną kontrolę po stronie serwera.
2. Finalna wersja skryptu online.js
Plik może być hostowany lokalnie lub na CDN.
Obsługuje parametry przekazywane przez data-*.
online.js
const script = document.currentScript;
const uri = script.dataset.uri;
const token = script.dataset.token;
const interval = Number(script.dataset.interval);
const idleLimit = Number(script.dataset.idle);
let lastActive = Date.now();
// Rejestrujemy aktywność
['mousemove', 'keydown', 'click', 'scroll', 'touchstart'].forEach(event => {
window.addEventListener(event, () => {
lastActive = Date.now();
});
});
// Powrót na kartę = aktywność
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") {
lastActive = Date.now();
}
});
// Pierwszy ping natychmiast po starcie
systemSetUserActive(uri, token);
// Heartbeat wysyłany tylko przy aktywności
setInterval(() => {
if (Date.now() - lastActive < idleLimit) {
systemSetUserActive(uri, token);
}
}, interval);
3. Wstawianie skryptu na stronie HTML
Kluczowe: skrypt musi być ładowany po axiosie i po funkcji systemSetUserActive().
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="/js/system.js"></script>
<script src="/js/online.js"
defer
data-uri="/"
data-token="1909ed1ea11248e646985fcd1cd0d108aed2e29f"
data-interval="30000"
data-idle="120000"></script>
Użycie defer gwarantuje:
- poprawną kolejność wykonywania,
- że dokument HTML jest sparsowany,
- bez konieczności używania
DOMContentLoaded.
4. Funkcja systemSetUserActive() w JavaScript
Może wyglądać tak:
function systemSetUserActive(uri, token) {
axios.post(uri + 'user/heartbeat', { token })
.catch(err => console.error('Heartbeat error:', err));
}
Do backendu wysyłamy minimalne dane: sam token. Reszta (np. powiązanie tokenu z użytkownikiem) musi być walidowana na serwerze.
5. Backend — zapis timestampu (PHP)
Przykład w Laravelu, ale logika jest uniwersalna:
public function heartbeat(Request $request)
{
$user = User::where('heartbeat_token', $request->token)->firstOrFail();
$user->last_activity = now();
$user->save();
return response()->json(['status' => 'ok']);
}
W bazie trzymamy kolumnę:
last_activity DATETIME
6. Obliczanie statusu online/idle/offline w PHP
public function getStatusAttribute()
{
$diff = now()->diffInSeconds($this->last_activity);
if ($diff < 120) {
return 'online';
} elseif ($diff < 300) {
return 'idle';
} else {
return 'offline';
}
}
Przykładowe wartości:
- 120s → online
- 300s → idle
- większe → offline
7. Jak to działa w praktyce (flow logiczny)
- Użytkownik wchodzi na stronę.
online.jsnatychmiast wysyła pierwszy ping → user = online.- Użytkownik wykonuje jakikolwiek ruch → aktualizujemy timestamp.
- Jeśli w ciągu 2 minut wystąpi aktywność → co 30s wysyłany jest ping.
- Jeśli użytkownik nic nie robi → ping przestaje się wysyłać.
- Backend widzi upływ czasu → zmienia status na idle → później offline.
Brak potrzeby websocketów, a system jest lekki, szybki, precyzyjny.
8. Najczęstsze błędy i pułapki
To jeden z najważniejszych rozdziałów — większość błędnych implementacji wynika z problemów opisanych poniżej.
1. Heartbeat wysyłany zawsze, nawet gdy użytkownik jest nieaktywny
Skutek:
- niepotrzebne obciążenie serwera,
- fałszywe statusy online.
Rozwiązanie: Ping warunkowy zależny od aktywności (jak w tym artykule).
2. Pierwszy ping opóźniony o 30–60 sekund
Bez „initial ping” status online jest spóźniony.
Rozwiązanie:
Natychmiastowe wywołanie systemSetUserActive() po starcie skryptu.
3. Zbyt uboga lista eventów aktywności
Często pomija się:
- scroll,
- touchstart,
- visibilitychange.
Skutki: Urządzenia mobilne i przełączanie kart są błędnie obsługiwane.
4. Brak konwersji data-* na liczby
script.dataset.interval zwraca string, nie liczbę.
Rozwiązanie:
Number(script.dataset.interval)
5. Ładowanie skryptu przed axiosem lub systemSetUserActive()
Skutkuje błędami typu: systemSetUserActive is not defined.
Rozwiązanie:
Kolejność skryptów lub defer.
6. Zbyt rzadkie lub zbyt częste pingowanie
Zbyt rzadko → opóźnione statusy. Zbyt często → zbędne obciążenie.
Rekomendacja:
- ping = 30 sekund,
- idle = 120–300 sekund.
7. Brak backendowej logiki statusów
Poleganie wyłącznie na tym, co wyśle frontend, jest błędne.
Backend musi obliczać status na podstawie czasu — zawsze.
8. Problemy z wieloma kartami
Każda karta wysyła własny heartbeat.
To normalne — nie próbujemy tego eliminować. Status obliczamy na backendzie → po braku aktywności we wszystkich kartach user przejdzie w idle/offline naturalnie.
9. trackowanie mousemove z dużym obciążeniem
Często w event handlerach wykonywane są ciężkie operacje. W naszym systemie to tylko aktualizacja timestampu → minimalne obciążenie.
9. Podsumowanie
System opisany w artykule:
- jest lekki i wydajny,
- pozwala dokładnie wykrywać stany użytkownika,
- działa poprawnie przy wielu kartach,
- nie wymaga WebSocketów,
- działa na każdym urządzeniu i przeglądarce,
- jest w pełni konfigurowalny przez
data-*.
W efekcie otrzymujemy profesjonalny, stabilny i skalowalny system statusów online, który można wdrożyć w każdym projekcie PHP + JavaScript.