How to collect feedback in an Electron or Tauri app
An Electron or Tauri app should send feedback from its main or Rust process to the Escuta Produto REST API, then expose one safe send method to the interface. Add the app version and operating system as metadata, and keep reports in a local queue until the network returns.
By Rafael Thayto 路 Published 路 Last updated
Why the main process should send desktop reports
A desktop app has a renderer, the window that shows your interface, and a main process that owns the app's files, network and system access. In Electron the main process is Node.js. In Tauri the core is Rust, and the window is a system web view. Feedback should leave the app from that privileged side, because the window's origin and content security rules are the most common source of blocked requests.
The REST API is the reliable path. It takes a JSON body with key, message and metadata, and it returns 201 with the item id when the report is saved. The REST API reference lists every field. The widget script can also run inside the window, which fits an app that loads its interface from the web, but a desktop app that loads local files needs more care, as covered below.
Expose one safe send method to the window
In Electron, the preload script is the only bridge between the window and the main process. Expose a single function that accepts a small object, rather than the whole IPC module:
// preload.js
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("feedback", {
send: (report) => ipcRenderer.invoke("feedback:send", report),
});
The interface then calls window.feedback.send({ kind: "bug", message: text }) and gets back a plain object. The window never receives the API key handling, the queue or the network code, which keeps the surface small. Validate the fields in the main process too, because anything the window sends should be treated as untrusted input.
Send the report from the main process
The main process builds the body, adds the app metadata and posts it with the built-in fetch available in Node 18 and later. Electron apps built on current Electron releases include it. The handler separates three outcomes: saved, refused by the API and not sent because the network failed.
// main.js
const { app, ipcMain } = require("electron");
const os = require("node:os");
const API_URL = "https://escutaproduto.com/api/v1/feedback";
const PUBLIC_KEY = "pk_your_product_key";
function buildBody(report) {
return {
key: PUBLIC_KEY,
kind: report.kind || "other",
message: report.message,
metadata: {
appVersion: app.getVersion(),
platform: process.platform,
osRelease: os.release(),
arch: process.arch,
},
};
}
async function postReport(body) {
const response = await fetch(API_URL, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
});
return response.status;
}
ipcMain.handle("feedback:send", async (_event, report) => {
const body = buildBody(report);
try {
const status = await postReport(body);
return status === 201 ? { ok: true } : { ok: false, status: status };
} catch (error) {
queueReport(body);
return { ok: false, queued: true };
}
});
A refusal such as 400 or 404 means the report will never succeed as written, so the handler returns the status and the window can explain the problem. A network error means the report is worth keeping, so it goes into the queue.
Which app and system details belong in metadata?
Desktop bugs depend on the environment, so a few details save a lot of back and forth. The most useful ones are the app version, the operating system and the architecture:
| Key | Source (Electron) | Source (Tauri) |
|---|---|---|
| appVersion | app.getVersion() | getVersion() from the app API |
| platform | process.platform | platform() from the OS plugin |
| osRelease | os.release() | version() from the OS plugin |
| arch | process.arch | arch() from the OS plugin |
Keep everything else out. File paths often contain user names, and document titles can reveal private work. If a bug depends on a document, ask the user to describe it in the message. The whole metadata object must stay under 4 KB, which these few values never approach.
What happens when the laptop is offline?
Desktop apps spend time offline, on planes, in basements or on flaky office networks. A report written during that time should not vanish. The API has no queue of its own, so the app keeps the report and sends it later. Store the queue in a JSON file in the user data folder, which Electron exposes through app.getPath("userData"):
const fs = require("node:fs");
const path = require("node:path");
function queuePath() {
return path.join(app.getPath("userData"), "feedback-queue.json");
}
function readQueue() {
try {
return JSON.parse(fs.readFileSync(queuePath(), "utf8"));
} catch (error) {
return [];
}
}
function queueReport(body) {
const items = readQueue();
if (items.length >= 100) return;
items.push(body);
fs.writeFileSync(queuePath(), JSON.stringify(items));
}
async function flushQueue() {
const remaining = [];
for (const body of readQueue()) {
try {
const status = await postReport(body);
if (status === 429 || status >= 500) remaining.push(body);
} catch (error) {
remaining.push(body);
}
}
fs.writeFileSync(queuePath(), JSON.stringify(remaining));
}
app.whenReady().then(() => {
flushQueue();
setInterval(flushQueue, 60000);
});
The cap of 100 items stops a broken network from filling the disk. Reports that get a 400 or 404 are dropped on purpose, because retrying them cannot help. Reports that fail with 429 or a server error stay in the queue for the next attempt.
Should the renderer load the widget?
The widget is a script from the Escuta Produto domain. It can load in the window when your app loads its interface from the web, and it gives users the familiar floating button. Three checks matter. The window's content security policy must allow the script source and the API address. Packaged builds often load from a file or custom protocol address, which can behave differently from a development server, so test the packaged app. And a widget open in the window cannot send while the window is blocked, so keep the preload path for critical reports.
For most desktop apps, a menu item or a button that opens a small form and calls window.feedback.send is simpler and easier to test.
Tauri apps use the same pattern
In Tauri, the window's JavaScript cannot make cross-origin requests the way a normal page can, so send the report through the HTTP plugin. Grant the plugin access to the Escuta Produto address in your capability file, then use its fetch from the front end:
import { fetch } from "@tauri-apps/plugin-http";
import { getVersion } from "@tauri-apps/api/app";
import { platform, version } from "@tauri-apps/plugin-os";
export async function sendFeedback(message, kind) {
const response = await fetch("https://escutaproduto.com/api/v1/feedback", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
key: "pk_your_product_key",
kind: kind,
message: message,
metadata: { appVersion: await getVersion(), platform: platform(), osRelease: version() },
}),
});
return response.status === 201;
}
Keep the queue logic from the Electron example in the Rust or front-end layer, whichever holds the storage you choose. The rule is the same in both frameworks: keep the report until the API has accepted it.
Putting Escuta Produto behind your desktop app
Create a product for the app and copy its public key into the main process or the front end. Send one report from a packaged build, check that the app version and platform appear in the inbox, then disconnect from the network and send another. Reconnect and confirm that the queued report arrives. Connect the Slack or Discord webhook so new reports reach the team.
The notifications guide explains the webhook. For the review routine, the triage guide shows how to sort reports, and the mobile app article covers the same REST API pattern for apps that run on phones.
Frequently asked questions
Should an Electron app send feedback from the renderer or the main process?
Prefer the main process. It can make network requests without the window's origin rules getting in the way, and it can keep reports in a queue on disk. Expose one send method to the renderer through the preload script.
What happens to feedback written while the app is offline?
A network error means the report is not sent, so the app saves it to a local queue file. The app tries again at startup and then every minute. Reports that the API refuses with a client error are dropped, because retrying them cannot succeed.
Which metadata should a desktop app send with feedback?
Send the app version, the operating system platform and release, and the processor architecture. Leave out file paths and document titles, because they can reveal private names or work.
Related
- How to collect feedback in a SaaS productCollect in-app feedback from SaaS customers: load the widget once, identify users after login, add plan metadata and place entry points where work happens.
- How to collect feedback in a mobile appCollect feedback from iOS, Android or React Native with one REST API request, app version metadata and a hosted form link in your store listing.
- How to collect feedback for a Chrome extensionCollect Chrome extension feedback with a REST API call from the popup, a hosted form as the uninstall URL and the extension version on every report.
- How to collect feedback for a WordPress pluginCollect feedback for a WordPress plugin with a settings page link to a hosted form, server-side API posts with site details, and no remote script in wp-admin.