Electron Logging and Crash Reporting: A Production Guide to Capturing What Your App Did Wrong

2026-11-05来源 dev.toEnglish

Electron Logging and Crash Reporting: A Production Guide to Capturing What Your App Did Wrong

TL;DR: In an Electron app, console.log disappears the moment you package it. Production debugging needs three separate trails — structured logs in the main process, a controlled IPC bridge from renderers, and a native crash reporter — plus a way to redact secrets and ship a bundle to support. This guide gives you a setup that works on Windows, macOS, and Linux without leaking user data.

When a user says "your app froze at 4pm and showed a white screen," what do you actually know? Without logging, the answer is "nothing." Browser-side devtools are gone in production, the main process stdout is captured by nobody, and a native crash leaves a .dmp file sitting on a machine you will never touch.

This is the setup we wish we had on day one.

Table of Contents

  • Why console.log is not enough
  • Layer 1: Structured main-process logs
  • Layer 2: Getting renderer logs to disk
  • Layer 3: Catching native and renderer crashes
  • Privacy: redact before you write
  • One-click "send me the logs"
  • Common pitfalls
  • Production checklist

Why console.log is not enough

Channel Where it goes in dev Where it goes in production
Renderer console.log DevTools console Nowhere — DevTools is detached, stdout is lost
Main console.log Terminal / VS Code Captured by the OS only if launched from a terminal
Native crash DevTools + terminal A *.dmp file in userData that no one reads
Renderer unhandled promise DevTools warning Silent failure, white screen, user force-quits

The fix is to treat logging as infrastructure: a single, rotating, on-disk trail the main process owns, plus a crash reporter that writes or uploads dumps.

Layer 1: Structured main-process logs

Use electron-log (the de-facto standard) rather than rolling your own. It handles transports, rotation, and file paths per-platform out of the box.

// main/log.js const { app } = require('electron'); const log = require('electron-log'); log.transports.file.resolvePathFn = () => `${app.getPath('userData')}/logs/main.log`; // Keep the file small; rotate automatically. log.transports.file.maxSize = 2 * 1024 * 1024; // 2 MB before rotation log.transports.file.format = '[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] {text}'; // In production, silence console noise; keep file. log.transports.console.level = process.env.NODE_ENV === 'development' ? 'debug' : 'warn'; log.transports.file.level = 'info'; module.exports = log; 

Why this shape:

  • userData/logs/ is the correct location. It is per-user, writable, and survives updates. Never write to the app bundle directory — on macOS it is read-only inside the .app, and on Windows Program Files needs admin rights.
  • Rotation matters. A log that grows forever becomes a support ticket of its own. Two megabytes is plenty; electron-log keeps a main.old.log alongside it.
  • JSON or text? Text is fine for humans; switch to json format when you ship logs into a central store.

Layer 2: Getting renderer logs to disk

The renderer cannot write files safely. It must send its logs over IPC to the main process, which is the only writer.

// preload.js — expose a safe, minimal API, not ipcRenderer itself const { contextBridge, ipcRenderer } = require('electron'); contextBridge.exposeInMainWorld('appLogger', { info: (msg, meta) => ipcRenderer.send('log', { level: 'info', msg, meta }), warn: (msg, meta) => ipcRenderer.send('log', { level: 'warn', msg, meta }), error: (msg, meta) => ipcRenderer.send('log', { level: 'error', msg, meta }), }); 
// main/index.js const log = require('./log'); const { ipcMain } = require('electron'); ipcMain.on('log', (event, entry) => { // Validate shape before writing — never trust renderer input. if (!entry || typeof entry.msg !== 'string') return; const meta = entry.meta ? JSON.stringify(entry.meta) : ''; log[entry.level]?.(`[renderer] ${entry.msg} ${meta}`); }); 

Two rules that prevent this channel from becoming a security hole:

  1. Validate and whitelist. A renderer can be compromised by malicious content; treat everything on log as untrusted data, not instructions.
  2. Don't expose ipcRenderer. contextBridge gives the renderer exactly three functions and nothing else. This is the same least-privilege boundary you should already be applying to every IPC channel.

You can also wire renderer-level global errors in one place:

window.addEventListener('error', (e) => { window.appLogger.error('uncaught error', { message: e.message, stack: e.error?.stack }); }); window.addEventListener('unhandledrejection', (e) => { window.appLogger.error('unhandled rejection', { reason: String(e.reason) }); }); 

Layer 3: Catching native and renderer crashes

Native crashes (a segfault in a native module, a Chromium crash) need Electron's built-in crash reporter. It writes minidumps and can upload them.

