From 0a8ae38debdb6b0bca5447aa92dcc1dabf7575e5 Mon Sep 17 00:00:00 2001 From: Robbe Verhelst Date: Sun, 27 Sep 2026 11:17:58 +0200 Subject: [PATCH] feat: stop counting bots and headless crawlers in Umami Nearly every visitor Umami counted besides the owner was a headless crawler: a stock Chrome, Edge or Safari user agent on a screen of exactly 800x600 (once 1024x1024), with no clicks. The docs now load no Umami script and send no event when navigator.webdriver is true, the user agent names a bot, crawler, spider, headless browser or link preview service, or the screen is exactly 800x600 or 1024x1024. The rule is src/lib/automated.ts, the same as in sluiceway/landing and sluiceway/app. The privacy page gains a section saying automated visitors are not counted, and page feedback from an automated browser says it was not sent. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/ci.yml | 5 +- README.md | 2 +- src/components/Analytics.astro | 2 +- src/components/Feedback.astro | 4 +- src/content/docs/privacy.mdx | 7 +++ src/lib/analytics.ts | 23 +++++++-- src/lib/automated.ts | 38 ++++++++++++++ test/analytics-built.test.ts | 4 ++ test/analytics.test.ts | 48 +++++++++++++++++- test/automated.test.ts | 93 ++++++++++++++++++++++++++++++++++ 10 files changed, 215 insertions(+), 11 deletions(-) create mode 100644 src/lib/automated.ts create mode 100644 test/automated.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 441f5ab..d070189 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -86,8 +86,9 @@ jobs: # Builds with the production address, serves dist/ the way Pages does (scripts/serve.ts) and # runs Lighthouse three times on each URL in lighthouserc.json, as a phone. A median score # under 90 for performance, accessibility, best practices or SEO on any URL fails the job. - # Umami's script loads here as for any reader, but it counts only on docs.sluiceway.dev, so - # these runs on localhost send nothing. + # Umami's script does not load here: Lighthouse names itself in its user agent, so the docs + # treat it as automated (src/lib/automated.ts). It would count only on docs.sluiceway.dev + # anyway. runs-on: ubuntu-latest timeout-minutes: 15 steps: diff --git a/README.md b/README.md index 20f9331..9008e7a 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ The reports are in `.lighthouseci/`. Set `CHROME_PATH` if Lighthouse cannot find ## Analytics -The Umami website id is `DEFAULT_UMAMI_WEBSITE_ID` in `src/lib/analytics-build.ts`, and `PUBLIC_UMAMI_WEBSITE_ID` overrides it at build time. An empty `PUBLIC_UMAMI_WEBSITE_ID` fails the build. To build without analytics, in a fork or locally, set `ANALYTICS_OFF=1` for the build and for `bun test`. `test/analytics-built.test.ts` follows each sampled page's scripts and their imports in `dist/` and fails when the script URL or the id is missing, so analytics cannot go quiet unnoticed. +Umami is not loaded for a program rather than a person: a browser driven by automation, a user agent that names a bot, crawler, spider, headless browser or link preview, or a screen of exactly 800x600 or 1024x1024 (`src/lib/automated.ts`, the same rule as in sluiceway/landing and sluiceway/app). The Umami website id is `DEFAULT_UMAMI_WEBSITE_ID` in `src/lib/analytics-build.ts`, and `PUBLIC_UMAMI_WEBSITE_ID` overrides it at build time. An empty `PUBLIC_UMAMI_WEBSITE_ID` fails the build. To build without analytics, in a fork or locally, set `ANALYTICS_OFF=1` for the build and for `bun test`. `test/analytics-built.test.ts` follows each sampled page's scripts and their imports in `dist/` and fails when the script URL or the id is missing, so analytics cannot go quiet unnoticed. ## Updating to a new release of Sluiceway diff --git a/src/components/Analytics.astro b/src/components/Analytics.astro index 2809730..0561c53 100644 --- a/src/components/Analytics.astro +++ b/src/components/Analytics.astro @@ -1,6 +1,6 @@ --- // Analytics: starts Umami on the page and hooks up the named events, for every reader but one -// whose browser sends Do Not Track. With analytics off, `init()` adds nothing and every event +// whose browser sends Do Not Track or who is a program (src/lib/automated.ts). With analytics off, `init()` adds nothing and every event // is dropped. --- diff --git a/src/components/Feedback.astro b/src/components/Feedback.astro index b171a46..19bea75 100644 --- a/src/components/Feedback.astro +++ b/src/components/Feedback.astro @@ -50,7 +50,9 @@ import PennyMark from "./PennyMark.astro"; ? "Thanks." : analytics?.blocked() ? "Thanks. Your browser sends Do Not Track, so this was not sent." - : "Thanks. Analytics is off, so this was not sent."; + : analytics?.automated() + ? "Thanks. This browser looks automated, so this was not sent." + : "Thanks. Analytics is off, so this was not sent."; // The pressed button is gone, so keyboard focus moves to the reply. if (hadFocus) reply.focus(); } diff --git a/src/content/docs/privacy.mdx b/src/content/docs/privacy.mdx index e021fbf..8d3330f 100644 --- a/src/content/docs/privacy.mdx +++ b/src/content/docs/privacy.mdx @@ -60,6 +60,13 @@ your browser clears when you close the tab. If your browser sends Do Not Track, the Umami script is not loaded and nothing is counted. +## Automated visitors + +Visits from programs are not counted. The Umami script is not loaded and nothing is sent when +your browser says it is driven by automation, when its user agent names a bot, a crawler, a +spider, a headless browser or a link preview service, or when its screen is +exactly 800 by 600 or 1024 by 1024 pixels, the sizes headless crawlers report. + ## How to object Email [robbe.verhelst@gmail.com](mailto:robbe.verhelst@gmail.com) to object, or to ask for what diff --git a/src/lib/analytics.ts b/src/lib/analytics.ts index de1e576..8887555 100644 --- a/src/lib/analytics.ts +++ b/src/lib/analytics.ts @@ -1,9 +1,11 @@ // Analytics with self-hosted Umami, for every reader. Umami sets no cookie and these docs // store nothing in the browser, so there is nothing to ask. Do Not Track blocks everything: the -// script is never added and no event is sent. The privacy page says the same in words, so a +// script is never added and no event is sent. So does a visit from a program rather than a +// person, by the rule in ./automated.ts. The privacy page says the same in words, so a // change here is a change there (src/content/docs/privacy.mdx). import { UMAMI_SCRIPT_URL } from "./analytics-build"; +import { isAutomated } from "./automated"; /** * The website id, set by astro.config.ts from `umamiWebsiteId()`: the docs' own id, an @@ -37,7 +39,8 @@ declare global { /** What analytics needs from the browser, so the tests can hand it a fake one. */ export interface AnalyticsEnv { storage: Pick | undefined; - navigator: { doNotTrack?: string | null }; + navigator: { doNotTrack?: string | null; webdriver?: boolean; userAgent?: string }; + screen: { width: number; height: number }; /** Add a deferred `