feat: improved analytics
This commit is contained in:
@@ -53,6 +53,25 @@ export default async function LocaleLayout({
|
|||||||
messages = {};
|
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 (
|
return (
|
||||||
<html lang={safeLocale} className="scroll-smooth overflow-x-hidden">
|
<html lang={safeLocale} className="scroll-smooth overflow-x-hidden">
|
||||||
<body className="flex flex-col min-h-screen font-sans selection:bg-accent selection:text-primary-dark antialiased overflow-x-hidden">
|
<body className="flex flex-col min-h-screen font-sans selection:bg-accent selection:text-primary-dark antialiased overflow-x-hidden">
|
||||||
|
|||||||
@@ -10,6 +10,23 @@ import { getServerAppServices } from '@/lib/services/create-services.server';
|
|||||||
export async function sendContactFormAction(formData: FormData) {
|
export async function sendContactFormAction(formData: FormData) {
|
||||||
const services = getServerAppServices();
|
const services = getServerAppServices();
|
||||||
const logger = services.logger.child({ action: 'sendContactFormAction' });
|
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 name = formData.get('name') as string;
|
||||||
const email = formData.get('email') as string;
|
const email = formData.get('email') as string;
|
||||||
const message = formData.get('message') as string;
|
const message = formData.get('message') as string;
|
||||||
@@ -110,6 +127,11 @@ export async function sendContactFormAction(formData: FormData) {
|
|||||||
priority: 5,
|
priority: 5,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Track success
|
||||||
|
services.analytics.track('contact-form-success', {
|
||||||
|
is_product_request: !!productName,
|
||||||
|
});
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ import { handleFeedbackRequest } from '@mintel/next-feedback';
|
|||||||
import { config } from '@/lib/config';
|
import { config } from '@/lib/config';
|
||||||
|
|
||||||
export async function GET(req: NextRequest) {
|
export async function GET(req: NextRequest) {
|
||||||
return handleFeedbackRequest(req, {
|
return handleFeedbackRequest(req as any, {
|
||||||
url: config.infraCMS.url,
|
url: config.infraCMS.url,
|
||||||
token: config.infraCMS.token,
|
token: config.infraCMS.token,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
return handleFeedbackRequest(req, {
|
return handleFeedbackRequest(req as any, {
|
||||||
url: config.infraCMS.url,
|
url: config.infraCMS.url,
|
||||||
token: config.infraCMS.token,
|
token: config.infraCMS.token,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,6 +25,12 @@ export class UmamiAnalyticsService implements AnalyticsService {
|
|||||||
private websiteId?: string;
|
private websiteId?: string;
|
||||||
private endpoint: string;
|
private endpoint: string;
|
||||||
private logger: LoggerService;
|
private logger: LoggerService;
|
||||||
|
private serverContext?: {
|
||||||
|
userAgent?: string;
|
||||||
|
language?: string;
|
||||||
|
referrer?: string;
|
||||||
|
ip?: string;
|
||||||
|
};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly options: UmamiAnalyticsServiceOptions,
|
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.
|
* Internal method to send the payload to Umami API.
|
||||||
*/
|
*/
|
||||||
@@ -63,8 +82,8 @@ export class UmamiAnalyticsService implements AnalyticsService {
|
|||||||
website: this.websiteId,
|
website: this.websiteId,
|
||||||
hostname: isClient ? window.location.hostname : 'server',
|
hostname: isClient ? window.location.hostname : 'server',
|
||||||
screen: isClient ? `${window.screen.width}x${window.screen.height}` : undefined,
|
screen: isClient ? `${window.screen.width}x${window.screen.height}` : undefined,
|
||||||
language: isClient ? navigator.language : undefined,
|
language: isClient ? navigator.language : this.serverContext?.language,
|
||||||
referrer: isClient ? document.referrer : undefined,
|
referrer: isClient ? document.referrer : this.serverContext?.referrer,
|
||||||
...data,
|
...data,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -74,13 +93,28 @@ export class UmamiAnalyticsService implements AnalyticsService {
|
|||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10s timeout
|
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10s timeout
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'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 {
|
try {
|
||||||
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': isClient ? navigator.userAgent : 'KLZ-Server',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ type, payload }),
|
body: JSON.stringify({ type, payload }),
|
||||||
keepalive: true,
|
keepalive: true,
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
|
|||||||
@@ -99,10 +99,5 @@
|
|||||||
"prepare": "husky"
|
"prepare": "husky"
|
||||||
},
|
},
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"pnpm": {
|
"version": "1.0.0"
|
||||||
"overrides": {
|
|
||||||
"next": "16.1.6",
|
|
||||||
"@sentry/nextjs": "10.38.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user