fix(telemetry): classify environmental database errors as operational (#540)

Postgres auth (28xxx), permission (42501), resource (class 53), and operator-intervention (class 57) failures now classify as operational via a cause-chain walk, not bug. pg query bugs (e.g. 42601) stay bugs.
This commit is contained in:
SnapOtter
2026-07-16 19:26:46 +08:00
committed by GitHub
parent 39b89b9fbd
commit 631d82eaae
3 changed files with 52 additions and 0 deletions
+2
View File
@@ -15,6 +15,7 @@ import {
connectivityClass,
extractErrorCode,
isClientAbort,
isEnvironmentalDbError,
isSafeMessageError,
isToolInputError,
} from "@snapotter/shared";
@@ -59,6 +60,7 @@ export function classifyError(err: unknown, source?: ReportContext["source"]): E
}
if (isSafeMessageError(err)) return err.kind === "bug" ? "bug" : "operational";
if (connectivityClass(err)) return "operational";
if (isEnvironmentalDbError(err)) return "operational";
if (e?.code && OPERATIONAL_CODES.has(e.code)) return "operational";
return "bug";
}
@@ -146,6 +146,33 @@ export function connectivityClass(err: unknown): ConnectivityClass | null {
}
}
// pg SQLSTATE classes that mean the deployment's database is misconfigured or
// resource-starved (the operator's environment), not that our code is wrong:
// class 28 (invalid authorization), 53 (insufficient resources), 57 (operator
// intervention). 42501 is insufficient_privilege, the one access code in class
// 42 (otherwise our query bugs). Connection loss (class 08 / 57P0x) is already
// covered by connectivityClass; overlap here is harmless.
const ENVIRONMENTAL_PG_CLASS = /^(28|53|57)/;
/**
* True when the error chain carries a pg SQLSTATE indicating an environmental
* database problem (bad credentials, missing privilege, exhausted resources,
* operator shutdown) rather than a bug in our queries. Lets self-hosters' DB
* misconfiguration classify as operational instead of a code bug.
*/
export function isEnvironmentalDbError(err: unknown): boolean {
try {
return chain(err).some((l) => {
if (typeof l.code !== "string" || !SQLSTATE.test(l.code) || NODE_CODE.test(l.code)) {
return false;
}
return ENVIRONMENTAL_PG_CLASS.test(l.code) || l.code === "42501";
});
} catch {
return false;
}
}
/** Client went away mid-request; operational noise, never reported. */
export function isClientAbort(err: unknown): boolean {
try {
+23
View File
@@ -35,6 +35,29 @@ describe("classifyError", () => {
"operational",
);
});
it("operational: environmental database errors (auth, permission, resources), not query bugs", () => {
// The deployment's DB is misconfigured or starved -- the operator's
// environment, not our code. These flooded the bug view as pg auth /
// permission failures from background sweeps (NODE-1G/1F/1D).
expect(
classifyError(Object.assign(new Error("password authentication failed"), { code: "28P01" })),
).toBe("operational");
// drizzle wraps the pg error, so the SQLSTATE is on the cause, not the top level.
expect(
classifyError(
Object.assign(new Error("Failed query: DELETE FROM jobs"), {
cause: Object.assign(new Error("permission denied for relation jobs"), { code: "42501" }),
}),
),
).toBe("operational");
expect(
classifyError(Object.assign(new Error("no space left on device"), { code: "53100" })),
).toBe("operational");
// A pg SYNTAX error is our query bug, not the environment -- must stay a bug.
expect(
classifyError(Object.assign(new Error("syntax error at or near"), { code: "42601" })),
).toBe("bug");
});
it("bug: everything else, including bug-kind SafeError and ReplyError", () => {
expect(classifyError(new Error("undefined is not a function"))).toBe("bug");
expect(classifyError(new SafeError("Impossible state", { kind: "bug" }))).toBe("bug");