mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-07-06 05:38:09 +02:00
Improve error handling
This commit is contained in:
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -5068,6 +5068,13 @@ body {
|
|||||||
max-height: 22em; overflow: auto;
|
max-height: 22em; overflow: auto;
|
||||||
box-shadow: var(--card-shadow);
|
box-shadow: var(--card-shadow);
|
||||||
}
|
}
|
||||||
|
.errors-page .stack-pre {
|
||||||
|
/* Stack frames look better with a tighter line-height than the curated
|
||||||
|
JSON view, since each frame is a single readable line. */
|
||||||
|
line-height: 1.45;
|
||||||
|
white-space: pre;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
.errors-page .related-row {
|
.errors-page .related-row {
|
||||||
display: flex; gap: 10px; padding: 6px 12px; align-items: center;
|
display: flex; gap: 10px; padding: 6px 12px; align-items: center;
|
||||||
border-bottom: 1px solid var(--border-soft);
|
border-bottom: 1px solid var(--border-soft);
|
||||||
|
|||||||
@@ -148,11 +148,13 @@
|
|||||||
<div class="errors-row-detail" ng-if="expanded[row._key]">
|
<div class="errors-row-detail" ng-if="expanded[row._key]">
|
||||||
<div class="detail-tabs">
|
<div class="detail-tabs">
|
||||||
<button type="button" ng-class="{active: detailTab[row._key] === 'raw' || !detailTab[row._key]}" ng-click="detailTab[row._key] = 'raw'">Raw</button>
|
<button type="button" ng-class="{active: detailTab[row._key] === 'raw' || !detailTab[row._key]}" ng-click="detailTab[row._key] = 'raw'">Raw</button>
|
||||||
|
<button type="button" ng-class="{active: detailTab[row._key] === 'stack'}" ng-click="detailTab[row._key] = 'stack'" ng-if="row._stack">Stack</button>
|
||||||
<button type="button" ng-class="{active: detailTab[row._key] === 'related'}" ng-click="detailTab[row._key] = 'related'" ng-if="row.count > 1">Related ({{row.count}})</button>
|
<button type="button" ng-class="{active: detailTab[row._key] === 'related'}" ng-click="detailTab[row._key] = 'related'" ng-if="row.count > 1">Related ({{row.count}})</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-body">
|
<div class="detail-body">
|
||||||
<div class="detail-main">
|
<div class="detail-main">
|
||||||
<pre ng-if="(detailTab[row._key] || 'raw') === 'raw'">{{row._detailJson}}</pre>
|
<pre ng-if="(detailTab[row._key] || 'raw') === 'raw'">{{row._detailJson}}</pre>
|
||||||
|
<pre ng-if="detailTab[row._key] === 'stack'" class="stack-pre">{{row._stack}}</pre>
|
||||||
<div ng-if="detailTab[row._key] === 'related'" class="related-list">
|
<div ng-if="detailTab[row._key] === 'related'" class="related-list">
|
||||||
<div class="related-row" ng-repeat="r in row._related track by $index">
|
<div class="related-row" ng-repeat="r in row._related track by $index">
|
||||||
<span class="when-abs">{{absTimeShort(r.ts)}}</span>
|
<span class="when-abs">{{absTimeShort(r.ts)}}</span>
|
||||||
|
|||||||
+28
-1
@@ -943,6 +943,16 @@ angular
|
|||||||
} else if (detail.code && errorKeyRe.test(String(detail.code))) {
|
} else if (detail.code && errorKeyRe.test(String(detail.code))) {
|
||||||
e.displayMessage = String(detail.code);
|
e.displayMessage = String(detail.code);
|
||||||
e.displayContext = e.message;
|
e.displayContext = e.message;
|
||||||
|
} else if (
|
||||||
|
detail.name &&
|
||||||
|
detail.name !== "AnonymousError" &&
|
||||||
|
detail.name !== "Error"
|
||||||
|
) {
|
||||||
|
// Plain JS errors (SyntaxError, TypeError, RangeError, ...) — use
|
||||||
|
// the class name as the visible code; the original message is
|
||||||
|
// shown as italic context.
|
||||||
|
e.displayMessage = detail.name;
|
||||||
|
e.displayContext = detail.message || e.message;
|
||||||
} else {
|
} else {
|
||||||
e.displayMessage = e.message;
|
e.displayMessage = e.message;
|
||||||
}
|
}
|
||||||
@@ -951,10 +961,20 @@ angular
|
|||||||
e._method = detail.method || null;
|
e._method = detail.method || null;
|
||||||
e._repoId = detail.repoId || detail.detail || null;
|
e._repoId = detail.repoId || detail.detail || null;
|
||||||
e._detail = detail.detail && detail.detail !== e._repoId ? detail.detail : null;
|
e._detail = detail.detail && detail.detail !== e._repoId ? detail.detail : null;
|
||||||
|
// Walk into `cause` to surface the deepest stack — for unhandled
|
||||||
|
// errors the inner cause is usually the actual JS error frame.
|
||||||
|
let s = typeof detail.stack === "string" ? detail.stack : null;
|
||||||
|
let c = detail.cause;
|
||||||
|
while (!s && c && typeof c === "object") {
|
||||||
|
if (typeof c.stack === "string") s = c.stack;
|
||||||
|
c = c.cause;
|
||||||
|
}
|
||||||
|
e._stack = s;
|
||||||
} else {
|
} else {
|
||||||
e.displayMessage = e.message;
|
e.displayMessage = e.message;
|
||||||
e._status = null;
|
e._status = null;
|
||||||
e._url = null;
|
e._url = null;
|
||||||
|
e._stack = null;
|
||||||
}
|
}
|
||||||
e._bucket = bucketFor(detail, e.level);
|
e._bucket = bucketFor(detail, e.level);
|
||||||
e._detailJson = renderDisplayPayload(e, detail);
|
e._detailJson = renderDisplayPayload(e, detail);
|
||||||
@@ -1138,9 +1158,16 @@ angular
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadEntries(append) {
|
function loadEntries(append) {
|
||||||
|
// On auto-refresh after the user has paginated ("Load older"),
|
||||||
|
// request the SAME-sized window from the head so we don't blow away
|
||||||
|
// their loaded tail. Newer entries take the top, the oldest visible
|
||||||
|
// ones drop off naturally as the redis list rotates.
|
||||||
const offset = append ? $scope.entries.length : 0;
|
const offset = append ? $scope.entries.length : 0;
|
||||||
|
const limit = append
|
||||||
|
? $scope.pageSize
|
||||||
|
: Math.max($scope.pageSize, $scope.entries.length || $scope.pageSize);
|
||||||
$http
|
$http
|
||||||
.get("/api/admin/errors", { params: { offset, limit: $scope.pageSize } })
|
.get("/api/admin/errors", { params: { offset, limit } })
|
||||||
.then(
|
.then(
|
||||||
(res) => {
|
(res) => {
|
||||||
const next = (res.data.entries || []).map(decorate);
|
const next = (res.data.entries || []).map(decorate);
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -12,6 +12,7 @@ export default class AnonymousError extends CustomError {
|
|||||||
value?: unknown;
|
value?: unknown;
|
||||||
httpStatus?: number;
|
httpStatus?: number;
|
||||||
cause?: Error;
|
cause?: Error;
|
||||||
|
private explicitUrl?: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
message: string,
|
message: string,
|
||||||
@@ -19,15 +20,18 @@ export default class AnonymousError extends CustomError {
|
|||||||
httpStatus?: number;
|
httpStatus?: number;
|
||||||
cause?: Error;
|
cause?: Error;
|
||||||
object?: unknown;
|
object?: unknown;
|
||||||
|
url?: string;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
super(message);
|
super(message);
|
||||||
this.value = opt?.object;
|
this.value = opt?.object;
|
||||||
this.httpStatus = opt?.httpStatus;
|
this.httpStatus = opt?.httpStatus;
|
||||||
this.cause = opt?.cause;
|
this.cause = opt?.cause;
|
||||||
|
this.explicitUrl = opt?.url;
|
||||||
}
|
}
|
||||||
|
|
||||||
url(): string | undefined {
|
url(): string | undefined {
|
||||||
|
if (this.explicitUrl) return this.explicitUrl;
|
||||||
if (this.value == null) return undefined;
|
if (this.value == null) return undefined;
|
||||||
try {
|
try {
|
||||||
if (this.value instanceof AnonymizedFile) {
|
if (this.value instanceof AnonymizedFile) {
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ export default class S3Storage extends StorageBase {
|
|||||||
if (!res) {
|
if (!res) {
|
||||||
throw new AnonymousError("file_not_found", {
|
throw new AnonymousError("file_not_found", {
|
||||||
httpStatus: 404,
|
httpStatus: 404,
|
||||||
object: join(this.repoPath(repoId), path),
|
url: join(this.repoPath(repoId), path),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return res as Readable;
|
return res as Readable;
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ router.get(
|
|||||||
.on("error", () => {
|
.on("error", () => {
|
||||||
handleError(
|
handleError(
|
||||||
new AnonymousError("file_not_found", {
|
new AnonymousError("file_not_found", {
|
||||||
object: req.params.repoId,
|
url: req.originalUrl,
|
||||||
httpStatus: 404,
|
httpStatus: 404,
|
||||||
}),
|
}),
|
||||||
res
|
res
|
||||||
|
|||||||
@@ -122,22 +122,42 @@ function printError(error: any, req?: express.Request) {
|
|||||||
...serializeError(error),
|
...serializeError(error),
|
||||||
url: req?.originalUrl,
|
url: req?.originalUrl,
|
||||||
};
|
};
|
||||||
|
// Use the error's snake_case message as the logger summary so the admin
|
||||||
|
// Errors page surfaces something meaningful (e.g. "repoId_already_used")
|
||||||
|
// instead of a generic "anonymous error" wrapper.
|
||||||
|
const summary = error.message || error.name || "AnonymousError";
|
||||||
// 4xx are expected client errors (not_found, expired, not_connected) —
|
// 4xx are expected client errors (not_found, expired, not_connected) —
|
||||||
// route them to warn so the admin Errors page can split server faults
|
// route them to warn so the admin Errors page can split server faults
|
||||||
// (5xx) from client misuse (4xx) cleanly.
|
// (5xx) from client misuse (4xx) cleanly.
|
||||||
const status = error.httpStatus;
|
const status = error.httpStatus;
|
||||||
if (typeof status === "number" && status >= 400 && status < 500) {
|
if (typeof status === "number" && status >= 400 && status < 500) {
|
||||||
logger.warn("anonymous error", payload);
|
logger.warn(summary, payload);
|
||||||
} else {
|
} else {
|
||||||
logger.error("anonymous error", payload);
|
logger.error(summary, payload);
|
||||||
}
|
}
|
||||||
} else if (error instanceof HTTPError) {
|
} else if (error instanceof HTTPError) {
|
||||||
logger.error("http error", {
|
logger.error(error.code || error.name || "HTTPError", {
|
||||||
code: error.code,
|
...serializeError(error),
|
||||||
message: error.message,
|
url: req?.originalUrl,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
logger.error("unhandled error", serializeError(error));
|
// Unhandled errors: use the error class name (SyntaxError, TypeError,
|
||||||
|
// RangeError, ...) as the summary so the admin page shows
|
||||||
|
// something far more useful than a generic "unhandled error" label.
|
||||||
|
const serialized = serializeError(error) as Record<string, unknown>;
|
||||||
|
if (
|
||||||
|
typeof serialized.status !== "number" &&
|
||||||
|
typeof serialized.httpStatus !== "number"
|
||||||
|
) {
|
||||||
|
serialized.httpStatus = 500;
|
||||||
|
}
|
||||||
|
if (req?.originalUrl) serialized.url = req.originalUrl;
|
||||||
|
const summary =
|
||||||
|
(error && typeof error === "object" &&
|
||||||
|
((error as { name?: string }).name ||
|
||||||
|
(error as { message?: string }).message)) ||
|
||||||
|
"UnhandledError";
|
||||||
|
logger.error(summary, serialized);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ app.all("*", (req, res) => {
|
|||||||
handleError(
|
handleError(
|
||||||
new AnonymousError("file_not_found", {
|
new AnonymousError("file_not_found", {
|
||||||
httpStatus: 404,
|
httpStatus: 404,
|
||||||
object: req.originalUrl,
|
url: req.originalUrl,
|
||||||
}),
|
}),
|
||||||
res,
|
res,
|
||||||
req
|
req
|
||||||
|
|||||||
Reference in New Issue
Block a user