feat(analytics): complete Rybbit tracking integration
Some checks failed
Build and Deploy / build (push) Has been cancelled
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:
@@ -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")}
|
||||||
|
|||||||
@@ -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("");
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user