fix: error-only Sentry telemetry, storm-proof capture, and crash fixes (#476)

Removes Sentry tracing entirely (BullMQ idle polling burned 4.8M transactions in 2 days at the baked 0.1 rate), decouples PostHog sampling, and replaces the type-only error scrub with a vetted-field sanitizer plus SafeError/ToolInputError contracts. One classified capture path with per-signature throttles and a per-process ceiling makes storms impossible (NODE-1E was 4,541 events from one 30s loop). Browser errors move to a dedicated web Sentry project with their own source maps. Adds the SNAPOTTER_TELEMETRY runtime kill switch and silences test fleets.

Crash fixes: remote 204/304 SSRF process kill (NODE-20), conversion-preset boot crash loop (NODE-21), Redis version preflight + unhandled subscribe rejection (NODE-1T), Sign PDF on plain-http origins (NODE-1K/1M), wavesurfer/pdf.js teardown rejections (NODE-1P/1N), bundle-import ZlibError to 400 (NODE-1Z), chart-maker input errors declassified (NODE-1H/1J), asset requests skip the session DB lookup (NODE-1D).
This commit is contained in:
SnapOtter
2026-07-10 21:41:49 +08:00
committed by GitHub
parent 3d1744aec8
commit ae6a4c8b7c
75 changed files with 2198 additions and 260 deletions
@@ -298,6 +298,23 @@ describe("importBundleArchive", () => {
/invalid model path/,
);
});
it("rejects a corrupt gzip stream with ImportValidationError, not a ZlibError", async () => {
// Gzip magic bytes followed by garbage: minizlib raises a ZlibError
// during tar parse (Sentry NODE-1Z shape).
const corruptPath = join(tmpdir(), `snapotter-corrupt-${randomUUID()}.tar.gz`);
writeFileSync(
corruptPath,
Buffer.concat([Buffer.from([0x1f, 0x8b, 0x08]), Buffer.alloc(64, 0x41)]),
);
await expect(importBundleArchive(createReadStream(corruptPath))).rejects.toThrow(
ImportValidationError,
);
await expect(importBundleArchive(createReadStream(corruptPath))).rejects.toThrow(
/not a valid bundle archive/i,
);
});
});
describe("site-packages import", () => {
@@ -441,6 +458,30 @@ describe("POST /api/v1/admin/features/import", () => {
expect(res.statusCode).toBeGreaterThanOrEqual(400);
});
it("rejects a non-archive upload with 400, not a crash", async () => {
const { body, contentType } = createMultipartPayload([
{
name: "file",
filename: "corrupt.tar.gz",
contentType: "application/gzip",
content: Buffer.from("this is not gzip data"),
},
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/admin/features/import",
headers: {
"content-type": contentType,
authorization: `Bearer ${token}`,
},
payload: body,
});
expect(res.statusCode).toBe(400);
expect(JSON.parse(res.body).error).toMatch(/not a valid bundle archive/i);
});
it("returns 409 when lock is held", async () => {
const locked = acquireInstallLock("blocking-bundle");
expect(locked).toBe(true);