ente/web/packages/next/log-web.ts
Manav Rathi e0fe018f34
Log on unhandled errors and promise rejections
Test code:

    useEffect(() => {
        setTimeout(() => testFunctionSync(), 5000);
    }, []);

    useEffect(() => {
        testFunction();
    }, []);

    const testFunctionSync = () => {
        console.log("sleeping not");
        // sleep(2000);
        console.log("woke up not");
        throw new Error("Handle me");
    };

    const testFunction = async () => {
        console.log("sleeping");
        sleep(2000);
        console.log("woke up");
        throw new Error("Handle me");
    };

Refs:
- https://developer.mozilla.org/en-US/docs/Web/API/Window/unhandledrejection_event
- https://github.com/megahertz/electron-log/blob/master/src/renderer/lib/RendererErrorHandler.js
2024-04-10 13:05:00 +05:30

108 lines
3.4 KiB
TypeScript

import { isDevBuild } from "@/next/env";
import log from "@/next/log";
/**
* Log a standard startup banner.
*
* This helps us identify app starts and other environment details in the logs.
*
* @param appId An identifier of the app that is starting.
* @param userId The uid for the currently logged in user, if any.
*/
export const logStartupBanner = (appId: string, userId?: number) => {
// TODO (MR): Remove the need to lowercase it, change the enum itself.
const appIdL = appId.toLowerCase();
const sha = process.env.GIT_SHA;
const buildId = isDevBuild ? "dev " : sha ? `git ${sha} ` : "";
log.info(`Starting ente-${appIdL}-web ${buildId}uid ${userId ?? 0}`);
};
/**
* Attach handlers to log any unhandled exceptions and promise rejections.
*
* @param attach If true, attach handlers, and if false, remove them. This
* allows us to use this in a React hook that cleans up after itself.
*/
export const logUnhandledErrorsAndRejections = (attach: boolean) => {
const handleError = (event: ErrorEvent) => {
log.error("Unhandled error", event.error);
};
const handleUnhandledRejection = (event: PromiseRejectionEvent) => {
log.error("Unhandled promise rejection", event.reason);
};
if (attach) {
window.addEventListener("error", handleError);
window.addEventListener("unhandledrejection", handleUnhandledRejection);
} else {
window.removeEventListener("error", handleError);
window.removeEventListener(
"unhandledrejection",
handleUnhandledRejection,
);
}
};
interface LogEntry {
timestamp: number;
logLine: string;
}
const lsKey = "logs";
/**
* Record {@link message} in a persistent log storage.
*
* These strings, alongwith associated timestamps, get added to a small ring
* buffer, whose contents can be later be retrieved by using {@link savedLogs}.
*
* This ring buffer is persisted in the browser's local storage.
*/
export const logToDisk = (message: string) => {
const maxCount = 1000;
const log: LogEntry = { logLine: message, timestamp: Date.now() };
try {
const logs = logEntries();
if (logs.length > maxCount) {
logs.slice(logs.length - maxCount);
}
logs.push(log);
localStorage.setItem(lsKey, JSON.stringify({ logs }));
} catch (e) {
console.error("Failed to persist log", e);
if (e instanceof Error && e.name === "QuotaExceededError") {
localStorage.removeItem(lsKey);
}
}
};
const logEntries = (): unknown[] => {
const s = localStorage.getItem("logs");
if (!s) return [];
const o: unknown = JSON.parse(s);
if (!(o && typeof o == "object" && "logs" in o && Array.isArray(o.logs))) {
console.error("Unexpected log entries obtained from local storage", o);
return [];
}
return o.logs;
};
/**
* Return a string containing all recently saved log messages.
*
* @see {@link persistLog}.
*/
export const savedLogs = () => logEntries().map(formatEntry).join("\n");
const formatEntry = (e: unknown) => {
if (e && typeof e == "object" && "timestamp" in e && "logLine" in e) {
const timestamp = e.timestamp;
const logLine = e.logLine;
if (typeof timestamp == "number" && typeof logLine == "string") {
return `[${new Date(timestamp).toISOString()}] ${logLine}`;
}
}
return String(e);
};