check(Http::clientIp())) { Http::fail(429, 'rate_limited', 'Zu viele Fehlermeldungen. Bitte Sendefrequenz reduzieren.'); } $data = Http::body(); if ($data === []) { Http::fail(400, 'empty_body', 'Der Request-Body ist leer.'); } $environment = is_string($data['environment'] ?? null) ? $data['environment'] : null; $context = ApiAuth::requireScope($db, 'bugtracker:report', $environment, false); $projectSlug = is_string($data['project_slug'] ?? null) ? trim($data['project_slug']) : null; ApiAuth::enforceProject($context, $projectSlug); $boundProject = ApiAuth::projectFilter($context); if ($boundProject !== null) { $projectSlug = $boundProject; } // --- Eingaben normalisieren --- $exception = trimOrNull($data['exception'] ?? $data['exception_class'] ?? $data['type'] ?? null); $message = trimOrNull($data['message'] ?? $data['error_message'] ?? null); $stack = trimOrNull($data['stack_trace'] ?? $data['stacktrace'] ?? $data['trace'] ?? null); if ($message === null && $exception === null) { Http::fail(400, 'missing_error', 'Es wird mindestens "message" oder "exception" benoetigt.'); } // fatal = Prozess beendet, error = Vorgang fehlgeschlagen, Programm laeuft weiter. $level = is_string($data['level'] ?? $data['error_level'] ?? null) ? strtolower(trim($data['level'] ?? $data['error_level'])) : 'error'; if (!in_array($level, BugRepo::ERROR_LEVELS, true)) { $level = 'error'; } $repo = new BugRepo($db); $result = $repo->reportItem([ 'project_slug' => $projectSlug ?? 'default', 'type' => 'bug', 'title' => buildTitle($exception, $message), 'error_message' => $message, 'stack_trace' => $stack, 'exception' => $exception, 'error_level' => $level, 'severity' => $level === 'fatal' ? 'high' : 'medium', 'environment' => $environment ?? 'production', 'build_version' => trimOrNull($data['build'] ?? $data['build_version'] ?? null) ?? 'unknown', 'created_by' => $context['actor'], 'client_ref' => trimOrNull($data['client_ref'] ?? null) ?? Http::header('idempotency-key'), // Optionaler Kontext - alles freiwillig 'repo_url' => trimOrNull($data['repo_url'] ?? null), 'git_branch' => trimOrNull($data['git_branch'] ?? null), 'commit_sha' => trimOrNull($data['commit_sha'] ?? null), 'file_path' => trimOrNull($data['file'] ?? $data['file_path'] ?? null), 'line_no' => $data['line'] ?? $data['line_no'] ?? null, 'tags' => trimOrNull($data['tags'] ?? null), 'context' => isset($data['context']) && is_array($data['context']) ? $data['context'] : null, ]); $ignored = (bool)($result['ignored'] ?? false); Http::ok([ 'item_id' => $result['id'], 'is_new' => $result['is_new'], 'ignored' => $ignored, 'ignore_rule_id' => $result['ignore_rule_id'] ?? null, 'occurrence_count' => $result['occurrence_count'], 'error_level' => $level, 'rate_alerted' => (bool)($result['rate_alerted'] ?? false), 'message' => $ignored ? 'Als bekannt eingestuft, gezaehlt, nicht gemeldet.' : ($result['is_new'] ? 'Fehler erfasst.' : 'Wiederkehrender Fehler, Zaehler erhoeht.'), ], $result['is_new'] ? 201 : 200); // ====================================================================== /** @param mixed $value */ function trimOrNull($value): ?string { if ($value === null || is_array($value) || is_object($value)) { return null; } $value = trim((string)$value); return $value === '' ? null : $value; } /** * Baut einen lesbaren Titel aus Ausnahmeklasse und Meldung. * * Die Meldung wird gekuerzt und von veraenderlichen Bestandteilen befreit, * damit der Titel bei wiederkehrenden Fehlern stabil bleibt. */ function buildTitle(?string $exception, ?string $message): string { $summary = $message ?? ''; // Anfuehrungszeichen mit wechselndem Inhalt entfernen, z. B. konkrete // Schluesselwerte in "Duplicate entry 'XY-123' for key ..." $summary = preg_replace("/'[^']{0,80}'/", "'…'", $summary) ?? $summary; $summary = trim(preg_replace('/\s+/', ' ', $summary) ?? $summary); if ($summary === '') { return $exception ?? 'Unbehandelter Fehler'; } if (mb_strlen($summary) > 180) { $summary = mb_substr($summary, 0, 177) . '...'; } return $exception !== null ? $exception . ': ' . $summary : $summary; }