Statistik — cookiefri besöksstatistik
På Zkrinet: lägg till blocket under Byggblock i panelen — ett klick, din egen kod rörs inte och en ögonblicksbild tas först. Installationsstegen nedan gäller om du kör PocketBase själv, till exempel efter en export.
Besök, unika besökare, toppsidor, varifrån folk kommer och egna händelser ("Köp klickat") — utan cookies, utan localStorage och utan att IP-adresser sparas. Samma modell som Plausible använder.
Testat mot PocketBase 0.40.4.
#Installation
Kopiera pb_hooks/ och pb_migrations/ till din PocketBase och starta om. Samlingen zk_statistik_dagar (aggregaten) dyker upp i adminpanelen.
Ligger PocketBase bakom en proxy (Cloudflare, nginx, Caddy …) måste du ställa in Inställningar → Trusted proxy så att PocketBase ser besökarens riktiga IP — annars ser alla besökare ut som proxyn och räknas som en och samma.
#Klistra in i appen
<script>
(function () {
var URL = "https://din-pocketbase.se/api/zk/statistik/besok"
function skicka(data) {
try {
fetch(URL, { method: "POST", body: JSON.stringify(data), keepalive: true,
headers: { "Content-Type": "text/plain" } }) // text/plain = ingen CORS-förfrågan i förväg
} catch (_) {}
}
function sida() { skicka({ sida: location.pathname, referrer: document.referrer }) }
sida()
// Appar med egen routing (React/Vue/Svelte): räkna även när adressen byts utan omladdning.
var push = history.pushState
history.pushState = function () { push.apply(this, arguments); sida() }
addEventListener("popstate", sida)
// Egna händelser: window.zkHandelse("Köp klickat")
window.zkHandelse = function (namn) { skicka({ sida: location.pathname, handelse: namn }) }
})()
</script>
Snutten sätter ingen cookie, skriver inget i webbläsaren och läser inget därifrån.
#Läsa statistiken
GET /api/zk/statistik?fran=2026-10-01&till=2026-10-31 (superuser)
Utan fran/till = de senaste 30 dagarna (UTC). Högst 367 dagar per fråga.
const s = await pb.send("/api/zk/statistik", { query: { fran: "2026-10-01", till: "2026-10-31" } })
// { fran, till, besok, unika,
// dagar: [{ dag, besok, unika }],
// toppsidor: [{ sida, besok, unika }], // topp 20
// toppreferrers: [{ referrer, besok, unika }], // domän; "(direkt)" = ingen referrer, "(okänd)" = gick inte att läsa
// handelser: [{ handelse, antal, unika }] }
besok= sidvisningar.unika= unika besökare per dygn. Över flera dagar ärunikasumman av dagarna — samma person två dagar räknas två gånger. Det är priset för att hasharna inte går att koppla ihop över dagar, och det är avsiktligt.- Händelser räknas för sig och ingår inte i
besok. - Länkar mellan sidor på din egen sajt räknas inte som referrer (kräver att webbläsaren skickar
Origin, vilket den gör vidfetchfrån en annan adress).
#Så räknas unika — och varför ingen cookie behövs
- Varje dygn (UTC) slumpas ett nytt salt. Det ligger i en intern tabell som inte är en samling och inte nås via API:t.
- Besökaren blir
SHA-256(dagens salt + IP + User-Agent + värd). IP och User-Agent finns bara i minnet under anropet. - Hashen sparas under dygnet så att samma besökare inte räknas två gånger. Sedan raderas saltet (vid första besöket nästa dygn, och av gallringen varje timme) och hasharna gallras.
- Kvar blir bara aggregat: antal per dag, sida, referrer-domän och händelse.
Sparas aldrig: IP-adress, User-Agent, cookies, frågesträngar (?epost=… och #… tas bort från sidans adress), hela referrer-adresser (bara domänen).
PocketBase egen aktivitetslogg sparar som standard IP och User-Agent för varje anrop. Besöksrutten är därför undantagen från loggen ($apis.skipSuccessActivityLog()) och svarar alltid 202, även vid trasig indata, så att den aldrig hamnar där. Testat: efter besök, botar, DNT, trasig indata och takträffar finns besökarens IP och User-Agent inte i någon tabell i data.db eller auxiliary.db.
#Vad som inte räknas
- Do Not Track (
DNT: 1) och Global Privacy Control (Sec-GPC: 1) — räknas inte alls. - Botar — grov lista över sökmotorer, AI-crawlers, förhandsvisningar (Slack/Discord/Facebook), övervakning,
curl/python/headless-webbläsare, samt allt som inte ser ut som en webbläsare (saknarMozilla/). - Uppblåsning — en och samma besökare räknas högst 300 gånger per dygn (
ZK_STATISTIK_TAK). Högst 2000 olika sidor/referrers/händelser per dygn (ZK_STATISTIK_MAX_NYCKLAR); resten samlas under(övrigt)så att ingen kan fylla databasen med påhittade adresser.
#Inställningar (miljövariabler)
| Variabel | Standard | |
|---|---|---|
ZK_STATISTIK_TAK | 300 | Räknade anrop per besökare och dygn |
ZK_STATISTIK_MAX_NYCKLAR | 2000 | Olika sidor (och referrers, händelser) per dygn |
ZK_STATISTIK_RADATA_DAGAR | 1 | Dagar som hasharna sparas (1 = bara i dag). Saltet raderas alltid efter dygnet oavsett. |
ZK_STATISTIK_AGGREGAT_DAGAR | 0 | 0 = aggregaten sparas för alltid; annars antal dagar |
Gallringen körs varje timme (cron) och kan köras för hand: POST /api/zk/statistik/gallra (superuser).
#Juridik — läs det här
Vi är inte jurister. Modellen ovan är samma som Plausible och liknande verktyg bygger på för att slippa samtyckesbanner: ingen information lagras eller läses i besökarens enhet (ePrivacy-direktivets artikel 5.3 — i Sverige lagen om elektronisk kommunikation 9 kap. 28 §), och det som sparas är aggregat som inte går att knyta till en person.
Men var ärlig mot dig själv om två saker:
- IP-adressen behandlas — den används i minnet för att räkna fram hashen. Det är en behandling av personuppgifter enligt GDPR, även om den inte sparas. Du behöver en rättslig grund (oftast berättigat intresse) och bör nämna statistiken i din integritetspolicy.
- Under dygnet finns saltet och hasharna i databasen. Den som har full åtkomst till servern skulle i teorin kunna testa alla IPv4-adresser mot en känd User-Agent och hitta en träff. Efter dygnet, när saltet är raderat, går det inte.
Bedömningen av om detta räcker för just din tjänst — ePrivacy, GDPR och eventuella nationella regler — ägs av dig som kund. Är du osäker, fråga en jurist.
#Begränsningar
- Dygnen är UTC. Svensk midnatt är kl. 23 eller 22 UTC; ett besök kl. 00:30 svensk tid hamnar på föregående UTC-dag.
- Unika är en uppskattning. Samma person på mobil och dator räknas två gånger; flera personer bakom samma IP med samma webbläsare räknas som en. Byter någon nätverk mitt på dagen räknas hen igen.
- Botfiltret är grovt. En bot som utger sig för att vara Chrome räknas. Den som vill blåsa upp siffrorna med många IP-adresser kan göra det inom taken — taken skyddar databasen, inte siffrornas sanning.
- Bakom en proxy utan Trusted proxy-inställning blir alla besökare en enda (proxyns IP).
- PocketBase inbyggda rate limit: om du slår på den och den slår till för besöksrutten loggas 429-svaret i aktivitetsloggen — med IP. Sätt i så fall
logs.logIPav, eller undanta rutten. - Andra fel före vår kod (t.ex. PocketBase avvisar anropet innan det når rutten) loggas av PocketBase som vanligt. Vill du vara helt säker: stäng av Log IP under Inställningar → Loggar.
- Ingen geografi (land/stad) och ingen enhetstyp — det skulle kräva att mer av besökaren behandlas.
- Inget färdigt diagram/dashboard; du får JSON att visa som du vill.
- Inte prestandamätt under hög last (varje besök är en liten skrivtransaktion i SQLite).
#Testa
./bank.py statistik
12 tester: samma besökare samma dag = 1 unik (annan IP/webbläsare = ny), nästa dag = ny besökare och gårdagens salt raderat (dagen och saltet manipuleras i databasen), att byte av salt ensamt räcker för en ny hash, att IP och User-Agent inte finns i någon tabell i någon databasfil (med en kontroll som bevisar att sökningen faktiskt ser PocketBase-loggen), DNT/GPC, botar, att bara superuser läser, taket per besökare, sidor/referrers/händelser (frågesträngar bort, intern navigering), datumintervall, gallring av rådata med aggregaten kvar, tak på antal sidor, trasig indata. Skydden är mutantverifierade: varje skydd har brutits med flit och fångats av minst ett test.