forward ui browser logs to go

This commit is contained in:
Eduard Gert
2026-06-09 10:55:56 +02:00
parent 727e6d3004
commit aaa5dbb606
4 changed files with 124 additions and 0 deletions

View File

@@ -15,6 +15,11 @@ import { welcome } from "@/lib/welcome";
import LoginWaitingForBrowserDialog from "@/modules/login/LoginWaitingForBrowserDialog.tsx";
import { initI18n } from "@/lib/i18n";
import { initPlatform } from "@/lib/platform";
import { initLogForwarding } from "@/lib/logs";
// Install console.* + uncaught-error forwarding before anything else runs
// so even init-time logs reach the Go log pipeline.
initLogForwarding();
welcome();

View File

@@ -0,0 +1,80 @@
import { UILog } from "@bindings/services";
// Forwards browser console output and uncaught errors into the Go logrus
// pipeline. Originals still fire, so DevTools is unchanged; the Go
// --log-level does the gating.
type Level = "trace" | "debug" | "info" | "warn" | "error";
const METHOD_LEVELS: Record<string, Level> = {
trace: "trace",
debug: "debug",
log: "info",
info: "info",
warn: "warn",
error: "error",
};
// Sources whose output is noise and shouldn't be forwarded.
const IGNORED_SOURCES = new Set(["welcome.ts"]);
let installed = false;
function format(args: unknown[]): string {
return args
.map((a) => {
if (typeof a === "string") return a;
if (a instanceof Error) return a.stack || a.message;
try {
return JSON.stringify(a);
} catch {
return String(a);
}
})
.join(" ");
}
// First stack frame outside this module as "<file>:<line>" (best-effort;
// minified prod stacks degrade to chunk names).
function callerSource(): string {
const stack = new Error().stack;
if (!stack) return "";
for (const line of stack.split("\n").slice(1)) {
if (line.includes("/logs.ts")) continue;
const m = line.match(/([^/\\() ]+\.[a-z]+):(\d+):\d+/i);
if (m) return `${m[1]}:${m[2]}`;
}
return "";
}
function forward(level: Level, args: unknown[]) {
try {
const source = callerSource();
if (IGNORED_SOURCES.has(source.split(":")[0])) return;
// Fire-and-forget; don't touch console here (would recurse).
void UILog.Log(level, source, format(args));
} catch {
// swallow
}
}
export function initLogForwarding() {
if (installed) return;
installed = true;
const c = console as unknown as Record<string, (...a: unknown[]) => void>;
for (const [method, level] of Object.entries(METHOD_LEVELS)) {
const original = c[method]?.bind(console);
c[method] = (...args: unknown[]) => {
original?.(...args);
forward(level, args);
};
}
window.addEventListener("error", (e) => {
forward("error", [`uncaught error: ${e.message}`, e.error ?? ""]);
});
window.addEventListener("unhandledrejection", (e) => {
forward("error", ["unhandled promise rejection:", e.reason]);
});
}

View File

@@ -316,6 +316,7 @@ func registerServices(app *application.App, conn *Conn, s registeredServices) {
app.RegisterService(application.NewService(services.NewPreferences(s.prefStore)))
app.RegisterService(application.NewService(services.NewAutostart(app.Autostart)))
app.RegisterService(application.NewService(services.NewVersion()))
app.RegisterService(application.NewService(services.NewUILog()))
}
// newMainWindow creates the hidden main window, sized to the user's last view

View File

@@ -0,0 +1,38 @@
//go:build !android && !ios && !freebsd && !js
package services
import (
"context"
"github.com/sirupsen/logrus"
)
// UILog lets the frontend forward console output into the Go logrus
// pipeline. The JS origin rides in the message ("[ui <file:line>] ...")
// because logrus's ReportCaller always pins the source to this file.
type UILog struct{}
func NewUILog() *UILog { return &UILog{} }
// Log forwards one frontend console entry. level is trace/debug/info/warn/
// error (anything else → info); source is the JS origin (may be empty).
func (s *UILog) Log(_ context.Context, level, source, msg string) {
if source != "" {
msg = "[ui " + source + "] " + msg
} else {
msg = "[ui] " + msg
}
switch level {
case "trace":
logrus.Trace(msg)
case "debug":
logrus.Debug(msg)
case "warn", "warning":
logrus.Warn(msg)
case "error":
logrus.Error(msg)
default:
logrus.Info(msg)
}
}