mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
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:
@@ -108,7 +108,12 @@ const OOM_EXIT_TEXT =
|
||||
* memory-aware callers still retry on a lighter model. Any other non-zero exit
|
||||
* is a bug, with the extracted reason (or an OOM reported in-band) preserved.
|
||||
*/
|
||||
function pythonExitError(code: number | null, signal: string | null, extracted: string): SafeError {
|
||||
function pythonExitError(
|
||||
code: number | null,
|
||||
signal: string | null,
|
||||
extracted: string,
|
||||
info?: PythonErrorInfo | null,
|
||||
): SafeError {
|
||||
if (signal === "SIGSEGV" || code === 139) {
|
||||
return new SafeError("Process crashed (segmentation fault)", {
|
||||
kind: "operational",
|
||||
@@ -122,10 +127,14 @@ function pythonExitError(code: number | null, signal: string | null, extracted:
|
||||
});
|
||||
}
|
||||
const message = extracted || `Python script exited with code ${code}`;
|
||||
return new SafeError(message, {
|
||||
const err = new SafeError(message, {
|
||||
kind: OOM_EXIT_TEXT.test(message) ? "operational" : "bug",
|
||||
code: `exit-${code ?? "unknown"}`,
|
||||
});
|
||||
if (info) {
|
||||
Object.assign(err, { pythonType: info.type, pythonFrames: info.frames });
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -186,6 +195,41 @@ function extractPythonError(error: unknown): string {
|
||||
return String(error);
|
||||
}
|
||||
|
||||
export interface PythonErrorInfo {
|
||||
type?: string;
|
||||
frames: Array<{ file: string; line: number; func: string }>;
|
||||
}
|
||||
|
||||
/** Read the structured {type, frames} envelope from a sidecar failure, or null. */
|
||||
export function extractPythonErrorInfo(error: unknown): PythonErrorInfo | null {
|
||||
if (!error || typeof error !== "object") return null;
|
||||
const pErr = error as { stdout?: string; stderr?: string };
|
||||
for (const output of [pErr.stdout, pErr.stderr]) {
|
||||
if (!output) continue;
|
||||
try {
|
||||
const parsed = JSON.parse(output.trim()) as { errorInfo?: unknown };
|
||||
const info = parsed.errorInfo as PythonErrorInfo | undefined;
|
||||
if (info && Array.isArray(info.frames)) {
|
||||
return {
|
||||
type: typeof info.type === "string" ? info.type : undefined,
|
||||
frames: info.frames
|
||||
.filter(
|
||||
(f): f is { file: string; line: number; func: string } =>
|
||||
!!f &&
|
||||
typeof (f as { file?: unknown }).file === "string" &&
|
||||
typeof (f as { line?: unknown }).line === "number" &&
|
||||
typeof (f as { func?: unknown }).func === "string",
|
||||
)
|
||||
.slice(0, 20),
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// not JSON; nothing structured to read
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export type ProgressCallback = (percent: number, stage: string) => void;
|
||||
|
||||
export interface PythonRunOptions {
|
||||
@@ -420,11 +464,11 @@ export class PythonDispatcher {
|
||||
if (req) {
|
||||
this.pending.delete(reqId);
|
||||
if (response.exitCode !== 0) {
|
||||
const extracted = extractPythonError({
|
||||
stdout: response.stdout,
|
||||
stderr: req.stderrLines.join("\n"),
|
||||
});
|
||||
req.reject(pythonExitError(response.exitCode, null, extracted));
|
||||
const raw = { stdout: response.stdout, stderr: req.stderrLines.join("\n") };
|
||||
const extracted = extractPythonError(raw);
|
||||
req.reject(
|
||||
pythonExitError(response.exitCode, null, extracted, extractPythonErrorInfo(raw)),
|
||||
);
|
||||
} else {
|
||||
req.resolve({
|
||||
stdout: response.stdout || "",
|
||||
@@ -737,8 +781,9 @@ export class PythonDispatcher {
|
||||
const stderr = stderrLines.join("\n");
|
||||
|
||||
if (code !== 0) {
|
||||
const raw = { stdout: stdout.trim(), stderr };
|
||||
rejectOnce(
|
||||
pythonExitError(code, signal, extractPythonError({ stdout: stdout.trim(), stderr })),
|
||||
pythonExitError(code, signal, extractPythonError(raw), extractPythonErrorInfo(raw)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user