feat: integrate Rybbit analytics alongside OpenPanel
Some checks failed
Build and Deploy / build (push) Has been cancelled

- Add RybbitService for tracking e-commerce events
- Update useAnalytics hook to track with both OpenPanel and Rybbit
- Add Rybbit script to layout for page view tracking
- Track all applicable store events: product views, cart, checkout, orders, search, etc.
This commit is contained in:
Unchained
2026-03-31 00:38:38 +02:00
parent 044aefae94
commit b3efebd3e4
11 changed files with 1422 additions and 118 deletions

View File

@@ -3,6 +3,8 @@ import { NextIntlClientProvider } from "next-intl";
import { getMessages, setRequestLocale } from "next-intl/server";
import { SUPPORTED_LOCALES, DEFAULT_LOCALE, isValidLocale } from "@/lib/i18n/locales";
import { OpenPanelComponent } from "@openpanel/nextjs";
import Script from "next/script";
import { RYBBIT_HOST, RYBBIT_SITE_ID } from "@/lib/services/RybbitService";
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://dev.manoonoils.com";
@@ -46,13 +48,18 @@ export default async function LocaleLayout({
return (
<>
<OpenPanelComponent
clientId={process.env.NEXT_PUBLIC_OPENPANEL_CLIENT_ID || ""}
trackScreenViews={true}
trackOutgoingLinks={true}
apiUrl="https://op.nodecrew.me/api"
scriptUrl="https://op.nodecrew.me/op1.js"
/>
<OpenPanelComponent
clientId={process.env.NEXT_PUBLIC_OPENPANEL_CLIENT_ID || ""}
trackScreenViews={true}
trackOutgoingLinks={true}
apiUrl="https://op.nodecrew.me/api"
scriptUrl="https://op.nodecrew.me/op1.js"
/>
<Script
src={`${RYBBIT_HOST}/api/script.js`}
data-site-id={RYBBIT_SITE_ID}
strategy="afterInteractive"
/>
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>

View File

@@ -2,11 +2,119 @@
import { useOpenPanel } from "@openpanel/nextjs";
import { useCallback } from "react";
import {
trackRybbitProductView,
trackRybbitAddToCart,
trackRybbitRemoveFromCart,
trackRybbitCheckoutStarted,
trackRybbitCheckoutStep,
trackRybbitOrderCompleted,
trackRybbitSearch,
trackRybbitExternalLink,
trackRybbitCartView,
trackRybbitWishlistAdd,
trackRybbitUserLogin,
trackRybbitUserRegister,
} from "@/lib/services/RybbitService";
export function useAnalytics() {
const op = useOpenPanel();
// Client-side tracking for user behavior
// 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;
}
} catch (e) {
console.warn("[Rybbit] Tracking error:", e);
}
}, [op]);
const trackProductView = useCallback((product: {
id: string;
name: string;
@@ -14,19 +122,15 @@ export function useAnalytics() {
currency: string;
category?: string;
}) => {
try {
op.track("product_viewed", {
product_id: product.id,
product_name: product.name,
price: product.price,
currency: product.currency,
category: product.category,
source: "client",
});
} catch (e) {
console.error("[Client Analytics] Product view error:", e);
}
}, [op]);
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;
@@ -36,37 +140,42 @@ export function useAnalytics() {
quantity: number;
variant?: string;
}) => {
try {
op.track("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",
});
} catch (e) {
console.error("[Client Analytics] Add to cart error:", e);
}
}, [op]);
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;
}) => {
try {
op.track("remove_from_cart", {
product_id: product.id,
product_name: product.name,
quantity: product.quantity,
source: "client",
});
} catch (e) {
console.error("[Client Analytics] Remove from cart error:", e);
}
}, [op]);
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;
@@ -79,36 +188,23 @@ export function useAnalytics() {
price: number;
}>;
}) => {
try {
op.track("checkout_started", {
cart_total: cart.total,
currency: cart.currency,
item_count: cart.item_count,
items: cart.items,
source: "client",
});
} catch (e) {
console.error("[Client Analytics] Checkout started error:", e);
}
}, [op]);
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>) => {
try {
op.track("checkout_step", {
step,
...data,
source: "client",
});
} catch (e) {
console.error("[Client Analytics] Checkout step error:", e);
}
}, [op]);
trackDual("checkout_step", {
step,
...data,
source: "client",
});
}, [trackDual]);
/**
* DUAL TRACKING: Order completion
* 1. Track client-side (immediate, captures user session)
* 2. Call server-side API (reliable, can't be blocked)
*/
const trackOrderCompleted = useCallback(async (order: {
order_id: string;
order_number: string;
@@ -119,37 +215,34 @@ export function useAnalytics() {
customer_email?: string;
payment_method?: string;
}) => {
console.log("[Dual Analytics] Tracking order:", order.order_number, "Total:", order.total);
console.log("[Analytics] Tracking order:", order.order_number);
// CLIENT-SIDE: Track immediately for user session data
// 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.track("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",
});
op.revenue(order.total, {
currency: order.currency,
transaction_id: order.order_number,
source: "client",
});
console.log("[Client Analytics] Order tracked");
} catch (e) {
console.error("[Client Analytics] Order tracking error:", e);
console.error("[OpenPanel] Revenue tracking error:", e);
}
// SERVER-SIDE: Call API for reliable tracking
// Server-side tracking for reliability
try {
console.log("[Server Analytics] Calling server-side tracking API...");
const response = await fetch("/api/analytics/track-order", {
method: "POST",
headers: { "Content-Type": "application/json" },
@@ -165,39 +258,54 @@ export function useAnalytics() {
}),
});
if (response.ok) {
console.log("[Server Analytics] Order tracked successfully");
} else {
if (!response.ok) {
console.error("[Server Analytics] Failed:", await response.text());
}
} catch (e) {
console.error("[Server Analytics] API call failed:", e);
}
}, [op]);
}, [op, trackDual]);
const trackSearch = useCallback((query: string, results_count: number) => {
try {
op.track("search", {
query,
results_count,
source: "client",
});
} catch (e) {
console.error("[Client Analytics] Search error:", e);
}
}, [op]);
trackDual("search", {
query,
results_count,
source: "client",
});
}, [trackDual]);
const trackExternalLink = useCallback((url: string, label?: string) => {
try {
op.track("external_link_click", {
url,
label,
source: "client",
});
} catch (e) {
console.error("[Client Analytics] External link error:", e);
}
}, [op]);
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 identifyUser = useCallback((user: {
profileId: string;
@@ -213,7 +321,7 @@ export function useAnalytics() {
email: user.email,
});
} catch (e) {
console.error("[Client Analytics] Identify error:", e);
console.error("[OpenPanel] Identify error:", e);
}
}, [op]);
@@ -221,11 +329,15 @@ export function useAnalytics() {
trackProductView,
trackAddToCart,
trackRemoveFromCart,
trackCartView,
trackCheckoutStarted,
trackCheckoutStep,
trackOrderCompleted,
trackSearch,
trackExternalLink,
trackWishlistAdd,
trackUserLogin,
trackUserRegister,
identifyUser,
};
}

