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,
|
connectivityClass,
|
||||||
extractErrorCode,
|
extractErrorCode,
|
||||||
isClientAbort,
|
isClientAbort,
|
||||||
|
isEnvironmentalDbError,
|
||||||
isSafeMessageError,
|
isSafeMessageError,
|
||||||
isToolInputError,
|
isToolInputError,
|
||||||
} from "@snapotter/shared";
|
} 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 (isSafeMessageError(err)) return err.kind === "bug" ? "bug" : "operational";
|
||||||
if (connectivityClass(err)) return "operational";
|
if (connectivityClass(err)) return "operational";
|
||||||
|
if (isEnvironmentalDbError(err)) return "operational";
|
||||||
if (e?.code && OPERATIONAL_CODES.has(e.code)) return "operational";
|
if (e?.code && OPERATIONAL_CODES.has(e.code)) return "operational";
|
||||||
return "bug";
|
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. */
|
/** Client went away mid-request; operational noise, never reported. */
|
||||||
export function isClientAbort(err: unknown): boolean {
|
export function isClientAbort(err: unknown): boolean {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -35,6 +35,29 @@ describe("classifyError", () => {
|
|||||||
"operational",
|
"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", () => {
|
it("bug: everything else, including bug-kind SafeError and ReplyError", () => {
|
||||||
expect(classifyError(new Error("undefined is not a function"))).toBe("bug");
|
expect(classifyError(new Error("undefined is not a function"))).toBe("bug");
|
||||||
expect(classifyError(new SafeError("Impossible state", { kind: "bug" }))).toBe("bug");
|
expect(classifyError(new SafeError("Impossible state", { kind: "bug" }))).toBe("bug");
|
||||||
|
|||||||
Reference in New Issue
Block a user