From 6eb79ea45294fe87b98af26d7167e8524ac59b53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20N=C3=A4geli?= Date: Wed, 14 Oct 2026 13:41:26 +0200 Subject: [PATCH] Count a visitor only when they opened a page A visitor was counted with the first request of the day from anything that named a browser, page or not, so scanners that send a browser's agent and guess paths (/contact, /staff, /.well-known/security.txt) counted as visitors without opening anything. genhttp.org showed 1,408 visitors against 1,244 page views. A visitor is now a person who opened a page, counted with the first page they opened that day, so there are never more visitors than page views. Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 5 ++++ README.md | 6 +++-- .../src/locales/de/editor/analytics.tsx | 2 +- src/Frontend/src/locales/de/guide.tsx | 2 +- .../src/locales/en/editor/analytics.tsx | 2 +- src/Frontend/src/locales/en/guide.tsx | 2 +- .../src/locales/es/editor/analytics.tsx | 2 +- src/Frontend/src/locales/es/guide.tsx | 10 +++---- .../src/locales/fr/editor/analytics.tsx | 2 +- src/Frontend/src/locales/fr/guide.tsx | 2 +- .../src/locales/id/editor/analytics.tsx | 2 +- src/Frontend/src/locales/id/guide.tsx | 2 +- .../src/locales/it/editor/analytics.tsx | 2 +- src/Frontend/src/locales/it/guide.tsx | 11 ++++---- .../src/locales/ja/editor/analytics.tsx | 2 +- src/Frontend/src/locales/ja/guide.tsx | 2 +- .../src/locales/ko/editor/analytics.tsx | 2 +- src/Frontend/src/locales/ko/guide.tsx | 2 +- .../src/locales/nl/editor/analytics.tsx | 2 +- src/Frontend/src/locales/nl/guide.tsx | 10 +++---- .../src/locales/pl/editor/analytics.tsx | 2 +- src/Frontend/src/locales/pl/guide.tsx | 10 +++---- .../src/locales/pt-pt/editor/analytics.tsx | 2 +- src/Frontend/src/locales/pt-pt/guide.tsx | 10 +++---- .../src/locales/pt/editor/analytics.tsx | 2 +- src/Frontend/src/locales/pt/guide.tsx | 10 +++---- .../src/locales/tr/editor/analytics.tsx | 2 +- src/Frontend/src/locales/tr/guide.tsx | 4 +-- src/GenHTTP.Lambda/Api/Mcp/McpTools.cs | 4 +-- .../Services/Analytics/VisitTally.cs | 8 +++--- .../Analytics/AnalyticsTests.cs | 27 +++++++++++++++++++ 31 files changed, 95 insertions(+), 58 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index eff0e13..d5607ba 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -475,6 +475,11 @@ section. and Chromium's client hints, and a value the sender wrote is only taken where it is one of a known few. Matching stays linear in the length of an agent: no backtracking expression ever reads one. +- **A visitor is a person who opened a page - decided by the owner.** One + the classifier takes for a browser and whose visit was a page view, + counted once a day with the first page they opened. A request for anything + else - a script, an API call, `robots.txt`, a path that was not there - + makes nobody a visitor, so there are never more visitors than page views. - **Nothing that names a visitor is kept.** Counts only; the referrer as its host; visitors told apart for a day by a hash under a random key per day and lambda, held in memory and never written. No cookie, no script in the diff --git a/README.md b/README.md index 850b811..574d13a 100644 --- a/README.md +++ b/README.md @@ -865,8 +865,10 @@ feature's preview. 500 characters of an agent are read. - **Visitors without keeping anybody.** A visitor is their address and their agent, hashed with a key made at random per day and lambda, held in memory - for today and yesterday only (`DailyVisitors`), and only a person is one. - So a visitor counts once a day; a restart counts somebody again who comes back the same day, and an + for today and yesterday only (`DailyVisitors`), and only a person who + opened a page is one: a request for anything else - a script, an API call, + a path a scanner guessed - makes nobody a visitor, whatever agent it names, + so there are never more visitors than page views. So a visitor counts once a day; a restart counts somebody again who comes back the same day, and an installation that keeps no addresses (`LAMBDA_LOG_CLIENT_ADDRESS=false`) counts no visitors. No cookie, no script in the lambda. - **A database of its own.** `analytics.db` beside `lambda.db`, a star: diff --git a/src/Frontend/src/locales/de/editor/analytics.tsx b/src/Frontend/src/locales/de/editor/analytics.tsx index d6bfe27..f6f2c7c 100644 --- a/src/Frontend/src/locales/de/editor/analytics.tsx +++ b/src/Frontend/src/locales/de/editor/analytics.tsx @@ -24,7 +24,7 @@ export const analytics: EditorMessages['analytics'] = { premiumRange: 'Premium zeigt bis zu einem Jahr', visitors: 'Besucher', - visitorsHint: 'Verschiedene Personen, jede einmal pro Tag gezählt. Suchmaschinen und andere Programme sind keine Besucher.', + visitorsHint: 'Verschiedene Personen, die eine Seite geöffnet haben, jede einmal pro Tag gezählt. Suchmaschinen und andere Programme sind keine Besucher.', views: 'Seitenaufrufe', viewsHint: 'Jedes Mal, wenn jemand eine Seite geöffnet hat.', hits: 'Requests', diff --git a/src/Frontend/src/locales/de/guide.tsx b/src/Frontend/src/locales/de/guide.tsx index 43f4e84..4709d81 100644 --- a/src/Frontend/src/locales/de/guide.tsx +++ b/src/Frontend/src/locales/de/guide.tsx @@ -518,7 +518,7 @@ export const guide: Messages['guide'] = { <> Gezählt wird von der Plattform, vor Ihrer App – Ihre App braucht dafür nichts: keine Cookies, kein Script, kein Banner, das Ihre Besucher akzeptieren müssten. Gespeichert werden nur Zahlen pro Tag – nie eine Adresse, und von der Seite, von - der jemand kam, nur die Website. Ein Besucher ist eine Person, einmal pro Tag gezählt: Suchmaschinen, KI-Crawler und + der jemand kam, nur die Website. Ein Besucher ist eine Person, die eine Seite geöffnet hat, einmal pro Tag gezählt: Suchmaschinen, KI-Crawler und andere Programme werden getrennt gezählt, nach Art, und Google, Bing, OpenAI, Apple und Perplexity nur dann mit Namen, wenn der Besuch von einer Adresse kam, die sie veröffentlichen – jeder kann behaupten, sie zu sein. Ein Jahr wird aufbewahrt; eine kostenlose App zeigt davon die letzten sieben Tage und {k.b('Premium')} alles, auch was vorher gezählt diff --git a/src/Frontend/src/locales/en/editor/analytics.tsx b/src/Frontend/src/locales/en/editor/analytics.tsx index 2ebf492..9e812bd 100644 --- a/src/Frontend/src/locales/en/editor/analytics.tsx +++ b/src/Frontend/src/locales/en/editor/analytics.tsx @@ -30,7 +30,7 @@ export const analytics = { /** The figures at the top; each one picks what the chart below shows. */ visitors: 'Visitors', - visitorsHint: 'Different people, each counted once a day. Search engines and other programs are not visitors.', + visitorsHint: 'Different people who opened a page, each counted once a day. Search engines and other programs are not visitors.', views: 'Page views', viewsHint: 'Every time somebody opened a page.', hits: 'Requests', diff --git a/src/Frontend/src/locales/en/guide.tsx b/src/Frontend/src/locales/en/guide.tsx index 65c5d2d..8719dd9 100644 --- a/src/Frontend/src/locales/en/guide.tsx +++ b/src/Frontend/src/locales/en/guide.tsx @@ -514,7 +514,7 @@ export const guide = { <> It is counted by the platform, in front of your app, so your app needs nothing for it: no cookies, no script, no banner for your visitors to accept. Only counts per day are kept - never an address, and of the page somebody - came from only the site. A visitor is a person, counted once a day: search engines, AI crawlers and other + came from only the site. A visitor is a person who opened a page, counted once a day: search engines, AI crawlers and other programs are counted apart, by what they are, and Google, Bing, OpenAI, Apple and Perplexity only by name when the visit came from an address they publish - anybody can claim to be them. A year is kept; a free app shows the last seven days of it and {k.b('Premium')} all of it, what was counted before included. Switching it off deletes what was diff --git a/src/Frontend/src/locales/es/editor/analytics.tsx b/src/Frontend/src/locales/es/editor/analytics.tsx index b348612..847195e 100644 --- a/src/Frontend/src/locales/es/editor/analytics.tsx +++ b/src/Frontend/src/locales/es/editor/analytics.tsx @@ -24,7 +24,7 @@ export const analytics: EditorMessages['analytics'] = { premiumRange: 'Premium muestra hasta un año', visitors: 'Visitantes', - visitorsHint: 'Personas distintas, cada una contada una vez al día. Los buscadores y otros programas no son visitantes.', + visitorsHint: 'Personas distintas que abrieron una página, cada una contada una vez al día. Los buscadores y otros programas no son visitantes.', views: 'Visualizaciones de página', viewsHint: 'Cada vez que alguien abrió una página.', hits: 'Peticiones', diff --git a/src/Frontend/src/locales/es/guide.tsx b/src/Frontend/src/locales/es/guide.tsx index 2ac92b7..6833123 100644 --- a/src/Frontend/src/locales/es/guide.tsx +++ b/src/Frontend/src/locales/es/guide.tsx @@ -508,11 +508,11 @@ export const guide: Messages['guide'] = { <> La cuenta la lleva la plataforma, delante de tu app, así que tu app no necesita nada para ello: ni cookies, ni scripts, ni avisos que tus visitantes tengan que aceptar. Solo se guardan recuentos por día, nunca una dirección, - y de la página de la que alguien viene solo se guarda el sitio. Un visitante es una persona, contada una vez al - día: los buscadores, los rastreadores de IA y otros programas se cuentan aparte, según lo que son, y Google, Bing, - OpenAI, Apple y Perplexity solo se nombran cuando la visita llegó desde una dirección que ellos publican: cualquiera - puede decir que es uno de ellos. Se guarda un año: una app gratuita muestra los últimos siete días y {k.b('Premium')} - todo, incluido lo contado antes. Al desactivarla se borra lo contado. + y de la página de la que alguien viene solo se guarda el sitio. Un visitante es una persona que abrió una página, + contada una vez al día: los buscadores, los rastreadores de IA y otros programas se cuentan aparte, según lo que + son, y Google, Bing, OpenAI, Apple y Perplexity solo se nombran cuando la visita llegó desde una dirección que + ellos publican: cualquiera puede decir que es uno de ellos. Se guarda un año: una app gratuita muestra los últimos + siete días y {k.b('Premium')} todo, incluido lo contado antes. Al desactivarla se borra lo contado. ), visitorsAside: diff --git a/src/Frontend/src/locales/fr/editor/analytics.tsx b/src/Frontend/src/locales/fr/editor/analytics.tsx index 493f3f7..ef7120a 100644 --- a/src/Frontend/src/locales/fr/editor/analytics.tsx +++ b/src/Frontend/src/locales/fr/editor/analytics.tsx @@ -24,7 +24,7 @@ export const analytics: EditorMessages['analytics'] = { premiumRange: 'Premium affiche jusqu’à un an', visitors: 'Visiteurs', - visitorsHint: 'Personnes différentes, chacune comptée une fois par jour. Les moteurs de recherche et les autres programmes ne sont pas des visiteurs.', + visitorsHint: 'Personnes différentes ayant ouvert une page, chacune comptée une fois par jour. Les moteurs de recherche et les autres programmes ne sont pas des visiteurs.', views: 'Pages vues', viewsHint: 'Chaque fois que quelqu’un a ouvert une page.', hits: 'Requêtes', diff --git a/src/Frontend/src/locales/fr/guide.tsx b/src/Frontend/src/locales/fr/guide.tsx index 11689e3..2f86b5b 100644 --- a/src/Frontend/src/locales/fr/guide.tsx +++ b/src/Frontend/src/locales/fr/guide.tsx @@ -521,7 +521,7 @@ export const guide: Messages['guide'] = { Le comptage est fait par la plateforme, en amont de votre application, qui n’a donc besoin de rien : pas de cookies, pas de script, pas de bannière à faire accepter à vos visiteurs. Seuls des totaux par jour sont conservés - jamais une adresse, et de la page d’où quelqu’un est venu, seulement le site. Un visiteur est une - personne, comptée une fois par jour : les moteurs de recherche, les robots d’exploration IA et les autres + personne ayant ouvert une page, comptée une fois par jour : les moteurs de recherche, les robots d’exploration IA et les autres programmes sont comptés à part, selon ce qu’ils sont, et Google, Bing, OpenAI, Apple et Perplexity ne sont nommés que si la visite provient d’une adresse qu’ils publient : n’importe qui peut se faire passer pour eux. Un an est conservé ; une application gratuite en montre les sept derniers jours et{' '} diff --git a/src/Frontend/src/locales/id/editor/analytics.tsx b/src/Frontend/src/locales/id/editor/analytics.tsx index 0b7d6b2..e45d438 100644 --- a/src/Frontend/src/locales/id/editor/analytics.tsx +++ b/src/Frontend/src/locales/id/editor/analytics.tsx @@ -24,7 +24,7 @@ export const analytics: EditorMessages['analytics'] = { premiumRange: 'Premium menampilkan hingga setahun', visitors: 'Pengunjung', - visitorsHint: 'Orang yang berbeda, masing-masing dihitung sekali sehari. Mesin pencari dan program lain bukan pengunjung.', + visitorsHint: 'Orang yang berbeda yang membuka sebuah halaman, masing-masing dihitung sekali sehari. Mesin pencari dan program lain bukan pengunjung.', views: 'Tampilan halaman', viewsHint: 'Setiap kali seseorang membuka sebuah halaman.', hits: 'Request', diff --git a/src/Frontend/src/locales/id/guide.tsx b/src/Frontend/src/locales/id/guide.tsx index 4a0487e..87b94e6 100644 --- a/src/Frontend/src/locales/id/guide.tsx +++ b/src/Frontend/src/locales/id/guide.tsx @@ -519,7 +519,7 @@ export const guide: Messages['guide'] = { Penghitungan dilakukan oleh platform, di depan aplikasi Anda, sehingga aplikasi Anda tidak memerlukan apa pun untuk itu: tanpa cookie, tanpa skrip, tanpa banner yang harus disetujui pengunjung Anda. Hanya jumlah per hari yang disimpan - tidak pernah alamat, dan dari halaman asal seorang pengunjung hanya situsnya. Pengunjung adalah - seseorang yang dihitung sekali sehari: mesin pencari, crawler AI, dan program lain dihitung terpisah menurut + seseorang yang membuka sebuah halaman, dihitung sekali sehari: mesin pencari, crawler AI, dan program lain dihitung terpisah menurut jenisnya, dan Google, Bing, OpenAI, Apple, dan Perplexity hanya disebut namanya jika kunjungan berasal dari alamat yang mereka publikasikan - siapa pun bisa mengaku sebagai mereka. Data disimpan selama setahun; aplikasi gratis menampilkan tujuh hari terakhir darinya dan {k.b('Premium')} menampilkan semuanya, termasuk yang dihitung diff --git a/src/Frontend/src/locales/it/editor/analytics.tsx b/src/Frontend/src/locales/it/editor/analytics.tsx index 035f098..c0a83f8 100644 --- a/src/Frontend/src/locales/it/editor/analytics.tsx +++ b/src/Frontend/src/locales/it/editor/analytics.tsx @@ -24,7 +24,7 @@ export const analytics: EditorMessages['analytics'] = { premiumRange: 'Premium mostra fino a un anno', visitors: 'Visitatori', - visitorsHint: 'Persone diverse, ognuna contata una volta al giorno. I motori di ricerca e gli altri programmi non sono visitatori.', + visitorsHint: 'Persone diverse che hanno aperto una pagina, ognuna contata una volta al giorno. I motori di ricerca e gli altri programmi non sono visitatori.', views: 'Visualizzazioni di pagina', viewsHint: 'Ogni volta che qualcuno ha aperto una pagina.', hits: 'Richieste', diff --git a/src/Frontend/src/locales/it/guide.tsx b/src/Frontend/src/locales/it/guide.tsx index 414c497..d6e5ec8 100644 --- a/src/Frontend/src/locales/it/guide.tsx +++ b/src/Frontend/src/locales/it/guide.tsx @@ -509,11 +509,12 @@ export const guide: Messages['guide'] = { <> Lo conta la piattaforma, davanti alla tua app, quindi alla tua app non serve nulla: niente cookie, niente script, nessun banner che i visitatori debbano accettare. Si conservano solo i conteggi per giorno: mai un indirizzo e, - della pagina da cui qualcuno è arrivato, solo il sito. Un visitatore è una persona, contata una volta al giorno: i - motori di ricerca, i crawler IA e gli altri programmi vengono contati a parte, per ciò che sono, e Google, Bing, - OpenAI, Apple e Perplexity solo per nome quando la visita arriva da un indirizzo che pubblicano: chiunque può - dichiararsi uno di loro. Si conserva un anno: un’app gratuita ne mostra gli ultimi sette giorni e {k.b('Premium')} - tutto, anche ciò che è stato contato prima. Disattivandolo, ciò che è stato contato viene eliminato. + della pagina da cui qualcuno è arrivato, solo il sito. Un visitatore è una persona che ha aperto una pagina, contata + una volta al giorno: i motori di ricerca, i crawler IA e gli altri programmi vengono contati a parte, per ciò che + sono, e Google, Bing, OpenAI, Apple e Perplexity solo per nome quando la visita arriva da un indirizzo che + pubblicano: chiunque può dichiararsi uno di loro. Si conserva un anno: un’app gratuita ne mostra gli ultimi sette + giorni e {k.b('Premium')} tutto, anche ciò che è stato contato prima. Disattivandolo, ciò che è stato contato viene + eliminato. )), visitorsAside: diff --git a/src/Frontend/src/locales/ja/editor/analytics.tsx b/src/Frontend/src/locales/ja/editor/analytics.tsx index c052204..6375a1e 100644 --- a/src/Frontend/src/locales/ja/editor/analytics.tsx +++ b/src/Frontend/src/locales/ja/editor/analytics.tsx @@ -28,7 +28,7 @@ export const analytics: EditorMessages['analytics'] = { /** The figures at the top; each one picks what the chart below shows. */ visitors: '訪問者', - visitorsHint: 'ユニークな訪問者数です。同じ人は1日に1回だけ数えます。検索エンジンやその他のプログラムは訪問者に含めません。', + visitorsHint: 'ページを開いたユニークな訪問者数です。同じ人は1日に1回だけ数えます。検索エンジンやその他のプログラムは訪問者に含めません。', views: 'ページビュー', viewsHint: 'ページが開かれた回数です。', hits: 'リクエスト', diff --git a/src/Frontend/src/locales/ja/guide.tsx b/src/Frontend/src/locales/ja/guide.tsx index 426b8c8..67e2e7f 100644 --- a/src/Frontend/src/locales/ja/guide.tsx +++ b/src/Frontend/src/locales/ja/guide.tsx @@ -471,7 +471,7 @@ export const guide: Messages['guide'] = { )), visitors2: ((k) => ( <> - 集計はアプリの手前でプラットフォームが行うので、アプリ側で必要なことはありません。Cookieも、スクリプトも、訪問者が同意するバナーも不要です。保存するのは日ごとの数だけで、IPアドレスは保存せず、訪問元のページについてもサイトだけを記録します。訪問者とは人のことで、1日に1回だけ数えます。検索エンジン、AIクローラー、その他のプログラムは、種類別に分けて別に集計します。Google、Bing、OpenAI、Apple、Perplexityは、それぞれが公開しているアドレスからの訪問だった場合にのみ名前で表示します。誰でもこれらを名乗れるためです。保存期間は1年で、無料のアプリでは直近7日間を表示し、{k.b('プレミアム')}ならそれ以前に集計した分も含めてすべて表示します。オフにすると、集計したものは削除されます。 + 集計はアプリの手前でプラットフォームが行うので、アプリ側で必要なことはありません。Cookieも、スクリプトも、訪問者が同意するバナーも不要です。保存するのは日ごとの数だけで、IPアドレスは保存せず、訪問元のページについてもサイトだけを記録します。訪問者とは、ページを開いた人のことで、1日に1回だけ数えます。検索エンジン、AIクローラー、その他のプログラムは、種類別に分けて別に集計します。Google、Bing、OpenAI、Apple、Perplexityは、それぞれが公開しているアドレスからの訪問だった場合にのみ名前で表示します。誰でもこれらを名乗れるためです。保存期間は1年で、無料のアプリでは直近7日間を表示し、{k.b('プレミアム')}ならそれ以前に集計した分も含めてすべて表示します。オフにすると、集計したものは削除されます。 )), visitorsAside: diff --git a/src/Frontend/src/locales/ko/editor/analytics.tsx b/src/Frontend/src/locales/ko/editor/analytics.tsx index ca49d5b..8be5f54 100644 --- a/src/Frontend/src/locales/ko/editor/analytics.tsx +++ b/src/Frontend/src/locales/ko/editor/analytics.tsx @@ -24,7 +24,7 @@ export const analytics: EditorMessages['analytics'] = { premiumRange: '프리미엄에서는 최대 1년까지 볼 수 있어요', visitors: '방문자', - visitorsHint: '서로 다른 사람을 하루에 한 번씩 세어요. 검색 엔진과 그 밖의 프로그램은 방문자가 아니에요.', + visitorsHint: '페이지를 연 서로 다른 사람을 하루에 한 번씩 세어요. 검색 엔진과 그 밖의 프로그램은 방문자가 아니에요.', views: '페이지 조회수', viewsHint: '누군가 페이지를 열 때마다 세어요.', hits: '요청', diff --git a/src/Frontend/src/locales/ko/guide.tsx b/src/Frontend/src/locales/ko/guide.tsx index 84016c6..e5308e4 100644 --- a/src/Frontend/src/locales/ko/guide.tsx +++ b/src/Frontend/src/locales/ko/guide.tsx @@ -494,7 +494,7 @@ export const guide: Messages['guide'] = { <> 집계는 플랫폼이 앱 앞에서 하기 때문에 앱에는 아무것도 필요 없어요. 쿠키도, 스크립트도, 방문자가 동의할 배너도 없어요. 하루 단위의 횟수만 저장하고, 주소는 저장하지 않으며, 방문자가 어디에서 왔는지도 사이트만 - 남겨요. 방문자는 한 사람을 뜻하고 하루에 한 번만 세어요. 검색 엔진, AI 크롤러, 그 밖의 프로그램은 종류별로 + 남겨요. 방문자는 페이지를 연 사람을 뜻하고 하루에 한 번만 세어요. 검색 엔진, AI 크롤러, 그 밖의 프로그램은 종류별로 따로 세고, Google, Bing, OpenAI, Apple, Perplexity는 해당 업체가 공개한 주소에서 방문했을 때만 이름으로 표시해요. 누구나 이들을 사칭할 수 있기 때문이에요. 1년 치를 보관하고, 무료 앱은 그중 최근 7일을, {k.b('프리미엄')}은 그 전에 집계한 것까지 전부 보여 줘요. 끄면 집계한 내용이 삭제돼요. diff --git a/src/Frontend/src/locales/nl/editor/analytics.tsx b/src/Frontend/src/locales/nl/editor/analytics.tsx index 77ff6df..e768393 100644 --- a/src/Frontend/src/locales/nl/editor/analytics.tsx +++ b/src/Frontend/src/locales/nl/editor/analytics.tsx @@ -24,7 +24,7 @@ export const analytics: EditorMessages['analytics'] = { premiumRange: 'Premium toont tot een jaar', visitors: 'Bezoekers', - visitorsHint: 'Verschillende mensen, elk één keer per dag geteld. Zoekmachines en andere programma’s zijn geen bezoekers.', + visitorsHint: 'Verschillende mensen die een pagina hebben geopend, elk één keer per dag geteld. Zoekmachines en andere programma’s zijn geen bezoekers.', views: 'Paginaweergaven', viewsHint: 'Elke keer dat iemand een pagina opende.', hits: 'Requests', diff --git a/src/Frontend/src/locales/nl/guide.tsx b/src/Frontend/src/locales/nl/guide.tsx index d8803ef..ba8d5cd 100644 --- a/src/Frontend/src/locales/nl/guide.tsx +++ b/src/Frontend/src/locales/nl/guide.tsx @@ -517,11 +517,11 @@ export const guide: Messages['guide'] = { <> Het wordt geteld door het platform, vóór je app, dus je app heeft er niets voor nodig: geen cookies, geen script, geen banner die je bezoekers moeten accepteren. Alleen aantallen per dag worden bewaard – nooit een adres, en van - de pagina waar iemand vandaan kwam alleen de site. Een bezoeker is een persoon, één keer per dag geteld: - zoekmachines, AI-crawlers en andere programma’s worden apart geteld, per soort, en Google, Bing, OpenAI, Apple en - Perplexity alleen bij naam als het bezoek van een door hen gepubliceerd adres kwam – iedereen kan zich als hen - voordoen. Een jaar wordt bewaard; een gratis app toont daarvan de laatste zeven dagen en {k.b('Premium')} alles, - ook wat eerder geteld is. Als je het uitzet, wordt wat geteld was verwijderd. + de pagina waar iemand vandaan kwam alleen de site. Een bezoeker is een persoon die een pagina heeft geopend, één + keer per dag geteld: zoekmachines, AI-crawlers en andere programma’s worden apart geteld, per soort, en Google, + Bing, OpenAI, Apple en Perplexity alleen bij naam als het bezoek van een door hen gepubliceerd adres kwam – + iedereen kan zich als hen voordoen. Een jaar wordt bewaard; een gratis app toont daarvan de laatste zeven dagen en + {k.b('Premium')} alles, ook wat eerder geteld is. Als je het uitzet, wordt wat geteld was verwijderd. )), visitorsAside: diff --git a/src/Frontend/src/locales/pl/editor/analytics.tsx b/src/Frontend/src/locales/pl/editor/analytics.tsx index 745ce7d..9e93478 100644 --- a/src/Frontend/src/locales/pl/editor/analytics.tsx +++ b/src/Frontend/src/locales/pl/editor/analytics.tsx @@ -24,7 +24,7 @@ export const analytics: EditorMessages['analytics'] = { premiumRange: 'Premium pokazuje do roku', visitors: 'Odwiedzający', - visitorsHint: 'Różne osoby, każda liczona raz dziennie. Wyszukiwarki i inne programy nie są odwiedzającymi.', + visitorsHint: 'Różne osoby, które otworzyły stronę, każda liczona raz dziennie. Wyszukiwarki i inne programy nie są odwiedzającymi.', views: 'Odsłony stron', viewsHint: 'Za każdym razem, gdy ktoś otworzył stronę.', hits: 'Żądania', diff --git a/src/Frontend/src/locales/pl/guide.tsx b/src/Frontend/src/locales/pl/guide.tsx index 95cd882..b1703c6 100644 --- a/src/Frontend/src/locales/pl/guide.tsx +++ b/src/Frontend/src/locales/pl/guide.tsx @@ -503,11 +503,11 @@ export const guide: Messages['guide'] = { <> Liczy to platforma, przed twoją aplikacją, więc aplikacja nie potrzebuje do tego niczego: ani plików cookie, ani skryptu, ani banera do zaakceptowania przez odwiedzających. Przechowujemy tylko liczby dzień po dniu – nigdy - adresu, a ze strony, z której ktoś przyszedł, tylko nazwę witryny. Odwiedzający to osoba, liczona raz dziennie: - wyszukiwarki, roboty AI i inne programy są liczone osobno, według tego, czym są, a Google, Bing, OpenAI, Apple - i Perplexity tylko z nazwy, gdy wizyta przyszła z adresu, który publikują – każdy może się za nie podać. - Dane są przechowywane przez rok; bezpłatna aplikacja pokazuje z nich ostatnie siedem dni, a {k.b('Premium')} - całość, także to, co policzono wcześniej. Wyłączenie analityki usuwa to, co policzono. + adresu, a ze strony, z której ktoś przyszedł, tylko nazwę witryny. Odwiedzający to osoba, która otworzyła stronę, + liczona raz dziennie: wyszukiwarki, roboty AI i inne programy są liczone osobno, według tego, czym są, a Google, + Bing, OpenAI, Apple i Perplexity tylko z nazwy, gdy wizyta przyszła z adresu, który publikują – każdy może się za + nie podać. Dane są przechowywane przez rok; bezpłatna aplikacja pokazuje z nich ostatnie siedem dni, a + {k.b('Premium')} całość, także to, co policzono wcześniej. Wyłączenie analityki usuwa to, co policzono. ), visitorsAside: diff --git a/src/Frontend/src/locales/pt-pt/editor/analytics.tsx b/src/Frontend/src/locales/pt-pt/editor/analytics.tsx index f65d497..c08cead 100644 --- a/src/Frontend/src/locales/pt-pt/editor/analytics.tsx +++ b/src/Frontend/src/locales/pt-pt/editor/analytics.tsx @@ -24,7 +24,7 @@ export const analytics: EditorMessages['analytics'] = { premiumRange: 'O Premium mostra até um ano', visitors: 'Visitantes', - visitorsHint: 'Pessoas diferentes, cada uma contada uma vez por dia. Motores de pesquisa e outros programas não são visitantes.', + visitorsHint: 'Pessoas diferentes que abriram uma página, cada uma contada uma vez por dia. Motores de pesquisa e outros programas não são visitantes.', views: 'Visualizações de páginas', viewsHint: 'Cada vez que alguém abriu uma página.', hits: 'Pedidos', diff --git a/src/Frontend/src/locales/pt-pt/guide.tsx b/src/Frontend/src/locales/pt-pt/guide.tsx index 1aef636..1665eaf 100644 --- a/src/Frontend/src/locales/pt-pt/guide.tsx +++ b/src/Frontend/src/locales/pt-pt/guide.tsx @@ -507,11 +507,11 @@ export const guide: Messages['guide'] = { <> É contado pela plataforma, à frente da tua app, por isso a tua app não precisa de nada para isso: sem cookies, sem script e sem banner para os teus visitantes aceitarem. Só se guardam contagens por dia - nunca um endereço - e, da página de onde alguém veio, apenas o site. Um visitante é uma pessoa, contada uma vez por dia: motores de - pesquisa, crawlers de IA e outros programas são contados à parte, por aquilo que são, e Google, Bing, OpenAI, - Apple e Perplexity só pelo nome quando a visita veio de um endereço que eles publicam - qualquer um pode - afirmar que é um deles. Guarda-se um ano; uma app gratuita mostra os últimos sete dias e o {k.b('Premium')} - mostra tudo, incluindo o que foi contado antes. Ao desligar, apaga-se o que foi contado. + e, da página de onde alguém veio, apenas o site. Um visitante é uma pessoa que abriu uma página, contada uma vez + por dia: motores de pesquisa, crawlers de IA e outros programas são contados à parte, por aquilo que são, e + Google, Bing, OpenAI, Apple e Perplexity só pelo nome quando a visita veio de um endereço que eles publicam - + qualquer um pode afirmar que é um deles. Guarda-se um ano; uma app gratuita mostra os últimos sete dias e o + {k.b('Premium')} mostra tudo, incluindo o que foi contado antes. Ao desligar, apaga-se o que foi contado. )), visitorsAside: diff --git a/src/Frontend/src/locales/pt/editor/analytics.tsx b/src/Frontend/src/locales/pt/editor/analytics.tsx index 48dbe51..f581f29 100644 --- a/src/Frontend/src/locales/pt/editor/analytics.tsx +++ b/src/Frontend/src/locales/pt/editor/analytics.tsx @@ -24,7 +24,7 @@ export const analytics: EditorMessages['analytics'] = { premiumRange: 'O Premium mostra até um ano', visitors: 'Visitantes', - visitorsHint: 'Pessoas diferentes, cada uma contada uma vez por dia. Mecanismos de busca e outros programas não são visitantes.', + visitorsHint: 'Pessoas diferentes que abriram uma página, cada uma contada uma vez por dia. Mecanismos de busca e outros programas não são visitantes.', views: 'Visualizações de página', viewsHint: 'Cada vez que alguém abriu uma página.', hits: 'Requisições', diff --git a/src/Frontend/src/locales/pt/guide.tsx b/src/Frontend/src/locales/pt/guide.tsx index 4ecae0e..78ea392 100644 --- a/src/Frontend/src/locales/pt/guide.tsx +++ b/src/Frontend/src/locales/pt/guide.tsx @@ -509,11 +509,11 @@ export const guide: Messages['guide'] = { <> A contagem é feita pela plataforma, na frente do seu app, então ele não precisa de nada para isso: sem cookies, sem script, sem banner para os seus visitantes aceitarem. Só as contagens por dia são guardadas - nunca um - endereço, e da página de onde alguém veio, apenas o site. Um visitante é uma pessoa, contada uma vez por dia: - mecanismos de busca, rastreadores de IA e outros programas são contados à parte, conforme o que são, e Google, - Bing, OpenAI, Apple e Perplexity só pelo nome quando a visita veio de um endereço que eles publicam - qualquer - um pode dizer que é eles. Um ano fica guardado; um app gratuito mostra os últimos sete dias dele e o {k.b('Premium')} - mostra tudo, inclusive o que foi contado antes. Desativar apaga o que foi contado. + endereço, e da página de onde alguém veio, apenas o site. Um visitante é uma pessoa que abriu uma página, contada + uma vez por dia: mecanismos de busca, rastreadores de IA e outros programas são contados à parte, conforme o que + são, e Google, Bing, OpenAI, Apple e Perplexity só pelo nome quando a visita veio de um endereço que eles + publicam - qualquer um pode dizer que é eles. Um ano fica guardado; um app gratuito mostra os últimos sete dias + dele e o {k.b('Premium')} mostra tudo, inclusive o que foi contado antes. Desativar apaga o que foi contado. )), visitorsAside: diff --git a/src/Frontend/src/locales/tr/editor/analytics.tsx b/src/Frontend/src/locales/tr/editor/analytics.tsx index beed438..f17f1bf 100644 --- a/src/Frontend/src/locales/tr/editor/analytics.tsx +++ b/src/Frontend/src/locales/tr/editor/analytics.tsx @@ -24,7 +24,7 @@ export const analytics: EditorMessages['analytics'] = { premiumRange: 'Premium bir yıla kadar gösterir', visitors: 'Ziyaretçiler', - visitorsHint: 'Farklı kişiler, her biri günde bir kez sayılır. Arama motorları ve diğer programlar ziyaretçi sayılmaz.', + visitorsHint: 'Bir sayfa açan farklı kişiler, her biri günde bir kez sayılır. Arama motorları ve diğer programlar ziyaretçi sayılmaz.', views: 'Sayfa görüntülemeleri', viewsHint: 'Birinin bir sayfayı her açışı.', hits: 'İstekler', diff --git a/src/Frontend/src/locales/tr/guide.tsx b/src/Frontend/src/locales/tr/guide.tsx index 053b4f3..7753784 100644 --- a/src/Frontend/src/locales/tr/guide.tsx +++ b/src/Frontend/src/locales/tr/guide.tsx @@ -514,8 +514,8 @@ export const guide: Messages['guide'] = { <> Sayımı platform yapar, uygulamanızın önünde; bu yüzden uygulamanızın bunun için hiçbir şeye ihtiyacı yoktur: çerez yok, script yok, ziyaretçilerinizin kabul etmesi gereken banner yok. Yalnızca günlük sayılar saklanır - hiçbir - adres saklanmaz, birinin geldiği sayfadan da yalnızca site saklanır. Ziyaretçi bir kişidir ve günde bir kez - sayılır: arama motorları, yapay zekâ tarayıcıları ve diğer programlar ne olduklarına göre ayrıca sayılır; Google, + adres saklanmaz, birinin geldiği sayfadan da yalnızca site saklanır. Ziyaretçi, bir sayfa açan kişidir ve günde bir + kez sayılır: arama motorları, yapay zekâ tarayıcıları ve diğer programlar ne olduklarına göre ayrıca sayılır; Google, Bing, OpenAI, Apple ve Perplexity ise yalnızca ziyaret, yayımladıkları bir adresten geldiyse adıyla sayılır - herkes onlardan biri olduğunu iddia edebilir. Bir yıl saklanır; ücretsiz bir uygulama bunun son yedi gününü, {k.b('Premium')} ise öncesinde sayılanlar dahil tamamını gösterir. Analitiği kapatmak, sayılanları siler. diff --git a/src/GenHTTP.Lambda/Api/Mcp/McpTools.cs b/src/GenHTTP.Lambda/Api/Mcp/McpTools.cs index 4c1c3b4..22e52de 100644 --- a/src/GenHTTP.Lambda/Api/Mcp/McpTools.cs +++ b/src/GenHTTP.Lambda/Api/Mcp/McpTools.cs @@ -2052,7 +2052,7 @@ private JsonObject Analytics(JsonObject arguments) analytics = AnalyticsResponse.Of(found), note = found.Shown < found.Kept && found.Earliest is { } earliest && earliest < found.From ? $"Counted by day in UTC, written down every {minutes} minutes. More is kept than this tier shows: the owner sees all of the year with premium." - : $"Counted by day in UTC, written down every {minutes} minutes - the visits of the last minutes are not in it yet. Views are pages opened, hits every request; visitors are counted once a day." + : $"Counted by day in UTC, written down every {minutes} minutes - the visits of the last minutes are not in it yet. Views are pages opened, hits every request; visitors are people who opened a page, counted once a day." }); } @@ -2500,7 +2500,7 @@ private JsonObject Guide(string origin) => McpProtocol.Say(new { what = "The owner can have a lambda's visits counted: visitors, pages opened and requests per day, and the pages, referring sites, countries, devices, browsers, operating systems and crawlers behind them. They read it in the editor, and the analytics tool reads the same.", when = "Off until the owner switches it on, in the editor or by asking you (analytics with enable: true). Switching off deletes what was counted, and is theirs to do. A feature's preview is never counted.", - how = "Counted by the platform, in front of the lambda, by day in UTC and written down every few minutes; a year is kept, of which a free lambda shows the last 7 days and a premium one all. Only the host of a referring site is kept, never an address, and a visitor - a person, never a program - is told apart for a day by a hash that is never stored. Crawlers are counted by their kind from a maintained list; Google, Bing, OpenAI, Apple and Perplexity by name only where the visit came from an address they publish. The lambda needs no code for it - do not build a counter of your own for what this counts." + how = "Counted by the platform, in front of the lambda, by day in UTC and written down every few minutes; a year is kept, of which a free lambda shows the last 7 days and a premium one all. Only the host of a referring site is kept, never an address, and a visitor - a person who opened a page, never a program - is told apart for a day by a hash that is never stored. Crawlers are counted by their kind from a maintained list; Google, Bing, OpenAI, Apple and Perplexity by name only where the visit came from an address they publish. The lambda needs no code for it - do not build a counter of your own for what this counts." }, afterDeploying = new { diff --git a/src/GenHTTP.Lambda/Services/Analytics/VisitTally.cs b/src/GenHTTP.Lambda/Services/Analytics/VisitTally.cs index 6603e12..94cb605 100644 --- a/src/GenHTTP.Lambda/Services/Analytics/VisitTally.cs +++ b/src/GenHTTP.Lambda/Services/Analytics/VisitTally.cs @@ -84,9 +84,11 @@ public void Add(LambdaVisit visit) var agent = classifier.Classify(visit.Agent, visit.Hints, visit.Client); - // a visitor is a person, counted with the first thing they asked for - // that day, whatever it was - a crawler opens pages, but is nobody - if (agent.Browser != null && visitors.IsNew(lambda, day, visit.Client, visit.Agent) == true) + // a visitor is a person who opened a page, counted with the first page + // they opened that day - a crawler opens pages, but is nobody, and a + // scanner that names a browser guesses paths, but opens none; so there + // are never more visitors than pages opened + if (visit.Page && agent.Browser != null && visitors.IsNew(lambda, day, visit.Client, visit.Agent) == true) { totals.Visitors++; } diff --git a/tests/GenHTTP.Lambda.Tests/Analytics/AnalyticsTests.cs b/tests/GenHTTP.Lambda.Tests/Analytics/AnalyticsTests.cs index c06b3f3..bc8ef53 100644 --- a/tests/GenHTTP.Lambda.Tests/Analytics/AnalyticsTests.cs +++ b/tests/GenHTTP.Lambda.Tests/Analytics/AnalyticsTests.cs @@ -29,6 +29,8 @@ public sealed class AnalyticsTests private const string Chrome = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36"; + private const string Firefox = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:143.0) Gecko/20100101 Firefox/143.0"; + private const string IPhone = "Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.6 Mobile/15E148 Safari/604.1"; private const string Googlebot = "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"; @@ -216,6 +218,31 @@ public async Task OnlyOpeningAPageIsAView() Assert.IsEmpty(shown.Referrers!, "the lambda referring to itself is nobody coming from elsewhere"); } + [TestMethod] + public async Task AVisitorIsSomebodyWhoOpenedAPage() + { + await using var fixture = await LambdaFixture.CreateAsync(); + + var lambda = await OnlineAsync(fixture, "visited"); + + await EnableAsync(fixture, lambda.PrivateKey); + + // programs that call themselves a browser and open nothing: a script + // fetched on its own, an app calling the API + await VisitAsync(fixture, "http://visited.localhost/app.js", Chrome, destination: "script"); + await VisitAsync(fixture, "http://visited.localhost/api/items", Firefox, destination: "empty"); + + await VisitAsync(fixture, "http://visited.localhost/", IPhone, destination: "document"); + + Flush(fixture); + + var shown = await ReadAsync(fixture, lambda.PrivateKey); + + Assert.AreEqual(3, shown.Totals!.Hits); + Assert.AreEqual(1, shown.Totals.Views); + Assert.AreEqual(1, shown.Totals.Visitors, "only the one who opened a page"); + } + [TestMethod] public async Task AScannerCannotFillADayWithPaths() {