Files
manoon-headless/src/lib/analytics.ts
Unchained d4039c6e3b
Some checks failed
Build and Deploy / build (push) Has been cancelled
feat(analytics): complete Rybbit tracking integration
- Add Rybbit server-side tracking to analytics-server.ts for order completion and revenue
- Add trackNewsletterSignup to analytics.ts and wire up NewsletterSection
- Add cart tracking to CartDrawer (cart view, remove from cart)
- All ecommerce events now track to both OpenPanel and Rybbit
2026-03-31 05:53:53 +02:00

358 lines
9.4 KiB
TypeScript

"use client";
import { useOpenPanel } from "@openpanel/nextjs";
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 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;
price: number;
currency: string;
category?: string;
}) => {
trackDual("product_viewed", {
product_id: product.id,
product_name: product.name,
price: product.price,
currency: product.currency,
category: product.category,
source: "client",
});
}, [trackDual]);
const trackAddToCart = useCallback((product: {
id: string;
name: string;
price: number;
currency: string;
quantity: number;
variant?: string;
}) => {
trackDual("add_to_cart", {
product_id: product.id,
product_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,
quantity: product.quantity,
source: "client",
});
}, [trackDual]);
const trackCartView = useCallback((cart: {
total: number;
currency: string;
item_count: number;
}) => {
trackDual("cart_view", {
cart_total: cart.total,
currency: cart.currency,
item_count: cart.item_count,
source: "client",
});
}, [trackDual]);
const trackCheckoutStarted = useCallback((cart: {
total: number;
currency: string;
item_count: number;
items: Array<{
id: string;
name: string;
quantity: number;
price: number;
}>;
}) => {
trackDual("checkout_started", {
cart_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]);
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);
// Track with both OpenPanel and Rybbit
trackDual("order_completed", {
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,
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", {
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);
}
}, [op, trackDual]);
const trackSearch = useCallback((query: string, results_count: number) => {
trackDual("search", {
query,
results_count,
source: "client",
});
}, [trackDual]);
const trackExternalLink = useCallback((url: string, label?: string) => {
trackDual("external_link_click", {
url,
label,
source: "client",
});
}, [trackDual]);
const trackWishlistAdd = useCallback((product: {
id: string;
name: string;
}) => {
trackDual("wishlist_add", {
product_id: product.id,
product_name: product.name,
source: "client",
});
}, [trackDual]);
const trackUserLogin = useCallback((method: string) => {
trackDual("user_login", {
method,
source: "client",
});
}, [trackDual]);
const trackUserRegister = useCallback((method: string) => {
trackDual("user_register", {
method,
source: "client",
});
}, [trackDual]);
const trackNewsletterSignup = useCallback((email: string, source: string) => {
trackDual("newsletter_signup", {
email,
source,
});
}, [trackDual]);
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]);
return {
trackProductView,
trackAddToCart,
trackRemoveFromCart,
trackCartView,
trackCheckoutStarted,
trackCheckoutStep,
trackOrderCompleted,
trackSearch,
trackExternalLink,
trackWishlistAdd,
trackUserLogin,
trackUserRegister,
trackNewsletterSignup,
identifyUser,
};
}
export default useAnalytics;