Files
manoon-headless/src/lib/analytics.ts
Unchained 0b9ddeedc8 fix(analytics): properly forward client IPs to Rybbit and OpenPanel
- Create new API route /api/rybbit/track to proxy Rybbit tracking requests
- Extract real client IP from Cloudflare headers (cf-connecting-ip)
- Forward X-Forwarded-For and X-Real-IP headers to analytics backends
- Update OpenPanel proxy to also forward client IP
- Update next.config.ts rewrite to use internal API route

This fixes geo-location issues where all traffic appeared to come from
Cloudflare edge locations instead of actual visitor countries.
2026-04-01 10:24:09 +02:00

211 lines
5.2 KiB
TypeScript

"use client";
import { useCallback } from "react";
import {
trackRybbitProductView,
trackRybbitAddToCart,
trackRybbitRemoveFromCart,
trackRybbitCheckoutStarted,
trackRybbitCheckoutStep,
trackRybbitOrderCompleted,
trackRybbitSearch,
trackRybbitExternalLink,
trackRybbitCartView,
trackRybbitWishlistAdd,
trackRybbitUserLogin,
trackRybbitUserRegister,
trackRybbitNewsletterSignup,
} from "@/lib/services/RybbitService";
export function useAnalytics() {
const trackProductView = useCallback((product: {
id: string;
name: string;
price: number;
currency: string;
category?: string;
}) => {
trackRybbitProductView({
id: product.id,
name: product.name,
price: product.price,
currency: product.currency,
category: product.category,
});
}, []);
const trackAddToCart = useCallback((product: {
id: string;
name: string;
price: number;
currency: string;
quantity: number;
variant?: string;
}) => {
trackRybbitAddToCart({
id: product.id,
name: product.name,
price: product.price,
currency: product.currency,
quantity: product.quantity,
variant: product.variant,
});
}, []);
const trackRemoveFromCart = useCallback((product: {
id: string;
name: string;
quantity: number;
}) => {
trackRybbitRemoveFromCart({
id: product.id,
name: product.name,
quantity: product.quantity,
});
}, []);
const trackCartView = useCallback((cart: {
total: number;
currency: string;
item_count: number;
}) => {
trackRybbitCartView({
total: cart.total,
currency: cart.currency,
item_count: cart.item_count,
});
}, []);
const trackCheckoutStarted = useCallback((cart: {
total: number;
currency: string;
item_count: number;
items: Array<{
id: string;
name: string;
quantity: number;
price: number;
}>;
}) => {
trackRybbitCheckoutStarted({
total: cart.total,
currency: cart.currency,
item_count: cart.item_count,
items: cart.items,
});
}, []);
const trackCheckoutStep = useCallback((step: string, data?: Record<string, unknown>) => {
trackRybbitCheckoutStep(step, data);
}, []);
const trackOrderCompleted = useCallback(async (order: {
order_id: string;
order_number: string;
total: number;
currency: string;
item_count: number;
shipping_cost?: number;
customer_email?: string;
payment_method?: string;
}) => {
console.log("[Analytics] Tracking order:", order.order_number);
// Rybbit tracking
trackRybbitOrderCompleted({
order_id: order.order_id,
order_number: order.order_number,
total: order.total,
currency: order.currency,
item_count: order.item_count,
shipping_cost: order.shipping_cost,
customer_email: order.customer_email,
payment_method: order.payment_method,
});
// Server-side tracking for reliability
try {
const response = await fetch("/api/analytics/track-order", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
orderId: order.order_id,
orderNumber: order.order_number,
total: order.total,
currency: order.currency,
itemCount: order.item_count,
customerEmail: order.customer_email,
paymentMethod: order.payment_method,
shippingCost: order.shipping_cost,
}),
});
if (!response.ok) {
console.error("[Server Analytics] Failed:", await response.text());
}
} catch (e) {
console.error("[Server Analytics] API call failed:", e);
}
}, []);
const trackSearch = useCallback((query: string, results_count: number) => {
trackRybbitSearch(query, results_count);
}, []);
const trackExternalLink = useCallback((url: string, label?: string) => {
trackRybbitExternalLink(url, label);
}, []);
const trackWishlistAdd = useCallback((product: {
id: string;
name: string;
}) => {
trackRybbitWishlistAdd({
id: product.id,
name: product.name,
});
}, []);
const trackUserLogin = useCallback((method: string) => {
trackRybbitUserLogin(method);
}, []);
const trackUserRegister = useCallback((method: string) => {
trackRybbitUserRegister(method);
}, []);
const trackNewsletterSignup = useCallback((email: string, source: string) => {
trackRybbitNewsletterSignup(email, source);
}, []);
// No-op placeholder for identifyUser (OpenPanel removed)
const identifyUser = useCallback((_user: {
profileId: string;
email?: string;
firstName?: string;
lastName?: string;
}) => {
// OpenPanel was removed - this is now a no-op
// User identification is handled by Rybbit automatically via cookies
}, []);
return {
trackProductView,
trackAddToCart,
trackRemoveFromCart,
trackCartView,
trackCheckoutStarted,
trackCheckoutStep,
trackOrderCompleted,
trackSearch,
trackExternalLink,
trackWishlistAdd,
trackUserLogin,
trackUserRegister,
trackNewsletterSignup,
identifyUser,
};
}
export default useAnalytics;