aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-10-26 14:45:36 +0100
committerAgando <roduccomm@gmail.com>2026-10-26 14:45:36 +0100
commit02ffef703a1b8d85915f344e182a1a4e67dac9df (patch)
treea0a5326b183015584a9f596ebfc2cf39ec32706f
parent9a770fcad562ce7f22a2738e17a69a10128abc2d (diff)
feat(newos4): filter network-class auto reports at ingest — count, don't file
The debug supervisor files an auto report for any failed fetch, so a flaky phone connection or a retired endpoint became a review card. One cluster held 14 407 + 1 031 occurrences and produced 3 of the 19 cards in the queue; 10 of 21 items reviewed on 2026-10-26 were noise for this reason. api/debug/report.php: an auto report whose *every* error is network-class (fetch failed / failed to fetch / network error / load failed / aborted / signal is aborted / ERR_INTERNET_*) now increments data/debug/network_failures.json (per day, per source path, top 40 kept) and returns 202 {filtered:"network"}. No RPT file, no card. Never filtered: any report with auto=false (user content), and any auto report carrying a stack/TypeError, i.e. a real defect. Verified on a throwaway tree and live through nginx: auto network poll.php -> 202 filtered, no file auto network /ollama/ -> 202 filtered, no file auto TypeError + stack -> 201 filed (RPT-20261026-134455-95f10612) user report w/ "fetch" -> 201 filed (RPT-20261026-134455-ab0f2cd4) Live: counter total=1 by_source {'/spaces/newos4/api/chat/poll.php': 1}, reports stayed at 19. The dashboard now shows the counter instead of silence: "🌐 1 network failure counted at ingest today, not filed as cards — /spaces/newos4/api/chat/poll.php ×1"
-rw-r--r--api/debug/report.php59
-rwxr-xr-xscripts/newos4-feedback-collect.py6
2 files changed, 65 insertions, 0 deletions
diff --git a/api/debug/report.php b/api/debug/report.php
index 5625ffe..2ee9c82 100644
--- a/api/debug/report.php
+++ b/api/debug/report.php
@@ -73,6 +73,65 @@ foreach ($rate as $k => $v) {
}
save_json(RATE_FILE, $rate);
+// ── Network-class filter (2026-10-26) ────────────────────────────────────────────────────────
+// The debug supervisor files an auto report for *any* failed fetch, so a flaky phone connection
+// or a retired endpoint becomes a review card. One cluster alone held 14 400 occurrences and
+// produced 3 cards. A `Fetch failed:` line is a retry, not a defect: count it and stop.
+// Only auto reports whose every error is network-class are filtered — a stack trace, TypeError
+// or user-filed report is never dropped here.
+define('NET_FILE', DEBUG_DATA_DIR . '/network_failures.json');
+define('NET_KEEP_SOURCES', 40); // bounded: keep the noisiest paths only
+
+$isNetworkMsg = function ($m) {
+ return (bool)preg_match(
+ '/fetch failed|failed to fetch|networkerror|network error|load failed'
+ . '|the user aborted a request|signal is aborted|err_internet_disconnected|err_network_changed/i',
+ (string)$m
+ );
+};
+
+if (!empty($diag['auto']) && !empty($diag['errors']) && is_array($diag['errors'])) {
+ $msgs = [];
+ foreach ($diag['errors'] as $de) {
+ if (is_array($de)) $msgs[] = (string)($de['message'] ?? '');
+ }
+ $netCount = count(array_filter($msgs, $isNetworkMsg));
+ if ($msgs && $netCount === count($msgs)) {
+ $day = gmdate('Y-m-d');
+ $net = load_json(NET_FILE);
+ if (!is_array($net) || ($net['day'] ?? '') !== $day) {
+ $net = ['day' => $day, 'total' => 0, 'filtered_reports' => 0, 'by_source' => []];
+ }
+ $net['total'] = (int)($net['total'] ?? 0) + 1;
+ $net['filtered_reports'] = (int)($net['filtered_reports'] ?? 0) + 1;
+ $src = 'unknown';
+ foreach (array_merge($msgs, [$title]) as $candidate) {
+ if (preg_match('~(/[A-Za-z0-9_\-./]+\.(?:php|json))~', (string)$candidate, $m)) {
+ $src = $m[1];
+ break;
+ }
+ if (preg_match('~(https?://[^\s]+)~', (string)$candidate, $m)) {
+ $src = preg_replace('~^https?://[^/]+~', '', $m[1]);
+ break;
+ }
+ }
+ $net['by_source'][$src] = (int)($net['by_source'][$src] ?? 0) + 1;
+ if (count($net['by_source']) > NET_KEEP_SOURCES) {
+ arsort($net['by_source']);
+ $net['by_source'] = array_slice($net['by_source'], 0, NET_KEEP_SOURCES, true);
+ }
+ save_json(NET_FILE, $net);
+ json_response([
+ 'success' => true,
+ 'filtered' => 'network',
+ 'source' => $src,
+ 'day' => $day,
+ 'count_today' => $net['total'],
+ 'url' => null,
+ ], 202);
+ }
+}
+
// Dedupe: same title + error set + version within 24h collapses onto the existing report.
// Hash only the fields the pre-2026-10-23 payload had. Adding `stack` to json_encode($diag['errors'])
// would change every fingerprint, so an identical error filed today would not dedupe against the
diff --git a/scripts/newos4-feedback-collect.py b/scripts/newos4-feedback-collect.py
index e73238c..226a74c 100755
--- a/scripts/newos4-feedback-collect.py
+++ b/scripts/newos4-feedback-collect.py
@@ -16,6 +16,7 @@ TRIAGE = os.path.join(ROOT, 'admin/docs/newos4-triage-latest.json')
STEER = os.path.join(ROOT, 'spaces/newos4/data/maintenance/steering.json')
DECISIONS = os.path.join(ROOT, 'spaces/newos4/data/feedback/decisions.jsonl')
APPLIED = os.path.join(ROOT, 'spaces/newos4/data/feedback/applied.json') # ledger written by the applier
+NETFAIL = os.path.join(ROOT, 'spaces/newos4/data/debug/network_failures.json') # ingest-filter counter
def load(path, default):
@@ -131,7 +132,12 @@ def main():
plans = plans_needing_decision(dec)
reports = reports_needing_decision(dec)
awaiting = [x for x in plans + reports if not x.get('decision')]
+ net = load(NETFAIL, {}) or {}
+ net_top = sorted(((net.get('by_source') or {}) if isinstance(net.get('by_source'), dict) else {}).items(),
+ key=lambda kv: -int(kv[1]))[:3]
doc = {
+ 'network_failures': {'day': net.get('day'), 'total': int(net.get('total') or 0),
+ 'top': [[str(k), int(v)] for k, v in net_top]},
'generated_at': datetime.now(timezone.utc).isoformat(timespec='seconds'),
'counts': {
'plans_review': len(plans), 'reports_total': len(reports),