start(); } $sid = session_id() ?: bin2hex(random_bytes(16)); } catch (\Throwable) { $sid = 'no-session'; // CLI/tests: stable, never random } $salt = env('encryption.key', 'tv-fallback-salt'); $hash = hash('sha256', $sid . '|' . $salt); } return substr($hash, 0, 32); } public function ipHash(): string { $salt = env('encryption.key', 'tv-fallback-salt'); return hash('sha256', service('request')->getIPAddress() . '|' . $salt); } public function track(string $name, array $data = []): void { if (! in_array($name, self::EVENTS, true)) { return; } try { model(AnalyticsEventModel::class)->record([ 'name' => $name, 'tool_slug' => $data['tool'] ?? null, 'format' => isset($data['format']) ? mb_substr((string) $data['format'], 0, 16) : null, 'value' => (int) ($data['value'] ?? 0), 'session_id' => $this->sessionHash(), 'country' => $this->guessCountry(), 'meta' => json_encode($data['meta'] ?? new \stdClass(), JSON_UNESCAPED_UNICODE), 'created_at' => date('Y-m-d H:i:s'), ]); } catch (\Throwable $e) { log_message('error', 'analytics write failed: {m}', ['m' => $e->getMessage()]); } } /** Country from CDN geo headers when present (Cloudflare/Fastly), else null. */ private function guessCountry(): ?string { $cc = $_SERVER['HTTP_CF_IPCOUNTRY'] ?? $_SERVER['X-Geo-Country'] ?? null; return is_string($cc) && preg_match('/^[A-Z]{2}$/', $cc) === 1 ? $cc : null; } }