feat(logging): capture frontend errors to disk with structured redaction

Frontend half of the logging overhaul.

Capture:
- Add a global error boundary plus window error/unhandledrejection
  handlers that persist renderer errors via tauri-plugin-log (new
  @tauri-apps/plugin-log dependency + log:default capability), so a
  white-screen crash leaves a trace.

Redaction (two layers over every logged value):
- Structured serializer: redact by sensitive property name (arrays and
  nested values included) and by opaque value shape.
- Text layer, the universal final pass: redact URL credentials/query,
  auth schemes, known secret shapes, and named-key array/object values.
  The container rule closes prefix+JSON, double-encoded, and POJO-error
  bypasses regardless of which entrypoint the structured data came from.
- Render Errors as redacted message + native stack across V8 and WebKit
  (macOS/Linux) without depending on a single stack format.
- Bound all inputs and drop oversized JSON to keep the UI responsive.

i18n:
- zh / en / ja / zh-TW for the new log settings and error text.
This commit is contained in:
Jason
2026-07-16 10:21:00 +08:00
parent 22d2872c33
commit 6274705848
12 changed files with 853 additions and 46 deletions
+25 -14
View File
@@ -14,6 +14,13 @@ import { listen } from "@tauri-apps/api/event";
import { invoke } from "@tauri-apps/api/core";
import { message } from "@tauri-apps/plugin-dialog";
import { exit } from "@tauri-apps/plugin-process";
import { FrontendErrorBoundary } from "./components/FrontendErrorBoundary";
import {
installGlobalErrorHandlers,
reportFrontendError,
} from "./lib/frontendLogger";
installGlobalErrorHandlers();
// 根据平台添加 body class,便于平台特定样式
try {
@@ -70,7 +77,7 @@ try {
});
} catch (e) {
// 忽略事件订阅异常(例如在非 Tauri 环境下)
console.error("订阅 configLoadError 事件失败", e);
reportFrontendError("config_load_error_listener", e);
}
async function bootstrap() {
@@ -83,10 +90,12 @@ async function bootstrap() {
// 数据库版本过新:渲染应用内「升级应用」恢复界面,不进入正常 App
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<ThemeProvider defaultTheme="system" storageKey="cc-switch-theme">
<DatabaseUpgrade payload={initError} />
<Toaster />
</ThemeProvider>
<FrontendErrorBoundary>
<ThemeProvider defaultTheme="system" storageKey="cc-switch-theme">
<DatabaseUpgrade payload={initError} />
<Toaster />
</ThemeProvider>
</FrontendErrorBoundary>
</React.StrictMode>,
);
return;
@@ -98,19 +107,21 @@ async function bootstrap() {
}
} catch (e) {
// 忽略拉取错误,继续渲染
console.error("拉取初始化错误失败", e);
reportFrontendError("get_init_error", e);
}
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<ThemeProvider defaultTheme="system" storageKey="cc-switch-theme">
<UpdateProvider>
<App />
<Toaster />
</UpdateProvider>
</ThemeProvider>
</QueryClientProvider>
<FrontendErrorBoundary>
<QueryClientProvider client={queryClient}>
<ThemeProvider defaultTheme="system" storageKey="cc-switch-theme">
<UpdateProvider>
<App />
<Toaster />
</UpdateProvider>
</ThemeProvider>
</QueryClientProvider>
</FrontendErrorBoundary>
</React.StrictMode>,
);
}