Files
mintel.me/apps/web/src/analytics/recordPageviewToUmami.ts
T
mmintel a54df524c7
Build & Deploy / 🔍 Prepare (push) Successful in 5s
Build & Deploy / 🏗️ Build (push) Successful in 6m31s
Build & Deploy / 🚀 Deploy (push) Successful in 19s
Build & Deploy / 🩺 Smoke Test (push) Successful in 5s
Build & Deploy / 🔔 Notify (push) Successful in 2s
feat(analytics): record pageviews server-side only
Umami never received data: the client-side path (browser beacon to
/stats/api/send) has been dead in production. Pageviews are now
classified and recorded entirely on the server in the request proxy;
no tracking script ships to the browser anymore.

- Pure domain decision (decidePageview) filters real page views from
  assets, infrastructure paths, prefetches and bot user agents
- Pure payload builder maps facts onto the Umami send contract as an
  unnamed event, with the client identity travelling in headers
- proxy.ts wires request facts into the domain and fires the send
  without delaying the response; configured via UMAMI_WEBSITE_ID and
  UMAMI_API_ENDPOINT
- Removes the dead client analytics stack (components, adapters,
  /stats proxy route) and its layout mount
2026-10-04 12:24:13 +02:00

37 lines
1.2 KiB
TypeScript

/**
* Imperative shell for page view analytics: sends one page view to the
* Umami collection API, identifying the visitor via request headers.
* Fire-and-forget by design — analytics must never delay a response.
*/
import type { PageviewFacts } from "@/src/domain/pageview-decision";
import { buildUmamiPageviewBody } from "@/src/domain/umami-payload";
const SEND_TIMEOUT_MS = 3_000;
const websiteId = process.env.UMAMI_WEBSITE_ID ?? "";
const apiEndpoint = (process.env.UMAMI_API_ENDPOINT ?? "").replace(/\/+$/, "");
export function recordPageviewToUmami(facts: PageviewFacts): void {
if (!websiteId || !apiEndpoint) return;
const body = buildUmamiPageviewBody(facts, websiteId);
void fetch(`${apiEndpoint}/api/send`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"User-Agent": facts.userAgent,
"X-Forwarded-For": facts.clientIp,
"Accept-Language": facts.language,
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(SEND_TIMEOUT_MS),
}).catch((error: unknown) => {
console.error("umami pageview send failed", {
url: facts.url,
error: error instanceof Error ? error.message : String(error),
});
});
}