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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
|
<?php
require_once __DIR__ . '/config.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
error_response('Method not allowed', 405);
}
$raw = file_get_contents('php://input');
if (!$raw) error_response('Empty request body');
$payload = json_decode($raw, true);
if (!$payload || !isset($payload['errors']) || !is_array($payload['errors'])) {
error_response('Invalid payload: expected { errors: [...], session: {...} }');
}
$errors = $payload['errors'];
$session = $payload['session'] ?? [];
$clientVersion = $payload['version'] ?? 'unknown';
// Client-supplied counts are untrusted: clamp so a bad client cannot store 38,495× rows.
function clamp_count($v) {
$n = (int)$v;
return $n < 1 ? 1 : ($n > 1000 ? 1000 : $n);
}
// Validate batch size
if (count($errors) > MAX_ERRORS_PER_BATCH) {
$errors = array_slice($errors, 0, MAX_ERRORS_PER_BATCH);
}
if (strlen($raw) > MAX_BATCH_SIZE_KB * 1024) {
error_response('Payload too large (max ' . MAX_BATCH_SIZE_KB . 'KB)', 413);
}
// Load existing data
$allErrors = load_json(ERRORS_FILE);
$sessions = load_json(SESSIONS_FILE);
$sessionId = $session['id'] ?? ('anon_' . bin2hex(random_bytes(4)));
$ingested = 0;
$now = date('c');
// Update session metadata
if (!isset($sessions[$sessionId])) {
$sessions[$sessionId] = [
'id' => $sessionId,
'first_seen' => $now,
'version' => $clientVersion,
'modules' => $session['modules'] ?? [],
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
'ip' => $_SERVER['REMOTE_ADDR'] ?? '',
'error_count' => 0,
];
}
$sessions[$sessionId]['last_seen'] = $now;
$sessions[$sessionId]['error_count'] += count($errors);
$sessions[$sessionId]['version'] = $clientVersion;
// Build a lookup of existing hashes for dedup
$existingHashes = [];
foreach ($allErrors as $idx => $existing) {
$h = $existing['hash'] ?? '';
if ($h) {
$existingHashes[$h] = $idx;
}
}
// Ingest each error (dedup by hash)
foreach ($errors as $e) {
$hash = $e['hash'] ?? md5(($e['message'] ?? '') . '|' . ($e['source'] ?? '') . '|' . ($e['type'] ?? ''));
if (isset($existingHashes[$hash])) {
// Update existing entry
$idx = $existingHashes[$hash];
$allErrors[$idx]['count'] += clamp_count($e['count'] ?? 1);
$allErrors[$idx]['last_seen'] = $e['lastSeen'] ?? $now;
$allErrors[$idx]['ingested_at'] = $now;
if (!empty($e['stack'])) {
$allErrors[$idx]['stack'] = substr($e['stack'], 0, 2000);
}
$ingested++;
continue;
}
$entry = [
'hash' => $hash,
'message' => substr($e['message'] ?? '', 0, 500),
'type' => $e['type'] ?? 'unknown',
'severity' => $e['severity'] ?? 'error',
'category' => $e['category'] ?? 'runtime',
'source' => $e['source'] ?? '',
'module' => $e['module'] ?? null,
'stack' => isset($e['stack']) ? substr($e['stack'], 0, 2000) : null,
'session_id' => $sessionId,
'count' => clamp_count($e['count'] ?? 1),
'first_seen' => $e['firstSeen'] ?? $now,
'last_seen' => $e['lastSeen'] ?? $now,
'ingested_at' => $now,
];
$allErrors[] = $entry;
$existingHashes[$hash] = count($allErrors) - 1;
$ingested++;
}
// Trim to last 10,000 errors (ring buffer)
if (count($allErrors) > 10000) {
$allErrors = array_slice($allErrors, -10000);
}
save_json(ERRORS_FILE, $allErrors);
save_json(SESSIONS_FILE, $sessions);
// Rebuild clusters asynchronously (flag file)
$rebuildFlag = DEBUG_DATA_DIR . '/.rebuild_clusters';
file_put_contents($rebuildFlag, time());
json_response([
'success' => true,
'ingested' => $ingested,
'session_id' => $sessionId,
'total_errors' => count($allErrors),
'total_sessions' => count($sessions),
]);
|