agmission/Development/server/helpers/fatal_error_reporter.js

136 lines
3.6 KiB
JavaScript

'use strict';
const fs = require('fs-extra');
const path = require('path');
const debug = require('debug')('agm:fatal');
function normalizeError(errLike) {
if (!errLike) {
return { code: undefined, message: 'Unknown error', stack: undefined };
}
if (errLike instanceof Error) {
return {
code: errLike.code,
message: String(errLike.message || errLike),
stack: errLike.stack,
};
}
if (typeof errLike === 'string') {
return { code: undefined, message: errLike, stack: undefined };
}
// Promise rejection reasons can be arbitrary
try {
const message = String(errLike.message || errLike.toString?.() || errLike);
return { code: errLike.code, message, stack: errLike.stack };
} catch {
return { code: undefined, message: 'Unknown error', stack: undefined };
}
}
async function readLastReportSafe(filePath) {
try {
const raw = await fs.readFile(filePath, 'utf8');
if (!raw) return null;
return JSON.parse(raw);
} catch (err) {
// Missing file is fine
if (err && (err.code === 'ENOENT' || err.code === 'ENOTDIR')) return null;
// Corrupt JSON (or other read error): archive it once so we can proceed.
try {
const dir = path.dirname(filePath);
const base = path.basename(filePath);
const archived = path.join(dir, `${base}.corrupt.${Date.now()}`);
await fs.move(filePath, archived, { overwrite: false });
debug('Archived corrupt fatal report:', archived);
} catch (archiveErr) {
debug('Failed to archive corrupt fatal report:', archiveErr && (archiveErr.message || archiveErr));
}
return null;
}
}
async function atomicWriteJson(filePath, obj) {
const dir = path.dirname(filePath);
await fs.ensureDir(dir);
const tmpPath = `${filePath}.tmp.${process.pid}.${Date.now()}`;
const payload = JSON.stringify(obj);
await fs.writeFile(tmpPath, payload, 'utf8');
// Atomic on same filesystem
await fs.rename(tmpPath, filePath);
}
function sameError(a, b) {
if (!a || !b) return false;
if (a.code && b.code && a.code === b.code) return true;
return a.message && b.message && a.message === b.message;
}
function parseWhen(when) {
if (!when) return null;
try {
const d = new Date(when);
if (Number.isNaN(d.getTime())) return null;
return d;
} catch {
return null;
}
}
async function reportFatal(opts) {
const {
filePath,
kind,
error,
message,
throttleMs = 2 * 60 * 1000,
emailEnabled = false,
emailTo,
mailer,
} = opts || {};
if (!filePath) return;
try {
const normalized = normalizeError(error);
const now = new Date();
const last = await readLastReportSafe(filePath);
const lastWhen = parseWhen(last && last.when);
const current = {
code: normalized.code,
message: normalized.message,
when: now.toISOString(),
kind: kind || 'fatal',
};
if (lastWhen && sameError(last, current)) {
const delta = now.getTime() - lastWhen.getTime();
if (delta >= 0 && delta < throttleMs) {
return;
}
}
await atomicWriteJson(filePath, current);
if (emailEnabled && mailer && typeof mailer.sendTextMail === 'function') {
const subject = `[Agm-Errors] ${current.kind}`;
const body = message || (normalized.stack ? `${normalized.message}\n${normalized.stack}` : normalized.message);
await mailer.sendTextMail({ subject, text: body }, emailTo);
}
} catch (err) {
// Never throw from a fatal reporter
debug('Fatal reporter failed:', err && (err.message || err));
}
}
module.exports = {
reportFatal,
};