feat: Implement comprehensive OpenPanel analytics tracking
Complete analytics overhaul with 30+ tracking events: E-commerce Events: - Product views, image views, variant selection - Add/remove from cart, quantity changes - Cart open and abandonment tracking - Checkout funnel (all steps) - Payment/shipping method selection - Order completion with revenue tracking User Engagement: - Search queries with filters - CTA clicks, external link clicks - Element engagement (click/hover/view) - Newsletter signups - Promo code usage - Wishlist actions User Identity: - User identification - Property setting - Screen/session tracking Technical: - Proper TypeScript types for all events - Increment/decrement counters - Pending revenue for cart abandonment - Comprehensive error handling Includes complete documentation in docs/ANALYTICS_GUIDE.md
This commit is contained in:
@@ -1,95 +1,282 @@
|
||||
"use client";
|
||||
|
||||
import { useOpenPanel } from "@openpanel/nextjs";
|
||||
import { useCallback } from "react";
|
||||
import { useCallback, useRef } from "react";
|
||||
|
||||
// E-commerce Events
|
||||
export type ProductViewData = {
|
||||
id: string;
|
||||
name: string;
|
||||
price: number;
|
||||
currency: string;
|
||||
category?: string;
|
||||
sku?: string;
|
||||
in_stock?: boolean;
|
||||
};
|
||||
|
||||
export type CartItemData = {
|
||||
id: string;
|
||||
name: string;
|
||||
price: number;
|
||||
currency: string;
|
||||
quantity: number;
|
||||
variant?: string;
|
||||
sku?: string;
|
||||
};
|
||||
|
||||
export type CartData = {
|
||||
total: number;
|
||||
currency: string;
|
||||
item_count: number;
|
||||
items: CartItemData[];
|
||||
coupon_code?: string;
|
||||
};
|
||||
|
||||
export type CheckoutData = {
|
||||
step: "email" | "shipping" | "billing" | "payment" | "review" | "complete";
|
||||
value?: number;
|
||||
currency?: string;
|
||||
shipping_method?: string;
|
||||
payment_method?: string;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export type OrderData = {
|
||||
order_id: string;
|
||||
order_number: string;
|
||||
total: number;
|
||||
currency: string;
|
||||
item_count: number;
|
||||
shipping_cost?: number;
|
||||
customer_email?: string;
|
||||
payment_method?: string;
|
||||
coupon_code?: string;
|
||||
};
|
||||
|
||||
// User Events
|
||||
export type UserData = {
|
||||
profileId: string;
|
||||
email?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
phone?: string;
|
||||
properties?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type SearchData = {
|
||||
query: string;
|
||||
results_count: number;
|
||||
filters?: Record<string, string>;
|
||||
category?: string;
|
||||
};
|
||||
|
||||
export type EngagementData = {
|
||||
element: string;
|
||||
action: "click" | "hover" | "scroll" | "view";
|
||||
value?: string | number;
|
||||
metadata?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export function useAnalytics() {
|
||||
const op = useOpenPanel();
|
||||
const startTimeRef = useRef<number>(Date.now());
|
||||
|
||||
// Page views are tracked automatically by OpenPanelComponent
|
||||
// but we can track specific events manually
|
||||
// ==================== E-COMMERCE EVENTS ====================
|
||||
|
||||
const trackProductView = useCallback((product: {
|
||||
id: string;
|
||||
name: string;
|
||||
price: number;
|
||||
currency: string;
|
||||
category?: string;
|
||||
}) => {
|
||||
/**
|
||||
* Track when user views a product
|
||||
*/
|
||||
const trackProductView = useCallback((product: ProductViewData) => {
|
||||
op.track("product_viewed", {
|
||||
product_id: product.id,
|
||||
product_name: product.name,
|
||||
price: product.price,
|
||||
currency: product.currency,
|
||||
category: product.category,
|
||||
sku: product.sku,
|
||||
in_stock: product.in_stock,
|
||||
});
|
||||
|
||||
// Also increment product view counter
|
||||
op.increment({ product_views: 1 });
|
||||
}, [op]);
|
||||
|
||||
/**
|
||||
* Track when user views product image gallery
|
||||
*/
|
||||
const trackProductImageView = useCallback((productId: string, imageIndex: number) => {
|
||||
op.track("product_image_viewed", {
|
||||
product_id: productId,
|
||||
image_index: imageIndex,
|
||||
});
|
||||
}, [op]);
|
||||
|
||||
const trackAddToCart = useCallback((product: {
|
||||
id: string;
|
||||
name: string;
|
||||
price: number;
|
||||
currency: string;
|
||||
quantity: number;
|
||||
variant?: string;
|
||||
}) => {
|
||||
/**
|
||||
* Track variant selection
|
||||
*/
|
||||
const trackVariantSelect = useCallback((productId: string, variantName: string, price: number) => {
|
||||
op.track("variant_selected", {
|
||||
product_id: productId,
|
||||
variant_name: variantName,
|
||||
price: price,
|
||||
});
|
||||
}, [op]);
|
||||
|
||||
/**
|
||||
* Track add to cart
|
||||
*/
|
||||
const trackAddToCart = useCallback((item: CartItemData) => {
|
||||
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,
|
||||
product_id: item.id,
|
||||
product_name: item.name,
|
||||
price: item.price,
|
||||
currency: item.currency,
|
||||
quantity: item.quantity,
|
||||
variant: item.variant,
|
||||
sku: item.sku,
|
||||
value: item.price * item.quantity,
|
||||
});
|
||||
|
||||
// Add to cart counter
|
||||
op.increment({ items_added_to_cart: item.quantity });
|
||||
}, [op]);
|
||||
|
||||
const trackRemoveFromCart = useCallback((product: {
|
||||
id: string;
|
||||
name: string;
|
||||
quantity: number;
|
||||
}) => {
|
||||
/**
|
||||
* Track remove from cart
|
||||
*/
|
||||
const trackRemoveFromCart = useCallback((item: CartItemData) => {
|
||||
op.track("remove_from_cart", {
|
||||
product_id: product.id,
|
||||
product_name: product.name,
|
||||
quantity: product.quantity,
|
||||
product_id: item.id,
|
||||
product_name: item.name,
|
||||
price: item.price,
|
||||
quantity: item.quantity,
|
||||
variant: item.variant,
|
||||
value: item.price * item.quantity,
|
||||
});
|
||||
}, [op]);
|
||||
|
||||
const trackCheckoutStarted = useCallback((cart: {
|
||||
total: number;
|
||||
currency: string;
|
||||
item_count: number;
|
||||
items: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
quantity: number;
|
||||
price: number;
|
||||
}>;
|
||||
}) => {
|
||||
/**
|
||||
* Track quantity change in cart
|
||||
*/
|
||||
const trackQuantityChange = useCallback((item: CartItemData, oldQuantity: number, newQuantity: number) => {
|
||||
op.track("quantity_changed", {
|
||||
product_id: item.id,
|
||||
product_name: item.name,
|
||||
old_quantity: oldQuantity,
|
||||
new_quantity: newQuantity,
|
||||
difference: newQuantity - oldQuantity,
|
||||
});
|
||||
}, [op]);
|
||||
|
||||
/**
|
||||
* Track cart drawer open
|
||||
*/
|
||||
const trackCartOpen = useCallback((cart: CartData) => {
|
||||
op.track("cart_opened", {
|
||||
cart_total: cart.total,
|
||||
currency: cart.currency,
|
||||
item_count: cart.item_count,
|
||||
items: cart.items.map(i => ({
|
||||
id: i.id,
|
||||
name: i.name,
|
||||
quantity: i.quantity,
|
||||
price: i.price,
|
||||
})),
|
||||
});
|
||||
}, [op]);
|
||||
|
||||
/**
|
||||
* Track cart abandonment
|
||||
*/
|
||||
const trackCartAbandonment = useCallback((cart: CartData, timeSpentMs: number) => {
|
||||
op.track("cart_abandoned", {
|
||||
cart_total: cart.total,
|
||||
currency: cart.currency,
|
||||
item_count: cart.item_count,
|
||||
items: cart.items.map(i => i.id),
|
||||
time_spent_seconds: Math.round(timeSpentMs / 1000),
|
||||
value: cart.total,
|
||||
});
|
||||
|
||||
// Track as pending revenue for recovery campaigns
|
||||
op.pendingRevenue(cart.total, {
|
||||
currency: cart.currency,
|
||||
item_count: cart.item_count,
|
||||
});
|
||||
}, [op]);
|
||||
|
||||
/**
|
||||
* Track checkout started
|
||||
*/
|
||||
const trackCheckoutStarted = useCallback((cart: CartData) => {
|
||||
startTimeRef.current = Date.now();
|
||||
|
||||
op.track("checkout_started", {
|
||||
cart_total: cart.total,
|
||||
currency: cart.currency,
|
||||
item_count: cart.item_count,
|
||||
items: cart.items,
|
||||
items: cart.items.map(i => ({
|
||||
id: i.id,
|
||||
name: i.name,
|
||||
quantity: i.quantity,
|
||||
price: i.price,
|
||||
})),
|
||||
coupon_code: cart.coupon_code,
|
||||
});
|
||||
}, [op]);
|
||||
|
||||
const trackCheckoutStep = useCallback((step: string, data?: Record<string, unknown>) => {
|
||||
op.track("checkout_step", {
|
||||
step,
|
||||
...data,
|
||||
/**
|
||||
* Track checkout step progression
|
||||
*/
|
||||
const trackCheckoutStep = useCallback((data: CheckoutData) => {
|
||||
const eventName = `checkout_${data.step}`;
|
||||
|
||||
op.track(eventName, {
|
||||
step: data.step,
|
||||
value: data.value,
|
||||
currency: data.currency,
|
||||
shipping_method: data.shipping_method,
|
||||
payment_method: data.payment_method,
|
||||
error: data.error,
|
||||
time_spent_ms: Date.now() - startTimeRef.current,
|
||||
});
|
||||
|
||||
// If there's an error, track it separately
|
||||
if (data.error) {
|
||||
op.track("checkout_error", {
|
||||
step: data.step,
|
||||
error_message: data.error,
|
||||
});
|
||||
}
|
||||
}, [op]);
|
||||
|
||||
/**
|
||||
* Track payment method selection
|
||||
*/
|
||||
const trackPaymentMethodSelect = useCallback((method: string, subtotal: number) => {
|
||||
op.track("payment_method_selected", {
|
||||
method: method,
|
||||
subtotal: subtotal,
|
||||
});
|
||||
}, [op]);
|
||||
|
||||
const trackOrderCompleted = useCallback((order: {
|
||||
order_id: string;
|
||||
order_number: string;
|
||||
total: number;
|
||||
currency: string;
|
||||
item_count: number;
|
||||
shipping_cost?: number;
|
||||
customer_email?: string;
|
||||
}) => {
|
||||
/**
|
||||
* Track shipping method selection
|
||||
*/
|
||||
const trackShippingMethodSelect = useCallback((method: string, cost: number) => {
|
||||
op.track("shipping_method_selected", {
|
||||
method: method,
|
||||
cost: cost,
|
||||
});
|
||||
}, [op]);
|
||||
|
||||
/**
|
||||
* Track order completion with revenue
|
||||
*/
|
||||
const trackOrderCompleted = useCallback((order: OrderData) => {
|
||||
const timeToComplete = Date.now() - startTimeRef.current;
|
||||
|
||||
// Track order event
|
||||
op.track("order_completed", {
|
||||
order_id: order.order_id,
|
||||
order_number: order.order_number,
|
||||
@@ -98,56 +285,196 @@ export function useAnalytics() {
|
||||
item_count: order.item_count,
|
||||
shipping_cost: order.shipping_cost,
|
||||
customer_email: order.customer_email,
|
||||
payment_method: order.payment_method,
|
||||
coupon_code: order.coupon_code,
|
||||
time_to_complete_ms: timeToComplete,
|
||||
});
|
||||
|
||||
// Also track revenue for analytics
|
||||
op.track("purchase", {
|
||||
transaction_id: order.order_number,
|
||||
value: order.total,
|
||||
// Track actual revenue
|
||||
op.revenue(order.total, {
|
||||
currency: order.currency,
|
||||
transaction_id: order.order_number,
|
||||
order_id: order.order_id,
|
||||
item_count: order.item_count,
|
||||
payment_method: order.payment_method,
|
||||
shipping_cost: order.shipping_cost,
|
||||
});
|
||||
|
||||
// Increment order counter for user
|
||||
op.increment({
|
||||
total_orders: 1,
|
||||
total_revenue: order.total,
|
||||
});
|
||||
}, [op]);
|
||||
|
||||
const trackSearch = useCallback((query: string, results_count: number) => {
|
||||
// ==================== USER ENGAGEMENT EVENTS ====================
|
||||
|
||||
/**
|
||||
* Track search queries
|
||||
*/
|
||||
const trackSearch = useCallback((data: SearchData) => {
|
||||
op.track("search", {
|
||||
query,
|
||||
results_count,
|
||||
query: data.query,
|
||||
results_count: data.results_count,
|
||||
filters: data.filters,
|
||||
category: data.category,
|
||||
});
|
||||
}, [op]);
|
||||
|
||||
const trackExternalLink = useCallback((url: string, label?: string) => {
|
||||
/**
|
||||
* Track user engagement with elements
|
||||
*/
|
||||
const trackEngagement = useCallback((data: EngagementData) => {
|
||||
op.track(`engagement_${data.action}`, {
|
||||
element: data.element,
|
||||
action: data.action,
|
||||
value: data.value,
|
||||
...data.metadata,
|
||||
});
|
||||
}, [op]);
|
||||
|
||||
/**
|
||||
* Track CTA button clicks
|
||||
*/
|
||||
const trackCTAClick = useCallback((ctaName: string, location: string, destination?: string) => {
|
||||
op.track("cta_click", {
|
||||
cta_name: ctaName,
|
||||
location: location,
|
||||
destination: destination,
|
||||
});
|
||||
}, [op]);
|
||||
|
||||
/**
|
||||
* Track external link clicks
|
||||
*/
|
||||
const trackExternalLink = useCallback((url: string, label?: string, location?: string) => {
|
||||
op.track("external_link_click", {
|
||||
url,
|
||||
label,
|
||||
url: url,
|
||||
label: label,
|
||||
location: location,
|
||||
});
|
||||
}, [op]);
|
||||
|
||||
const identifyUser = useCallback((user: {
|
||||
profileId: string;
|
||||
email?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
properties?: Record<string, unknown>;
|
||||
}) => {
|
||||
/**
|
||||
* Track newsletter signup
|
||||
*/
|
||||
const trackNewsletterSignup = useCallback((email: string, location: string) => {
|
||||
op.track("newsletter_signup", {
|
||||
email: email,
|
||||
location: location,
|
||||
});
|
||||
op.increment({ newsletter_signups: 1 });
|
||||
}, [op]);
|
||||
|
||||
/**
|
||||
* Track promo code usage
|
||||
*/
|
||||
const trackPromoCode = useCallback((code: string, discount: number, success: boolean) => {
|
||||
op.track("promo_code_applied", {
|
||||
code: code,
|
||||
discount: discount,
|
||||
success: success,
|
||||
});
|
||||
}, [op]);
|
||||
|
||||
/**
|
||||
* Track wishlist actions
|
||||
*/
|
||||
const trackWishlistAction = useCallback((action: "add" | "remove", productId: string, productName: string) => {
|
||||
op.track(`wishlist_${action}`, {
|
||||
product_id: productId,
|
||||
product_name: productName,
|
||||
});
|
||||
|
||||
if (action === "add") {
|
||||
op.increment({ wishlist_items: 1 });
|
||||
} else {
|
||||
op.decrement({ wishlist_items: 1 });
|
||||
}
|
||||
}, [op]);
|
||||
|
||||
// ==================== USER IDENTIFICATION ====================
|
||||
|
||||
/**
|
||||
* Identify user
|
||||
*/
|
||||
const identifyUser = useCallback((user: UserData) => {
|
||||
op.identify({
|
||||
profileId: user.profileId,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
email: user.email,
|
||||
properties: user.properties,
|
||||
properties: {
|
||||
phone: user.phone,
|
||||
...user.properties,
|
||||
},
|
||||
});
|
||||
}, [op]);
|
||||
|
||||
/**
|
||||
* Set user properties
|
||||
*/
|
||||
const setUserProperties = useCallback((properties: Record<string, unknown>) => {
|
||||
op.setGlobalProperties(properties);
|
||||
}, [op]);
|
||||
|
||||
// ==================== SCREEN/SESSION TRACKING ====================
|
||||
|
||||
/**
|
||||
* Track screen/page view
|
||||
*/
|
||||
const trackScreenView = useCallback((path: string, title?: string) => {
|
||||
op.screenView(path, {
|
||||
title: title,
|
||||
url: window.location.href,
|
||||
referrer: document.referrer,
|
||||
});
|
||||
}, [op]);
|
||||
|
||||
/**
|
||||
* Track session start
|
||||
*/
|
||||
const trackSessionStart = useCallback(() => {
|
||||
op.track("session_started", {
|
||||
url: window.location.href,
|
||||
referrer: document.referrer,
|
||||
user_agent: navigator.userAgent,
|
||||
screen_resolution: `${window.screen.width}x${window.screen.height}`,
|
||||
});
|
||||
}, [op]);
|
||||
|
||||
return {
|
||||
// E-commerce
|
||||
trackProductView,
|
||||
trackProductImageView,
|
||||
trackVariantSelect,
|
||||
trackAddToCart,
|
||||
trackRemoveFromCart,
|
||||
trackQuantityChange,
|
||||
trackCartOpen,
|
||||
trackCartAbandonment,
|
||||
trackCheckoutStarted,
|
||||
trackCheckoutStep,
|
||||
trackPaymentMethodSelect,
|
||||
trackShippingMethodSelect,
|
||||
trackOrderCompleted,
|
||||
|
||||
// User Engagement
|
||||
trackSearch,
|
||||
trackEngagement,
|
||||
trackCTAClick,
|
||||
trackExternalLink,
|
||||
trackNewsletterSignup,
|
||||
trackPromoCode,
|
||||
trackWishlistAction,
|
||||
|
||||
// User Identity
|
||||
identifyUser,
|
||||
setUserProperties,
|
||||
|
||||
// Session/Screen
|
||||
trackScreenView,
|
||||
trackSessionStart,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user