Observability
Logging with redaction, diagnostics, opt-in crash reporting, and analytics.
The template ships four observability layers, all wired to be safe by default: logging with automatic redaction, a diagnostics blob for bug reports, an opt-in crash-reporting slot, and an opt-in analytics slot. Nothing leaves the device unless you configure it and the user opts in.
Logging
lib/main/logger.ts wraps electron-log.
The file transport logs at info, the console transport at debug, and the log
file is capped at 5 MB. startCatching also routes uncaught main-process errors
into the optional crash reporter.
Write logs with writeLog, which tags each line with its source:
import { writeLog } from '@/lib/main/logger'
writeLog({ level: 'warn', message: 'Retry scheduled', context: { attempt: 2 }, source: 'main' })Renderer logs are forwarded over the app-log Conveyor channel and land in the
same file tagged [renderer] — and their context is redacted on the way in
(see below). Log file locations follow electron-log's defaults:
| OS | Path |
|---|---|
| macOS | ~/Library/Logs/<AppName>/main.log |
| Windows | %USERPROFILE%\AppData\Roaming\<AppName>\logs\main.log |
| Linux | ~/.config/<AppName>/logs/main.log |
The in-app Logs page (app/routes/LogsPage.tsx, logs Conveyor domain)
shows the resolved path and a "Open log folder" button (logs-open-folder), so a
user filing a report can find the file without knowing OS conventions.
Redaction
lib/main/log-redact.ts is a selling point: it scrubs secrets out of anything
that gets logged, put in diagnostics, or sent to the crash reporter. redactSensitive
walks strings, objects, arrays, and Errors (cycle-safe) and replaces matches:
| Pattern | Catches |
|---|---|
| Sensitive keys | password, secret, token, apikey / api_key, auth, cookie, session, credential, pin → value becomes [redacted]. |
| Sensitive values | key: value / key=value pairs for the same terms. |
| Bearer tokens | bearer <token> → bearer [redacted]. |
| URL credentials | https://user:pass@host → https://[redacted]@host. |
It's applied at the boundaries where secrets could leak: renderer log context
(redactContext in the app-log handler), the diagnostics blob, and the crash
reporter's beforeSend. That means a stray token in a log line or a signed URL
in a diagnostics field is stripped before it's persisted or transmitted.
Diagnostics
lib/main/diagnostics.ts assembles a build-and-runtime metadata blob: app
name/version/commit/build-time, Electron/Chromium/Node/V8 versions, OS
platform/release/arch, and the log path. The whole blob is passed through
redactSensitive before it crosses IPC, so any field that later starts carrying
a secret is stripped defensively.
The About page reads it over app-get-diagnostics and offers a "Copy
diagnostics" button — the exact (already-redacted) text a user can paste into a
bug report.
Crash reporting (opt-in slot)
Error reporting is an opt-in slot, dark unless two things are true: a DSN is
configured and the user has opted in. lib/main/error-reporting.ts
(ErrorReportingService) integrates optional @sentry/electron:
- DSN comes from
MAIN_VITE_SENTRY_DSN(main-process build var) or theSENTRY_DSNruntime env. No DSN → the service is inert. - Consent is
settings.errorReportingOptIn(default off, toggled in the Advanced settings tab).initialize()andreport()both bail if it's off. - The Sentry SDK is loaded with a guarded dynamic
require, so if the optional dependency isn't installed the service logs a warning and stays dark instead of crashing. sendDefaultPii: falseand abeforeSendthat runsredactSensitivekeep captured events scrubbed.
Both main-process crashes (via the logger's startCatching) and renderer errors
(forwarded through the app-report-renderer-error channel to
reportRendererError) flow into the same slot.
Product analytics (opt-in slot)
app/services/analytics.ts is a second, deliberately thin opt-in slot — separate
from the error slot. No analytics provider ships by default; a buyer points
it at their own collector. It is a hard no-op unless both:
VITE_ANALYTICS_ENDPOINTis set at build time (analyticsConfig.configured), and- the user opts in (
settings.analyticsOptIn, default off).
When enabled, track() / page() fire-and-forget a JSON POST to the endpoint
(with an optional VITE_ANALYTICS_WRITE_KEY bearer token, keepalive: true).
Events carry only what the caller passes plus a locally generated anonymous id —
never the Supabase session, JWTs, emails, or file paths. Network failures are
swallowed so analytics can never disrupt the app.
Both opt-in slots default off and ship dark. Turning them on is a deliberate, documented act — set the env/DSN, then honor the user's Advanced-tab consent. See Environment Variables.
Renderer error UX
Two React error boundaries plus a themed error route keep a renderer failure from becoming a blank window:
RouterErrorBoundary(app/app.tsx) catches errors within the routed tree and can recover via navigation.ErrorBoundary(app/renderer.tsx) is the last-resort fallback wrapping the whole app.ErrorPage(/error) is the themed screen shown for routed errors.
Renderer errors are also logged and (when the slot is on) reported, so a crash is both visible to the user and captured for you.
