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
+22 -1
View File
@@ -1,4 +1,5 @@
import { spawn } from "node:child_process";
import { markToolInputError } from "@snapotter/shared";
import { resolveFfmpeg } from "./binaries.js";
import { type FfmpegProgress, parseProgressBlock } from "./progress.js";
@@ -10,6 +11,23 @@ export interface RunFfmpegOptions {
const STDERR_RING_MAX = 16 * 1024;
// stderr shapes that mean "the input media is unusable", not an ffmpeg bug.
// Marked errors are classified expected upstream and never reach Sentry.
const INPUT_ERROR_PATTERNS = [
/received no packets/i,
/invalid data found when processing input/i,
/could not find codec parameters/i,
/moov atom not found/i,
];
/**
* Applies the isToolInputError marker (see @snapotter/shared tool-errors.ts)
* when the captured stderr matches a known input-failure shape.
*/
export function markIfInputError<E extends Error>(err: E, stderr: string): E {
return INPUT_ERROR_PATTERNS.some((re) => re.test(stderr)) ? markToolInputError(err) : err;
}
/**
* Runs ffmpeg with `-progress pipe:1` appended, parsing progress blocks from
* stdout. Rejects with the tail of stderr on non-zero exit, timeout or abort.
@@ -76,7 +94,10 @@ export async function runFfmpeg(args: string[], opts: RunFfmpegOptions = {}): Pr
settled = true;
cleanup();
if (code === 0) resolvePromise(stderrTail);
else reject(new Error(`ffmpeg exited ${code ?? signal}: ${stderrTail.slice(-2000)}`));
else {
const err = new Error(`ffmpeg exited ${code ?? signal}: ${stderrTail.slice(-2000)}`);
reject(markIfInputError(err, stderrTail));
}
});
});
}
+5 -1
View File
@@ -1,5 +1,6 @@
import { spawn } from "node:child_process";
import { resolveFfprobe } from "./binaries.js";
import { markIfInputError } from "./ffmpeg.js";
export interface MediaStreamInfo {
type: "video" | "audio" | "other";
@@ -68,7 +69,10 @@ export async function probeMedia(filePath: string, opts: ProbeOptions = {}): Pro
settled = true;
clearTimeout(timer);
if (code === 0) resolvePromise(out);
else reject(new Error(`ffprobe exited ${code ?? signal}: ${err.slice(-1000)}`));
else {
const probeErr = new Error(`ffprobe exited ${code ?? signal}: ${err.slice(-1000)}`);
reject(markIfInputError(probeErr, err));
}
});
});
const parsed = JSON.parse(stdout) as {