291 lines
8.2 KiB
TypeScript
291 lines
8.2 KiB
TypeScript
/**
|
|
* Checkout Analytics - Multi-step checkout funnel tracking
|
|
*
|
|
* Tracks the entire checkout process from cart to purchase confirmation.
|
|
*/
|
|
|
|
import type { AnalyticsClient } from '@analytics/client';
|
|
|
|
import type { Cart, CartItem } from './cart-analytics';
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// Types
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
export type CheckoutStep =
|
|
| 'cart_review'
|
|
| 'shipping_info'
|
|
| 'shipping_method'
|
|
| 'payment_info'
|
|
| 'review_order'
|
|
| 'purchase_complete';
|
|
|
|
export interface ShippingInfo {
|
|
country: string;
|
|
region?: string;
|
|
city?: string;
|
|
postalCode?: string;
|
|
}
|
|
|
|
export interface ShippingMethod {
|
|
id: string;
|
|
name: string;
|
|
price: number;
|
|
estimatedDays?: number;
|
|
}
|
|
|
|
export interface PaymentMethod {
|
|
type: 'card' | 'paypal' | 'apple_pay' | 'google_pay' | 'bank_transfer' | string;
|
|
lastFour?: string;
|
|
}
|
|
|
|
export interface Order {
|
|
id: string;
|
|
items: CartItem[];
|
|
subtotal: number;
|
|
shipping: number;
|
|
tax: number;
|
|
total: number;
|
|
currency: string;
|
|
paymentMethod: PaymentMethod;
|
|
shippingMethod: ShippingMethod;
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// Step Tracking
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
const STEP_ORDER: CheckoutStep[] = [
|
|
'cart_review',
|
|
'shipping_info',
|
|
'shipping_method',
|
|
'payment_info',
|
|
'review_order',
|
|
'purchase_complete',
|
|
];
|
|
|
|
/**
|
|
* Track checkout step completion.
|
|
*/
|
|
export function trackCheckoutStep(
|
|
client: AnalyticsClient,
|
|
step: CheckoutStep,
|
|
cart: Cart,
|
|
metadata?: Record<string, unknown>,
|
|
): void {
|
|
const stepIndex = STEP_ORDER.indexOf(step);
|
|
|
|
client.trackEngagement({
|
|
type: 'ecommerce',
|
|
action: 'checkout_step',
|
|
metadata: {
|
|
step,
|
|
stepNumber: stepIndex + 1,
|
|
totalSteps: STEP_ORDER.length,
|
|
funnelId: 'checkout',
|
|
cartValue: cart.subtotal,
|
|
itemCount: cart.items.length,
|
|
currency: cart.currency,
|
|
...metadata,
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Track when user enters shipping information.
|
|
*/
|
|
export function trackShippingInfo(
|
|
client: AnalyticsClient,
|
|
shipping: ShippingInfo,
|
|
cart: Cart,
|
|
): void {
|
|
trackCheckoutStep(client, 'shipping_info', cart, {
|
|
country: shipping.country,
|
|
region: shipping.region,
|
|
// Don't include city/postalCode for privacy
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Track shipping method selection.
|
|
*/
|
|
export function trackShippingMethodSelect(
|
|
client: AnalyticsClient,
|
|
method: ShippingMethod,
|
|
cart: Cart,
|
|
): void {
|
|
trackCheckoutStep(client, 'shipping_method', cart, {
|
|
shippingMethodId: method.id,
|
|
shippingMethodName: method.name,
|
|
shippingCost: method.price,
|
|
estimatedDays: method.estimatedDays,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Track payment method selection.
|
|
*/
|
|
export function trackPaymentMethodSelect(
|
|
client: AnalyticsClient,
|
|
method: PaymentMethod,
|
|
cart: Cart,
|
|
): void {
|
|
trackCheckoutStep(client, 'payment_info', cart, {
|
|
paymentType: method.type,
|
|
// Never include card details, only type
|
|
});
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// Purchase Tracking
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Track successful purchase.
|
|
*
|
|
* This is the primary conversion event for e-commerce.
|
|
*/
|
|
export function trackPurchase(client: AnalyticsClient, order: Order): void {
|
|
client.trackEngagement({
|
|
type: 'ecommerce',
|
|
action: 'purchase',
|
|
metadata: {
|
|
orderId: order.id,
|
|
subtotal: order.subtotal,
|
|
shipping: order.shipping,
|
|
tax: order.tax,
|
|
total: order.total,
|
|
currency: order.currency,
|
|
itemCount: order.items.length,
|
|
paymentType: order.paymentMethod.type,
|
|
shippingMethod: order.shippingMethod.name,
|
|
// Key conversion metrics
|
|
isConversion: true,
|
|
conversionValue: order.total,
|
|
funnelId: 'checkout',
|
|
funnelStep: 'purchase_complete',
|
|
// Product breakdown
|
|
products: order.items.map((item) => ({
|
|
productId: item.id,
|
|
productName: item.name,
|
|
quantity: item.quantity,
|
|
price: item.price,
|
|
itemTotal: item.price * item.quantity,
|
|
})),
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Track checkout abandonment.
|
|
*
|
|
* Call when user leaves checkout without completing purchase.
|
|
*/
|
|
export function trackCheckoutAbandonment(
|
|
client: AnalyticsClient,
|
|
lastStep: CheckoutStep,
|
|
cart: Cart,
|
|
reason?: 'navigation' | 'error' | 'timeout' | 'browser_close',
|
|
): void {
|
|
const stepIndex = STEP_ORDER.indexOf(lastStep);
|
|
|
|
client.trackEngagement({
|
|
type: 'ecommerce',
|
|
action: 'checkout_abandonment',
|
|
metadata: {
|
|
lastStep,
|
|
stepNumber: stepIndex + 1,
|
|
totalSteps: STEP_ORDER.length,
|
|
funnelId: 'checkout',
|
|
abandonedValue: cart.subtotal,
|
|
itemCount: cart.items.length,
|
|
currency: cart.currency,
|
|
reason,
|
|
products: cart.items.map((item) => ({
|
|
productId: item.id,
|
|
quantity: item.quantity,
|
|
})),
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Track payment failure.
|
|
*/
|
|
export function trackPaymentFailure(
|
|
client: AnalyticsClient,
|
|
cart: Cart,
|
|
errorType: 'declined' | 'invalid' | 'network' | 'fraud' | string,
|
|
paymentMethod: PaymentMethod,
|
|
): void {
|
|
client.trackEngagement({
|
|
type: 'ecommerce',
|
|
action: 'payment_failure',
|
|
metadata: {
|
|
errorType,
|
|
paymentType: paymentMethod.type,
|
|
cartValue: cart.subtotal,
|
|
currency: cart.currency,
|
|
itemCount: cart.items.length,
|
|
},
|
|
});
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// React Hook
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Hook for checkout funnel tracking.
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* function CheckoutPage() {
|
|
* const { trackStep, trackPurchase, trackAbandonment } = useCheckoutFunnel(cart);
|
|
*
|
|
* return (
|
|
* <CheckoutWizard
|
|
* onStepComplete={(step) => trackStep(step)}
|
|
* onPurchase={(order) => trackPurchase(order)}
|
|
* />
|
|
* );
|
|
* }
|
|
* ```
|
|
*/
|
|
export function createCheckoutTracker(client: AnalyticsClient, cart: Cart) {
|
|
return {
|
|
trackStep: (step: CheckoutStep, metadata?: Record<string, unknown>) => {
|
|
trackCheckoutStep(client, step, cart, metadata);
|
|
},
|
|
|
|
trackShipping: (info: ShippingInfo) => {
|
|
trackShippingInfo(client, info, cart);
|
|
},
|
|
|
|
trackShippingMethod: (method: ShippingMethod) => {
|
|
trackShippingMethodSelect(client, method, cart);
|
|
},
|
|
|
|
trackPaymentMethod: (method: PaymentMethod) => {
|
|
trackPaymentMethodSelect(client, method, cart);
|
|
},
|
|
|
|
trackPurchase: (order: Order) => {
|
|
trackPurchase(client, order);
|
|
},
|
|
|
|
trackAbandonment: (
|
|
lastStep: CheckoutStep,
|
|
reason?: 'navigation' | 'error' | 'timeout' | 'browser_close',
|
|
) => {
|
|
trackCheckoutAbandonment(client, lastStep, cart, reason);
|
|
},
|
|
|
|
trackPaymentError: (
|
|
errorType: 'declined' | 'invalid' | 'network' | 'fraud' | string,
|
|
paymentMethod: PaymentMethod,
|
|
) => {
|
|
trackPaymentFailure(client, cart, errorType, paymentMethod);
|
|
},
|
|
};
|
|
}
|