aboutsummaryrefslogtreecommitdiff
path: root/api/debug/clusters.php
blob: 51743175a07393226f4ebe24a03b5dff1e7a7cf8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
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),
]);