$max) { self::count_clamp($key); return $max; } return $v; } public static function is_bot_user_agent($ua) { $ua = strtolower(trim((string) $ua)); if ('' === $ua) { return true; } foreach (array('cubot', 'robot vacuum') as $safe) { if (false !== strpos($ua, $safe)) { $ua = str_replace($safe, '', $ua); } } $named = array( 'externalagent', 'externalads', 'facebookexternalhit', 'storebot', 'googlebot', 'bingbot', 'yandex', 'baiduspider', 'duckduckbot', 'slurp', 'petalbot', 'applebot', 'bytespider', 'ahrefsbot', 'semrushbot', 'mj12bot', 'dotbot', 'dataforseo', 'screaming frog', 'gptbot', 'claudebot', 'anthropic-ai', 'perplexitybot', 'ccbot', 'chatgpt-user', 'oai-searchbot', 'google-extended', 'headlesschrome', 'phantomjs', 'puppeteer', 'playwright', 'lighthouse', 'pagespeed', 'gtmetrix', 'pingdom', 'uptimerobot', 'python-requests', 'python-urllib', 'go-http-client', 'okhttp', 'java/', 'curl/', 'wget/', 'axios/', 'node-fetch', 'guzzlehttp', 'libwww-perl', 'scrapy', 'httpclient', 'postman', ); foreach ($named as $needle) { if (false !== strpos($ua, $needle)) { return true; } } if (preg_match('/(^|[^a-z])(bot|crawler|spider|scraper|monitor)([^a-z]|$)/i', $ua)) { return true; } return (bool) apply_filters('aieo_is_bot_user_agent', false, $ua); } private static function count_bot_rejection($ua) { $k = 'aieo_tracker_bots_rejected'; $c = get_transient($k); $c = is_array($c) ? $c : array(); $ua = strtolower((string) $ua); $sig = 'other'; foreach (array('externalagent', 'externalads', 'storebot', 'googlebot', 'bingbot', 'gptbot', 'claudebot', 'bytespider', 'ahrefsbot', 'semrushbot', 'headlesschrome') as $needle) { if (false !== strpos($ua, $needle)) { $sig = $needle; break; } } $c[$sig] = 1 + (isset($c[$sig]) ? (int) $c[$sig] : 0); $c['_total'] = 1 + (isset($c['_total']) ? (int) $c['_total'] : 0); set_transient($k, $c, DAY_IN_SECONDS); } private static function count_clamp($key) { $k = 'aieo_tracker_clamped'; $c = get_transient($k); $c = is_array($c) ? $c : array(); $c[$key] = 1 + (isset($c[$key]) ? (int) $c[$key] : 0); set_transient($k, $c, DAY_IN_SECONDS); } const CLOCK_FLOOR_MS = 1577836800000; const CLOCK_FUTURE_MS = 172800000; private static function clamp_event_timestamp($raw) { $v = (int) $raw; $now = (int) (time() * 1000); if ($v < self::CLOCK_FLOOR_MS || $v > ($now + self::CLOCK_FUTURE_MS)) { self::count_clamp('timestamp'); return $now; } return $v; } private static $instance = null; public static function get_instance() { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } private function __construct() { add_action('rest_api_init', array($this, 'register_routes')); add_action('wp_enqueue_scripts', array($this, 'enqueue_tracking_script')); add_action('wp_head', array($this, 'print_cart_add_detector'), 2); add_filter('rocket_delay_js_exclusions', array($this, 'rocket_delay_exclusions')); add_filter('cron_schedules', array($this, 'add_custom_cron_schedules')); add_action('aieo_cleanup_old_sessions', array($this, 'cleanup_old_sessions')); add_action('aieo_archive_sessions', array($this, 'archive_sessions')); if ( (int) get_option( 'aieo_session_archive_rolling_v', 0 ) < 1 ) { update_option( 'aieo_session_archive_frequency', 'daily', false ); $this->schedule_archive_cron(); update_option( 'aieo_session_archive_rolling_v', 1, false ); } if (!wp_next_scheduled('aieo_archive_sessions')) { $this->schedule_archive_cron(); } } public function add_custom_cron_schedules($schedules) { if (!isset($schedules['weekly'])) { $schedules['weekly'] = array( 'interval' => 604800, 'display' => __('Once Weekly', 'ai-eshop-optimizer') ); } if (!isset($schedules['monthly'])) { $schedules['monthly'] = array( 'interval' => 2592000, 'display' => __('Once Monthly', 'ai-eshop-optimizer') ); } return $schedules; } public function cleanup_old_sessions() { global $wpdb; $archive_table = $wpdb->prefix . 'aieo_sessions_archive'; $archive_statics_table = $wpdb->prefix . 'aieo_sessions_statics_archive'; $retention_days = (int) get_option('aieo_session_retention_days', 30); $deleted_events = $wpdb->query($wpdb->prepare( "DELETE FROM {$archive_table} WHERE archived_at < DATE_SUB(UTC_TIMESTAMP(), INTERVAL %d DAY)", $retention_days )); $deleted_statics = $wpdb->query($wpdb->prepare( "DELETE FROM {$archive_statics_table} WHERE archived_at < DATE_SUB(UTC_TIMESTAMP(), INTERVAL %d DAY)", $retention_days )); if ($deleted_events > 0 || $deleted_statics > 0) { if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log("AIEO: Cleaned up archived session data - {$deleted_events} events, {$deleted_statics} sessions older than {$retention_days} days"); } } } public function schedule_archive_cron() { $timestamp = wp_next_scheduled('aieo_archive_sessions'); if ($timestamp) { wp_unschedule_event($timestamp, 'aieo_archive_sessions'); } $frequency = get_option('aieo_session_archive_frequency', 'daily'); $hour = (int) get_option('aieo_session_archive_hour', 4); $now = current_time('timestamp', true); $next_run = strtotime('today ' . sprintf('%02d:00:00', $hour), $now); if ($next_run <= $now) { $next_run = strtotime('tomorrow ' . sprintf('%02d:00:00', $hour), $now); } wp_schedule_event($next_run, $frequency, 'aieo_archive_sessions'); } public function archive_sessions() { if ('1' !== (string) get_option('aieo_session_tracking_enabled', '0')) { return; } global $wpdb; $sessions_table = $wpdb->prefix . 'aieo_sessions'; $statics_table = $wpdb->prefix . 'aieo_sessions_statics'; $archive_table = $wpdb->prefix . 'aieo_sessions_archive'; $archive_statics_table = $wpdb->prefix . 'aieo_sessions_statics_archive'; $event_days = max( 1, (int) apply_filters( 'aieo_session_hot_days', (int) get_option( 'aieo_session_hot_days', 3 ) ) ); $statics_days = max( $event_days, (int) get_option( 'aieo_session_retention_days', 30 ) ); $now = current_time( 'timestamp', true ); $event_cutoff_ms = ( $now - $event_days * DAY_IN_SECONDS ) * 1000; $statics_cutoff_ms = ( $now - $statics_days * DAY_IN_SECONDS ) * 1000; if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log( 'AIEO: rolling session prune at ' . gmdate( 'Y-m-d H:i:s', $now ) . " GMT (events>{$event_days}d, statics>{$statics_days}d)" ); } try { $events_max_id = (int) apply_filters( 'aieo_archive_move_max_id', (int) $wpdb->get_var( "SELECT COALESCE(MAX(id), 0) FROM {$sessions_table}" ), 'aieo_sessions' ); $events_copied = $wpdb->query( $wpdb->prepare( "INSERT INTO {$archive_table} (session_id, user_id, event_type, product_id, product_name, category_id, category_name, scroll_depth, click_count, event_sequence, is_first_event, is_last_event, is_only_event, time_on_page, visit_number, order_id, checkout_step, event_data, page_url, timestamp, created_at, archived_at) SELECT session_id, user_id, event_type, product_id, product_name, category_id, category_name, scroll_depth, click_count, event_sequence, is_first_event, is_last_event, is_only_event, time_on_page, visit_number, order_id, checkout_step, event_data, page_url, timestamp, created_at, UTC_TIMESTAMP() FROM {$sessions_table} WHERE timestamp < %d AND id <= %d", $event_cutoff_ms, $events_max_id ) ); $events_pruned = ( false === $events_copied || $events_max_id < 1 ) ? 0 : self::batched_delete( $sessions_table, 'timestamp', $event_cutoff_ms, 5000, 400, $events_max_id ); $aieo_canon_cols = ''; $aieo_canon_set = array( 'landing_path', 'utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term', 'utm_id', 'exit_path', 'referrer_host', 'channel', 'pages_viewed', 'value_tier' ); $aieo_stat_cols = (array) $wpdb->get_col( "SHOW COLUMNS FROM {$statics_table}" ); $aieo_arch_cols = (array) $wpdb->get_col( "SHOW COLUMNS FROM {$archive_statics_table}" ); $aieo_shared = array_values( array_intersect( $aieo_canon_set, $aieo_stat_cols, $aieo_arch_cols ) ); if ( ! empty( $aieo_shared ) ) { $aieo_canon_cols = ', ' . implode( ', ', $aieo_shared ); } $statics_max_id = (int) $wpdb->get_var( "SELECT COALESCE(MAX(id), 0) FROM {$statics_table}" ); $statics_copied = $wpdb->query( $wpdb->prepare( "INSERT INTO {$archive_statics_table} (session_id, user_id, user_agent, device_type, browser, os, landing_page, referrer, first_timestamp, last_timestamp, total_events, total_product_views, total_add_to_cart, total_scroll_events, total_click_events, session_duration_seconds, created_at, updated_at{$aieo_canon_cols}, archived_at) SELECT session_id, user_id, user_agent, device_type, browser, os, landing_page, referrer, first_timestamp, last_timestamp, total_events, total_product_views, total_add_to_cart, total_scroll_events, total_click_events, session_duration_seconds, created_at, updated_at{$aieo_canon_cols}, UTC_TIMESTAMP() FROM {$statics_table} WHERE last_timestamp < %d AND id <= %d", $statics_cutoff_ms, $statics_max_id ) ); $statics_pruned = ( false === $statics_copied || $statics_max_id < 1 ) ? 0 : self::batched_delete( $statics_table, 'last_timestamp', $statics_cutoff_ms, 5000, 400, $statics_max_id ); if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log( "AIEO: rolling prune aged out {$events_pruned} events, {$statics_pruned} statics" ); } do_action( 'aieo_archive_run', current_time( 'mysql', true ) ); } catch (Exception $e) { if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log('AIEO: Session archival failed - ' . $e->getMessage()); } } } private static function batched_delete( $table, $ts_col, $cutoff_ms, $batch = 5000, $max_iter = 400, $max_id = 0 ) { global $wpdb; $total = 0; $ids = $max_id > 0 ? ' AND id <= ' . (int) $max_id : ''; for ( $i = 0; $i < $max_iter; $i++ ) { $deleted = (int) $wpdb->query( $wpdb->prepare( "DELETE FROM {$table} WHERE {$ts_col} < %d{$ids} LIMIT %d", $cutoff_ms, $batch ) ); $total += $deleted; if ( $deleted < $batch ) { break; } } return $total; } public function register_routes() { register_rest_route('aieo/v1', '/track', array( 'methods' => 'POST', 'callback' => array($this, 'handle_tracking_batch'), 'permission_callback' => '__return_true', )); register_rest_route('aieo/v1', '/session/(?P[a-zA-Z0-9_-]+)', array( 'methods' => 'GET', 'callback' => array($this, 'get_session_context'), 'permission_callback' => array($this, 'can_read_session_context'), )); } public function can_read_session_context($request) { if (current_user_can('manage_woocommerce') || current_user_can('edit_pages')) { return true; } $requested = (string) $request['session_id']; if ($requested !== '') { foreach (array('aieo_attr_sid', 'aieo_session_id') as $cookie_name) { if (!isset($_COOKIE[$cookie_name])) { continue; } $own = sanitize_text_field(wp_unslash($_COOKIE[$cookie_name])); if ($own !== '' && hash_equals($own, $requested)) { return true; } } } return new WP_Error( 'aieo_session_forbidden', __('You can only read your own session context.', 'ai-eshop-optimizer'), array('status' => rest_authorization_required_code()) ); } public function enqueue_tracking_script() { $tracking_enabled = get_option('aieo_session_tracking_enabled', '0'); if ($tracking_enabled != '1' && $tracking_enabled !== 1) { return; } add_action('wp_enqueue_scripts', array($this, 'enqueue_tracker_loader'), 20); } public function rocket_delay_exclusions($list) { $list = is_array($list) ? $list : array(); $list[] = 'aieo-session-tracker'; $list[] = 'aieo-cart-add-detector'; $list[] = 'aieo-session-loader'; return array_values(array_unique($list)); } public function cart_detector_wanted() { if (is_admin()) { return false; } $st = get_option('aieo_session_tracking_enabled', '0'); if ($st == '1') { return true; } return class_exists('WooCommerce') && (bool) apply_filters('aieo_attribution_client_enabled', true); } public static function cart_add_detector_js() { $file = dirname(__DIR__) . '/assets/js/aieo-cart-add-detector.js'; return is_readable($file) ? (string) file_get_contents($file) : ''; } public function print_cart_add_detector() { if (!$this->cart_detector_wanted()) { return; } $dir = dirname(__DIR__) . '/'; $rel = 'assets/js/aieo-cart-add-detector.js'; if (!file_exists($dir . $rel)) { return; } wp_enqueue_script('aieo-cart-add-detector', plugins_url($rel, $dir . 'plugin.php'), array(), (string) filemtime($dir . $rel), array('in_footer' => false, 'strategy' => 'defer')); } public static function consent_required_rule() { $enabled = get_option('aieo_session_tracking_enabled', '0'); if ($enabled !== '1' && $enabled !== 1) { return false; } $bypass = get_option('aieo_session_bypass_consent', '1'); if ($bypass == '1' || $bypass === 1) { return false; } $require = get_option('aieo_session_tracking_require_consent', '1'); return ($require == '1' || $require === 1); } private static function cookieyes_state() { if (!isset($_COOKIE['cookieyes-consent'])) { return null; } $raw = stripslashes(sanitize_text_field( wp_unslash( $_COOKIE['cookieyes-consent'] ) )); $decoded = json_decode($raw, true); if (is_array($decoded)) { return $decoded; } $decoded = json_decode(rawurldecode($raw), true); if (is_array($decoded)) { return $decoded; } $out = array(); foreach (explode(',', rawurldecode($raw)) as $pair) { $kv = explode(':', $pair, 2); if (2 === count($kv)) { $out[trim($kv[0])] = trim($kv[1]); } } return $out ? $out : null; } public function enqueue_tracker_loader() { $version = get_option('aieo_session_tracker_version', time()); $src = add_query_arg('ver', rawurlencode((string) $version), plugin_dir_url(__FILE__) . '../assets/js/aieo-session-tracker.js'); $config = array( 'endpoint' => rest_url('aieo/v1/track'), 'userId' => get_current_user_id(), 'cookiePath' => (defined('COOKIEPATH') && COOKIEPATH) ? COOKIEPATH : '/', 'cookieDomain' => (defined('COOKIE_DOMAIN') && COOKIE_DOMAIN) ? COOKIE_DOMAIN : '', 'consentRequired' => self::consent_required_rule(), 'defaultConsent' => ('1' === (string) get_option('aieo_session_tracking_default_consent', '1')), 'flushInterval' => (int) get_option('aieo_session_flush_interval', 5000), 'trackScrollDepth' => get_option('aieo_track_scroll_depth', '1') === '1', 'trackClicks' => get_option('aieo_track_clicks', '1') === '1', 'throttleClicks' => get_option('aieo_throttle_clicks', '1') === '1', 'trackMouseMovement' => get_option('aieo_track_mouse_movement', '0') === '1', ); $rules = (string) get_option('aieo_custom_image_click_rules_js', ''); $config['cardsSrc'] = add_query_arg('ver', rawurlencode((string) $version), plugin_dir_url(__FILE__) . '../assets/js/aieo-cards.js'); $config['trackerSrc'] = $src; $dir = dirname(__DIR__) . '/'; $rel = 'assets/js/aieo-session-loader.js'; if (!file_exists($dir . $rel)) { return; } wp_register_script('aieo-session-loader', plugins_url($rel, $dir . 'plugin.php'), array(), (string) filemtime($dir . $rel), array('in_footer' => true, 'strategy' => 'defer')); $before = 'window.aieoSessionConfig = ' . wp_json_encode($config) . ';'; if ('' !== $rules) { $before .= "\n" . 'window.aieoSessionCustomRules = function($target, self){ ' . $rules . ' };'; } wp_add_inline_script('aieo-session-loader', $before, 'before'); wp_enqueue_script('aieo-session-loader'); } public function add_defer_attribute($tag, $handle, $src) { if ($handle !== 'aieo-session-tracker') { return $tag; } if (strpos($tag, 'defer') !== false) { return $tag; } $tag = str_replace( ' src=', ' defer data-cfasync="false" data-no-optimize="1" src=', $tag ); return $tag; } private function get_or_create_session_id() { if (isset($_COOKIE['aieo_session_id'])) { return sanitize_text_field(wp_unslash( $_COOKIE['aieo_session_id'] )); } $session_id = 'session_' . time() . '_' . wp_generate_password(12, false); setcookie('aieo_session_id', $session_id, time() + 1800, COOKIEPATH, COOKIE_DOMAIN); return $session_id; } public static function is_consent_blocking_writes() { $tracking_enabled = get_option('aieo_session_tracking_enabled', '0'); if ($tracking_enabled !== '1' && $tracking_enabled !== 1) { return false; } $bypass_consent = get_option('aieo_session_bypass_consent', '1'); if ($bypass_consent == '1' || $bypass_consent === 1) { return false; } $require_consent = get_option('aieo_session_tracking_require_consent', '1'); if ($require_consent != '1' && $require_consent !== 1) { return false; } $inst = self::get_instance_for_consent_check(); if (!$inst) { return false; } return ! $inst->user_has_given_consent(); } private static function get_instance_for_consent_check() { if (method_exists(__CLASS__, 'get_instance')) { $inst = self::get_instance(); if ($inst instanceof self) { return $inst; } } return null; } private function user_has_given_consent() { if (class_exists('AIEO_Consent') && AIEO_Consent::has_choice()) { return AIEO_Consent::allows('analytics'); } if (isset($_COOKIE['cookieyes-consent'])) { $consent = self::cookieyes_state(); return isset($consent['analytics']) && $consent['analytics'] === 'yes'; } if (isset($_COOKIE['cookie_notice_accepted']) && $_COOKIE['cookie_notice_accepted'] === 'true') { return true; } if (function_exists('cmplz_has_consent') && cmplz_has_consent('statistics')) { return true; } return '1' === (string) get_option('aieo_session_tracking_default_consent', '1'); } public static function is_marketing_consent_blocking() { $tracking_enabled = get_option('aieo_session_tracking_enabled', '0'); if ($tracking_enabled !== '1' && $tracking_enabled !== 1) { return false; } $bypass_consent = get_option('aieo_session_bypass_consent', '1'); if ($bypass_consent == '1' || $bypass_consent === 1) { return false; } $require_consent = get_option('aieo_session_tracking_require_consent', '1'); if ($require_consent != '1' && $require_consent !== 1) { return false; } $inst = self::get_instance_for_consent_check(); if (!$inst) { return false; } return ! $inst->user_has_given_marketing_consent(); } private function user_has_given_marketing_consent() { if (class_exists('AIEO_Consent') && AIEO_Consent::has_choice()) { return AIEO_Consent::allows('marketing'); } if (isset($_COOKIE['cookieyes-consent'])) { $consent = self::cookieyes_state(); if (is_array($consent)) { if (isset($consent['advertisement'])) { return $consent['advertisement'] === 'yes'; } if (isset($consent['marketing'])) { return $consent['marketing'] === 'yes'; } } return false; } if (isset($_COOKIE['cookie_notice_accepted']) && $_COOKIE['cookie_notice_accepted'] === 'true') { return true; } if (function_exists('cmplz_has_consent') && cmplz_has_consent('marketing')) { return true; } return '1' === (string) get_option('aieo_session_tracking_default_consent', '1'); } private static function aieo_memory_limit_bytes() { $v = trim((string) ini_get('memory_limit')); if ($v === '' || $v === '-1') { return 0; } $unit = strtolower(substr($v, -1)); $n = (int) $v; if ($unit === 'g') { return $n * 1073741824; } if ($unit === 'm') { return $n * 1048576; } if ($unit === 'k') { return $n * 1024; } return $n; } public function handle_tracking_batch(WP_REST_Request $request) { global $wpdb; $sessions_table = $wpdb->prefix . 'aieo_sessions'; $statics_table = $wpdb->prefix . 'aieo_sessions_statics'; $aieo_mem_enter = memory_get_peak_usage(true); $aieo_mem_limit = self::aieo_memory_limit_bytes(); if ($aieo_mem_limit > 0 && memory_get_usage(true) > (int) ($aieo_mem_limit * 0.9)) { if (!defined('AIEO_TRACK_MEM_LOG') || AIEO_TRACK_MEM_LOG) { error_log(sprintf('[AIEO_TRACK_MEM] pid=%d SOFT-EARLY-OUT usage=%dMB limit=%dMB', getmypid(), (int) round(memory_get_usage(true) / 1048576), (int) round($aieo_mem_limit / 1048576))); } return new WP_REST_Response(null, 204); } if (apply_filters('aieo_reject_bot_traffic', true)) { $aieo_ua = isset($_SERVER['HTTP_USER_AGENT']) ? (string) wp_unslash($_SERVER['HTTP_USER_AGENT']) : ''; if (self::is_bot_user_agent($aieo_ua)) { self::count_bot_rejection($aieo_ua); return new WP_REST_Response(null, 204); } } $aieo_enabled = get_option('aieo_session_tracking_enabled', '0'); if ($aieo_enabled != '1' && $aieo_enabled !== 1) { return new WP_REST_Response(array('success' => false, 'reason' => 'tracking_disabled'), 403); } if (self::is_consent_blocking_writes()) { return new WP_REST_Response(array('success' => false, 'reason' => 'consent_required'), 403); } $aieo_ip = isset($_SERVER['REMOTE_ADDR']) ? (string) $_SERVER['REMOTE_ADDR'] : ''; if ($aieo_ip !== '') { $aieo_rl_cap = (int) apply_filters('aieo_track_rate_limit_per_minute', 120); if ($aieo_rl_cap > 0) { $aieo_rl_key = 'aieo_trk_rl_' . md5($aieo_ip); $aieo_rl_hits = (int) get_transient($aieo_rl_key); if ($aieo_rl_hits >= $aieo_rl_cap) { return new WP_REST_Response(array('success' => false, 'reason' => 'rate_limited'), 429); } set_transient($aieo_rl_key, $aieo_rl_hits + 1, MINUTE_IN_SECONDS); } } $events = $request->get_param('events'); if (empty($events) || !is_array($events)) { return new WP_Error('invalid_data', __('No events provided', 'ai-eshop-optimizer'), array('status' => 400)); } $aieo_max = defined('AIEO_TRACK_MAX_EVENTS') ? (int) AIEO_TRACK_MAX_EVENTS : 100; if ($aieo_max > 0 && count($events) > $aieo_max) { if (!defined('AIEO_TRACK_MEM_LOG') || AIEO_TRACK_MEM_LOG) { error_log(sprintf('[AIEO_TRACK_MEM] pid=%d capped events %d -> %d', getmypid(), count($events), $aieo_max)); } $events = array_slice($events, 0, $aieo_max); } $session_id = sanitize_text_field($events[0]['session_id']); $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; $device_info = $this->parse_user_agent($user_agent); $first_event = $events[0]; $wpdb->query($wpdb->prepare( "INSERT IGNORE INTO {$statics_table} (session_id, user_agent, device_type, browser, os, landing_page, referrer, first_timestamp) VALUES (%s, %s, %s, %s, %s, %s, %s, %d)", $session_id, $user_agent, $device_info['device_type'], $device_info['browser'], $device_info['os'], isset($first_event['url']) ? (class_exists('AIEO_Url_Redaction') ? AIEO_Url_Redaction::clean(esc_url_raw($first_event['url'])) : esc_url_raw($first_event['url'])) : '', isset($first_event['referrer']) ? (class_exists('AIEO_Url_Redaction') ? AIEO_Url_Redaction::clean(esc_url_raw($first_event['referrer'])) : esc_url_raw($first_event['referrer'])) : '', self::clamp_event_timestamp(isset($first_event['timestamp']) ? $first_event['timestamp'] : 0) )); static $aieo_si_ready = null; if (null === $aieo_si_ready) { $aieo_si_tbl = $wpdb->prefix . 'aieo_session_identity'; $aieo_si_ready = ((string) $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $aieo_si_tbl)) === $aieo_si_tbl); } if ($aieo_si_ready && '' !== $session_id) { $aieo_visitor_key = ''; if (class_exists('AIEO_Attribution') && method_exists('AIEO_Attribution', 'read_visitor_key')) { $aieo_visitor_key = (string) AIEO_Attribution::read_visitor_key(); } elseif (!empty($_COOKIE['aieo_attr_sid'])) { $aieo_visitor_key = sanitize_text_field(wp_unslash($_COOKIE['aieo_attr_sid'])); } if ('' !== $aieo_visitor_key) { $aieo_si_now = gmdate('Y-m-d H:i:s'); $wpdb->query($wpdb->prepare( "INSERT IGNORE INTO {$wpdb->prefix}aieo_session_identity (session_id, visitor_key, user_id, resolved_via, created, updated) VALUES (%s, %s, %d, 'pending', %s, %s)", $session_id, $aieo_visitor_key, (int) get_current_user_id(), $aieo_si_now, $aieo_si_now )); } } $rows = array(); foreach ($events as $event) { if (empty($event['session_id']) || empty($event['event_type'])) { continue; } $event_data = isset($event['data']) ? $event['data'] : array(); $product_id = isset($event_data['product_id']) ? (int) $event_data['product_id'] : null; $product_name = isset($event_data['product_name']) ? sanitize_text_field($event_data['product_name']) : null; $category_id = isset($event_data['category_id']) ? (int) $event_data['category_id'] : null; $category_name = isset($event_data['category_name']) ? sanitize_text_field($event_data['category_name']) : null; $scroll_depth = self::clamp_event_int($event_data, 'scroll_depth', 100); $click_count = self::clamp_event_int($event_data, 'click_count', 10000); $event_sequence = self::clamp_event_int($event_data, 'event_sequence', 100000); $is_first_event = isset($event_data['is_first_event']) ? ((int) $event_data['is_first_event'] ? 1 : 0) : 0; $is_last_event = isset($event_data['is_last_event']) ? ((int) $event_data['is_last_event'] ? 1 : 0) : 0; $is_only_event = ($is_first_event && $is_last_event) ? 1 : 0; $time_on_page = self::clamp_event_int($event_data, 'time_on_page', self::MAX_TIME_ON_PAGE); $visit_number = self::clamp_event_int($event_data, 'visit_number', 1000); $order_id = isset($event_data['order_id']) ? (int) $event_data['order_id'] : null; $checkout_step = isset($event_data['checkout_step']) ? sanitize_text_field($event_data['checkout_step']) : null; if (class_exists('AIEO_Url_Redaction')) { $event_data = AIEO_Url_Redaction::clean_deep($event_data); } $event_data_json = wp_json_encode($event_data); if (!is_string($event_data_json)) { $event_data_json = '{}'; } if (strlen($event_data_json) > 8192) { $event_data_json = wp_json_encode(array('_aieo_truncated' => 1, 'event_type' => (string) $event['event_type'], 'orig_bytes' => strlen($event_data_json))); } $page_url = isset($event['url']) ? esc_url_raw($event['url']) : ''; if (class_exists('AIEO_Url_Redaction')) { $page_url = AIEO_Url_Redaction::clean($page_url); } if (strlen($page_url) > 2048) { $page_url = substr($page_url, 0, 2048); } $event_ts = self::clamp_event_timestamp(isset($event['timestamp']) ? $event['timestamp'] : 0); $event_id_val = (isset($event['event_id']) && $event['event_id'] !== '') ? substr(sanitize_text_field($event['event_id']), 0, 36) : substr(md5($session_id . '|' . $event_ts . '|' . sanitize_text_field($event['event_type']) . '|' . (string) $product_id), 0, 36); $rows[] = array( $event_id_val, $session_id, isset($event['user_id']) ? (int) $event['user_id'] : 0, sanitize_text_field($event['event_type']), $product_id, $product_name, $category_id, $category_name, $scroll_depth, $click_count, $event_sequence, $is_first_event, $is_last_event, $is_only_event, $time_on_page, $visit_number, $order_id, $checkout_step, $event_data_json, $page_url, $event_ts, ); } if (empty($rows)) { return new WP_Error('no_valid_events', __('No valid events to record', 'ai-eshop-optimizer'), array('status' => 400)); } $aieo_row_ph = '(%s, %s, %d, %s, %d, %s, %d, %s, %d, %d, %d, %d, %d, %d, %d, %d, %d, %s, %s, %s, %d)'; $aieo_cols = "(event_id, session_id, user_id, event_type, product_id, product_name, category_id, category_name, scroll_depth, click_count, event_sequence, is_first_event, is_last_event, is_only_event, time_on_page, visit_number, order_id, checkout_step, event_data, page_url, timestamp)"; $result = true; foreach (array_chunk($rows, 50) as $aieo_chunk) { $aieo_ph_str = implode(', ', array_fill(0, count($aieo_chunk), $aieo_row_ph)); $aieo_flat = array(); foreach ($aieo_chunk as $aieo_r) { foreach ($aieo_r as $aieo_v) { $aieo_flat[] = $aieo_v; } } $aieo_q = "INSERT IGNORE INTO {$sessions_table} {$aieo_cols} VALUES " . $aieo_ph_str; if ($wpdb->query($wpdb->prepare($aieo_q, $aieo_flat)) === false) { $result = false; } } if ($result === false) { return new WP_Error('database_error', __('Failed to save tracking data', 'ai-eshop-optimizer'), array('status' => 500)); } $this->update_session_summary($session_id, $events); $this->store_checkout_timing($events, $session_id, $device_info['device_type']); $aieo_mem_exit = memory_get_peak_usage(true); $aieo_mem_delta = $aieo_mem_exit - $aieo_mem_enter; if ((!defined('AIEO_TRACK_MEM_LOG') || AIEO_TRACK_MEM_LOG) && ($aieo_mem_exit > 256 * 1024 * 1024 || $aieo_mem_delta > 16 * 1024 * 1024)) { error_log(sprintf( '[AIEO_TRACK_MEM] pid=%d enter=%dMB exit=%dMB delta=%dMB events=%d session=%s', getmypid(), (int) round($aieo_mem_enter / 1024 / 1024), (int) round($aieo_mem_exit / 1024 / 1024), (int) round($aieo_mem_delta / 1024 / 1024), count($rows), substr($session_id, 0, 12) )); } return rest_ensure_response(array( 'success' => true, 'recorded' => count($rows), 'session_id' => $session_id )); } private function store_checkout_timing($events, $session_id, $device_type) { if (!is_array($events) || !function_exists('wc_get_order')) { return; } global $wpdb; $table = $wpdb->prefix . 'aieo_checkout_timing'; static $table_ok = null; if ($table_ok === null) { $table_ok = ((string) $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table)) === $table); } if (!$table_ok) { return; } $oa_table = $wpdb->prefix . 'aieo_dmm_offer_attributions'; static $oa_ok = null; if ($oa_ok === null) { $oa_ok = ((string) $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $oa_table)) === $oa_table); } foreach ($events as $event) { if (empty($event['event_type']) || $event['event_type'] !== 'checkout_timing') { continue; } $d = (isset($event['data']) && is_array($event['data'])) ? $event['data'] : array(); $order_id = isset($d['order_id']) ? (int) $d['order_id'] : 0; if ($order_id <= 0) { continue; } $order = wc_get_order($order_id); if (!$order) { continue; } $total = isset($d['total_seconds']) ? max(0, (int) $d['total_seconds']) : 0; $active = isset($d['active_seconds']) ? max(0, (int) $d['active_seconds']) : 0; $idle = isset($d['idle_seconds']) ? max(0, (int) $d['idle_seconds']) : 0; if ($active + $idle > $total) { $total = $active + $idle; } $steps = isset($d['step_durations']) ? wp_json_encode($d['step_durations']) : null; $codes = (array) $order->get_coupon_codes(); $has_coupon = !empty($codes) ? 1 : 0; $codes_str = $has_coupon ? substr(implode(',', array_map('sanitize_text_field', $codes)), 0, 255) : null; $coupon_is_offer = 0; if ($has_coupon && $oa_ok) { $coupon_is_offer = (int) (bool) $wpdb->get_var($wpdb->prepare( "SELECT 1 FROM `{$oa_table}` WHERE order_id = %d LIMIT 1", $order_id )); } $now = current_time('mysql', true); $started_at = gmdate('Y-m-d H:i:s', max(0, time() - $total)); $wpdb->query($wpdb->prepare( "INSERT INTO `{$table}` (order_id, session_id, total_seconds, active_seconds, idle_seconds, step_durations, device_type, has_coupon, coupon_codes, coupon_is_offer, started_at, completed_at, created_at) VALUES (%d, %s, %d, %d, %d, %s, %s, %d, %s, %d, %s, %s, %s) ON DUPLICATE KEY UPDATE total_seconds = VALUES(total_seconds), active_seconds = VALUES(active_seconds), idle_seconds = VALUES(idle_seconds), step_durations = VALUES(step_durations), device_type = VALUES(device_type), has_coupon = VALUES(has_coupon), coupon_codes = VALUES(coupon_codes), coupon_is_offer = VALUES(coupon_is_offer)", $order_id, $session_id, $total, $active, $idle, $steps, $device_type, $has_coupon, $codes_str, $coupon_is_offer, $started_at, $now, $now )); } } private function update_session_summary($session_id, $events) { global $wpdb; $statics_table = $wpdb->prefix . 'aieo_sessions_statics'; $sessions_table = $wpdb->prefix . 'aieo_sessions'; $agg = $wpdb->get_row($wpdb->prepare( "SELECT COUNT(*) AS total, SUM(event_type = 'product_view') AS pv, SUM(event_type = 'add_to_cart') AS atc, SUM(event_type = 'scroll_depth') AS sc, SUM(event_type = 'click') AS cl, MAX(timestamp) AS last_ts FROM {$sessions_table} WHERE session_id = %s", $session_id )); if (!$agg) { return; } $last = (int) $agg->last_ts; $wpdb->query($wpdb->prepare( "UPDATE {$statics_table} SET last_timestamp = GREATEST(COALESCE(last_timestamp, 0), %d), total_events = %d, total_product_views = %d, total_add_to_cart = %d, total_scroll_events = %d, total_click_events = %d, session_duration_seconds = CAST(GREATEST(0, GREATEST(COALESCE(last_timestamp, first_timestamp), %d) - first_timestamp) / 1000 AS UNSIGNED), updated_at = UTC_TIMESTAMP() WHERE session_id = %s", $last, (int) $agg->total, (int) $agg->pv, (int) $agg->atc, (int) $agg->sc, (int) $agg->cl, $last, $session_id )); } private function parse_user_agent($user_agent) { $device_type = 'desktop'; $browser = 'unknown'; $os = 'unknown'; if (preg_match('/mobile|android|iphone|ipad|ipod|blackberry|iemobile|opera mini/i', $user_agent)) { if (preg_match('/ipad|tablet|playbook|silk/i', $user_agent)) { $device_type = 'tablet'; } else { $device_type = 'mobile'; } } if (preg_match('/MSIE|Trident/i', $user_agent)) { $browser = 'Internet Explorer'; } elseif (preg_match('/Edge/i', $user_agent)) { $browser = 'Microsoft Edge'; } elseif (preg_match('/Chrome/i', $user_agent)) { $browser = 'Chrome'; } elseif (preg_match('/Safari/i', $user_agent)) { $browser = 'Safari'; } elseif (preg_match('/Firefox/i', $user_agent)) { $browser = 'Firefox'; } elseif (preg_match('/Opera|OPR/i', $user_agent)) { $browser = 'Opera'; } if (preg_match('/Windows NT 10/i', $user_agent)) { $os = 'Windows 10/11'; } elseif (preg_match('/Windows NT/i', $user_agent)) { $os = 'Windows'; } elseif (preg_match('/Mac OS X/i', $user_agent)) { $os = 'macOS'; } elseif (preg_match('/Android/i', $user_agent)) { $os = 'Android'; } elseif (preg_match('/iOS|iPhone|iPad/i', $user_agent)) { $os = 'iOS'; } elseif (preg_match('/Linux/i', $user_agent)) { $os = 'Linux'; } return array( 'device_type' => $device_type, 'browser' => $browser, 'os' => $os ); } public function get_session_context(WP_REST_Request $request) { $session_id = $request->get_param('session_id'); if (empty($session_id)) { return new WP_Error('missing_session_id', __('Session ID required', 'ai-eshop-optimizer'), array('status' => 400)); } $context = $this->parse_session_data($session_id); return rest_ensure_response($context); } public function parse_session_data($session_id, $time_window = 30) { global $wpdb; $table_name = $wpdb->prefix . 'aieo_sessions'; $statics_table = $wpdb->prefix . 'aieo_sessions_statics'; $cutoff_time = (time() - ($time_window * 60)) * 1000; $events = $wpdb->get_results($wpdb->prepare( "SELECT e.event_type, e.event_data, e.page_url, e.timestamp, s.device_type, s.browser, s.os FROM {$table_name} e LEFT JOIN {$statics_table} s ON s.session_id = e.session_id WHERE e.session_id = %s AND e.timestamp > %d ORDER BY e.timestamp ASC", $session_id, $cutoff_time )); if (empty($events)) { return array( 'has_data' => false, 'message' => 'No recent activity found' ); } $context = array( 'has_data' => true, 'session_id' => $session_id, 'device_type' => $events[0]->device_type, 'browser' => $events[0]->browser, 'os' => $events[0]->os, 'viewed_products' => array(), 'viewed_categories' => array(), 'viewed_pages' => array(), 'cart_additions' => array(), 'cart_removals' => array(), 'wishlist_additions' => array(), 'scroll_engagement' => array(), 'total_time_on_site' => 0, 'page_views' => 0, 'engagement_score' => 0, 'first_event_time' => null, 'last_event_time' => null ); $page_timings = array(); $last_page_url = null; $last_page_timestamp = null; foreach ($events as $event) { $data = json_decode($event->event_data, true); $timestamp = (int) $event->timestamp; if (!$context['first_event_time']) { $context['first_event_time'] = $timestamp; } $context['last_event_time'] = $timestamp; if ($event->page_url !== $last_page_url && $last_page_url !== null) { $time_spent = ($timestamp - $last_page_timestamp) / 1000; if (!isset($page_timings[$last_page_url])) { $page_timings[$last_page_url] = 0; } $page_timings[$last_page_url] += $time_spent; } $last_page_url = $event->page_url; $last_page_timestamp = $timestamp; switch ($event->event_type) { case 'product_view': $context['viewed_products'][] = array( 'product_id' => isset($data['product_id']) ? (int) $data['product_id'] : 0, 'product_name' => isset($data['product_name']) ? $data['product_name'] : '', 'timestamp' => $timestamp, 'time_spent' => 0 ); $context['page_views']++; break; case 'category_view': $context['viewed_categories'][] = array( 'category_id' => isset($data['category_id']) ? (int) $data['category_id'] : 0, 'category_name' => isset($data['category_name']) ? $data['category_name'] : '', 'timestamp' => $timestamp, 'time_spent' => 0 ); $context['page_views']++; break; case 'page_view': $context['viewed_pages'][] = array( 'page_id' => isset($data['page_id']) ? (int) $data['page_id'] : 0, 'page_title' => isset($data['page_title']) ? $data['page_title'] : '', 'page_type' => isset($data['page_type']) ? $data['page_type'] : 'unknown', 'timestamp' => $timestamp ); $context['page_views']++; break; case 'add_to_cart': $context['cart_additions'][] = array( 'product_id' => isset($data['product_id']) ? (int) $data['product_id'] : 0, 'product_name' => isset($data['product_name']) ? $data['product_name'] : '', 'quantity' => isset($data['quantity']) ? (int) $data['quantity'] : 1, 'timestamp' => $timestamp ); break; case 'remove_from_cart': $context['cart_removals'][] = array( 'product_id' => isset($data['product_id']) ? (int) $data['product_id'] : 0, 'timestamp' => $timestamp ); break; case 'add_to_wishlist': $context['wishlist_additions'][] = array( 'product_id' => isset($data['product_id']) ? (int) $data['product_id'] : 0, 'timestamp' => $timestamp ); break; case 'scroll_depth': $page_url = $event->page_url; if (!isset($context['scroll_engagement'][$page_url])) { $context['scroll_engagement'][$page_url] = array( 'max_depth' => 0, 'scroll_events' => 0 ); } $depth = isset($data['depth']) ? (int) $data['depth'] : 0; $context['scroll_engagement'][$page_url]['max_depth'] = max( $context['scroll_engagement'][$page_url]['max_depth'], $depth ); $context['scroll_engagement'][$page_url]['scroll_events']++; break; case 'click': $context['engagement_score'] += 1; break; } } if ($context['first_event_time'] && $context['last_event_time']) { $context['total_time_on_site'] = ($context['last_event_time'] - $context['first_event_time']) / 1000; } foreach ($page_timings as $url => $time) { foreach ($context['viewed_products'] as &$product) { if (strpos($url, 'product') !== false) { $product['time_spent'] = $time; } } foreach ($context['viewed_categories'] as &$category) { if (strpos($url, 'product-category') !== false) { $category['time_spent'] = $time; } } } $context['engagement_score'] = $this->calculate_engagement_score($context); return $context; } private function calculate_engagement_score($context) { $avg_scroll_depth = 0.0; if (!empty($context['scroll_engagement'])) { $total_depth = 0; foreach ($context['scroll_engagement'] as $page_data) { $total_depth += $page_data['max_depth']; } $avg_scroll_depth = $total_depth / count($context['scroll_engagement']); } $time_sec = isset($context['total_time_on_site']) ? (float) $context['total_time_on_site'] : 0.0; $clicks = isset($context['engagement_score']) ? (int) $context['engagement_score'] : 0; $views = isset($context['viewed_products']) ? count($context['viewed_products']) : 0; $cart = isset($context['cart_additions']) ? count($context['cart_additions']) : 0; $scroll_c = min($avg_scroll_depth * 0.40, 40); $time_c = min(($time_sec / 60) * 30, 30); $click_c = min($clicks * 4, 20); $intent_c = min(($cart / max(1, $views)) * 10, 10); return (int) round(min(100, $scroll_c + $time_c + $click_c + $intent_c)); } public function generate_chat_context_summary($session_id) { $context = $this->parse_session_data($session_id); if (!$context['has_data']) { return ''; } $summary = array(); $summary[] = sprintf( "USER DEVICE: %s (%s on %s)", ucfirst($context['device_type']), $context['browser'], $context['os'] ); if ($context['total_time_on_site'] > 0) { $summary[] = sprintf( "SESSION DURATION: %s (%d page views)", gmdate("i:s", $context['total_time_on_site']), $context['page_views'] ); } $summary[] = sprintf( "ENGAGEMENT SCORE: %d/100 (%s)", $context['engagement_score'], $this->get_engagement_level($context['engagement_score']) ); if (!empty($context['viewed_products'])) { $summary[] = "\nRECENTLY VIEWED PRODUCTS:"; foreach (array_slice($context['viewed_products'], -5) as $product) { $time_ago = human_time_diff($product['timestamp'] / 1000, time()); $time_spent = $product['time_spent'] > 0 ? sprintf(" (%d seconds)", $product['time_spent']) : ''; $summary[] = sprintf( " - %s (viewed %s ago%s)", $product['product_name'], $time_ago, $time_spent ); } } if (!empty($context['viewed_categories'])) { $summary[] = "\nCATEGORIES BROWSED:"; foreach ($context['viewed_categories'] as $category) { $summary[] = sprintf(" - %s", $category['category_name']); } } if (!empty($context['cart_additions'])) { $summary[] = "\nCART ACTIVITY:"; foreach ($context['cart_additions'] as $addition) { $time_ago = human_time_diff($addition['timestamp'] / 1000, time()); $summary[] = sprintf( " + Added: %s (Qty: %d) %s ago", $addition['product_name'], $addition['quantity'], $time_ago ); } } if (!empty($context['cart_removals'])) { foreach ($context['cart_removals'] as $removal) { $time_ago = human_time_diff($removal['timestamp'] / 1000, time()); $summary[] = sprintf(" - Removed product ID %d %s ago", $removal['product_id'], $time_ago); } } if (!empty($context['wishlist_additions'])) { $summary[] = sprintf("\nWISHLIST: %d items added", count($context['wishlist_additions'])); } return implode("\n", $summary); } private function get_engagement_level($score) { if ($score >= 80) return 'Very High'; if ($score >= 60) return 'High'; if ($score >= 40) return 'Medium'; if ($score >= 20) return 'Low'; return 'Very Low'; } } }