getMessage()); flock($lock_fp, LOCK_UN); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose fclose($lock_fp); return false; } flock($lock_fp, LOCK_UN); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose fclose($lock_fp); return $result; } private static function log($message) { if (defined('WP_DEBUG') && WP_DEBUG && function_exists('wp_debug_log')) { wp_debug_log('EISTAT aggregator: ' . $message); } } private static function do_run($force = false) { global $wpdb; $eistat_p = $wpdb->prefix . 'eistat_'; $buffer_dir = eistat_get_buffer_dir(); $files = glob($buffer_dir . '*.jsonl'); if (!$files) return 0; $events = []; foreach ($files as $file) { if (!$force) { $age = time() - filemtime($file); if ($age < 300) continue; } $tmp = $file . '.processing'; // phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename if (!@rename($file, $tmp)) continue; $lines = file($tmp, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink @unlink($tmp); foreach ($lines as $line) { $e = json_decode($line, true); if (is_array($e) && !empty($e['session_hash'])) { $events[] = $e; } } } if (!$events) return 0; usort($events, function ($a, $b) { return $a['ts'] - $b['ts']; }); // --- In-memory aggregation --- $pageviews = []; $uniques = []; $page_time = []; $session_pages = []; $session_times = []; $session_data = []; $clicks = []; foreach ($events as $e) { $date = wp_date('Y-m-d', $e['ts']); $ptype = self::valid_type($e['page_type'] ?? 'other'); $pid = (int)$e['page_id']; $sid = $e['session_hash']; $key = "$date|$ptype|$pid"; if ($e['event'] === 'pageview') { if (!isset($pageviews[$key])) { $pageviews[$key] = [ 'date' => $date, 'ptype' => $ptype, 'pid' => $pid, 'url' => $e['page_url'], 'views' => 0, ]; } $pageviews[$key]['views']++; $uniques[$key][$sid] = true; if (!isset($session_pages[$sid])) $session_pages[$sid] = []; $last = end($session_pages[$sid]); if (!$last || $last[0] !== $ptype || $last[1] !== $pid) { $session_pages[$sid][] = [$ptype, $pid, $e['page_url']]; } if (!isset($session_data[$sid])) { $utm_source = $e['utm_source'] ?? ''; $utm_medium = $e['utm_medium'] ?? ''; $utm_campaign = $e['utm_campaign'] ?? ''; $referrer = $e['referrer'] ?? ''; list($source, $source_detail) = self::classify_source( $referrer, $utm_source, $utm_medium ); $session_data[$sid] = [ 'sid' => $sid, 'date' => $date, 'first_ts' => $e['ts'], 'last_ts' => $e['ts'], 'entry_type' => $ptype, 'entry_id' => $pid, 'entry_url' => $e['page_url'], 'exit_type' => $ptype, 'exit_id' => $pid, 'exit_url' => $e['page_url'], 'pageviews' => 0, 'source' => $source, 'source_detail' => $source_detail, 'referrer_host' => self::host($referrer), 'utm_source' => substr((string)$utm_source, 0, 50), 'utm_medium' => substr((string)$utm_medium, 0, 50), 'utm_campaign' => substr((string)$utm_campaign, 0, 100), 'device' => $e['device'], 'is_bot' => (int)$e['is_bot'], 'ip_hash' => $e['ip_hash'] ?? '', ]; } $session_data[$sid]['pageviews']++; $session_data[$sid]['last_ts'] = max($session_data[$sid]['last_ts'], $e['ts']); $session_data[$sid]['exit_type'] = $ptype; $session_data[$sid]['exit_id'] = $pid; $session_data[$sid]['exit_url'] = $e['page_url']; $session_data[$sid]['is_bot'] = max($session_data[$sid]['is_bot'], (int)$e['is_bot']); } elseif ($e['event'] === 'unload') { $dur = (int)$e['duration']; if (!isset($page_time[$key])) $page_time[$key] = 0; $page_time[$key] += $dur; $skey = "$ptype|$pid"; if (!isset($session_times[$sid][$skey])) $session_times[$sid][$skey] = 0; $session_times[$sid][$skey] += $dur; if (isset($session_data[$sid])) { $session_data[$sid]['last_ts'] = max($session_data[$sid]['last_ts'], $e['ts']); } } elseif ($e['event'] === 'outbound_click') { $link_type = in_array($e['link_type'] ?? '', ['outbound','email','phone'], true) ? $e['link_type'] : 'outbound'; $link_url = substr((string)($e['link_url'] ?? ''), 0, 500); $link_text = substr((string)($e['link_text'] ?? ''), 0, 120); $ckey = "$date|$ptype|$pid|$link_type|$link_url"; if (!isset($clicks[$ckey])) { $clicks[$ckey] = [ 'date' => $date, 'ptype' => $ptype, 'pid' => $pid, 'link_type' => $link_type, 'link_url' => $link_url, 'link_text' => $link_text, 'count' => 0, ]; } $clicks[$ckey]['count']++; } } // --- Write pageviews --- foreach ($pageviews as $key => $row) { $uniq = count($uniques[$key] ?? []); $time = $page_time[$key] ?? 0; $sql = "INSERT INTO {$eistat_p}pageviews (date, page_type, page_id, page_url, views, unique_views, total_time) VALUES (%s, %s, %d, %s, %d, %d, %d) ON DUPLICATE KEY UPDATE views = views + VALUES(views), unique_views = unique_views + VALUES(unique_views), total_time = total_time + VALUES(total_time)"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $wpdb->query($wpdb->prepare($sql, $row['date'], $row['ptype'], $row['pid'], $row['url'], $row['views'], $uniq, $time)); } // --- Write events --- foreach ($clicks as $row) { $sql = "INSERT INTO {$eistat_p}events (date, page_type, page_id, link_type, link_url, link_text, count) VALUES (%s, %s, %d, %s, %s, %s, %d) ON DUPLICATE KEY UPDATE count = count + VALUES(count)"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $wpdb->query($wpdb->prepare($sql, $row['date'], $row['ptype'], $row['pid'], $row['link_type'], $row['link_url'], $row['link_text'], $row['count'])); } // --- Write sessions and transitions --- foreach ($session_data as $sid => $s) { $sql_prev = "SELECT exit_page_type, exit_page_id, pages_json FROM {$eistat_p}sessions WHERE session_hash = %s"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $prev = $wpdb->get_row($wpdb->prepare($sql_prev, $sid)); $pages = $session_pages[$sid] ?? []; $pages_arr = []; foreach ($pages as $pg) { $skey = $pg[0] . '|' . $pg[1]; $t = $session_times[$sid][$skey] ?? 0; $pages_arr[] = [$pg[1], $t, self::type_to_char($pg[0])]; if (count($pages_arr) >= 30) break; } $new_unique = count($pages); $new_time = array_sum($session_times[$sid] ?? []); if ($prev && $prev->exit_page_id && !empty($pages) && ($prev->exit_page_id != $pages[0][1] || $prev->exit_page_type != $pages[0][0])) { self::transition($eistat_p, $s['date'], $prev->exit_page_type, $prev->exit_page_id, $pages[0][0], $pages[0][1]); } for ($i = 1; $i < count($pages); $i++) { self::transition($eistat_p, $s['date'], $pages[$i-1][0], $pages[$i-1][1], $pages[$i][0], $pages[$i][1]); } if ($prev === null) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->insert("{$eistat_p}sessions", [ 'session_hash' => $sid, 'ip_hash' => $s['ip_hash'], 'date' => $s['date'], 'first_seen' => wp_date('Y-m-d H:i:s', $s['first_ts']), 'last_seen' => wp_date('Y-m-d H:i:s', $s['last_ts']), 'entry_page_type' => $s['entry_type'], 'entry_page_id' => $s['entry_id'], 'entry_page_url' => $s['entry_url'], 'exit_page_type' => $s['exit_type'], 'exit_page_id' => $s['exit_id'], 'exit_page_url' => $s['exit_url'], 'pageviews' => $s['pageviews'], 'unique_pages' => $new_unique, 'total_time' => $new_time, 'pages_json' => wp_json_encode($pages_arr), 'source' => $s['source'], 'source_detail' => $s['source_detail'], 'referrer_host' => $s['referrer_host'], 'utm_source' => $s['utm_source'], 'utm_medium' => $s['utm_medium'], 'utm_campaign' => $s['utm_campaign'], 'device' => $s['device'], 'is_bot' => $s['is_bot'], ]); if ($wpdb->last_error) { self::log('INSERT session failed: ' . $wpdb->last_error); } } else { $existing = json_decode($prev->pages_json ?: '[]', true) ?: []; if (count($existing) < 30) { $existing = array_slice(array_merge($existing, $pages_arr), 0, 30); } $sql_update = "UPDATE {$eistat_p}sessions SET last_seen = GREATEST(last_seen, %s), exit_page_type = %s, exit_page_id = %d, exit_page_url = %s, pageviews = pageviews + %d, unique_pages = unique_pages + %d, total_time = total_time + %d, pages_json = %s, is_bot = GREATEST(is_bot, %d) WHERE session_hash = %s"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $wpdb->query($wpdb->prepare($sql_update, wp_date('Y-m-d H:i:s', $s['last_ts']), $s['exit_type'], $s['exit_id'], $s['exit_url'], $s['pageviews'], $new_unique, $new_time, wp_json_encode($existing), $s['is_bot'], $sid )); if ($wpdb->last_error) { self::log('UPDATE session failed: ' . $wpdb->last_error); } } } // --- Rebuild summary tables --- $dates = array_unique(array_column($session_data, 'date')); foreach ($dates as $date) { $sql_del_sources = "DELETE FROM {$eistat_p}sources WHERE date = %s"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $wpdb->query($wpdb->prepare($sql_del_sources, $date)); $sql_ins_sources = "INSERT INTO {$eistat_p}sources (date, source, source_detail, referrer_host, sessions, pageviews, total_duration) SELECT date, source, source_detail, referrer_host, COUNT(*), SUM(pageviews), SUM(total_time) FROM {$eistat_p}sessions WHERE date = %s GROUP BY source, source_detail, referrer_host"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $wpdb->query($wpdb->prepare($sql_ins_sources, $date)); $sql_del_daily = "DELETE FROM {$eistat_p}daily WHERE date = %s"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $wpdb->query($wpdb->prepare($sql_del_daily, $date)); $sql_ins_daily = "INSERT INTO {$eistat_p}daily (date, sessions, unique_visitors, pageviews, bounces, bounces_human, bot_sessions, total_duration, total_duration_human, total_duration_approx, total_duration_approx_human) SELECT date, COUNT(*), COUNT(DISTINCT ip_hash), SUM(pageviews), SUM(CASE WHEN unique_pages <= 1 AND total_time < 15 THEN 1 ELSE 0 END), SUM(CASE WHEN unique_pages <= 1 AND total_time < 15 AND is_bot = 0 THEN 1 ELSE 0 END), SUM(CASE WHEN is_bot = 1 THEN 1 ELSE 0 END), SUM(total_time), SUM(CASE WHEN is_bot = 0 THEN total_time ELSE 0 END), SUM(CASE WHEN total_time > 0 THEN total_time ELSE LEAST(TIMESTAMPDIFF(SECOND, first_seen, last_seen), 600) END), SUM(CASE WHEN is_bot = 1 THEN 0 WHEN total_time > 0 THEN total_time ELSE LEAST(TIMESTAMPDIFF(SECOND, first_seen, last_seen), 600) END) FROM {$eistat_p}sessions WHERE date = %s"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $wpdb->query($wpdb->prepare($sql_ins_daily, $date)); if ($wpdb->last_error) { self::log('daily rebuild failed: ' . $wpdb->last_error); } } return count($events); } private static function transition($eistat_p, $date, $from_t, $from_id, $to_t, $to_id) { global $wpdb; $sql = "INSERT INTO {$eistat_p}transitions (date, from_page_type, from_page_id, to_page_type, to_page_id, count) VALUES (%s, %s, %d, %s, %d, 1) ON DUPLICATE KEY UPDATE count = count + 1"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $wpdb->query($wpdb->prepare($sql, $date, $from_t, $from_id, $to_t, $to_id)); } private static function classify_source($referrer, $utm_source, $utm_medium) { $utm_source = strtolower(trim($utm_source)); $utm_medium = strtolower(trim($utm_medium)); if ($utm_source !== '') { $detail = $utm_source; if ($utm_medium === 'messenger' || $utm_medium === 'im') return ['social', $detail]; if ($utm_medium === 'social') return ['social', $detail]; if ($utm_medium === 'email') return ['referral', $detail]; if ($utm_medium === 'cpc' || $utm_medium === 'paid' || $utm_medium === 'ads') return ['other', $detail]; return ['other', $detail]; } if (empty($referrer)) return ['direct', '']; $host = wp_parse_url($referrer, PHP_URL_HOST); if (!$host) return ['direct', '']; $home = wp_parse_url(home_url(), PHP_URL_HOST); if ($home && $host === $home) return ['internal', '']; if (preg_match('/google\./i', $host)) return ['google', '']; if (preg_match('/yandex\./i', $host)) return ['yandex', '']; if (preg_match('/bing\./i', $host)) return ['bing', '']; if (preg_match('/duckduckgo\./i', $host)) return ['other', 'duckduckgo']; if (preg_match('/(^|\.)t\.me$/i', $host)) return ['social', 'telegram']; if (preg_match('/telegram\./i', $host)) return ['social', 'telegram']; if (preg_match('/whatsapp\./i', $host)) return ['social', 'whatsapp']; if (preg_match('/viber\./i', $host)) return ['social', 'viber']; if (preg_match('/signal\./i', $host)) return ['social', 'signal']; if (preg_match('/vk\.com/i', $host)) return ['social', 'vk']; if (preg_match('/facebook\./i', $host)) return ['social', 'facebook']; if (preg_match('/instagram\./i', $host)) return ['social', 'instagram']; if (preg_match('/twitter\.com|x\.com/i', $host)) return ['social', 'twitter']; if (preg_match('/linkedin\./i', $host)) return ['social', 'linkedin']; if (preg_match('/reddit\./i', $host)) return ['social', 'reddit']; if (preg_match('/pinterest\./i', $host)) return ['social', 'pinterest']; return ['referral', '']; } private static function host($referrer) { if (empty($referrer)) return ''; return substr((string)wp_parse_url($referrer, PHP_URL_HOST), 0, 100); } private static function valid_type($t) { static $valid = ['post','page','category','tag','tax','archive','home','search','404','other']; return in_array($t, $valid, true) ? $t : 'other'; } private static function type_to_char($t) { static $map = [ 'post' => 'p', 'page' => 'g', 'category' => 'c', 'tag' => 't', 'tax' => 'x', 'archive' => 'a', 'home' => 'h', 'search' => 's', '404' => '4', 'other' => 'o', ]; return $map[$t] ?? 'o'; } public static function char_to_type($c) { static $map = [ 'p' => 'post', 'g' => 'page', 'c' => 'category', 't' => 'tag', 'x' => 'tax', 'a' => 'archive', 'h' => 'home', 's' => 'search', '4' => '404', 'o' => 'other', ]; return $map[$c] ?? 'other'; } }