diff --git a/src/app/api/analytics/track-order/route.ts b/src/app/api/analytics/track-order/route.ts new file mode 100644 index 0000000..4ebf361 --- /dev/null +++ b/src/app/api/analytics/track-order/route.ts @@ -0,0 +1,62 @@ +import { NextRequest, NextResponse } from "next/server"; +import { trackOrderCompletedServer, trackServerEvent } from "@/lib/analytics-server"; + +/** + * POST /api/analytics/track-order + * + * Server-side order tracking endpoint + * Called from client after successful order completion + */ +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + + const { + orderId, + orderNumber, + total, + currency, + itemCount, + customerEmail, + paymentMethod, + shippingCost, + couponCode, + } = body; + + // Validate required fields + if (!orderId || !orderNumber || total === undefined) { + return NextResponse.json( + { error: "Missing required fields" }, + { status: 400 } + ); + } + + // Track server-side + const result = await trackOrderCompletedServer({ + orderId, + orderNumber, + total, + currency: currency || "RSD", + itemCount: itemCount || 0, + customerEmail, + paymentMethod, + shippingCost, + couponCode, + }); + + if (result.success) { + return NextResponse.json({ success: true }); + } else { + return NextResponse.json( + { error: result.error }, + { status: 500 } + ); + } + } catch (error) { + console.error("[API Analytics] Error:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 } + ); + } +} diff --git a/src/lib/analytics-server.ts b/src/lib/analytics-server.ts new file mode 100644 index 0000000..f451d9e --- /dev/null +++ b/src/lib/analytics-server.ts @@ -0,0 +1,98 @@ +"use server"; + +import { OpenPanel } from "@openpanel/nextjs"; + +// Server-side OpenPanel instance +const op = new OpenPanel({ + clientId: process.env.NEXT_PUBLIC_OPENPANEL_CLIENT_ID || "", + clientSecret: process.env.OPENPANEL_CLIENT_SECRET || "", + apiUrl: process.env.OPENPANEL_API_URL || "https://op.nodecrew.me/api", +}); + +export interface ServerOrderData { + orderId: string; + orderNumber: string; + total: number; + currency: string; + itemCount: number; + customerEmail?: string; + paymentMethod?: string; + shippingCost?: number; + couponCode?: string; +} + +export interface ServerEventData { + event: string; + properties?: Record; +} + +/** + * Server-side analytics tracking + * Called from API routes or Server Components + */ +export async function trackOrderCompletedServer(data: ServerOrderData) { + try { + console.log("[Server Analytics] Tracking order:", data.orderNumber, "Total:", data.total); + + // Track order event + await op.track("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, + source: "server", + }); + + // Track revenue (this is the important part!) + await op.revenue(data.total, { + currency: data.currency, + transaction_id: data.orderNumber, + order_id: data.orderId, + source: "server", + }); + + console.log("[Server Analytics] Order tracked successfully"); + return { success: true }; + } catch (error) { + console.error("[Server Analytics] Failed to track order:", error); + // Don't throw - analytics shouldn't break the app + return { success: false, error: String(error) }; + } +} + +/** + * Track any server-side event + */ +export async function trackServerEvent(data: ServerEventData) { + try { + await op.track(data.event, { + ...data.properties, + source: "server", + }); + return { success: true }; + } catch (error) { + console.error("[Server Analytics] Event tracking failed:", error); + return { success: false, error: String(error) }; + } +} + +/** + * Identify user server-side + */ +export async function identifyUserServer(profileId: string, properties?: Record) { + try { + await op.identify({ + profileId, + ...properties, + }); + return { success: true }; + } catch (error) { + console.error("[Server Analytics] Identify failed:", error); + return { success: false, error: String(error) }; + } +} diff --git a/src/lib/analytics.ts b/src/lib/analytics.ts index da9e584..3d6d1ea 100644 --- a/src/lib/analytics.ts +++ b/src/lib/analytics.ts @@ -6,6 +6,7 @@ import { useCallback } from "react"; export function useAnalytics() { const op = useOpenPanel(); + // Client-side tracking for user behavior const trackProductView = useCallback((product: { id: string; name: string; @@ -20,9 +21,10 @@ export function useAnalytics() { price: product.price, currency: product.currency, category: product.category, + source: "client", }); } catch (e) { - console.error("Track error:", e); + console.error("[Client Analytics] Product view error:", e); } }, [op]); @@ -42,9 +44,10 @@ export function useAnalytics() { currency: product.currency, quantity: product.quantity, variant: product.variant, + source: "client", }); } catch (e) { - console.error("Track error:", e); + console.error("[Client Analytics] Add to cart error:", e); } }, [op]); @@ -58,9 +61,10 @@ export function useAnalytics() { product_id: product.id, product_name: product.name, quantity: product.quantity, + source: "client", }); } catch (e) { - console.error("Track error:", e); + console.error("[Client Analytics] Remove from cart error:", e); } }, [op]); @@ -81,9 +85,10 @@ export function useAnalytics() { currency: cart.currency, item_count: cart.item_count, items: cart.items, + source: "client", }); } catch (e) { - console.error("Track error:", e); + console.error("[Client Analytics] Checkout started error:", e); } }, [op]); @@ -92,13 +97,19 @@ export function useAnalytics() { op.track("checkout_step", { step, ...data, + source: "client", }); } catch (e) { - console.error("Track error:", e); + console.error("[Client Analytics] Checkout step error:", e); } }, [op]); - const trackOrderCompleted = useCallback((order: { + /** + * 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; total: number; @@ -106,11 +117,12 @@ export function useAnalytics() { item_count: number; shipping_cost?: number; customer_email?: string; + payment_method?: string; }) => { + console.log("[Dual Analytics] Tracking order:", order.order_number, "Total:", order.total); + + // CLIENT-SIDE: Track immediately for user session data try { - console.log("[Analytics] Tracking order completed:", order.order_number, "Total:", order.total, order.currency); - - // Track order event op.track("order_completed", { order_id: order.order_id, order_number: order.order_number, @@ -119,20 +131,47 @@ export function useAnalytics() { item_count: order.item_count, shipping_cost: order.shipping_cost, customer_email: order.customer_email, + payment_method: order.payment_method, + source: "client", }); - // Track revenue - console.log("[Analytics] Tracking revenue:", order.total, order.currency); op.revenue(order.total, { currency: order.currency, transaction_id: order.order_number, - }).then((result) => { - console.log("[Analytics] Revenue tracked successfully:", result); - }).catch((err) => { - console.error("[Analytics] Revenue tracking failed:", err); + source: "client", }); + + console.log("[Client Analytics] Order tracked"); } catch (e) { - console.error("[Analytics] Track error:", e); + console.error("[Client Analytics] Order tracking error:", e); + } + + // SERVER-SIDE: Call API for reliable tracking + 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" }, + 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.log("[Server Analytics] Order tracked successfully"); + } else { + console.error("[Server Analytics] Failed:", await response.text()); + } + } catch (e) { + console.error("[Server Analytics] API call failed:", e); } }, [op]); @@ -141,9 +180,10 @@ export function useAnalytics() { op.track("search", { query, results_count, + source: "client", }); } catch (e) { - console.error("Track error:", e); + console.error("[Client Analytics] Search error:", e); } }, [op]); @@ -152,9 +192,10 @@ export function useAnalytics() { op.track("external_link_click", { url, label, + source: "client", }); } catch (e) { - console.error("Track error:", e); + console.error("[Client Analytics] External link error:", e); } }, [op]); @@ -172,7 +213,7 @@ export function useAnalytics() { email: user.email, }); } catch (e) { - console.error("Identify error:", e); + console.error("[Client Analytics] Identify error:", e); } }, [op]);