error logging improvement, regex fix

This commit is contained in:
tdurieux
2026-05-06 11:09:17 +03:00
parent e34f45522f
commit c2d43164d0
39 changed files with 747 additions and 126 deletions
+1 -1
View File
File diff suppressed because one or more lines are too long
+20 -1
View File
@@ -4702,4 +4702,23 @@ textarea::selection {
}
.file.folder.truncated > a {
color: #d39e00;
}
}
/* Errors admin */
.errors-table .error-when time { font-variant-numeric: tabular-nums; color: #555; cursor: help; }
.errors-table .error-msg-line { display: flex; flex-wrap: wrap; gap: 6px; align-items: baseline; }
.errors-table .error-chip {
display: inline-flex; align-items: center; gap: 4px;
font-size: 0.78rem; padding: 1px 6px; border-radius: 999px;
background: #eef0f3; color: #333; border: 1px solid #dde0e4;
max-width: 36em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.errors-table .error-chip .chip-label { color: #777; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.03em; }
.errors-table .error-chip.chip-err { background: #fdecec; border-color: #f5c2c2; color: #8a1f1f; }
.errors-table .error-chip.chip-warn { background: #fff5e1; border-color: #f3d9a4; color: #7a4d00; }
.errors-table .error-chip.chip-ok { background: #e9f6ec; border-color: #b8dfc1; color: #1f6b32; }
.errors-table .error-chip.chip-mono .chip-value { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.78rem; }
.errors-table .pill-module { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.78rem; background: #eef0f3; color: #333; padding: 1px 6px; border-radius: 4px; }
.errors-table .error-details { margin-top: 6px; }
.errors-table .error-details summary { cursor: pointer; color: #666; font-size: 0.82rem; }
.errors-table .error-details pre { background: #fafafa; border: 1px solid #ececec; border-radius: 4px; padding: 8px; font-size: 0.78rem; max-height: 18em; overflow: auto; }
.errors-table .error-context { color: #888; font-size: 0.78rem; font-style: italic; margin-left: 4px; }
+1
View File
@@ -7,6 +7,7 @@
<a href="/admin/users"><i class="fas fa-users"></i> Users</a>
<a href="/admin/conferences" class="active"><i class="fas fa-chalkboard-teacher"></i> Conferences</a>
<a href="/admin/queues"><i class="fas fa-tasks"></i> Queues</a>
<a href="/admin/errors"><i class="fas fa-bug"></i> Errors</a>
</nav>
<div class="admin-summary">
+77
View File
@@ -0,0 +1,77 @@
<div class="container paper-page admin-page">
<div class="paper-crumbs">Admin &nbsp;/&nbsp; <span class="here">Errors</span></div>
<h1 class="paper-page-title">Errors</h1>
<nav class="admin-nav">
<a href="/admin/"><i class="fas fa-code-branch"></i> Repositories</a>
<a href="/admin/users"><i class="fas fa-users"></i> Users</a>
<a href="/admin/conferences"><i class="fas fa-chalkboard-teacher"></i> Conferences</a>
<a href="/admin/queues"><i class="fas fa-tasks"></i> Queues</a>
<a href="/admin/errors" class="active"><i class="fas fa-bug"></i> Errors</a>
</nav>
<div class="admin-summary">
<span class="summary-pill error">{{filtered.length}} shown</span>
<span class="summary-pill">{{entries.length}} captured</span>
<span class="summary-pill" ng-if="!available">redis sink unavailable</span>
</div>
<form class="w-100 admin-filter-toolbar" aria-label="Error filters">
<div class="admin-filter-row">
<div class="search-wrap">
<input type="search" class="form-control" placeholder="Search message, module, or url…" ng-model="query.search" autocomplete="off" />
</div>
<span class="admin-filter-inline">
<label>Module</label>
<select class="form-control form-control-sm" ng-model="query.module">
<option value="">Any</option>
<option ng-repeat="m in modules" value="{{m}}">{{m}}</option>
</select>
</span>
<span class="admin-filter-spacer"></span>
<label class="admin-filter-inline" style="cursor:pointer;">
<input type="checkbox" ng-model="query.autoRefresh" />
Auto-refresh
</label>
<button class="btn btn-sm" type="button" ng-click="refreshNow()" title="Refresh now"><i class="fas fa-sync"></i></button>
<button class="btn btn-sm btn-danger" type="button" ng-click="clearAll()" title="Clear all errors"><i class="fas fa-trash"></i> Clear</button>
</div>
</form>
<div ng-if="!filtered.length" class="admin-empty">No errors captured.</div>
<table class="table errors-table" ng-if="filtered.length">
<thead>
<tr>
<th style="width: 9em;">When</th>
<th style="width: 9em;">Module</th>
<th>Message</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="e in filtered track by $index">
<td class="error-when">
<time title="{{absTime(e.ts)}}">{{relTime(e.ts)}}</time>
</td>
<td><span class="pill pill-module">{{e.module}}</span></td>
<td class="error-msg">
<div class="error-msg-line">
<strong>{{e.displayMessage}}</strong>
<span class="error-context" ng-if="e.displayContext && e.displayContext !== e.displayMessage">{{e.displayContext}}</span>
<span class="error-chip"
ng-repeat="c in e._chips track by $index"
ng-class="{'chip-err': c.kind === 'err', 'chip-warn': c.kind === 'warn', 'chip-ok': c.kind === 'ok', 'chip-mono': c.mono}"
title="{{c.label}}: {{c.value}}">
<span class="chip-label">{{c.label}}</span>
<span class="chip-value">{{c.value}}</span>
</span>
</div>
<details ng-if="e._detailJson" class="error-details">
<summary>raw</summary>
<pre>{{e._detailJson}}</pre>
</details>
</td>
</tr>
</tbody>
</table>
</div>
+1
View File
@@ -7,6 +7,7 @@
<a href="/admin/users"><i class="fas fa-users"></i> Users</a>
<a href="/admin/conferences"><i class="fas fa-chalkboard-teacher"></i> Conferences</a>
<a href="/admin/queues" class="active"><i class="fas fa-tasks"></i> Queues</a>
<a href="/admin/errors"><i class="fas fa-bug"></i> Errors</a>
</nav>
<div class="admin-summary">
+1
View File
@@ -7,6 +7,7 @@
<a href="/admin/users"><i class="fas fa-users"></i> Users</a>
<a href="/admin/conferences"><i class="fas fa-chalkboard-teacher"></i> Conferences</a>
<a href="/admin/queues"><i class="fas fa-tasks"></i> Queues</a>
<a href="/admin/errors"><i class="fas fa-bug"></i> Errors</a>
</nav>
<div class="admin-summary">
+1
View File
@@ -7,6 +7,7 @@
<a href="/admin/users" class="active"><i class="fas fa-users"></i> Users</a>
<a href="/admin/conferences"><i class="fas fa-chalkboard-teacher"></i> Conferences</a>
<a href="/admin/queues"><i class="fas fa-tasks"></i> Queues</a>
<a href="/admin/errors"><i class="fas fa-bug"></i> Errors</a>
</nav>
<div class="user-detail-card" ng-if="userInfo">
+1
View File
@@ -7,6 +7,7 @@
<a href="/admin/users" class="active"><i class="fas fa-users"></i> Users</a>
<a href="/admin/conferences"><i class="fas fa-chalkboard-teacher"></i> Conferences</a>
<a href="/admin/queues"><i class="fas fa-tasks"></i> Queues</a>
<a href="/admin/errors"><i class="fas fa-bug"></i> Errors</a>
</nav>
<div class="admin-summary">
+7
View File
@@ -9,6 +9,13 @@
<h1 class="paper-page-title pr-title">
<span ng-if="details.description" ng-bind="details.description"></span>
<span ng-if="!details.description" class="text-muted">Untitled gist</span>
<a
ng-if="options.isAdmin || options.isOwner"
ng-href="/gist-anonymize/{{gistId}}"
class="btn btn-sm"
aria-label="Edit"
><i class="far fa-edit"></i><span class="d-none d-md-inline"> Edit</span></a
>
</h1>
<div class="pr-header-meta">
<span class="paper-pill" ng-class="{'good': details.isPublic, 'warn': !details.isPublic}">
+7
View File
@@ -9,6 +9,13 @@
<h1 class="paper-page-title pr-title">
<span ng-if="details.title" ng-bind="details.title"></span>
<span ng-if="!details.title" class="text-muted">Untitled pull request</span>
<a
ng-if="options.isAdmin || options.isOwner"
ng-href="/pull-request-anonymize/{{pullRequestId}}"
class="btn btn-sm"
aria-label="Edit"
><i class="far fa-edit"></i><span class="d-none d-md-inline"> Edit</span></a
>
</h1>
<div class="pr-header-meta">
<span class="paper-pill" ng-class="{'good': details.merged, 'warn': details.state == 'open', 'bad': details.state == 'closed' && !details.merged}">
+146
View File
@@ -849,4 +849,150 @@ angular
);
$scope.$watch("query.state", getQueues);
},
])
.controller("errorsAdminController", [
"$scope",
"$http",
"$location",
"$interval",
function ($scope, $http, $location, $interval) {
$scope.$watch("user.status", () => {
if ($scope.user == null) {
$location.url("/");
}
});
if ($scope.user == null) {
$location.url("/");
}
$scope.entries = [];
$scope.filtered = [];
$scope.modules = [];
$scope.available = true;
$scope.query = {
search: "",
module: "",
autoRefresh: true,
};
$scope.relTime = (iso) => {
if (!iso) return "";
const t = new Date(iso).getTime();
if (isNaN(t)) return iso;
const diff = Math.max(0, Date.now() - t);
const s = Math.floor(diff / 1000);
if (s < 5) return "just now";
if (s < 60) return `${s}s ago`;
const m = Math.floor(s / 60);
if (m < 60) return `${m}m ago`;
const h = Math.floor(m / 60);
if (h < 24) return `${h}h ago`;
const d = Math.floor(h / 24);
if (d < 7) return `${d}d ago`;
return new Date(iso).toLocaleDateString();
};
$scope.absTime = (iso) => {
if (!iso) return "";
const d = new Date(iso);
if (isNaN(d.getTime())) return iso;
return d.toLocaleString();
};
// Decorate each entry once with derived display fields (chips + json).
// Returning a fresh array from a template-bound function each digest
// cycle triggers Angular's $rootScope:infdig — so we precompute on load.
function statusKind(s) {
const n = parseInt(s, 10);
if (!n) return "";
if (n >= 500) return "err";
if (n >= 400) return "warn";
return "ok";
}
// snake_case identifier looking like an error key (e.g. "repo_not_found").
const errorKeyRe = /^[a-z][a-z0-9]*(?:_[a-z0-9]+)+$/;
function decorate(e) {
const chips = [];
const detail = (e.raw || []).find(
(a) => a && typeof a === "object" && !Array.isArray(a)
);
if (detail) {
// Prefer the structured error key (e.g. "pull_request_not_found")
// over the generic logger message ("anonymous error", "http error").
if (detail.message && errorKeyRe.test(detail.message)) {
e.displayMessage = detail.message;
e.displayContext = e.message;
} else if (detail.code && errorKeyRe.test(String(detail.code))) {
e.displayMessage = String(detail.code);
e.displayContext = e.message;
} else {
e.displayMessage = e.message;
}
if (detail.httpStatus) chips.push({ label: "status", value: detail.httpStatus, kind: statusKind(detail.httpStatus) });
else if (detail.status) chips.push({ label: "status", value: detail.status, kind: statusKind(detail.status) });
if (detail.method) chips.push({ label: "method", value: detail.method });
if (detail.url) chips.push({ label: "url", value: detail.url, mono: true });
if (detail.repoId) chips.push({ label: "repo", value: detail.repoId, mono: true });
if (detail.code && detail.code !== detail.message && detail.code !== e.displayMessage) {
chips.push({ label: "code", value: detail.code });
}
} else {
e.displayMessage = e.message;
}
const tail = (e.raw || []).slice(1);
const detailJson = !tail.length
? ""
: tail.length === 1
? JSON.stringify(tail[0], null, 2)
: JSON.stringify(tail, null, 2);
e._chips = chips;
e._detailJson = detailJson;
return e;
}
function applyFilter() {
const q = ($scope.query.search || "").toLowerCase();
const mod = $scope.query.module || "";
$scope.filtered = $scope.entries.filter((e) => {
if (mod && e.module !== mod) return false;
if (!q) return true;
const hay = (
(e.displayMessage || e.message || "") +
" " +
e.module +
" " +
JSON.stringify(e.raw || [])
).toLowerCase();
return hay.indexOf(q) > -1;
});
}
function load() {
$http.get("/api/admin/errors").then(
(res) => {
$scope.entries = (res.data.entries || []).map(decorate);
$scope.available = !!res.data.available;
const set = new Set();
$scope.entries.forEach((e) => e.module && set.add(e.module));
$scope.modules = Array.from(set).sort();
applyFilter();
},
(err) => console.error(err)
);
}
$scope.refreshNow = load;
$scope.clearAll = () => {
if (!confirm("Clear all captured errors?")) return;
$http.delete("/api/admin/errors").then(load, (err) => console.error(err));
};
load();
const stop = $interval(() => {
if ($scope.query.autoRefresh) load();
}, 5000);
$scope.$on("$destroy", () => $interval.cancel(stop));
$scope.$watch("query.search", applyFilter);
$scope.$watch("query.module", applyFilter);
},
]);
+5
View File
@@ -137,6 +137,11 @@ angular
controller: "queuesAdminController",
title: "Admin · Queues Anonymous GitHub",
})
.when("/admin/errors", {
templateUrl: "/partials/admin/errors.htm",
controller: "errorsAdminController",
title: "Admin · Errors Anonymous GitHub",
})
.when("/404", {
templateUrl: "/partials/404.htm",
title: "Page not found Anonymous GitHub",
+1 -1
View File
File diff suppressed because one or more lines are too long