'Method not allowed']); exit; } // Read JSON body $raw = file_get_contents('php://input'); $data = json_decode($raw, true); if (!$data || empty($data['event']) || empty($data['page'])) { http_response_code(400); echo json_encode(['error' => 'Invalid payload']); exit; } // Open/create SQLite database $dbPath = __DIR__ . '/data/analytics.db'; $dbDir = dirname($dbPath); if (!is_dir($dbDir)) { mkdir($dbDir, 0750, true); } try { $db = new SQLite3($dbPath); $db->busyTimeout(5000); $db->exec('PRAGMA journal_mode=WAL'); $db->exec('PRAGMA synchronous=NORMAL'); // Tables and indexes are pre-created via CLI setup. // See analytics/setup.php for schema if DB needs recreation. // Privacy: hash the IP, don't store raw $ipHash = hash('sha256', ($_SERVER['REMOTE_ADDR'] ?? '') . date('Y-m-d')); $uaHash = hash('sha256', $_SERVER['HTTP_USER_AGENT'] ?? ''); $sessionId = hash('sha256', $ipHash . $uaHash); // Extract UTM params $utm = $data['utm'] ?? []; // Extract extra data (duration, scroll depth, href, etc.) $extra = []; foreach (['seconds', 'depth', 'href', 'text', 'form_id', 'form_action'] as $key) { if (isset($data[$key])) { $extra[$key] = $data[$key]; } } // Insert event $now = gmdate('Y-m-d H:i:s'); $stmt = $db->prepare('INSERT INTO events (event, page, title, referrer, screen, language, utm_source, utm_medium, utm_campaign, utm_term, utm_content, extra, ip_hash, user_agent_hash, session_id, created_at) VALUES (:event, :page, :title, :referrer, :screen, :language, :utm_source, :utm_medium, :utm_campaign, :utm_term, :utm_content, :extra, :ip_hash, :ua_hash, :session_id, :created_at)'); $stmt->bindValue(':event', substr($data['event'], 0, 50), SQLITE3_TEXT); $stmt->bindValue(':page', substr($data['page'], 0, 500), SQLITE3_TEXT); $stmt->bindValue(':title', substr($data['title'] ?? '', 0, 200), SQLITE3_TEXT); $stmt->bindValue(':referrer', substr($data['referrer'] ?? '', 0, 500), SQLITE3_TEXT); $stmt->bindValue(':screen', substr($data['screen'] ?? '', 0, 20), SQLITE3_TEXT); $stmt->bindValue(':language', substr($data['language'] ?? '', 0, 10), SQLITE3_TEXT); $stmt->bindValue(':utm_source', substr($utm['utm_source'] ?? '', 0, 100), SQLITE3_TEXT); $stmt->bindValue(':utm_medium', substr($utm['utm_medium'] ?? '', 0, 100), SQLITE3_TEXT); $stmt->bindValue(':utm_campaign', substr($utm['utm_campaign'] ?? '', 0, 100), SQLITE3_TEXT); $stmt->bindValue(':utm_term', substr($utm['utm_term'] ?? '', 0, 100), SQLITE3_TEXT); $stmt->bindValue(':utm_content', substr($utm['utm_content'] ?? '', 0, 100), SQLITE3_TEXT); $stmt->bindValue(':extra', !empty($extra) ? json_encode($extra) : null, SQLITE3_TEXT); $stmt->bindValue(':ip_hash', $ipHash, SQLITE3_TEXT); $stmt->bindValue(':ua_hash', $uaHash, SQLITE3_TEXT); $stmt->bindValue(':session_id', $sessionId, SQLITE3_TEXT); $stmt->bindValue(':created_at', $now, SQLITE3_TEXT); $stmt->execute(); // Update daily stats for pageviews if ($data['event'] === 'pageview') { $today = date('Y-m-d'); $safePage = SQLite3::escapeString(substr($data['page'], 0, 500)); $db->exec("INSERT INTO daily_stats (date, page, pageviews, unique_visitors) VALUES ('$today', '$safePage', 1, 1) ON CONFLICT(date, page) DO UPDATE SET pageviews = pageviews + 1, unique_visitors = ( SELECT COUNT(DISTINCT session_id) FROM events WHERE page = '$safePage' AND event = 'pageview' AND date(created_at) = '$today' )"); } // Update duration in daily stats if ($data['event'] === 'duration' && isset($data['seconds'])) { $today = date('Y-m-d'); $safePage = SQLite3::escapeString(substr($data['page'], 0, 500)); $db->exec("UPDATE daily_stats SET avg_duration = ( SELECT AVG(CAST(json_extract(extra, '$.seconds') AS REAL)) FROM events WHERE page = '$safePage' AND event = 'duration' AND date(created_at) = '$today' ) WHERE date = '$today' AND page = '$safePage'"); } $db->close(); http_response_code(200); echo json_encode(['ok' => true]); } catch (Exception $e) { http_response_code(500); echo json_encode(['error' => 'Database error']); }