'network_error', 'http_request_timeout' => 'timeout_error', 'http_failure' => 'network_error', ]; public static function report(array $entry): void { $sanitized = self::sanitizeEntry($entry); $service = ContaiLogsService::getInstance(); $response = $service->reportClientLogs([$sanitized]); if (!$response->isSuccess()) { self::bufferLocally($sanitized); } } public static function bufferLocally(array $entry): void { $sanitized = self::sanitizeEntry($entry); $buffer = get_transient(self::BUFFER_TRANSIENT); if (!is_array($buffer)) { $buffer = []; } $buffer[] = $sanitized; // FIFO: remove oldest if over limit while (count($buffer) > self::MAX_BUFFER_SIZE) { array_shift($buffer); } set_transient(self::BUFFER_TRANSIENT, $buffer, self::BUFFER_EXPIRY); } public static function syncBuffer(): void { $lock = get_transient(self::LOCK_TRANSIENT); if ($lock) { return; } set_transient(self::LOCK_TRANSIENT, true, self::LOCK_EXPIRY); try { $buffer = get_transient(self::BUFFER_TRANSIENT); if (!is_array($buffer) || empty($buffer)) { delete_transient(self::LOCK_TRANSIENT); return; } // Process in chunks of 50 $chunks = array_chunk($buffer, 50); $remainingEntries = []; foreach ($chunks as $chunk) { $service = ContaiLogsService::getInstance(); $response = $service->reportClientLogs($chunk); if (!$response->isSuccess()) { // Backend still down, keep all entries $remainingEntries = array_merge($remainingEntries, $chunk); continue; } $data = $response->getData(); $rejected = $data['rejected'] ?? 0; if ($rejected > 0 && !empty($data['errors'])) { // Keep rejected entries $rejectedIndices = array_column($data['errors'], 'index'); foreach ($rejectedIndices as $index) { if (isset($chunk[$index])) { $remainingEntries[] = $chunk[$index]; } } } } if (empty($remainingEntries)) { delete_transient(self::BUFFER_TRANSIENT); } else { set_transient(self::BUFFER_TRANSIENT, $remainingEntries, self::BUFFER_EXPIRY); } } finally { delete_transient(self::LOCK_TRANSIENT); } } public static function getBufferCount(): int { $buffer = get_transient(self::BUFFER_TRANSIENT); return is_array($buffer) ? count($buffer) : 0; } public static function buildEntryFromWpError( WP_Error $error, string $method, string $endpoint, string $action, string $sourceModule, ?string $websiteId = null, ?string $provider = null ): array { return [ 'timestamp' => gmdate('c'), 'method' => $method, 'endpoint' => $endpoint, 'action' => $action, 'source_module' => $sourceModule, 'website_id' => $websiteId, 'provider' => $provider, 'response_status' => 0, 'response_message' => substr($error->get_error_message(), 0, 2000), 'success' => false, 'error_type' => self::mapWpErrorToType($error->get_error_code()), 'trace_id' => null, 'request_context' => [ 'wp_error_code' => $error->get_error_code(), 'wp_error_message' => substr($error->get_error_message(), 0, 200), 'php_version' => PHP_VERSION, 'wp_version' => get_bloginfo('version'), 'plugin_version' => defined('CONTAI_VERSION') ? CONTAI_VERSION : '2.3.5', ], ]; } private static function mapWpErrorToType(string $code): string { return self::WP_ERROR_TYPE_MAP[$code] ?? 'unknown_error'; } private static function sanitizeEntry(array $entry): array { $sanitized = []; foreach (self::ALLOWED_ENTRY_KEYS as $key) { if (isset($entry[$key])) { $sanitized[$key] = $entry[$key]; } } // Truncate response_message if (isset($sanitized['response_message'])) { $sanitized['response_message'] = substr($sanitized['response_message'], 0, 2000); } // Filter request_context if (isset($sanitized['request_context']) && is_array($sanitized['request_context'])) { $filteredContext = []; foreach (self::ALLOWED_CONTEXT_KEYS as $key) { if (isset($sanitized['request_context'][$key])) { $filteredContext[$key] = substr((string) $sanitized['request_context'][$key], 0, 200); } } $sanitized['request_context'] = $filteredContext; } return $sanitized; } }