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:

OSPath
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:

PatternCatches
Sensitive keyspassword, secret, token, apikey / api_key, auth, cookie, session, credential, pin → value becomes [redacted].
Sensitive valueskey: value / key=value pairs for the same terms.
Bearer tokensbearer <token>bearer [redacted].
URL credentialshttps://user:pass@hosthttps://[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 the SENTRY_DSN runtime env. No DSN → the service is inert.
  • Consent is settings.errorReportingOptIn (default off, toggled in the Advanced settings tab). initialize() and report() 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: false and a beforeSend that runs redactSensitive keep 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:

  1. VITE_ANALYTICS_ENDPOINT is set at build time (analyticsConfig.configured), and
  2. 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.

On this page