feat(errors): add Sentry observability behind a sinks facade
logError / reportError now route through a pair of sinks installed at
bootstrap by each process (main + every renderer entry). No call site
knows about Sentry — if sentryDsn is empty, the sinks simply aren't
installed and logError/reportError stay console-only.
- appConfig.sentryDsn: per-env string (empty for now — populate when
ops creates the DSNs). Empty is the no-op mode for dev.
- lib/errors.ts: installErrorSinks({ capture, breadcrumb }) gates
Sentry.captureException / Sentry.addBreadcrumb. Everything flows
through toUserMessage and the two existing call types.
- lib/sentry.ts: initSentryRenderer() for the main window + autoplay,
huddle, and screen-record renderers.
- main/sentry.ts: initSentryMain() runs before anything else in
main.ts to catch bootstrap failures. Captures uncaught exceptions
and the crash reporter automatically.
- main/ipc-utils.ts::safeHandle now routes through reportError.
- main.ts::fetchLinkMetadata logs via logError.
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { AutoplayApp } from './AutoplayApp';
|
||||
import { initSentryRenderer } from '@/lib/sentry';
|
||||
import '@/styles/globals.css';
|
||||
|
||||
initSentryRenderer();
|
||||
|
||||
const root = createRoot(document.getElementById('root')!);
|
||||
root.render(<AutoplayApp />);
|
||||
|
||||
@@ -23,6 +23,8 @@ type AppConfig = {
|
||||
orionUrl: string;
|
||||
pusherUrl: string;
|
||||
firebase: FirebaseConfig;
|
||||
/** Empty string disables Sentry. Populate per-env when ready to ship. */
|
||||
sentryDsn: string;
|
||||
};
|
||||
|
||||
const configs: Record<"dev" | "prod", AppConfig> = {
|
||||
@@ -37,6 +39,7 @@ const configs: Record<"dev" | "prod", AppConfig> = {
|
||||
projectId: "flowy-dev-440017",
|
||||
storageBucket: "flowy-dev-440017.firebasestorage.app",
|
||||
},
|
||||
sentryDsn: "",
|
||||
},
|
||||
prod: {
|
||||
orionUrl: "https://orion.flowy.live",
|
||||
@@ -49,6 +52,7 @@ const configs: Record<"dev" | "prod", AppConfig> = {
|
||||
projectId: "flowy-prod-440017",
|
||||
storageBucket: "flowy-prod-440017.firebasestorage.app",
|
||||
},
|
||||
sentryDsn: "",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { HuddleApp } from './HuddleApp';
|
||||
import { initSentryRenderer } from '@/lib/sentry';
|
||||
import '@/styles/globals.css';
|
||||
|
||||
initSentryRenderer();
|
||||
|
||||
const root = createRoot(document.getElementById('root')!);
|
||||
root.render(<HuddleApp />);
|
||||
|
||||
+19
-2
@@ -59,17 +59,34 @@ export function toUserMessage(err: unknown): string {
|
||||
}
|
||||
|
||||
type ErrorContext = Record<string, unknown>;
|
||||
type ErrorSink = (err: unknown, context?: ErrorContext) => void;
|
||||
|
||||
/** Expected-but-recordable failures. Dev logs; prod is currently a no-op. */
|
||||
// Sentry (or any observability backend) installs itself via `installErrorSinks`
|
||||
// from renderer.tsx / main.ts. Until then, logError is a dev-only console call
|
||||
// and reportError always prints — no call site needs to know.
|
||||
let captureSink: ErrorSink | null = null;
|
||||
let breadcrumbSink: ErrorSink | null = null;
|
||||
|
||||
export function installErrorSinks(sinks: {
|
||||
capture: ErrorSink;
|
||||
breadcrumb: ErrorSink;
|
||||
}): void {
|
||||
captureSink = sinks.capture;
|
||||
breadcrumbSink = sinks.breadcrumb;
|
||||
}
|
||||
|
||||
/** Expected-but-recordable failures. Breadcrumb only — never pages anyone. */
|
||||
export function logError(err: unknown, context?: ErrorContext): void {
|
||||
if (appEnv === "dev") {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("[error]", err, context ?? {});
|
||||
}
|
||||
breadcrumbSink?.(err, context);
|
||||
}
|
||||
|
||||
/** Unexpected failures the user may not see. Always surfaces somewhere. */
|
||||
/** Unexpected failures the user may not see. Always captured. */
|
||||
export function reportError(err: unknown, context?: ErrorContext): void {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("[error]", err, context ?? {});
|
||||
captureSink?.(err, context);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import * as Sentry from "@sentry/electron/renderer";
|
||||
import { appConfig } from "@/config/env";
|
||||
import { installErrorSinks } from "@/lib/errors";
|
||||
|
||||
/**
|
||||
* Initialise Sentry for a renderer process. No-ops when `sentryDsn` is empty
|
||||
* so dev builds and unconfigured envs stay quiet.
|
||||
*/
|
||||
export function initSentryRenderer(): void {
|
||||
if (!appConfig.sentryDsn) return;
|
||||
|
||||
Sentry.init({
|
||||
dsn: appConfig.sentryDsn,
|
||||
tracesSampleRate: 0,
|
||||
});
|
||||
|
||||
installErrorSinks({
|
||||
capture: (err, context) =>
|
||||
Sentry.captureException(err, { extra: context }),
|
||||
breadcrumb: (err, context) =>
|
||||
Sentry.addBreadcrumb({
|
||||
category: "error",
|
||||
level: "error",
|
||||
message: err instanceof Error ? err.message : String(err),
|
||||
data: context,
|
||||
}),
|
||||
});
|
||||
}
|
||||
+5
-2
@@ -5,7 +5,11 @@ import { updateElectronApp, UpdateSourceType } from 'update-electron-app';
|
||||
|
||||
import type { LinkMetadata } from './lib/link-metadata';
|
||||
import { appConfig } from './config/env';
|
||||
import { logError } from './lib/errors';
|
||||
import { safeHandle } from './main/ipc-utils';
|
||||
import { initSentryMain } from './main/sentry';
|
||||
|
||||
initSentryMain();
|
||||
|
||||
if (app.isPackaged) {
|
||||
updateElectronApp({
|
||||
@@ -461,8 +465,7 @@ async function fetchLinkMetadata(url: string): Promise<LinkMetadata | null> {
|
||||
} catch (err) {
|
||||
// Metadata is a progressive enhancement — keep the null contract, but log
|
||||
// so upstream failures (DNS, TLS, aborted fetches) aren't invisible.
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('[ipc:link.fetch-metadata]', err);
|
||||
logError(err, { scope: 'link.fetchMetadata', url });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ipcMain, type IpcMainInvokeEvent } from "electron";
|
||||
import { reportError } from "../lib/errors";
|
||||
|
||||
type InvokeHandler = (
|
||||
event: IpcMainInvokeEvent,
|
||||
@@ -14,8 +15,7 @@ export function safeHandle(channel: string, handler: InvokeHandler): void {
|
||||
try {
|
||||
return await handler(event, ...(args as unknown[]));
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`[ipc:${channel}]`, err);
|
||||
reportError(err, { scope: `ipc.${channel}` });
|
||||
const message = err instanceof Error ? err.message : "IPC error";
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { app } from "electron";
|
||||
import * as Sentry from "@sentry/electron/main";
|
||||
import { appConfig } from "../config/env";
|
||||
import { installErrorSinks } from "../lib/errors";
|
||||
|
||||
/**
|
||||
* Initialise Sentry for the main process. Captures uncaught exceptions from
|
||||
* the Node side and the crash reporter. Safe to call before `app.whenReady`.
|
||||
*/
|
||||
export function initSentryMain(): void {
|
||||
if (!appConfig.sentryDsn) return;
|
||||
|
||||
Sentry.init({
|
||||
dsn: appConfig.sentryDsn,
|
||||
tracesSampleRate: 0,
|
||||
release: app.getVersion(),
|
||||
});
|
||||
|
||||
installErrorSinks({
|
||||
capture: (err, context) =>
|
||||
Sentry.captureException(err, { extra: context }),
|
||||
breadcrumb: (err, context) =>
|
||||
Sentry.addBreadcrumb({
|
||||
category: "error",
|
||||
level: "error",
|
||||
message: err instanceof Error ? err.message : String(err),
|
||||
data: context,
|
||||
}),
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import App from './App';
|
||||
import { initSentryRenderer } from '@/lib/sentry';
|
||||
import './styles/globals.css';
|
||||
|
||||
initSentryRenderer();
|
||||
|
||||
const root = createRoot(document.getElementById('root')!);
|
||||
root.render(<App />);
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { ScreenRecordControlApp } from './ScreenRecordControlApp';
|
||||
import { initSentryRenderer } from '@/lib/sentry';
|
||||
import '@/styles/globals.css';
|
||||
|
||||
initSentryRenderer();
|
||||
|
||||
const root = createRoot(document.getElementById('root')!);
|
||||
root.render(<ScreenRecordControlApp />);
|
||||
|
||||
Reference in New Issue
Block a user