Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 42295c3c41 | |||
| 1e00690dd8 | |||
| 90e9f37849 | |||
| 9eaaa798a3 |
@@ -200,6 +200,7 @@ jobs:
|
||||
--build-arg NEXT_PUBLIC_TARGET=${{ needs.prepare.outputs.target }} \
|
||||
--build-arg DIRECTUS_URL=${{ needs.prepare.outputs.directus_url }} \
|
||||
--build-arg UMAMI_API_ENDPOINT=${{ secrets.UMAMI_API_ENDPOINT || secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }} \
|
||||
--build-arg NEXT_PUBLIC_UMAMI_WEBSITE_ID=${{ secrets.UMAMI_WEBSITE_ID || secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID || vars.NEXT_PUBLIC_UMAMI_WEBSITE_ID }} \
|
||||
-t registry.infra.mintel.me/mintel/mb-grid-solutions:${{ needs.prepare.outputs.image_tag }} \
|
||||
--push .
|
||||
|
||||
@@ -269,6 +270,7 @@ jobs:
|
||||
GOTIFY_URL=${{ secrets.GOTIFY_URL || vars.GOTIFY_URL }}
|
||||
GOTIFY_TOKEN=${{ secrets.GOTIFY_TOKEN || vars.GOTIFY_TOKEN }}
|
||||
UMAMI_WEBSITE_ID=${{ secrets.UMAMI_WEBSITE_ID || secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID || vars.NEXT_PUBLIC_UMAMI_WEBSITE_ID }}
|
||||
NEXT_PUBLIC_UMAMI_WEBSITE_ID=${{ secrets.UMAMI_WEBSITE_ID || secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID || vars.NEXT_PUBLIC_UMAMI_WEBSITE_ID }}
|
||||
UMAMI_API_ENDPOINT=${{ secrets.UMAMI_API_ENDPOINT || secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
|
||||
|
||||
# Project
|
||||
|
||||
@@ -11,12 +11,14 @@ ARG NEXT_PUBLIC_BASE_URL
|
||||
ARG UMAMI_API_ENDPOINT
|
||||
ARG NEXT_PUBLIC_TARGET
|
||||
ARG DIRECTUS_URL
|
||||
ARG NEXT_PUBLIC_UMAMI_WEBSITE_ID
|
||||
ARG NPM_TOKEN
|
||||
|
||||
ENV NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL
|
||||
ENV UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT
|
||||
ENV NEXT_PUBLIC_TARGET=$NEXT_PUBLIC_TARGET
|
||||
ENV DIRECTUS_URL=$DIRECTUS_URL
|
||||
ENV NEXT_PUBLIC_UMAMI_WEBSITE_ID=$NEXT_PUBLIC_UMAMI_WEBSITE_ID
|
||||
ENV NPM_TOKEN=$NPM_TOKEN
|
||||
ENV SENTRY_SUPPRESS_TURBOPACK_WARNING=1
|
||||
ENV SKIP_RUNTIME_ENV_VALIDATION=true
|
||||
|
||||
@@ -6,6 +6,8 @@ import { NextIntlClientProvider } from "next-intl";
|
||||
import { getMessages } from "next-intl/server";
|
||||
import { notFound } from "next/navigation";
|
||||
import { LazyMotion, domAnimation } from "framer-motion";
|
||||
import AnalyticsProvider from "@/components/analytics/AnalyticsProvider";
|
||||
import { config } from "@/lib/config";
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
@@ -108,10 +110,26 @@ export default async function RootLayout({
|
||||
|
||||
// Track pageview on the server
|
||||
// This is safe to call here because layout is a Server Component
|
||||
const services = (
|
||||
const serverServices = (
|
||||
await import("@/lib/services/create-services.server")
|
||||
).getServerAppServices();
|
||||
services.analytics.trackPageview();
|
||||
|
||||
// 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)
|
||||
serverServices.analytics.trackPageview("/");
|
||||
|
||||
return (
|
||||
<html lang={locale} className={`${inter.variable}`}>
|
||||
@@ -123,6 +141,7 @@ export default async function RootLayout({
|
||||
</head>
|
||||
<body className="antialiased">
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
<AnalyticsProvider websiteId={config.analytics.umami.websiteId} />
|
||||
<LazyMotion features={domAnimation}>
|
||||
<Layout>{children}</Layout>
|
||||
</LazyMotion>
|
||||
|
||||
@@ -8,9 +8,23 @@ export async function POST(req: Request) {
|
||||
const services = getServerAppServices();
|
||||
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 {
|
||||
const { name, email, company, message, website } = await req.json();
|
||||
|
||||
// Track attempt
|
||||
services.analytics.track("contact-form-attempt");
|
||||
|
||||
// Honeypot check
|
||||
if (website) {
|
||||
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" });
|
||||
} catch (error) {
|
||||
logger.error("Global API Error", { error });
|
||||
|
||||
@@ -27,9 +27,11 @@ function createConfig() {
|
||||
|
||||
analytics: {
|
||||
umami: {
|
||||
websiteId: env.UMAMI_WEBSITE_ID,
|
||||
websiteId: env.NEXT_PUBLIC_UMAMI_WEBSITE_ID || env.UMAMI_WEBSITE_ID,
|
||||
apiEndpoint: env.UMAMI_API_ENDPOINT,
|
||||
enabled: Boolean(env.UMAMI_WEBSITE_ID),
|
||||
enabled: Boolean(
|
||||
env.NEXT_PUBLIC_UMAMI_WEBSITE_ID || env.UMAMI_WEBSITE_ID,
|
||||
),
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -26,6 +26,10 @@ export const envSchema = z
|
||||
preprocessEmptyString,
|
||||
z.string().optional(),
|
||||
),
|
||||
NEXT_PUBLIC_UMAMI_WEBSITE_ID: z.preprocess(
|
||||
preprocessEmptyString,
|
||||
z.string().optional(),
|
||||
),
|
||||
UMAMI_API_ENDPOINT: z.preprocess(
|
||||
preprocessEmptyString,
|
||||
z.string().url().default("https://analytics.infra.mintel.me"),
|
||||
@@ -115,6 +119,7 @@ export function getRawEnv() {
|
||||
NEXT_PUBLIC_TARGET: process.env.NEXT_PUBLIC_TARGET,
|
||||
UMAMI_WEBSITE_ID:
|
||||
process.env.UMAMI_WEBSITE_ID || process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID,
|
||||
NEXT_PUBLIC_UMAMI_WEBSITE_ID: process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID,
|
||||
UMAMI_API_ENDPOINT:
|
||||
process.env.UMAMI_API_ENDPOINT ||
|
||||
process.env.UMAMI_SCRIPT_URL ||
|
||||
|
||||
@@ -25,6 +25,12 @@ export type UmamiAnalyticsServiceOptions = {
|
||||
export class UmamiAnalyticsService implements AnalyticsService {
|
||||
private websiteId?: string;
|
||||
private endpoint: string;
|
||||
private serverContext?: {
|
||||
userAgent?: string;
|
||||
language?: string;
|
||||
referrer?: string;
|
||||
ip?: string;
|
||||
};
|
||||
|
||||
constructor(private readonly options: UmamiAnalyticsServiceOptions) {
|
||||
this.websiteId = config.analytics.umami.websiteId;
|
||||
@@ -36,6 +42,19 @@ export class UmamiAnalyticsService implements AnalyticsService {
|
||||
: "/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.
|
||||
*/
|
||||
@@ -53,18 +72,37 @@ export class UmamiAnalyticsService implements AnalyticsService {
|
||||
? `${window.screen.width}x${window.screen.height}`
|
||||
: undefined,
|
||||
language:
|
||||
typeof window !== "undefined" ? navigator.language : undefined,
|
||||
referrer: typeof window !== "undefined" ? document.referrer : undefined,
|
||||
typeof window !== "undefined"
|
||||
? navigator.language
|
||||
: this.serverContext?.language,
|
||||
referrer:
|
||||
typeof window !== "undefined"
|
||||
? document.referrer
|
||||
: this.serverContext?.referrer,
|
||||
...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`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent":
|
||||
typeof window === "undefined" ? "KLZ-Server" : navigator.userAgent,
|
||||
},
|
||||
headers,
|
||||
body: JSON.stringify({ type, payload }),
|
||||
keepalive: true,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
@@ -14,5 +14,9 @@ export default createMiddleware({
|
||||
export const config = {
|
||||
// Matcher for all pages and internationalized pathnames
|
||||
// excluding api, _next, static files, etc.
|
||||
matcher: ["/((?!api|_next|_vercel|.*\\..*).*)", "/", "/(de)/:path*"],
|
||||
matcher: [
|
||||
"/((?!api|stats|errors|_next|_vercel|.*\\..*).*)",
|
||||
"/",
|
||||
"/(de)/:path*",
|
||||
],
|
||||
};
|
||||
|
||||
@@ -17,10 +17,18 @@ const nextConfig = {
|
||||
source: "/stats/:path*",
|
||||
destination: `${umamiUrl}/:path*`,
|
||||
},
|
||||
{
|
||||
source: "/:locale(de)/stats/:path*",
|
||||
destination: `${umamiUrl}/:path*`,
|
||||
},
|
||||
{
|
||||
source: "/errors/:path*",
|
||||
destination: `${glitchtipUrl}/:path*`,
|
||||
},
|
||||
{
|
||||
source: "/:locale(de)/errors/:path*",
|
||||
destination: `${glitchtipUrl}/:path*`,
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user