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
+25 -12
View File
@@ -3,6 +3,7 @@ import { randomUUID } from "node:crypto";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { context, propagation, SpanStatusCode, trace } from "@opentelemetry/api";
import { SafeError } from "@snapotter/shared";
import { missingBundleForScript } from "./feature-gate.js";
import { acquireVenvRead, tryAcquireVenvRead } from "./venv-lock.js";
@@ -327,17 +328,24 @@ export class PythonDispatcher {
if (req) {
this.pending.delete(reqId);
if (response.exitCode !== 0) {
const errText =
extractPythonError({
stdout: response.stdout,
stderr: req.stderrLines.join("\n"),
}) ||
(response.exitCode === 137
? "Process killed (out of memory) -- try a lighter model or smaller image"
: response.exitCode === 139
? "Process crashed (segmentation fault)"
: `Python script exited with code ${response.exitCode}`);
req.reject(new Error(errText));
const extracted = extractPythonError({
stdout: response.stdout,
stderr: req.stderrLines.join("\n"),
});
if (!extracted && (response.exitCode === 137 || response.exitCode === 139)) {
req.reject(
new SafeError(
response.exitCode === 137
? "Process killed (out of memory) -- try a lighter model or smaller image"
: "Process crashed (segmentation fault)",
{ kind: "operational", code: `exit-${response.exitCode}` },
),
);
} else {
req.reject(
new Error(extracted || `Python script exited with code ${response.exitCode}`),
);
}
} else {
req.resolve({
stdout: response.stdout || "",
@@ -564,8 +572,13 @@ export class PythonDispatcher {
: signal === "SIGSEGV" || code === 139
? "Process crashed (segmentation fault)"
: null;
if (signalMsg) {
rejectPromise(
new SafeError(signalMsg, { kind: "operational", code: `exit-${code ?? signal}` }),
);
return;
}
const errorText =
signalMsg ||
extractPythonError({ stdout: stdout.trim(), stderr }) ||
`Python script exited with code ${code}`;
rejectPromise(new Error(errorText));