feat(telemetry): readable Sentry errors, Python tracebacks, and diagnostic mode

Keeps a real, redacted error message instead of "Error: Error", surfaces Python tracebacks in Sentry as a vetted context, and adds an opt-in SNAPOTTER_SENTRY_DIAGNOSTIC verbose mode plus SNAPOTTER_SENTRY_DSN_OVERRIDE. The default fleet path ships nothing on the never-collect list; raw detail is reachable only via the opt-in flag. Also classifies Redis OOM/READONLY replies as operational and removes a ReDoS in stack-frame extraction.
This commit is contained in:
SnapOtter
2026-08-03 13:13:38 +08:00
committed by GitHub
parent 865390ce66
commit 04ef1141fb
22 changed files with 593 additions and 178 deletions
+1
View File
@@ -39,6 +39,7 @@ vi.mock("@sentry/node", () => ({
vi.mock("../../../apps/api/src/lib/analytics-gate.js", () => ({
analyticsEnabled: () => true,
sentryDiagnostic: () => false,
}));
beforeEach(() => {
+11
View File
@@ -113,6 +113,17 @@ describe("classifyError", () => {
});
});
describe("classifyError redis", () => {
it("classifies a Redis OOM ReplyError as operational", () => {
const err = Object.assign(new Error("OOM command not allowed"), { name: "ReplyError" });
expect(classifyError(err, "worker")).toBe("operational");
});
it("classifies a Redis READONLY ReplyError as operational", () => {
const err = Object.assign(new Error("READONLY You can't write"), { name: "ReplyError" });
expect(classifyError(err, "worker")).toBe("operational");
});
});
describe("throttle", () => {
beforeEach(() => resetThrottleForTests());
it("operational: 1 per signature per hour; bug: 10", () => {
+33 -2
View File
@@ -40,6 +40,18 @@ describe("buildBeforeSend (api)", () => {
it("returns null when the gate is off", () => {
expect(buildBeforeSend(() => false)(evt(), {})).toBeNull();
});
it("keeps the raw message and request when diagnostic is on", () => {
const diag = buildBeforeSend(() => true, true);
const event = {
exception: { values: [{ type: "Error", value: "open /data/uploads/a/report.pdf" }] },
request: { method: "POST", url: "https://host/api/v1/tools/image/rounded-crop" },
};
const out = diag(event as never, {
originalException: new Error("open /data/uploads/a/report.pdf"),
}) as never as { exception: { values: Array<{ value: string }> }; request?: unknown };
expect(out.exception.values[0].value).toBe("open /data/uploads/a/report.pdf");
expect(out.request).toBeDefined();
});
it("strips high-risk surfaces but keeps full stack paths for debugging", () => {
const hint = {
originalException: Object.assign(new Error("x"), { code: "EACCES", syscall: "mkdir" }),
@@ -78,9 +90,9 @@ describe("buildBeforeSend (api)", () => {
{ message: "reading <path>", category: "console", level: "info" },
]);
});
it("falls back to type-only for unknown errors", () => {
it("keeps a redacted message for unknown errors", () => {
const out = send(evt(), { originalException: new Error("user path /tmp/z") })!;
expect(out.exception.values[0].value).toBe("Error");
expect(out.exception.values[0].value).toBe("user path <path>");
});
it("applies the rebuilt value to the last (original) exception entry only", () => {
const event = evt({
@@ -125,6 +137,25 @@ describe("buildBeforeSend (api)", () => {
const out = send(evt({ contexts: { device: { hostname: "leak" } } }), {})!;
expect(out.contexts).toBeUndefined();
});
it("keeps a vetted python context and drops overlong fields", () => {
const event = {
...evt(),
contexts: {
python: {
type: "RuntimeError",
frames: [
{ file: "remove_bg.py", line: 88, func: "run" },
{ file: "x".repeat(200), line: 1, func: "y".repeat(200) },
],
},
},
};
const out = send(event, { originalException: new Error("x") })!;
expect(out.contexts.python.type).toBe("RuntimeError");
expect(out.contexts.python.frames).toHaveLength(2);
expect(out.contexts.python.frames[1].file.length).toBeLessThanOrEqual(64);
expect(out.contexts.python.frames[1].func.length).toBeLessThanOrEqual(64);
});
it("enforces the 500-events-per-hour ceiling", () => {
for (let i = 0; i < 500; i++) expect(send(evt(), {})).not.toBeNull();
expect(send(evt(), {})).toBeNull();