const { crashReporter } = require('electron'); crashReporter.start({ productName: app.getName(), companyName: 'YourCompany', submitURL: 'https://logs.example.com/crash', // or '' for local-only uploadToServer: false, // flip to true once you have an endpoint ignoreSystemCrashHandler: true, }); 

While submitURL is empty, dumps land in userData/Crashpad/ and you can retrieve them on demand. Set uploadToServer: true only when you control the endpoint and have a privacy review (see next section).

On the main side, also catch the process-level failures that would otherwise vanish:

process.on('uncaughtException', (err) => { log.error('main uncaughtException', { error: err.stack }); // Give the logger a tick to flush before exiting. setTimeout(() => app.exit(1), 500); }); process.on('unhandledRejection', (reason) => { log.error('main unhandledRejection', { reason: String(reason) }); }); 

Privacy: redact before you write

Logs are a privacy surface. Users do not expect passwords, tokens, API keys, emails, or file paths from their documents to be uploaded to your server.

// redact.js — a small middleware applied to every logged object. const SECRET = /(Bearer\s+[A-Za-z0-9\-._~+\/]+|eyJ[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+|password|token|api[_-]?key)/gi; function redact(input) { if (typeof input === 'string') return input.replace(SECRET, '[REDACTED]'); if (typeof input !== 'object' || input === null) return input; const out = {}; for (const [k, v] of Object.entries(input)) { out[k] = /password|token|secret|key|cookie/i.test(k) ? '[REDACTED]' : redact(v); } return out; } module.exports = { redact }; 

Apply it at the boundary — before the IPC handler writes, and before crashReporter uploads — not inside every call site. It is much easier to forget one call site than to forget one middleware.

One-click "send me the logs"

The highest ROI feature for a support team: a menu item that bundles the current logs and a few pieces of metadata into a zip, and lets the user attach it to a bug report.

// main/collect-logs.js const { app, dialog } = require('electron'); const fs = require('fs'); const path = require('path'); const AdmZip = require('adm-zip'); async function collectLogs() { const dir = app.getPath('userData'); const zip = new AdmZip(); const logsDir = path.join(dir, 'logs'); if (fs.existsSync(logsDir)) zip.addLocalFolder(logsDir, 'logs'); const meta = { appVersion: app.getVersion(), electron: process.versions.electron, platform: process.platform, arch: process.arch, date: new Date().toISOString(), }; zip.addFile('meta.json', Buffer.from(JSON.stringify(meta, null, 2))); const out = path.join(app.getPath('downloads'), `app-logs-${Date.now()}.zip`); zip.writeZip(out); await dialog.showMessageBox({ type: 'info', message: 'Logs saved', detail: `Saved to:\n${out}\nPlease attach this to your bug report.`, }); } 

This is intentionally user-initiated: the user chooses when to send what. That is both a privacy win and a support win — people trust the app more when logs are visible and opt-in.

Common pitfalls

Pitfall What happens Fix
Writing logs next to the app bundle Read-only on macOS / Program Files on Windows Use app.getPath('userData')
Uploading renderer logs straight to a server PII and tokens leak Redact at the IPC boundary, then upload
crashReporter.start before app.ready Dumps are not captured reliably Start it early, after the module graph loads
Logging every IPC message File grows to hundreds of MB Sample noisy channels; keep debug level local-only
Forgetting to flush before exit Last log line is missing on crash setTimeout(() => app.exit(1), 500) after uncaughtException
Trusting renderer meta A malicious renderer can forge fields Whitelist meta keys, stringify everything

Production checklist

  • [ ] Main process writes rotating logs under userData/logs/ with a max size.
  • [ ] Renderer logs go through a contextBridge IPC channel, never raw ipcRenderer.
  • [ ] IPC handler validates message shape and redacts secrets before writing.
  • [ ] crashReporter.start() runs early, with local dumps until you are ready to upload.
  • [ ] Main process handles uncaughtException and unhandledRejection, with a flush before exit.
  • [ ] A user-facing "send logs" action zips logs/ + meta.json to the Downloads folder.
  • [ ] File logging level is info in production, debug in development.
  • [ ] No secret, token, or email pattern appears in a sample log line you review before shipping.

Good logging is invisible until the day you need it. Build it on a quiet Tuesday, because on a Friday night at 9pm you will be very glad it is there.

原文:https://www.a8q9.cn/en/article/electron-logging-and-crash-reporting-a-production-guide-to-capturing-what-your-a-c0f9ba.html

相关文章