diff options
Diffstat (limited to 'api/debug/clusters.php')
| -rw-r--r-- | api/debug/clusters.php | 98 |
1 files changed, 98 insertions, 0 deletions
diff --git a/api/debug/clusters.php b/api/debug/clusters.php new file mode 100644 index 0000000..5174317 --- /dev/null +++ b/api/debug/clusters.php @@ -0,0 +1,98 @@ +<?php +require_once __DIR__ . '/config.php'; + +$errors = load_json(ERRORS_FILE); +$rebuildFlag = DEBUG_DATA_DIR . '/.rebuild_clusters'; + +// Check if cached clusters exist and are fresh +if (file_exists(CLUSTERS_FILE) && !file_exists($rebuildFlag)) { + $clusters = load_json(CLUSTERS_FILE); + json_response([ + 'success' => true, + 'cached' => true, + 'clusters' => $clusters, + 'total_errors' => count($errors), + ]); +} + +// Rebuild clusters +$normalized = []; +foreach ($errors as $e) { + $msg = strtolower($e['message'] ?? ''); + $msg = preg_replace('/\b0x[a-f0-9]+\b/', '0xHEX', $msg); + $msg = preg_replace('/\b\d+\b/', 'N', $msg); + $msg = preg_replace('/["\'].*?["\']/', '"STR"', $msg); + $msg = preg_replace('/[^\w\s]/', ' ', $msg); + $msg = preg_replace('/\s+/', ' ', $msg); + $normalized[] = trim($msg); +} + +$clusters = []; +$assigned = []; +$threshold = CLUSTER_SIMILARITY_THRESHOLD; + +for ($i = 0; $i < count($errors); $i++) { + if (isset($assigned[$i])) continue; + + $cluster = [ + 'errors' => [$i], + 'messages' => [$errors[$i]['message']], + 'count' => $errors[$i]['count'] ?? 1, + 'types' => [$errors[$i]['type'] ?? 'unknown'], + 'categories' => [$errors[$i]['category'] ?? 'runtime'], + 'label' => $errors[$i]['message'] ?? '', + ]; + $assigned[$i] = true; + + for ($j = $i + 1; $j < count($errors); $j++) { + if (isset($assigned[$j])) continue; + + // Jaccard similarity + $tokensA = array_unique(array_filter(explode(' ', $normalized[$i]))); + $tokensB = array_unique(array_filter(explode(' ', $normalized[$j]))); + if (empty($tokensA) && empty($tokensB)) { + $similarity = 1; + } else { + $intersection = count(array_intersect($tokensA, $tokensB)); + $union = count($tokensA) + count($tokensB) - $intersection; + $similarity = $union > 0 ? $intersection / $union : 0; + } + + if ($similarity >= $threshold) { + $cluster['errors'][] = $j; + $cluster['messages'][] = $errors[$j]['message']; + $cluster['count'] += $errors[$j]['count'] ?? 1; + if (!in_array($errors[$j]['type'] ?? 'unknown', $cluster['types'])) { + $cluster['types'][] = $errors[$j]['type'] ?? 'unknown'; + } + if (!in_array($errors[$j]['category'] ?? 'runtime', $cluster['categories'])) { + $cluster['categories'][] = $errors[$j]['category'] ?? 'runtime'; + } + $assigned[$j] = true; + } + } + + // Most frequent message as label + $freq = array_count_values($cluster['messages']); + arsort($freq); + $cluster['label'] = substr(key($freq) ?: $cluster['label'], 0, 200); + $cluster['error_count'] = count($cluster['errors']); + unset($cluster['errors']); + unset($cluster['messages']); + + $clusters[] = $cluster; +} + +usort($clusters, fn($a, $b) => $b['count'] - $a['count']); + +// Cache +save_json(CLUSTERS_FILE, $clusters); +if (file_exists($rebuildFlag)) unlink($rebuildFlag); + +json_response([ + 'success' => true, + 'cached' => false, + 'clusters' => array_slice($clusters, 0, 50), + 'total_errors' => count($errors), + 'total_clusters' => count($clusters), +]); |