From 92003693fe630caad89649090580150326753aa3 Mon Sep 17 00:00:00 2001 From: Cyanoxide Date: Thu, 30 Jul 2026 16:21:53 +0100 Subject: [PATCH] Add cookieless analytics, kept out of the repository MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Umami rather than Google Analytics: it sets no cookies, so the site needs no consent banner, which GA4 would have required. The website ID is read from .env.production.local, which is gitignored, and the script is injected at runtime only when that ID is present. A fork therefore gets nothing at all — with no ID the guard makes the whole branch unreachable and the bundler drops it, so even the tracker's URL is absent from the build. Vite reads .env.production.local for `vite build` and never for `vite dev`, so working locally cannot put localhost traffic in the stats. The env block in .gitignore listed five specific files with no glob, which left .env.production and the like committable. Replaced with a glob and an exception for the template. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013yxFHbQB5DPVd9cRZNW3A9 --- .gitignore | 6 ++---- frontend/.env.example | 10 ++++++++++ frontend/src/main.tsx | 5 +++++ frontend/src/utils/analytics.ts | 30 ++++++++++++++++++++++++++++++ frontend/src/vite-env.d.ts | 6 ++++++ 5 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 frontend/.env.example create mode 100644 frontend/src/utils/analytics.ts create mode 100644 frontend/src/vite-env.d.ts diff --git a/.gitignore b/.gitignore index 1c12e81..c650edd 100644 --- a/.gitignore +++ b/.gitignore @@ -16,10 +16,8 @@ coverage/ # Environment files .env -.env.local -.env.development.local -.env.test.local -.env.production.local +.env.* +!.env.example # Python __pycache__/ diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..1a3b987 --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,10 @@ +# Umami website ID, from the tracking code panel in the Umami dashboard. +# +# Copy this file to .env.production.local and paste the ID in. That file is +# gitignored, and `vite build` reads it while `vite dev` does not — so the ID +# never enters the repository, and running the dev server does not put localhost +# traffic in the stats. +# +# Leave it empty, or skip the file entirely, and analytics are a no-op: no script +# tag, no requests. That is what a fork gets. +VITE_UMAMI_WEBSITE_ID= diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 2871df0..78db04c 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -4,6 +4,11 @@ import App from './App.tsx'; import { Provider } from './context/provider.tsx'; import './index.css'; import './styles/scrollbars.css'; +import { initAnalytics } from './utils/analytics.ts'; + +// Straight to the document rather than through a component, since it has nothing +// to say about the tree. +initAnalytics(); createRoot(document.getElementById('root')!).render( diff --git a/frontend/src/utils/analytics.ts b/frontend/src/utils/analytics.ts new file mode 100644 index 0000000..cb92696 --- /dev/null +++ b/frontend/src/utils/analytics.ts @@ -0,0 +1,30 @@ +/** + * Umami, injected at runtime and only when a website ID was built in. + * + * The ID comes from `.env.production.local`, which is gitignored, so a clone or + * a fork gets no script tag, no request and no ID — analytics are simply absent + * rather than broken or pointed at someone else's account. See `.env.example`. + * + * `.env.production.local` is read by `vite build` and never by `vite dev`, so + * working locally cannot put localhost traffic in the stats. The `PROD` guard + * says the same thing twice, in case the value is ever put in a file the dev + * server does read. + * + * Umami is cookieless, which is why there is no consent banner in the app. + * Anything added here that sets a cookie changes that. + */ +const websiteId = import.meta.env.VITE_UMAMI_WEBSITE_ID; + +export function initAnalytics() { + if (!websiteId || !import.meta.env.PROD) return; + + const script = document.createElement('script'); + script.src = 'https://cloud.umami.is/script.js'; + // The tracker reads its configuration off `document.currentScript`, so the + // attributes have to be set before it is appended, and it has to stay a + // classic script — as a module `currentScript` is null and it gives up + // without a word. For the same reason there is no `data-host-url`: absent, + // it defaults to Umami Cloud's own endpoint. + script.setAttribute('data-website-id', websiteId); + document.head.appendChild(script); +} diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts new file mode 100644 index 0000000..feb0072 --- /dev/null +++ b/frontend/src/vite-env.d.ts @@ -0,0 +1,6 @@ +/// + +interface ImportMetaEnv { + /** Umami website ID. Optional: absent from any build but the live one. */ + readonly VITE_UMAMI_WEBSITE_ID?: string; +}