mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
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:
@@ -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 {
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user