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; +}