View File

@@ -0,0 +1,209 @@
"use client";
// Rybbit Analytics Service
// Self-hosted instance at rybbit.nodecrew.me
declare global {
interface Window {
rybbit?: {
event: (eventName: string, eventData?: Record<string, any>) => void;
pageview: () => void;
};
}
}
export const RYBBIT_HOST = "https://rybbit.nodecrew.me";
export const RYBBIT_SITE_ID = "1";
/**
* Check if Rybbit is loaded and available
*/
export function isRybbitAvailable(): boolean {
return typeof window !== "undefined" &&
!!window.rybbit &&
typeof window.rybbit.event === "function";
}
/**
* Track a custom event with Rybbit
*/
export function trackRybbitEvent(
eventName: string,
eventData?: Record<string, any>
): void {
if (isRybbitAvailable()) {
try {
window.rybbit!.event(eventName, eventData);
} catch (e) {
console.warn("[Rybbit] Event tracking error:", e);
}
} else {
console.warn("[Rybbit] Not available for event:", eventName);
}
}
/**
* Track page view manually (usually auto-tracked by Rybbit script)
*/
export function trackRybbitPageview(): void {
if (isRybbitAvailable()) {
try {
window.rybbit!.pageview();
} catch (e) {
console.warn("[Rybbit] Pageview error:", e);
}
}
}
// E-commerce Event Tracking Functions
export function trackRybbitProductView(product: {
id: string;
name: string;
price: number;
currency: string;
category?: string;
variant?: string;
}): void {
trackRybbitEvent("product_view", {
product_id: product.id,
product_name: product.name,
price: product.price,
currency: product.currency,
category: product.category,
variant: product.variant,
});
}
export function trackRybbitAddToCart(product: {
id: string;
name: string;
price: number;
currency: string;
quantity: number;
variant?: string;
}): void {
trackRybbitEvent("add_to_cart", {
product_id: product.id,
product_name: product.name,
price: product.price,
currency: product.currency,
quantity: product.quantity,
variant: product.variant,
});
}
export function trackRybbitRemoveFromCart(product: {
id: string;
name: string;
quantity: number;
}): void {
trackRybbitEvent("remove_from_cart", {
product_id: product.id,
product_name: product.name,
quantity: product.quantity,
});
}
export function trackRybbitCartView(cart: {
total: number;
currency: string;
item_count: number;
}): void {
trackRybbitEvent("cart_view", {
cart_total: cart.total,
currency: cart.currency,
item_count: cart.item_count,
});
}
export function trackRybbitCheckoutStarted(cart: {
total: number;
currency: string;
item_count: number;
items: Array<{
id: string;
name: string;
quantity: number;
price: number;
}>;
}): void {
trackRybbitEvent("checkout_started", {
cart_total: cart.total,
currency: cart.currency,
item_count: cart.item_count,
items: cart.items,
});
}
export function trackRybbitCheckoutStep(step: string, data?: Record<string, unknown>): void {
trackRybbitEvent("checkout_step", {
step,
...data,
});
}
export function trackRybbitOrderCompleted(order: {
order_id: string;
order_number: string;
total: number;
currency: string;
item_count: number;
shipping_cost?: number;
customer_email?: string;
payment_method?: string;
}): void {
trackRybbitEvent("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,
});
}
export function trackRybbitSearch(query: string, results_count: number): void {
trackRybbitEvent("search", {
query,
results_count,
});
}
export function trackRybbitExternalLink(url: string, label?: string): void {
trackRybbitEvent("external_link_click", {
url,
label,
});
}
export function trackRybbitNewsletterSignup(email: string, source: string): void {
trackRybbitEvent("newsletter_signup", {
email,
source,
});
}
export function trackRybbitWishlistAdd(product: {
id: string;
name: string;
}): void {
trackRybbitEvent("wishlist_add", {
product_id: product.id,
product_name: product.name,
});
}
export function trackRybbitUserLogin(method: string): void {
trackRybbitEvent("user_login", {
method,
});
}
export function trackRybbitUserRegister(method: string): void {
trackRybbitEvent("user_register", {
method,
});
}