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.
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useOpenPanel } from "@openpanel/nextjs";
|
||||
import { useCallback } from "react";
|
||||
import {
|
||||
trackRybbitProductView,
|
||||
@@ -19,106 +18,6 @@ import {
|
||||
} from "@/lib/services/RybbitService";
|
||||
|
||||
export function useAnalytics() {
|
||||
const op = useOpenPanel();
|
||||
|
||||
// Helper to track with both OpenPanel and Rybbit
|
||||
const trackDual = useCallback((
|
||||
eventName: string,
|
||||
openPanelData: Record<string, any>
|
||||
) => {
|
||||
// OpenPanel tracking
|
||||
try {
|
||||
op.track(eventName, openPanelData);
|
||||
} catch (e) {
|
||||
console.error("[OpenPanel] Tracking error:", e);
|
||||
}
|
||||
|
||||
// Rybbit tracking (fire-and-forget)
|
||||
try {
|
||||
switch (eventName) {
|
||||
case "product_viewed":
|
||||
trackRybbitProductView({
|
||||
id: openPanelData.product_id,
|
||||
name: openPanelData.product_name,
|
||||
price: openPanelData.price,
|
||||
currency: openPanelData.currency,
|
||||
category: openPanelData.category,
|
||||
});
|
||||
break;
|
||||
case "add_to_cart":
|
||||
trackRybbitAddToCart({
|
||||
id: openPanelData.product_id,
|
||||
name: openPanelData.product_name,
|
||||
price: openPanelData.price,
|
||||
currency: openPanelData.currency,
|
||||
quantity: openPanelData.quantity,
|
||||
variant: openPanelData.variant,
|
||||
});
|
||||
break;
|
||||
case "remove_from_cart":
|
||||
trackRybbitRemoveFromCart({
|
||||
id: openPanelData.product_id,
|
||||
name: openPanelData.product_name,
|
||||
quantity: openPanelData.quantity,
|
||||
});
|
||||
break;
|
||||
case "cart_view":
|
||||
trackRybbitCartView({
|
||||
total: openPanelData.cart_total,
|
||||
currency: openPanelData.currency,
|
||||
item_count: openPanelData.item_count,
|
||||
});
|
||||
break;
|
||||
case "checkout_started":
|
||||
trackRybbitCheckoutStarted({
|
||||
total: openPanelData.cart_total,
|
||||
currency: openPanelData.currency,
|
||||
item_count: openPanelData.item_count,
|
||||
items: openPanelData.items,
|
||||
});
|
||||
break;
|
||||
case "checkout_step":
|
||||
trackRybbitCheckoutStep(openPanelData.step, openPanelData);
|
||||
break;
|
||||
case "order_completed":
|
||||
trackRybbitOrderCompleted({
|
||||
order_id: openPanelData.order_id,
|
||||
order_number: openPanelData.order_number,
|
||||
total: openPanelData.total,
|
||||
currency: openPanelData.currency,
|
||||
item_count: openPanelData.item_count,
|
||||
shipping_cost: openPanelData.shipping_cost,
|
||||
customer_email: openPanelData.customer_email,
|
||||
payment_method: openPanelData.payment_method,
|
||||
});
|
||||
break;
|
||||
case "search":
|
||||
trackRybbitSearch(openPanelData.query, openPanelData.results_count);
|
||||
break;
|
||||
case "external_link_click":
|
||||
trackRybbitExternalLink(openPanelData.url, openPanelData.label);
|
||||
break;
|
||||
case "wishlist_add":
|
||||
trackRybbitWishlistAdd({
|
||||
id: openPanelData.product_id,
|
||||
name: openPanelData.product_name,
|
||||
});
|
||||
break;
|
||||
case "user_login":
|
||||
trackRybbitUserLogin(openPanelData.method);
|
||||
break;
|
||||
case "user_register":
|
||||
trackRybbitUserRegister(openPanelData.method);
|
||||
break;
|
||||
case "newsletter_signup":
|
||||
trackRybbitNewsletterSignup(openPanelData.email, openPanelData.source);
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("[Rybbit] Tracking error:", e);
|
||||
}
|
||||
}, [op]);
|
||||
|
||||
const trackProductView = useCallback((product: {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -126,15 +25,14 @@ export function useAnalytics() {
|
||||
currency: string;
|
||||
category?: string;
|
||||
}) => {
|
||||
trackDual("product_viewed", {
|
||||
product_id: product.id,
|
||||
product_name: product.name,
|
||||
trackRybbitProductView({
|
||||
id: product.id,
|
||||
name: product.name,
|
||||
price: product.price,
|
||||
currency: product.currency,
|
||||
category: product.category,
|
||||
source: "client",
|
||||
});
|
||||
}, [trackDual]);
|
||||
}, []);
|
||||
|
||||
const trackAddToCart = useCallback((product: {
|
||||
id: string;
|
||||
@@ -144,42 +42,39 @@ export function useAnalytics() {
|
||||
quantity: number;
|
||||
variant?: string;
|
||||
}) => {
|
||||
trackDual("add_to_cart", {
|
||||
product_id: product.id,
|
||||
product_name: product.name,
|
||||
trackRybbitAddToCart({
|
||||
id: product.id,
|
||||
name: product.name,
|
||||
price: product.price,
|
||||
currency: product.currency,
|
||||
quantity: product.quantity,
|
||||
variant: product.variant,
|
||||
source: "client",
|
||||
});
|
||||
}, [trackDual]);
|
||||
}, []);
|
||||
|
||||
const trackRemoveFromCart = useCallback((product: {
|
||||
id: string;
|
||||
name: string;
|
||||
quantity: number;
|
||||
}) => {
|
||||
trackDual("remove_from_cart", {
|
||||
product_id: product.id,
|
||||
product_name: product.name,
|
||||
trackRybbitRemoveFromCart({
|
||||
id: product.id,
|
||||
name: product.name,
|
||||
quantity: product.quantity,
|
||||
source: "client",
|
||||
});
|
||||
}, [trackDual]);
|
||||
}, []);
|
||||
|
||||
const trackCartView = useCallback((cart: {
|
||||
total: number;
|
||||
currency: string;
|
||||
item_count: number;
|
||||
}) => {
|
||||
trackDual("cart_view", {
|
||||
cart_total: cart.total,
|
||||
trackRybbitCartView({
|
||||
total: cart.total,
|
||||
currency: cart.currency,
|
||||
item_count: cart.item_count,
|
||||
source: "client",
|
||||
});
|
||||
}, [trackDual]);
|
||||
}, []);
|
||||
|
||||
const trackCheckoutStarted = useCallback((cart: {
|
||||
total: number;
|
||||
@@ -192,22 +87,17 @@ export function useAnalytics() {
|
||||
price: number;
|
||||
}>;
|
||||
}) => {
|
||||
trackDual("checkout_started", {
|
||||
cart_total: cart.total,
|
||||
trackRybbitCheckoutStarted({
|
||||
total: cart.total,
|
||||
currency: cart.currency,
|
||||
item_count: cart.item_count,
|
||||
items: cart.items,
|
||||
source: "client",
|
||||
});
|
||||
}, [trackDual]);
|
||||
}, []);
|
||||
|
||||
const trackCheckoutStep = useCallback((step: string, data?: Record<string, unknown>) => {
|
||||
trackDual("checkout_step", {
|
||||
step,
|
||||
...data,
|
||||
source: "client",
|
||||
});
|
||||
}, [trackDual]);
|
||||
trackRybbitCheckoutStep(step, data);
|
||||
}, []);
|
||||
|
||||
const trackOrderCompleted = useCallback(async (order: {
|
||||
order_id: string;
|
||||
@@ -221,8 +111,8 @@ export function useAnalytics() {
|
||||
}) => {
|
||||
console.log("[Analytics] Tracking order:", order.order_number);
|
||||
|
||||
// Track with both OpenPanel and Rybbit
|
||||
trackDual("order_completed", {
|
||||
// Rybbit tracking
|
||||
trackRybbitOrderCompleted({
|
||||
order_id: order.order_id,
|
||||
order_number: order.order_number,
|
||||
total: order.total,
|
||||
@@ -231,20 +121,8 @@ export function useAnalytics() {
|
||||
shipping_cost: order.shipping_cost,
|
||||
customer_email: order.customer_email,
|
||||
payment_method: order.payment_method,
|
||||
source: "client",
|
||||
});
|
||||
|
||||
// OpenPanel revenue tracking
|
||||
try {
|
||||
op.revenue(order.total, {
|
||||
currency: order.currency,
|
||||
transaction_id: order.order_number,
|
||||
source: "client",
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("[OpenPanel] Revenue tracking error:", e);
|
||||
}
|
||||
|
||||
// Server-side tracking for reliability
|
||||
try {
|
||||
const response = await fetch("/api/analytics/track-order", {
|
||||
@@ -268,73 +146,48 @@ export function useAnalytics() {
|
||||
} catch (e) {
|
||||
console.error("[Server Analytics] API call failed:", e);
|
||||
}
|
||||
}, [op, trackDual]);
|
||||
}, []);
|
||||
|
||||
const trackSearch = useCallback((query: string, results_count: number) => {
|
||||
trackDual("search", {
|
||||
query,
|
||||
results_count,
|
||||
source: "client",
|
||||
});
|
||||
}, [trackDual]);
|
||||
trackRybbitSearch(query, results_count);
|
||||
}, []);
|
||||
|
||||
const trackExternalLink = useCallback((url: string, label?: string) => {
|
||||
trackDual("external_link_click", {
|
||||
url,
|
||||
label,
|
||||
source: "client",
|
||||
});
|
||||
}, [trackDual]);
|
||||
trackRybbitExternalLink(url, label);
|
||||
}, []);
|
||||
|
||||
const trackWishlistAdd = useCallback((product: {
|
||||
id: string;
|
||||
name: string;
|
||||
}) => {
|
||||
trackDual("wishlist_add", {
|
||||
product_id: product.id,
|
||||
product_name: product.name,
|
||||
source: "client",
|
||||
trackRybbitWishlistAdd({
|
||||
id: product.id,
|
||||
name: product.name,
|
||||
});
|
||||
}, [trackDual]);
|
||||
}, []);
|
||||
|
||||
const trackUserLogin = useCallback((method: string) => {
|
||||
trackDual("user_login", {
|
||||
method,
|
||||
source: "client",
|
||||
});
|
||||
}, [trackDual]);
|
||||
trackRybbitUserLogin(method);
|
||||
}, []);
|
||||
|
||||
const trackUserRegister = useCallback((method: string) => {
|
||||
trackDual("user_register", {
|
||||
method,
|
||||
source: "client",
|
||||
});
|
||||
}, [trackDual]);
|
||||
trackRybbitUserRegister(method);
|
||||
}, []);
|
||||
|
||||
const trackNewsletterSignup = useCallback((email: string, source: string) => {
|
||||
trackDual("newsletter_signup", {
|
||||
email,
|
||||
source,
|
||||
});
|
||||
}, [trackDual]);
|
||||
trackRybbitNewsletterSignup(email, source);
|
||||
}, []);
|
||||
|
||||
const identifyUser = useCallback((user: {
|
||||
// No-op placeholder for identifyUser (OpenPanel removed)
|
||||
const identifyUser = useCallback((_user: {
|
||||
profileId: string;
|
||||
email?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
}) => {
|
||||
try {
|
||||
op.identify({
|
||||
profileId: user.profileId,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
email: user.email,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("[OpenPanel] Identify error:", e);
|
||||
}
|
||||
}, [op]);
|
||||
// OpenPanel was removed - this is now a no-op
|
||||
// User identification is handled by Rybbit automatically via cookies
|
||||
}, []);
|
||||
|
||||
return {
|
||||
trackProductView,
|
||||
|
||||
Reference in New Issue
Block a user