feat(analytics): complete Rybbit tracking integration
Some checks failed
Build and Deploy / build (push) Has been cancelled

- 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
This commit is contained in:
Unchained
2026-03-31 05:53:53 +02:00
parent bbe618f22d
commit d4039c6e3b
4 changed files with 96 additions and 3 deletions

View File

@@ -8,6 +8,7 @@ import { X, Minus, Plus, Trash2, ShoppingBag } from "lucide-react";
import { useTranslations, useLocale } from "next-intl"; import { useTranslations, useLocale } from "next-intl";
import { useSaleorCheckoutStore } from "@/stores/saleorCheckoutStore"; import { useSaleorCheckoutStore } from "@/stores/saleorCheckoutStore";
import { formatPrice } from "@/lib/saleor"; import { formatPrice } from "@/lib/saleor";
import { useAnalytics } from "@/lib/analytics";
export default function CartDrawer() { export default function CartDrawer() {
const t = useTranslations("Cart"); const t = useTranslations("Cart");
@@ -26,11 +27,13 @@ export default function CartDrawer() {
initCheckout, initCheckout,
clearError, clearError,
} = useSaleorCheckoutStore(); } = useSaleorCheckoutStore();
const { trackCartView, trackRemoveFromCart } = useAnalytics();
const lines = getLines(); const lines = getLines();
const total = getTotal(); const total = getTotal();
const lineCount = getLineCount(); const lineCount = getLineCount();
const initializedRef = useRef(false); const initializedRef = useRef(false);
const lastCartStateRef = useRef<{ count: number; total: number } | null>(null);
useEffect(() => { useEffect(() => {
if (!initializedRef.current && locale) { if (!initializedRef.current && locale) {
@@ -52,6 +55,22 @@ export default function CartDrawer() {
}; };
}, [isOpen]); }, [isOpen]);
useEffect(() => {
if (isOpen && lines.length > 0) {
const currentState = { count: lineCount, total };
if (!lastCartStateRef.current ||
lastCartStateRef.current.count !== currentState.count ||
lastCartStateRef.current.total !== currentState.total) {
trackCartView({
total,
currency: checkout?.totalPrice?.gross?.currency || "RSD",
item_count: lineCount,
});
lastCartStateRef.current = currentState;
}
}
}, [isOpen, lineCount, total]);
return ( return (
<AnimatePresence> <AnimatePresence>
{isOpen && ( {isOpen && (
@@ -181,7 +200,14 @@ export default function CartDrawer() {
</div> </div>
<button <button
onClick={() => removeLine(line.id)} onClick={() => {
trackRemoveFromCart({
id: line.variant.product.id,
name: line.variant.product.name,
quantity: line.quantity,
});
removeLine(line.id);
}}
disabled={isLoading} disabled={isLoading}
className="p-2 text-[#999999] hover:text-red-500 transition-colors" className="p-2 text-[#999999] hover:text-red-500 transition-colors"
aria-label={t("removeItem")} aria-label={t("removeItem")}

View File

@@ -4,14 +4,17 @@ import { motion } from "framer-motion";
import { useState } from "react"; import { useState } from "react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { ArrowRight } from "lucide-react"; import { ArrowRight } from "lucide-react";
import { useAnalytics } from "@/lib/analytics";
export default function NewsletterSection() { export default function NewsletterSection() {
const t = useTranslations("Newsletter"); const t = useTranslations("Newsletter");
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
const [status, setStatus] = useState<"idle" | "success" | "error">("idle"); const [status, setStatus] = useState<"idle" | "success" | "error">("idle");
const { trackNewsletterSignup } = useAnalytics();
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
trackNewsletterSignup(email, "footer");
setStatus("success"); setStatus("success");
setEmail(""); setEmail("");
}; };

View File

@@ -9,6 +9,11 @@ const op = new OpenPanel({
apiUrl: process.env.OPENPANEL_API_URL || "https://op.nodecrew.me/api", apiUrl: process.env.OPENPANEL_API_URL || "https://op.nodecrew.me/api",
}); });
// Rybbit server-side tracking
const RYBBIT_HOST = process.env.NEXT_PUBLIC_RYBBIT_HOST || "https://rybbit.nodecrew.me";
const RYBBIT_API_KEY = process.env.RYBBIT_API_KEY;
const RYBBIT_SITE_ID = process.env.NEXT_PUBLIC_RYBBIT_SITE_ID || "1";
export interface ServerOrderData { export interface ServerOrderData {
orderId: string; orderId: string;
orderNumber: string; orderNumber: string;
@@ -26,6 +31,34 @@ export interface ServerEventData {
properties?: Record<string, any>; properties?: Record<string, any>;
} }
async function trackRybbitServer(eventName: string, properties?: Record<string, any>) {
try {
const headers: Record<string, string> = {
"Content-Type": "application/json",
};
if (RYBBIT_API_KEY) {
headers["Authorization"] = `Bearer ${RYBBIT_API_KEY}`;
}
const response = await fetch(`${RYBBIT_HOST}/api/track`, {
method: "POST",
headers,
body: JSON.stringify({
site_id: RYBBIT_SITE_ID,
type: "custom_event",
event_name: eventName,
properties: JSON.stringify(properties || {}),
}),
});
if (!response.ok) {
console.warn("[Rybbit Server] Track failed:", await response.text());
}
} catch (error) {
console.warn("[Rybbit Server] Track error:", error);
}
}
/** /**
* Server-side analytics tracking * Server-side analytics tracking
* Called from API routes or Server Components * Called from API routes or Server Components
@@ -34,7 +67,7 @@ export async function trackOrderCompletedServer(data: ServerOrderData) {
try { try {
console.log("[Server Analytics] Tracking order:", data.orderNumber, "Total:", data.total); console.log("[Server Analytics] Tracking order:", data.orderNumber, "Total:", data.total);
// Track order event // Track order event with OpenPanel
await op.track("order_completed", { await op.track("order_completed", {
order_id: data.orderId, order_id: data.orderId,
order_number: data.orderNumber, order_number: data.orderNumber,
@@ -48,7 +81,7 @@ export async function trackOrderCompletedServer(data: ServerOrderData) {
source: "server", source: "server",
}); });
// Track revenue (this is the important part!) // Track revenue with OpenPanel
await op.revenue(data.total, { await op.revenue(data.total, {
currency: data.currency, currency: data.currency,
transaction_id: data.orderNumber, transaction_id: data.orderNumber,
@@ -56,6 +89,21 @@ export async function trackOrderCompletedServer(data: ServerOrderData) {
source: "server", source: "server",
}); });
// Track conversion/revenue with Rybbit
await trackRybbitServer("order_completed", {
order_id: data.orderId,
order_number: data.orderNumber,
total: data.total,
currency: data.currency,
item_count: data.itemCount,
customer_email: data.customerEmail,
payment_method: data.paymentMethod,
shipping_cost: data.shippingCost,
coupon_code: data.couponCode,
revenue: data.total,
source: "server",
});
console.log("[Server Analytics] Order tracked successfully"); console.log("[Server Analytics] Order tracked successfully");
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
@@ -74,6 +122,10 @@ export async function trackServerEvent(data: ServerEventData) {
...data.properties, ...data.properties,
source: "server", source: "server",
}); });
// Also track to Rybbit
await trackRybbitServer(data.event, data.properties);
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
console.error("[Server Analytics] Event tracking failed:", error); console.error("[Server Analytics] Event tracking failed:", error);

View File

@@ -15,6 +15,7 @@ import {
trackRybbitWishlistAdd, trackRybbitWishlistAdd,
trackRybbitUserLogin, trackRybbitUserLogin,
trackRybbitUserRegister, trackRybbitUserRegister,
trackRybbitNewsletterSignup,
} from "@/lib/services/RybbitService"; } from "@/lib/services/RybbitService";
export function useAnalytics() { export function useAnalytics() {
@@ -109,6 +110,9 @@ export function useAnalytics() {
case "user_register": case "user_register":
trackRybbitUserRegister(openPanelData.method); trackRybbitUserRegister(openPanelData.method);
break; break;
case "newsletter_signup":
trackRybbitNewsletterSignup(openPanelData.email, openPanelData.source);
break;
} }
} catch (e) { } catch (e) {
console.warn("[Rybbit] Tracking error:", e); console.warn("[Rybbit] Tracking error:", e);
@@ -307,6 +311,13 @@ export function useAnalytics() {
}); });
}, [trackDual]); }, [trackDual]);
const trackNewsletterSignup = useCallback((email: string, source: string) => {
trackDual("newsletter_signup", {
email,
source,
});
}, [trackDual]);
const identifyUser = useCallback((user: { const identifyUser = useCallback((user: {
profileId: string; profileId: string;
email?: string; email?: string;
@@ -338,6 +349,7 @@ export function useAnalytics() {
trackWishlistAdd, trackWishlistAdd,
trackUserLogin, trackUserLogin,
trackUserRegister, trackUserRegister,
trackNewsletterSignup,
identifyUser, identifyUser,
}; };
} }