feat: improved analytics
Some checks failed
Build & Deploy / 🔍 Prepare Environment (push) Successful in 5s
Build & Deploy / 🧪 QA (push) Failing after 36s
Build & Deploy / 🏗️ Build (push) Failing after 1m56s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🔔 Notifications (push) Successful in 2s

This commit is contained in:
2026-02-09 23:36:05 +01:00
parent 1e00690dd8
commit 42295c3c41
3 changed files with 78 additions and 7 deletions

View File

@@ -114,6 +114,20 @@ export default async function RootLayout({
await import("@/lib/services/create-services.server") await import("@/lib/services/create-services.server")
).getServerAppServices(); ).getServerAppServices();
// Populate analytics context with headers for high-fidelity server-side tracking
const { headers } = await import("next/headers");
const requestHeaders = await headers();
if ("setServerContext" in serverServices.analytics) {
(serverServices.analytics as any).setServerContext({
userAgent: requestHeaders.get("user-agent") || undefined,
language:
requestHeaders.get("accept-language")?.split(",")[0] || undefined,
referrer: requestHeaders.get("referer") || undefined,
ip: requestHeaders.get("x-forwarded-for")?.split(",")[0] || undefined,
});
}
// Track server-side (initial load) // Track server-side (initial load)
serverServices.analytics.trackPageview("/"); serverServices.analytics.trackPageview("/");

View File

@@ -8,9 +8,23 @@ export async function POST(req: Request) {
const services = getServerAppServices(); const services = getServerAppServices();
const logger = services.logger.child({ action: "contact_submission" }); const logger = services.logger.child({ action: "contact_submission" });
// Set analytics context from request headers for high-fidelity server-side tracking
// This fulfills the "server-side via nextjs proxy" requirement
if ("setServerContext" in services.analytics) {
(services.analytics as any).setServerContext({
userAgent: req.headers.get("user-agent") || undefined,
language: req.headers.get("accept-language")?.split(",")[0] || undefined,
referrer: req.headers.get("referer") || undefined,
ip: req.headers.get("x-forwarded-for")?.split(",")[0] || undefined,
});
}
try { try {
const { name, email, company, message, website } = await req.json(); const { name, email, company, message, website } = await req.json();
// Track attempt
services.analytics.track("contact-form-attempt");
// Honeypot check // Honeypot check
if (website) { if (website) {
logger.info("Spam detected (honeypot)"); logger.info("Spam detected (honeypot)");
@@ -118,6 +132,11 @@ ${message}
}); });
} }
// Track success
services.analytics.track("contact-form-success", {
has_company: Boolean(company),
});
return NextResponse.json({ message: "Ok" }); return NextResponse.json({ message: "Ok" });
} catch (error) { } catch (error) {
logger.error("Global API Error", { error }); logger.error("Global API Error", { error });

View File

@@ -25,6 +25,12 @@ export type UmamiAnalyticsServiceOptions = {
export class UmamiAnalyticsService implements AnalyticsService { export class UmamiAnalyticsService implements AnalyticsService {
private websiteId?: string; private websiteId?: string;
private endpoint: string; private endpoint: string;
private serverContext?: {
userAgent?: string;
language?: string;
referrer?: string;
ip?: string;
};
constructor(private readonly options: UmamiAnalyticsServiceOptions) { constructor(private readonly options: UmamiAnalyticsServiceOptions) {
this.websiteId = config.analytics.umami.websiteId; this.websiteId = config.analytics.umami.websiteId;
@@ -36,6 +42,19 @@ export class UmamiAnalyticsService implements AnalyticsService {
: "/stats"; : "/stats";
} }
/**
* Set the server-side context for the current request.
* This allows the service to use real request headers for tracking.
*/
setServerContext(context: {
userAgent?: string;
language?: string;
referrer?: string;
ip?: string;
}) {
this.serverContext = context;
}
/** /**
* Internal method to send the payload to Umami API. * Internal method to send the payload to Umami API.
*/ */
@@ -53,18 +72,37 @@ export class UmamiAnalyticsService implements AnalyticsService {
? `${window.screen.width}x${window.screen.height}` ? `${window.screen.width}x${window.screen.height}`
: undefined, : undefined,
language: language:
typeof window !== "undefined" ? navigator.language : undefined, typeof window !== "undefined"
referrer: typeof window !== "undefined" ? document.referrer : undefined, ? navigator.language
: this.serverContext?.language,
referrer:
typeof window !== "undefined"
? document.referrer
: this.serverContext?.referrer,
...data, ...data,
}; };
const headers: Record<string, string> = {
"Content-Type": "application/json",
};
// Set User-Agent
if (typeof window !== "undefined") {
headers["User-Agent"] = navigator.userAgent;
} else if (this.serverContext?.userAgent) {
headers["User-Agent"] = this.serverContext.userAgent;
} else {
headers["User-Agent"] = "Mintel-Server-Proxy";
}
// Forward client IP if available (Umami must be configured to trust this)
if (this.serverContext?.ip) {
headers["X-Forwarded-For"] = this.serverContext.ip;
}
const response = await fetch(`${this.endpoint}/api/send`, { const response = await fetch(`${this.endpoint}/api/send`, {
method: "POST", method: "POST",
headers: { headers,
"Content-Type": "application/json",
"User-Agent":
typeof window === "undefined" ? "KLZ-Server" : navigator.userAgent,
},
body: JSON.stringify({ type, payload }), body: JSON.stringify({ type, payload }),
keepalive: true, keepalive: true,
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any