From 8872d2424a9b5b7a249c5700f4bbca9ac02077a1 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Mon, 9 Feb 2026 23:47:56 +0100 Subject: [PATCH] feat: improved analytics --- app/[locale]/layout.tsx | 19 ++++++++ app/actions/contact.ts | 22 +++++++++ app/api/feedback/route.ts | 4 +- .../analytics/umami-analytics-service.ts | 46 ++++++++++++++++--- package.json | 7 +-- 5 files changed, 84 insertions(+), 14 deletions(-) diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx index b7dde6ab..eaee3a6f 100644 --- a/app/[locale]/layout.tsx +++ b/app/[locale]/layout.tsx @@ -53,6 +53,25 @@ export default async function LocaleLayout({ messages = {}; } + // Track pageview on the server with high-fidelity header context + const { getServerAppServices } = await import('@/lib/services/create-services.server'); + const serverServices = getServerAppServices(); + + 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 initial server-side pageview + serverServices.analytics.trackPageview(); + return ( diff --git a/app/actions/contact.ts b/app/actions/contact.ts index b3979220..bbd180de 100644 --- a/app/actions/contact.ts +++ b/app/actions/contact.ts @@ -10,6 +10,23 @@ import { getServerAppServices } from '@/lib/services/create-services.server'; export async function sendContactFormAction(formData: FormData) { const services = getServerAppServices(); const logger = services.logger.child({ action: 'sendContactFormAction' }); + + // Set analytics context from request headers for high-fidelity server-side tracking + const { headers } = await import('next/headers'); + const requestHeaders = await headers(); + + if ('setServerContext' in services.analytics) { + (services.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 attempt + services.analytics.track('contact-form-attempt'); + const name = formData.get('name') as string; const email = formData.get('email') as string; const message = formData.get('message') as string; @@ -110,6 +127,11 @@ export async function sendContactFormAction(formData: FormData) { priority: 5, }); + // Track success + services.analytics.track('contact-form-success', { + is_product_request: !!productName, + }); + return { success: true }; } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); diff --git a/app/api/feedback/route.ts b/app/api/feedback/route.ts index c1f316d2..d09fabf2 100644 --- a/app/api/feedback/route.ts +++ b/app/api/feedback/route.ts @@ -3,14 +3,14 @@ import { handleFeedbackRequest } from '@mintel/next-feedback'; import { config } from '@/lib/config'; export async function GET(req: NextRequest) { - return handleFeedbackRequest(req, { + return handleFeedbackRequest(req as any, { url: config.infraCMS.url, token: config.infraCMS.token, }); } export async function POST(req: NextRequest) { - return handleFeedbackRequest(req, { + return handleFeedbackRequest(req as any, { url: config.infraCMS.url, token: config.infraCMS.token, }); diff --git a/lib/services/analytics/umami-analytics-service.ts b/lib/services/analytics/umami-analytics-service.ts index a4c6f255..cf70c6d0 100644 --- a/lib/services/analytics/umami-analytics-service.ts +++ b/lib/services/analytics/umami-analytics-service.ts @@ -25,6 +25,12 @@ export class UmamiAnalyticsService implements AnalyticsService { private websiteId?: string; private endpoint: string; private logger: LoggerService; + private serverContext?: { + userAgent?: string; + language?: string; + referrer?: string; + ip?: string; + }; constructor( private readonly options: UmamiAnalyticsServiceOptions, @@ -43,6 +49,19 @@ export class UmamiAnalyticsService implements AnalyticsService { }); } + /** + * 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. */ @@ -63,8 +82,8 @@ export class UmamiAnalyticsService implements AnalyticsService { website: this.websiteId, hostname: isClient ? window.location.hostname : 'server', screen: isClient ? `${window.screen.width}x${window.screen.height}` : undefined, - language: isClient ? navigator.language : undefined, - referrer: isClient ? document.referrer : undefined, + language: isClient ? navigator.language : this.serverContext?.language, + referrer: isClient ? document.referrer : this.serverContext?.referrer, ...data, }; @@ -74,13 +93,28 @@ export class UmamiAnalyticsService implements AnalyticsService { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 10000); // 10s timeout + const headers: Record = { + 'Content-Type': 'application/json', + }; + + // Set User-Agent + if (isClient) { + headers['User-Agent'] = navigator.userAgent; + } else if (this.serverContext?.userAgent) { + headers['User-Agent'] = this.serverContext.userAgent; + } else { + headers['User-Agent'] = 'KLZ-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; + } + try { const response = await fetch(`${this.endpoint}/api/send`, { method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'User-Agent': isClient ? navigator.userAgent : 'KLZ-Server', - }, + headers, body: JSON.stringify({ type, payload }), keepalive: true, signal: controller.signal, diff --git a/package.json b/package.json index fa8b858c..d0b8ffd8 100644 --- a/package.json +++ b/package.json @@ -99,10 +99,5 @@ "prepare": "husky" }, "version": "1.0.0", - "pnpm": { - "overrides": { - "next": "16.1.6", - "@sentry/nextjs": "10.38.0" - } - } + "version": "1.0.0" }