Stop Debugging From User Screenshots: Production Error Tracking for Vibe Projects
After launch, errors happen where you are not watching. This guide builds production error tracking for a vibe project: capture, scrubbing, fingerprinting, source maps, sampling, alerting, release health, and a solo dev's daily triage.


One thing first: the example product and person in this guide are fictional. The to-do app "Tomorrow List" and its solo maker Chen Mo do not exist; they are props for explaining a method. The method itself is real, the story is invented, and none of the numbers have any real source.
The most common illusion on launch day is believing that errors will come and find you. During development you sit at your computer with the console open, and every error is visible the moment it happens. After launch you are not in the room. Errors happen on someone else's phone, on someone else's network, down someone else's path through the product. The only signal you usually receive is a screenshot, a vague "it won't open," or nothing at all — the user quietly uninstalls without even bothering to complain.
Debugging from user screenshots has three hard flaws. First, a screenshot has no context: you do not know what the user tapped, which page they were on, or which version they were running. Second, a screenshot cannot be reproduced: you walk through the described steps and everything works, because the trigger may have been an old data record, a timezone edge case, or a momentary network interruption. Third, a screenshot arrives too late: by the time the user takes it, it is often already their third failure, and the first two errors have vanished into thin air. By the time the screenshot reaches you, the valuable scene has already been washed away by time.
Console logging has the same problem. In development it is a microscope; after launch it is a security camera with no recording function — you can only see what happens while you happen to be watching. Worse, many projects keep plenty of logs after launch that nobody ever reads. Logs written into a corner nobody looks at are the same as no logs at all. Production error tracking solves one fundamental problem: the second an error occurs, the system must automatically preserve the scene, deliver it somewhere you will actually see it, and organize it into a form you can act on.
1. Build the Capture Layer First: Save the Scene Automatically When an Error Happens
The capture layer is the foundation of the whole method. Its job is not to "record all logs" but to "take a snapshot at the instant an error occurs that is sufficient to locate the problem without crossing a line." It must cover at least three kinds of errors: uncaught exceptions, unhandled promise rejections, and critical business failures you report explicitly. Catching only the first kind is not enough — many production problems show up as a 500 from an API, a payment callback timing out, or data written halfway. These do not surface as exceptions, so they must be reported explicitly in business code.
A good error snapshot contains seven things: the error message, the stack trace, the release version, the current route or page, a user identifier (an internal ID only — never a name or phone number), the timestamp, and a short trail of actions before the error, known as breadcrumbs. Breadcrumbs are the most easily underestimated part: which button the user tapped, which API was called, how many times they switched pages. That trail often tells you "how they got here" better than the stack trace itself. Without breadcrumbs, even a perfect stack trace leaves you guessing.
But capturing more does not mean capturing correctly. Some things should never enter an error-tracking system: passwords, tokens, secrets, full request bodies, chat content, national ID numbers, bank card numbers. Once such data enters a third-party error platform, you have added a new leak surface, and you have often broken the promises in your own privacy policy. The correct approach is scrubbing before reporting: filter by a blocklist of field names and replace matches with placeholders, truncate long text, and by default redact any field you are unsure about. Seeing a little less is far better than seeing one field too many. Here is a sketch of a scrub function:
const SCRUB_KEYS = ["password", "token", "secret", "apiKey",
"authorization", "cardNumber", "idCard"];
function scrub(obj) {
if (!obj || typeof obj !== "object") return obj;
const out = {};
for (const [key, value] of Object.entries(obj)) {
if (SCRUB_KEYS.includes(key.toLowerCase())) {
out[key] = "[REDACTED]";
} else if (typeof value === "object") {
out[key] = scrub(value);
} else if (typeof value === "string" && value.length > 500) {
out[key] = value.slice(0, 500) + "…[truncated]";
} else {
out[key] = value;
}
}
return out;
}
The reporter itself also needs self-protection. Reporting an error must never throw a new error because reporting failed, must never block the main flow because the network jitters, and must never retry endlessly on a user's phone. The right pattern is an asynchronous queue: if reporting fails, drop the event and increment a local counter, and never pop up a dialog to bother the user. An error-reporting system that becomes a new bug itself is worse than none at all.
When initializing the reporter, lock in three things at once: the current release version, the environment, and the sampling switch. The version is the anchor for all later grouping, comparison, and rollback decisions — without it, you cannot even say whether an error was introduced by the new release or is an old problem. A sketch:
initErrorReporter({
release: "1.4.2", // must change with every release
environment: "production",
beforeSend: (event) => scrub(event),
sampleRate: 1.0, // fatal errors stay at 100%, see Section 5
});
2. Grouping and Fingerprinting: 100,000 Identical Errors Are Really One Bug
In its first week after launch, Chen Mo's fictional "Tomorrow List" collected tens of thousands of error reports. The dashboard was dense, and his first reaction was that the project was collapsing. When he opened the reports, he found that ninety percent came from a single null-pointer error: an old data record was missing a field, and the list page crashed every time it rendered. One hundred thousand reports, one fix, all gone — provided the system had grouped them together.
That is the point of grouping. The first unit on an error dashboard should not be "one error report" but "one kind of error." Grouping relies on fingerprints: an identifier distilled from the error type, the file and line number, and the most stable frames of the call stack. Fingerprint rules should be conservative: take the stable frames at the top of the stack and ignore things that change every time, such as memory addresses, timestamps, and user IDs.
Two grouping bans belong on the wall. First, never group by timestamp or by user — that turns every user and every occurrence into its own entry, and grouping stops working. Second, never group by the full text of the error message — messages often carry dynamic content, such as a product ID or a piece of user input, so one bug shatters into hundreds of groups as soon as the text changes. The good practice is to group by error type plus stable stack frames first, then normalize the dynamic parts: numbers, IDs, and path parameters all become placeholders.
Once grouping works, the dashboard starts speaking plain language: which kind of error affects the most people, which kind appeared for the first time today, which kind suddenly spiked after a release. When a solo developer opens the dashboard each day, they should see a dozen or so to-dos, not tens of thousands of raw log lines. Volume itself is not information; organized volume is.
3. Source Maps: Without Them, a Stack Trace Is Gibberish
Frontend code shipped to production is usually minified, so a production stack trace points to a line and column in the minified bundle that reads like gibberish. Source maps are the map that translates minified code back to the source positions. Without them, you know "something crashed somewhere" but not which function or which line; with them, the stack trace points directly at the source code you actually wrote.
The key is "upload per release." Every build's source maps must be uploaded to the error platform and matched strictly to that release version. A mismatched version means a wrong map, which is worse than none at all. Many teams fail not because they never configured source maps, but because they configured them once and never maintained them — new releases keep being decoded with the old map, and debugging drifts further off course with every release.
There is a public-versus-private trade-off here. Publishing source maps on a public CDN lets anyone download them and reconstruct your source code — harmless for an open-source project, but for a commercial project it lays your implementation open for inspection. The safer approach is to upload source maps only to the error platform's private storage, not alongside the public frontend bundle. The cost is one extra upload step; the benefit is that your source stays private. There is no universal answer, but it must be a conscious choice, not a default that leaves the map sitting at a public URL unnoticed.
One more trap: when a source-map upload fails, the build pipeline should alarm rather than pass silently. A release shipped with missing maps turns every error into gibberish, and you may not discover it until the first time you actually need to debug. Put map upload on the release checklist, and do not ship a release whose maps are missing.
4. Sampling and Quotas: Stop the Error Storm Before It Hits Your Bill
The most expensive lessons usually come from error storms. An error inside a loop firing dozens of times per second, or an API failing on every request during an outage, can burn through a usage-based quota overnight. The error system itself must have rate limits: throttle reports for the same fingerprint, for example a fixed maximum per minute; apply a global rate limit to the reporter; and deduplicate errors triggered in loops.
Sampling must not be one-size-fits-all. A sensible policy is tiered: fatal errors and payment-related errors are always reported at 100% — not one may be dropped; high-frequency but harmless known errors can be sampled, say one percent; purely cosmetic glitches can be at full volume only for the first few days of a new release, then sampled down once stable. Tiering only works if you tag severity at report time, not by hand-picking reports afterward.
The error system also needs a budget ceiling: a monthly quota, a daily cap, and a degradation policy for when the cap is hit. When the cap is hit, the right response is not to stop all reporting but to drop automatically to fatal errors only. The worst policy is unlimited full-volume reporting — the bill arrives before the fix, and the money for fixing the bug has already been spent by the error system.
A practical habit follows from this: treat "a sudden surge in error volume" itself as a signal worth alerting on. Normal error volume has a baseline, and a doubling after a release often tells you something is wrong earlier than any single report does. The next section builds alerting on exactly this idea.
5. Alerting: Alert on New Error Types and Rate Spikes, Not on Every Occurrence
Notifying on every single error is the fastest way to train yourself to ignore notifications. After a phone buzzes hundreds of times in one night, a developer will instinctively mute it — and then the genuinely serious error gets muted along with the noise. The principle of alerting is: only interrupt a human when a human judgment is needed right now.
Two kinds of alert are worth an immediate interruption. The first is a new error type: a fingerprint never seen before appearing for the first time, especially within the first hour after a release. Its value is earliness — it moves you from "learning about it from user feedback" to "knowing within an hour of shipping." The second is a rate spike: an existing error's per-unit-time count rising several times above baseline, or its affected user count suddenly expanding. Its value is change — it tells you an old problem is getting worse, or an external dependency is failing.
An alert must carry enough information for a decision: the error type, the number of affected users, when it first appeared, the associated release, and a link straight to the dashboard. A notification that only says "an error occurred" is no notification at all — the recipient still has to dig through the dashboard for context, and the golden window is spent searching. Conversely, an alert does not need the full stack trace pasted in; the link takes you there.
Alert channels should be tiered. Fatal errors deserve a phone-call or SMS-level interruption; new error types go to instant messaging; rate spikes below fatal level can start as a daily digest. Tiering is not bureaucracy; it protects your attention. Attention is a solo developer's scarcest resource, and the alert system's job is to help allocate it, not to exhaust it.
6. Release Health: Judge a Release by Its Error Rate, and Know When to Roll Back
With per-release grouped data, you can judge release health: compare each release's error rate — not the absolute number of errors, but the number per thousand sessions or per ten thousand requests. A new release has fewer users, so its absolute count is naturally lower; comparing raw counts produces the false conclusion that "the new release is healthy." Comparing rates shows the real change.
The right posture after shipping is: check after one hour, then check after one day. Within the hour, look for new error types and fatal errors; within the day, look at whether the overall error rate rose or fell versus the previous release, and whether the affected surface expanded. Set a rollback trigger line before shipping, and write it down: for example, if the fatal error rate runs several times above the previous release, or the payment success rate drops below a threshold, roll back — no meeting, no hesitation, no "let's watch a little longer."
The trigger line must be written in advance because that is exactly when people are best at talking themselves out of it. "It might just be statistical noise" and "let's wait until traffic picks up" both sound very reasonable at three in the morning. A line written in advance exists to make the decision for you at that moment. It does not need to be perfectly precise; what matters is that it exists, and that after you have used it once, you tune it from experience.
Release health has a side benefit: it turns "did this release get better" from a feeling into data. The next time someone asks whether the last refactor actually helped, you do not need to recall — you put the two releases' error rates side by side. Data will not make the decision for you, but it stops the decision from being made by whoever shouts loudest.
7. A Solo Developer's Daily Triage: Ten Minutes, Four Moves
No matter how complete the method is, it is worthless if it never becomes a daily rhythm. For a solo developer, I recommend a fixed ten minutes of triage every day, four moves, no more. First, look at new errors: any new error type today gets read first, regardless of how many people it affects — new errors are tomorrow's big problems, and the earlier you handle them, the cheaper they are. Second, look at spikes: which old error grew the most, and prioritize the biggest growth. Third, order fixes by user loss: payment problems first, login problems second, core flows third, cosmetic issues last. This order is not by technical difficulty but by user loss: users who cannot pay leave immediately, while users facing a misaligned layout can still cope.
Fourth, maintain a "known and ignored" list. Some errors you cannot fix yet, some are third-party services jittering, some exist only in old versions whose users are naturally declining. Mark these as known, write down the reason and a review date, and let the date automatically bring them back for reconsideration. An ignore without an expiry date is a permanent ignore, and a permanent ignore will eventually bite you. The point of the list is not to pretend problems do not exist, but to consciously decide "this problem will not be handled this month" and to remake that decision when the date arrives.
During triage, resist one impulse: do not start with the most interesting bug. The most interesting bug is usually not the most urgent one. Fix in order of loss, finish one, then look at the next. A solo developer's schedule has no room for "fixing this on the side" — every distraction steals time from core features.
8. Pre-Launch Checklist: Tick These Boxes on Release Day
Finally, here is a pre-launch checklist you can copy directly. First, the error reporter is initialized, its version matches this release, and the environment is marked production, not testing. Second, scrubbing rules are on, the sensitive-field blocklist covers passwords, tokens, and secrets, and full request bodies are never reported. Third, this build's source maps are uploaded and matched to the version, and a failed upload raises an alarm. Fourth, sampling and rate limits are configured: fatal and payment errors at full volume, high-frequency errors throttled, a quota ceiling and an over-quota degradation policy set. Fifth, alert rules are on: new error types and rate spikes notify you, and each notification carries the version and a dashboard link. Sixth, the rollback trigger line is written down, so even a team of one knows which line, once crossed, means roll back. Seventh, triage is on the calendar: ten minutes a day, new errors and spikes first, then fixes in payment, login, core flow, cosmetic order.
Each item on this list maps to a pitfall from an earlier section. None of them looks impressive alone; together they are the whole of "no longer living off screenshots after launch." Production error tracking is not a tool you buy and forget; it is a set of habits: preserve the scene at capture time, remove noise at grouping time, align the map at release time, cap the bill at quota time, protect attention at alert time, and read release health at review time. Tools can change; the habits cannot be skipped.
Back to Chen Mo's fictional "Tomorrow List" — an invented story, but its two possible endings are ones every real project may face: either guessing from user screenshots, or fixing bugs in order from your own dashboard. The difference is not project size. It is whether, on launch day, you gave errors a way home.
Comments (0)
Related articles

Leaked passwords are routine, and a password alone cannot guard an account that can delete data, spend money, or see other people's data. This guide covers TOTP enrolment, verification, and recovery: verify before enabling, one-time backup codes, a 5-attempt lockout, and revocable trusted devices.

Maintenance mode is not a sign on a page; it is a mechanism that keeps user data intact while you migrate. This guide covers three levels, the four-part backend switch, easily missed chains, and three-phase communication.

Magic Link is the best-value passwordless option for a solo team, but login security then depends on mailbox security and email deliverability. This guide covers seven link-security rules, delivery testing, cross-device handling, and fallbacks.