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
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:
@@ -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("/");
|
||||||
|
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user