Notifio is two things: a Next.js site that sells a licence, and an Electron app on your own machine that watches rental search pages and emails you when something new appears. The site has error reporting. The app has none, on purpose, and the gap between those two decisions is the whole of this post.
The website, three times over
// sentry.server.config.ts
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.SENTRY_DSN,
tracesSampleRate: 0.1,
});
// sentry.edge.config.ts
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.SENTRY_DSN,
tracesSampleRate: 0.1,
});
// instrumentation-client.ts
import * as Sentry from "@sentry/nextjs";
// Browser Sentry init. Next.js loads this natively (no build plugin required).
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
tracesSampleRate: 0.1,
});
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;
Two of those three files are byte-for-byte identical, and people reliably want to deduplicate them into a shared initSentry() helper. I have left them alone, because they are three different runtimes with three different lifecycles, and the first time one of them needs a setting the others must not have, the shared helper grows a parameter and stops being shorter than the duplication it replaced.
The one real difference is the DSN variable. Server and edge read SENTRY_DSN, which stays on the server. The browser reads NEXT_PUBLIC_SENTRY_DSN, which is compiled into the client bundle and visible to anybody who looks. That is normal for Sentry, since a DSN is a write-only ingest key, but it is the reason the names differ: if the browser read SENTRY_DSN, it would be undefined at runtime and client errors would silently go nowhere. A reporting tool that fails silently is worse than not having one, because you stop looking.
It is running, which you can check from a console on any page of the site:
> window.__SENTRY__.version
"10.67.0"
The file that makes server errors actually arrive
import * as Sentry from "@sentry/nextjs";
// Next.js calls register() once at server startup. Load the Sentry config for
// whichever runtime we're in.
export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
await import("./sentry.server.config");
}
if (process.env.NEXT_RUNTIME === "edge") {
await import("./sentry.edge.config");
}
}
// Next.js forwards uncaught server/route errors here so Sentry can capture them.
export const onRequestError = Sentry.captureRequestError;
The dynamic imports are load-bearing. A static import at the top of this file would run both configs in both runtimes, and the edge runtime does not have everything the Node SDK expects.
onRequestError is the line that is easy to leave out and impossible to notice leaving out. Without it, Sentry.init runs happily on the server, the SDK is loaded, nothing errors, and uncaught route handler errors are never reported. You would conclude your API routes never throw, which is a conclusion nobody should reach about their own code.
The build plugin we did not add
Our next.config.ts has no withSentryConfig wrapper. It has no Sentry anything:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
// (one comment about raw bodies for the Stripe webhook route, and nothing else)
};
export default nextConfig;
Skipping the plugin costs us source map upload, which means a browser stack trace arrives pointing at minified chunk names rather than our files. For server and route errors, where most of what can actually break on this site lives, the traces are readable without it.
What it buys is that next build is plain next build. No auth token in CI, no upload step that can fail a deploy for a reason unrelated to the code, no wrapped config to reason about when a build behaves oddly. For a site whose client-side JavaScript is a currency context, a FAQ accordion and a contact form, a minified client trace plus the knowledge that something threw is enough to go and look.
Ten percent tracing is the same kind of judgement. Performance data on a site with this much traffic is a sample of a sample, and the errors are the part we actually act on.
The desktop app reports nothing, and that is the feature
The app's entire runtime dependency list is six packages, and no error reporter is among them. There is no Sentry, no Crashpad upload, no telemetry endpoint, no "help us improve by sending anonymous usage data" checkbox.
This is not laziness, it is the privacy claim the product is sold on. Our privacy policy lists what the app keeps on your machine and never transmits: the search URLs you add, the browser sessions for sites you log into, snapshots used to detect new listings, your activity log, your reply profile. It then says the only thing the app sends us is your email address and licence token.
An error reporter breaks every line of that. A stack trace carries file paths, which carry a macOS or Windows username. A breadcrumb trail carries the URL that was being processed, which on this product is a rental search with somebody's budget and preferred neighbourhood in the query string. A captured exception from a Chromium page carries whatever the page was. The crash report that would be most useful to me is exactly the one I have promised not to receive, and "we only send anonymised diagnostics" is a sentence that requires the user to trust a claim they cannot check.
For contrast, the website's Sentry usage is disclosed in that same policy, in plain terms: Sentry receives technical diagnostic data which can include your IP address, browser and page URL, when a page throws or is in the traced sample. A marketing page has nothing of yours on it. A monitor that knows what you are looking for does.
So what replaces it
Three local channels, in order of how often they are used:
- An activity log in the window, capped at 200 lines, which is what somebody reads when they want to know why nothing has arrived yet.
-
A log file in the app's data directory, appended to by a logger that also installs
process.on("uncaughtException")andprocess.on("unhandledRejection")handlers. It is on their disk, so they decide whether we see it. - A startup failure window, because a packaged Electron app has no console to print to. If the local server cannot start, the app builds an error window out of a data URL, which I wrote up in a packaged Electron app has no console.
All three end in the same place: a person deciding to tell us. Which means the contact form on the help page is our crash reporter, and the slowest possible one, and the only one compatible with the thing we promised.
It also changes what the app has to do when something goes wrong. A reported crash is an invitation to fail loudly and fix it later. An unreported one has to degrade into something a user can act on by themselves, which is why a missing desktop notification never fails a check, a licence server that does not answer leaves yesterday's answer in place, and every one of those decisions ends up in a log line somebody can read instead of a ticket I never see.
If you want to see the quiet half of this, the app is a one-time licence and the download page has builds for both platforms. Nothing in it will ever phone home about a crash.
Top comments (0)