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