Notifio is a desktop app that sends no telemetry. No analytics, no crash reporting, no error service. The website is instrumented three times over, and I wrote about that gap deliberately in Three Sentry inits of four lines each on the website, and deliberately none in the desktop app.
That decision has a consequence I had not examined closely until this week: when something goes wrong on a user's machine, the only artefact that exists is a text file. So I went and audited the text file, and it turns out it is missing most of what I assumed was in it.
The logger
All of it:
const LOG_PATH = path.join(app.getPath('userData'), 'logs.txt'); function logFile(msg: string): void { const line = `[${new Date().toISOString()}] ${msg}\n`; process.stdout.write(line); try { fs.appendFileSync(LOG_PATH, line); } catch { /* ignore */ } } An ISO timestamp, the message, a newline. Append synchronously, and swallow any error, because a logger that can throw is a logger that turns a recoverable problem into a crash. appendFileSync rather than a stream so that a line written immediately before a hard exit is on disk rather than in a buffer, which is the entire reason the file exists.
Above it, two process level handlers:
process.on('uncaughtException', (err: Error) => { logFile(`UNCAUGHT: ${err.stack || err.message}`); }); process.on('unhandledRejection', (reason: unknown) => { logFile(`UNHANDLED REJECTION: ${String(reason)}`); }); Note what is absent from both: process.exit(). Installing an uncaughtException handler suppresses Node's default behaviour of printing the error and terminating, so this app records the exception and keeps going.
That is a choice people will argue with, and the argument against it is correct in general. After an uncaught exception the process is in an unknown state: a function somewhere stopped half way through, and whatever invariant it was in the middle of establishing may now be false. The textbook advice is to log and die.
The reason I made the other choice is what this app is. It is a poll loop that watches rental searches for you and is only valuable while it is running. The realistic uncaught exception here is one search's turn in the cycle going wrong. Terminating means the user's laptop is sitting there not monitoring anything, silently, until they next look at the window, and the whole product promise is that they do not have to look at the window. A wrong state in one search's turn is recoverable at the start of the next turn, because the poll loop re-reads from disk each time around rather than trusting anything it held in memory. Crashing is not recoverable, because nothing restarts the app.
The audit, and what it found
That is the design. Here is what is actually in the file:
$ grep -c "logFile(" src/main.ts 18 Eighteen call sites, every one of them in main.ts. They cover app ready, server start, the startup error path, window and tray creation, suspend and resume, and the login window's session handling. All of it process lifecycle.
Now the other half:
$ grep -c "console\." src/*.ts src/entitlement.ts:1 src/finds.ts:1 src/ledger.ts:3 src/main.ts:1 src/monitor.ts:2 src/recipes.ts:2 src/scraper.ts:7 src/server.ts:4 src/settings.ts:1 Twenty two calls across nine modules, and not one of them goes through logFile. They go to console.error and console.log, which means stdout and stderr, which in a packaged Electron app launched from Finder or the Start menu means nowhere at all.
So the modules that do the actual work report nothing to disk. settings.ts logs when a store fails to read. ledger.ts logs three different kinds of problem. finds.ts logs when the history file cannot be parsed. Those are precisely the lines you would want when a user says the app forgot something, and in a packaged build they are written to a file descriptor nobody is reading.
And the crash window names the file:
mainWindow.loadURL( `data:text/html,<pre ...>Notifio failed to start.<br><br>${msg}<br><br>Check logs: ${LOG_PATH}</pre>` ); That window is a good mechanism and I still like it, which is why it has a post of its own. For a startup failure it works, because startup failures happen in main.ts where the eighteen calls are. For anything later it is pointing a user at a file that has one line per app launch and nothing about the thing they are reporting.
The fix is one line, which is why this is embarrassing
logFile lives in main.ts and everything else would have to import it, which in this app means importing a module that pulls in Electron, and testable files may not import electron. That rule is why the logger ended up where it did and why nothing else uses it.
The shape that respects the rule is a log.ts with no Electron import, taking the destination path from a setter that main.ts calls once during the initialisation sequence the app already has. Nine modules change console.error to log.error. The logger stays dull and the test rule stays intact.
The other thing I did not find
There is no rotation. No size check, no truncation, no second file. The renderer's in-app activity log is capped at 200 lines, and that cap was a deliberate decision with consequences I wrote about in Our activity log caps at 200 lines, and the cap makes every React key wrong on purpose. The file on disk has no equivalent.
Today that is harmless, because eighteen lifecycle call sites produce a handful of lines per launch. It stops being harmless the moment the fix above lands, because then every poll cycle can contribute, and a monitor left running for a month on a machine the user never thinks about is exactly the situation where an unbounded append-only file is a bad idea. So the rotation has to be part of the same change rather than a follow up, which is the useful thing this audit told me.
Why no telemetry remains right
Nothing here makes me want a crash reporter. The app holds your rental searches, the sites you have logged into, and a reply history. Section 2 of the privacy policy says which of that never leaves your machine, and a stack trace uploaded automatically would quietly make part of that untrue, because stack traces carry paths and paths carry usernames.
A local text file the user can read, delete, or paste into the contact form is a worse debugging experience for me and the correct one for them. It just has to actually contain the logs. The app itself is on the download page, and on macOS the file this post is about is at ~/Library/Application Support/Notifio/logs.txt.
![Why Object.keys(user) Returns string[], Not (keyof User)[]: TypeScript Means It](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2q4w48b9bzs43tynyhsf.png)
