diff --git a/docs/README.md b/docs/README.md index fb66367ca1..40f96b0e1b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -58,6 +58,7 @@ an issue: * [DevTools Extension](tutorial/devtools-extension.md) * [Automated Testing](tutorial/automated-testing.md) * [REPL](tutorial/repl.md) + * [Crash Reporting](tutorial/crash-reporting.md) * [Distribution](tutorial/application-distribution.md) * [Code Signing](tutorial/code-signing.md) * [Mac App Store](tutorial/mac-app-store-submission-guide.md) diff --git a/docs/api/app.md b/docs/api/app.md index b594e829c0..cfdce138bb 100644 --- a/docs/api/app.md +++ b/docs/api/app.md @@ -656,7 +656,8 @@ Returns `string` - The current application directory. * `videos` Directory for a user's videos. * `recent` Directory for the user's recent files (Windows only). * `logs` Directory for your app's log folder. - * `crashDumps` Directory where crash dumps are stored. + * `crashDumps` Directory where crash dumps are stored. See + [Crash Reporting](../tutorial/crash-reporting.md#what-happens-when-your-app-crashes). Returns `string` - A path to a special directory or file associated with `name`. On failure, an `Error` is thrown. diff --git a/docs/api/crash-reporter.md b/docs/api/crash-reporter.md index 9fb3d01311..884a8ae4f3 100644 --- a/docs/api/crash-reporter.md +++ b/docs/api/crash-reporter.md @@ -19,30 +19,27 @@ const { crashReporter } = require('electron') crashReporter.start({ submitURL: 'https://your-domain.com/url-to-submit' }) ``` -For setting up a server to accept and process crash reports, you can use -following projects: +For a guide to collecting, receiving and symbolicating crash reports, including +how to run your own crash server or use a hosted service, see the +[Crash Reporting](../tutorial/crash-reporting.md) tutorial. -* [socorro](https://github.com/mozilla-services/socorro) -* [mini-breakpad-server](https://github.com/electron/mini-breakpad-server) +Electron uses [Crashpad](https://chromium.googlesource.com/crashpad/crashpad/+/refs/heads/main/README.md) +to monitor and report crashes. Crashpad uses the same +[upload protocol](https://chromium.googlesource.com/crashpad/crashpad/+/HEAD/doc/overview_design.md#Upload-to-collection-server) +as Breakpad, so servers that accept Breakpad minidumps can receive Electron's +crash reports. -> [!NOTE] -> Electron uses Crashpad, not Breakpad, to collect and upload -> crashes, but for the time being, the [upload protocol is the same](https://chromium.googlesource.com/crashpad/crashpad/+/HEAD/doc/overview_design.md#Upload-to-collection-server). +Crash reports are stored under the directory returned by +[`app.getPath('crashDumps')`](app.md#appgetpathname). You can override it by calling +[`app.setPath('crashDumps', '/path/to/crashes')`](app.md#appsetpathname-path) before starting the crash +reporter. The layout of files inside this directory is an implementation detail +and may change between versions of Electron. -Or use a 3rd party hosted solution: - -* [Backtrace](https://backtrace.io/electron/) -* [Sentry](https://docs.sentry.io/clients/electron) -* [BugSplat](https://www.bugsplat.com/docs/platforms/electron) -* [Bugsnag](https://docs.bugsnag.com/platforms/electron/) - -Crash reports are stored temporarily before being uploaded in a directory -underneath the app's user data directory, called 'Crashpad'. You can override -this directory by calling `app.setPath('crashDumps', '/path/to/crashes')` -before starting the crash reporter. - -Electron uses [crashpad](https://chromium.googlesource.com/crashpad/crashpad/+/refs/heads/main/README.md) -to monitor and report crashes. +The `crashReporter` module is disabled in Mac App Store builds. Its methods can +be called, but they do nothing: no crash reports are collected or uploaded, +[`getUploadedReports()`](#crashreportergetuploadedreports) returns an empty array and +[`getUploadToServer()`](#crashreportergetuploadtoserver) returns +`false`. On Windows, some crashes never reach an in-process crash handler, most notably `__fastfail` terminations (`STATUS_STACK_BUFFER_OVERRUN`, raised by @@ -90,13 +87,16 @@ changes: * `uploadToServer` boolean (optional) - Whether crash reports should be sent to the server. If false, crash reports will be collected and stored in the crashes directory, but not uploaded. Default is `true`. - * `ignoreSystemCrashHandler` boolean (optional) - If true, crashes generated - in the main process will not be forwarded to the system crash handler. - Default is `false`. - * `rateLimit` boolean (optional) _macOS_ _Windows_ - If true, limit the - number of crashes uploaded to 1/hour. Default is `false`. + * `ignoreSystemCrashHandler` boolean (optional) _macOS_ _Linux_ - If true, + crashes generated in the main process will not be forwarded to the system + crash handler. This option has no effect on Windows. Default is `false`. + * `rateLimit` boolean (optional) - If true, limit the number of crashes + uploaded to 1/hour. Crash reports over the limit are not uploaded, but are + still stored on disk. Default is `false`. * `compress` boolean (optional) - If true, crash reports will be compressed - and uploaded with `Content-Encoding: gzip`. Default is `true`. + and uploaded with `Content-Encoding: gzip`. Setting this + to `false` while `uploadToServer` is `true` is deprecated and logs a + deprecation warning. Default is `true`. * `extra` Record\ (optional) - Extra string key/value annotations that will be sent along with crash reports that are generated in the main process. Only string values are supported. Crashes generated in @@ -110,7 +110,8 @@ changes: been started. If a key is present in both the global extra parameters and the process-specific extra parameters, then the global one will take precedence. By default, `productName` and the app version are included, as - well as the Electron version. + well as the Electron version. Global extra parameters are not returned by + [`getParameters()`](#crashreportergetparameters). This method must be called before using any other `crashReporter` APIs. Once initialized this way, the crashpad handler collects crashes from all @@ -132,10 +133,10 @@ by the crash reporter. > [!NOTE] > Parameters passed in `extra`, `globalExtra` or set with -> `addExtraParameter` have limits on the length of the keys and values. Key names -> must be at most 39 bytes long, and values must be no longer than 127 bytes. -> Keys with names longer than the maximum will be silently ignored. Key values -> longer than the maximum length will be truncated. +> `addExtraParameter` have limits on the length of the keys and values. Key +> names must be at most 39 bytes long, and values must be no longer than 20320 +> bytes. Keys with names longer than the maximum are ignored, and a warning is +> emitted. Values longer than the maximum length are truncated. > [!NOTE] > This method is only available in the main process. @@ -152,9 +153,13 @@ changes: --> Returns [`CrashReport | null`](structures/crash-report.md) - The date and ID of the -last crash report. Only crash reports that have been uploaded will be returned; -even if a crash report is present on disk it will not be returned until it is -uploaded. In the case that there are no uploaded reports, `null` is returned. +crash report with the most recent upload time, from the list returned by +[`getUploadedReports()`](#crashreportergetuploadedreports). If there are no crash +reports at all, `null` is returned. + +If no report has been uploaded yet but some are stored on disk, a report that has +not been uploaded may be returned. Check that its `id` is not empty before +treating it as uploaded. > [!NOTE] > This method is only available in the main process. @@ -172,8 +177,14 @@ changes: Returns [`CrashReport[]`](structures/crash-report.md): -Returns all uploaded crash reports. Each report contains the date and uploaded -ID. +Returns the crash reports stored on disk. Each report contains the date it was +uploaded and the ID that the crash server returned for it. + +Despite the method's name, reports that have not been uploaded (for example +because `uploadToServer` is `false`, the upload failed, or the report was rate +limited) are included too. For those reports, `id` is an empty string and `date` +is not meaningful. To list only uploaded reports, filter out reports with an +empty `id`. > [!NOTE] > This method is only available in the main process. @@ -217,11 +228,13 @@ called before `start` is called. ### `crashReporter.addExtraParameter(key, value)` * `key` string - Parameter key, must be no longer than 39 bytes. -* `value` string - Parameter value, must be no longer than 127 bytes. +* `value` string - Parameter value, must be no longer than 20320 bytes. Set an extra parameter to be sent with the crash report. The values specified here will be sent in addition to any values set via the `extra` option when -`start` was called. +`start` was called. Calling this again with the same key replaces the value. +The value is read when a crash happens, so you can update it as your app's +state changes. Parameters added in this fashion (or via the `extra` parameter to `crashReporter.start`) are specific to the calling process. Adding extra @@ -229,12 +242,15 @@ parameters in the main process will not cause those parameters to be sent along with crashes from renderer or other child processes. Similarly, adding extra parameters in a renderer process will not result in those parameters being sent with crashes that occur in other renderer processes or in the main process. +Processes created with [`utilityProcess`](utility-process.md) have no API for +setting extra parameters, so only `globalExtra` values are sent with their +crashes. > [!NOTE] > Parameters have limits on the length of the keys and values. Key > names must be no longer than 39 bytes, and values must be no longer than 20320 -> bytes. Keys with names longer than the maximum will be silently ignored. Key -> values longer than the maximum length will be truncated. +> bytes. Keys with names longer than the maximum are ignored, and a warning is +> emitted. Values longer than the maximum length are truncated. ### `crashReporter.removeExtraParameter(key)` @@ -245,21 +261,21 @@ will not include this parameter. ### `crashReporter.getParameters()` -Returns `Record` - The current 'extra' parameters of the crash reporter. +Returns `Record` - The current 'extra' parameters of the crash +reporter in the calling process, as set with the `extra` option and +`addExtraParameter`. Parameters set with the `globalExtra` option are not +included. ## In Node child processes -Since `require('electron')` is not available in Node child processes, the -following APIs are available on the `process` object in Node child processes. +Since `require('electron')` is not available in Node child processes (processes +run with `ELECTRON_RUN_AS_NODE`, such as those created with +`child_process.fork()`), the following APIs are available on the `process` +object in Node child processes. -#### `process.crashReporter.start(options)` - -See [`crashReporter.start()`](#crashreporterstartoptions). - -Note that if the crash reporter is started in the main process, it will -automatically monitor child processes, so it should not be started in the child -process. Only use this method if the main process does not initialize the crash -reporter. +If the crash reporter is started in the main process, Node child processes are +monitored automatically. There is no way to start the crash reporter from a +Node child process. #### `process.crashReporter.getParameters()` @@ -276,18 +292,28 @@ See [`crashReporter.removeExtraParameter(key)`](#crashreporterremoveextraparamet ## Crash Report Payload The crash reporter will send the following data to the `submitURL` as -a `multipart/form-data` `POST`: +a `multipart/form-data` `POST`. Unless `compress` is `false`, the request body +is gzip-compressed and sent with `Content-Encoding: gzip`. * `ver` string - The version of Electron. * `platform` string - e.g. 'win32'. -* `process_type` string - e.g. 'renderer'. +* `process_type` string - e.g. 'renderer', or 'browser' for the main process. * `guid` string - e.g. '5e1286fc-da97-479e-918b-6bfb0c3d1c72'. * `_version` string - The version in `package.json`. * `_productName` string - The product name in the `crashReporter` `options` object. * `prod` string - Name of the underlying product. In this case Electron. * `_companyName` string - The company name in the `crashReporter` `options` - object. + object. Only sent if the deprecated `companyName` option is set. * `upload_file_minidump` File - The crash report in the format of `minidump`. -* All level one properties of the `extra` object in the `crashReporter` +* All level one properties of the `globalExtra` object in the `crashReporter` `options` object. +* All extra parameters of the process that crashed, set with the `extra` + option (main process only) or `addExtraParameter`. + +Crashpad and Chromium may add other fields to the upload. These are not part +of Electron's API and can change without notice, so don't rely on them. + +The body of the server's response is stored as the crash report's ID, and is +returned in the `id` field by +[`getUploadedReports()`](#crashreportergetuploadedreports). diff --git a/docs/tutorial/crash-reporting.md b/docs/tutorial/crash-reporting.md new file mode 100644 index 0000000000..af5582d99a --- /dev/null +++ b/docs/tutorial/crash-reporting.md @@ -0,0 +1,373 @@ +# Crash Reporting + +## Overview + +When a native crash happens in your app (e.g. a segfault in Chromium, a fatal V8 error, an +out-of-memory renderer, or a bug in a native Node.js module), there is no JavaScript +exception to catch. Electron's [`crashReporter`](../api/crash-reporter.md) module +captures these crashes as minidumps that you can collect, upload and turn back into +readable stack traces. + +This guide covers how crash reporting works in Electron, how to attach useful context to +reports, how to receive them, and how to symbolicate them. + +## What happens when your app crashes + +Electron uses [Crashpad](https://chromium.googlesource.com/crashpad/crashpad/+/refs/heads/main/README.md), +the same crash reporting system as Chromium. When you call `crashReporter.start()` in the +main process: + +1. Electron starts a separate crash handler process. +2. Child processes created after `start()` is called (renderers, the GPU process, utility + processes and Node.js child processes) are monitored automatically. +3. When a monitored process crashes, the handler writes a minidump: a snapshot of the + crashed process's threads, stacks and loaded modules. This works even when the crashed + process is too broken to run any code of its own. +4. If uploads are enabled, the handler sends the minidump to your `submitURL`, along with + the annotations (key/value metadata) that were set when the process crashed. + +Crash reports are stored under the directory returned by `app.getPath('crashDumps')`. To +store them somewhere else, call `app.setPath('crashDumps', path)` before calling +`crashReporter.start()`. The layout of files inside this directory is an implementation +detail and may change between versions of Electron, so don't depend on it. + +## Setting up the crash reporter + +Call `crashReporter.start()` in the main process as early as possible, ideally before +`app.whenReady()`. Renderer and child processes that start before the crash reporter +won't be monitored. + +```js title='main.js' +const { app, crashReporter } = require('electron') + +crashReporter.start({ + submitURL: 'https://crashes.example.com/submit', + uploadToServer: true, + globalExtra: { + releaseChannel: 'beta' + } +}) + +app.whenReady().then(() => { + // create windows... +}) +``` + +The options that matter most are: + +* `submitURL` - Where crash reports are sent, as a `multipart/form-data` `POST`. + Required unless `uploadToServer` is `false`. +* `uploadToServer` - Whether to upload reports. If you ask users for consent to send + crash reports, start with `uploadToServer: false` and call + `crashReporter.setUploadToServer(true)` when they opt in. Reports are still written + to disk when uploads are disabled. +* `productName` - Sent as `_productName`. Defaults to `app.name`. +* `globalExtra` - Annotations that are sent with crashes from every process. See + [Attaching context to crash reports](#attaching-context-to-crash-reports). +* `extra` - Annotations for crashes in the main process only. +* `rateLimit` - Limits uploads to one per hour. Reports over the limit are not uploaded + but stay on disk. +* `compress` - Uploads are gzip-compressed (`Content-Encoding: gzip`) by default. Most + crash servers expect this. Setting `compress: false` is deprecated and logs a + warning. +* `ignoreSystemCrashHandler` - Stops main process crashes from also being passed to the + operating system's crash handler. This has no effect on Windows. + +Calling `start()` a second time does nothing, and the crash reporter can't be stopped +once it has started. + +To test your setup, call `process.crash()` from the process you want to crash. + +### Mac App Store builds + +The crash reporter is disabled in [Mac App Store](./mac-app-store-submission-guide.md) +builds of Electron. All `crashReporter` methods can still be called, but they do +nothing: no minidumps are written and `getUploadedReports()` always returns an empty +array. Crashes in MAS builds are reported only through Apple's own crash reporting. + +### Sandboxed renderers and preload scripts + +`crashReporter` is one of the modules available to [sandboxed](./sandbox.md) preload +scripts. In renderer processes it only provides `addExtraParameter()`, +`removeExtraParameter()` and `getParameters()`. The crash reporter itself is started in +the main process. + +With [context isolation](./context-isolation.md) enabled, web content can't reach +`crashReporter` directly. Call it from your preload script, or expose a narrow function +with the [`contextBridge`](../api/context-bridge.md), as shown in the next section. + +## Attaching context to crash reports + +A stack trace tells you where a crash happened. Annotations tell you what the app was +doing at the time: which window was open, which feature was in use, which account type +the user has. Annotation values are captured at the moment of the crash, so what you get +is whatever the value was when the process died. + +### Where annotations apply + +There are two kinds of annotation, and it's important to put each value in the right +place: + +* **`globalExtra`** is set once, in `crashReporter.start()`, and is sent with crashes + from every process. It can't be changed later. Use it for values that don't change + while the app is running, such as a release channel or build ID. +* **`extra` and `addExtraParameter()`** are per-process. A value set in the main + process is only sent with main process crashes. A value set in one renderer is only + sent when that renderer crashes. Each process has to set its own values. + +If the same key is set in both `globalExtra` and a process's own parameters, the +`globalExtra` value is used. + +Where you can set per-process values: + +| Process | How to set values | +| --- | --- | +| Main | `extra` in `crashReporter.start()`, or `crashReporter.addExtraParameter()` | +| Renderer | `crashReporter.addExtraParameter()` in the preload script | +| Node.js child process (`child_process.fork()`) | `process.crashReporter.addExtraParameter()` | +| Utility process (`utilityProcess.fork()`) | No API. Only `globalExtra` values are sent. | + +`crashReporter.getParameters()` returns the current process's own parameters. It does not +include `globalExtra`. + +### Limits + +* Key names must be at most 39 bytes long. Longer keys are ignored, and Electron emits a + process warning when you try to set one. +* Values are strings of at most 20320 bytes. Longer values are truncated. +* Limits are in bytes, not characters, so non-ASCII text uses up the limit faster. + +### Keep annotations current + +Because values are read at crash time, update them as your app's state changes. For +example, you can record how many windows are open and which feature the user is using +in the main process: + +```js title='main.js' +const { app, crashReporter } = require('electron') + +let windowCount = 0 + +app.on('browser-window-created', (event, win) => { + windowCount++ + crashReporter.addExtraParameter('windowCount', String(windowCount)) + win.on('closed', () => { + windowCount-- + crashReporter.addExtraParameter('windowCount', String(windowCount)) + }) +}) + +async function exportProject() { + crashReporter.addExtraParameter('feature', 'export') + try { + // ...run the export + } finally { + crashReporter.removeExtraParameter('feature') + } +} +``` + +For renderer crashes, set values in the renderer. This preload script records the current +route of a single-page app and lets the page mark which feature is active. It only +accepts known keys, so the page can't fill reports with arbitrary data: + +```js title='preload.js' +const { contextBridge, crashReporter } = require('electron') + +const allowedKeys = new Set(['feature', 'route']) + +contextBridge.exposeInMainWorld('crashContext', { + set: (key, value) => { + if (allowedKeys.has(key)) { + crashReporter.addExtraParameter(key, String(value)) + } + } +}) + +window.addEventListener('DOMContentLoaded', () => { + crashReporter.addExtraParameter('route', location.pathname) +}) +``` + +```js title='renderer.js' @ts-nocheck +window.crashContext.set('route', '/settings') +window.crashContext.set('feature', 'image-editor') +``` + +## Reacting to crashes at runtime + +Minidumps are for diagnosing a crash later. To react when a process dies, for example to +log it or recover, listen for these events in the main process: + +* [`render-process-gone`](../api/app.md#event-render-process-gone) on `app`, or on a + single [`webContents`](../api/web-contents.md#event-render-process-gone), for renderer + processes. +* [`child-process-gone`](../api/app.md#event-child-process-gone) on `app` for all other + child processes, such as the GPU and utility processes. + +Both events provide a `reason` (such as `crashed`, `oom`, `killed` or `clean-exit`) and +an `exitCode`. + +```js title='main.js' +const { app, BrowserWindow } = require('electron') + +app.on('child-process-gone', (event, details) => { + console.error(`${details.type} process gone: ${details.reason} (exit code ${details.exitCode})`) +}) + +app.whenReady().then(() => { + const win = new BrowserWindow() + let recentCrashes = 0 + + win.webContents.on('render-process-gone', (event, details) => { + console.error(`Renderer gone: ${details.reason} (exit code ${details.exitCode})`) + if (details.reason === 'clean-exit') return + + // Reload the page in a new renderer process, but don't retry forever. + recentCrashes++ + if (recentCrashes <= 3) { + win.reload() + setTimeout(() => { + recentCrashes-- + }, 60 * 1000) + } + }) + + win.loadFile('index.html') +}) +``` + +## Receiving crash reports + +### On your own server + +Crash reports are sent to `submitURL` as a `multipart/form-data` `POST`, gzip-compressed +unless you set `compress: false`. The form contains: + +* `upload_file_minidump` - The minidump file. +* `process_type` - The type of process that crashed, such as `renderer`, or `browser` + for the main process. +* `prod` - Always `Electron`. +* `ver` - The Electron version. +* `_productName` - The `productName` option, which defaults to `app.name`. +* `_version` - Your app's version, from `app.getVersion()`. +* `guid` - An ID for this installation. +* `platform` - `win32`, `darwin` or `linux`. +* Your `globalExtra` values and the crashed process's own parameters. + +Crashpad and Chromium may add other fields. These aren't part of Electron's API and can +change without notice, so don't rely on them. See +[Crash Report Payload](../api/crash-reporter.md#crash-report-payload) for the full list +of documented fields. + +Respond with a `200` status. The body of the response is stored as the report's ID, which +`crashReporter.getUploadedReports()` returns, so you can use it to link a user's report +to your server's record. + +Crashpad uses the Breakpad upload protocol, so any server that accepts Breakpad or +Crashpad minidumps can receive Electron's reports. + +### Collecting reports locally + +If you set `uploadToServer: false`, crash reports are still written under +`app.getPath('crashDumps')`, but they are not sent anywhere. This is useful while +developing, or if you want to ask the user before sending anything: once they agree, call +`crashReporter.setUploadToServer(true)`. Only crashes that happen after that are +uploaded; reports written while uploads were disabled are not sent later. + +Electron doesn't provide an API for reading minidump files from disk, and the layout of +the crash dumps directory can change between versions. If you need the minidumps +themselves, receive them with your own `submitURL` (which can be a server running on +the same machine) instead of reading files from the directory. + +## Symbolicating crash reports + +A minidump contains raw memory addresses, not function names. Electron's release builds +are stripped of debug information, so to turn those addresses into a readable stack +trace you need the symbol files for the exact Electron version, platform and +architecture that crashed. This is called symbolication. + +### Getting Electron's symbols + +Each [Electron release](https://github.com/electron/electron/releases) on GitHub ships +symbol archives for every platform it supports: + +* [Breakpad](https://chromium.googlesource.com/breakpad/breakpad/) symbols for all + platforms. These are what most minidump tools use. +* dSYMs for macOS, for use with Apple's tools such as `atos` and `lldb`. +* PDBs for Windows, for use with WinDbg and Visual Studio. +* Debug info for Linux, for use with `gdb`. + +Electron also runs a symbol server at `https://symbols.electronjs.org`. Tools that +support symbol servers can download the symbols they need from it, so you don't have to +download the archives for each version. For debuggers on Windows, see +[Setting Up Symbol Server in Debugger](../development/debugging-with-symbol-server.md). + +### Example: symbolicating a minidump + +The [`electron-minidump`](https://www.npmjs.com/package/electron-minidump) package +symbolicates an Electron minidump in one step. It works out which Electron version the +dump came from, downloads the matching symbols, and prints +the stack of every thread: + +```sh +npx electron-minidump /path/to/crash.dmp +``` + +It runs on macOS and Linux, and can symbolicate dumps from any platform. + +If you also have symbols for your own native code, pass a directory containing all the +Breakpad symbols to Breakpad's `minidump_stackwalk` tool instead. + +### Symbols for your own code + +Electron's symbols only cover Electron's own binaries. If your app includes native Node.js +modules or other native libraries, frames in those will stay unsymbolicated unless you +also keep symbols for them. Generate Breakpad symbols for each binary you ship with +Breakpad's `dump_syms` tool, and store them where your symbolication tool can find them. Keep them for every +version you release, since symbols only match the exact build they came from. + +## macOS system crash reports + +When an app crashes on macOS, the system may also write a crash report (an `.ips` file, +shown in the Console app) to `~/Library/Logs/DiagnosticReports`. For Electron's release +builds these reports are misleading: because the binaries are stripped, macOS labels each +frame with the nearest exported symbol plus a large offset, such as +`v8::internal::SetupIsolateDelegate::SetupHeap(v8::internal::Heap*) + 2919558`. These +function names are almost always wrong. + +To get a correct stack from one of these reports, use +[`@electron/symbolicate-mac`](https://github.com/electron/symbolicate-mac). It reads text +crash reports, spindumps and `sample` output, works out the Electron version from the +report's "Binary Images" section, and downloads the right symbols: + +```sh +npx @electron/symbolicate-mac /path/to/crash.txt +``` + +For an `.ips` file, open it in the Console app and save the full text report, including +the "Binary Images" section, first. + +You can also symbolicate individual addresses yourself with `atos` and the dSYM from the +matching release. Take the load address of `Electron Framework` from the "Binary Images" +section and the frame's address from the stack: + +```sh +atos -arch arm64 -o "Electron Framework.dSYM/Contents/Resources/DWARF/Electron Framework" \ + -l 0x109b49000 0x10ae32a06 +``` + +## Using a hosted crash reporting service + +Instead of running your own server, you can send crash reports to a hosted service. These +services receive Electron's minidumps, symbolicate them (usually with Electron's public +symbols) and group similar crashes. Many also offer SDKs that report JavaScript errors +alongside native crashes. Services with Electron support include: + +* [Sentry](https://docs.sentry.io/platforms/javascript/guides/electron/) +* [BugSplat](https://docs.bugsplat.com/integrations/desktop/electron) +* [Backtrace](https://github.com/backtrace-labs/backtrace-javascript/tree/main/packages/electron) (part of Sauce Labs) +* [Bugsnag](https://docs.bugsnag.com/platforms/electron/) + +This list is provided for convenience. The Electron project doesn't endorse or support +any of these services; check each one's documentation for how it works with Electron.