mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 13:52:20 +00:00
The two-o'clock status crons generated for every account, ignoring both the per-user coach opt-out and the operator kill switch; they now share one gated discovery that also skips users the pregenerate pass will rewarm anyway, and the mood card finally gets the nightly run the other six always had. Deleted measurements no longer feed any prompt. Locales without an assistant language fall back to English consistently instead of German texts with English fallbacks. A rate-limited regenerate stops claiming success, the dashboard briefing state distinguishes a missing provider from a pending generation and serves a stale briefing with its timestamp instead of hiding it, narrative warming actually processes oldest first, and worker failures reach the error tracker through one shared reporter while the insight queues gain real retries.
77 lines
2.8 KiB
TypeScript
77 lines
2.8 KiB
TypeScript
/**
|
|
* Central error reporter for pg-boss worker handlers.
|
|
*
|
|
* Worker errors used to be visible only as a stderr line (`workerLog`)
|
|
* — they never reached GlitchTip, so a nightly pass that failed for
|
|
* weeks was invisible unless the operator happened to read container
|
|
* logs. This helper gives every worker catch the same two sinks the
|
|
* HTTP layer has:
|
|
*
|
|
* 1. stderr (the operator's container log), and
|
|
* 2. GlitchTip, through the same `sendGlitchtipEvent` payload shape
|
|
* `api-handler.ts` uses, with the same redaction
|
|
* (`redactSecrets` on the message, `redactOptional` on the stack)
|
|
* so a Telegram bot token or Bearer credential embedded in an
|
|
* error message cannot leak into the incident UI.
|
|
*
|
|
* Fire-and-forget by contract: the reporter NEVER throws, so a sink
|
|
* failure (GlitchTip down, settings read failing) cannot mask or
|
|
* replace the original handler error.
|
|
*/
|
|
import { redactOptional, redactSecrets } from "@/lib/logging/redact";
|
|
|
|
/**
|
|
* Report one worker error. `queue` is the pg-boss queue (or logical
|
|
* pass) name; `meta` is a small pinned-shape key/value bag folded into
|
|
* the message tail (GlitchTip's event payload has no free-form extras
|
|
* field) — keep values to identifiers and counts, never payload data.
|
|
*/
|
|
export async function reportWorkerError(
|
|
queue: string,
|
|
error: unknown,
|
|
meta: Record<string, string | number | boolean> = {},
|
|
): Promise<void> {
|
|
const err =
|
|
error instanceof Error
|
|
? error
|
|
: new Error(typeof error === "string" ? error : "Unknown worker error");
|
|
|
|
const metaEntries = Object.entries(meta);
|
|
const metaSuffix =
|
|
metaEntries.length > 0
|
|
? ` (${metaEntries.map(([k, v]) => `${k}=${v}`).join(" ")})`
|
|
: "";
|
|
const message = redactSecrets(`[${queue}] ${err.message}${metaSuffix}`);
|
|
|
|
// Sink 1 — stderr, mirroring `workerLog("error", …)` so the operator
|
|
// keeps the container-log trail even when GlitchTip is disabled.
|
|
console.error(`[pg-boss] ${message}`, err);
|
|
|
|
// Sink 2 — GlitchTip, fire-and-forget. Dynamic imports mirror the
|
|
// api-handler forwarder (no startup cost, no import cycle).
|
|
try {
|
|
const [{ getGlitchtipSettings }, { sendGlitchtipEvent }] =
|
|
await Promise.all([
|
|
import("@/lib/monitoring-settings"),
|
|
import("@/lib/monitoring/glitchtip"),
|
|
]);
|
|
const settings = await getGlitchtipSettings();
|
|
if (!settings.glitchtipEnabled || !settings.glitchtipDsn) return;
|
|
|
|
await sendGlitchtipEvent({
|
|
dsn: settings.glitchtipDsn,
|
|
input: {
|
|
environment: settings.glitchtipEnvironment || "production",
|
|
message,
|
|
level: "error",
|
|
type: err.name || "Error",
|
|
stack: redactOptional(err.stack),
|
|
sourceTag: "healthlog-worker",
|
|
},
|
|
});
|
|
} catch {
|
|
// The reporter must never throw — a sink failure would otherwise
|
|
// mask the original handler error.
|
|
}
|
|
}
|