cart) { echo '' . "\n"; } if (empty($events)) { return; } $json = self::encode_context_json($events); if ($json === null) { return; } wp_add_inline_script( 'agenized-context', 'window.__agenized_context_events=' . $json . ';', 'before' ); } /** * Collect all context events for this request (native WP/WC only). * * @return list}> */ private static function collect_events(): array { $events = []; $url_path = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; if ( strpos( $url_path, '?' ) !== false ) { $url_path = strstr( $url_path, '?', true ) ?: $url_path; } $document_title = wp_get_document_title(); // 1. add_to_cart (from transient, set by woocommerce_add_to_cart or fragment) $add_payload = self::get_last_add_to_cart_payload(); if ($add_payload !== null) { $events[] = ['event' => 'add_to_cart', 'data' => $add_payload]; self::clear_last_add_to_cart(); } // 2. product_view (single product page) if (function_exists('is_product') && is_product()) { $product = wc_get_product(get_the_ID()); if ($product && is_a($product, 'WC_Product')) { $events[] = [ 'event' => 'product_view', 'data' => self::normalize_product_data($product, null), ]; } } // 3. cart_view (cart page) if (function_exists('is_cart') && is_cart() && function_exists('WC') && WC()->cart) { $cart = WC()->cart; $events[] = [ 'event' => 'cart_view', 'data' => [ 'cart_item_count' => $cart->get_cart_contents_count(), 'cart_total' => (float) $cart->get_total('edit'), 'currency' => get_woocommerce_currency(), ], ]; } // 3b. checkout_viewed (checkout; not thank-you — that is order_completed) if ( function_exists( 'is_checkout' ) && is_checkout() && function_exists( 'is_wc_endpoint_url' ) && ! is_wc_endpoint_url( 'order-received' ) ) { if ( strlen( $document_title ) >= 4 ) { $events[] = [ 'event' => 'checkout_viewed', 'data' => [ 'url_path' => $url_path ?: '/', 'title' => $document_title, 'url' => home_url( $url_path ?: '/' ), ], ]; } } // 4. search_performed (search results) if (is_search()) { global $wp_query; $events[] = [ 'event' => 'search_performed', 'data' => [ 'query' => get_search_query(), 'results_count' => $wp_query->found_posts ?? 0, ], ]; } // 5. checkout_error (after validation or stored from process) if (function_exists('is_checkout') && is_checkout() && !is_wc_endpoint_url('order-received')) { $error_payload = self::get_checkout_error_payload(); if ($error_payload !== null) { $events[] = ['event' => 'checkout_error', 'data' => $error_payload]; } } // 6. order_completed (thank you / order-received page) if (function_exists('is_checkout') && is_checkout() && function_exists('is_wc_endpoint_url') && is_wc_endpoint_url('order-received')) { $order_id = absint(get_query_var('order-received')); if ($order_id && function_exists('wc_get_order')) { $order = wc_get_order($order_id); if ($order && is_a($order, 'WC_Order')) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only query param used to verify order ownership on thank-you page. $key = isset($_GET['key']) ? sanitize_text_field(wp_unslash($_GET['key'])) : ''; if ($key !== '' && $order->get_order_key() === $key) { $events[] = [ 'event' => 'order_completed', 'data' => [ 'order_id' => (string) $order->get_id(), 'amount' => (float) $order->get_total(), 'currency' => $order->get_currency(), ], ]; } } } } // 7. page_view (generic pages — skip cart and checkout; those use cart_view / checkout_viewed) $emit_page_view = strlen( $document_title ) >= 4; if ( $emit_page_view && function_exists( 'is_cart' ) && is_cart() ) { $emit_page_view = false; } if ( $emit_page_view && function_exists( 'is_checkout' ) && is_checkout() && function_exists( 'is_wc_endpoint_url' ) && ! is_wc_endpoint_url( 'order-received' ) ) { $emit_page_view = false; } if ( $emit_page_view ) { $events[] = [ 'event' => 'page_view', 'data' => [ 'url_path' => $url_path ?: '/', 'title' => $document_title, ], ]; } return $events; } private static function get_session_key(): string { try { if (function_exists('WC') && WC()->session) { if (method_exists(WC()->session, 'get_customer_unique_id')) { $id = WC()->session->get_customer_unique_id(); return $id ? (string) $id : 'guest'; } if (method_exists(WC()->session, 'get_customer_id')) { $id = WC()->session->get_customer_id(); return $id ? (string) $id : 'guest'; } } } catch (\Throwable $e) { // Never break the store for a context event session key. } return 'guest'; } private static function transient_key_add(): string { return self::TRANSIENT_LAST_ADD_PREFIX . self::get_session_key(); } private static function get_last_add_to_cart_payload(): ?array { $key = self::transient_key_add(); $raw = get_transient($key); if (!is_array($raw)) { return null; } return $raw; } private static function clear_last_add_to_cart(): void { delete_transient(self::transient_key_add()); } /** * WooCommerce add to cart hook: store payload for next page load or fragment. */ public static function on_add_to_cart($cart_item_key, $product_id, $quantity, $variation_id, $variation, $cart_item_data): void { try { $product = wc_get_product($variation_id ? $variation_id : $product_id); if (!$product || !is_a($product, 'WC_Product')) { return; } $cart = function_exists('WC') && WC()->cart ? WC()->cart : null; $data = self::normalize_product_data($product, $quantity); if ($cart) { $data['cart_item_count'] = $cart->get_cart_contents_count(); $data['cart_total'] = (float) $cart->get_total('edit'); $data['currency'] = get_woocommerce_currency(); } set_transient(self::transient_key_add(), $data, self::TRANSIENT_LAST_ADD_TTL); } catch (\Throwable $e) { // Never break add-to-cart for a context event. } } /** * Add a fragment so AJAX add-to-cart can send context event. * Fragment replaces #agenized-context-add with a script that pushes the event. */ public static function add_fragment_for_add_to_cart(array $fragments): array { $payload = self::get_last_add_to_cart_payload(); if ($payload !== null) { self::clear_last_add_to_cart(); $json = self::encode_context_json($payload); if ($json !== null) { $js = '(function(){var aw=window.AgenizedWidget;if(aw&&typeof aw.pushContext==="function")aw.pushContext({event:"add_to_cart",data:' . $json . '});})();'; if (function_exists('wp_get_inline_script_tag')) { $fragments['#agenized-context-add'] = wp_get_inline_script_tag($js); } else { $fragments['#agenized-context-add'] = ''; } } } return $fragments; } /** * JSON-encode for embedding in inline <script> (hex-escapes + UTF-8 safety). * * @param mixed $data */ private static function encode_context_json($data): ?string { $options = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT; if (defined('JSON_INVALID_UTF8_SUBSTITUTE')) { $options |= JSON_INVALID_UTF8_SUBSTITUTE; } $json = wp_json_encode($data, $options); return \is_string($json) ? $json : null; } private static function normalize_product_data(\WC_Product $product, ?int $quantity): array { $data = [ 'product_id' => (string) $product->get_id(), 'name' => $product->get_name(), 'price' => (float) $product->get_price(), 'currency' => get_woocommerce_currency(), ]; if ($quantity !== null) { $data['quantity'] = $quantity; } if ($product->get_sku()) { $data['product_id'] = $product->get_sku(); } return $data; } private const TRANSIENT_CHECKOUT_ERROR = 'agenized_checkout_error'; public static function maybe_capture_checkout_error(): void { try { if (!function_exists('wc_get_notices')) { return; } $errors = wc_get_notices('error'); if (empty($errors)) { return; } $payload = [ 'error_type' => 'checkout_validation', 'step' => 'checkout', ]; set_transient(self::TRANSIENT_CHECKOUT_ERROR . '_' . self::get_session_key(), $payload, 120); } catch (\Throwable $e) { // Never break checkout for a context event. } } private static function get_checkout_error_payload(): ?array { $key = self::TRANSIENT_CHECKOUT_ERROR . '_' . self::get_session_key(); $raw = get_transient($key); if (!is_array($raw)) { return null; } delete_transient($key); return $raw; } }